refactor: Move libp2p-community-tor into monorepo (#484)

* refactor: Move libp2p-community-tor into monorepo

* fmt
This commit is contained in:
Mohan 2025-07-29 20:10:15 +02:00 committed by GitHub
parent 69ddd2486d
commit db5d02ea3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1795 additions and 473 deletions

1199
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
resolver = "2"
members = [
"electrum-pool",
"libp2p-community-tor",
"monero-rpc",
"monero-rpc-pool",
"monero-seed",
@ -36,6 +37,13 @@ typeshare = "1.0"
url = { version = "2", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
# Tor/Arti crates
arti-client = { git = "https://github.com/eigenwallet/arti", rev = "5db9ecbd2872d76243dd62be887efd67e4609c87", default-features = false }
tor-cell = { git = "https://github.com/eigenwallet/arti", rev = "5db9ecbd2872d76243dd62be887efd67e4609c87" }
tor-hsservice = { git = "https://github.com/eigenwallet/arti", rev = "5db9ecbd2872d76243dd62be887efd67e4609c87" }
tor-proto = { git = "https://github.com/eigenwallet/arti", rev = "5db9ecbd2872d76243dd62be887efd67e4609c87" }
tor-rtcompat = { git = "https://github.com/eigenwallet/arti", rev = "5db9ecbd2872d76243dd62be887efd67e4609c87" }
[patch.crates-io]
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
jsonrpc_client = { git = "https://github.com/delta1/rust-jsonrpc-client.git", rev = "3b6081697cd616c952acb9c2f02d546357d35506" }

View file

@ -0,0 +1,47 @@
# 0.4.1
- Remove double features: See [PR 21]
- Correct typo in `src/lib.rs`: See [PR 21]
- Update `CHANGELOG.md`: See [PR 21]
[PR 21]: https://github.com/umgefahren/libp2p-tor/pull/21
# 0.4.0
## Changes
- Updated dependencies: See [PR 18]
- [`arti-client` to `v0.24.0`]
- [`libp2p` to `v0.53.0`]
- [`tor-rtcompat` to `v0.24.0`]
- Add tracing: See [PR 18]
- Update CI: See [PR 20]
- `actions/checkout` to `v4`
- Remove `arduino/setup-protoc`
## First time contributor
- @binarybaron
Thanks! :rocket:
[PR 18]: https://github.com/umgefahren/libp2p-tor/pull/18
[PR 20]: https://github.com/umgefahren/libp2p-tor/pull/20
# 0.3.0-alpha
- Updated dependencies: See [PR 6].
- [`arti-client` to `v0.8`
- Updated dependencies: See [PR 8].
- `libp2p-core` to `v0.39`
- `libp2p` to `0.51`
[PR 6]: https://github.com/umgefahren/libp2p-tor/pull/6
[PR 8]: https://github.com/umgefahren/libp2p-tor/pull/8
# 0.2.0-alpha
- Updated dependencies:
- [`libp2p` to `v0.50.0`](#2)
- [`libp2p-core` to `v0.38.0`](#3)

View file

@ -0,0 +1,47 @@
[package]
name = "libp2p-community-tor"
version = "0.5.0"
authors = ["umgefahren <hannes@umgefahren.xyz>"]
edition = "2021"
license = "MIT"
repository = "https://github.com/umgefahren/libp2p-tor"
resolver = "2"
description = "Tor transport for libp2p."
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
arti-client = { workspace = true, features = ["tokio", "rustls", "onion-service-client", "static-sqlite"] }
libp2p = { workspace = true, features = ["tokio", "tcp", "tls"] }
data-encoding = { version = "2.6.0" }
tor-cell = { workspace = true, optional = true }
tor-hsservice = { workspace = true, optional = true }
tor-proto = { workspace = true, optional = true }
tor-rtcompat = { workspace = true, features = ["tokio", "rustls"] }
tracing = { workspace = true }
[dev-dependencies]
libp2p = { workspace = true, features = ["tokio", "noise", "yamux", "ping", "macros", "tcp", "tls"] }
tokio = { workspace = true, features = ["macros"] }
tokio-test = "0.4.4"
tracing-subscriber = { workspace = true }
[features]
listen-onion-service = [
"arti-client/onion-service-service",
"dep:tor-hsservice",
"dep:tor-cell",
"dep:tor-proto",
]
[[example]]
name = "ping-onion"
required-features = ["listen-onion-service"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Hannes Furmans
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,51 @@
[![Continuous integration](https://github.com/umgefahren/libp2p-tor/actions/workflows/ci.yml/badge.svg)](https://github.com/umgefahren/libp2p-tor/actions/workflows/ci.yml)
[![docs.rs](https://img.shields.io/docsrs/libp2p-community-tor?style=flat-square)](https://docs.rs/libp2p-community-tor/latest)
[![Crates.io](https://img.shields.io/crates/v/libp2p-community-tor?style=flat-square)](https://crates.io/crates/libp2p-community-tor)
# libp2p Tor
Tor based transport for libp2p. Connect through the Tor network to TCP listeners.
Build on top of [Arti](https://gitlab.torproject.org/tpo/core/arti).
## New Feature
This crate supports, since #21 (thanks to @binarybaron), listening as a Tor hidden service as well as connecting to them.
## ⚠️ Misuse warning ⚠️ - read carefully before using
Although the sound of "Tor" might convey a sense of security it is _very_ easy to misuse this
crate and leaking private information while using. Study libp2p carefully and try to make sure
you fully understand it's current limits regarding privacy. I.e. using identify might already
render this transport obsolete.
This transport explicitly **doesn't** provide any enhanced privacy if it's just used like a regular transport.
Use with caution and at your own risk. **Don't** just blindly advertise Tor without fully understanding what you
are dealing with.
### Add to your dependencies
```bash
cargo add libp2p-community-tor
```
This crate uses tokio with rustls for its runtime and TLS implementation.
No other combinations are supported.
- [`rustls`](https://github.com/rustls/rustls)
- [`tokio`](https://github.com/tokio-rs/tokio)
### Example
```rust
let address = "/dns/www.torproject.org/tcp/1000".parse()?;
let mut transport = libp2p_community_tor::TorTransport::bootstrapped().await?;
// we have achieved tor connection
let _conn = transport.dial(address)?.await?;
```
### About
This crate originates in a PR to bring Tor support too rust-libp2p. Read more about it here: libp2p/rust-libp2p#2899
License: MIT

View file

@ -0,0 +1,159 @@
// Copyright 2022 Hannes Furmans
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! Ping-Onion example
//!
//! See ../src/tutorial.rs for a step-by-step guide building the example below.
//!
//! This example requires two seperate computers, one of which has to be reachable from the
//! internet.
//!
//! On the first computer run:
//! ```sh
//! cargo run --example ping
//! ```
//!
//! It will print the PeerId and the listening addresses, e.g. `Listening on
//! "/ip4/0.0.0.0/tcp/24915"`
//!
//! Make sure that the first computer is reachable under one of these ip addresses and port.
//!
//! On the second computer run:
//! ```sh
//! cargo run --example ping-onion -- /ip4/123.45.67.89/tcp/24915
//! ```
//!
//! The two nodes establish a connection, negotiate the ping protocol
//! and begin pinging each other over Tor.
use futures::StreamExt;
use libp2p::core::upgrade::Version;
use libp2p::Transport;
use libp2p::{
core::muxing::StreamMuxerBox,
identity, noise,
swarm::{NetworkBehaviour, SwarmEvent},
yamux, Multiaddr, PeerId, SwarmBuilder,
};
use libp2p_community_tor::{AddressConversion, TorTransport};
use std::error::Error;
use tor_hsservice::config::OnionServiceConfigBuilder;
/// Create a transport
/// Returns a tuple of the transport and the onion address we can instruct it to listen on
async fn onion_transport(
keypair: identity::Keypair,
) -> Result<
(
libp2p::core::transport::Boxed<(PeerId, libp2p::core::muxing::StreamMuxerBox)>,
Multiaddr,
),
Box<dyn Error>,
> {
let mut transport = TorTransport::bootstrapped()
.await?
.with_address_conversion(AddressConversion::IpAndDns);
// We derive the nickname for the onion address from the peer id
let svg_cfg = OnionServiceConfigBuilder::default()
.nickname(
keypair
.public()
.to_peer_id()
.to_base58()
.to_ascii_lowercase()
.parse()
.unwrap(),
)
.num_intro_points(3)
.build()
.unwrap();
let onion_listen_address = transport.add_onion_service(svg_cfg, 999).unwrap();
let auth_upgrade = noise::Config::new(&keypair)?;
let multiplex_upgrade = yamux::Config::default();
let transport = transport
.boxed()
.upgrade(Version::V1)
.authenticate(auth_upgrade)
.multiplex(multiplex_upgrade)
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
Ok((transport, onion_listen_address))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::fmt::init();
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {local_peer_id}");
let (transport, onion_listen_address) = onion_transport(local_key).await?;
let mut swarm = SwarmBuilder::with_new_identity()
.with_tokio()
.with_other_transport(|_| transport)
.unwrap()
.with_behaviour(|_| Behaviour {
ping: libp2p::ping::Behaviour::default(),
})
.unwrap()
.build();
// Dial the peer identified by the multi-address given as the second
// command-line argument, if any.
if let Some(addr) = std::env::args().nth(1) {
let remote: Multiaddr = addr.parse()?;
swarm.dial(remote)?;
println!("Dialed {addr}")
} else {
// If we are not dialing, we need to listen
// Tell the swarm to listen on a specific onion address
swarm.listen_on(onion_listen_address).unwrap();
}
loop {
match swarm.select_next_some().await {
SwarmEvent::ConnectionEstablished {
endpoint, peer_id, ..
} => {
println!("Connection established with {peer_id} on {endpoint:?}");
}
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
println!("Outgoing connection error with {peer_id:?}: {error:?}");
}
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"),
SwarmEvent::Behaviour(event) => println!("{event:?}"),
_ => {}
}
}
}
/// Our network behaviour.
#[derive(NetworkBehaviour)]
struct Behaviour {
ping: libp2p::ping::Behaviour,
}

View file

@ -0,0 +1,176 @@
// Copyright 2022 Hannes Furmans
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use arti_client::{DangerouslyIntoTorAddr, IntoTorAddr, TorAddr};
use libp2p::{core::multiaddr::Protocol, multiaddr::Onion3Addr, Multiaddr};
use std::net::SocketAddr;
/// "Dangerously" extract a Tor address from the provided [`Multiaddr`].
///
/// See [`DangerouslyIntoTorAddr`] for details around the safety / privacy considerations.
pub fn dangerous_extract(multiaddr: &Multiaddr) -> Option<TorAddr> {
if let Some(tor_addr) = safe_extract(multiaddr) {
return Some(tor_addr);
}
let mut protocols = multiaddr.into_iter();
let tor_addr = try_to_socket_addr(&protocols.next()?, &protocols.next()?)?
.into_tor_addr_dangerously()
.ok()?;
Some(tor_addr)
}
/// "Safely" extract a Tor address from the provided [`Multiaddr`].
///
/// See [`IntoTorAddr`] for details around the safety / privacy considerations.
pub fn safe_extract(multiaddr: &Multiaddr) -> Option<TorAddr> {
let mut protocols = multiaddr.into_iter();
let tor_addr = try_to_domain_and_port(&protocols.next()?, &protocols.next())?
.into_tor_addr()
.ok()?;
Some(tor_addr)
}
fn libp2p_onion_address_to_domain_and_port<'a>(
onion_address: &'a Onion3Addr<'_>,
) -> (&'a str, u16) {
// Here we convert from Onion3Addr to TorAddr
// We need to leak the string because it's a temporary string that would otherwise be freed
let hash = data_encoding::BASE32.encode(onion_address.hash());
let onion_domain = format!("{hash}.onion");
let onion_domain = Box::leak(onion_domain.into_boxed_str());
(onion_domain, onion_address.port())
}
fn try_to_domain_and_port<'a>(
maybe_domain: &'a Protocol,
maybe_port: &Option<Protocol>,
) -> Option<(&'a str, u16)> {
match (maybe_domain, maybe_port) {
(
Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain),
Some(Protocol::Tcp(port)),
) => Some((domain.as_ref(), *port)),
(Protocol::Onion3(domain), _) => Some(libp2p_onion_address_to_domain_and_port(domain)),
_ => None,
}
}
fn try_to_socket_addr(maybe_ip: &Protocol, maybe_port: &Protocol) -> Option<SocketAddr> {
match (maybe_ip, maybe_port) {
(Protocol::Ip4(ip), Protocol::Tcp(port)) => Some(SocketAddr::from((*ip, *port))),
(Protocol::Ip6(ip), Protocol::Tcp(port)) => Some(SocketAddr::from((*ip, *port))),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use arti_client::TorAddr;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn extract_correct_address_from_dns() {
let addresses = [
"/dns/ip.tld/tcp/10".parse().unwrap(),
"/dns4/dns.ip4.tld/tcp/11".parse().unwrap(),
"/dns6/dns.ip6.tld/tcp/12".parse().unwrap(),
];
let actual = addresses
.iter()
.filter_map(safe_extract)
.collect::<Vec<_>>();
assert_eq!(
&[
TorAddr::from(("ip.tld", 10)).unwrap(),
TorAddr::from(("dns.ip4.tld", 11)).unwrap(),
TorAddr::from(("dns.ip6.tld", 12)).unwrap(),
],
actual.as_slice()
);
}
#[test]
fn extract_correct_address_from_ips() {
let addresses = [
"/ip4/127.0.0.1/tcp/10".parse().unwrap(),
"/ip6/::1/tcp/10".parse().unwrap(),
];
let actual = addresses
.iter()
.filter_map(dangerous_extract)
.collect::<Vec<_>>();
assert_eq!(
&[
TorAddr::dangerously_from((Ipv4Addr::LOCALHOST, 10)).unwrap(),
TorAddr::dangerously_from((Ipv6Addr::LOCALHOST, 10)).unwrap(),
],
actual.as_slice()
);
}
#[test]
fn dangerous_extract_works_on_domains_too() {
let addresses = [
"/dns/ip.tld/tcp/10".parse().unwrap(),
"/ip4/127.0.0.1/tcp/10".parse().unwrap(),
"/ip6/::1/tcp/10".parse().unwrap(),
];
let actual = addresses
.iter()
.filter_map(dangerous_extract)
.collect::<Vec<_>>();
assert_eq!(
&[
TorAddr::from(("ip.tld", 10)).unwrap(),
TorAddr::dangerously_from((Ipv4Addr::LOCALHOST, 10)).unwrap(),
TorAddr::dangerously_from((Ipv6Addr::LOCALHOST, 10)).unwrap(),
],
actual.as_slice()
);
}
#[test]
fn detect_incorrect_address() {
let addresses = [
"/tcp/10/udp/12".parse().unwrap(),
"/dns/ip.tld/dns4/ip.tld/dns6/ip.tld".parse().unwrap(),
"/tcp/10/ip4/1.1.1.1".parse().unwrap(),
];
let all_correct = addresses.iter().map(safe_extract).all(|res| res.is_none());
assert!(
all_correct,
"During the parsing of the faulty addresses, there was an incorrectness"
);
}
}

View file

@ -0,0 +1,466 @@
// Copyright 2022 Hannes Furmans
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![warn(clippy::pedantic)]
#![deny(unsafe_code)]
//! Tor based transport for libp2p. Connect through the Tor network to TCP listeners.
//!
//! # ⚠️ Misuse warning ⚠️ - read carefully before using
//! Although the sound of "Tor" might convey a sense of security it is *very* easy to misuse this
//! crate and leaking private information while using. Study libp2p carefully and try to make sure
//! you fully understand it's current limits regarding privacy. I.e. using identify might already
//! render this transport obsolete.
//!
//! This transport explicitly **doesn't** provide any enhanced privacy if it's just used like a regular transport.
//! Use with caution and at your own risk. **Don't** just blindly advertise Tor without fully understanding what you
//! are dealing with.
//!
//! ## Runtime
//!
//! This crate uses tokio with rustls for its runtime and TLS implementation.
//! No other combinations are supported.
//!
//! ## Example
//! ```no_run
//! use libp2p::core::Transport;
//! # async fn test_func() -> Result<(), Box<dyn std::error::Error>> {
//! let address = "/dns/www.torproject.org/tcp/1000".parse()?;
//! let mut transport = libp2p_community_tor::TorTransport::bootstrapped().await?;
//! // we have achieved tor connection
//! let _conn = transport.dial(address)?.await?;
//! # Ok(())
//! # }
//! # tokio_test::block_on(test_func());
//! ```
use arti_client::{TorClient, TorClientBuilder};
use futures::future::BoxFuture;
use libp2p::{
core::transport::{ListenerId, TransportEvent},
Multiaddr, Transport, TransportError,
};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use thiserror::Error;
use tor_rtcompat::tokio::TokioRustlsRuntime;
// We only need these imports if the `listen-onion-service` feature is enabled
#[cfg(feature = "listen-onion-service")]
use std::collections::HashMap;
#[cfg(feature = "listen-onion-service")]
use std::str::FromStr;
#[cfg(feature = "listen-onion-service")]
use tor_cell::relaycell::msg::{Connected, End, EndReason};
#[cfg(feature = "listen-onion-service")]
use tor_hsservice::{
handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig,
RunningOnionService, StreamRequest,
};
#[cfg(feature = "listen-onion-service")]
use tor_proto::stream::IncomingStreamRequest;
mod address;
mod provider;
use address::{dangerous_extract, safe_extract};
pub use provider::TokioTorStream;
pub type TorError = arti_client::Error;
type PendingUpgrade = BoxFuture<'static, Result<TokioTorStream, TorTransportError>>;
#[cfg(feature = "listen-onion-service")]
type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>;
#[cfg(feature = "listen-onion-service")]
type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>;
/// Struct representing an onion address we are listening on for libp2p connections.
#[cfg(feature = "listen-onion-service")]
struct TorListener {
#[allow(dead_code)] // We need to own this to keep the RunningOnionService alive
/// The onion service we are listening on
service: Arc<RunningOnionService>,
/// The stream of status updates for the onion service
status_stream: OnionServiceStatusStream,
/// The stream incoming [`StreamRequest`]s
request_stream: OnionServiceStream,
/// The port we are listening on
port: u16,
/// The onion address we are listening on
onion_address: Multiaddr,
/// Whether we have already announced this address
announced: bool,
}
/// Mode of address conversion.
/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details
#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum AddressConversion {
/// Uses only DNS for address resolution (default).
#[default]
DnsOnly,
/// Uses IP and DNS for addresses.
IpAndDns,
}
pub struct TorTransport {
pub conversion_mode: AddressConversion,
/// The Tor client.
client: Arc<TorClient<TokioRustlsRuntime>>,
/// Onion services we are listening on.
#[cfg(feature = "listen-onion-service")]
listeners: HashMap<ListenerId, TorListener>,
/// Onion services we are running but currently not listening on
#[cfg(feature = "listen-onion-service")]
services: Vec<(Arc<RunningOnionService>, OnionServiceStream)>,
}
impl TorTransport {
/// Creates a new `TorClientBuilder`.
///
/// # Panics
/// Panics if the current runtime is not a `TokioRustlsRuntime`.
pub fn builder() -> TorClientBuilder<TokioRustlsRuntime> {
let runtime =
TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime");
TorClient::with_runtime(runtime)
}
/// Creates a bootstrapped `TorTransport`
///
/// # Errors
/// Could return error emitted during Tor bootstrap by Arti.
pub async fn bootstrapped() -> Result<Self, TorError> {
let builder = Self::builder();
let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?;
ret.bootstrap().await?;
Ok(ret)
}
/// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it.
///
/// # Errors
/// Could return error emitted during creation of the `TorClient`.
pub fn from_builder(
builder: &TorClientBuilder<TokioRustlsRuntime>,
conversion_mode: AddressConversion,
) -> Result<Self, TorError> {
let client = Arc::new(builder.create_unbootstrapped()?);
Ok(Self::from_client(client, conversion_mode))
}
/// Builds a `TorTransport` from an existing Arti `TorClient`.
pub fn from_client(
client: Arc<TorClient<TokioRustlsRuntime>>,
conversion_mode: AddressConversion,
) -> Self {
Self {
conversion_mode,
client,
#[cfg(feature = "listen-onion-service")]
listeners: HashMap::new(),
#[cfg(feature = "listen-onion-service")]
services: Vec::new(),
}
}
/// Bootstraps the `TorTransport` into the Tor network.
///
/// # Errors
/// Could return error emitted during bootstrap by Arti.
pub async fn bootstrap(&self) -> Result<(), TorError> {
self.client.bootstrap().await
}
/// Set the address conversion mode
#[must_use]
pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self {
self.conversion_mode = conversion_mode;
self
}
/// Call this function to instruct the transport to listen on a specific onion address
/// You need to call this function **before** calling `listen_on`
///
/// # Returns
/// Returns the Multiaddr of the onion address that the transport can be instructed to listen on
/// To actually listen on the address, you need to call [`listen_on`] with the returned address
///
/// # Errors
/// Returns an error if we cannot get the onion address of the service
#[cfg(feature = "listen-onion-service")]
pub fn add_onion_service(
&mut self,
svc_cfg: OnionServiceConfig,
port: u16,
) -> anyhow::Result<Multiaddr> {
let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?;
let request_stream = Box::pin(handle_rend_requests(request_stream));
let multiaddr = service
.onion_name()
.ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))?
.to_multiaddr(port);
self.services.push((service, request_stream));
Ok(multiaddr)
}
}
#[derive(Debug, Error)]
pub enum TorTransportError {
#[error(transparent)]
Client(#[from] TorError),
#[cfg(feature = "listen-onion-service")]
#[error(transparent)]
Service(#[from] tor_hsservice::ClientError),
#[cfg(feature = "listen-onion-service")]
#[error("Stream closed before receiving data")]
StreamClosed,
#[cfg(feature = "listen-onion-service")]
#[error("Stream port does not match listener port")]
StreamPortMismatch,
#[cfg(feature = "listen-onion-service")]
#[error("Onion service is broken")]
Broken,
}
#[cfg(feature = "listen-onion-service")]
trait HsIdExt {
fn to_multiaddr(&self, port: u16) -> Multiaddr;
}
#[cfg(feature = "listen-onion-service")]
impl HsIdExt for HsId {
/// Convert an `HsId` to a `Multiaddr`
fn to_multiaddr(&self, port: u16) -> Multiaddr {
let onion_domain = self.to_string();
let onion_without_dot_onion = onion_domain
.split('.')
.nth(0)
.expect("Display formatting of HsId to contain .onion suffix");
let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}");
Multiaddr::from_str(&multiaddress_string)
.expect("A valid onion address to be convertible to a Multiaddr")
}
}
impl Transport for TorTransport {
type Output = TokioTorStream;
type Error = TorTransportError;
type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>;
type ListenerUpgrade = PendingUpgrade;
#[cfg(not(feature = "listen-onion-service"))]
fn listen_on(
&mut self,
_id: ListenerId,
onion_address: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
// If the `listen-onion-service` feature is not enabled, we do not support listening
Err(TransportError::MultiaddrNotSupported(onion_address.clone()))
}
#[cfg(feature = "listen-onion-service")]
fn listen_on(
&mut self,
id: ListenerId,
onion_address: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
// If the address is not an onion3 address, return an error
let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0)
else {
return Err(TransportError::MultiaddrNotSupported(onion_address.clone()));
};
// Find the running onion service that matches the requested address
// If we find it, remove it from [`services`] and insert it into [`listeners`]
let position = self
.services
.iter()
.position(|(service, _)| {
service.onion_name().map_or(false, |name| {
name.to_multiaddr(address.port()) == onion_address
})
})
.ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?;
let (service, request_stream) = self.services.remove(position);
let status_stream = Box::pin(service.status_events());
self.listeners.insert(
id,
TorListener {
service,
request_stream,
onion_address: onion_address.clone(),
port: address.port(),
status_stream,
announced: false,
},
);
Ok(())
}
// We do not support removing listeners if the `listen-onion-service` feature is not enabled
#[cfg(not(feature = "listen-onion-service"))]
fn remove_listener(&mut self, _id: ListenerId) -> bool {
false
}
#[cfg(feature = "listen-onion-service")]
fn remove_listener(&mut self, id: ListenerId) -> bool {
// Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore)
// However, we will not stop the onion service itself because we might want to reuse it later
// The onion service will be stopped when the transport is dropped
if let Some(listener) = self.listeners.remove(&id) {
self.services
.push((listener.service, listener.request_stream));
return true;
}
false
}
fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
let maybe_tor_addr = match self.conversion_mode {
AddressConversion::DnsOnly => safe_extract(&addr),
AddressConversion::IpAndDns => dangerous_extract(&addr),
};
let tor_address =
maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?;
let onion_client = self.client.clone();
Ok(Box::pin(async move {
let stream = onion_client.connect(tor_address).await?;
tracing::debug!(%addr, "Established connection to peer through Tor");
Ok(TokioTorStream::from(stream))
}))
}
fn dial_as_listener(
&mut self,
addr: Multiaddr,
) -> Result<Self::Dial, TransportError<Self::Error>> {
self.dial(addr)
}
fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option<Multiaddr> {
None
}
#[cfg(not(feature = "listen-onion-service"))]
fn poll(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
// If the `listen-onion-service` feature is not enabled, we do not support listening
Poll::Pending
}
#[cfg(feature = "listen-onion-service")]
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
for (listener_id, listener) in &mut self.listeners {
// Check if the service has any new statuses
if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) {
tracing::debug!(
status = ?status.state(),
address = listener.onion_address.to_string(),
"Onion service status changed"
);
}
// Check if we have already announced this address, if not, do it now
if !listener.announced {
listener.announced = true;
// We announce the address here to the swarm even though we technically cannot guarantee
// that the address is reachable yet from the outside. We might not have registered the
// onion service fully yet (introduction points, hsdir, ...)
//
// However, we need to announce it now because otherwise libp2p might not poll the listener
// again and we will not be able to announce it later.
// TODO: Find out why this is the case, if this is intended behaviour or a bug
return Poll::Ready(TransportEvent::NewAddress {
listener_id: *listener_id,
listen_addr: listener.onion_address.clone(),
});
}
match listener.request_stream.as_mut().poll_next(cx) {
Poll::Ready(Some(request)) => {
let port = listener.port;
let upgrade: PendingUpgrade = Box::pin(async move {
// Check if the port matches what we expect
if let IncomingStreamRequest::Begin(begin) = request.request() {
if begin.port() != port {
// Reject the connection with CONNECTREFUSED
request
.reject(End::new_with_reason(EndReason::CONNECTREFUSED))
.await?;
return Err(TorTransportError::StreamPortMismatch);
}
}
// Accept the stream and forward it to the swarm
let data_stream = request.accept(Connected::new_empty()).await?;
Ok(TokioTorStream::from(data_stream))
});
return Poll::Ready(TransportEvent::Incoming {
listener_id: *listener_id,
upgrade,
local_addr: listener.onion_address.clone(),
send_back_addr: listener.onion_address.clone(),
});
}
// The stream has ended
// This means that the onion service was shut down, and we will not receive any more connections on it
Poll::Ready(None) => {
return Poll::Ready(TransportEvent::ListenerClosed {
listener_id: *listener_id,
reason: Ok(()),
});
}
Poll::Pending => {}
}
}
Poll::Pending
}
}

View file

@ -0,0 +1,86 @@
// Copyright 2022 Hannes Furmans
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use arti_client::DataStream;
use futures::{AsyncRead, AsyncWrite};
use tokio::io::{AsyncRead as TokioAsyncRead, AsyncWrite as TokioAsyncWrite, ReadBuf};
#[derive(Debug)]
pub struct TokioTorStream {
inner: DataStream,
}
impl From<DataStream> for TokioTorStream {
fn from(inner: DataStream) -> Self {
Self { inner }
}
}
impl AsyncRead for TokioTorStream {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
let mut read_buf = ReadBuf::new(buf);
futures::ready!(TokioAsyncRead::poll_read(
std::pin::Pin::new(&mut self.inner),
cx,
&mut read_buf
))?;
std::task::Poll::Ready(Ok(read_buf.filled().len()))
}
}
impl AsyncWrite for TokioTorStream {
#[inline]
fn poll_write(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
TokioAsyncWrite::poll_write(std::pin::Pin::new(&mut self.inner), cx, buf)
}
#[inline]
fn poll_flush(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
TokioAsyncWrite::poll_flush(std::pin::Pin::new(&mut self.inner), cx)
}
#[inline]
fn poll_close(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
TokioAsyncWrite::poll_shutdown(std::pin::Pin::new(&mut self.inner), cx)
}
#[inline]
fn poll_write_vectored(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> std::task::Poll<std::io::Result<usize>> {
TokioAsyncWrite::poll_write_vectored(std::pin::Pin::new(&mut self.inner), cx, bufs)
}
}

View file

@ -1,5 +1,5 @@
[toolchain]
# also update this in the readme, changelog, and github actions
channel = "1.85"
channel = "1.87"
components = ["clippy"]
targets = ["armv7-unknown-linux-gnueabihf"]

View file

@ -13,7 +13,7 @@ tauri = ["dep:tauri", "dep:dfx-swiss-sdk"]
[dependencies]
anyhow = { workspace = true }
arti-client = { version = "0.25.0", features = ["static-sqlite", "tokio", "rustls", "onion-service-service"], default-features = false }
arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] }
async-compression = { version = "0.3", features = ["bzip2", "tokio"] }
async-trait = "0.1"
asynchronous-codec = "0.7.0"
@ -42,7 +42,7 @@ fns = "0.0.7"
futures = { workspace = true }
hex = { workspace = true }
libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] }
libp2p-community-tor = { git = "https://github.com/umgefahren/libp2p-tor", rev = "e6b913e0f1ac1fc90b3ee4dd31b5511140c4a9af", features = ["listen-onion-service"] }
libp2p-community-tor = { path = "../libp2p-community-tor", features = ["listen-onion-service"] }
moka = { version = "0.12", features = ["sync", "future"] }
monero = { workspace = true }
monero-rpc = { path = "../monero-rpc" }
@ -80,7 +80,7 @@ time = "0.3"
tokio = { workspace = true, features = ["process", "fs", "net", "parking_lot", "rt"] }
tokio-tungstenite = { version = "0.15", features = ["rustls-tls"] }
tokio-util = { version = "0.7", features = ["io", "codec", "rt"] }
tor-rtcompat = { version = "0.25.0", features = ["tokio"] }
tor-rtcompat = { workspace = true, features = ["tokio"] }
tower = { version = "0.4.13", features = ["full"] }
tower-http = { version = "0.3.4", features = ["full"] }
tracing = { workspace = true }