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";
};
};
}
|