Files
tora/crates/torad/src/vpn/mod.rs
T
Alexander b489ff7135 feat(torad): route source fetches through the tunnel and package VPN mode
Three things, all tail end of VPN mode.

Source fetches are HTTP, not BitTorrent, so librqbit's SO_BINDTODEVICE
never covered them. SourceResolver now holds two clients and picks one
per URL: remote indexer and .torrent fetches go through a client bound
to wg0, so a future route change cannot quietly send them around the
tunnel; loopback URLs keep the unbound client, because pasta splices the
namespace's loopback to the host's and that is how a self-hosted Jackett
stays reachable. That traffic never leaves the machine, so keeping it off
the tunnel is deliberate.

This replaces the planned request-time URL rewriting, which turned out to
be unnecessary: measured, pasta reaches host services on 127.0.0.1 from
inside the namespace even when they bind after the namespace starts, so
neither Jackett URLs nor a loopback DATABASE_URL need touching.

Second, a defect the packaging work surfaced: killing the pid in the pid
file killed pasta but left torad running, reparented to init, with a dead
tap interface -- the daemon outliving the only documented way to stop it.
The re-executed process now sets PR_SET_PDEATHSIG so it dies with pasta.

Third, packaging: passt, wireguard-go and iproute2 in both devenv files,
and the module documentation states the Linux-only, leech-only and
pinned-endpoint limitations along with what happens when the tunnel drops.
wireguard-tools is deliberately absent -- the device is configured over
UAPI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:09:03 +02:00

89 lines
4.3 KiB
Rust

//! VPN mode: bring up a WireGuard tunnel from a config file, entirely in user
//! space, and confine every torrent socket to it.
//!
//! # Using it
//!
//! ```text
//! torad --wireguard-config /path/to/wg0.conf ...
//! ```
//!
//! That is the whole interface. No `sudo`, no `wg-quick`, no pre-created
//! interface, no kernel module, and no one-time host setup. `--bind-device` is
//! ignored when this is given; the tunnel is the bind device.
//!
//! Requires `pasta` (from passt), `wireguard-go`, and `ip` on `PATH`.
//! [`namespace::preflight`] checks for all three before anything is torn down,
//! so a missing one fails immediately with a message naming it.
//!
//! # How it works
//!
//! ```text
//! host │ torad's user+network namespace
//! ────────────────────────────────┼────────────────────────────────────────
//! aggregator ──unix socket───────────► torad gRPC server
//! postgres, jackett ◄────────────┐ (mount ns shares the path, so the
//! │ host still reaches the socket)
//! │
//! │ pasta ── tap iface, host addressing
//! │ ├─ 127.0.0.1 → host loopback
//! │ └─ 169.254.1.1 → host loopback
//! │
//! │ wireguard-go ── wg0 (TUN)
//! │ └─ default route ──► internet
//! │
//! │ librqbit: SO_BINDTODEVICE(wg0)
//! │ peers, trackers, DHT, LSD
//! ```
//!
//! 1. torad re-executes itself under `pasta`, which supplies an unprivileged
//! user+network namespace with `CAP_NET_ADMIN` ([`namespace`]).
//! 2. It unshares a mount namespace so `wireguard-go` has a writable socket
//! directory ([`namespace::enter_mount_namespace`]).
//! 3. It starts `wireguard-go` and configures the device over its UAPI socket
//! ([`uapi`]) — no `wg` binary involved, so `wireguard-tools` is not a
//! dependency.
//! 4. It assigns the address, pins the peer endpoint outside the tunnel, moves
//! the default route onto `wg0`, and installs the tunnel's DNS ([`tunnel`]).
//! 5. librqbit binds every socket to `wg0` via `SO_BINDTODEVICE`.
//!
//! Host services stay reachable without any URL rewriting: pasta splices the
//! namespace's loopback to the host's, so `http://localhost:9117/...` for
//! Jackett and a loopback `--database-url` for Postgres work unchanged, and
//! deliberately do not traverse the tunnel.
//!
//! # When the tunnel drops
//!
//! Two independent mechanisms, both fail-closed:
//!
//! - `SO_BINDTODEVICE` is kernel-enforced. If `wg0` goes away, librqbit's
//! sockets error rather than falling back to the host route.
//! - torad supervises `wireguard-go` and shuts down if it exits. A live torad
//! with a dead tunnel is the failure mode that leaks.
//!
//! DNS is inside the tunnel too, so a dead tunnel means tracker hostnames stop
//! resolving rather than being resolved by the host's resolver.
//!
//! # Limitations
//!
//! - **Linux only.** User namespaces, `SO_BINDTODEVICE`, and pasta are all
//! Linux-specific.
//! - **Leech-only.** librqbit runs with `listen: None`, so there is no
//! listening socket and no uTP in either direction. Incoming peer
//! connections would need NAT-PMP port forwarding through the provider,
//! which is not implemented.
//! - **The peer endpoint is pinned at startup.** It is resolved once and given
//! a host route so the handshake does not enter its own tunnel; an endpoint
//! that roams to a new address mid-session is not followed. Restart to pick
//! up a new one.
//! - **Userspace crypto.** Throughput is lower than the kernel module's.
//! - **One peer.** [`config`] rejects configs with more than one `[Peer]`
//! rather than silently using the last.
pub mod config;
pub mod namespace;
pub mod tunnel;
pub mod uapi;
pub use config::{WireguardConfig, parse};
pub use tunnel::{INTERFACE, Tunnel};