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>
`--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>
The parser stored every value as a raw String and only ever rejected a
missing key. Those values are about to be handed to `ip addr add`, route
setup, and the wireguard-go UAPI socket in Wave 3, none of which give a
diagnostic worth reading: a mistyped address comes back as a bare EINVAL
or a UAPI errno with no hint of which config line caused it.
Parse into typed values instead — Key, IpCidr, Endpoint — and report
failures with a line number and the offending value.
Key validation was not in the original scope, but the fixture in this
file's own tests was a 41-character "private key" that parsed clean:
long enough to look right, short enough to fail at the UAPI socket three
waves later. Decoding here also yields the hex encoding that UAPI
expects, so it replaces work rather than adding it. Errors on the
private key deliberately describe only the shape of the problem, never
the input, and there is a test that fails if the value leaks.
Address and DNS become lists because wg-quick allows comma-separated
values and dual-stack providers emit them; parsing only the first would
have silently dropped the IPv6 address.
Also reject what used to be accepted in silence: a second [Peer] or
[Interface] section, a repeated key within a section, an unknown
section, and a key appearing before any section header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
reqwest honours HTTP_PROXY/ALL_PROXY from the environment by default. A
self-hosted Jackett is normally reached over loopback, and routing
loopback through an inherited proxy breaks it — which matters once torad
runs in an environment where those vars are set for VPN reasons.
Extracts the builder into `build_http_client` so the behaviour is
directly testable, and adds a test that serves one response from a
loopback listener while ALL_PROXY points at a dead port. The test is
falsifiable: removing .no_proxy() makes it fail with ConnectionRefused
against the proxy address, which was verified before committing.
The proxy URL is deliberately http:// rather than socks5:// — reqwest is
built here without its `socks` feature, so a SOCKS proxy would be ignored
and the test would pass either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinned exactly (=9.0.0-rc.0) — no v9 stable exists yet, so a caret range
would silently drift onto a future prerelease.
Two source changes were needed for the upgrade:
- `torrent_from_bytes_ext` moved to `librqbit_core::torrent_metainfo::
torrent_from_bytes` and no longer wraps the result in a `meta` field.
- `peer_stats.live` / `.not_needed` are now `u32`, so the casts are
redundant.
v9 also adds `SessionOptions::bind_device_name`, which is the reason for
the upgrade: it applies SO_BINDTODEVICE to every librqbit socket — peer
connections, trackers, DHT and LSD. Binding those to a VPN interface
means that when the interface goes away the sockets error out instead of
falling back to the host route, giving a kernel-enforced kill switch.
Exposed as --bind-device / TORAD_BIND_DEVICE; unset reproduces today's
behaviour exactly.
Note that `listen` stays at its default of None, so there is no listener
and no uTP socket in either direction — torad is TCP-only and leech-only.
That is unchanged from v8 but now written down, since incoming
connections need NAT-PMP port forwarding that we have not built.
Also drops torad's `nix` pin from 0.29 to 0.31.3 to match the sibling
tora crate; the workspace was carrying three copies.
Verified end-to-end against a real swarm rather than by compiling alone:
a 755 MiB torrent added via HTTP .torrent URL, driven through
pending -> downloading -> finished, with pause/resume on an active
torrent, remove, and the notification stream all exercised. Evidence in
.omo/evidence/task-5-torad-vpn-namespace.txt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>