From 7107fb77a13d77a360816d853d0cf32a570c0671 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 14:49:31 -0600 Subject: [PATCH 1/5] feat: tweaking torrent vm --- modules/nixos/guests/torrent/default.nix | 134 +++++++++++++++++++++-- secrets/secrets.yaml | 9 +- systems/ceres/config/networking.nix | 63 +++++++---- 3 files changed, 171 insertions(+), 35 deletions(-) diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index fe7f6a5..875764a 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -125,9 +125,109 @@ in "d ${serviceCfg.varPaths.path0}/downloads 755 ${serviceCfg.name} ${serviceCfg.name} -" ]; - services.qbittorrent = { - after = [ "wg-quick-wg0.service" ]; - requires = [ "wg-quick-wg0.service" ]; + services = { + qbittorrent = { + after = [ "wg-quick-wg0.service" ]; + requires = [ "wg-quick-wg0.service" ]; + }; + + natpmp-portforward = { + description = "NAT-PMP Port Forwarding for VPN"; + after = [ + "wg-quick-wg0.service" + "qbittorrent.service" + ]; + requires = [ + "wg-quick-wg0.service" + "qbittorrent.service" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = "10s"; + }; + script = '' + PASSWORD=$(cat /run/secrets/qbittorrent-pass) + # Wait for qBittorrent to be ready + echo "Waiting for qBittorrent to start..." + sleep 10 + + # Enable IP forwarding + echo 1 > /proc/sys/net/ipv4/ip_forward + + while true; do + echo "Requesting port forwarding from VPN..." + + # Get the forwarded port for UDP + UDP_OUTPUT=$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 udp 60 -g 10.2.0.1 2>&1) + UDP_PORT=$(echo "$UDP_OUTPUT" | ${pkgs.gnugrep}/bin/grep "Mapped public port" | ${pkgs.gawk}/bin/awk '{print $4}' | head -1) + + # Get the forwarded port for TCP + TCP_OUTPUT=$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 tcp 60 -g 10.2.0.1 2>&1) + TCP_PORT=$(echo "$TCP_OUTPUT" | ${pkgs.gnugrep}/bin/grep "Mapped public port" | ${pkgs.gawk}/bin/awk '{print $4}' | head -1) + + if [ -n "$UDP_PORT" ] && [ -n "$TCP_PORT" ]; then + echo "Port forwarding successful: UDP=$UDP_PORT, TCP=$TCP_PORT" + + # Clear old PREROUTING rules to avoid duplicates + ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -p tcp -j DNAT --to-destination 10.2.0.2 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -p udp -j DNAT --to-destination 10.2.0.2 2>/dev/null || true + + # Forward traffic from enp0s5 (VM's main interface) to wg0 (WireGuard interface) + ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -p tcp --dport "$TCP_PORT" -j DNAT --to-destination 10.2.0.2:$TCP_PORT + ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -p udp --dport "$UDP_PORT" -j DNAT --to-destination 10.2.0.2:$UDP_PORT + + # Enable masquerading for responses + ${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -o wg0 -j MASQUERADE 2>/dev/null || \ + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE + + # Allow forwarding between interfaces + ${pkgs.iptables}/bin/iptables -C FORWARD -i enp0s5 -o wg0 -j ACCEPT 2>/dev/null || \ + ${pkgs.iptables}/bin/iptables -A FORWARD -i enp0s5 -o wg0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -C FORWARD -i wg0 -o enp0s5 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o enp0s5 -m state --state RELATED,ESTABLISHED -j ACCEPT + + echo "Firewall forwarding rules updated for ports: UDP=$UDP_PORT, TCP=$TCP_PORT" + + # Open the ports in the firewall dynamically (INPUT chain) + ${pkgs.iptables}/bin/iptables -C INPUT -p udp --dport "$UDP_PORT" -j ACCEPT 2>/dev/null || \ + ${pkgs.iptables}/bin/iptables -A INPUT -p udp --dport "$UDP_PORT" -j ACCEPT + + ${pkgs.iptables}/bin/iptables -C INPUT -p tcp --dport "$TCP_PORT" -j ACCEPT 2>/dev/null || \ + ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport "$TCP_PORT" -j ACCEPT + + echo "Firewall INPUT rules updated for ports: UDP=$UDP_PORT, TCP=$TCP_PORT" + + # Update qBittorrent listening port via API + echo "Logging into qBittorrent API..." + COOKIE=$(${pkgs.curl}/bin/curl -s -i \ + --header "Referer: http://localhost:${toString serviceCfg.ports.port0}" \ + --data "username=user&password=$PASSWORD" \ + "http://localhost:${toString serviceCfg.ports.port0}/api/v2/auth/login" | \ + ${pkgs.gnugrep}/bin/grep -i "set-cookie" | ${pkgs.gawk}/bin/awk -F'SID=|;' '{print $2}') + if [ -n "$COOKIE" ]; then + echo "Authentication successful, updating port..." + ${pkgs.curl}/bin/curl -s \ + --cookie "SID=$COOKIE" \ + --data "json={\"listen_port\":$UDP_PORT}" \ + "http://localhost:${toString serviceCfg.ports.port0}/api/v2/app/setPreferences" + + echo "Updated qBittorrent listening port to $UDP_PORT" + else + echo "WARNING: Failed to authenticate with qBittorrent API" + fi + else + echo "ERROR: Failed to get forwarded ports" + echo "UDP output: $UDP_OUTPUT" + echo "TCP output: $TCP_OUTPUT" + exit 1 + fi + + sleep 45 + done + ''; + }; }; }; @@ -173,7 +273,7 @@ in { mountPoint = "/run/secrets"; proto = "virtiofs"; - source = "/run/secrets/proton"; + source = "/run/secrets/torrent"; tag = "host_secrets"; } ]; @@ -181,9 +281,12 @@ in environment.systemPackages = builtins.attrValues { inherit (pkgs) - wireguard-tools - speedtest-go bottom + gawk + iptables + libnatpmp + speedtest-go + wireguard-tools ; }; }; @@ -214,10 +317,15 @@ in group = "caddy"; mode = "0400"; }; - "proton/wireguard-pass" = { + "torrent/wireguard-pass" = { owner = "root"; mode = "0400"; }; + "torrent/qbittorrent-pass" = { + owner = "root"; + mode = "0400"; + }; + }; security.acme.certs."${host}" = { @@ -240,4 +348,16 @@ in "d /var/log/caddy 755 caddy caddy -" ]; }; + + networking.firewall = { + allowedTCPPorts = [ + 38834 + torrentPort + ]; + allowedUDPPorts = [ + 38834 + torrentPort + ]; + }; + } diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index db9465f..b410887 100755 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -55,8 +55,9 @@ firefly-iii: pass: ENC[AES256_GCM,data:WjHcoTuEzEq9pfw4QoqRjI4jhu5VPEMOXlHL0olg9dqUj4EGa1Shv5T/kIxdRFuao0y3zQ==,iv:4/fmFOxxDLzplsNGpSJMQOeoNviZw2c2pFlB1ZkRu+o=,tag:7TQ2q/kEFDU4tZxPx53ebw==,type:str] data: ENC[AES256_GCM,data:921LhcRTWVk24eEAQoDMV+RllSP3PbSXCCIDXlQA80Mq,iv:YXEgas77DgdyPTnBZa/ySjcERBIwmdDZJbijeNKNF24=,tag:Wj25wA7tLJ2bZ/faG9DUhg==,type:str] smtp: ENC[AES256_GCM,data:+e4MiRZ2WOZyWYpMf+By1Eb45ih4TA+svLI2+00yQk82,iv:+52+kJouMwkOSDEaOCA8V80+wT/VzNxgtCkOO68SCdk=,tag:YrtrJAXIhQpsUTEeYvrVwQ==,type:str] -proton: - wireguard-pass: ENC[AES256_GCM,data:u3riHMDyK+DxFAGpdP7zTqZMfp//W3pb3WRp/iS/pAf6ItY9PNFClm7Gh4Dn,iv:8jfPQALR+J3VZVL+a2XHwuL8P8yhZ4OcjhVbCTBADwE=,tag:H42pKD/tlDXUlIVVzdv+DQ==,type:str] +torrent: + wireguard-pass: ENC[AES256_GCM,data:fNNHuOvaRRpiS7c9n/l6lB0A1J4VboJxIh+hrMrTfjFS2grpgRATLHhjZ/wo,iv:CVZIG3Gq+O1/qPqu0XBH/5XsTpAe9xe52/CtBHaIOPI=,tag:8RfoFjz0Ecmx8O7Bt/90ig==,type:str] + qbittorrent-pass: ENC[AES256_GCM,data:W1p7cYWbBNeAtEEL7Tb0pG27TSniqTrNMN6gxFFlli27,iv:seiWOr6V8pyjioBkKKEtCXC17RctDScA37E7uFbnmzk=,tag:KYz92O6XUvJob74LnGlYNg==,type:str] backblaze: env: ENC[AES256_GCM,data:cdOYt77KocuGB3aqYz13oBokoLkEIgI1AW+cYC5uutgZYujG3PqoLEh6Gvbpzn3O+0OWg1/4UAYr4f2v7oCsgwFzPWS3HrhqC5+kIBjrPCyAnxDxlu2xaQ9hR+ogFh5UTDo=,iv:6+jx4Dj5CNV72DAss6NNYm44f9gSHco/EUBvL2o2CNI=,tag:6/cx84MgTDqQJxu/zINEeA==,type:str] repo: ENC[AES256_GCM,data:sRae9XELIfkWPaXelCdgEXIDbLTHVqGcRO0o+WA9aBfB8MUw92JjRCYgMgGXT0Apy38eszyuEHFB3XPpRmtQ7g==,iv:EilVA9zdHm6B9pTIhNxyj6Th1248nXvh0kpnEqZJ5HI=,tag:q9ASAgx5vgY0IePws4rT5Q==,type:str] @@ -73,7 +74,7 @@ sops: bXBOa1VSakoyaWxpODJEOU11QUZCaUEK8Ch9Ten3DdrPHF1DTH2qei85AlHUOaLD aNfzakake7ej+MxJYdKEU0bcWofNMKzIlZa2uM10KZSENDP8d8qlig== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-27T09:53:41Z" - mac: ENC[AES256_GCM,data:fV10gBOFgCTTMWAKv8tGnCz3TG0t0G19di4N4wEi2lsHeYDBk8ijumOM/wLY6Zds80y5u6s6hGwFpCcY5pChjQ/E/pOURHAY7rIqQmGm9yQnpECCfWLDJVgsQjliYRPD76WTEKlsevz/XCGfdITNuRAlU7tuWEEEgXvIKt+0Z/8=,iv:dA/zt7WQA5w+dZM53VunucZAQTBKRyzLaWaShhTMWzI=,tag:CUD7KGes5pNquMJfCm6dYQ==,type:str] + lastmodified: "2025-11-27T18:54:52Z" + mac: ENC[AES256_GCM,data:ZxkJZUJJ1AfDlmxAy8Botu73EPt+1prsdbX7RhU9bVNaEhpPzrvqlO74D8ek/OqFG51k1K4mdW5SWXWs/D5oR34i/yA+329j4jHNAe3Yajkx1gn/xDEa/kgiVGkc7dE3dnzmy5zr4X8U06khJl9rg+qLujke0GCgIv+82xkFFRI=,iv:0UYNIZTxXdPqrZsjVYNGfSlt6UH3+Q102EF6XeC5yh4=,tag:3oj0X73xRnGBXWdGsUv2xg==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/systems/ceres/config/networking.nix b/systems/ceres/config/networking.nix index 2b58aba..c9c3cf8 100755 --- a/systems/ceres/config/networking.nix +++ b/systems/ceres/config/networking.nix @@ -10,35 +10,37 @@ in { microvm.host.enable = true; - systemd.network.enable = true; - - systemd.network.netdevs."10-br-vms" = { - netdevConfig = { - Name = "br-vms"; - Kind = "bridge"; + systemd.network = { + enable = true; + netdevs."10-br-vms" = { + netdevConfig = { + Name = "br-vms"; + Kind = "bridge"; + }; }; - }; - systemd.network.networks."20-lan" = { - matchConfig.Name = [ - "enp10s0" - "vm-*" - ]; - networkConfig = { - Bridge = "br-vms"; + networks = { + "20-lan" = { + matchConfig.Name = [ + "enp10s0" + "vm-*" + ]; + networkConfig = { + Bridge = "br-vms"; + }; + }; + "30-br-vms" = { + matchConfig.Name = "br-vms"; + networkConfig = { + Address = "192.168.50.240/24"; + Gateway = "192.168.50.1"; + DNS = [ "192.168.50.1" ]; + }; + linkConfig.RequiredForOnline = "routable"; + }; }; }; - systemd.network.networks."30-br-vms" = { - matchConfig.Name = "br-vms"; - networkConfig = { - Address = "192.168.50.240/24"; - Gateway = "192.168.50.1"; - DNS = [ "192.168.50.1" ]; - }; - linkConfig.RequiredForOnline = "routable"; - }; - networking = { hostName = ceres.name; networkmanager.enable = false; @@ -59,6 +61,19 @@ in wireguardService.ports.port0 # WireGuard wireguardService.ports.port1 # WireGuard ]; + # Add port ranges for VPN dynamic port forwarding + allowedTCPPortRanges = [ + { + from = 30000; + to = 65535; + } + ]; + allowedUDPPortRanges = [ + { + from = 30000; + to = 65535; + } + ]; }; }; From 866c07482417a7b59145a9495af0ef0e7881f3ce Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 14:58:37 -0600 Subject: [PATCH 2/5] feat: got port forwarding working --- modules/nixos/guests/torrent/default.nix | 168 +++++++++++++++-------- 1 file changed, 111 insertions(+), 57 deletions(-) diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index 875764a..6be19be 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -12,6 +12,8 @@ let dns0 = instances.web.dns.provider0; dns0Path = "dns/${dns0}"; torrentPort = 51820; + vpnEndpoint = "185.111.110.1"; + localNet = "192.168.50.0/24"; in { microvm.vms.${serviceCfg.name} = { @@ -19,18 +21,64 @@ in config = { system.stateVersion = "25.05"; + # VPN Killswitch - configured BEFORE networking starts + boot.kernel.sysctl = { + "net.ipv4.ip_forward" = 1; + }; + networking = { + # Disable default firewall - we're doing it manually + firewall.enable = false; + + # Configure iptables killswitch at boot, before any services + localCommands = '' + # Default DROP everything + ${pkgs.iptables}/bin/iptables -P INPUT DROP + ${pkgs.iptables}/bin/iptables -P OUTPUT DROP + ${pkgs.iptables}/bin/iptables -P FORWARD DROP + + # Flush existing rules + ${pkgs.iptables}/bin/iptables -F + ${pkgs.iptables}/bin/iptables -t nat -F + ${pkgs.iptables}/bin/iptables -X + + # Allow loopback + ${pkgs.iptables}/bin/iptables -A INPUT -i lo -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT + + # Allow established/related connections + ${pkgs.iptables}/bin/iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + + # Allow SSH on all interfaces (for management) + ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport 22 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT + + # Allow local network for management (WebUI, etc) + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -j ACCEPT + + # CRITICAL: Only allow WireGuard endpoint before VPN is up + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport ${toString torrentPort} -d ${vpnEndpoint} -j ACCEPT + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -p udp --sport ${toString torrentPort} -s ${vpnEndpoint} -j ACCEPT + + # Allow DNS for WireGuard resolution (if needed) + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport 53 -d ${localNet} -j ACCEPT + + # Log dropped packets (optional, for debugging) + ${pkgs.iptables}/bin/iptables -A OUTPUT -j LOG --log-prefix "KILLSWITCH-OUT: " --log-level 4 + ${pkgs.iptables}/bin/iptables -A INPUT -j LOG --log-prefix "KILLSWITCH-IN: " --log-level 4 + ''; wg-quick.interfaces = { wg0 = { - address = [ - "10.2.0.2/32" - ]; + address = [ "10.2.0.2/32" ]; dns = [ "10.2.0.1" ]; privateKeyFile = "/run/secrets/wireguard-pass"; + peers = [ { publicKey = "QPfiwJQmt5VLEOh1ufLbi1lj6LUnwQY0tgDSh3pWx1k="; - endpoint = "185.111.110.1:${builtins.toString torrentPort}"; + endpoint = "${vpnEndpoint}:${toString torrentPort}"; allowedIPs = [ "0.0.0.0/0" "::/0" @@ -38,33 +86,43 @@ in persistentKeepalive = 25; } ]; + + # Now we can safely open the VPN tunnel for all traffic + postUp = '' + # Allow all traffic through VPN interface + ${pkgs.iptables}/bin/iptables -A INPUT -i wg0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o wg0 -j ACCEPT + + # NAT for VPN + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE + + # Allow forwarding through VPN + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -j ACCEPT + ''; + + preDown = '' + # Remove VPN-specific rules (killswitch rules stay!) + ${pkgs.iptables}/bin/iptables -D INPUT -i wg0 -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D OUTPUT -o wg0 -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -j ACCEPT 2>/dev/null || true + ''; }; }; - firewall = { - enable = true; - allowedTCPPorts = [ - 22 - torrentPort - serviceCfg.ports.port0 - ]; - allowedUDPPorts = [ - torrentPort - ]; - }; + dhcpcd.enable = false; useNetworkd = true; }; - # imports = [ - # ./rqbit.nix - # ]; - services = { qbittorrent = { enable = true; webuiPort = serviceCfg.ports.port0; torrentingPort = torrentPort; - openFirewall = true; + openFirewall = false; # We're managing firewall manually + serverConfig = { LegalNotice.Accepted = true; @@ -85,14 +143,13 @@ in Preferences = { WebUI = { Username = "user"; - # generate new passwords with this: - # https://codeberg.org/feathecutie/qbittorrent_password Password_PBKDF2 = "@ByteArray(1bJKXLVSLU6kgCHbCS2lDg==:BmyrMaod6dbJqEe7Ud/JgKAxRMqzsAuEjHcTvLzIBgc5rc5Z7J2X9mbH0cDEAhXqc+O3gQxrckt8S2Gf+zlO9w==)"; }; General = { Locale = "en"; }; + Downloads = { SavePath = "${serviceCfg.varPaths.path0}/downloads"; TempPathEnabled = false; @@ -126,9 +183,20 @@ in ]; services = { + # Ensure qBittorrent ONLY starts after VPN is up qbittorrent = { - after = [ "wg-quick-wg0.service" ]; + after = [ + "wg-quick-wg0.service" + "network-online.target" + ]; requires = [ "wg-quick-wg0.service" ]; + wants = [ "network-online.target" ]; + bindsTo = [ "wg-quick-wg0.service" ]; # Stop if VPN stops + + serviceConfig = { + Restart = "on-failure"; + RestartSec = "10s"; + }; }; natpmp-portforward = { @@ -142,70 +210,56 @@ in "qbittorrent.service" ]; wantedBy = [ "multi-user.target" ]; + serviceConfig = { Type = "simple"; Restart = "always"; RestartSec = "10s"; }; + script = '' PASSWORD=$(cat /run/secrets/qbittorrent-pass) - # Wait for qBittorrent to be ready echo "Waiting for qBittorrent to start..." sleep 10 - # Enable IP forwarding - echo 1 > /proc/sys/net/ipv4/ip_forward - while true; do echo "Requesting port forwarding from VPN..." - # Get the forwarded port for UDP UDP_OUTPUT=$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 udp 60 -g 10.2.0.1 2>&1) UDP_PORT=$(echo "$UDP_OUTPUT" | ${pkgs.gnugrep}/bin/grep "Mapped public port" | ${pkgs.gawk}/bin/awk '{print $4}' | head -1) - # Get the forwarded port for TCP TCP_OUTPUT=$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 tcp 60 -g 10.2.0.1 2>&1) TCP_PORT=$(echo "$TCP_OUTPUT" | ${pkgs.gnugrep}/bin/grep "Mapped public port" | ${pkgs.gawk}/bin/awk '{print $4}' | head -1) if [ -n "$UDP_PORT" ] && [ -n "$TCP_PORT" ]; then echo "Port forwarding successful: UDP=$UDP_PORT, TCP=$TCP_PORT" - # Clear old PREROUTING rules to avoid duplicates - ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -p tcp -j DNAT --to-destination 10.2.0.2 2>/dev/null || true - ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -p udp -j DNAT --to-destination 10.2.0.2 2>/dev/null || true + # Clean up old dynamic rules + ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -s ${localNet} -p tcp -j DNAT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -t nat -D PREROUTING -i enp0s5 -s ${localNet} -p udp -j DNAT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i enp0s5 -o wg0 -p tcp -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i enp0s5 -o wg0 -p udp -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o enp0s5 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true - # Forward traffic from enp0s5 (VM's main interface) to wg0 (WireGuard interface) - ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -p tcp --dport "$TCP_PORT" -j DNAT --to-destination 10.2.0.2:$TCP_PORT - ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -p udp --dport "$UDP_PORT" -j DNAT --to-destination 10.2.0.2:$UDP_PORT + # DNAT: Forward LAN traffic to qBittorrent on WireGuard interface + ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -s ${localNet} -p tcp --dport "$TCP_PORT" -j DNAT --to-destination 10.2.0.2:"$TCP_PORT" + ${pkgs.iptables}/bin/iptables -t nat -A PREROUTING -i enp0s5 -s ${localNet} -p udp --dport "$UDP_PORT" -j DNAT --to-destination 10.2.0.2:"$UDP_PORT" - # Enable masquerading for responses - ${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -o wg0 -j MASQUERADE 2>/dev/null || \ - ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE - - # Allow forwarding between interfaces - ${pkgs.iptables}/bin/iptables -C FORWARD -i enp0s5 -o wg0 -j ACCEPT 2>/dev/null || \ - ${pkgs.iptables}/bin/iptables -A FORWARD -i enp0s5 -o wg0 -j ACCEPT - ${pkgs.iptables}/bin/iptables -C FORWARD -i wg0 -o enp0s5 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o enp0s5 -m state --state RELATED,ESTABLISHED -j ACCEPT + # Allow forwarding for these specific ports + ${pkgs.iptables}/bin/iptables -A FORWARD -i enp0s5 -o wg0 -d 10.2.0.2 -p tcp --dport "$TCP_PORT" -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -i enp0s5 -o wg0 -d 10.2.0.2 -p udp --dport "$UDP_PORT" -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o enp0s5 -s 10.2.0.2 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT echo "Firewall forwarding rules updated for ports: UDP=$UDP_PORT, TCP=$TCP_PORT" - # Open the ports in the firewall dynamically (INPUT chain) - ${pkgs.iptables}/bin/iptables -C INPUT -p udp --dport "$UDP_PORT" -j ACCEPT 2>/dev/null || \ - ${pkgs.iptables}/bin/iptables -A INPUT -p udp --dport "$UDP_PORT" -j ACCEPT - - ${pkgs.iptables}/bin/iptables -C INPUT -p tcp --dport "$TCP_PORT" -j ACCEPT 2>/dev/null || \ - ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport "$TCP_PORT" -j ACCEPT - - echo "Firewall INPUT rules updated for ports: UDP=$UDP_PORT, TCP=$TCP_PORT" - # Update qBittorrent listening port via API echo "Logging into qBittorrent API..." COOKIE=$(${pkgs.curl}/bin/curl -s -i \ --header "Referer: http://localhost:${toString serviceCfg.ports.port0}" \ --data "username=user&password=$PASSWORD" \ "http://localhost:${toString serviceCfg.ports.port0}/api/v2/auth/login" | \ - ${pkgs.gnugrep}/bin/grep -i "set-cookie" | ${pkgs.gawk}/bin/awk -F'SID=|;' '{print $2}') + ${pkgs.gnugrep}/bin/grep -i "set-cookie" | ${pkgs.gawk}/bin/awk -F'SID=|;' '{print $2}') + if [ -n "$COOKIE" ]; then echo "Authentication successful, updating port..." ${pkgs.curl}/bin/curl -s \ @@ -221,7 +275,6 @@ in echo "ERROR: Failed to get forwarded ports" echo "UDP output: $UDP_OUTPUT" echo "TCP output: $TCP_OUTPUT" - exit 1 fi sleep 45 @@ -282,6 +335,7 @@ in environment.systemPackages = builtins.attrValues { inherit (pkgs) bottom + conntrack-tools gawk iptables libnatpmp @@ -292,6 +346,7 @@ in }; }; + # Host configuration remains the same services = { caddy = { virtualHosts = { @@ -325,7 +380,6 @@ in owner = "root"; mode = "0400"; }; - }; security.acme.certs."${host}" = { @@ -342,6 +396,7 @@ in EnvironmentFile = config.sops.secrets."caddy/share-auth".path; }; }; + tmpfiles.rules = [ "d ${serviceCfg.mntPaths.path0} 0755 microvm wheel - -" "d ${serviceCfg.secretPaths.path0}/caddy 755 caddy caddy -" @@ -359,5 +414,4 @@ in torrentPort ]; }; - } From 70f7fefd71ad15dbfaf4fd5a9c219d1d4d998cbf Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 16:04:45 -0600 Subject: [PATCH 3/5] feat: got port forwarding working --- modules/nixos/guests/torrent/default.nix | 27 +++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index 6be19be..b58f8c4 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -46,15 +46,19 @@ in ${pkgs.iptables}/bin/iptables -A INPUT -i lo -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT - # Allow established/related connections - ${pkgs.iptables}/bin/iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + # Allow established/related connections on VPN interface only + ${pkgs.iptables}/bin/iptables -A INPUT -i wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + + # Allow established/related connections on local network (for management) + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Allow SSH on all interfaces (for management) ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport 22 -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - # Allow local network for management (WebUI, etc) + # Allow local network for management (WebUI, etc) - NEW connections ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -j ACCEPT @@ -62,10 +66,17 @@ in ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport ${toString torrentPort} -d ${vpnEndpoint} -j ACCEPT ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -p udp --sport ${toString torrentPort} -s ${vpnEndpoint} -j ACCEPT - # Allow DNS for WireGuard resolution (if needed) - ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport 53 -d ${localNet} -j ACCEPT + # DNS: Only allow through VPN gateway (10.2.0.1) - no local DNS leaks + # This rule will only work once WireGuard is up + # If you need DNS before VPN for WireGuard hostname resolution, uncomment below: + # ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport 53 -d 192.168.50.1 -j ACCEPT - # Log dropped packets (optional, for debugging) + # Block IPv6 completely (defense in depth, even though we disabled it) + ${pkgs.iptables}/bin/ip6tables -P INPUT DROP 2>/dev/null || true + ${pkgs.iptables}/bin/ip6tables -P OUTPUT DROP 2>/dev/null || true + ${pkgs.iptables}/bin/ip6tables -P FORWARD DROP 2>/dev/null || true + + # Log dropped packets (optional, for debugging - comment out in production) ${pkgs.iptables}/bin/iptables -A OUTPUT -j LOG --log-prefix "KILLSWITCH-OUT: " --log-level 4 ${pkgs.iptables}/bin/iptables -A INPUT -j LOG --log-prefix "KILLSWITCH-IN: " --log-level 4 ''; From 909f531fd7b8c975d433beade514438555a992cb Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 17:17:13 -0600 Subject: [PATCH 4/5] feat: got port forwarding working --- modules/nixos/guests/torrent/default.nix | 121 +++++++++++++---------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index b58f8c4..682a133 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -30,56 +30,6 @@ in # Disable default firewall - we're doing it manually firewall.enable = false; - # Configure iptables killswitch at boot, before any services - localCommands = '' - # Default DROP everything - ${pkgs.iptables}/bin/iptables -P INPUT DROP - ${pkgs.iptables}/bin/iptables -P OUTPUT DROP - ${pkgs.iptables}/bin/iptables -P FORWARD DROP - - # Flush existing rules - ${pkgs.iptables}/bin/iptables -F - ${pkgs.iptables}/bin/iptables -t nat -F - ${pkgs.iptables}/bin/iptables -X - - # Allow loopback - ${pkgs.iptables}/bin/iptables -A INPUT -i lo -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT - - # Allow established/related connections on VPN interface only - ${pkgs.iptables}/bin/iptables -A INPUT -i wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - - # Allow established/related connections on local network (for management) - ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - - # Allow SSH on all interfaces (for management) - ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport 22 -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - - # Allow local network for management (WebUI, etc) - NEW connections - ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -j ACCEPT - ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -j ACCEPT - - # CRITICAL: Only allow WireGuard endpoint before VPN is up - ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport ${toString torrentPort} -d ${vpnEndpoint} -j ACCEPT - ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -p udp --sport ${toString torrentPort} -s ${vpnEndpoint} -j ACCEPT - - # DNS: Only allow through VPN gateway (10.2.0.1) - no local DNS leaks - # This rule will only work once WireGuard is up - # If you need DNS before VPN for WireGuard hostname resolution, uncomment below: - # ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport 53 -d 192.168.50.1 -j ACCEPT - - # Block IPv6 completely (defense in depth, even though we disabled it) - ${pkgs.iptables}/bin/ip6tables -P INPUT DROP 2>/dev/null || true - ${pkgs.iptables}/bin/ip6tables -P OUTPUT DROP 2>/dev/null || true - ${pkgs.iptables}/bin/ip6tables -P FORWARD DROP 2>/dev/null || true - - # Log dropped packets (optional, for debugging - comment out in production) - ${pkgs.iptables}/bin/iptables -A OUTPUT -j LOG --log-prefix "KILLSWITCH-OUT: " --log-level 4 - ${pkgs.iptables}/bin/iptables -A INPUT -j LOG --log-prefix "KILLSWITCH-IN: " --log-level 4 - ''; wg-quick.interfaces = { wg0 = { address = [ "10.2.0.2/32" ]; @@ -100,25 +50,44 @@ in # Now we can safely open the VPN tunnel for all traffic postUp = '' - # Allow all traffic through VPN interface + echo "VPN UP: Opening network for VPN and local traffic" + + # Allow ALL traffic through VPN interface ${pkgs.iptables}/bin/iptables -A INPUT -i wg0 -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -o wg0 -j ACCEPT + # Allow local network traffic (WebUI, management) + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -j ACCEPT + # NAT for VPN ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE - # Allow forwarding through VPN + # Allow forwarding through VPN (for port forwarding) ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -j ACCEPT ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -i enp0s5 -o wg0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -o enp0s5 -i wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + + echo "VPN UP: Network opened for VPN and local traffic" ''; preDown = '' - # Remove VPN-specific rules (killswitch rules stay!) + echo "VPN DOWN: Removing VPN rules, killswitch remains active" ${pkgs.iptables}/bin/iptables -D INPUT -i wg0 -j ACCEPT 2>/dev/null || true ${pkgs.iptables}/bin/iptables -D OUTPUT -o wg0 -j ACCEPT 2>/dev/null || true + + ${pkgs.iptables}/bin/iptables -D INPUT -i enp0s5 -s ${localNet} -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D OUTPUT -o enp0s5 -d ${localNet} -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -j ACCEPT 2>/dev/null || true ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -i enp0s5 -o wg0 -j ACCEPT 2>/dev/null || true + ${pkgs.iptables}/bin/iptables -D FORWARD -o enp0s5 -i wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true + + echo "VPN DOWN: Killswitch rules remain - no internet access" ''; }; }; @@ -292,6 +261,52 @@ in done ''; }; + killswitch-init = { + description = "Initialize VPN Killswitch Before Network"; + wantedBy = [ "network-pre.target" ]; + before = [ + "network-pre.target" + "network.target" + ]; + after = [ "systemd-modules-load.service" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + echo "KILLSWITCH: Setting up firewall rules BEFORE network services" + + # Default DROP everything + ${pkgs.iptables}/bin/iptables -P INPUT DROP + ${pkgs.iptables}/bin/iptables -P OUTPUT DROP + ${pkgs.iptables}/bin/iptables -P FORWARD DROP + + ${pkgs.iptables}/bin/iptables -F + ${pkgs.iptables}/bin/iptables -t nat -F + ${pkgs.iptables}/bin/iptables -X + + # Allow loopback + ${pkgs.iptables}/bin/iptables -A INPUT -i lo -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT + + # CRITICAL: Only allow WireGuard endpoint traffic before VPN is up + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -p udp --dport ${toString torrentPort} -d ${vpnEndpoint} -j ACCEPT + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -p udp --sport ${toString torrentPort} -s ${vpnEndpoint} -j ACCEPT + + # Allow SSH from local network (for management) + ${pkgs.iptables}/bin/iptables -A INPUT -i enp0s5 -s ${localNet} -p tcp --dport 22 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A OUTPUT -o enp0s5 -d ${localNet} -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + + # Block IPv6 completely + ${pkgs.iptables}/bin/ip6tables -P INPUT DROP 2>/dev/null || true + ${pkgs.iptables}/bin/ip6tables -P OUTPUT DROP 2>/dev/null || true + ${pkgs.iptables}/bin/ip6tables -P FORWARD DROP 2>/dev/null || true + + echo "KILLSWITCH: Initialized - Network locked down" + ''; + }; }; }; From cc288ad959fddcea86d49c324d48048b4d7ab1db Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 17:41:02 -0600 Subject: [PATCH 5/5] feat: uncucked queuing --- modules/nixos/guests/torrent/default.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index 682a133..3ce323b 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -114,9 +114,7 @@ in Port = torrentPort; MaxConnectionsPerTorrent = -1; MaxUploads = -1; - MaxActiveDownloads = 999; - MaxActiveUploads = 999; - MaxActiveTorrents = 999; + QueueingSystemEnabled = false; }; };