From 1256570a846fbb14481966ce7237f323bfa1d0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Sat, 26 Sep 2026 22:40:02 +0800 Subject: [PATCH] feat(youko): firewall and tune transmission, stop homepage RPC probes (#326) * fix(youko): stop homepage from probing the authenticated transmission RPC The siteMonitor hit /transmission/rpc unauthenticated, so every probe returned 401 and counted against transmission's anti-brute-force threshold. * feat(youko): tune transmission and firewall its network namespace Raise the upload limit and queues, relax required encryption to preferred, and add a default-drop nftables input chain inside the netns so the peer port stays open while everything else is closed. Allowlist the nftables 'iif' keyword for typos. * feat(youko): export transmission metrics and add a Grafana dashboard nixpkgs has no transmission exporter, so add a small stdlib Python exporter that queries the authenticated netns RPC on loopback, scrape it from VictoriaMetrics, and ship a Transmission dashboard. * fix(youko): cap transmission upload at 2500 kB/s 30 MB/s ignored the 50 Mbps uplink; keep seeding around 20 Mbps. * Revert "feat(youko): export transmission metrics and add a Grafana dashboard" This reverts commit 9df7a265268b279b7a5ebd44ea8a9aae91d6e888. --- .typos.toml | 3 ++ .../homepage/config/services.yaml | 3 +- .../homelab-services/transmission.nix | 42 ++++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/.typos.toml b/.typos.toml index ec378d48..8abd3921 100644 --- a/.typos.toml +++ b/.typos.toml @@ -45,4 +45,7 @@ extend-ignore-re = [ # The upstream dictionary maps UPnP to "UOnP", which is wrong; keep UPnP. "UPnP", + + # nftables keyword `iif` (input interface); the spell checker wants `if`. + "iif", ] diff --git a/hosts/12kingdoms-youko/homelab-services/oci-containers/homepage/config/services.yaml b/hosts/12kingdoms-youko/homelab-services/oci-containers/homepage/config/services.yaml index c504c284..f471cf4f 100644 --- a/hosts/12kingdoms-youko/homelab-services/oci-containers/homepage/config/services.yaml +++ b/hosts/12kingdoms-youko/homelab-services/oci-containers/homepage/config/services.yaml @@ -74,7 +74,8 @@ icon: transmission.png href: "https://transmission.writefor.fun/" description: Transmission BitTorrent client - siteMonitor: "https://transmission.writefor.fun/" + # No siteMonitor: the RPC requires auth, so the probe returns 401 and + # every hit counts against transmission's anti-brute-force threshold. - Immich: icon: immich.png href: "https://immich.writefor.fun/" diff --git a/hosts/12kingdoms-youko/homelab-services/transmission.nix b/hosts/12kingdoms-youko/homelab-services/transmission.nix index cb73569b..02c0ddf6 100644 --- a/hosts/12kingdoms-youko/homelab-services/transmission.nix +++ b/hosts/12kingdoms-youko/homelab-services/transmission.nix @@ -34,6 +34,27 @@ let ip = "${pkgs.iproute2}/bin/ip"; sysctl = "${pkgs.procps}/bin/sysctl"; + nft = "${pkgs.nftables}/bin/nft"; + + # The namespace is a first-class host on br0, so once the router's IPv6 + # firewall is relaxed it is reachable from the Internet with no host firewall + # in front of it. Keep the peer port open (that is the point) and the RPC + # LAN-only; drop everything else. + netnsFirewall = pkgs.writeText "transmission-netns.nft" '' + table inet transmission-fw { + chain input { + type filter hook input priority filter; policy drop; + iif "lo" accept + ct state established,related accept + meta l4proto icmp accept + meta l4proto ipv6-icmp accept + tcp dport 51413 accept + udp dport 51413 accept + udp dport 6771 accept + ip saddr ${myvars.networking.lanCidr} tcp dport 9091 accept + } + } + ''; in { # Join the shared fileshare group so transmission can read/write files @@ -89,7 +110,9 @@ in # 0 = Prefer unencrypted connections, # 1 = Prefer encrypted connections, # 2 = Require encrypted connections; default = 1) - encryption = 2; + # Keep it at "prefer": forcing encryption drops peers that cannot do + # MSE, which costs upload opportunities on public swarms. + encryption = 1; # rpc = Web Interface rpc-port = 9091; @@ -101,7 +124,9 @@ in # After this amount of failed authentication attempts is surpassed, # the RPC server will deny any further authentication attempts until it is restarted. # This is not tracked per IP but in total. - anti-brute-force-threshold = 20; + # Health-check probes (homepage/uptime-kuma) hit the authenticated RPC + # gateway and each 401 counts, so keep this well above a 401 burst. + anti-brute-force-threshold = 100; rpc-authentication-required = true; # Comma-delimited list of IP addresses. @@ -144,9 +169,10 @@ in # "normal" speed limits speed-limit-down-enabled = true; speed-limit-down = 30000; # KB/s + # Keep seeding under the 50 Mbps uplink (~20 Mbps). speed-limit-up-enabled = true; - speed-limit-up = 500; # KB/s - upload-slots-per-torrent = 8; + speed-limit-up = 2500; # KB/s + upload-slots-per-torrent = 16; # Start torrents as soon as they are added start-added-torrents = true; @@ -155,7 +181,7 @@ in # When true, Transmission will only download # download-queue-size non-stalled torrents at once. download-queue-enabled = true; - download-queue-size = 5; + download-queue-size = 10; # When true, torrents that have not shared data for # queue-stalled-minutes are treated as 'stalled' @@ -167,7 +193,8 @@ in # When true. Transmission will only seed seed-queue-size # non-stalled torrents at once. seed-queue-enabled = true; - seed-queue-size = 10; + # With 30+ torrents, 10 was leaving most seeds queued (never uploading). + seed-queue-size = 50; }; }; @@ -210,6 +237,9 @@ in ${ip} -n ${netns} link set ${nsVeth} up ${ip} -n ${netns} addr add ${nsIp}/24 dev ${nsVeth} ${ip} -n ${netns} route add default via ${myvars.networking.mainGateway} + + # Apply the namespace firewall after the interface is up. + ${ip} netns exec ${netns} ${nft} -f ${netnsFirewall} ''; };