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>
This commit is contained in:
@@ -1,10 +1,83 @@
|
||||
//! VPN mode: bring up a WireGuard tunnel from a config file, entirely in user
|
||||
//! space, and confine every torrent socket to it.
|
||||
//!
|
||||
//! The operator passes `--wireguard-config`. Nothing else is required: no
|
||||
//! `sudo`, no `wg-quick`, no pre-created interface, no kernel module, and no
|
||||
//! one-time host setup. See [`namespace`] for how the namespace is obtained
|
||||
//! and [`tunnel`] for the bring-up order.
|
||||
//! # 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;
|
||||
|
||||
Reference in New Issue
Block a user