WIP: libp2p-tor

debug-remodel-tor
Thomas Eizinger 3 years ago
parent ada5acb2b5
commit 44dddcd6bc
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96

@ -1,2 +1,5 @@
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

221
Cargo.lock generated

@ -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"

@ -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" }

@ -0,0 +1,28 @@
[package]
name = "libp2p-tor"
version = "0.1.0"
authors = ["Thomas Eizinger <thomas@eizinger.io>"]
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"

@ -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");
}

@ -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::<Multiaddr>()
.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<Ping> {
let identity = identity::Keypair::generate_ed25519();
SwarmBuilder::new(
dial_only::TorConfig::new(9050)
.upgrade(Version::V1)
.authenticate(
noise::NoiseConfig::xx(
noise::Keypair::<noise::X25519Spec>::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()
}

@ -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<Ping> {
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::<noise::X25519Spec>::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()
}

@ -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<Self, Error> {
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<ListenerEvent<Self::ListenerUpgrade, Self::Error>, Self::Error>>;
type ListenerUpgrade = BoxFuture<'static, Result<Self::Output, Self::Error>>;
type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>;
fn listen_on(self, addr: Multiaddr) -> Result<Self::Listener, TransportError<Self::Error>> {
Err(TransportError::MultiaddrNotSupported(addr))
}
fn dial(self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
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<Multiaddr> {
None // address translation for tor doesn't make any sense :)
}
}

@ -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<dyn Future<Output = Result<(), torut::control::ConnError>> + Unpin + Send>;
#[derive(Clone)]
pub struct TorConfig {
inner: MapErr<GenTcpConfig<libp2p::tcp::tokio::Tcp>, fn(std::io::Error) -> Error>, /* TODO: Make generic over async-std / tokio */
tor_client: Arc<Mutex<AuthenticatedConn<tokio::net::TcpStream, TorutAsyncEventHandler>>>,
onion_key_generator: Arc<dyn (Fn() -> TorSecretKeyV3) + Send + Sync>,
socks_port: u16,
}
impl TorConfig {
pub async fn new(
mut client: AuthenticatedConn<tokio::net::TcpStream, TorutAsyncEventHandler>,
onion_key_generator: impl (Fn() -> TorSecretKeyV3) + Send + Sync + 'static,
) -> Result<Self, Error> {
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<Self, Error> {
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<ListenerEvent<Self::ListenerUpgrade, Self::Error>, Self::Error>>;
type ListenerUpgrade = BoxFuture<'static, Result<Self::Output, Self::Error>>;
type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>;
fn listen_on(self, addr: Multiaddr) -> Result<Self::Listener, TransportError<Self::Error>> {
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<Self::Dial, TransportError<Self::Error>> {
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<Multiaddr> {
None // address translation for tor doesn't make any sense :)
}
}

@ -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<String> {
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)
}
}
}

@ -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<TcpStream, Error> {
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<torut_ext::Error> for Error {
fn from(e: torut_ext::Error) -> Self {
Error::Torut(e)
}
}

@ -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<dyn Future<Output = Result<(), torut::control::ConnError>> + Unpin + Send>;
#[derive(Debug)]
pub enum Error {
FailedToConnect(io::Error),
NoAuthData(Option<io::Error>),
Connection(torut::control::ConnError),
FailedToAddHiddenService(torut::control::ConnError),
FailedToParsePort(ParseIntError),
}
impl From<torut::control::ConnError> for Error {
fn from(e: torut::control::ConnError) -> Self {
Error::Connection(e)
}
}
impl From<ParseIntError> 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<Self, Error>;
async fn with_password(control_port: u16, password: &str) -> Result<Self, Error>;
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<u16, Error>;
}
#[async_trait::async_trait]
impl AuthenticatedConnectionExt for AuthenticatedConn<tokio::net::TcpStream, AsyncEventHandler> {
async fn new(control_port: u16) -> Result<Self, Error> {
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<Self, Error> {
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<u16, Error> {
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)
}
}

@ -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<Output = transport::Boxed<TcpStream>>,
) -> Swarm<Behaviour> {
let identity = libp2p::identity::Keypair::generate_ed25519();
let dh_keys = noise::Keypair::<noise::X25519Spec>::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<PingEvent> 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<u16>,
socks_port: Option<u16>,
}
impl IntoIterator for TorArgs {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
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<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = Infallible;
fn descriptor(&self) -> String {
"testcontainers-tor:latest".to_owned() // this is build locally using
// the buildscript
}
fn wait_until_ready<D: Docker>(&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 }
}
}

@ -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"]
Loading…
Cancel
Save