summaryrefslogtreecommitdiffstats
path: root/hosts/poweredge/vpn-router.nix
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/poweredge/vpn-router.nix')
-rw-r--r--hosts/poweredge/vpn-router.nix121
1 files changed, 121 insertions, 0 deletions
diff --git a/hosts/poweredge/vpn-router.nix b/hosts/poweredge/vpn-router.nix
new file mode 100644
index 0000000..ac7ce16
--- /dev/null
+++ b/hosts/poweredge/vpn-router.nix
@@ -0,0 +1,121 @@
+{ 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";
+ };
+ };
+}