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 89861f7d..fd7f2a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1735,6 +1735,7 @@ dependencies = [ "libp2p-dns", "libp2p-mplex", "libp2p-noise", + "libp2p-ping", "libp2p-request-response", "libp2p-swarm", "libp2p-swarm-derive", @@ -1844,6 +1845,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "libp2p-ping" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4bfaffac63bf3c7ec11ed9d8879d455966ddea7e78ee14737f0b6dce0d1cd1" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-swarm", + "log 0.4.14", + "rand 0.7.3", + "void", + "wasm-timer", +] + [[package]] name = "libp2p-request-response" version = "0.11.0" @@ -1907,6 +1923,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "libp2p-tor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "data-encoding", + "futures", + "libp2p", + "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" diff --git a/Cargo.toml b/Cargo.toml index 56e4fe63..7cfce0d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ] +members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet", "libp2p-tor" ] 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"]