Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bitcoin = { version = "0.32.7", default-features = false, features = [
bip324 = { version = "0.7.0", default-features = false, features = [
"tokio",
] }
hashes = { package = "bitcoin_hashes", version = "0.20.0" }
tokio = { version = "1.19", default-features = false, features = [
"rt-multi-thread",
"sync",
Expand Down
2 changes: 2 additions & 0 deletions examples/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ async fn main() {
))
// Add some initial peers
.add_peers(seeds.into_iter().map(From::from))
// Connections over Tor are supported by Socks5 proxy
// .socks5_proxy(bip157::Socks5Proxy::local())
// Create the node and client
.build();
// Run the node on a separate task
Expand Down
5 changes: 2 additions & 3 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::net::SocketAddr;
use std::{path::PathBuf, time::Duration};

use bitcoin::Network;

use super::{client::Client, node::Node};
use crate::chain::ChainState;
use crate::network::ConnectionType;
use crate::TrustedPeer;
use crate::{Config, FilterType};
use crate::{Socks5Proxy, TrustedPeer};

const MIN_PEERS: u8 = 1;
const MAX_PEERS: u8 = 15;
Expand Down Expand Up @@ -125,7 +124,7 @@ impl Builder {

/// Route network traffic through a Tor daemon using a Socks5 proxy. Currently, proxies
/// must be reachable by IP address.
pub fn socks5_proxy(mut self, proxy: impl Into<SocketAddr>) -> Self {
pub fn socks5_proxy(mut self, proxy: impl Into<Socks5Proxy>) -> Self {
let ip_addr = proxy.into();
let connection = ConnectionType::Socks5Proxy(ip_addr);
self.config.connection_type = connection;
Expand Down
22 changes: 21 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub mod node;

use chain::Filter;

use std::net::{IpAddr, SocketAddr};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;

// Re-exports
Expand Down Expand Up @@ -294,6 +294,26 @@ impl From<SocketAddr> for TrustedPeer {
}
}

/// Route network traffic through a Socks5 proxy, typically used by a Tor daemon.
#[derive(Debug, Clone)]
pub struct Socks5Proxy(SocketAddr);

impl Socks5Proxy {
/// Connect to the default local Socks5 proxy hosted at `127.0.0.1:9050`.
pub const fn local() -> Self {
Socks5Proxy(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
9050,
))
}
}

impl From<SocketAddr> for Socks5Proxy {
fn from(value: SocketAddr) -> Self {
Self(value)
}
}

#[derive(Debug, Clone, Copy)]
enum NodeState {
// We are behind on block headers according to our peers.
Expand Down
40 changes: 24 additions & 16 deletions src/network/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
collections::{HashMap, HashSet},
fs::{self, File},
net::{IpAddr, SocketAddr},
net::IpAddr,
path::PathBuf,
time::Duration,
};
Expand All @@ -23,11 +23,13 @@ use bitcoin::{
},
Block, BlockHash, FeeRate, Wtxid,
};
use socks::create_socks5;
use socks::{create_socks5, SocksConnection};
use tokio::{net::TcpStream, time::Instant};

use error::PeerError;

use crate::Socks5Proxy;

pub(crate) mod dns;
pub(crate) mod error;
pub(crate) mod inbound;
Expand Down Expand Up @@ -130,18 +132,20 @@ impl LastBlockMonitor {
}
}

#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Default)]
pub(crate) enum ConnectionType {
#[default]
ClearNet,
Socks5Proxy(SocketAddr),
Socks5Proxy(Socks5Proxy),
}

impl ConnectionType {
pub(crate) fn can_connect(&self, addr: &AddrV2) -> bool {
match &self {
Self::ClearNet => matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_)),
Self::Socks5Proxy(_) => matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_)),
Self::Socks5Proxy(_) => {
matches!(addr, AddrV2::Ipv4(_) | AddrV2::Ipv6(_) | AddrV2::TorV3(_))
}
}
}

Expand All @@ -151,13 +155,13 @@ impl ConnectionType {
port: u16,
handshake_timeout: Duration,
) -> Result<TcpStream, PeerError> {
let socket_addr = match addr {
AddrV2::Ipv4(ip) => IpAddr::V4(ip),
AddrV2::Ipv6(ip) => IpAddr::V6(ip),
_ => return Err(PeerError::UnreachableSocketAddr),
};
match &self {
Self::ClearNet => {
let socket_addr = match addr {
AddrV2::Ipv4(ip) => IpAddr::V4(ip),
AddrV2::Ipv6(ip) => IpAddr::V6(ip),
_ => return Err(PeerError::UnreachableSocketAddr),
};
let timeout = tokio::time::timeout(
handshake_timeout,
TcpStream::connect((socket_addr, port)),
Expand All @@ -168,12 +172,16 @@ impl ConnectionType {
Ok(tcp_stream)
}
Self::Socks5Proxy(proxy) => {
let socks5_timeout = tokio::time::timeout(
handshake_timeout,
create_socks5(*proxy, socket_addr, port),
)
.await
.map_err(|_| PeerError::ConnectionFailed)?;
let addr = match addr {
AddrV2::Ipv4(ipv4) => SocksConnection::ClearNet(IpAddr::V4(ipv4)),
AddrV2::Ipv6(ipv6) => SocksConnection::ClearNet(IpAddr::V6(ipv6)),
AddrV2::TorV3(onion) => SocksConnection::OnionService(onion),
_ => return Err(PeerError::UnreachableSocketAddr),
};
let socks5_timeout =
tokio::time::timeout(handshake_timeout, create_socks5(proxy.0, addr, port))
.await
.map_err(|_| PeerError::ConnectionFailed)?;
let tcp_stream = socks5_timeout.map_err(PeerError::Socks5)?;
Ok(tcp_stream)
}
Expand Down
128 changes: 119 additions & 9 deletions src/network/socks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
time::Duration,
};

use hashes::sha3_256;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
Expand All @@ -21,27 +22,114 @@ const CMD_CONNECT: u8 = 1;
const RESPONSE_SUCCESS: u8 = 0;
const RSV: u8 = 0;
const ADDR_TYPE_IPV4: u8 = 1;
const ADDR_TYPE_DOMAIN: u8 = 3;
const ADDR_TYPE_IPV6: u8 = 4;
// Tor constants
const SALT: &[u8] = b".onion checksum";
const TOR_VERSION: u8 = 0x03;
const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";

fn pubkey_to_service(ed25519: [u8; 32]) -> String {
let mut cs_input = Vec::with_capacity(48);
// SHA3(".onion checksum" + public key + version)
cs_input.extend_from_slice(SALT);
cs_input.extend_from_slice(&ed25519);
cs_input.push(TOR_VERSION);
let cs = sha3_256::hash(&cs_input).to_byte_array();
// Onion address = public key + 2 byte checksum + version
let mut input_buf = [0u8; 35];
input_buf[..32].copy_from_slice(&ed25519);
input_buf[32] = cs[0];
input_buf[33] = cs[1];
input_buf[34] = TOR_VERSION;
let mut encoding = base32_encode(&input_buf);
debug_assert!(encoding.len() == 56);
encoding.push_str(".onion");
encoding
}

fn base32_encode(data: &[u8]) -> String {
let mut result = String::with_capacity((data.len() * 8).div_ceil(5));
let mut buffer: u64 = 0;
let mut bits_left: u32 = 0;
for &byte in data {
buffer = (buffer << 8) | byte as u64;
bits_left += 8;
while bits_left >= 5 {
bits_left -= 5;
let index = ((buffer >> bits_left) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
// Keep only the unconsumed bits
buffer &= (1u64 << bits_left) - 1;
}
if bits_left > 0 {
let index = ((buffer << (5 - bits_left)) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
result
}

#[derive(Debug)]
pub(crate) enum SocksConnection {
ClearNet(IpAddr),
OnionService([u8; 32]),
}

impl SocksConnection {
fn encode(&self) -> Vec<u8> {
match self {
Self::ClearNet(net) => match net {
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
},
Self::OnionService(onion) => {
let service = pubkey_to_service(*onion);
let enc = service.as_bytes();
let mut buf = Vec::with_capacity(enc.len() + 1);
buf.push(enc.len() as u8);
buf.extend_from_slice(enc);
buf
}
}
}

fn type_byte(&self) -> u8 {
match self {
Self::ClearNet(net) => match net {
IpAddr::V4(_) => ADDR_TYPE_IPV4,
IpAddr::V6(_) => ADDR_TYPE_IPV6,
},
Self::OnionService(_) => ADDR_TYPE_DOMAIN,
}
}
}

impl From<IpAddr> for SocksConnection {
fn from(value: IpAddr) -> Self {
Self::ClearNet(value)
}
}

impl From<[u8; 32]> for SocksConnection {
fn from(value: [u8; 32]) -> Self {
Self::OnionService(value)
}
}

pub(crate) async fn create_socks5(
proxy: SocketAddr,
ip_addr: IpAddr,
addr: SocksConnection,
port: u16,
) -> Result<TcpStream, Socks5Error> {
// Connect to the proxy, likely a local Tor daemon.
let timeout = tokio::time::timeout(CONNECTION_TIMEOUT, TcpStream::connect(proxy))
.await
.map_err(|_| Socks5Error::ConnectionTimeout)?;
// Format the destination IP address and port according to the Socks5 spec
let dest_ip_bytes = match ip_addr {
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
};
let dest_ip_bytes = addr.encode();
let dest_port_bytes = port.to_be_bytes();
let ip_type_byte = match ip_addr {
IpAddr::V4(_) => ADDR_TYPE_IPV4,
IpAddr::V6(_) => ADDR_TYPE_IPV6,
};
let ip_type_byte = addr.type_byte();
// Begin the handshake by requesting a connection to the proxy.
let mut tcp_stream = timeout.map_err(|_| Socks5Error::ConnectionFailed)?;
tcp_stream.write_all(&[VERSION, METHODS, NOAUTH]).await?;
Expand Down Expand Up @@ -81,9 +169,31 @@ pub(crate) async fn create_socks5(
let mut buf = [0_u8; 18];
tcp_stream.read_exact(&mut buf).await?;
}
ADDR_TYPE_DOMAIN => {
let mut len = [0_u8; 1];
tcp_stream.read_exact(&mut len).await?;
let mut buf = vec![0_u8; u8::from_le_bytes(len) as usize];
tcp_stream.read_exact(&mut buf).await?;
}
_ => return Err(Socks5Error::ConnectionFailed),
}

// Proxy handshake is complete, the TCP reader/writer can be returned
Ok(tcp_stream)
}

#[cfg(test)]
mod tests {
use super::pubkey_to_service;

#[test]
fn public_key_to_service() {
let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
let hsid: [u8; 32] = hex::decode(hex).unwrap().try_into().unwrap();
let service = pubkey_to_service(hsid);
assert_eq!(
"25njqamcweflpvkl73j4szahhihoc4xt3ktcgjnpaingr5yhkenl5sid.onion",
service
);
}
}