{ lib, config, pkgs, ... }: { # Secrets sops.secrets.vpn-router-ovpn-config = { sopsFile = ./resources/secrets/vpn-router.yaml; key = "ovpn-config"; }; sops.secrets.vpn-router-ovpn-auth = { sopsFile = ./resources/secrets/vpn-router.yaml; key = "ovpn-auth"; }; # Container containers.vpn-router = { autoStart = true; privateNetwork = true; enableTun = true; # OpenVPN requires hostBridge = "br-lan0"; localMacAddress = "02:00:00:00:01:09"; # Bind secrets bindMounts = { "/run/secrets/ovpn-config.ovpn" = { hostPath = config.sops.secrets.vpn-router-ovpn-config.path; isReadOnly = true; }; "/run/secrets/ovpn-auth" = { hostPath = config.sops.secrets.vpn-router-ovpn-auth.path; isReadOnly = true; }; }; config = { lib, pkgs, ... }: { # Networking networking = { enableIPv6 = false; # Prevent ip leaks interfaces.eth0.useDHCP = true; }; # Router config # This container acts as a router that clients can connect to via default # route. All traffic is forwarded over the VPN tunnel instead of WAN. # This allows multiple containers to share the same VPN connection. boot.kernel.sysctl."net.ipv4.ip_forward" = 1; # Enable IPv4 forwarding networking.nat = { enable = true; externalInterface = "tun0"; internalInterfaces = [ "eth0" ]; # Forward ports over VPN forwardPorts = [ { sourcePort = 51413; proto = "tcp"; destination = "192.168.1.47:51413"; } # Transmission TCP { sourcePort = 51413; proto = "udp"; destination = "192.168.1.47:51413"; } # Transmission UDP { sourcePort = 50300; proto = "tcp"; destination = "192.168.1.51:50300"; } # Slskd TCP { sourcePort = 50300; proto = "udp"; destination = "192.168.1.51:50300"; } # Slskd UDP ]; }; # OpenVPN services.openvpn.servers.main = { config = '' config /run/secrets/ovpn-config.ovpn auth-user-pass /run/secrets/ovpn-auth # The server pushes tun-mtu 1500. A full 1500 byte packet plus # AES-256-GCM/UDP/IP encapsulation (~52 bytes) is ~1552 on the wire, # so it has to be IP fragmented to cross a 1500 byte path -- and the # fragments get dropped upstream, blackholing every full size packet # while small ones (dns, tcp handshakes) still pass. Clamp the TCP MSS # so the encapsulated datagram never exceeds this, in both directions. # Must come after the `config` include above so it wins. #mssfix 1400 ''; autoStart = true; updateResolvConf = true; }; # VPN "killswitch", split across two chains: networking.firewall.extraCommands = '' # Get domain name host and port from ovpn config SERVER_HOST=$(${pkgs.gawk}/bin/awk '/^remote /{print $2;exit}' /run/secrets/ovpn-config.ovpn) SERVER_PORT=$(${pkgs.gawk}/bin/awk '/^remote /{print $3;exit}' /run/secrets/ovpn-config.ovpn) [ -n "$SERVER_PORT" ] || SERVER_PORT=1194 # Router's own traffic: only lo, tun0, and established connections ${pkgs.iptables}/bin/iptables -P OUTPUT DROP ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -o tun0 -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Get dns ip from resolv.conf DNS_IP=$(${pkgs.gawk}/bin/awk '/^nameserver /{ if ($2 !~ /:/) { print $2; exit } }' /etc/resolv.conf) # Allow dns ${pkgs.iptables}/bin/iptables -A OUTPUT -o eth0 -p udp -d "$DNS_IP" --dport 53 -j ACCEPT ${pkgs.iptables}/bin/iptables -A OUTPUT -o eth0 -p tcp -d "$DNS_IP" --dport 53 -j ACCEPT # Resolve server ips from host SERVER_IPS="" ATTEMPT=0 while [ -z "$SERVER_IPS" ] && [ "$ATTEMPT" -lt 10 ]; do ATTEMPT=$((ATTEMPT + 1)) SERVER_IPS=$(${pkgs.getent}/bin/getent ahostsv4 "$SERVER_HOST" 2>/dev/null | ${pkgs.gawk}/bin/awk '/STREAM/{print $1}') [ -n "$SERVER_IPS" ] || sleep 3 done # Die if no ips are resolved if [ -z "$SERVER_IPS" ]; then echo "killswitch: could not resolve '$SERVER_HOST' after $ATTEMPT attempts, failing closed with no vpn egress" exit 1 fi # Allow udp traffic to each vpn server ip (for openvpn udp tunnels only, strictly better than tcp) for SERVER_IP in $SERVER_IPS; do echo "killswitch: allowing $SERVER_IP:$SERVER_PORT/udp ($SERVER_HOST)" ${pkgs.iptables}/bin/iptables -A OUTPUT -p udp -d "$SERVER_IP" --dport "$SERVER_PORT" -j ACCEPT done # Anything forwarded through this router: only allowed to reach tun0 ${pkgs.iptables}/bin/iptables -P FORWARD DROP ${pkgs.iptables}/bin/iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT ${pkgs.iptables}/bin/iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT ''; system.stateVersion = "26.05"; }; }; }