Files
tora/devenv.nix
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

118 lines
3.6 KiB
Nix

{
pkgs,
lib,
config,
inputs,
...
}:
let
# devenv's `languages.rust.import` only builds single root-package crates
# (it hardcodes `cargoNix.rootCrate.build`). tora is a virtual workspace with
# no root package, so we drive crate2nix directly and select the `torad` member
# out of `workspaceMembers`. src = ./. gives crate2nix the whole workspace, so
# torad's path-dependency on ../proto (tora-proto) resolves.
crate2nixTools = pkgs.callPackage "${inputs.crate2nix}/tools.nix" { };
cargoNix =
pkgs.callPackage
(crate2nixTools.generatedCargoNix {
name = "tora";
src = ./.;
})
{
# Build with the toolchain configured for this dev environment,
# matching what languages.rust.import does internally.
buildRustCrateForPkgs =
_:
pkgs.buildRustCrate.override {
rustc = config.languages.rust.toolchainPackage;
cargo = config.languages.rust.toolchainPackage;
};
};
in
{
languages.rust.enable = true;
git-hooks.hooks = {
clippy = {
enable = true;
settings.allFeatures = true;
};
treefmt.enable = true;
};
treefmt = {
enable = true;
config.programs = {
nixfmt.enable = true;
rustfmt.enable = true;
};
};
packages = with pkgs; [
git
just
protobuf
buf
grpcurl
# VPN mode (torad --wireguard-config). Both are runtime dependencies torad
# execs, not build inputs.
# passt -> `pasta`, which creates the unprivileged user+network
# namespace torad re-executes itself into.
# wireguard-go -> the userspace WireGuard datapath.
# `wireguard-tools` is deliberately absent: torad configures the device by
# speaking the UAPI protocol over its socket, so no `wg` binary is needed.
# `iproute2` provides the `ip` used for the tunnel address and routes.
passt
wireguard-go
iproute2
# Protobuf generators
protoc-gen-prost-crate
protoc-gen-prost-serde
protoc-gen-tonic
protoc-gen-prost
opencode
];
services.postgres = {
enable = true;
initialDatabases = [
{
name = "toradb";
schema = ./db/schema.sql;
}
];
};
env.PGDATABASE = "toradb";
env.DATABASE_URL = "postgresql://${builtins.getEnv "USER"}@localhost/${config.env.PGDATABASE}?host=${config.env.PGHOST}";
env.TORAD_SOCKET = "${config.env.DEVENV_RUNTIME}/torad.sock";
env.TORAD_PID_FILE = "${config.env.DEVENV_RUNTIME}/torad.pid";
processes.torad = {
# VPN mode is opt-in so the default `devenv up` is unchanged: set
# TORAD_WIREGUARD_CONFIG to a wg-quick config path and torad will re-exec
# itself into a namespace and route all torrent traffic through the tunnel.
# torad reads that variable itself (clap `env`), so nothing is needed here
# beyond leaving it unset by default.
exec = ''
${config.outputs.torad}/bin/torad --socket "$TORAD_SOCKET" --pid-file "$TORAD_PID_FILE"
'';
# Waits for postgres's own readiness probe (pg_isready + SELECT 1), not just process start.
after = [ "devenv:processes:postgres" ];
# torad speaks gRPC over a Unix socket and does not register reflection,
# so grpcurl would need proto files. Instead use the `tora health` CLI
# (already knows the proto, reads TORAD_SOCKET env), which exits 0 iff
# torad reports SERVING. torad's poll_db_health flips the status to
# NotServing when postgres ping fails, so this also gates on a live DB.
ready.exec = "${config.outputs.tora}/bin/tora health";
};
outputs = {
torad = cargoNix.workspaceMembers.torad.build;
tora = cargoNix.workspaceMembers.tora.build;
};
}