summaryrefslogtreecommitdiffstats
path: root/hosts/poweredge/vpn-router.nix
blob: ac7ce16cc5fb3d47110d1cdabbe71a96f46c2e8b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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";
		};
	};
}