{ config, flake, pkgs, ... }: let inherit (flake.config.people) user0; inherit (flake.config.services) instances; serviceCfg = instances.torrent; host = instances.torrent.domains.url0; 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} = { autostart = true; 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" ]; dns = [ "10.2.0.1" ]; privateKeyFile = "/run/secrets/wireguard-pass"; peers = [ { publicKey = "QPfiwJQmt5VLEOh1ufLbi1lj6LUnwQY0tgDSh3pWx1k="; endpoint = "${vpnEndpoint}:${toString torrentPort}"; allowedIPs = [ "0.0.0.0/0" "::/0" ]; 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" ''; }; }; dhcpcd.enable = false; useNetworkd = true; }; services = { qbittorrent = { enable = true; webuiPort = serviceCfg.ports.port0; torrentingPort = torrentPort; openFirewall = false; # We're managing firewall manually serverConfig = { LegalNotice.Accepted = true; BitTorrent = { Session = { Interface = "wg0"; InterfaceName = "wg0"; MaxConnections = -1; Port = torrentPort; MaxConnectionsPerTorrent = -1; MaxUploads = -1; QueueingSystemEnabled = false; }; }; Preferences = { WebUI = { Username = "user"; Password_PBKDF2 = "@ByteArray(1bJKXLVSLU6kgCHbCS2lDg==:BmyrMaod6dbJqEe7Ud/JgKAxRMqzsAuEjHcTvLzIBgc5rc5Z7J2X9mbH0cDEAhXqc+O3gQxrckt8S2Gf+zlO9w==)"; }; General = { Locale = "en"; }; Downloads = { SavePath = "${serviceCfg.varPaths.path0}/downloads"; TempPathEnabled = false; PreAllocation = false; }; }; }; }; openssh = { enable = true; settings.PasswordAuthentication = false; }; }; users.users.root.openssh.authorizedKeys.keys = flake.config.people.users.${user0}.sshKeys; systemd = { network = { enable = true; networks."10-enp" = { matchConfig.Name = "enp0s5"; addresses = [ { Address = "${serviceCfg.interface.ip}/24"; } ]; gateway = [ serviceCfg.interface.gate ]; }; }; tmpfiles.rules = [ "d ${serviceCfg.varPaths.path0} 755 ${serviceCfg.name} ${serviceCfg.name} -" "d ${serviceCfg.varPaths.path0}/downloads 755 ${serviceCfg.name} ${serviceCfg.name} -" ]; 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" ''; }; }; }; microvm = { vcpu = 4; mem = 1024 * 1; hypervisor = "qemu"; interfaces = [ { type = "tap"; id = serviceCfg.interface.id; mac = serviceCfg.interface.mac; } { type = "user"; id = serviceCfg.interface.idUser; mac = serviceCfg.interface.macUser; } ]; forwardPorts = [ { from = "host"; host.port = serviceCfg.interface.ssh; guest.port = 22; } ]; shares = [ { source = "/nix/store"; mountPoint = "/nix/.ro-store"; tag = "ro-store"; proto = "virtiofs"; } { mountPoint = serviceCfg.varPaths.path0; proto = "virtiofs"; source = serviceCfg.mntPaths.path0; tag = "${serviceCfg.name}_data"; } { mountPoint = "/run/secrets"; proto = "virtiofs"; source = "/run/secrets/torrent"; tag = "host_secrets"; } ]; }; environment.systemPackages = builtins.attrValues { inherit (pkgs) bottom conntrack-tools gawk iptables libnatpmp speedtest-go wireguard-tools ; }; }; }; # Host configuration remains the same services = { caddy = { virtualHosts = { "${host}" = { extraConfig = '' basic_auth { {$CADDY_AUTH_USER} {$CADDY_AUTH_PASSWORD_HASH} } reverse_proxy ${serviceCfg.interface.ip}:${toString serviceCfg.ports.port0} tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key} encode zstd gzip ''; }; }; }; }; sops.secrets = { "caddy/share-auth" = { owner = "caddy"; group = "caddy"; mode = "0400"; }; "torrent/wireguard-pass" = { owner = "root"; mode = "0400"; }; "torrent/qbittorrent-pass" = { owner = "root"; mode = "0400"; }; }; security.acme.certs."${host}" = { dnsProvider = dns0; environmentFile = config.sops.secrets.${dns0Path}.path; group = "caddy"; }; users.users.caddy.extraGroups = [ "acme" ]; systemd = { services.caddy = { serviceConfig = { 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 ]; }; }