Commit Graph

3 Commits

Author SHA1 Message Date
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
Alexander 62db1e2a9e feat(torad): bring up a WireGuard tunnel in user space from a config file
`--wireguard-config` is now the only thing an operator supplies. torad
re-executes itself inside a private user+network namespace, brings up a
userspace WireGuard datapath, and binds every torrent socket to it. No
root, no wg-quick, no pre-created interface, no kernel module, no one-time
host setup.

The mechanism differs from what was planned, because the plan's
assumptions did not survive being checked against the actual binaries:

- pasta already creates an unprivileged user+net namespace with full
  capabilities and a configured tap interface, so the hand-rolled
  unshare(CLONE_NEWUSER|CLONE_NEWNET) with uid_map, gid_map and
  setgroups=deny is gone. torad re-execs itself under pasta instead.

- WG_SOCKET_DIRECTORY does not exist; wireguard-go's socket directory is
  a build-time linker variable. WG_UAPI_FD does not avoid it either,
  because UAPIListen inotify-watches that path even when handed a
  pre-bound socket. Since /var/run is a symlink to /run, a tmpfs over
  /var inside a private mount namespace makes /var/run/wireguard
  writable while leaving the real /run visible -- which matters, because
  torad's gRPC socket lives under /run/user and the aggregator connects
  to it from the host.

- The peer endpoint needs a host route via pasta's gateway before the
  default route moves to wg0, or WireGuard's own handshake is routed
  into the tunnel it is trying to establish.

The device is configured by speaking WireGuard's UAPI protocol over its
unix socket, so wireguard-tools is not a dependency either.

If the datapath exits, torad shuts down instead of routing around it: a
live torad with a dead tunnel is the failure mode that leaks.

pasta also creates a PID namespace, so the pid file is written on the
host before the re-exec and not again inside -- otherwise it would
record PID 1, which names init when read from the host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:01:33 +02:00
Alexander e321300f67 feat(torad): add WireGuard config parser and user-namespace preflight
Groundwork for VPN mode; nothing calls it yet, so the module is dead code
until the namespace bootstrap lands.

vpn::config parses the ProtonVPN-style WireGuard INI into typed sections.
`InterfaceSection` gets a hand-written Debug that redacts the private
key — the derived one would print it verbatim, and this struct is about
to start flowing through error contexts during interface setup.

vpn::namespace::preflight checks user.max_user_namespaces and fails with
the sysctl name and its remediation when unprivileged user namespaces are
disabled. The decision logic is split into `evaluate` so the tests
exercise the real code path rather than a copy of it — the /proc file
itself cannot be mocked.

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