├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── LICENSE ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── src ├── provider.rs ├── address.rs └── lib.rs └── examples └── ping-onion.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 9999 8 | commit-message: 9 | prefix: "deps" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | commit-message: 15 | prefix: "deps" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hannes Furmans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.1 2 | 3 | - Remove double features: See [PR 21] 4 | - Correct typo in `src/lib.rs`: See [PR 21] 5 | - Update `CHANGELOG.md`: See [PR 21] 6 | 7 | [PR 21]: https://github.com/umgefahren/libp2p-tor/pull/21 8 | 9 | # 0.4.0 10 | 11 | ## Changes 12 | 13 | - Updated dependencies: See [PR 18] 14 | - [`arti-client` to `v0.24.0`] 15 | - [`libp2p` to `v0.53.0`] 16 | - [`tor-rtcompat` to `v0.24.0`] 17 | - Add tracing: See [PR 18] 18 | - Update CI: See [PR 20] 19 | - `actions/checkout` to `v4` 20 | - Remove `arduino/setup-protoc` 21 | 22 | ## First time contributor 23 | 24 | - @binarybaron 25 | 26 | Thanks! :rocket: 27 | 28 | [PR 18]: https://github.com/umgefahren/libp2p-tor/pull/18 29 | [PR 20]: https://github.com/umgefahren/libp2p-tor/pull/20 30 | 31 | 32 | # 0.3.0-alpha 33 | 34 | - Updated dependencies: See [PR 6]. 35 | - [`arti-client` to `v0.8` 36 | 37 | - Updated dependencies: See [PR 8]. 38 | - `libp2p-core` to `v0.39` 39 | - `libp2p` to `0.51` 40 | 41 | [PR 6]: https://github.com/umgefahren/libp2p-tor/pull/6 42 | [PR 8]: https://github.com/umgefahren/libp2p-tor/pull/8 43 | 44 | # 0.2.0-alpha 45 | 46 | - Updated dependencies: 47 | - [`libp2p` to `v0.50.0`](#2) 48 | - [`libp2p-core` to `v0.38.0`](#3) 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libp2p-community-tor" 3 | version = "0.5.0" 4 | edition = "2021" 5 | license = "MIT" 6 | resolver = "2" 7 | description = "Tor transport for libp2p." 8 | repository = "https://github.com/umgefahren/libp2p-tor" 9 | authors = ["umgefahren "] 10 | 11 | [dependencies] 12 | thiserror = "1.0" 13 | anyhow = "1.0.93" 14 | tokio = "1.41.1" 15 | futures = "0.3" 16 | 17 | arti-client = { version = "^0.29", default-features = false, features = ["tokio", "rustls", "onion-service-client", "static-sqlite"] } 18 | libp2p = { version = "^0.53", default-features = false, features = ["tokio", "tcp", "tls"] } 19 | 20 | tor-rtcompat = { version = "^0.29", features = ["tokio", "rustls"] } 21 | tracing = "0.1.40" 22 | tor-hsservice = { version = "^0.29", optional = true } 23 | tor-cell = { version = "^0.29", optional = true } 24 | tor-proto = { version = "^0.29", optional = true } 25 | data-encoding = { version = "2.6.0" } 26 | 27 | [dev-dependencies] 28 | libp2p = { version = "0.53", default-features = false, features = ["tokio", "noise", "yamux", "ping", "macros", "tcp", "tls"] } 29 | tokio-test = "0.4.4" 30 | tokio = { version = "1.41.1", features = ["macros"] } 31 | tracing-subscriber = "0.2" 32 | 33 | [features] 34 | listen-onion-service = [ 35 | "arti-client/onion-service-service", 36 | "dep:tor-hsservice", 37 | "dep:tor-cell", 38 | "dep:tor-proto" 39 | ] 40 | 41 | [[example]] 42 | name = "ping-onion" 43 | required-features = ["listen-onion-service"] 44 | 45 | [package.metadata.docs.rs] 46 | all-features = true 47 | rustdoc-args = ["--cfg", "docsrs"] 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: Build ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ ubuntu-latest, macos-latest, windows-latest ] 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: Swatinem/rust-cache@v2 25 | 26 | - name: Check 27 | run: cargo check --all-features 28 | 29 | - name: Build 30 | run: cargo build --all-features 31 | 32 | test: 33 | name: Test 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - uses: Swatinem/rust-cache@v2 39 | 40 | - name: Run tests with all features enabled 41 | run: cargo test --all-features 42 | 43 | - name: Run tests without features enabled 44 | run: cargo test 45 | 46 | clippy: 47 | name: Clippy 48 | runs-on: ubuntu-latest 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | rust-version: [ stable, beta, nightly ] 53 | steps: 54 | - uses: actions/checkout@v4 55 | 56 | - uses: dtolnay/rust-toolchain@master 57 | with: 58 | toolchain: ${{ matrix.rust-version }} 59 | components: clippy 60 | 61 | - uses: Swatinem/rust-cache@v2 62 | 63 | - name: Run cargo clippy with all features enabled 64 | run: cargo clippy --all-features 65 | 66 | - name: Run cargo clippy without any features enabled 67 | run: cargo clippy 68 | 69 | rustfmt: 70 | name: Rust format 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - uses: dtolnay/rust-toolchain@stable 76 | with: 77 | components: rustfmt 78 | 79 | - name: Check formatting 80 | run: cargo fmt -- --check 81 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Continuous integration](https://github.com/umgefahren/libp2p-tor/actions/workflows/ci.yml/badge.svg)](https://github.com/umgefahren/libp2p-tor/actions/workflows/ci.yml) 2 | [![docs.rs](https://img.shields.io/docsrs/libp2p-community-tor?style=flat-square)](https://docs.rs/libp2p-community-tor/latest) 3 | [![Crates.io](https://img.shields.io/crates/v/libp2p-community-tor?style=flat-square)](https://crates.io/crates/libp2p-community-tor) 4 | 5 | # libp2p Tor 6 | 7 | Tor based transport for libp2p. Connect through the Tor network to TCP listeners. 8 | 9 | Build on top of [Arti](https://gitlab.torproject.org/tpo/core/arti). 10 | 11 | ## New Feature 12 | 13 | This crate supports, since #21 (thanks to @binarybaron), listening as a Tor hidden service as well as connecting to them. 14 | 15 | ## ⚠️ Misuse warning ⚠️ - read carefully before using 16 | 17 | Although the sound of "Tor" might convey a sense of security it is *very* easy to misuse this 18 | crate and leaking private information while using. Study libp2p carefully and try to make sure 19 | you fully understand it's current limits regarding privacy. I.e. using identify might already 20 | render this transport obsolete. 21 | 22 | This transport explicitly **doesn't** provide any enhanced privacy if it's just used like a regular transport. 23 | Use with caution and at your own risk. **Don't** just blindly advertise Tor without fully understanding what you 24 | are dealing with. 25 | 26 | ### Add to your dependencies 27 | 28 | ```bash 29 | cargo add libp2p-community-tor 30 | ``` 31 | 32 | This crate uses tokio with rustls for its runtime and TLS implementation. 33 | No other combinations are supported. 34 | 35 | - [`rustls`](https://github.com/rustls/rustls) 36 | - [`tokio`](https://github.com/tokio-rs/tokio) 37 | 38 | ### Example 39 | ```rust 40 | let address = "/dns/www.torproject.org/tcp/1000".parse()?; 41 | let mut transport = libp2p_community_tor::TorTransport::bootstrapped().await?; 42 | // we have achieved tor connection 43 | let _conn = transport.dial(address)?.await?; 44 | ``` 45 | 46 | ### About 47 | 48 | This crate originates in a PR to bring Tor support too rust-libp2p. Read more about it here: libp2p/rust-libp2p#2899 49 | 50 | License: MIT 51 | -------------------------------------------------------------------------------- /src/provider.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Hannes Furmans 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use arti_client::DataStream; 22 | use futures::{AsyncRead, AsyncWrite}; 23 | use tokio::io::{AsyncRead as TokioAsyncRead, AsyncWrite as TokioAsyncWrite, ReadBuf}; 24 | 25 | #[derive(Debug)] 26 | pub struct TokioTorStream { 27 | inner: DataStream, 28 | } 29 | 30 | impl From for TokioTorStream { 31 | fn from(inner: DataStream) -> Self { 32 | Self { inner } 33 | } 34 | } 35 | 36 | impl AsyncRead for TokioTorStream { 37 | fn poll_read( 38 | mut self: std::pin::Pin<&mut Self>, 39 | cx: &mut std::task::Context<'_>, 40 | buf: &mut [u8], 41 | ) -> std::task::Poll> { 42 | let mut read_buf = ReadBuf::new(buf); 43 | futures::ready!(TokioAsyncRead::poll_read( 44 | std::pin::Pin::new(&mut self.inner), 45 | cx, 46 | &mut read_buf 47 | ))?; 48 | std::task::Poll::Ready(Ok(read_buf.filled().len())) 49 | } 50 | } 51 | 52 | impl AsyncWrite for TokioTorStream { 53 | #[inline] 54 | fn poll_write( 55 | mut self: std::pin::Pin<&mut Self>, 56 | cx: &mut std::task::Context<'_>, 57 | buf: &[u8], 58 | ) -> std::task::Poll> { 59 | TokioAsyncWrite::poll_write(std::pin::Pin::new(&mut self.inner), cx, buf) 60 | } 61 | 62 | #[inline] 63 | fn poll_flush( 64 | mut self: std::pin::Pin<&mut Self>, 65 | cx: &mut std::task::Context<'_>, 66 | ) -> std::task::Poll> { 67 | TokioAsyncWrite::poll_flush(std::pin::Pin::new(&mut self.inner), cx) 68 | } 69 | 70 | #[inline] 71 | fn poll_close( 72 | mut self: std::pin::Pin<&mut Self>, 73 | cx: &mut std::task::Context<'_>, 74 | ) -> std::task::Poll> { 75 | TokioAsyncWrite::poll_shutdown(std::pin::Pin::new(&mut self.inner), cx) 76 | } 77 | 78 | #[inline] 79 | fn poll_write_vectored( 80 | mut self: std::pin::Pin<&mut Self>, 81 | cx: &mut std::task::Context<'_>, 82 | bufs: &[std::io::IoSlice<'_>], 83 | ) -> std::task::Poll> { 84 | TokioAsyncWrite::poll_write_vectored(std::pin::Pin::new(&mut self.inner), cx, bufs) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/ping-onion.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Hannes Furmans 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! Ping-Onion example 22 | //! 23 | //! See ../src/tutorial.rs for a step-by-step guide building the example below. 24 | //! 25 | //! This example requires two seperate computers, one of which has to be reachable from the 26 | //! internet. 27 | //! 28 | //! On the first computer run: 29 | //! ```sh 30 | //! cargo run --example ping 31 | //! ``` 32 | //! 33 | //! It will print the PeerId and the listening addresses, e.g. `Listening on 34 | //! "/ip4/0.0.0.0/tcp/24915"` 35 | //! 36 | //! Make sure that the first computer is reachable under one of these ip addresses and port. 37 | //! 38 | //! On the second computer run: 39 | //! ```sh 40 | //! cargo run --example ping-onion -- /ip4/123.45.67.89/tcp/24915 41 | //! ``` 42 | //! 43 | //! The two nodes establish a connection, negotiate the ping protocol 44 | //! and begin pinging each other over Tor. 45 | 46 | use futures::StreamExt; 47 | use libp2p::core::upgrade::Version; 48 | use libp2p::Transport; 49 | use libp2p::{ 50 | core::muxing::StreamMuxerBox, 51 | identity, noise, 52 | swarm::{NetworkBehaviour, SwarmEvent}, 53 | yamux, Multiaddr, PeerId, SwarmBuilder, 54 | }; 55 | use libp2p_community_tor::{AddressConversion, TorTransport}; 56 | use std::error::Error; 57 | use tor_hsservice::config::OnionServiceConfigBuilder; 58 | 59 | /// Create a transport 60 | /// Returns a tuple of the transport and the onion address we can instruct it to listen on 61 | async fn onion_transport( 62 | keypair: identity::Keypair, 63 | ) -> Result< 64 | ( 65 | libp2p::core::transport::Boxed<(PeerId, libp2p::core::muxing::StreamMuxerBox)>, 66 | Multiaddr, 67 | ), 68 | Box, 69 | > { 70 | let mut transport = TorTransport::bootstrapped() 71 | .await? 72 | .with_address_conversion(AddressConversion::IpAndDns); 73 | 74 | // We derive the nickname for the onion address from the peer id 75 | let svg_cfg = OnionServiceConfigBuilder::default() 76 | .nickname( 77 | keypair 78 | .public() 79 | .to_peer_id() 80 | .to_base58() 81 | .to_ascii_lowercase() 82 | .parse() 83 | .unwrap(), 84 | ) 85 | .num_intro_points(3) 86 | .build() 87 | .unwrap(); 88 | 89 | let onion_listen_address = transport.add_onion_service(svg_cfg, 999).unwrap(); 90 | 91 | let auth_upgrade = noise::Config::new(&keypair)?; 92 | let multiplex_upgrade = yamux::Config::default(); 93 | 94 | let transport = transport 95 | .boxed() 96 | .upgrade(Version::V1) 97 | .authenticate(auth_upgrade) 98 | .multiplex(multiplex_upgrade) 99 | .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) 100 | .boxed(); 101 | 102 | Ok((transport, onion_listen_address)) 103 | } 104 | 105 | #[tokio::main] 106 | async fn main() -> Result<(), Box> { 107 | tracing_subscriber::fmt::init(); 108 | 109 | let local_key = identity::Keypair::generate_ed25519(); 110 | let local_peer_id = PeerId::from(local_key.public()); 111 | 112 | println!("Local peer id: {local_peer_id}"); 113 | 114 | let (transport, onion_listen_address) = onion_transport(local_key).await?; 115 | 116 | let mut swarm = SwarmBuilder::with_new_identity() 117 | .with_tokio() 118 | .with_other_transport(|_| transport) 119 | .unwrap() 120 | .with_behaviour(|_| Behaviour { 121 | ping: libp2p::ping::Behaviour::default(), 122 | }) 123 | .unwrap() 124 | .build(); 125 | 126 | // Dial the peer identified by the multi-address given as the second 127 | // command-line argument, if any. 128 | if let Some(addr) = std::env::args().nth(1) { 129 | let remote: Multiaddr = addr.parse()?; 130 | swarm.dial(remote)?; 131 | println!("Dialed {addr}") 132 | } else { 133 | // If we are not dialing, we need to listen 134 | // Tell the swarm to listen on a specific onion address 135 | swarm.listen_on(onion_listen_address).unwrap(); 136 | } 137 | 138 | loop { 139 | match swarm.select_next_some().await { 140 | SwarmEvent::ConnectionEstablished { 141 | endpoint, peer_id, .. 142 | } => { 143 | println!("Connection established with {peer_id} on {endpoint:?}"); 144 | } 145 | SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { 146 | println!("Outgoing connection error with {peer_id:?}: {error:?}"); 147 | } 148 | SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"), 149 | SwarmEvent::Behaviour(event) => println!("{event:?}"), 150 | _ => {} 151 | } 152 | } 153 | } 154 | 155 | /// Our network behaviour. 156 | #[derive(NetworkBehaviour)] 157 | struct Behaviour { 158 | ping: libp2p::ping::Behaviour, 159 | } 160 | -------------------------------------------------------------------------------- /src/address.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Hannes Furmans 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | use arti_client::{DangerouslyIntoTorAddr, IntoTorAddr, TorAddr}; 21 | use libp2p::{core::multiaddr::Protocol, multiaddr::Onion3Addr, Multiaddr}; 22 | use std::net::SocketAddr; 23 | 24 | /// "Dangerously" extract a Tor address from the provided [`Multiaddr`]. 25 | /// 26 | /// See [`DangerouslyIntoTorAddr`] for details around the safety / privacy considerations. 27 | pub fn dangerous_extract(multiaddr: &Multiaddr) -> Option { 28 | if let Some(tor_addr) = safe_extract(multiaddr) { 29 | return Some(tor_addr); 30 | } 31 | 32 | let mut protocols = multiaddr.into_iter(); 33 | 34 | let tor_addr = try_to_socket_addr(&protocols.next()?, &protocols.next()?)? 35 | .into_tor_addr_dangerously() 36 | .ok()?; 37 | 38 | Some(tor_addr) 39 | } 40 | 41 | /// "Safely" extract a Tor address from the provided [`Multiaddr`]. 42 | /// 43 | /// See [`IntoTorAddr`] for details around the safety / privacy considerations. 44 | pub fn safe_extract(multiaddr: &Multiaddr) -> Option { 45 | let mut protocols = multiaddr.into_iter(); 46 | 47 | let tor_addr = try_to_domain_and_port(&protocols.next()?, &protocols.next())? 48 | .into_tor_addr() 49 | .ok()?; 50 | 51 | Some(tor_addr) 52 | } 53 | 54 | fn libp2p_onion_address_to_domain_and_port<'a>( 55 | onion_address: &'a Onion3Addr<'_>, 56 | ) -> (&'a str, u16) { 57 | // Here we convert from Onion3Addr to TorAddr 58 | // We need to leak the string because it's a temporary string that would otherwise be freed 59 | let hash = data_encoding::BASE32.encode(onion_address.hash()); 60 | let onion_domain = format!("{hash}.onion"); 61 | let onion_domain = Box::leak(onion_domain.into_boxed_str()); 62 | 63 | (onion_domain, onion_address.port()) 64 | } 65 | 66 | fn try_to_domain_and_port<'a>( 67 | maybe_domain: &'a Protocol, 68 | maybe_port: &Option, 69 | ) -> Option<(&'a str, u16)> { 70 | match (maybe_domain, maybe_port) { 71 | ( 72 | Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain), 73 | Some(Protocol::Tcp(port)), 74 | ) => Some((domain.as_ref(), *port)), 75 | (Protocol::Onion3(domain), _) => Some(libp2p_onion_address_to_domain_and_port(domain)), 76 | _ => None, 77 | } 78 | } 79 | 80 | fn try_to_socket_addr(maybe_ip: &Protocol, maybe_port: &Protocol) -> Option { 81 | match (maybe_ip, maybe_port) { 82 | (Protocol::Ip4(ip), Protocol::Tcp(port)) => Some(SocketAddr::from((*ip, *port))), 83 | (Protocol::Ip6(ip), Protocol::Tcp(port)) => Some(SocketAddr::from((*ip, *port))), 84 | _ => None, 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | use arti_client::TorAddr; 92 | use std::net::{Ipv4Addr, Ipv6Addr}; 93 | 94 | #[test] 95 | fn extract_correct_address_from_dns() { 96 | let addresses = [ 97 | "/dns/ip.tld/tcp/10".parse().unwrap(), 98 | "/dns4/dns.ip4.tld/tcp/11".parse().unwrap(), 99 | "/dns6/dns.ip6.tld/tcp/12".parse().unwrap(), 100 | ]; 101 | 102 | let actual = addresses 103 | .iter() 104 | .filter_map(safe_extract) 105 | .collect::>(); 106 | 107 | assert_eq!( 108 | &[ 109 | TorAddr::from(("ip.tld", 10)).unwrap(), 110 | TorAddr::from(("dns.ip4.tld", 11)).unwrap(), 111 | TorAddr::from(("dns.ip6.tld", 12)).unwrap(), 112 | ], 113 | actual.as_slice() 114 | ); 115 | } 116 | 117 | #[test] 118 | fn extract_correct_address_from_ips() { 119 | let addresses = [ 120 | "/ip4/127.0.0.1/tcp/10".parse().unwrap(), 121 | "/ip6/::1/tcp/10".parse().unwrap(), 122 | ]; 123 | 124 | let actual = addresses 125 | .iter() 126 | .filter_map(dangerous_extract) 127 | .collect::>(); 128 | 129 | assert_eq!( 130 | &[ 131 | TorAddr::dangerously_from((Ipv4Addr::LOCALHOST, 10)).unwrap(), 132 | TorAddr::dangerously_from((Ipv6Addr::LOCALHOST, 10)).unwrap(), 133 | ], 134 | actual.as_slice() 135 | ); 136 | } 137 | 138 | #[test] 139 | fn dangerous_extract_works_on_domains_too() { 140 | let addresses = [ 141 | "/dns/ip.tld/tcp/10".parse().unwrap(), 142 | "/ip4/127.0.0.1/tcp/10".parse().unwrap(), 143 | "/ip6/::1/tcp/10".parse().unwrap(), 144 | ]; 145 | 146 | let actual = addresses 147 | .iter() 148 | .filter_map(dangerous_extract) 149 | .collect::>(); 150 | 151 | assert_eq!( 152 | &[ 153 | TorAddr::from(("ip.tld", 10)).unwrap(), 154 | TorAddr::dangerously_from((Ipv4Addr::LOCALHOST, 10)).unwrap(), 155 | TorAddr::dangerously_from((Ipv6Addr::LOCALHOST, 10)).unwrap(), 156 | ], 157 | actual.as_slice() 158 | ); 159 | } 160 | 161 | #[test] 162 | fn detect_incorrect_address() { 163 | let addresses = [ 164 | "/tcp/10/udp/12".parse().unwrap(), 165 | "/dns/ip.tld/dns4/ip.tld/dns6/ip.tld".parse().unwrap(), 166 | "/tcp/10/ip4/1.1.1.1".parse().unwrap(), 167 | ]; 168 | 169 | let all_correct = addresses.iter().map(safe_extract).all(|res| res.is_none()); 170 | 171 | assert!( 172 | all_correct, 173 | "During the parsing of the faulty addresses, there was an incorrectness" 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Hannes Furmans 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 22 | #![warn(clippy::pedantic)] 23 | #![deny(unsafe_code)] 24 | //! Tor based transport for libp2p. Connect through the Tor network to TCP listeners. 25 | //! 26 | //! # ⚠️ Misuse warning ⚠️ - read carefully before using 27 | //! Although the sound of "Tor" might convey a sense of security it is *very* easy to misuse this 28 | //! crate and leaking private information while using. Study libp2p carefully and try to make sure 29 | //! you fully understand it's current limits regarding privacy. I.e. using identify might already 30 | //! render this transport obsolete. 31 | //! 32 | //! This transport explicitly **doesn't** provide any enhanced privacy if it's just used like a regular transport. 33 | //! Use with caution and at your own risk. **Don't** just blindly advertise Tor without fully understanding what you 34 | //! are dealing with. 35 | //! 36 | //! ## Runtime 37 | //! 38 | //! This crate uses tokio with rustls for its runtime and TLS implementation. 39 | //! No other combinations are supported. 40 | //! 41 | //! ## Example 42 | //! ```no_run 43 | //! use libp2p::core::Transport; 44 | //! # async fn test_func() -> Result<(), Box> { 45 | //! let address = "/dns/www.torproject.org/tcp/1000".parse()?; 46 | //! let mut transport = libp2p_community_tor::TorTransport::bootstrapped().await?; 47 | //! // we have achieved tor connection 48 | //! let _conn = transport.dial(address)?.await?; 49 | //! # Ok(()) 50 | //! # } 51 | //! # tokio_test::block_on(test_func()); 52 | //! ``` 53 | 54 | use arti_client::{TorClient, TorClientBuilder}; 55 | use futures::future::BoxFuture; 56 | use libp2p::{ 57 | core::transport::{ListenerId, TransportEvent}, 58 | Multiaddr, Transport, TransportError, 59 | }; 60 | use std::pin::Pin; 61 | use std::sync::Arc; 62 | use std::task::{Context, Poll}; 63 | use thiserror::Error; 64 | use tor_rtcompat::tokio::TokioRustlsRuntime; 65 | 66 | // We only need these imports if the `listen-onion-service` feature is enabled 67 | #[cfg(feature = "listen-onion-service")] 68 | use std::collections::HashMap; 69 | #[cfg(feature = "listen-onion-service")] 70 | use std::str::FromStr; 71 | #[cfg(feature = "listen-onion-service")] 72 | use tor_cell::relaycell::msg::{Connected, End, EndReason}; 73 | #[cfg(feature = "listen-onion-service")] 74 | use tor_hsservice::{ 75 | handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig, 76 | RunningOnionService, StreamRequest, 77 | }; 78 | #[cfg(feature = "listen-onion-service")] 79 | use tor_proto::stream::IncomingStreamRequest; 80 | 81 | mod address; 82 | mod provider; 83 | 84 | use address::{dangerous_extract, safe_extract}; 85 | pub use provider::TokioTorStream; 86 | 87 | pub type TorError = arti_client::Error; 88 | 89 | type PendingUpgrade = BoxFuture<'static, Result>; 90 | #[cfg(feature = "listen-onion-service")] 91 | type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>; 92 | #[cfg(feature = "listen-onion-service")] 93 | type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>; 94 | 95 | /// Struct representing an onion address we are listening on for libp2p connections. 96 | #[cfg(feature = "listen-onion-service")] 97 | struct TorListener { 98 | #[allow(dead_code)] // We need to own this to keep the RunningOnionService alive 99 | /// The onion service we are listening on 100 | service: Arc, 101 | /// The stream of status updates for the onion service 102 | status_stream: OnionServiceStatusStream, 103 | /// The stream incoming [`StreamRequest`]s 104 | request_stream: OnionServiceStream, 105 | 106 | /// The port we are listening on 107 | port: u16, 108 | /// The onion address we are listening on 109 | onion_address: Multiaddr, 110 | /// Whether we have already announced this address 111 | announced: bool, 112 | } 113 | 114 | /// Mode of address conversion. 115 | /// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details 116 | #[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] 117 | pub enum AddressConversion { 118 | /// Uses only DNS for address resolution (default). 119 | #[default] 120 | DnsOnly, 121 | /// Uses IP and DNS for addresses. 122 | IpAndDns, 123 | } 124 | 125 | pub struct TorTransport { 126 | pub conversion_mode: AddressConversion, 127 | 128 | /// The Tor client. 129 | client: Arc>, 130 | 131 | /// Onion services we are listening on. 132 | #[cfg(feature = "listen-onion-service")] 133 | listeners: HashMap, 134 | 135 | /// Onion services we are running but currently not listening on 136 | #[cfg(feature = "listen-onion-service")] 137 | services: Vec<(Arc, OnionServiceStream)>, 138 | } 139 | 140 | impl TorTransport { 141 | /// Creates a new `TorClientBuilder`. 142 | /// 143 | /// # Panics 144 | /// Panics if the current runtime is not a `TokioRustlsRuntime`. 145 | pub fn builder() -> TorClientBuilder { 146 | let runtime = 147 | TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime"); 148 | TorClient::with_runtime(runtime) 149 | } 150 | 151 | /// Creates a bootstrapped `TorTransport` 152 | /// 153 | /// # Errors 154 | /// Could return error emitted during Tor bootstrap by Arti. 155 | pub async fn bootstrapped() -> Result { 156 | let builder = Self::builder(); 157 | let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; 158 | ret.bootstrap().await?; 159 | Ok(ret) 160 | } 161 | 162 | /// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it. 163 | /// 164 | /// # Errors 165 | /// Could return error emitted during creation of the `TorClient`. 166 | pub fn from_builder( 167 | builder: &TorClientBuilder, 168 | conversion_mode: AddressConversion, 169 | ) -> Result { 170 | let client = Arc::new(builder.create_unbootstrapped()?); 171 | 172 | Ok(Self::from_client(client, conversion_mode)) 173 | } 174 | 175 | /// Builds a `TorTransport` from an existing Arti `TorClient`. 176 | pub fn from_client( 177 | client: Arc>, 178 | conversion_mode: AddressConversion, 179 | ) -> Self { 180 | Self { 181 | conversion_mode, 182 | client, 183 | #[cfg(feature = "listen-onion-service")] 184 | listeners: HashMap::new(), 185 | #[cfg(feature = "listen-onion-service")] 186 | services: Vec::new(), 187 | } 188 | } 189 | 190 | /// Bootstraps the `TorTransport` into the Tor network. 191 | /// 192 | /// # Errors 193 | /// Could return error emitted during bootstrap by Arti. 194 | pub async fn bootstrap(&self) -> Result<(), TorError> { 195 | self.client.bootstrap().await 196 | } 197 | 198 | /// Set the address conversion mode 199 | #[must_use] 200 | pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { 201 | self.conversion_mode = conversion_mode; 202 | self 203 | } 204 | 205 | /// Call this function to instruct the transport to listen on a specific onion address 206 | /// You need to call this function **before** calling `listen_on` 207 | /// 208 | /// # Returns 209 | /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on 210 | /// To actually listen on the address, you need to call [`listen_on`] with the returned address 211 | /// 212 | /// # Errors 213 | /// Returns an error if we cannot get the onion address of the service 214 | #[cfg(feature = "listen-onion-service")] 215 | pub fn add_onion_service( 216 | &mut self, 217 | svc_cfg: OnionServiceConfig, 218 | port: u16, 219 | ) -> anyhow::Result { 220 | let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?; 221 | let request_stream = Box::pin(handle_rend_requests(request_stream)); 222 | 223 | let multiaddr = service 224 | .onion_name() 225 | .ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))? 226 | .to_multiaddr(port); 227 | 228 | self.services.push((service, request_stream)); 229 | 230 | Ok(multiaddr) 231 | } 232 | } 233 | 234 | #[derive(Debug, Error)] 235 | pub enum TorTransportError { 236 | #[error(transparent)] 237 | Client(#[from] TorError), 238 | #[cfg(feature = "listen-onion-service")] 239 | #[error(transparent)] 240 | Service(#[from] tor_hsservice::ClientError), 241 | #[cfg(feature = "listen-onion-service")] 242 | #[error("Stream closed before receiving data")] 243 | StreamClosed, 244 | #[cfg(feature = "listen-onion-service")] 245 | #[error("Stream port does not match listener port")] 246 | StreamPortMismatch, 247 | #[cfg(feature = "listen-onion-service")] 248 | #[error("Onion service is broken")] 249 | Broken, 250 | } 251 | 252 | #[cfg(feature = "listen-onion-service")] 253 | trait HsIdExt { 254 | fn to_multiaddr(&self, port: u16) -> Multiaddr; 255 | } 256 | 257 | #[cfg(feature = "listen-onion-service")] 258 | impl HsIdExt for HsId { 259 | /// Convert an `HsId` to a `Multiaddr` 260 | fn to_multiaddr(&self, port: u16) -> Multiaddr { 261 | let onion_domain = self.to_string(); 262 | let onion_without_dot_onion = onion_domain 263 | .split('.') 264 | .nth(0) 265 | .expect("Display formatting of HsId to contain .onion suffix"); 266 | let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}"); 267 | 268 | Multiaddr::from_str(&multiaddress_string) 269 | .expect("A valid onion address to be convertible to a Multiaddr") 270 | } 271 | } 272 | 273 | impl Transport for TorTransport { 274 | type Output = TokioTorStream; 275 | type Error = TorTransportError; 276 | type Dial = BoxFuture<'static, Result>; 277 | type ListenerUpgrade = PendingUpgrade; 278 | 279 | #[cfg(not(feature = "listen-onion-service"))] 280 | fn listen_on( 281 | &mut self, 282 | _id: ListenerId, 283 | onion_address: Multiaddr, 284 | ) -> Result<(), TransportError> { 285 | // If the `listen-onion-service` feature is not enabled, we do not support listening 286 | Err(TransportError::MultiaddrNotSupported(onion_address.clone())) 287 | } 288 | 289 | #[cfg(feature = "listen-onion-service")] 290 | fn listen_on( 291 | &mut self, 292 | id: ListenerId, 293 | onion_address: Multiaddr, 294 | ) -> Result<(), TransportError> { 295 | // If the address is not an onion3 address, return an error 296 | let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0) 297 | else { 298 | return Err(TransportError::MultiaddrNotSupported(onion_address.clone())); 299 | }; 300 | 301 | // Find the running onion service that matches the requested address 302 | // If we find it, remove it from [`services`] and insert it into [`listeners`] 303 | let position = self 304 | .services 305 | .iter() 306 | .position(|(service, _)| { 307 | service.onion_name().map_or(false, |name| { 308 | name.to_multiaddr(address.port()) == onion_address 309 | }) 310 | }) 311 | .ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?; 312 | 313 | let (service, request_stream) = self.services.remove(position); 314 | 315 | let status_stream = Box::pin(service.status_events()); 316 | 317 | self.listeners.insert( 318 | id, 319 | TorListener { 320 | service, 321 | request_stream, 322 | onion_address: onion_address.clone(), 323 | port: address.port(), 324 | status_stream, 325 | announced: false, 326 | }, 327 | ); 328 | 329 | Ok(()) 330 | } 331 | 332 | // We do not support removing listeners if the `listen-onion-service` feature is not enabled 333 | #[cfg(not(feature = "listen-onion-service"))] 334 | fn remove_listener(&mut self, _id: ListenerId) -> bool { 335 | false 336 | } 337 | 338 | #[cfg(feature = "listen-onion-service")] 339 | fn remove_listener(&mut self, id: ListenerId) -> bool { 340 | // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) 341 | // However, we will not stop the onion service itself because we might want to reuse it later 342 | // The onion service will be stopped when the transport is dropped 343 | if let Some(listener) = self.listeners.remove(&id) { 344 | self.services 345 | .push((listener.service, listener.request_stream)); 346 | return true; 347 | } 348 | 349 | false 350 | } 351 | 352 | fn dial(&mut self, addr: Multiaddr) -> Result> { 353 | let maybe_tor_addr = match self.conversion_mode { 354 | AddressConversion::DnsOnly => safe_extract(&addr), 355 | AddressConversion::IpAndDns => dangerous_extract(&addr), 356 | }; 357 | 358 | let tor_address = 359 | maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?; 360 | let onion_client = self.client.clone(); 361 | 362 | Ok(Box::pin(async move { 363 | let stream = onion_client.connect(tor_address).await?; 364 | 365 | tracing::debug!(%addr, "Established connection to peer through Tor"); 366 | 367 | Ok(TokioTorStream::from(stream)) 368 | })) 369 | } 370 | 371 | fn dial_as_listener( 372 | &mut self, 373 | addr: Multiaddr, 374 | ) -> Result> { 375 | self.dial(addr) 376 | } 377 | 378 | fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { 379 | None 380 | } 381 | 382 | #[cfg(not(feature = "listen-onion-service"))] 383 | fn poll( 384 | self: Pin<&mut Self>, 385 | _cx: &mut Context<'_>, 386 | ) -> Poll> { 387 | // If the `listen-onion-service` feature is not enabled, we do not support listening 388 | Poll::Pending 389 | } 390 | 391 | #[cfg(feature = "listen-onion-service")] 392 | fn poll( 393 | mut self: Pin<&mut Self>, 394 | cx: &mut Context<'_>, 395 | ) -> Poll> { 396 | for (listener_id, listener) in &mut self.listeners { 397 | // Check if the service has any new statuses 398 | if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) { 399 | tracing::debug!( 400 | status = ?status.state(), 401 | address = listener.onion_address.to_string(), 402 | "Onion service status changed" 403 | ); 404 | } 405 | 406 | // Check if we have already announced this address, if not, do it now 407 | if !listener.announced { 408 | listener.announced = true; 409 | 410 | // We announce the address here to the swarm even though we technically cannot guarantee 411 | // that the address is reachable yet from the outside. We might not have registered the 412 | // onion service fully yet (introduction points, hsdir, ...) 413 | // 414 | // However, we need to announce it now because otherwise libp2p might not poll the listener 415 | // again and we will not be able to announce it later. 416 | // TODO: Find out why this is the case, if this is intended behaviour or a bug 417 | return Poll::Ready(TransportEvent::NewAddress { 418 | listener_id: *listener_id, 419 | listen_addr: listener.onion_address.clone(), 420 | }); 421 | } 422 | 423 | match listener.request_stream.as_mut().poll_next(cx) { 424 | Poll::Ready(Some(request)) => { 425 | let port = listener.port; 426 | let upgrade: PendingUpgrade = Box::pin(async move { 427 | // Check if the port matches what we expect 428 | if let IncomingStreamRequest::Begin(begin) = request.request() { 429 | if begin.port() != port { 430 | // Reject the connection with CONNECTREFUSED 431 | request 432 | .reject(End::new_with_reason(EndReason::CONNECTREFUSED)) 433 | .await?; 434 | 435 | return Err(TorTransportError::StreamPortMismatch); 436 | } 437 | } 438 | 439 | // Accept the stream and forward it to the swarm 440 | let data_stream = request.accept(Connected::new_empty()).await?; 441 | Ok(TokioTorStream::from(data_stream)) 442 | }); 443 | 444 | return Poll::Ready(TransportEvent::Incoming { 445 | listener_id: *listener_id, 446 | upgrade, 447 | local_addr: listener.onion_address.clone(), 448 | send_back_addr: listener.onion_address.clone(), 449 | }); 450 | } 451 | 452 | // The stream has ended 453 | // This means that the onion service was shut down, and we will not receive any more connections on it 454 | Poll::Ready(None) => { 455 | return Poll::Ready(TransportEvent::ListenerClosed { 456 | listener_id: *listener_id, 457 | reason: Ok(()), 458 | }); 459 | } 460 | Poll::Pending => {} 461 | } 462 | } 463 | 464 | Poll::Pending 465 | } 466 | } 467 | --------------------------------------------------------------------------------