nixpkgs-immich/nixos/tests/libreswan-nat.nix
2024-09-17 10:23:40 -06:00

239 lines
6.7 KiB
Nix

# This test sets up an IPsec VPN server that allows a client behind an IPv4 NAT
# router to access the IPv6 internet. We check that the client initially can't
# ping an IPv6 hosts and its connection to the server can be eavesdropped by
# the router, but once the IPsec tunnel is enstablished it can talk to an
# IPv6-only host and the connection is secure.
#
# Notes:
# - the VPN is implemented using policy-based routing.
# - the client is assigned an IPv6 address from the same /64 subnet
# of the server, without DHCPv6 or SLAAC.
# - the server acts as NDP proxy for the client, so that the latter
# becomes reachable at its assigned IPv6 via the server.
# - the client falls back to TCP if UDP is blocked
{ lib, pkgs, ... }:
let
# Common network setup
baseNetwork = {
# shared hosts file
networking.extraHosts = lib.mkVMOverride ''
203.0.113.1 router
203.0.113.2 server
2001:db8::2 inner
192.168.1.1 client
'';
# open a port for testing
networking.firewall.allowedUDPPorts = [ 1234 ];
};
# Common IPsec configuration
baseTunnel = {
services.libreswan.enable = true;
environment.etc."ipsec.d/tunnel.secrets" =
{ text = ''@server %any : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"'';
mode = "600";
};
};
# Helpers to add a static IP address on an interface
setAddress4 = iface: addr: {
networking.interfaces.${iface}.ipv4.addresses =
lib.mkVMOverride [ { address = addr; prefixLength = 24; } ];
};
setAddress6 = iface: addr: {
networking.interfaces.${iface}.ipv6.addresses =
lib.mkVMOverride [ { address = addr; prefixLength = 64; } ];
};
in
{
name = "libreswan-nat";
meta = with lib.maintainers; {
maintainers = [ rnhmjoj ];
};
nodes.router = { pkgs, ... }: lib.mkMerge [
baseNetwork
(setAddress4 "eth1" "203.0.113.1")
(setAddress4 "eth2" "192.168.1.1")
{
virtualisation.vlans = [ 1 2 ];
environment.systemPackages = [ pkgs.tcpdump ];
networking.nat = {
enable = true;
externalInterface = "eth1";
internalInterfaces = [ "eth2" ];
};
networking.firewall.trustedInterfaces = [ "eth2" ];
}
];
nodes.inner = lib.mkMerge [
baseNetwork
(setAddress6 "eth1" "2001:db8::2")
{ virtualisation.vlans = [ 3 ]; }
];
nodes.server = lib.mkMerge [
baseNetwork
baseTunnel
(setAddress4 "eth1" "203.0.113.2")
(setAddress6 "eth2" "2001:db8::1")
{
virtualisation.vlans = [ 1 3 ];
networking.firewall.allowedUDPPorts = [ 500 4500 ];
networking.firewall.allowedTCPPorts = [ 993 ];
# see https://github.com/NixOS/nixpkgs/pull/310857
networking.firewall.checkReversePath = false;
boot.kernel.sysctl = {
# enable forwarding packets
"net.ipv6.conf.all.forwarding" = 1;
"net.ipv4.conf.all.forwarding" = 1;
# enable NDP proxy for VPN clients
"net.ipv6.conf.all.proxy_ndp" = 1;
};
services.libreswan.configSetup = "listen-tcp=yes";
services.libreswan.connections.tunnel = ''
# server
left=203.0.113.2
leftid=@server
leftsubnet=::/0
leftupdown=${pkgs.writeScript "updown" ''
# act as NDP proxy for VPN clients
if test "$PLUTO_VERB" = up-client-v6; then
ip neigh add proxy "$PLUTO_PEER_CLIENT_NET" dev eth2
fi
if test "$PLUTO_VERB" = down-client-v6; then
ip neigh del proxy "$PLUTO_PEER_CLIENT_NET" dev eth2
fi
''}
# clients
right=%any
rightaddresspool=2001:db8:0:0:c::/97
modecfgdns=2001:db8::1
# clean up vanished clients
dpddelay=30
auto=add
keyexchange=ikev2
rekey=no
narrowing=yes
fragmentation=yes
authby=secret
leftikeport=993
retransmit-timeout=10s
'';
}
];
nodes.client = lib.mkMerge [
baseNetwork
baseTunnel
(setAddress4 "eth1" "192.168.1.2")
{
virtualisation.vlans = [ 2 ];
networking.defaultGateway = {
address = "192.168.1.1";
interface = "eth1";
};
services.libreswan.connections.tunnel = ''
# client
left=%defaultroute
leftid=@client
leftmodecfgclient=yes
leftsubnet=::/0
# server
right=203.0.113.2
rightid=@server
rightsubnet=::/0
auto=add
narrowing=yes
rekey=yes
fragmentation=yes
authby=secret
# fallback when UDP is blocked
enable-tcp=fallback
tcp-remoteport=993
retransmit-timeout=5s
'';
}
];
testScript =
''
def client_to_host(machine, msg: str):
"""
Sends a message from client to server
"""
machine.execute("nc -lu :: 1234 >/tmp/msg &")
client.sleep(1)
client.succeed(f"echo '{msg}' | nc -uw 0 {machine.name} 1234")
client.sleep(1)
machine.succeed(f"grep '{msg}' /tmp/msg")
def eavesdrop():
"""
Starts eavesdropping on the router
"""
match = "udp port 1234"
router.execute(f"tcpdump -i eth1 -c 1 -Avv {match} >/tmp/log &")
start_all()
with subtest("Network is up"):
client.wait_until_succeeds("ping -c1 server")
client.succeed("systemctl restart ipsec")
server.succeed("systemctl restart ipsec")
with subtest("Router can eavesdrop cleartext traffic"):
eavesdrop()
client_to_host(server, "I secretly love turnip")
router.sleep(1)
router.succeed("grep turnip /tmp/log")
with subtest("Libreswan is ready"):
client.wait_for_unit("ipsec")
server.wait_for_unit("ipsec")
client.succeed("ipsec checkconfig")
server.succeed("ipsec checkconfig")
with subtest("Client can't ping VPN host"):
client.fail("ping -c1 inner")
with subtest("Client can start the tunnel"):
client.succeed("ipsec start tunnel")
client.succeed("ip -6 addr show lo | grep -q 2001:db8:0:0:c")
with subtest("Client can ping VPN host"):
client.wait_until_succeeds("ping -c1 2001:db8::1")
client.succeed("ping -c1 inner")
with subtest("Eve no longer can eavesdrop"):
eavesdrop()
client_to_host(inner, "Just kidding, I actually like rhubarb")
router.sleep(1)
router.fail("grep rhubarb /tmp/log")
with subtest("TCP fallback is available"):
server.succeed("iptables -I nixos-fw -p udp -j DROP")
client.succeed("ipsec restart")
client.execute("ipsec start tunnel")
client.wait_until_succeeds("ping -c1 inner")
'';
}