Compare commits
17 Commits
master
...
debug-remo
Author | SHA1 | Date |
---|---|---|
Daniel Karzel | f892efc0ac | 3 years ago |
Daniel Karzel | 7a3054267b | 3 years ago |
Thomas Eizinger | 56285883e8 | 3 years ago |
Thomas Eizinger | 04d7508f4e | 3 years ago |
Thomas Eizinger | 140ab200fc | 3 years ago |
Thomas Eizinger | 89b6131252 | 3 years ago |
Thomas Eizinger | 83c4be1657 | 3 years ago |
Thomas Eizinger | 085848d646 | 3 years ago |
Thomas Eizinger | 0a69eb5866 | 3 years ago |
Daniel Karzel | 6b72108681 | 3 years ago |
Daniel Karzel | 0c057e2e5e | 3 years ago |
Daniel Karzel | b51c7bcd36 | 3 years ago |
Daniel Karzel | 6fd36dbeb5 | 3 years ago |
Daniel Karzel | dede7b38f9 | 3 years ago |
Daniel Karzel | 10a5bf05ca | 3 years ago |
Daniel Karzel | 52bdaf2c1d | 3 years ago |
Thomas Eizinger | 44dddcd6bc | 3 years ago |
@ -1,2 +1,5 @@
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
@ -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
|
||||
async-trait = "0.1"
|
||||
data-encoding = "2.3"
|
||||
futures = "0.3"
|
||||
libp2p = { version = "0.37", default-features = false, features = [ "tcp-tokio" ] }
|
||||
rand = { version = "0.8", optional = true }
|
||||
reqwest = { version = "0.11", features = [ "socks", "rustls-tls" ], default-features = false }
|
||||
tokio = { version = "1", features = [ "sync" ] }
|
||||
tokio-socks = "0.5"
|
||||
torut = { version = "0.1", default-features = false, features = [ "v3", "control" ] }
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
libp2p = { version = "0.37", default-features = false, features = [ "yamux", "noise", "ping", "mplex" ] }
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false }
|
||||
tempfile = "3"
|
||||
testcontainers = "0.12"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }
|
@ -0,0 +1,102 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use libp2p::core::muxing::StreamMuxerBox;
|
||||
use libp2p::core::upgrade::{SelectUpgrade, Version};
|
||||
use libp2p::mplex::MplexConfig;
|
||||
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;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("debug,libp2p_tor=debug") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients
|
||||
.with_test_writer()
|
||||
.set_default();
|
||||
|
||||
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050")
|
||||
.map_err(|_| anyhow!("tor proxy should be there"))?;
|
||||
let client = reqwest::Client::builder().proxy(proxy).build()?;
|
||||
|
||||
let res = client.get("https://check.torproject.org").send().await?;
|
||||
let text = res.text().await?;
|
||||
|
||||
if !text.contains("Congratulations. This browser is configured to use Tor.") {
|
||||
bail!("Tor is currently not running")
|
||||
}
|
||||
|
||||
let addr_to_dial = "/onion3/jpclybnowuibjexya3qggzvzkoeruuav4nyjlxpnkrosldsvykfbn6qd:7654/p2p/12D3KooWHKqGyK4hVtf5BQY8GpbY6fSGKDZ8eBXMQ3H2RsdnKVzC"
|
||||
.parse::<Multiaddr>()
|
||||
.unwrap();
|
||||
|
||||
let mut swarm = new_swarm();
|
||||
|
||||
println!("Peer-ID: {}", swarm.local_peer_id());
|
||||
|
||||
println!("Dialing {}", addr_to_dial);
|
||||
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)
|
||||
}
|
||||
},
|
||||
event => {
|
||||
println!("Swarm event: {:?}", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new swarm that is capable of dialling onion address.
|
||||
fn new_swarm() -> Swarm<Ping> {
|
||||
let identity = identity::Keypair::generate_ed25519();
|
||||
let peer_id = identity.public().into_peer_id();
|
||||
|
||||
println!("peer id upon swarm setup: {}", peer_id);
|
||||
|
||||
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(SelectUpgrade::new(
|
||||
yamux::YamuxConfig::default(),
|
||||
MplexConfig::new(),
|
||||
))
|
||||
.timeout(Duration::from_secs(20))
|
||||
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
|
||||
.boxed(),
|
||||
Ping::default(),
|
||||
peer_id,
|
||||
)
|
||||
.executor(Box::new(|f| {
|
||||
tokio::spawn(f);
|
||||
}))
|
||||
.build()
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
use libp2p::core::muxing::StreamMuxerBox;
|
||||
use libp2p::core::upgrade::{SelectUpgrade, Version};
|
||||
use libp2p::identity::Keypair;
|
||||
use libp2p::mplex::MplexConfig;
|
||||
use libp2p::ping::{Ping, PingEvent, PingSuccess};
|
||||
use libp2p::swarm::{SwarmBuilder, SwarmEvent};
|
||||
use libp2p::{identity, noise, yamux, Multiaddr, Swarm, Transport};
|
||||
use libp2p_tor::duplex;
|
||||
use libp2p_tor::torut_ext::AuthenticatedConnectionExt;
|
||||
use noise::NoiseConfig;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use torut::control::AuthenticatedConn;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use libp2p_tor::duplex::TorutAsyncEventHandler;
|
||||
use libp2p::tcp::TokioTcpConfig;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("trace") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients
|
||||
.init();
|
||||
|
||||
let key = fixed_onion_identity();
|
||||
|
||||
let onion_address = key
|
||||
.public()
|
||||
.get_onion_address()
|
||||
.get_address_without_dot_onion();
|
||||
|
||||
tracing::info!("{}", onion_address);
|
||||
|
||||
let onion_port = 7654;
|
||||
|
||||
let mut client = AuthenticatedConn::new(9051).await.unwrap();
|
||||
|
||||
client
|
||||
.add_ephemeral_service(&key, onion_port, onion_port)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut swarm = new_swarm(client, key).await;
|
||||
let peer_id = *swarm.local_peer_id();
|
||||
|
||||
tracing::info!("Peer-ID: {}", peer_id);
|
||||
// TODO: Figure out what to with the port, we could also set it to 0 and then
|
||||
// imply it from the assigned port swarm.listen_on(Multiaddr::
|
||||
// from_str(format!("/onion3/{}:{}", onion_address,
|
||||
// onion_port).as_str()).unwrap()).unwrap();
|
||||
// swarm
|
||||
// .listen_on(
|
||||
// Multiaddr::from_str(format!("/onion3/{}:{}", onion_address, onion_port).as_str()).unwrap(),
|
||||
// )
|
||||
// .unwrap();
|
||||
swarm
|
||||
.listen_on(
|
||||
Multiaddr::from_str(format!("/ip4/127.0.0.1/tcp/{}", onion_port).as_str()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
match swarm.next_event().await {
|
||||
SwarmEvent::NewListenAddr(addr) => {
|
||||
tracing::info!("Listening on {}", addr);
|
||||
tracing::info!("Connection string: {}/p2p/{}", addr, peer_id);
|
||||
}
|
||||
SwarmEvent::ConnectionEstablished {
|
||||
peer_id, endpoint, ..
|
||||
} => {
|
||||
tracing::info!(
|
||||
"Connected to {} via {}",
|
||||
peer_id,
|
||||
endpoint.get_remote_address()
|
||||
);
|
||||
}
|
||||
SwarmEvent::Behaviour(PingEvent { result, peer }) => match result {
|
||||
Ok(PingSuccess::Pong) => {
|
||||
tracing::info!("Got pong from {}", peer);
|
||||
}
|
||||
Ok(PingSuccess::Ping { rtt }) => {
|
||||
tracing::info!("Pinged {} with rtt of {}s", peer, rtt.as_secs());
|
||||
}
|
||||
Err(failure) => {
|
||||
tracing::info!("Failed to ping {}: {}", peer, failure)
|
||||
}
|
||||
},
|
||||
event => {
|
||||
tracing::debug!("Swarm event: {:?}", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(client: AuthenticatedConn<tokio::net::TcpStream, TorutAsyncEventHandler>, key: TorSecretKeyV3) -> Swarm<Ping> {
|
||||
let identity = fixed_libp2p_identity();
|
||||
|
||||
SwarmBuilder::new(
|
||||
TokioTcpConfig::new().nodelay(true)
|
||||
.boxed()
|
||||
.upgrade(Version::V1)
|
||||
.authenticate(
|
||||
NoiseConfig::xx(
|
||||
noise::Keypair::<noise::X25519Spec>::new()
|
||||
.into_authentic(&identity)
|
||||
.unwrap(),
|
||||
)
|
||||
.into_authenticated(),
|
||||
)
|
||||
.multiplex(SelectUpgrade::new(
|
||||
yamux::YamuxConfig::default(),
|
||||
MplexConfig::new(),
|
||||
))
|
||||
.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 fixed_onion_identity() -> TorSecretKeyV3 {
|
||||
TorSecretKeyV3::generate()
|
||||
}
|
||||
|
||||
fn fixed_libp2p_identity() -> Keypair {
|
||||
// randomly venerated bytes, corresponding peer-id:
|
||||
// 12D3KooWHKqGyK4hVtf5BQY8GpbY6fSGKDZ8eBXMQ3H2RsdnKVzC
|
||||
let fixed_identity = [
|
||||
75, 146, 26, 107, 50, 252, 71, 2, 238, 224, 92, 112, 216, 238, 131, 57, 84, 9, 218, 120,
|
||||
195, 9, 129, 102, 42, 206, 165, 102, 32, 238, 158, 248,
|
||||
];
|
||||
|
||||
let key =
|
||||
identity::ed25519::SecretKey::from_bytes(fixed_identity).expect("we always pass 32 bytes");
|
||||
identity::Keypair::Ed25519(key.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,163 @@
|
||||
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;
|
||||
|
||||
pub 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>>>,
|
||||
key: TorSecretKeyV3,
|
||||
socks_port: u16,
|
||||
}
|
||||
|
||||
impl TorConfig {
|
||||
pub async fn new(
|
||||
mut client: AuthenticatedConn<tokio::net::TcpStream, TorutAsyncEventHandler>,
|
||||
key: TorSecretKeyV3,
|
||||
) -> Result<Self, Error> {
|
||||
let socks_port = client.get_socks_port().await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: TokioTcpConfig::new().nodelay(true).map_err(Error::InnerTransport),
|
||||
tor_client: Arc::new(Mutex::new(client)),
|
||||
key,
|
||||
socks_port,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_control_port(control_port: u16, key: TorSecretKeyV3) -> Result<Self, Error> {
|
||||
let client = AuthenticatedConn::new(control_port).await?;
|
||||
|
||||
Self::new(client, key).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));
|
||||
};
|
||||
|
||||
let key: TorSecretKeyV3 = self.key;
|
||||
let onion_bytes = key.public().get_onion_address().get_raw_bytes();
|
||||
let onion_port = onion.port();
|
||||
|
||||
if onion.hash() != &onion_bytes {
|
||||
return Err(TransportError::MultiaddrNotSupported(addr));
|
||||
}
|
||||
|
||||
let localhost_tcp_random_port_addr = format!("/ip4/127.0.0.1/tcp/{}", onion_port)
|
||||
.as_str()
|
||||
.parse()
|
||||
.expect("always a valid multiaddr");
|
||||
|
||||
let listener = self.inner.listen_on(localhost_tcp_random_port_addr)?;
|
||||
|
||||
let tor_client = self.tor_client.clone();
|
||||
|
||||
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) => {
|
||||
ListenerEvent::NewAddress(onion_multiaddress.clone())
|
||||
}
|
||||
ListenerEvent::Upgrade {
|
||||
upgrade,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
} => ListenerEvent::Upgrade {
|
||||
upgrade: upgrade.boxed(),
|
||||
local_addr,
|
||||
remote_addr,
|
||||
},
|
||||
// TODO: why was the constructed multiaddr used here?
|
||||
ListenerEvent::AddressExpired(adr) => {
|
||||
// TODO: even if so, why would we ignore it? Far more logical to
|
||||
// just use it... 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();
|
||||
|
||||
tracing::debug!(
|
||||
"Listening expired, removing onion {}",
|
||||
onion_address_without_dot_onion
|
||||
);
|
||||
|
||||
match tor_client
|
||||
.lock()
|
||||
.await
|
||||
.del_onion(&onion_address_without_dot_onion)
|
||||
.await
|
||||
{
|
||||
Ok(()) => ListenerEvent::AddressExpired(adr),
|
||||
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, listen: &Multiaddr, observed: &Multiaddr) -> Option<Multiaddr> {
|
||||
self.inner.address_translation(listen, observed)
|
||||
}
|
||||
}
|
@ -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, dbg!(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),
|
||||
InnerTransport(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,121 @@
|
||||
use std::borrow::Cow;
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
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> {
|
||||
tracing::debug!(
|
||||
"Attempting to add ephemeral service, onion port {}, local port {}",
|
||||
onion_port, local_port
|
||||
);
|
||||
|
||||
self.add_onion_v3(
|
||||
&key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut iter::once(&(
|
||||
onion_port,
|
||||
SocketAddr::new(IpAddr::from(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,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"]
|
@ -1,128 +1,21 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::future::Future;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use tokio::net::TcpStream;
|
||||
use torut::control::{AsyncEvent, AuthenticatedConn, ConnError, UnauthenticatedConn};
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
pub const DEFAULT_SOCKS5_PORT: u16 = 9050;
|
||||
pub const DEFAULT_CONTROL_PORT: u16 = 9051;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Client {
|
||||
socks5_address: SocketAddrV4,
|
||||
control_port_address: SocketAddr,
|
||||
}
|
||||
/// Check if Tor daemon is running on the given port.
|
||||
pub async fn is_daemon_running_on_port(port: u16) -> Result<()> {
|
||||
// Make sure you are running tor and this is your socks port
|
||||
let proxy = reqwest::Proxy::all(format!("socks5h://127.0.0.1:{}", port).as_str())
|
||||
.map_err(|_| anyhow!("tor proxy should be there"))?;
|
||||
let client = reqwest::Client::builder().proxy(proxy).build()?;
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socks5_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, DEFAULT_SOCKS5_PORT),
|
||||
control_port_address: SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::LOCALHOST,
|
||||
DEFAULT_CONTROL_PORT,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let res = client.get("https://check.torproject.org").send().await?;
|
||||
let text = res.text().await?;
|
||||
|
||||
impl Client {
|
||||
pub fn new(socks5_port: u16) -> Self {
|
||||
Self {
|
||||
socks5_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, socks5_port),
|
||||
control_port_address: SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::LOCALHOST,
|
||||
DEFAULT_CONTROL_PORT,
|
||||
)),
|
||||
}
|
||||
}
|
||||
pub fn with_control_port(self, control_port: u16) -> Self {
|
||||
Self {
|
||||
control_port_address: SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::LOCALHOST,
|
||||
control_port,
|
||||
)),
|
||||
..self
|
||||
}
|
||||
if !text.contains("Congratulations. This browser is configured to use Tor.") {
|
||||
bail!("Tor is currently not running")
|
||||
}
|
||||
|
||||
/// checks if tor is running
|
||||
pub async fn assert_tor_running(&self) -> Result<()> {
|
||||
// Make sure you are running tor and this is your socks port
|
||||
let proxy = reqwest::Proxy::all(format!("socks5h://{}", self.socks5_address).as_str())
|
||||
.map_err(|_| anyhow!("tor proxy should be there"))?;
|
||||
let client = reqwest::Client::builder().proxy(proxy).build()?;
|
||||
|
||||
let res = client.get("https://check.torproject.org").send().await?;
|
||||
let text = res.text().await?;
|
||||
|
||||
if !text.contains("Congratulations. This browser is configured to use Tor.") {
|
||||
bail!("Tor is currently not running")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn init_unauthenticated_connection(&self) -> Result<UnauthenticatedConn<TcpStream>> {
|
||||
// Connect to local tor service via control port
|
||||
let sock = TcpStream::connect(self.control_port_address).await?;
|
||||
let uc = UnauthenticatedConn::new(sock);
|
||||
Ok(uc)
|
||||
}
|
||||
|
||||
/// Create a new authenticated connection to your local Tor service
|
||||
pub async fn into_authenticated_client(self) -> Result<AuthenticatedClient> {
|
||||
self.assert_tor_running().await?;
|
||||
|
||||
let mut uc = self
|
||||
.init_unauthenticated_connection()
|
||||
.await
|
||||
.map_err(|_| anyhow!("Could not connect to Tor. Tor might not be running or the control port is incorrect."))?;
|
||||
|
||||
let tor_info = uc
|
||||
.load_protocol_info()
|
||||
.await
|
||||
.map_err(|_| anyhow!("Failed to load protocol info from Tor."))?;
|
||||
|
||||
let tor_auth_data = tor_info
|
||||
.make_auth_data()?
|
||||
.context("Failed to make Tor auth data.")?;
|
||||
|
||||
// Get an authenticated connection to the Tor via the Tor Controller protocol.
|
||||
uc.authenticate(&tor_auth_data)
|
||||
.await
|
||||
.map_err(|_| anyhow!("Failed to authenticate with Tor"))?;
|
||||
|
||||
Ok(AuthenticatedClient {
|
||||
inner: uc.into_authenticated().await,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tor_proxy_port(&self) -> u16 {
|
||||
self.socks5_address.port()
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = fn(AsyncEvent<'_>) -> Box<dyn Future<Output = Result<(), ConnError>> + Unpin>;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct AuthenticatedClient {
|
||||
inner: AuthenticatedConn<TcpStream, Handler>,
|
||||
}
|
||||
|
||||
impl AuthenticatedClient {
|
||||
/// Add an ephemeral tor service on localhost with the provided key
|
||||
/// `service_port` and `onion_port` can be different but don't have to as
|
||||
/// they are on different networks.
|
||||
pub async fn add_services(
|
||||
&mut self,
|
||||
services: &[(u16, SocketAddr)],
|
||||
tor_key: &TorSecretKeyV3,
|
||||
) -> Result<()> {
|
||||
let mut listeners = services.iter();
|
||||
self.inner
|
||||
.add_onion_v3(tor_key, false, false, false, None, &mut listeners)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Could not add onion service.: {:#?}", e))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -0,0 +1,121 @@
|
||||
use std::borrow::Cow;
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
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, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Failed to connect to Tor control port")]
|
||||
FailedToConnect(#[source] io::Error),
|
||||
#[error("Failed to read Tor auth-cookie filed")]
|
||||
FailedToReadCookieFile(#[source] io::Error),
|
||||
#[error("No authentication information could be found")]
|
||||
NoAuthData,
|
||||
#[error("Failed to communicate with Tor control port")]
|
||||
Connection(torut::control::ConnError),
|
||||
#[error("Failed to add hidden service")]
|
||||
FailedToAddHiddenService(torut::control::ConnError),
|
||||
#[error("Failed to parse port")]
|
||||
FailedToParsePort(#[from] ParseIntError),
|
||||
}
|
||||
|
||||
// TODO: Use #[from] once available: https://github.com/teawithsand/torut/issues/12
|
||||
impl From<torut::control::ConnError> for Error {
|
||||
fn from(e: torut::control::ConnError) -> Self {
|
||||
Error::Connection(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(Error::FailedToReadCookieFile)?
|
||||
.ok_or(Error::NoAuthData)?;
|
||||
|
||||
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> {
|
||||
tracing::debug!(
|
||||
"Adding ephemeral service, onion port {}, local port {}",
|
||||
onion_port,
|
||||
local_port
|
||||
);
|
||||
|
||||
self.add_onion_v3(
|
||||
&key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut iter::once(&(
|
||||
onion_port,
|
||||
SocketAddr::new(IpAddr::from(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?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in new issue