mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
fix(youko): run transmission in its own network namespace (#324)
This commit is contained in:
1 parent
f326dcb139
commit
2314754655
4 files changed
+123
-10
No files matched your search
@@ -42,4 +42,7 @@ extend-ignore-re = [
|
||||
"f5e0dc",
|
||||
"f9e2af",
|
||||
"fab387",
|
||||
|
||||
# The upstream dictionary maps UPnP to "UOnP", which is wrong; keep UPnP.
|
||||
"UPnP",
|
||||
]
|
||||
@@ -21,13 +21,22 @@ Services:
|
||||
1. victoriametrics + vmalert + alertmanager + grafana: Monitor the metrics of my homelab.
|
||||
1. homepage + uptime-kuma: Service dashboard and uptime checks.
|
||||
|
||||
All the services assumes a reverse proxy to be setup in the front, they are all listening on
|
||||
localhost, and a caddy service is listening on the local network interface and proxy the requests to
|
||||
the services.
|
||||
All the services assume a reverse proxy in front: they listen on localhost, and a caddy service
|
||||
listens on the local network interface and proxies requests to them. The exception is transmission,
|
||||
which runs in its own network namespace and binds its RPC to `192.168.5.118`.
|
||||
|
||||
## transmission networking
|
||||
|
||||
transmission deliberately does not use the host's default gateway `suzi` (192.168.5.178): it is a
|
||||
mihomo transparent proxy that hijacks DNS to fake IPs (only routable through the proxy) and has no
|
||||
UPnP/NAT-PMP, so a proxied transmission can never accept inbound peers.
|
||||
|
||||
Instead the service runs in its own network namespace, with a veth on `br0`, the address
|
||||
`192.168.5.118`, a default route at the main router (`192.168.5.1`), a real resolver, and a fixed
|
||||
MAC for a stable IPv6 IID. It listens on `192.168.5.118:51413` (TCP/UDP and IPv6) and caddy reaches
|
||||
its RPC at `192.168.5.118:9091`. See `transmission.nix` for the implementation.
|
||||
|
||||
## TODO
|
||||
|
||||
- transmission has no reachable inbound port: youko's default gateway is `suzi` (192.168.5.178,
|
||||
mihomo transparent proxy, no UPnP/NAT-PMP) and its traffic is proxied, so
|
||||
`port-forwarding-enabled` never maps 51413. Fix by routing transmission **direct** and using its
|
||||
public IPv6 (or a static v4 port-forward on the main router, or a port-forwarding VPN).
|
||||
- Forward `51413/tcp+udp` to `192.168.5.118` on the main router, or enable UPnP there. NAT-PMP and
|
||||
UPnP now target the main router, which is reachable from the namespace.
|
||||
@@ -160,7 +160,9 @@ in
|
||||
virtualHosts."transmission.writefor.fun".extraConfig = ''
|
||||
${privateTlsConfig}
|
||||
encode zstd gzip
|
||||
reverse_proxy http://localhost:9091
|
||||
# transmission runs in its own network namespace (see transmission.nix),
|
||||
# so its RPC is reached over that namespace address, not localhost.
|
||||
reverse_proxy http://192.168.5.118:9091
|
||||
'';
|
||||
|
||||
# Monitoring
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
myvars,
|
||||
pkgs,
|
||||
...
|
||||
@@ -7,6 +8,32 @@
|
||||
let
|
||||
dataDir = "/data/fileshare/public/transmission";
|
||||
name = "transmission";
|
||||
|
||||
# Transmission runs in its own network namespace with a direct path to the
|
||||
# main router: the default host gateway is suzi, a transparent proxy that
|
||||
# hijacks DNS to fake IPs (which only resolve on the proxied path) and has no
|
||||
# UPnP/NAT-PMP. In the namespace it gets its own address, a real resolver, and
|
||||
# a default route at the main router, so peers can reach it and it can resolve
|
||||
# real addresses without going through the proxy.
|
||||
netns = "transmission";
|
||||
nsIp = "192.168.5.118";
|
||||
hostVeth = "tr-hveth";
|
||||
nsVeth = "tr-nveth";
|
||||
|
||||
# Real resolvers for the namespace (v4 first; the host's resolver is suzi).
|
||||
resolvConf = pkgs.writeText "transmission-resolv.conf" (
|
||||
lib.concatMapStrings (ns: "nameserver ${ns}\n") myvars.networking.nameservers
|
||||
);
|
||||
|
||||
# Drop nss-resolve from the host's nsswitch for this service only: it talks to
|
||||
# the host's resolved over a unix socket, which would resolve through suzi and
|
||||
# return fake IPs again, bypassing the resolvers above.
|
||||
nsswitchConf = pkgs.runCommand "transmission-nsswitch.conf" { } ''
|
||||
sed -E 's/ resolve \[!UNAVAIL=return\]//' ${config.environment.etc."nsswitch.conf".source} > $out
|
||||
'';
|
||||
|
||||
ip = "${pkgs.iproute2}/bin/ip";
|
||||
sysctl = "${pkgs.procps}/bin/sysctl";
|
||||
in
|
||||
{
|
||||
# Join the shared fileshare group so transmission can read/write files
|
||||
@@ -66,7 +93,10 @@ in
|
||||
|
||||
# rpc = Web Interface
|
||||
rpc-port = 9091;
|
||||
rpc-bind-address = "127.0.0.1";
|
||||
# The netns has its own loopback, so caddy (on the host) reaches the RPC
|
||||
# over the namespace address instead of 127.0.0.1. Plain HTTP on the LAN;
|
||||
# the RPC auth and whitelists below still apply.
|
||||
rpc-bind-address = nsIp;
|
||||
anti-brute-force-enabled = true;
|
||||
# After this amount of failed authentication attempts is surpassed,
|
||||
# the RPC server will deny any further authentication attempts until it is restarted.
|
||||
@@ -103,7 +133,11 @@ in
|
||||
lpd-enabled = true;
|
||||
# The peer port to listen for incoming connections.
|
||||
peer-port = 51413;
|
||||
# Enable UOnP or NAT-PMP to forward a port through your firewall(NAT).
|
||||
# Bind peer sockets to the namespace address, so the listener, outgoing
|
||||
# source, and UPnP/NAT-PMP advertisement all use the address the main
|
||||
# router forwards 51413 to.
|
||||
bind-address-ipv4 = nsIp;
|
||||
# Enable UPnP or NAT-PMP to forward a port through your firewall(NAT).
|
||||
# https://github.com/transmission/transmission/blob/main/docs/Port-Forwarding-Guide.md
|
||||
port-forwarding-enabled = true;
|
||||
|
||||
@@ -137,6 +171,71 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# Create the namespace and its veth (the host end becomes another port on
|
||||
# br0), so the service below can join it and appear as its own LAN host.
|
||||
systemd.services.transmission-netns = {
|
||||
description = "Network namespace for transmission";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "systemd-networkd.service" ];
|
||||
preStart = ''
|
||||
for _ in $(seq 1 30); do
|
||||
${ip} link show br0 >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
${ip} link show br0 >/dev/null 2>&1 || { echo "br0 is not ready" >&2; exit 1; }
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStop = "${ip} netns del ${netns}";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
# idempotent: clean up leftovers from an unclean stop
|
||||
${ip} netns del ${netns} 2>/dev/null || true
|
||||
${ip} link del ${hostVeth} 2>/dev/null || true
|
||||
|
||||
${ip} netns add ${netns}
|
||||
${ip} link add ${hostVeth} type veth peer name ${nsVeth}
|
||||
${ip} link set ${hostVeth} master br0
|
||||
${ip} link set ${hostVeth} up
|
||||
${ip} link set ${nsVeth} netns ${netns}
|
||||
|
||||
${ip} -n ${netns} link set lo up
|
||||
# Pin the MAC so the SLAAC-based IPv6 address is stable across reboots.
|
||||
${ip} -n ${netns} link set ${nsVeth} address 02:00:00:00:76:01
|
||||
# Keep a stable address: temporary addresses rotate and would not match
|
||||
# what transmission listens on and advertises.
|
||||
${ip} netns exec ${netns} ${sysctl} -q -w net.ipv6.conf.${nsVeth}.use_tempaddr=0 || true
|
||||
${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}
|
||||
'';
|
||||
};
|
||||
|
||||
# Join the namespace and resolve through real resolvers instead of suzi.
|
||||
systemd.services.transmission = {
|
||||
after = [ "transmission-netns.service" ];
|
||||
requires = [ "transmission-netns.service" ];
|
||||
# restart together with the namespace; otherwise the service would stay in
|
||||
# the deleted namespace after the netns unit is recreated
|
||||
partOf = [ "transmission-netns.service" ];
|
||||
serviceConfig = {
|
||||
NetworkNamespacePath = "/run/netns/${netns}";
|
||||
# Joining a namespace owned by the host is incompatible with the module's
|
||||
# default PrivateUsers=; the rest of the sandbox stays in place.
|
||||
PrivateUsers = false;
|
||||
# Resolve directly instead of through the host's systemd-resolved (suzi).
|
||||
BindReadOnlyPaths = [
|
||||
"${resolvConf}:/etc/resolv.conf"
|
||||
"${nsswitchConf}:/etc/nsswitch.conf"
|
||||
];
|
||||
# glibc's resolver uses netlink to pick source addresses; the module's
|
||||
# default set does not include it.
|
||||
RestrictAddressFamilies = [ "AF_NETLINK" ];
|
||||
};
|
||||
};
|
||||
|
||||
# The /data mounts are `nofail`, so nothing orders transmission after them;
|
||||
# without this the unit can start before /data/fileshare/public is mounted and
|
||||
# fail its mount-namespace setup (seen on the host, where the tasks race).
|
||||
|
||||
Reference in new issue
Block a user