diff --git a/.cargo/config.toml b/.cargo/config.toml index 0c1c209f..e2f942b5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/Cargo.lock b/Cargo.lock index ff3dc569..f81784e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "aead" version = "0.4.1" @@ -17,6 +26,17 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "aes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2bc6d3f370b5666245ff421e231cba4353df936e26986d2918e61a8fd6aef6" +dependencies = [ + "aes-soft", + "aesni", + "block-cipher", +] + [[package]] name = "aes" version = "0.7.2" @@ -29,20 +49,54 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "aes-gcm" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0301c9e9c443494d970a07885e8cf3e587bae8356a1d5abd0999068413f7205f" +dependencies = [ + "aead 0.3.2", + "aes 0.5.0", + "block-cipher", + "ghash 0.3.1", + "subtle 2.4.0", +] + [[package]] name = "aes-gcm" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee2263805ba4537ccbb19db28525a7b1ebc7284c228eb5634c3124ca63eb03f" dependencies = [ - "aead", - "aes", + "aead 0.4.1", + "aes 0.7.2", "cipher", "ctr", - "ghash", + "ghash 0.4.1", "subtle 2.4.0", ] +[[package]] +name = "aes-soft" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dd91889c49327ad7ef3b500fd1109dbd3c509a03db0d4a9ce413b79f575cb6" +dependencies = [ + "block-cipher", + "byteorder", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aesni" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6fe808308bb07d393e2ea47780043ec47683fcf19cf5efc8ca51c50cc8c68a" +dependencies = [ + "block-cipher", + "opaque-debug 0.3.0", +] + [[package]] name = "ahash" version = "0.4.7" @@ -436,6 +490,15 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "block-cipher" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -549,6 +612,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244fbce0d47e97e8ef2f63b81d5e05882cb518c68531eb33194990d7b7e85845" +dependencies = [ + "stream-cipher", + "zeroize", +] + [[package]] name = "chacha20" version = "0.7.1" @@ -561,16 +634,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chacha20poly1305" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf18d374d66df0c05cdddd528a7db98f78c28e2519b120855c4f84c5027b1f5" +dependencies = [ + "aead 0.3.2", + "chacha20 0.5.0", + "poly1305 0.6.2", + "stream-cipher", + "zeroize", +] + [[package]] name = "chacha20poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" dependencies = [ - "aead", - "chacha20", + "aead 0.4.1", + "chacha20 0.7.1", "cipher", - "poly1305", + "poly1305 0.7.0", "zeroize", ] @@ -701,6 +787,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + [[package]] name = "crc32fast" version = "1.2.1" @@ -1302,6 +1394,16 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug 0.3.0", + "polyval 0.4.5", +] + [[package]] name = "ghash" version = "0.4.1" @@ -1309,7 +1411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6fb2a26dd2ebd268a68bc8e9acc9e67e487952f33384055a1cbe697514c64e" dependencies = [ "opaque-debug 0.3.0", - "polyval", + "polyval 0.5.0", ] [[package]] @@ -1779,8 +1881,12 @@ dependencies = [ "futures", "lazy_static", "libp2p-core", + "libp2p-noise 0.30.0", + "libp2p-ping", "libp2p-swarm", "libp2p-swarm-derive", + "libp2p-tcp", + "libp2p-yamux", "parity-multiaddr", "parking_lot 0.11.1", "pin-project 1.0.5", @@ -1801,7 +1907,7 @@ dependencies = [ "libp2p-core", "libp2p-dns", "libp2p-mplex", - "libp2p-noise", + "libp2p-noise 0.31.0", "libp2p-ping", "libp2p-request-response", "libp2p-swarm", @@ -1890,6 +1996,28 @@ dependencies = [ "unsigned-varint 0.7.0", ] +[[package]] +name = "libp2p-noise" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36db0f0db3b0433f5b9463f1c0cd9eadc0a3734a9170439ce501ff99733a88bd" +dependencies = [ + "bytes 1.0.1", + "curve25519-dalek", + "futures", + "lazy_static", + "libp2p-core", + "log 0.4.14", + "prost", + "prost-build", + "rand 0.7.3", + "sha2 0.9.5", + "snow 0.7.2", + "static_assertions", + "x25519-dalek", + "zeroize", +] + [[package]] name = "libp2p-noise" version = "0.31.0" @@ -1906,7 +2034,7 @@ dependencies = [ "prost-build", "rand 0.8.3", "sha2 0.9.5", - "snow", + "snow 0.8.0", "static_assertions", "x25519-dalek", "zeroize", @@ -1990,6 +2118,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "libp2p-tor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "data-encoding", + "futures", + "libp2p 0.37.1", + "rand 0.8.3", + "reqwest", + "tempfile", + "testcontainers 0.12.0", + "tokio", + "tokio-socks", + "torut", + "tracing", + "tracing-subscriber", +] + [[package]] name = "libp2p-websocket" version = "0.29.0" @@ -2674,6 +2822,16 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "poly1305" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7456bc1ad2d4cf82b3a016be4c2ac48daf11bf990c1603ebd447fe6f30fca8" +dependencies = [ + "cpuid-bool 0.2.0", + "universal-hash", +] + [[package]] name = "poly1305" version = "0.7.0" @@ -2685,6 +2843,17 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool 0.2.0", + "opaque-debug 0.3.0", + "universal-hash", +] + [[package]] name = "polyval" version = "0.5.0" @@ -3589,7 +3758,7 @@ checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpuid-bool", + "cpuid-bool 0.1.2", "digest 0.9.0", "opaque-debug 0.3.0", ] @@ -3703,15 +3872,33 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "snow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795dd7aeeee24468e5a32661f6d27f7b5cbed802031b2d7640c7b10f8fb2dd50" +dependencies = [ + "aes-gcm 0.7.0", + "blake2", + "chacha20poly1305 0.6.0", + "rand 0.7.3", + "rand_core 0.5.1", + "ring", + "rustc_version 0.2.3", + "sha2 0.9.5", + "subtle 2.4.0", + "x25519-dalek", +] + [[package]] name = "snow" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6142f7c25e94f6fd25a32c3348ec230df9109b463f59c8c7acc4bd34936babb7" dependencies = [ - "aes-gcm", + "aes-gcm 0.9.1", "blake2", - "chacha20poly1305", + "chacha20poly1305 0.8.0", "rand 0.8.3", "rand_core 0.6.2", "ring", @@ -3849,6 +4036,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "stream-cipher" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e15f898d8d8f25db24c253ea615cc14acf418ff307822995814e7d42cfa89" +dependencies = [ + "block-cipher", + "generic-array 0.14.4", +] + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 0f654d91..b74ee31c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ] +members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet", "libp2p-tor" ] [patch.crates-io] monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" } diff --git a/libp2p-tor/Cargo.toml b/libp2p-tor/Cargo.toml new file mode 100644 index 00000000..d52402f9 --- /dev/null +++ b/libp2p-tor/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "libp2p-tor" +version = "0.1.0" +authors = ["Thomas Eizinger "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" # TODO: Get rid of anyhow dependency +torut = { version = "0.1", default-features = false, features = ["v3", "control"] } +tokio-socks = "0.5" +libp2p = { version = "0.37", default-features = false, features = ["tcp-tokio"] } +tokio = { version = "1", features = ["sync"] } +futures = "0.3" +tracing = "0.1" +data-encoding = "2.3" +reqwest = { version = "0.11", features = ["socks", "rustls-tls"], default-features = false } +async-trait = "0.1" +rand = { version = "0.8", optional = true } + +[dev-dependencies] +tempfile = "3" +tokio = { version = "1", features = ["full"] } +rand = "0.8" +libp2p = { version = "0.37", default-features = false, features = ["yamux", "noise", "ping"] } +tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter", "chrono", "tracing-log"] } +testcontainers = "0.12" diff --git a/libp2p-tor/build.rs b/libp2p-tor/build.rs new file mode 100644 index 00000000..9287f019 --- /dev/null +++ b/libp2p-tor/build.rs @@ -0,0 +1,17 @@ +use std::process::Command; + +fn main() { + let status = Command::new("docker") + .arg("build") + .arg("-f") + .arg("./tor.Dockerfile") + .arg(".") + .arg("-t") + .arg("testcontainers-tor:latest") + .status() + .unwrap(); + + assert!(status.success()); + + println!("cargo:rerun-if-changed=./tor.Dockerfile"); +} diff --git a/libp2p-tor/examples/dialer.rs b/libp2p-tor/examples/dialer.rs new file mode 100644 index 00000000..738cbcf6 --- /dev/null +++ b/libp2p-tor/examples/dialer.rs @@ -0,0 +1,75 @@ +use libp2p::core::muxing::StreamMuxerBox; +use libp2p::core::upgrade::Version; +use libp2p::ping::{Ping, PingEvent, PingSuccess}; +use libp2p::swarm::{SwarmBuilder, SwarmEvent}; +use libp2p::{identity, noise, yamux, Multiaddr, Swarm, Transport}; +use libp2p_tor::dial_only; +use std::time::Duration; + +#[tokio::main] +async fn main() { + let addr_to_dial = std::env::args() + .next() + .unwrap() + .parse::() + .unwrap(); + + let mut swarm = new_swarm(); + + println!("Peer-ID: {}", swarm.local_peer_id()); + swarm.dial_addr(addr_to_dial).unwrap(); + + loop { + match swarm.next_event().await { + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + println!( + "Connected to {} via {}", + peer_id, + endpoint.get_remote_address() + ); + } + SwarmEvent::Behaviour(PingEvent { result, peer }) => match result { + Ok(PingSuccess::Pong) => { + println!("Got pong from {}", peer); + } + Ok(PingSuccess::Ping { rtt }) => { + println!("Pinged {} with rtt of {}s", peer, rtt.as_secs()); + } + Err(failure) => { + println!("Failed to ping {}: {}", peer, failure) + } + }, + _ => {} + } + } +} + +/// Builds a new swarm that is capable of dialling onion address. +fn new_swarm() -> Swarm { + let identity = identity::Keypair::generate_ed25519(); + + SwarmBuilder::new( + dial_only::TorConfig::new(9050) + .upgrade(Version::V1) + .authenticate( + noise::NoiseConfig::xx( + noise::Keypair::::new() + .into_authentic(&identity) + .unwrap(), + ) + .into_authenticated(), + ) + .multiplex(yamux::YamuxConfig::default()) + .timeout(Duration::from_secs(20)) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(), + Ping::default(), + identity.public().into_peer_id(), + ) + .executor(Box::new(|f| { + tokio::spawn(f); + })) + .build() +} diff --git a/libp2p-tor/examples/listener.rs b/libp2p-tor/examples/listener.rs new file mode 100644 index 00000000..c45c7c7e --- /dev/null +++ b/libp2p-tor/examples/listener.rs @@ -0,0 +1,98 @@ +use libp2p::core::muxing::StreamMuxerBox; +use libp2p::core::upgrade::Version; +use libp2p::ping::{Ping, PingEvent, PingSuccess}; +use libp2p::swarm::{SwarmBuilder, SwarmEvent}; +use libp2p::{identity, noise, yamux, Swarm, Transport}; +use libp2p_tor::duplex; +use libp2p_tor::torut_ext::AuthenticatedConnectionExt; +use noise::NoiseConfig; +use rand::Rng; +use std::time::Duration; +use torut::control::AuthenticatedConn; +use torut::onion::TorSecretKeyV3; + +#[tokio::main] +async fn main() { + let wildcard_multiaddr = + "/onion3/WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW:8080" + .parse() + .unwrap(); + + let mut swarm = new_swarm().await; + + println!("Peer-ID: {}", swarm.local_peer_id()); + swarm.listen_on(wildcard_multiaddr).unwrap(); + + loop { + match swarm.next_event().await { + SwarmEvent::NewListenAddr(addr) => { + println!("Listening on {}", addr); + } + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + println!( + "Connected to {} via {}", + peer_id, + endpoint.get_remote_address() + ); + } + SwarmEvent::Behaviour(PingEvent { result, peer }) => match result { + Ok(PingSuccess::Pong) => { + println!("Got pong from {}", peer); + } + Ok(PingSuccess::Ping { rtt }) => { + println!("Pinged {} with rtt of {}s", peer, rtt.as_secs()); + } + Err(failure) => { + println!("Failed to ping {}: {}", peer, failure) + } + }, + _ => {} + } + } +} + +/// Builds a new swarm that is capable of listening and dialling on the Tor +/// network. +/// +/// In particular, this swarm can create ephemeral hidden services on the +/// configured Tor node. +async fn new_swarm() -> Swarm { + let identity = identity::Keypair::generate_ed25519(); + + SwarmBuilder::new( + duplex::TorConfig::new( + AuthenticatedConn::new(9051).await.unwrap(), + random_onion_identity, + ) + .await + .unwrap() + .upgrade(Version::V1) + .authenticate( + NoiseConfig::xx( + noise::Keypair::::new() + .into_authentic(&identity) + .unwrap(), + ) + .into_authenticated(), + ) + .multiplex(yamux::YamuxConfig::default()) + .timeout(Duration::from_secs(20)) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(), + Ping::default(), + identity.public().into_peer_id(), + ) + .executor(Box::new(|f| { + tokio::spawn(f); + })) + .build() +} + +fn random_onion_identity() -> TorSecretKeyV3 { + let mut onion_key_bytes = [0u8; 64]; + rand::thread_rng().fill(&mut onion_key_bytes); + + onion_key_bytes.into() +} diff --git a/libp2p-tor/src/dial_only.rs b/libp2p-tor/src/dial_only.rs new file mode 100644 index 00000000..042d4bb9 --- /dev/null +++ b/libp2p-tor/src/dial_only.rs @@ -0,0 +1,57 @@ +use crate::torut_ext::AuthenticatedConnectionExt; +use crate::{fmt_as_tor_compatible_address, Error}; +use anyhow::Result; +use fmt_as_tor_compatible_address::fmt_as_tor_compatible_address; +use futures::future::BoxFuture; +use futures::prelude::*; +use libp2p::core::multiaddr::Multiaddr; +use libp2p::core::transport::{ListenerEvent, TransportError}; +use libp2p::core::Transport; +use libp2p::futures::stream::BoxStream; +use libp2p::tcp::tokio::TcpStream; +use torut::control::AuthenticatedConn; + +#[derive(Clone)] +pub struct TorConfig { + socks_port: u16, +} + +impl TorConfig { + pub fn new(socks_port: u16) -> Self { + Self { socks_port } + } + + pub async fn from_control_port(control_port: u16) -> Result { + let mut client = AuthenticatedConn::new(control_port).await?; + let socks_port = client.get_socks_port().await?; + + Ok(Self::new(socks_port)) + } +} + +impl Transport for TorConfig { + type Output = TcpStream; + type Error = Error; + #[allow(clippy::type_complexity)] + type Listener = + BoxStream<'static, Result, Self::Error>>; + type ListenerUpgrade = BoxFuture<'static, Result>; + type Dial = BoxFuture<'static, Result>; + + fn listen_on(self, addr: Multiaddr) -> Result> { + Err(TransportError::MultiaddrNotSupported(addr)) + } + + fn dial(self, addr: Multiaddr) -> Result> { + tracing::debug!("Connecting through Tor proxy to address {}", addr); + + let address = fmt_as_tor_compatible_address(addr.clone()) + .ok_or(TransportError::MultiaddrNotSupported(addr))?; + + Ok(crate::dial_via_tor(address, self.socks_port).boxed()) + } + + fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option { + None // address translation for tor doesn't make any sense :) + } +} diff --git a/libp2p-tor/src/duplex.rs b/libp2p-tor/src/duplex.rs new file mode 100644 index 00000000..3b9d3f9b --- /dev/null +++ b/libp2p-tor/src/duplex.rs @@ -0,0 +1,187 @@ +use crate::torut_ext::AuthenticatedConnectionExt; +use crate::{fmt_as_tor_compatible_address, torut_ext, Error}; +use fmt_as_tor_compatible_address::fmt_as_tor_compatible_address; +use futures::future::BoxFuture; +use futures::prelude::*; +use libp2p::core::multiaddr::{Multiaddr, Protocol}; +use libp2p::core::transport::map_err::MapErr; +use libp2p::core::transport::{ListenerEvent, TransportError}; +use libp2p::core::Transport; +use libp2p::futures::stream::BoxStream; +use libp2p::futures::{StreamExt, TryStreamExt}; +use libp2p::tcp::{GenTcpConfig, TokioTcpConfig}; +use std::sync::Arc; +use tokio::sync::Mutex; +use torut::control::{AsyncEvent, AuthenticatedConn}; +use torut::onion::TorSecretKeyV3; + +/// This is the hash of +/// `/onion3/WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW`. +const WILDCARD_ONION_ADDR_HASH: [u8; 35] = [ + 181, 173, 107, 90, 214, 181, 173, 107, 90, 214, 181, 173, 107, 90, 214, 181, 173, 107, 90, 214, + 181, 173, 107, 90, 214, 181, 173, 107, 90, 214, 181, 173, 107, 90, 214, +]; + +type TorutAsyncEventHandler = + fn( + AsyncEvent<'_>, + ) -> Box> + Unpin + Send>; + +#[derive(Clone)] +pub struct TorConfig { + inner: MapErr, fn(std::io::Error) -> Error>, /* TODO: Make generic over async-std / tokio */ + tor_client: Arc>>, + onion_key_generator: Arc TorSecretKeyV3) + Send + Sync>, + socks_port: u16, +} + +impl TorConfig { + pub async fn new( + mut client: AuthenticatedConn, + onion_key_generator: impl (Fn() -> TorSecretKeyV3) + Send + Sync + 'static, + ) -> Result { + let socks_port = client.get_socks_port().await?; + + Ok(Self { + inner: TokioTcpConfig::new().map_err(Error::InnerTransprot), + tor_client: Arc::new(Mutex::new(client)), + onion_key_generator: Arc::new(onion_key_generator), + socks_port, + }) + } + + pub async fn from_control_port( + control_port: u16, + key_generator: impl (Fn() -> TorSecretKeyV3) + Send + Sync + 'static, + ) -> Result { + let client = AuthenticatedConn::new(control_port).await?; + + Self::new(client, key_generator).await + } +} + +impl Transport for TorConfig { + type Output = libp2p::tcp::tokio::TcpStream; + type Error = Error; + #[allow(clippy::type_complexity)] + type Listener = + BoxStream<'static, Result, Self::Error>>; + type ListenerUpgrade = BoxFuture<'static, Result>; + type Dial = BoxFuture<'static, Result>; + + fn listen_on(self, addr: Multiaddr) -> Result> { + let mut protocols = addr.iter(); + let onion = if let Protocol::Onion3(onion) = protocols + .next() + .ok_or_else(|| TransportError::MultiaddrNotSupported(addr.clone()))? + { + onion + } else { + return Err(TransportError::MultiaddrNotSupported(addr)); + }; + + if onion.hash() != &WILDCARD_ONION_ADDR_HASH { + return Err(TransportError::Other(Error::OnlyWildcardAllowed)); + } + + let localhost_tcp_random_port_addr = "/ip4/127.0.0.1/tcp/0" + .parse() + .expect("always a valid multiaddr"); + + let listener = self.inner.listen_on(localhost_tcp_random_port_addr)?; + + let key: TorSecretKeyV3 = (self.onion_key_generator)(); + let onion_bytes = key.public().get_onion_address().get_raw_bytes(); + let onion_port = onion.port(); + + let tor_client = self.tor_client; + + let listener = listener + .and_then({ + move |event| { + let tor_client = tor_client.clone(); + let key = key.clone(); + let onion_multiaddress = + Multiaddr::empty().with(Protocol::Onion3((onion_bytes, onion_port).into())); + + async move { + Ok(match event { + ListenerEvent::NewAddress(address) => { + let local_port = address + .iter() + .find_map(|p| match p { + Protocol::Tcp(port) => Some(port), + _ => None, + }) + .expect("TODO: Error handling"); + + tracing::debug!( + "Setting up hidden service at {} to forward to {}", + onion_multiaddress, + address + ); + + match tor_client + .clone() + .lock() + .await + .add_ephemeral_service(&key, onion_port, local_port) + .await + { + Ok(()) => ListenerEvent::NewAddress(onion_multiaddress.clone()), + Err(e) => ListenerEvent::Error(Error::Torut(e)), + } + } + ListenerEvent::Upgrade { + upgrade, + local_addr, + remote_addr, + } => ListenerEvent::Upgrade { + upgrade: upgrade.boxed(), + local_addr, + remote_addr, + }, + ListenerEvent::AddressExpired(_) => { + // can ignore address because we only ever listened on one and we + // know which one that was + + let onion_address_without_dot_onion = key + .public() + .get_onion_address() + .get_address_without_dot_onion(); + + match tor_client + .lock() + .await + .del_onion(&onion_address_without_dot_onion) + .await + { + Ok(()) => ListenerEvent::AddressExpired(onion_multiaddress), + Err(e) => ListenerEvent::Error(Error::Torut( + torut_ext::Error::Connection(e), + )), + } + } + ListenerEvent::Error(e) => ListenerEvent::Error(e), + }) + } + } + }) + .boxed(); + + Ok(listener) + } + + fn dial(self, addr: Multiaddr) -> Result> { + tracing::debug!("Connecting through Tor proxy to address {}", addr); + + let address = fmt_as_tor_compatible_address(addr.clone()) + .ok_or(TransportError::MultiaddrNotSupported(addr))?; + + Ok(crate::dial_via_tor(address, self.socks_port).boxed()) + } + + fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option { + None // address translation for tor doesn't make any sense :) + } +} diff --git a/libp2p-tor/src/fmt_as_tor_compatible_address.rs b/libp2p-tor/src/fmt_as_tor_compatible_address.rs new file mode 100644 index 00000000..180eb41f --- /dev/null +++ b/libp2p-tor/src/fmt_as_tor_compatible_address.rs @@ -0,0 +1,60 @@ +use data_encoding::BASE32; +use libp2p::multiaddr::Protocol; +use libp2p::Multiaddr; + +/// Tor expects an address format of ADDR:PORT. +/// This helper function tries to convert the provided multi-address into this +/// format. None is returned if an unsupported protocol was provided. +pub fn fmt_as_tor_compatible_address(multi: Multiaddr) -> Option { + let mut protocols = multi.iter(); + let address_string = match protocols.next()? { + // if it is an Onion address, we have all we need and can return + Protocol::Onion3(addr) => { + return Some(format!( + "{}.onion:{}", + BASE32.encode(addr.hash()).to_lowercase(), + addr.port() + )); + } + // Deal with non-onion addresses + Protocol::Ip4(addr) => format!("{}", addr), + Protocol::Ip6(addr) => format!("{}", addr), + Protocol::Dns(addr) => format!("{}", addr), + Protocol::Dns4(addr) => format!("{}", addr), + _ => return None, + }; + + let port = match protocols.next()? { + Protocol::Tcp(port) => port, + Protocol::Udp(port) => port, + _ => return None, + }; + + Some(format!("{}:{}", address_string, port)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_fmt_as_tor_compatible_address() { + let test_cases = &[ + ("/onion3/oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did:1024/p2p/12D3KooWPD4uHN74SHotLN7VCH7Fm8zZgaNVymYcpeF1fpD2guc9", Some("oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did.onion:1024")), + ("/ip4/127.0.0.1/tcp/7777", Some("127.0.0.1:7777")), + ("/ip6/2001:db8:85a3:8d3:1319:8a2e:370:7348/tcp/7777", Some("2001:db8:85a3:8d3:1319:8a2e:370:7348:7777")), + ("/ip4/127.0.0.1/udp/7777", Some("127.0.0.1:7777")), + ("/ip4/127.0.0.1/tcp/7777/ws", Some("127.0.0.1:7777")), + ("/dns4/randomdomain.com/tcp/7777", Some("randomdomain.com:7777")), + ("/dns/randomdomain.com/tcp/7777", Some("randomdomain.com:7777")), + ("/dnsaddr/randomdomain.com", None), + ]; + + for (multiaddress, expected_address) in test_cases { + let actual_address = + fmt_as_tor_compatible_address(multiaddress.parse().expect("a valid multi-address")); + + assert_eq!(&actual_address.as_deref(), expected_address) + } + } +} diff --git a/libp2p-tor/src/lib.rs b/libp2p-tor/src/lib.rs new file mode 100644 index 00000000..4e014ac4 --- /dev/null +++ b/libp2p-tor/src/lib.rs @@ -0,0 +1,42 @@ +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::{fmt, io}; + +use libp2p::tcp::tokio::TcpStream; +use tokio_socks::tcp::Socks5Stream; + +pub mod dial_only; +pub mod duplex; +mod fmt_as_tor_compatible_address; +pub mod torut_ext; + +async fn dial_via_tor(onion_address: String, socks_port: u16) -> anyhow::Result { + let sock = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, socks_port)); + let stream = Socks5Stream::connect(sock, onion_address) + .await + .map_err(Error::UnreachableProxy)?; + let stream = TcpStream(stream.into_inner()); + + Ok(stream) +} + +#[derive(Debug)] +pub enum Error { + OnlyWildcardAllowed, + Torut(torut_ext::Error), + UnreachableProxy(tokio_socks::Error), + InnerTransprot(io::Error), +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl From for Error { + fn from(e: torut_ext::Error) -> Self { + Error::Torut(e) + } +} diff --git a/libp2p-tor/src/torut_ext.rs b/libp2p-tor/src/torut_ext.rs new file mode 100644 index 00000000..094376cf --- /dev/null +++ b/libp2p-tor/src/torut_ext.rs @@ -0,0 +1,116 @@ +use std::borrow::Cow; +use std::future::Future; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::num::ParseIntError; +use std::{io, iter}; +use torut::control::{AsyncEvent, AuthenticatedConn, TorAuthData, UnauthenticatedConn}; +use torut::onion::TorSecretKeyV3; + +pub type AsyncEventHandler = + fn( + AsyncEvent<'_>, + ) -> Box> + Unpin + Send>; + +#[derive(Debug)] +pub enum Error { + FailedToConnect(io::Error), + NoAuthData(Option), + Connection(torut::control::ConnError), + FailedToAddHiddenService(torut::control::ConnError), + FailedToParsePort(ParseIntError), +} + +impl From for Error { + fn from(e: torut::control::ConnError) -> Self { + Error::Connection(e) + } +} + +impl From for Error { + fn from(e: ParseIntError) -> Self { + Error::FailedToParsePort(e) + } +} + +#[async_trait::async_trait] +pub trait AuthenticatedConnectionExt: Sized { + async fn new(control_port: u16) -> Result; + async fn with_password(control_port: u16, password: &str) -> Result; + async fn add_ephemeral_service( + &mut self, + key: &TorSecretKeyV3, + onion_port: u16, + local_port: u16, + ) -> Result<(), Error>; + async fn get_socks_port(&mut self) -> Result; +} + +#[async_trait::async_trait] +impl AuthenticatedConnectionExt for AuthenticatedConn { + async fn new(control_port: u16) -> Result { + let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", control_port)) + .await + .map_err(Error::FailedToConnect)?; + let mut uac = UnauthenticatedConn::new(stream); + + let tor_info = uac.load_protocol_info().await?; + + let tor_auth_data = tor_info + .make_auth_data() + .map_err(|e| Error::NoAuthData(Some(e)))? + .ok_or(Error::NoAuthData(None))?; + + uac.authenticate(&tor_auth_data).await?; + + Ok(uac.into_authenticated().await) + } + + async fn with_password(control_port: u16, password: &str) -> Result { + let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", control_port)) + .await + .map_err(Error::FailedToConnect)?; + let mut uac = UnauthenticatedConn::new(stream); + + uac.authenticate(&TorAuthData::HashedPassword(Cow::Borrowed(password))) + .await?; + + Ok(uac.into_authenticated().await) + } + + async fn add_ephemeral_service( + &mut self, + key: &TorSecretKeyV3, + onion_port: u16, + local_port: u16, + ) -> Result<(), Error> { + self.add_onion_v3( + &key, + false, + false, + false, + None, + &mut iter::once(&( + onion_port, + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), local_port)), + )), + ) + .await + .map_err(Error::FailedToAddHiddenService) + } + + async fn get_socks_port(&mut self) -> Result { + const DEFAULT_SOCKS_PORT: u16 = 9050; + + let mut vec = self + .get_conf("SocksPort") + .await + .map_err(Error::Connection)?; + + let first_element = vec + .pop() + .expect("exactly one element because we requested one config option"); + let port = first_element.map_or(Ok(DEFAULT_SOCKS_PORT), |port| port.parse())?; // if config is empty, we are listing on the default port + + Ok(port) + } +} diff --git a/libp2p-tor/tests/integration_test.rs b/libp2p-tor/tests/integration_test.rs new file mode 100644 index 00000000..6478997d --- /dev/null +++ b/libp2p-tor/tests/integration_test.rs @@ -0,0 +1,203 @@ +use libp2p::core::muxing::StreamMuxerBox; +use libp2p::core::transport; +use libp2p::core::upgrade::Version; +use libp2p::ping::{Ping, PingEvent}; +use libp2p::swarm::{SwarmBuilder, SwarmEvent}; +use libp2p::tcp::tokio::TcpStream; +use libp2p::{noise, yamux, Swarm, Transport}; +use libp2p_tor::torut_ext::AuthenticatedConnectionExt; +use libp2p_tor::{dial_only, duplex}; +use rand::Rng; +use std::collections::HashMap; +use std::convert::Infallible; +use std::future::Future; +use std::time::Duration; +use testcontainers::{Container, Docker, Image, WaitForMessage}; +use torut::control::AuthenticatedConn; + +#[tokio::test(flavor = "multi_thread")] +async fn create_ephemeral_service() { + tracing_subscriber::fmt().with_env_filter("debug").init(); + let wildcard_multiaddr = + "/onion3/WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW:8080" + .parse() + .unwrap(); + + // let docker = Cli::default(); + // + // let tor1 = docker.run(TorImage::default().with_args(TorArgs { + // control_port: Some(9051), + // socks_port: None + // })); + // let tor2 = docker.run(TorImage::default()); + // + // let tor1_control_port = tor1.get_host_port(9051).unwrap(); + // let tor2_socks_port = tor2.get_host_port(9050).unwrap(); + + let mut listen_swarm = make_swarm(async move { + let mut onion_key_bytes = [0u8; 64]; + rand::thread_rng().fill(&mut onion_key_bytes); + + duplex::TorConfig::new( + AuthenticatedConn::with_password(9051, "supersecret") + .await + .unwrap(), + move || onion_key_bytes.into(), + ) + .await + .unwrap() + .boxed() + }) + .await; + let mut dial_swarm = make_swarm(async { dial_only::TorConfig::new(9050).boxed() }).await; + + listen_swarm.listen_on(wildcard_multiaddr).unwrap(); + + let onion_listen_addr = loop { + let event = listen_swarm.next_event().await; + + tracing::info!("{:?}", event); + + if let SwarmEvent::NewListenAddr(addr) = event { + break addr; + } + }; + + dial_swarm.dial_addr(onion_listen_addr).unwrap(); + + loop { + tokio::select! { + event = listen_swarm.next_event() => { + tracing::info!("{:?}", event); + }, + event = dial_swarm.next_event() => { + tracing::info!("{:?}", event); + } + } + } +} + +async fn make_swarm( + transport_future: impl Future>, +) -> Swarm { + let identity = libp2p::identity::Keypair::generate_ed25519(); + + let dh_keys = noise::Keypair::::new() + .into_authentic(&identity) + .unwrap(); + let noise = noise::NoiseConfig::xx(dh_keys).into_authenticated(); + + let transport = transport_future + .await + .upgrade(Version::V1) + .authenticate(noise) + .multiplex(yamux::YamuxConfig::default()) + .timeout(Duration::from_secs(20)) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(); + + SwarmBuilder::new( + transport, + Behaviour::default(), + identity.public().into_peer_id(), + ) + .executor(Box::new(|f| { + tokio::spawn(f); + })) + .build() +} + +#[derive(Debug)] +enum OutEvent { + Ping(PingEvent), +} + +impl From for OutEvent { + fn from(e: PingEvent) -> Self { + OutEvent::Ping(e) + } +} + +#[derive(libp2p::NetworkBehaviour, Default)] +#[behaviour(event_process = false, out_event = "OutEvent")] +struct Behaviour { + ping: Ping, +} + +#[derive(Default)] +struct TorImage { + args: TorArgs, +} + +impl TorImage { + // fn control_port_password(&self) -> String { + // "supersecret".to_owned() + // } +} + +#[derive(Default, Copy, Clone)] +struct TorArgs { + control_port: Option, + socks_port: Option, +} + +impl IntoIterator for TorArgs { + type Item = String; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let mut args = Vec::new(); + + if let Some(port) = self.socks_port { + args.push(format!("SocksPort")); + args.push(format!("0.0.0.0:{}", port)); + } + + if let Some(port) = self.control_port { + args.push(format!("ControlPort")); + args.push(format!("0.0.0.0:{}", port)); + args.push(format!("HashedControlPassword")); + args.push(format!( + "16:436B425404AA332A60B4F341C2023146C4B3A80548D757F0BB10DE81B4" + )) + } + + args.into_iter() + } +} + +impl Image for TorImage { + type Args = TorArgs; + type EnvVars = HashMap; + type Volumes = HashMap; + type EntryPoint = Infallible; + + fn descriptor(&self) -> String { + "testcontainers-tor:latest".to_owned() // this is build locally using + // the buildscript + } + + fn wait_until_ready(&self, container: &Container<'_, D, Self>) { + container + .logs() + .stdout + .wait_for_message("Bootstrapped 100% (done): Done") + .unwrap(); + } + + fn args(&self) -> Self::Args { + self.args.clone() + } + + fn env_vars(&self) -> Self::EnvVars { + HashMap::new() + } + + fn volumes(&self) -> Self::Volumes { + HashMap::new() + } + + fn with_args(self, args: Self::Args) -> Self { + Self { args } + } +} diff --git a/libp2p-tor/tor.Dockerfile b/libp2p-tor/tor.Dockerfile new file mode 100644 index 00000000..a4b63683 --- /dev/null +++ b/libp2p-tor/tor.Dockerfile @@ -0,0 +1,13 @@ +# set alpine as the base image of the Dockerfile +FROM alpine:latest + +# update the package repository and install Tor +RUN apk update && apk add tor +# Set `tor` as the default user during the container runtime +USER tor + +EXPOSE 9050 +EXPOSE 9051 + +# Set `tor` as the entrypoint for the image +ENTRYPOINT ["tor"]