From 866c07482417a7b59145a9495af0ef0e7881f3ce Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Nov 2025 14:58:37 -0600 Subject: [PATCH] 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 ]; }; - }