From ea8a08154a2364ca59413a5c615daf203b45dcab Mon Sep 17 00:00:00 2001 From: SyntheticBird45 Date: Sat, 6 Sep 2025 11:37:22 +0100 Subject: [PATCH] Add Tor section to User Book and Architecture Book --- binaries/cuprated/src/config/p2p.rs | 5 +- binaries/cuprated/src/config/tor.rs | 2 +- books/architecture/src/SUMMARY.md | 4 +- books/architecture/src/networking/tor.md | 284 ++++++++++++++++++++++- books/user/src/SUMMARY.md | 2 + books/user/src/tor.md | 131 +++++++++++ 6 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 books/user/src/tor.md diff --git a/binaries/cuprated/src/config/p2p.rs b/binaries/cuprated/src/config/p2p.rs index e4b13a847..f89279b9f 100644 --- a/binaries/cuprated/src/config/p2p.rs +++ b/binaries/cuprated/src/config/p2p.rs @@ -248,9 +248,8 @@ config_struct! { /// /// In Daemon mode, setting this to `true` will enable a TCP server listening for inbound connections /// from your Tor daemon. Refer to the `tor.anonymous_inbound` and `tor.listening_addr` field for onion address - /// and listening configuration. - /// - /// The server will listen on port `p2p.tor_net.p2p_port` + /// and listening configuration. In this mode, `p2p.tor_net.p2p_port` field is the advertized virtual port + /// of the hidden service. /// /// Type | boolean /// Valid values | false, true diff --git a/binaries/cuprated/src/config/tor.rs b/binaries/cuprated/src/config/tor.rs index 1fbdbfe45..6c2467f54 100644 --- a/binaries/cuprated/src/config/tor.rs +++ b/binaries/cuprated/src/config/tor.rs @@ -91,7 +91,7 @@ config_struct! { /// Enable Tor network by specifying how to connect to it. /// /// When "Daemon" is set, the Tor daemon address to use can be - /// specified in `tor.daemon_addr`. + /// specified in `tor.daemon.address`. /// /// Type | String /// Valid values | "Arti", "Daemon", "Off" diff --git a/books/architecture/src/SUMMARY.md b/books/architecture/src/SUMMARY.md index a99d099f4..cdfbcabdb 100644 --- a/books/architecture/src/SUMMARY.md +++ b/books/architecture/src/SUMMARY.md @@ -100,7 +100,7 @@ - [⚪️ P2P](networking/p2p.md) - [⚪️ Dandelion++](networking/dandelion.md) - [⚪️ Proxy](networking/proxy.md) - - [⚪️ Tor](networking/tor.md) + - [🟢 Tor](networking/tor.md) - [⚪️ i2p](networking/i2p.md) - [⚪️ IPv4/IPv6](networking/ipv4-ipv6.md) @@ -174,4 +174,4 @@ - [🔴 Contributing](appendix/contributing.md) - [🔴 Build targets](appendix/build-targets.md) - [🔴 Protocol book](appendix/protocol-book.md) - - [⚪️ User book](appendix/user-book.md) \ No newline at end of file + - [⚪️ User book](appendix/user-book.md) diff --git a/books/architecture/src/networking/tor.md b/books/architecture/src/networking/tor.md index cc0a809bd..f7ca931f7 100644 --- a/books/architecture/src/networking/tor.md +++ b/books/architecture/src/networking/tor.md @@ -1 +1,283 @@ -# ⚪️ Tor +# 🟢 Tor + +## Overview + +Cuprate can connect to the Tor network using either Arti or an external daemon that exposes a SOCKS5 interface. They are categorized as modes of connection, and Cuprate can use them to also anonymize clearnet connections. + +The Tor implementation is concentrated into four main crates. In order of relevance: +1. **cuprated**: contains the configuration, initialization, and Dandelion support for Tor. +2. **cuprate-p2p-transport**: defines all the transport logic necessary for establishing and accepting connections over the two modes. +3. **cuprate-p2p-core**: defines the Tor Zone address. +4. **cuprate-wire**: defines an onion address. + +The dependency graph is the following and will define the order in which we will treat the topic: +``` +cuprate-wire -> cuprate-p2p-core -> cuprate-p2p-transport -> cuprated +``` + +## Onion addresses + +The first building block of the Tor implementation is the `OnionAddr` type, which is necessary for the definition of the `Tor` network zone. +```rust +/// A v3, `Copy`able onion address. +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)] +pub struct OnionAddr { + /// 56 characters encoded onion v3 domain without the .onion suffix + /// + domain: [u8; 56], + /// Virtual port of the peer + pub port: u16, +} +``` + +Only V3 onion addresses are supported. The domain is directly written as bytes and not as a string to avoid allocation. Counterintuitively, and in opposition to the Tor project's standard definition, Cuprate calls an onion address the combination of a domain and a virtual port. This is done on purpose to keep the type name clear while acknowledging the uselessness of not having these two components simultaneously. + +The port is a public field, but the domain is private, as it requires basic validation. Cuprate makes sure to check the length and character set of an externally supplied domain. This mirrors the behavior of monerod. Similarly, Cuprate does not perform the standard checksum validation. This is not an issue, as trying to connect to these incorrect onion addresses would be immediately rejected. + +## Tor Zone + +```rust +#[derive(Clone, Copy)] +pub struct Tor; + +impl NetworkZone for Tor { + const NAME: &'static str = "Tor"; + + const CHECK_NODE_ID: bool = false; + + const BROADCAST_OWN_ADDR: bool = true; + + type Addr = OnionAddr; +} +``` + +Aside from the onion address type being used: +- the `CHECK_NODE_ID` is set to `false`, as all Tor peers have the exact same peer ID (0). +- The `BROADCAST_OWN_ADDR` is set to `true`, since incoming connections are not identifiable and therefore not routable. Cuprate, like all conforming Monero agents, will therefore include its own address in the peer list being sent to a peer. + +A sharp reader will observe that this configuration is the complete opposite of ClearNet. This is because these constants have been made consequently to the limitations observed in following the, at the time, (and unfortunately still actual) P2P protocol over anonymous networks instead of the internet. Maybe one day, a new network type will be supported for varying the combinations of these two booleans. + +## Transport + +The Tor implementation originally caused the separation of the connection methods from the zone definition. This is because several transport protocols could be used for the same network. Thus, the distinction between Zone, which relates to addressing, and Transport, which relates to the protocol used to connect to an address. + +Within the `cuprate-p2p-transport`, the following Tor-related transports are defined: + +- `impl Transport for Arti` +- `impl Transport for Arti` +- `impl Transport for Daemon` + +There is no `Transport for Daemon`, as it would be redundant. Instead, `cuprated` (will) make use of the `Transport for Socks` (when available) with the Tor configuration field. + +### Arti + +Arti is implemented for both Tor and ClearNet zones. Both establish outgoing connections using Arti's `TorClient::connect()`. However, for the Tor zone, Arti is launching an onion service prepared within cuprated, while for ClearNet, the server is simply disabled. + +### Daemon + +The daemon implementation is very similar to the classic TCP transport for ClearNet. +The `connect_to_peer` method establishes a TCP connection over the SOCKS5 proxy specified by the Tor daemon config. The Tor daemon makes the job of resolving the address and establishing a virtual connection. +Similarly, the `incoming_connection_listener` method is just a TCP server listening on the `tor.daemon.listening_addr`. + +## Cuprated + +### User configuration + +Under `cuprated/src/config/tor.rs`, the `TorConfig` struct contains Tor-specific (as in non-zone related) configuration fields: + +```rust +// attributes & comments trimmed. +pub struct TorConfig { + /// Enable Tor network by specifying how to connect to it. + /// Valid values | "Arti", "Daemon", "Off" + pub mode: TorMode, + + /// Arti config + pub arti: ArtiConfig, + + /// Tor Daemon config + pub daemon: TorDaemonConfig, +} +``` + +The content of both `[tor.arti]` and `[tor.daemon]` is left as an exploratory exercise for the reader. + +Under `cuprated/src/config/p2p.rs`, the `TorNetConfig` struct contains the Tor zone-specific fields that are added into the `[p2p.tor_net]` section: + +```rust +// attributes & comments trimmed +/// The config values for P2P tor. +pub struct TorNetConfig { + /// Enable the Tor P2P network. + pub enabled: bool, + + #[comment_out = true] + /// Enable Tor inbound onion server. + /// + /// In Arti mode, setting this to `true` will enable Arti's onion service for accepting inbound + /// Tor P2P connections. The keypair and therefore onion address are generated randomly on the first run. + /// + /// In Daemon mode, setting this to `true` will enable a TCP server listening for inbound connections + /// from your Tor daemon. Refer to the `tor.anonymous_inbound` and `tor.listening_addr` fields for onion address + /// and listening configuration. In this mode, `p2p.tor_net.p2p_port` field is the advertised virtual port + /// of the hidden service. + /// + pub inbound_onion: bool, +} +``` + +### Dandelion router + +The main feature of enabling Tor is being able to send user transactions over Tor. This is in the realm of the Dandelion router's job and, as its name suggests, can be found within `cuprated/src/txpool/dandelion.rs`: + +```rust +/// The dandelion router used to send transactions to the network. +pub(super) struct MainDandelionRouter { + clearnet_router: ConcreteDandelionRouter, + tor_router: Option>, +} +``` + +The `AnonTxService` defined in `cuprated/src/txpool/dandelion/anon_net_service.rs` is a stream of anonymous peers to be selected for relaying transactions. + +The `Service>` `call` function is pretty straightforward. If a Tor router exists (meaning Tor is enabled), and if the transaction is local (meaning it is emitted by this node), then it is stemmed to a peer over the Tor network. + +```rust +fn call(...) -> Self::Future { + if req.state == TxState::Local { + if let Some(tor_router) = self.tor_router.as_mut() { + if let Some(mut peer) = tor_router.peer.take() { + return peer + .call(StemRequest(req.tx)) + //... + } + + tracing::warn!( + "failed to route tx over Tor, no connections, falling back to Clearnet" + ); + } + } + + self.clearnet_router.call(req) +} +``` + +The `AnonTxService` should be populated soon after the Cuprate P2P zone is initialized. This leads us to the next section of code: + +### Tor initialization + +The initialization of the different components of Tor is achieved in three places within cuprated. + +First, within `cuprated/src/main.rs`: + +```rust +fn main() { + // ... + + rt.block_on(async move { + //... + + // Bootstrap or configure Tor if enabled. + let tor_context = initialize_tor_if_enabled(&config).await; + + //... + } + // ... +} +``` + +The `initialize_tor_if_enabled` function (located in `cuprated/src/tor.rs`) will parse the configuration and return a `TorContext` structure: + +```rust +pub struct TorContext { + pub mode: TorMode, + + // -------- Only in Arti mode + pub bootstrapped_client: Option>, + pub arti_client_config: Option, + pub arti_onion_service: Option, +} +``` + +As per the comment, the last three fields are initialized if cuprated boots with Arti mode enabled. The onion service is only created if the inbound server is enabled. + +This structure's purpose is to propagate which mode of Tor, if enabled, is going to be used and the needed resources to the `p2p::initialize_zones_p2p` function. + +The relevant logic to this function is the following (`cuprated/src/p2p.rs`): + +```rust +// Trimmed for clarity +pub async fn initialize_zones_p2p( + config: &Config, + // ... + tor_ctx: TorContext, +) -> (NetworkInterfaces, Vec>) { + + // Start clearnet P2P. + let (clearnet, incoming_tx_handler_tx) = { + // If proxy is set + match config.p2p.clear_net.proxy { + ProxySettings::Tor => match tor_ctx.mode { + TorMode::Arti => { + start_zone_p2p::( + // ... + config.clearnet_p2p_config(), + transport_clearnet_arti_config(&tor_ctx), + ) + .await + .unwrap() + } + TorMode::Daemon | TorMode::Off => { + // ... + std::process::exit(0); + } + }, + ProxySettings::Socks(ref s) => { + // ... + } + } + }; + + // ... + + // Start Tor P2P (if enabled) + let tor = if config.p2p.tor_net.enabled { + match tor_ctx.mode { + TorMode::Off => None, + TorMode::Daemon => Some( + start_zone_p2p::( + // ... + config.tor_p2p_config(&tor_ctx), + transport_daemon_config(config), + ) + .await + .unwrap(), + ), + TorMode::Arti => Some( + start_zone_p2p::( + // ... + config.tor_p2p_config(&tor_ctx), + transport_arti_config(config, tor_ctx), + ) + .await + .unwrap(), + ), + } + } else { + None + }; + if let Some((tor, incoming_tx_handler_tx)) = tor { + network_interfaces.tor_network_interface = Some(tor); + tx_handler_subscribers.push(incoming_tx_handler_tx); + } + + // ... +} +``` + +As you can see, the `TorContext` mode is checked to initialize the `ClearNet` and `Tor` zones with the correct `Transport`. +The `start_zone_p2p::` function requires a `P2PConfig` and `TransportConfig` structures in arguments. You can notice that these structures are returned by a few functions: + +- `Config::tor_p2p_config` method from `cuprated/src/config.rs`, will return the `P2PConfig` with the help of a supplied `TorContext`. + +- `transport_arti_config`, `transport_daemon_config`, `transport_clearnet_arti_config` helpers from `cuprated/src/tor.rs`, are functions that take into argument `Config` or `TorContext` or both and return `TransportConfig`, `TransportConfig`, and `TransportConfig` respectively. diff --git a/books/user/src/SUMMARY.md b/books/user/src/SUMMARY.md index e67105f1e..2b37b4844 100644 --- a/books/user/src/SUMMARY.md +++ b/books/user/src/SUMMARY.md @@ -23,6 +23,8 @@ - [RPC](rpc.md) +- [Tor](tor.md) + - [Platform support](platform.md) - [License](license.md) diff --git a/books/user/src/tor.md b/books/user/src/tor.md new file mode 100644 index 000000000..c4c1d6123 --- /dev/null +++ b/books/user/src/tor.md @@ -0,0 +1,131 @@ +# Tor + +`cuprated` is capable of connecting to the Tor network for interacting with anonymous peers or using it to anonymize your nodes on the internet. + +## Transaction broadcasting + +The primary intent of this feature, similarly to `monerod`, is to improve your privacy by hiding the origin of a transaction broadcasted on the network. Using an anonymous overlay network yields better privacy improvements than mitigations like Dandelion++. +When `cuprated`'s Tor mode is enabled, user-initiated transactions will be broadcasted to a peer over Tor, or will fail if `cuprated` could not select one. + +## Connecting to the network + +`cuprated` can connect to the Tor network in two different ways: + +- **Arti**: `cuprated` comes with the `Arti` library embedded, which permits it to bootstrap an internal Tor router within the `cuprated` process. This router is then used to connect to or create hidden services. + +- **Tor daemon**: `cuprated` will connect to a running Tor daemon address. It will listen for incoming connections from there and use a SOCKS5 interface to connect to hidden services. This is similar to how `monerod` actually connects to Tor through `tx-proxy` and `anonymous-inbound` flags. + +> **🤷 What method to choose?** +> +> **Arti is recommended for beginners** because it is capable of generating a hidden service automatically. This makes accepting inbound connections much easier to configure. It also shows decent performance for usual usage. +> +> **Consider using an external Tor daemon for any advanced configurations**, such as vanity addresses, pluggable transports, and circuit geo-restrictions. + +## Enabling Tor + +First, select the mode you want to use in the `[tor]` section of the TOML configuration file: + +```toml +## Configuration for cuprated's Tor component +[tor] +## Enable Tor network by specifying how to connect to it. +## +## When "Daemon" is set, the Tor daemon address to use can be +## specified in `tor.daemon.address`. +## +## Type | String +## Valid values | "Arti", "Daemon", "Off" +## Examples | "Arti" +mode = "Off" # <----- Here +``` + +Other sections of the configuration file will then become relevant: + +| Section | Purpose | Notes | +|-------------------------------------|--------------------------------------|------------------------------------------------------------------------| +| `[tor.arti]` | Arti's specific parameters | Only relevant if `mode` = `"Arti"` or `p2p.clear_net.proxy` = `"Arti"` +| `[tor.daemon]` | Tor mode's specific parameters | Only relevant if `mode` = `"Daemon"` +| `[p2p.tor_net]` | Tor P2P zone parameters | +| `[p2p.tor_net.address_book_config]` | Tor P2P zone address book parameters | Avoid tuning this section if you do not know what you are doing. + +### Arti + +If you selected the `"Arti"` mode, you can start your node right away. `cuprated` will take care of bootstrapping to the Tor network at boot and connecting to Tor peers. + +### Tor Daemon + +If you selected the `"Daemon"` mode, a field within the `[tor.daemon]` section requires your attention. + +The `address` field signifies the SOCKS5 address of a/your Tor daemon that `cuprated` can use to initiate connections towards Tor. These are for outgoing connections. Most of the time, the system Tor daemon opens this interface on port 9050, in which case the default should be left. + +## Accepting inbound connections + +In order to participate in propagating transactions of other peers, your node must accept inbound connections over Tor. This requires the use of a hidden service, or commonly known as an onion service. + +To start, enable the `inbound_onion` option in your TOML configuration file: + +```toml +[p2p.tor_net] +#... +## Enable Tor inbound onion server. +## +## [...] +## +## Type | boolean +## Valid values | false, true +## Examples | false +inbound_onion = true +``` + +### Arti + +If you are using Arti, that's it. `cuprated` will auto-generate a hidden service at startup. + +A few notes: +- The onion address is generated randomly. +- The onion address is persistent across reboots. +- The onion address cannot be changed without deleting the Arti state directory. + +### Daemon + +If you are using Daemon, three fields need to be edited. + +You first need to create a hidden service within your Tor daemon configuration file. If you do not know how, please follow this guide: https://community.torproject.org/onion-services/setup/ while ignoring steps 1 and 6. + +Assuming that your hidden service is operational, that your torrc looks like this: +``` +HiddenServiceDir /var/lib/tor/my_awesome_cuprated_hs/ +HiddenServicePort 18083 127.0.0.1:18090 +``` +and supposing that your onion address is `allyouhavetodecideiswhattodowiththetimethatisgiventoyouu.onion`. + +In the `[tor.daemon]` section, +The `listening_addr` field must be set to the IP and port on which `cuprated` will listen for connections coming from your Tor daemon. This is the destination of your hidden service. +``` +HiddenServicePort 18083 [127.0.0.1:18090] <-- This part +``` +The `anonymous_inbound` field must be set to the onion address of your hidden service. In this case, `"allyouhavetodecideiswhattodowiththetimethatisgiventoyouu.onion"`. + +In the `[p2p.tor_net]` section, +The `p2p_port` field must be set to the port that other Tor peers will attempt to establish connections to. This is the virtual port of your hidden service. +``` +HiddenServicePort [18083] 127.0.0.1:18090 + ^ + The virtual port +``` + +If everything has been set correctly, your node will start to broadcast its onion address to other Tor peers in order to be reached. + +## Anonymizing cuprate + +`cuprated` can use Arti to anonymize internet connections to other nodes. This makes it possible to sync with the rest of the network while your node remains anonymous within the bounds of the Tor network. + +To enable this mode, set the `p2p.clear_net.proxy` configuration field to `"Arti"`. + +> **⚠️ Warning ⚠️** +> +> A few caveats must be acknowledged: +> +> - The blockchain is of significant size for low-throughput networks such as Tor. You should expect the syncing process to be much longer. +> - Connecting to a lot of peers will put a load on your Tor circuit's nodes, which can eventually evict you. You should be conservative with your maximum number of peers. +> - Incoming internet connections are disabled in this mode. This is inherent to the Monero P2P protocol, which identifies the remote IP address as being end-to-end routable. However, your node will be seen through Tor exit nodes, which refuse incoming connections and are even more unlikely to forward traffic to you specifically.