diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index fe7f6a5..3ce323b 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,25 @@ 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; + 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 +47,62 @@ in persistentKeepalive = 25; } ]; + + # Now we can safely open the VPN tunnel for all traffic + postUp = '' + 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 (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 = '' + 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" + ''; }; }; - 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; @@ -76,23 +114,20 @@ in Port = torrentPort; MaxConnectionsPerTorrent = -1; MaxUploads = -1; - MaxActiveDownloads = 999; - MaxActiveUploads = 999; - MaxActiveTorrents = 999; + QueueingSystemEnabled = false; }; }; 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; @@ -125,9 +160,151 @@ in "d ${serviceCfg.varPaths.path0}/downloads 755 ${serviceCfg.name} ${serviceCfg.name} -" ]; - services.qbittorrent = { - after = [ "wg-quick-wg0.service" ]; - requires = [ "wg-quick-wg0.service" ]; + services = { + # Ensure qBittorrent ONLY starts after VPN is up + qbittorrent = { + 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 = { + 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) + echo "Waiting for qBittorrent to start..." + sleep 10 + + while true; do + echo "Requesting port forwarding from VPN..." + + 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) + + 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" + + # 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 + + # 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" + + # 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" + + # 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" + fi + + sleep 45 + 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" + ''; + }; }; }; @@ -173,7 +350,7 @@ in { mountPoint = "/run/secrets"; proto = "virtiofs"; - source = "/run/secrets/proton"; + source = "/run/secrets/torrent"; tag = "host_secrets"; } ]; @@ -181,14 +358,19 @@ in environment.systemPackages = builtins.attrValues { inherit (pkgs) - wireguard-tools - speedtest-go bottom + conntrack-tools + gawk + iptables + libnatpmp + speedtest-go + wireguard-tools ; }; }; }; + # Host configuration remains the same services = { caddy = { virtualHosts = { @@ -214,7 +396,11 @@ in group = "caddy"; mode = "0400"; }; - "proton/wireguard-pass" = { + "torrent/wireguard-pass" = { + owner = "root"; + mode = "0400"; + }; + "torrent/qbittorrent-pass" = { owner = "root"; mode = "0400"; }; @@ -234,10 +420,22 @@ 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 -" "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; + } + ]; }; };