Skip to content

Commit 60000a3

Browse files
authored
Merge pull request #541 from rustaceanrob/26-2-28-tor-onion
Connect to Tor hidden services
2 parents 3a3a19b + 0d8efcb commit 60000a3

File tree

6 files changed

+169
-29
lines changed

6 files changed

+169
-29
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ bitcoin = { version = "0.32.7", default-features = false, features = [
1919
bip324 = { version = "0.7.0", default-features = false, features = [
2020
"tokio",
2121
] }
22+
hashes = { package = "bitcoin_hashes", version = "0.20.0" }
2223
tokio = { version = "1.19", default-features = false, features = [
2324
"rt-multi-thread",
2425
"sync",

examples/bitcoin.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ async fn main() {
3232
))
3333
// Add some initial peers
3434
.add_peers(seeds.into_iter().map(From::from))
35+
// Connections over Tor are supported by Socks5 proxy
36+
// .socks5_proxy(bip157::Socks5Proxy::local())
3537
// Create the node and client
3638
.build();
3739
// Run the node on a separate task

src/builder.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
use std::net::SocketAddr;
21
use std::{path::PathBuf, time::Duration};
32

43
use bitcoin::Network;
54

65
use super::{client::Client, node::Node};
76
use crate::chain::ChainState;
87
use crate::network::ConnectionType;
9-
use crate::TrustedPeer;
108
use crate::{Config, FilterType};
9+
use crate::{Socks5Proxy, TrustedPeer};
1110

1211
const MIN_PEERS: u8 = 1;
1312
const MAX_PEERS: u8 = 15;
@@ -125,7 +124,7 @@ impl Builder {
125124

126125
/// Route network traffic through a Tor daemon using a Socks5 proxy. Currently, proxies
127126
/// must be reachable by IP address.
128-
pub fn socks5_proxy(mut self, proxy: impl Into<SocketAddr>) -> Self {
127+
pub fn socks5_proxy(mut self, proxy: impl Into<Socks5Proxy>) -> Self {
129128
let ip_addr = proxy.into();
130129
let connection = ConnectionType::Socks5Proxy(ip_addr);
131130
self.config.connection_type = connection;

src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub mod node;
6262

6363
use chain::Filter;
6464

65-
use std::net::{IpAddr, SocketAddr};
65+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
6666
use std::path::PathBuf;
6767

6868
// Re-exports
@@ -294,6 +294,26 @@ impl From<SocketAddr> for TrustedPeer {
294294
}
295295
}
296296

297+
/// Route network traffic through a Socks5 proxy, typically used by a Tor daemon.
298+
#[derive(Debug, Clone)]
299+
pub struct Socks5Proxy(SocketAddr);
300+
301+
impl Socks5Proxy {
302+
/// Connect to the default local Socks5 proxy hosted at `127.0.0.1:9050`.
303+
pub const fn local() -> Self {
304+
Socks5Proxy(SocketAddr::new(
305+
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
306+
9050,
307+
))
308+
}
309+
}
310+
311+
impl From<SocketAddr> for Socks5Proxy {
312+
fn from(value: SocketAddr) -> Self {
313+
Self(value)
314+
}
315+
}
316+
297317
#[derive(Debug, Clone, Copy)]
298318
enum NodeState {
299319
// We are behind on block headers according to our peers.

src/network/mod.rs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{
22
collections::{HashMap, HashSet},
33
fs::{self, File},
4-
net::{IpAddr, SocketAddr},
4+
net::IpAddr,
55
path::PathBuf,
66
time::Duration,
77
};
@@ -23,11 +23,13 @@ use bitcoin::{
2323
},
2424
Block, BlockHash, FeeRate, Wtxid,
2525
};
26-
use socks::create_socks5;
26+
use socks::{create_socks5, SocksConnection};
2727
use tokio::{net::TcpStream, time::Instant};
2828

2929
use error::PeerError;
3030

31+
use crate::Socks5Proxy;
32+
3133
pub(crate) mod dns;
3234
pub(crate) mod error;
3335
pub(crate) mod inbound;
@@ -130,18 +132,20 @@ impl LastBlockMonitor {
130132
}
131133
}
132134

133-
#[derive(Debug, Clone, Copy, Default)]
135+
#[derive(Debug, Clone, Default)]
134136
pub(crate) enum ConnectionType {
135137
#[default]
136138
ClearNet,
137-
Socks5Proxy(SocketAddr),
139+
Socks5Proxy(Socks5Proxy),
138140
}
139141

140142
impl ConnectionType {
141143
pub(crate) fn can_connect(&self, addr: &AddrV2) -> bool {
142144
match &self {
143145
Self::ClearNet => matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_)),
144-
Self::Socks5Proxy(_) => matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_)),
146+
Self::Socks5Proxy(_) => {
147+
matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_) | AddrV2::TorV3(_))
148+
}
145149
}
146150
}
147151

@@ -151,13 +155,13 @@ impl ConnectionType {
151155
port: u16,
152156
handshake_timeout: Duration,
153157
) -> Result<TcpStream, PeerError> {
154-
let socket_addr = match addr {
155-
AddrV2::Ipv4(ip) => IpAddr::V4(ip),
156-
AddrV2::Ipv6(ip) => IpAddr::V6(ip),
157-
_ => return Err(PeerError::UnreachableSocketAddr),
158-
};
159158
match &self {
160159
Self::ClearNet => {
160+
let socket_addr = match addr {
161+
AddrV2::Ipv4(ip) => IpAddr::V4(ip),
162+
AddrV2::Ipv6(ip) => IpAddr::V6(ip),
163+
_ => return Err(PeerError::UnreachableSocketAddr),
164+
};
161165
let timeout = tokio::time::timeout(
162166
handshake_timeout,
163167
TcpStream::connect((socket_addr, port)),
@@ -168,12 +172,16 @@ impl ConnectionType {
168172
Ok(tcp_stream)
169173
}
170174
Self::Socks5Proxy(proxy) => {
171-
let socks5_timeout = tokio::time::timeout(
172-
handshake_timeout,
173-
create_socks5(*proxy, socket_addr, port),
174-
)
175-
.await
176-
.map_err(|_| PeerError::ConnectionFailed)?;
175+
let addr = match addr {
176+
AddrV2::Ipv4(ipv4) => SocksConnection::ClearNet(IpAddr::V4(ipv4)),
177+
AddrV2::Ipv6(ipv6) => SocksConnection::ClearNet(IpAddr::V6(ipv6)),
178+
AddrV2::TorV3(onion) => SocksConnection::OnionService(onion),
179+
_ => return Err(PeerError::UnreachableSocketAddr),
180+
};
181+
let socks5_timeout =
182+
tokio::time::timeout(handshake_timeout, create_socks5(proxy.0, addr, port))
183+
.await
184+
.map_err(|_| PeerError::ConnectionFailed)?;
177185
let tcp_stream = socks5_timeout.map_err(PeerError::Socks5)?;
178186
Ok(tcp_stream)
179187
}

src/network/socks.rs

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
time::Duration,
77
};
88

9+
use hashes::sha3_256;
910
use tokio::{
1011
io::{AsyncReadExt, AsyncWriteExt},
1112
net::TcpStream,
@@ -21,27 +22,114 @@ const CMD_CONNECT: u8 = 1;
2122
const RESPONSE_SUCCESS: u8 = 0;
2223
const RSV: u8 = 0;
2324
const ADDR_TYPE_IPV4: u8 = 1;
25+
const ADDR_TYPE_DOMAIN: u8 = 3;
2426
const ADDR_TYPE_IPV6: u8 = 4;
27+
// Tor constants
28+
const SALT: &[u8] = b".onion checksum";
29+
const TOR_VERSION: u8 = 0x03;
30+
const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
31+
32+
fn pubkey_to_service(ed25519: [u8; 32]) -> String {
33+
let mut cs_input = Vec::with_capacity(48);
34+
// SHA3(".onion checksum" + public key + version)
35+
cs_input.extend_from_slice(SALT);
36+
cs_input.extend_from_slice(&ed25519);
37+
cs_input.push(TOR_VERSION);
38+
let cs = sha3_256::hash(&cs_input).to_byte_array();
39+
// Onion address = public key + 2 byte checksum + version
40+
let mut input_buf = [0u8; 35];
41+
input_buf[..32].copy_from_slice(&ed25519);
42+
input_buf[32] = cs[0];
43+
input_buf[33] = cs[1];
44+
input_buf[34] = TOR_VERSION;
45+
let mut encoding = base32_encode(&input_buf);
46+
debug_assert!(encoding.len() == 56);
47+
encoding.push_str(".onion");
48+
encoding
49+
}
50+
51+
fn base32_encode(data: &[u8]) -> String {
52+
let mut result = String::with_capacity((data.len() * 8).div_ceil(5));
53+
let mut buffer: u64 = 0;
54+
let mut bits_left: u32 = 0;
55+
for &byte in data {
56+
buffer = (buffer << 8) | byte as u64;
57+
bits_left += 8;
58+
while bits_left >= 5 {
59+
bits_left -= 5;
60+
let index = ((buffer >> bits_left) & 0x1f) as usize;
61+
result.push(ALPHABET[index] as char);
62+
}
63+
// Keep only the unconsumed bits
64+
buffer &= (1u64 << bits_left) - 1;
65+
}
66+
if bits_left > 0 {
67+
let index = ((buffer << (5 - bits_left)) & 0x1f) as usize;
68+
result.push(ALPHABET[index] as char);
69+
}
70+
result
71+
}
72+
73+
#[derive(Debug)]
74+
pub(crate) enum SocksConnection {
75+
ClearNet(IpAddr),
76+
OnionService([u8; 32]),
77+
}
78+
79+
impl SocksConnection {
80+
fn encode(&self) -> Vec<u8> {
81+
match self {
82+
Self::ClearNet(net) => match net {
83+
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
84+
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
85+
},
86+
Self::OnionService(onion) => {
87+
let service = pubkey_to_service(*onion);
88+
let enc = service.as_bytes();
89+
let mut buf = Vec::with_capacity(enc.len() + 1);
90+
buf.push(enc.len() as u8);
91+
buf.extend_from_slice(enc);
92+
buf
93+
}
94+
}
95+
}
96+
97+
fn type_byte(&self) -> u8 {
98+
match self {
99+
Self::ClearNet(net) => match net {
100+
IpAddr::V4(_) => ADDR_TYPE_IPV4,
101+
IpAddr::V6(_) => ADDR_TYPE_IPV6,
102+
},
103+
Self::OnionService(_) => ADDR_TYPE_DOMAIN,
104+
}
105+
}
106+
}
107+
108+
impl From<IpAddr> for SocksConnection {
109+
fn from(value: IpAddr) -> Self {
110+
Self::ClearNet(value)
111+
}
112+
}
113+
114+
impl From<[u8; 32]> for SocksConnection {
115+
fn from(value: [u8; 32]) -> Self {
116+
Self::OnionService(value)
117+
}
118+
}
25119

26120
pub(crate) async fn create_socks5(
27121
proxy: SocketAddr,
28-
ip_addr: IpAddr,
122+
addr: SocksConnection,
29123
port: u16,
30124
) -> Result<TcpStream, Socks5Error> {
31125
// Connect to the proxy, likely a local Tor daemon.
32126
let timeout = tokio::time::timeout(CONNECTION_TIMEOUT, TcpStream::connect(proxy))
33127
.await
34128
.map_err(|_| Socks5Error::ConnectionTimeout)?;
35129
// Format the destination IP address and port according to the Socks5 spec
36-
let dest_ip_bytes = match ip_addr {
37-
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
38-
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
39-
};
130+
let dest_ip_bytes = addr.encode();
40131
let dest_port_bytes = port.to_be_bytes();
41-
let ip_type_byte = match ip_addr {
42-
IpAddr::V4(_) => ADDR_TYPE_IPV4,
43-
IpAddr::V6(_) => ADDR_TYPE_IPV6,
44-
};
132+
let ip_type_byte = addr.type_byte();
45133
// Begin the handshake by requesting a connection to the proxy.
46134
let mut tcp_stream = timeout.map_err(|_| Socks5Error::ConnectionFailed)?;
47135
tcp_stream.write_all(&[VERSION, METHODS, NOAUTH]).await?;
@@ -81,9 +169,31 @@ pub(crate) async fn create_socks5(
81169
let mut buf = [0_u8; 18];
82170
tcp_stream.read_exact(&mut buf).await?;
83171
}
172+
ADDR_TYPE_DOMAIN => {
173+
let mut len = [0_u8; 1];
174+
tcp_stream.read_exact(&mut len).await?;
175+
let mut buf = vec![0_u8; u8::from_le_bytes(len) as usize];
176+
tcp_stream.read_exact(&mut buf).await?;
177+
}
84178
_ => return Err(Socks5Error::ConnectionFailed),
85179
}
86180

87181
// Proxy handshake is complete, the TCP reader/writer can be returned
88182
Ok(tcp_stream)
89183
}
184+
185+
#[cfg(test)]
186+
mod tests {
187+
use super::pubkey_to_service;
188+
189+
#[test]
190+
fn public_key_to_service() {
191+
let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
192+
let hsid: [u8; 32] = hex::decode(hex).unwrap().try_into().unwrap();
193+
let service = pubkey_to_service(hsid);
194+
assert_eq!(
195+
"25njqamcweflpvkl73j4szahhihoc4xt3ktcgjnpaingr5yhkenl5sid.onion",
196+
service
197+
);
198+
}
199+
}

0 commit comments

Comments
 (0)