This PR does a few things. * It adds a TorTransport which either dials through Tor's socks5 proxy or via clearnet. * It enables ASB to register hidden services for each network it is listening on. We assume that we only care about different ports and re-use the same onion-address for all of them. The ASB requires to have access to Tor's control port. * It adds support to dial through a local Tor socks5 proxy. We assume that Tor is always available on localhost. Swap cli only requires Tor to be running so that it can send messages via Tor's socks5 proxy. * It adds a new e2e test which swaps through Tor. For this we assume that Tor is currently running on localhost. All other tests are running via clear net.pull/438/head
parent
e262345b4f
commit
632293cf91
@ -0,0 +1,238 @@
|
||||
use data_encoding::BASE32;
|
||||
use futures::future::Ready;
|
||||
use futures::prelude::*;
|
||||
use libp2p::core::multiaddr::{Multiaddr, Protocol};
|
||||
use libp2p::core::transport::TransportError;
|
||||
use libp2p::core::Transport;
|
||||
use libp2p::tcp::tokio::{Tcp, TcpStream};
|
||||
use libp2p::tcp::{GenTcpConfig, TcpListenStream, TokioTcpConfig};
|
||||
use std::cmp::Ordering;
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::pin::Pin;
|
||||
use tokio_socks::tcp::Socks5Stream;
|
||||
use tokio_socks::IntoTargetAddr;
|
||||
|
||||
/// Represents the configuration for a TCP/IP transport capability for libp2p.
|
||||
#[derive(Clone)]
|
||||
pub struct TorTcpConfig {
|
||||
inner: GenTcpConfig<Tcp>,
|
||||
/// Tor SOCKS5 proxy port number.
|
||||
socks_port: u16,
|
||||
}
|
||||
|
||||
impl TorTcpConfig {
|
||||
pub fn new(tcp: TokioTcpConfig, socks_port: u16) -> Self {
|
||||
Self {
|
||||
inner: tcp,
|
||||
socks_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for TorTcpConfig {
|
||||
type Output = TcpStream;
|
||||
type Error = io::Error;
|
||||
type Listener = TcpListenStream<Tcp>;
|
||||
type ListenerUpgrade = Ready<Result<Self::Output, Self::Error>>;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn listen_on(self, addr: Multiaddr) -> Result<Self::Listener, TransportError<Self::Error>> {
|
||||
self.inner.listen_on(addr)
|
||||
}
|
||||
|
||||
// dials via Tor's socks5 proxy if configured and if the provided address is an
|
||||
// onion address. or it falls back to Tcp dialling
|
||||
fn dial(self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
async fn do_tor_dial(socks_port: u16, dest: String) -> Result<TcpStream, io::Error> {
|
||||
tracing::trace!("Connecting through Tor proxy to address: {}", dest);
|
||||
let stream = connect_to_socks_proxy(dest, socks_port)
|
||||
.await
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?;
|
||||
tracing::trace!("Connection through Tor established");
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
match to_address_string(addr.clone()) {
|
||||
Some(tor_address_string) => {
|
||||
Ok(Box::pin(do_tor_dial(self.socks_port, tor_address_string)))
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Address {} could not be formatted. Dialling via clear net",
|
||||
addr
|
||||
);
|
||||
self.inner.dial(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn address_translation(&self, listen: &Multiaddr, observed: &Multiaddr) -> Option<Multiaddr> {
|
||||
self.inner.address_translation(listen, observed)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn to_address_string(multi: Multiaddr) -> Option<String> {
|
||||
let components = multi.iter();
|
||||
for protocol in components {
|
||||
match protocol {
|
||||
Protocol::Onion(addr, port) => {
|
||||
tracing::warn!("Onion service v2 is being deprecated, consider upgrading to v3");
|
||||
return Some(format!(
|
||||
"{}.onion:{}",
|
||||
BASE32.encode(addr.as_ref()).to_lowercase(),
|
||||
port
|
||||
));
|
||||
}
|
||||
Protocol::Onion3(addr) => {
|
||||
return Some(format!(
|
||||
"{}.onion:{}",
|
||||
BASE32.encode(addr.hash()).to_lowercase(),
|
||||
addr.port()
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with non-onion addresses
|
||||
let protocols = multi.iter().collect::<Vec<_>>();
|
||||
let address_string = protocols
|
||||
.iter()
|
||||
.filter_map(|protocol| match protocol {
|
||||
Protocol::Ip4(addr) => Some(format!("{}", addr)),
|
||||
Protocol::Ip6(addr) => Some(format!("{}", addr)),
|
||||
Protocol::Dns(addr) => Some(format!("{}", addr)),
|
||||
Protocol::Dns4(addr) => Some(format!("{}", addr)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if address_string.is_empty() {
|
||||
tracing::warn!(
|
||||
"Could not format address {}. Please consider reporting this issue. ",
|
||||
multi
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let address_string = address_string
|
||||
.get(0)
|
||||
.expect("Valid multiaddr consist out of max 1 address")
|
||||
.clone();
|
||||
|
||||
// check for port
|
||||
let port = protocols
|
||||
.iter()
|
||||
.filter_map(|protocol| match protocol {
|
||||
Protocol::Tcp(port) => Some(format!("{}", port)),
|
||||
Protocol::Udp(port) => Some(format!("{}", port)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match port.len().cmp(&1) {
|
||||
Ordering::Greater => {
|
||||
tracing::warn!(
|
||||
"Did not expect more than 1 port in address {}. Please consider reporting this issue.",
|
||||
multi
|
||||
);
|
||||
Some(address_string)
|
||||
}
|
||||
Ordering::Less => Some(address_string),
|
||||
Ordering::Equal => Some(format!(
|
||||
"{}:{}",
|
||||
address_string,
|
||||
port.get(0)
|
||||
.expect("Already verified the length of the vec.")
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the SOCKS5 proxy socket.
|
||||
async fn connect_to_socks_proxy<'a>(
|
||||
dest: impl IntoTargetAddr<'a>,
|
||||
port: u16,
|
||||
) -> Result<TcpStream, tokio_socks::Error> {
|
||||
let sock = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port));
|
||||
let stream = Socks5Stream::connect(sock, dest).await?;
|
||||
Ok(TcpStream(stream.into_inner()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use crate::network::tor_transport::to_address_string;
|
||||
|
||||
#[test]
|
||||
fn test_tor_address_string() {
|
||||
let address =
|
||||
"/onion3/oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did:1024/p2p/12D3KooWPD4uHN74SHotLN7VCH7Fm8zZgaNVymYcpeF1fpD2guc9"
|
||||
;
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a multi formatted address.");
|
||||
assert_eq!(
|
||||
address_string,
|
||||
"oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did.onion:1024"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_to_address_string_should_be_some() {
|
||||
let address = "/ip4/127.0.0.1/tcp/7777";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "127.0.0.1:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip6_to_address_string_should_be_some() {
|
||||
let address = "/ip6/2001:db8:85a3:8d3:1319:8a2e:370:7348/tcp/7777";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "2001:db8:85a3:8d3:1319:8a2e:370:7348:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_to_address_string_should_be_some() {
|
||||
let address = "/ip4/127.0.0.1/udp/7777";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "127.0.0.1:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ws_to_address_string_should_be_some() {
|
||||
let address = "/ip4/127.0.0.1/tcp/7777/ws";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "127.0.0.1:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns4_to_address_string_should_be_some() {
|
||||
let address = "/dns4/randomdomain.com/tcp/7777";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "randomdomain.com:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns_to_address_string_should_be_some() {
|
||||
let address = "/dns/randomdomain.com/tcp/7777";
|
||||
let address_string =
|
||||
to_address_string(address.parse().unwrap()).expect("To be a formatted multi address. ");
|
||||
assert_eq!(address_string, "randomdomain.com:7777");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dnsaddr_to_address_string_should_be_none() {
|
||||
let address = "/dnsaddr/randomdomain.com";
|
||||
let address_string = to_address_string(address.parse().unwrap());
|
||||
assert_eq!(address_string, None);
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use tokio::net::TcpStream;
|
||||
use torut::control::{AsyncEvent, AuthenticatedConn, ConnError, UnauthenticatedConn};
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_service(
|
||||
&mut self,
|
||||
service_port: u16,
|
||||
onion_port: u16,
|
||||
tor_key: &TorSecretKeyV3,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.add_onion_v3(
|
||||
tor_key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut [(
|
||||
onion_port,
|
||||
SocketAddr::new(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1)), service_port),
|
||||
)]
|
||||
.iter(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Could not add onion service.: {:#?}", e))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
Loading…
Reference in new issue