diff --git a/CHANGELOG.md b/CHANGELOG.md index 165517b0e14..65b704c4aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [`libp2p-wasm-ext` CHANGELOG](transports/wasm-ext/CHANGELOG.md) - [`libp2p-websocket` CHANGELOG](transports/websocket/CHANGELOG.md) - [`libp2p-tls` CHANGELOG](transports/tls/CHANGELOG.md) +- [`libp2p-onion` CHANGELOG](transports/onion/CHANGELOG.md) ## Multiplexers @@ -45,8 +46,9 @@ # `libp2p` facade crate -# 0.50.0 - [unreleased] +# 0.50.0 [unreleased] +- New `libp2p-onion` crate. See [PR 2899]. - Introduce [`libp2p-tls` `v0.1.0-alpha`](transports/tls/CHANGELOG.md#010-alpha). See [PR 2945]. - Introduce [`libp2p-quic` `v0.7.0-alpha`](transports/quic/CHANGELOG.md#070-alpha). See [PR 2289]. - Remove deprecated features: `tcp-tokio`, `mdns-tokio`, `dns-tokio`, `tcp-async-io`, `mdns-async-io`, `dns-async-std`. @@ -82,6 +84,7 @@ - Update to [`libp2p-websocket` `v0.40.0`](transports/websocket/CHANGELOG.md#0400). - Update to [`libp2p-yamux` `v0.42.0`](muxers/yamux/CHANGELOG.md#0420). +[PR 2899]: https://github.com/libp2p/rust-libp2p/pull/2899 [PR 2945]: https://github.com/libp2p/rust-libp2p/pull/2945 [PR 3001]: https://github.com/libp2p/rust-libp2p/pull/3001 [PR 2945]: https://github.com/libp2p/rust-libp2p/pull/2945 diff --git a/Cargo.toml b/Cargo.toml index 7ba80244ab6..86b36627972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ full = [ "metrics", "mplex", "noise", + "onion", + "onion-native-tls", + "onion-rustls", "ping", "plaintext", "pnet", @@ -49,7 +52,7 @@ full = [ "yamux", ] -async-std = ["libp2p-swarm/async-std", "libp2p-mdns?/async-io", "libp2p-tcp?/async-io", "libp2p-dns?/async-std", "libp2p-quic?/async-std"] +async-std = ["libp2p-swarm/async-std", "libp2p-mdns?/async-io", "libp2p-tcp?/async-io", "libp2p-dns?/async-std", "libp2p-quic?/async-std", "libp2p-onion?/async-std"] autonat = ["dep:libp2p-autonat"] dcutr = ["dep:libp2p-dcutr", "libp2p-metrics?/dcutr"] deflate = ["dep:libp2p-deflate"] @@ -65,6 +68,9 @@ tls = ["dep:libp2p-tls"] metrics = ["dep:libp2p-metrics"] mplex = ["dep:libp2p-mplex"] noise = ["dep:libp2p-noise"] +onion = ["dep:libp2p-onion"] +onion-native-tls = ["dep:libp2p-onion", "libp2p-onion?/native-tls"] +onion-rustls = ["dep:libp2p-onion", "libp2p-onion?/rustls"] ping = ["dep:libp2p-ping", "libp2p-metrics?/ping"] plaintext = ["dep:libp2p-plaintext"] pnet = ["dep:libp2p-pnet"] @@ -76,7 +82,7 @@ rsa = ["libp2p-core/rsa"] secp256k1 = ["libp2p-core/secp256k1"] serde = ["libp2p-core/serde", "libp2p-kad?/serde", "libp2p-gossipsub?/serde"] tcp = ["dep:libp2p-tcp"] -tokio = ["libp2p-swarm/tokio", "libp2p-mdns?/tokio", "libp2p-tcp?/tokio", "libp2p-dns?/tokio", "libp2p-quic?/tokio", "libp2p-webrtc?/tokio"] +tokio = ["libp2p-swarm/tokio", "libp2p-mdns?/tokio", "libp2p-tcp?/tokio", "libp2p-dns?/tokio", "libp2p-quic?/tokio", "libp2p-webrtc?/tokio", "libp2p-onion?/tokio"] uds = ["dep:libp2p-uds"] wasm-bindgen = ["futures-timer/wasm-bindgen", "instant/wasm-bindgen", "getrandom/js"] wasm-ext = ["dep:libp2p-wasm-ext"] @@ -85,6 +91,7 @@ webrtc = ["dep:libp2p-webrtc", "libp2p-webrtc?/pem"] websocket = ["dep:libp2p-websocket"] yamux = ["dep:libp2p-yamux"] + [dependencies] bytes = "1" futures = "0.3.1" @@ -120,6 +127,7 @@ smallvec = "1.6.1" libp2p-deflate = { version = "0.38.0", path = "transports/deflate", optional = true } libp2p-dns = { version = "0.38.0", path = "transports/dns", optional = true } libp2p-mdns = { version = "0.42.0", path = "protocols/mdns", optional = true } +libp2p-onion = { version = "0.1.0-alpha", path = "transports/onion", default-features = false, optional = true } libp2p-quic = { version = "0.7.0-alpha", path = "transports/quic", optional = true } libp2p-tcp = { version = "0.38.0", path = "transports/tcp", optional = true } libp2p-tls = { version = "0.1.0-alpha", path = "transports/tls", optional = true } @@ -139,37 +147,39 @@ tokio = { version = "1.15", features = ["io-util", "io-std", "macros", "rt", "rt [workspace] members = [ "core", + "misc/keygen", "misc/metrics", "misc/multistream-select", - "misc/rw-stream-sink", - "misc/keygen", "misc/prost-codec", "misc/quickcheck-ext", + "misc/rw-stream-sink", "muxers/mplex", - "muxers/yamux", "muxers/test-harness", - "protocols/dcutr", + "muxers/yamux", "protocols/autonat", + "protocols/dcutr", "protocols/floodsub", "protocols/gossipsub", - "protocols/rendezvous", "protocols/identify", "protocols/kad", "protocols/mdns", "protocols/ping", "protocols/relay", + "protocols/rendezvous", "protocols/request-response", "swarm", "swarm-derive", "transports/deflate", "transports/dns", "transports/noise", - "transports/tls", + "transports/onion", "transports/plaintext", "transports/pnet", "transports/quic", "transports/tcp", + "transports/tls", "transports/uds", + "transports/wasm-ext", "transports/websocket", "transports/wasm-ext", "transports/webrtc" @@ -195,6 +205,10 @@ required-features = ["full"] name = "ipfs-private" required-features = ["full"] +[[example]] +name = "ping-onion" +required-features = ["full"] + [[example]] name = "ipfs-kad" required-features = ["full"] diff --git a/deny.toml b/deny.toml index 6634887128e..989a2892f9c 100644 --- a/deny.toml +++ b/deny.toml @@ -78,6 +78,8 @@ exceptions = [ { allow = ["ISC", "MIT", "OpenSSL"], name = "ring" }, # libp2p is not re-distributing unicode tables data by itself { allow = ["MIT", "Apache-2.0", "Unicode-DFS-2016"], name = "unicode-ident" }, + # libp2p is not re-distributing unicode tables data by iteself + { allow = ["Unicode-DFS-2016"], name = "tinystr"}, ] # Some crates don't have (easily) machine readable licensing information, @@ -88,6 +90,11 @@ name = "ring" expression = "ISC AND MIT AND OpenSSL" license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] +[[licenses.clarify]] +name = "bounded-vec-deque" +expression = "GPL-3.0 OR BSD-3-Clause" +license-files = [{ path = "LICENSE", hash = 0x40145f22 }] + [licenses.private] # If true, ignores workspace crates that aren't published, or are only # published to private registries diff --git a/examples/README.md b/examples/README.md index e791ebb803a..5dd31deda00 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,6 +48,12 @@ A set of examples showcasing how to use rust-libp2p. Tutorial on how to overcome firewalls and NATs with libp2p’s hole punching mechanism. +## Special transports + +- [Ping over Onion](ping-onion.rs) + + Client counterpart to normal ping, that routes traffic over Tor. + ## Integration into a larger application - [File sharing application](./file-sharing.rs) diff --git a/examples/ping-onion.rs b/examples/ping-onion.rs new file mode 100644 index 00000000000..c15d68ce478 --- /dev/null +++ b/examples/ping-onion.rs @@ -0,0 +1,120 @@ +// 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::prelude::*; +use libp2p::onion::AddressConversion; +use libp2p::swarm::{keep_alive, Swarm, SwarmEvent}; +use libp2p::{ + core::upgrade, identity, mplex, noise, onion, ping, yamux, Multiaddr, NetworkBehaviour, PeerId, + Transport, +}; +use std::error::Error; + +async fn onion_transport( + keypair: identity::Keypair, +) -> Result< + libp2p_core::transport::Boxed<(PeerId, libp2p_core::muxing::StreamMuxerBox)>, + Box, +> { + use std::time::Duration; + + let transport = onion::AsyncStdNativeTlsOnionTransport::bootstrapped() + .await? + .with_address_conversion(AddressConversion::IpAndDns); + Ok(transport + .upgrade(upgrade::Version::V1) + .authenticate( + noise::NoiseAuthenticated::xx(&keypair) + .expect("Signing libp2p-noise static DH keypair failed."), + ) + .multiplex(upgrade::SelectUpgrade::new( + yamux::YamuxConfig::default(), + mplex::MplexConfig::default(), + )) + .timeout(Duration::from_secs(20)) + .boxed()) +} + +#[async_std::main] +async fn main() -> Result<(), Box> { + let addr = std::env::args().nth(1).expect("no multiaddr given"); + 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_transport(local_key).await?; + + let mut swarm = Swarm::new(transport, Behaviour::default(), local_peer_id); + + // Dial the peer identified by the multi-address given as the second + // command-line argument, if any. + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + println!("Dialed {}", addr); + + loop { + match swarm.select_next_some().await { + SwarmEvent::ConnectionEstablished { endpoint, .. } => { + let endpoint_addr = endpoint.get_remote_address(); + println!("Connection established to {:?}", endpoint_addr); + } + SwarmEvent::OutgoingConnectionError { error, .. } => { + println!("Error establishing outgoing connection: {:?}", error) + } + SwarmEvent::Behaviour(event) => println!("{:?}", event), + _ => {} + } + } +} + +/// Our network behaviour. +/// +/// For illustrative purposes, this includes the [`KeepAlive`](behaviour::KeepAlive) behaviour so a continuous sequence of +/// pings can be observed. +#[derive(NetworkBehaviour, Default)] +struct Behaviour { + keep_alive: keep_alive::Behaviour, + ping: ping::Behaviour, +} diff --git a/src/lib.rs b/src/lib.rs index d4d72eeaf7d..64e8e63f9a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,11 @@ pub use libp2p_mplex as mplex; #[cfg(feature = "noise")] #[doc(inline)] pub use libp2p_noise as noise; +#[cfg(feature = "onion")] +#[cfg_attr(docsrs, doc(cfg(feature = "onion")))] +#[cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")))] +#[doc(inline)] +pub use libp2p_onion as onion; #[cfg(feature = "ping")] #[doc(inline)] pub use libp2p_ping as ping; diff --git a/transports/onion/CHANGELOG.md b/transports/onion/CHANGELOG.md new file mode 100644 index 00000000000..f9546572918 --- /dev/null +++ b/transports/onion/CHANGELOG.md @@ -0,0 +1,6 @@ +# 0.37.0 - [unreleased] + +- Initial Implementation. See [PR 2899]. + +[PR 2899]: https://github.com/libp2p/rust-libp2p/pull/2899 + diff --git a/transports/onion/Cargo.toml b/transports/onion/Cargo.toml new file mode 100644 index 00000000000..183e04b5353 --- /dev/null +++ b/transports/onion/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "libp2p-onion" +version = "0.1.0-alpha" +edition = "2021" +license = "MIT" +resolver = "2" + +[dependencies] +arti-client = { version = "0.7", default-features = false } +async-std-crate = { package = "async-std", version = "1", optional = true, default-features = false } +futures = "0.3" +libp2p-core = { version = "0.38", path = "../../core" } +thiserror = "1" +tokio-crate = { package = "tokio", version = "1", optional = true, default-features = false } +tor-rtcompat = "0.7" + +[dev-dependencies] +libp2p = { version = "0.50", path = "../../" } +tokio-crate = { package = "tokio", version = "1", features = ["rt", "macros"] } +async-std-crate = { package = "async-std", version = "1", features = ["attributes"] } + +[features] +tokio = ["arti-client/tokio", "dep:tokio-crate"] +async-std = ["arti-client/async-std", "dep:async-std-crate"] +native-tls = ["arti-client/native-tls"] +rustls = ["arti-client/rustls"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +rustc-args = ["--cfg", "docsrs"] diff --git a/transports/onion/src/address.rs b/transports/onion/src/address.rs new file mode 100644 index 00000000000..564f97193f5 --- /dev/null +++ b/transports/onion/src/address.rs @@ -0,0 +1,167 @@ +// 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}; +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_tor_address(multiaddr: &Multiaddr) -> Option { + if let Some(tor_addr) = safe_extract_tor_address(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_tor_address(multiaddr: &Multiaddr) -> Option { + 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 try_to_domain_and_port<'a, 'b>( + maybe_domain: &'a Protocol, + maybe_port: &'b Protocol, +) -> Option<(&'a str, u16)> { + match (maybe_domain, maybe_port) { + ( + Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain), + Protocol::Tcp(port), + ) => Some((domain.as_ref(), *port)), + _ => None, + } +} + +fn try_to_socket_addr(maybe_ip: &Protocol, maybe_port: &Protocol) -> Option { + 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_tor_address) + .collect::>(); + + 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_tor_address) + .collect::>(); + + 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_tor_address) + .collect::>(); + + 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_tor_address) + .all(|res| res.is_none()); + + assert!( + all_correct, + "During the parsing of the faulty addresses, there was an incorrectness" + ); + } +} diff --git a/transports/onion/src/lib.rs b/transports/onion/src/lib.rs new file mode 100644 index 00000000000..e2adf239a76 --- /dev/null +++ b/transports/onion/src/lib.rs @@ -0,0 +1,246 @@ +// 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. + +#![doc(html_logo_url = "https://libp2p.io/img/logo_small.png")] +#![doc(html_favicon_url = "https://libp2p.io/img/favicon.png")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +//! Tor based transport for libp2p. Connect through the Tor network to TCP listeners. +//! +//! Main entrypoint of the crate: [`OnionTransport`] +//! +//! ## Example +//! ```no_run +//! # use async_std_crate as async_std; +//! # use libp2p_core::Transport; +//! # async fn test_func() -> Result<(), Box> { +//! let address = "/dns/www.torproject.org/tcp/1000".parse()?; +//! let mut transport = libp2p_onion::AsyncStdNativeTlsOnionTransport::bootstrapped().await?; +//! // we have achieved tor connection +//! let _conn = transport.dial(address)?.await?; +//! # Ok(()) +//! # } +//! # async_std::task::block_on(async { test_func().await.unwrap() }); +//! ``` + +use address::{dangerous_extract_tor_address, safe_extract_tor_address}; +use arti_client::{TorClient, TorClientBuilder}; +use futures::{future::BoxFuture, FutureExt}; +use libp2p_core::{transport::TransportError, Multiaddr, Transport}; +use provider::OnionStream; +use std::sync::Arc; +use std::{marker::PhantomData, pin::Pin}; +use tor_rtcompat::Runtime; + +mod address; +mod provider; + +#[cfg(feature = "tokio")] +#[doc(inline)] +pub use provider::TokioOnionStream; + +#[cfg(feature = "async-std")] +#[doc(inline)] +pub use provider::AsyncStdOnionStream; + +pub type OnionError = arti_client::Error; + +#[derive(Clone)] +pub struct OnionTransport { + // client is in an Arc, because without it the [`Transport::dial`] method can't be implemented, + // due to lifetime issues. With the, eventual, stabilization of static async traits this issue + // will be resolved. + client: Arc>, + /// The used conversion mode to resolve addresses. One probably shouldn't access this directly. + /// The usage of [OnionTransport::with_address_conversion] at construction is recommended. + pub conversion_mode: AddressConversion, + phantom: PhantomData, +} + +/// Configure the onion transport from here. +pub type OnionBuilder = TorClientBuilder; + +/// 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, +} + +impl OnionTransport { + pub fn from_builder( + builder: OnionBuilder, + conversion_mode: AddressConversion, + ) -> Result { + let client = Arc::new(builder.create_unbootstrapped()?); + Ok(Self { + client, + conversion_mode, + phantom: PhantomData::default(), + }) + } + + pub async fn bootstrap(&self) -> Result<(), OnionError> { + self.client.bootstrap().await + } + + pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { + self.conversion_mode = conversion_mode; + self + } +} + +macro_rules! default_constructor { + () => { + pub async fn bootstrapped() -> Result { + let builder = Self::builder(); + let ret = Self::from_builder(builder, AddressConversion::DnsOnly)?; + ret.bootstrap().await?; + Ok(ret) + } + }; +} + +#[cfg(all(feature = "native-tls", feature = "async-std"))] +impl OnionTransport { + pub fn builder() -> OnionBuilder { + let runtime = tor_rtcompat::async_std::AsyncStdNativeTlsRuntime::current() + .expect("Couldn't get the current async_std native-tls runtime"); + TorClient::with_runtime(runtime) + } + default_constructor!(); +} + +#[cfg(all(feature = "rustls", feature = "async-std"))] +impl OnionTransport { + pub fn builder() -> OnionBuilder { + let runtime = tor_rtcompat::async_std::AsyncStdRustlsRuntime::current() + .expect("Couldn't get the current async_std rustls runtime"); + TorClient::with_runtime(runtime) + } + default_constructor!(); +} + +#[cfg(all(feature = "native-tls", feature = "tokio"))] +impl OnionTransport { + pub fn builder() -> OnionBuilder { + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::current() + .expect("Couldn't get the current tokio native-tls runtime"); + TorClient::with_runtime(runtime) + } + default_constructor!(); +} + +#[cfg(all(feature = "rustls", feature = "tokio"))] +impl OnionTransport { + pub fn builder() -> OnionBuilder { + let runtime = tor_rtcompat::tokio::TokioRustlsRuntime::current() + .expect("Couldn't get the current tokio rustls runtime"); + TorClient::with_runtime(runtime) + } + default_constructor!(); +} + +#[cfg(all(feature = "native-tls", feature = "async-std"))] +pub type AsyncStdNativeTlsOnionTransport = + OnionTransport; +#[cfg(all(feature = "rustls", feature = "async-std"))] +pub type AsyncStdRustlsOnionTransport = + OnionTransport; +#[cfg(all(feature = "native-tls", feature = "tokio"))] +pub type TokioNativeTlsOnionTransport = + OnionTransport; +#[cfg(all(feature = "rustls", feature = "tokio"))] +pub type TokioRustlsOnionTransport = + OnionTransport; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AlwaysErrorListenerUpgrade(PhantomData); + +impl core::future::Future for AlwaysErrorListenerUpgrade { + type Output = Result; + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + panic!("onion services are not implented yet, since arti doesn't support it. (awaiting Arti 1.2.0)") + } +} + +impl Transport for OnionTransport +where + S: OnionStream, +{ + type Output = S; + type Error = OnionError; + type Dial = BoxFuture<'static, Result>; + type ListenerUpgrade = AlwaysErrorListenerUpgrade; + + /// Always returns `TransportError::MultiaddrNotSupported` + fn listen_on( + &mut self, + addr: libp2p_core::Multiaddr, + ) -> Result< + libp2p_core::transport::ListenerId, + libp2p_core::transport::TransportError, + > { + // although this address might be supported, this is returned in order to not provoke an + // error when trying to listen on this transport. + Err(TransportError::MultiaddrNotSupported(addr)) + } + + fn remove_listener(&mut self, _id: libp2p_core::transport::ListenerId) -> bool { + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let maybe_tor_addr = match self.conversion_mode { + AddressConversion::DnsOnly => safe_extract_tor_address(&addr), + AddressConversion::IpAndDns => dangerous_extract_tor_address(&addr), + }; + + let tor_address = maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr))?; + let onion_client = self.client.clone(); + + Ok(async move { onion_client.connect(tor_address).await.map(S::from) }.boxed()) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { + None + } + + fn poll( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> + { + // pending is returned here because this transport doesn't support listening + std::task::Poll::Pending + } +} diff --git a/transports/onion/src/provider.rs b/transports/onion/src/provider.rs new file mode 100644 index 00000000000..841cb79ef02 --- /dev/null +++ b/transports/onion/src/provider.rs @@ -0,0 +1,103 @@ +// 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}; + +pub trait OnionStream: AsyncRead + AsyncWrite + From {} + +impl OnionStream for DataStream {} + +#[cfg(feature = "async-std")] +pub type AsyncStdOnionStream = DataStream; + +#[cfg(feature = "tokio")] +#[derive(Debug)] +pub struct TokioOnionStream { + inner: DataStream, +} + +#[cfg(feature = "tokio")] +impl From for TokioOnionStream { + fn from(inner: DataStream) -> Self { + Self { inner } + } +} + +#[cfg(feature = "tokio")] +impl OnionStream for TokioOnionStream {} + +#[cfg(feature = "tokio")] +impl AsyncRead for TokioOnionStream { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + let mut read_buf = tokio_crate::io::ReadBuf::new(buf); + futures::ready!(tokio_crate::io::AsyncRead::poll_read( + std::pin::Pin::new(&mut self.inner), + cx, + &mut read_buf + ))?; + std::task::Poll::Ready(Ok(read_buf.filled().len())) + } +} + +#[cfg(feature = "tokio")] +impl AsyncWrite for TokioOnionStream { + #[inline] + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + tokio_crate::io::AsyncWrite::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> { + tokio_crate::io::AsyncWrite::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> { + tokio_crate::io::AsyncWrite::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> { + tokio_crate::io::AsyncWrite::poll_write_vectored( + std::pin::Pin::new(&mut self.inner), + cx, + bufs, + ) + } +}