Add WireGuard tunnels as firewall zone members
router.wireguard.<name> configures tunnels through networkd: road warrior (listening, dual-stack), site-to-site (no transfer network) and provider dial-out with masquerade. A tunnel has a zone like a LAN, so inter-zone rules apply; management, DNS and internet are opt-in. Peers' allowedIPs become routes (destination-based routing; default-route tunnels are rejected). Tunnels have a kill switch (default on): forwarded packets for their allowedIPs are dropped if they would leave via an uplink, before the established-accept so flows that used the tunnel are cut too. Private keys stay outside the Nix store and are passed to the unprivileged systemd-networkd with LoadCredential, so key files can be root-only. Assertions cover CIDR/key/endpoint formats, duplicate allowedIPs, overlaps with LAN subnets, port and device clashes, and rule destinations inside tunnel networks. A five-VM `wireguard` check covers road warrior, site-to-site, the provider tunnel with an IPv6 transfer net, zone rules and the kill switch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+162
-1
@@ -49,6 +49,18 @@ let
|
||||
} overrides
|
||||
);
|
||||
|
||||
# A throwaway public key, valid in format only.
|
||||
pub = "7MkFXOxlCsmXm98uv0txvYzx5Ce80mkg2mDsswY4FyI=";
|
||||
wg =
|
||||
overrides:
|
||||
lib.recursiveUpdate {
|
||||
privateKeyFile = "/var/lib/secrets/wg.key";
|
||||
peers.p1 = {
|
||||
publicKey = pub;
|
||||
allowedIPs = [ "10.100.0.2/32" ];
|
||||
};
|
||||
} overrides;
|
||||
|
||||
failed = router: map (a: a.message) (lib.filter (a: !a.assertion) (evalRouter router).assertions);
|
||||
|
||||
cases = {
|
||||
@@ -328,7 +340,7 @@ let
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "is not inside any LAN of zone `main`" ];
|
||||
expect = [ "is not inside any network of zone `main`" ];
|
||||
};
|
||||
ruleDestinationHostBits = {
|
||||
router = {
|
||||
@@ -346,6 +358,154 @@ let
|
||||
};
|
||||
expect = [ "has host bits set" ];
|
||||
};
|
||||
wgValid = {
|
||||
router = {
|
||||
wan.interface = "eth0";
|
||||
lans.main = lan { };
|
||||
wireguard = {
|
||||
rw = wg {
|
||||
listenPort = 51820;
|
||||
addresses = [
|
||||
"10.100.0.1/24"
|
||||
"fd42:100::1/64"
|
||||
];
|
||||
dns = true;
|
||||
peers.p1.allowedIPs = [
|
||||
"10.100.0.2/32"
|
||||
"fd42:100::2/128"
|
||||
];
|
||||
};
|
||||
s2s = wg {
|
||||
listenPort = 51821;
|
||||
zone = "remote";
|
||||
peers.p1 = {
|
||||
endpoint = "203.0.113.60:51821";
|
||||
allowedIPs = [ "192.168.50.0/24" ];
|
||||
persistentKeepalive = 25;
|
||||
};
|
||||
};
|
||||
ff = wg {
|
||||
zone = "freifunk";
|
||||
addresses = [
|
||||
"10.200.0.2/32"
|
||||
"fd42:200::2/128"
|
||||
];
|
||||
masquerade = true;
|
||||
peers.p1 = {
|
||||
endpoint = "[2001:db8::1]:51822";
|
||||
allowedIPs = [
|
||||
"198.18.0.0/24"
|
||||
"fd00:ff::/32"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "main";
|
||||
to = "remote";
|
||||
destination = "192.168.50.7";
|
||||
}
|
||||
{
|
||||
from = "main";
|
||||
to = "freifunk";
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ ];
|
||||
};
|
||||
wgNoPeers = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers = lib.mkForce { }; };
|
||||
};
|
||||
expect = [ "has no peers" ];
|
||||
};
|
||||
wgBadAddress = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { addresses = [ "10.100.0.1" ]; };
|
||||
};
|
||||
expect = [ "addresses must be IPv4/IPv6 addresses in CIDR notation" ];
|
||||
};
|
||||
wgDefaultRoute = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.allowedIPs = [ "0.0.0.0/0" ]; };
|
||||
};
|
||||
expect = [ "default-route tunnels" ];
|
||||
};
|
||||
wgDuplicateAllowedIps = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg {
|
||||
peers.p2 = {
|
||||
publicKey = pub;
|
||||
allowedIPs = [ "10.100.0.2/32" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
expect = [ "appears more than once" ];
|
||||
};
|
||||
wgOverlapsLan = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.allowedIPs = [ "192.168.10.0/25" ]; };
|
||||
};
|
||||
expect = [ "overlaps one of the LAN subnets" ];
|
||||
};
|
||||
wgBadKey = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.publicKey = "not-a-key"; };
|
||||
};
|
||||
expect = [ "not a valid WireGuard public key" ];
|
||||
};
|
||||
wgBadEndpoint = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.endpoint = "vpn.example.org"; };
|
||||
};
|
||||
expect = [ "endpoint must look like host:port" ];
|
||||
};
|
||||
wgSamePort = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard = {
|
||||
a = wg { listenPort = 51820; };
|
||||
b = wg { listenPort = 51820; };
|
||||
};
|
||||
};
|
||||
expect = [ "listen on the same UDP port" ];
|
||||
};
|
||||
wgDeviceClash = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { interface = "eth1"; };
|
||||
};
|
||||
expect = [ "clashes with a LAN device or the WAN interface" ];
|
||||
};
|
||||
wgNameTooLong = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.a-very-long-tunnel-name = wg { };
|
||||
};
|
||||
expect = [ "longer than 15 characters" ];
|
||||
};
|
||||
wgRuleDestinationOutside = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.s2s = wg { peers.p1.allowedIPs = [ "192.168.50.0/24" ]; };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "main";
|
||||
to = "s2s";
|
||||
destination = "192.168.60.1";
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "is not inside any network of zone `s2s`" ];
|
||||
};
|
||||
nonOverlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
@@ -391,6 +551,7 @@ let
|
||||
"pppoeValid"
|
||||
"vlansOnOneTrunk"
|
||||
"ruleValid"
|
||||
"wgValid"
|
||||
];
|
||||
|
||||
subnet =
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
netcat-openbsd
|
||||
curl
|
||||
tcpdump
|
||||
wireguard-tools
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
wan4 = "pppoe";
|
||||
};
|
||||
vlans = import ./vlans.nix { inherit self pkgs; };
|
||||
wireguard = import ./wireguard.nix { inherit self pkgs; };
|
||||
lib = import ./lib.nix { inherit pkgs; };
|
||||
assertions = import ./assertions.nix { inherit self pkgs; };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
# WireGuard: road warrior, site-to-site and a provider ("Freifunk-style")
|
||||
# tunnel, all on one router.
|
||||
#
|
||||
# laptop -----+ +--- wg-rw (dual-stack, zone vpn)
|
||||
# siteb ------+-- vlan 1 -- router -- vlan 2 -- main LAN (zone trusted)
|
||||
# isp/provider+ +--- wg-s2s (no transfer net, zone remote)
|
||||
# +--- wg-ff (dual-stack, zone freifunk)
|
||||
#
|
||||
# rules: vpn -> trusted, trusted -> remote, vpn -> remote, trusted -> freifunk
|
||||
#
|
||||
# The keys below are throwaway test keys, generated for this test only.
|
||||
{ self, pkgs }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
common = import ./common.nix;
|
||||
|
||||
key = {
|
||||
router-rw = {
|
||||
priv = "wHjLTos8CeZxnCPWPKlZjlzDuer2uyhhNbXR6BYYkXY=";
|
||||
pub = "7BBr4sUxdMI9OlHcEUY+sg5eh8fbeo2nZzSXEI7IJQQ=";
|
||||
};
|
||||
router-s2s = {
|
||||
priv = "CM1gl4vlKo7iULJ07wykWxrHke4YTahZq+LCakFmT0g=";
|
||||
pub = "8/XG5fihWpnHQWQ/cKEjCBZvtk+DmBK/05gR26/Wfzo=";
|
||||
};
|
||||
router-ff = {
|
||||
priv = "2FFVahDzQTlzIpHmX95WeliWpSFKGVlfvJ6iC9iqA1Q=";
|
||||
pub = "lgTbki7gCrRFQa1WmV2IVlocWDMh5Fta0S2+DfVF93k=";
|
||||
};
|
||||
laptop = {
|
||||
priv = "uEWQDRJkpi7JFThKSdLASqd+Z8+2buzzEdcehNXU9Uc=";
|
||||
pub = "ru65s6+UROJ6CDfU15MS83Wlq1jYOJ5Leus1+3FkWmo=";
|
||||
};
|
||||
siteb = {
|
||||
priv = "WPqvH1WTD2nqq/+Ja474J79PAEUNomSVNvUvuQIAw1Q=";
|
||||
pub = "VPHx2uwfYY8BVeZRMUHRBAyFJ+CUnulnlkvzibBGSXo=";
|
||||
};
|
||||
isp = {
|
||||
priv = "sNot2jWhW+MJKBVScvm56FiA0Ekt/DfrFfKYXpK5RUQ=";
|
||||
pub = "7MkFXOxlCsmXm98uv0txvYzx5Ce80mkg2mDsswY4FyI=";
|
||||
};
|
||||
};
|
||||
|
||||
# Plain (non-router) hosts: static address on the shared segment plus one
|
||||
# WireGuard device configured straight through networkd.
|
||||
segmentHost =
|
||||
{
|
||||
address,
|
||||
wgName,
|
||||
wgKey,
|
||||
listenPort ? null,
|
||||
wgAddresses ? [ ],
|
||||
peer,
|
||||
}:
|
||||
{ lib, ... }:
|
||||
{
|
||||
virtualisation.vlans = [ 1 ];
|
||||
networking.useNetworkd = true;
|
||||
networking.useDHCP = false;
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
|
||||
environment.etc."wg/${wgName}.key" = {
|
||||
text = wgKey.priv;
|
||||
mode = "0400";
|
||||
# These hosts use plain networkd, which reads the key itself.
|
||||
user = "systemd-network";
|
||||
group = "systemd-network";
|
||||
};
|
||||
systemd.network.networks."10-seg" = {
|
||||
matchConfig.Name = "eth1";
|
||||
address = [ "${address}/24" ];
|
||||
};
|
||||
systemd.network.netdevs."30-${wgName}" = {
|
||||
netdevConfig = {
|
||||
Kind = "wireguard";
|
||||
Name = wgName;
|
||||
};
|
||||
wireguardConfig = {
|
||||
PrivateKeyFile = "/etc/wg/${wgName}.key";
|
||||
}
|
||||
// lib.optionalAttrs (listenPort != null) { ListenPort = listenPort; };
|
||||
wireguardPeers = [ peer ];
|
||||
};
|
||||
systemd.network.networks."30-${wgName}" = {
|
||||
matchConfig.Name = wgName;
|
||||
address = wgAddresses;
|
||||
routes = map (ip: { Destination = ip; }) peer.AllowedIPs;
|
||||
};
|
||||
};
|
||||
|
||||
dummy = name: addresses: {
|
||||
systemd.network.netdevs."20-${name}".netdevConfig = {
|
||||
Kind = "dummy";
|
||||
Name = name;
|
||||
};
|
||||
systemd.network.networks."20-${name}" = {
|
||||
matchConfig.Name = name;
|
||||
address = addresses;
|
||||
};
|
||||
};
|
||||
|
||||
echo = {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.default = {
|
||||
default = true;
|
||||
locations."/".return = ''200 "$remote_addr"'';
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
pkgs.testers.runNixOSTest {
|
||||
name = "router-wireguard";
|
||||
|
||||
defaults.imports = [ common.tools ];
|
||||
interactive.defaults = common.interactiveDefaults;
|
||||
|
||||
nodes = {
|
||||
# ISP, DHCP server for the router, and the VPN provider: the tunnel leads
|
||||
# to 198.18.0.1 / fd00:ff::1, which are also reachable via the WAN on this
|
||||
# very host (so a leak past the kill switch would succeed).
|
||||
isp = {
|
||||
imports = [
|
||||
(segmentHost {
|
||||
address = "203.0.113.1";
|
||||
wgName = "wg-ff";
|
||||
wgKey = key.isp;
|
||||
listenPort = 51822;
|
||||
wgAddresses = [
|
||||
"10.200.0.1/24"
|
||||
"fd42:200::1/64"
|
||||
];
|
||||
peer = {
|
||||
PublicKey = key.router-ff.pub;
|
||||
AllowedIPs = [
|
||||
"10.200.0.2/32"
|
||||
"fd42:200::2/128"
|
||||
];
|
||||
};
|
||||
})
|
||||
(dummy "ff0" [
|
||||
"198.18.0.1/24"
|
||||
"fd00:ff::1/128"
|
||||
])
|
||||
echo
|
||||
];
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings = {
|
||||
interface = "eth1";
|
||||
bind-interfaces = true;
|
||||
dhcp-range = "203.0.113.100,203.0.113.100,255.255.255.0,1h";
|
||||
dhcp-option = [ "option:router,203.0.113.1" ];
|
||||
address = "/example.test/203.0.113.1";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
router =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ self.nixosModules.router ];
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
networking.interfaces.eth2.ipv4.addresses = lib.mkForce [ ];
|
||||
|
||||
environment.etc = lib.listToAttrs (
|
||||
map
|
||||
(n: {
|
||||
name = "wg/${n}.key";
|
||||
value = {
|
||||
text = key."router-${n}".priv;
|
||||
mode = "0600";
|
||||
};
|
||||
})
|
||||
[
|
||||
"rw"
|
||||
"s2s"
|
||||
"ff"
|
||||
]
|
||||
);
|
||||
|
||||
router = {
|
||||
enable = true;
|
||||
wan.interface = "eth1";
|
||||
lans.main = {
|
||||
interface = "eth2";
|
||||
zone = "trusted";
|
||||
internet = true;
|
||||
ipv4.address = "192.168.10.1";
|
||||
dhcp.pool = {
|
||||
start = "192.168.10.100";
|
||||
end = "192.168.10.199";
|
||||
};
|
||||
};
|
||||
dns.forwarders = [ "203.0.113.1" ];
|
||||
|
||||
wireguard = {
|
||||
rw = {
|
||||
privateKeyFile = "/etc/wg/rw.key";
|
||||
listenPort = 51820;
|
||||
addresses = [
|
||||
"10.100.0.1/24"
|
||||
"fd42:100::1/64"
|
||||
];
|
||||
zone = "vpn";
|
||||
allowManagement = true;
|
||||
dns = true;
|
||||
peers.laptop = {
|
||||
publicKey = key.laptop.pub;
|
||||
allowedIPs = [
|
||||
"10.100.0.2/32"
|
||||
"fd42:100::2/128"
|
||||
];
|
||||
};
|
||||
};
|
||||
# Site-to-site: no transfer network, routing by allowedIPs alone.
|
||||
s2s = {
|
||||
privateKeyFile = "/etc/wg/s2s.key";
|
||||
listenPort = 51821;
|
||||
zone = "remote";
|
||||
peers.siteb = {
|
||||
publicKey = key.siteb.pub;
|
||||
endpoint = "203.0.113.60:51821";
|
||||
allowedIPs = [ "192.168.50.0/24" ];
|
||||
persistentKeepalive = 5;
|
||||
};
|
||||
};
|
||||
# Provider tunnel: only the listed destinations go through it.
|
||||
ff = {
|
||||
privateKeyFile = "/etc/wg/ff.key";
|
||||
zone = "freifunk";
|
||||
masquerade = true;
|
||||
addresses = [
|
||||
"10.200.0.2/32"
|
||||
"fd42:200::2/128"
|
||||
];
|
||||
peers.provider = {
|
||||
publicKey = key.isp.pub;
|
||||
endpoint = "203.0.113.1:51822";
|
||||
allowedIPs = [
|
||||
# The provider's own transfer addresses, plus what it serves.
|
||||
"10.200.0.1/32"
|
||||
"fd42:200::1/128"
|
||||
"198.18.0.0/24"
|
||||
"fd00:ff::/32"
|
||||
];
|
||||
persistentKeepalive = 5;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "vpn";
|
||||
to = "trusted";
|
||||
}
|
||||
{
|
||||
from = "trusted";
|
||||
to = "remote";
|
||||
}
|
||||
{
|
||||
from = "vpn";
|
||||
to = "remote";
|
||||
}
|
||||
{
|
||||
from = "trusted";
|
||||
to = "freifunk";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# See basic.nix: relax Unbound for the made-up test domain only.
|
||||
services.unbound.settings.server = {
|
||||
local-zone = [ "test. transparent" ];
|
||||
domain-insecure = [ "example.test" ];
|
||||
};
|
||||
};
|
||||
|
||||
laptop = segmentHost {
|
||||
address = "203.0.113.50";
|
||||
wgName = "wg-rw";
|
||||
wgKey = key.laptop;
|
||||
wgAddresses = [
|
||||
"10.100.0.2/24"
|
||||
"fd42:100::2/64"
|
||||
];
|
||||
peer = {
|
||||
PublicKey = key.router-rw.pub;
|
||||
Endpoint = "203.0.113.100:51820";
|
||||
PersistentKeepalive = 5;
|
||||
AllowedIPs = [
|
||||
"10.100.0.0/24"
|
||||
"192.168.10.0/24"
|
||||
"192.168.50.0/24"
|
||||
"fd42:100::/64"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
siteb = {
|
||||
imports = [
|
||||
(segmentHost {
|
||||
address = "203.0.113.60";
|
||||
wgName = "wg-s2s";
|
||||
wgKey = key.siteb;
|
||||
listenPort = 51821;
|
||||
peer = {
|
||||
PublicKey = key.router-s2s.pub;
|
||||
Endpoint = "203.0.113.100:51821";
|
||||
PersistentKeepalive = 5;
|
||||
AllowedIPs = [
|
||||
"192.168.10.0/24"
|
||||
"10.100.0.0/24"
|
||||
];
|
||||
};
|
||||
})
|
||||
(dummy "lan0" [ "192.168.50.1/24" ])
|
||||
echo
|
||||
];
|
||||
};
|
||||
|
||||
main =
|
||||
{ lib, ... }:
|
||||
{
|
||||
virtualisation.vlans = [ 2 ];
|
||||
networking.useNetworkd = true;
|
||||
networking.useDHCP = false;
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
systemd.network.networks."20-lan" = {
|
||||
matchConfig.Name = "eth1";
|
||||
networkConfig.DHCP = "ipv4";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
isp.wait_for_unit("dnsmasq.service")
|
||||
isp.wait_for_unit("nginx.service")
|
||||
siteb.wait_for_unit("nginx.service")
|
||||
|
||||
router.wait_for_unit("network-online.target")
|
||||
router.wait_until_succeeds("ip -4 addr show dev eth1 | grep -q 203.0.113.100")
|
||||
router.wait_for_unit("nftables.service")
|
||||
router.wait_for_unit("kea-dhcp4-server.service")
|
||||
router.wait_for_unit("unbound.service")
|
||||
main.wait_until_succeeds("ip -4 addr show dev eth1 | grep -q 192.168.10.100")
|
||||
|
||||
with subtest("all three tunnels complete a handshake"):
|
||||
for dev in ("wg-rw", "wg-s2s", "wg-ff"):
|
||||
router.wait_until_succeeds(f"wg show {dev} latest-handshakes | awk '$2 > 0' | grep -q .")
|
||||
|
||||
with subtest("road warrior: dual-stack tunnel to the router, DNS and SSH over it"):
|
||||
laptop.succeed("ping -c 1 -W 2 10.100.0.1")
|
||||
laptop.succeed("ping -6 -c 1 -W 2 fd42:100::1")
|
||||
laptop.succeed("dig +short +time=2 +tries=1 @10.100.0.1 example.test | grep -qx 203.0.113.1")
|
||||
laptop.succeed("dig +short +time=2 +tries=1 @fd42:100::1 example.test | grep -qx 203.0.113.1")
|
||||
laptop.succeed("nc -z -w 2 10.100.0.1 22")
|
||||
|
||||
with subtest("road warrior reaches the LAN, but not the other way round"):
|
||||
laptop.succeed("ping -c 1 -W 2 192.168.10.100")
|
||||
# Positive control: the LAN host can reach the router's own LAN address.
|
||||
main.succeed("ping -c 1 -W 2 192.168.10.1")
|
||||
main.fail("ping -c 1 -W 2 10.100.0.2")
|
||||
|
||||
with subtest("site-to-site: routed without NAT and without a transfer network"):
|
||||
assert router.succeed("ip -4 addr show dev wg-s2s | grep -c inet || true").strip() == "0"
|
||||
main.succeed("ping -c 1 -W 2 192.168.50.1")
|
||||
out = main.succeed("curl -sf --max-time 5 http://192.168.50.1/")
|
||||
assert out.strip() == "192.168.10.100", f"expected the LAN address to be preserved, got {out!r}"
|
||||
out = laptop.succeed("curl -sf --max-time 5 http://192.168.50.1/")
|
||||
assert out.strip() == "10.100.0.2", f"expected the tunnel address to be preserved, got {out!r}"
|
||||
|
||||
with subtest("site-to-site: the remote site cannot initiate towards the LAN"):
|
||||
# Positive control: it can reach the router itself over the tunnel.
|
||||
siteb.succeed("ping -c 1 -W 2 -I 192.168.50.1 192.168.10.1")
|
||||
siteb.fail("ping -c 1 -W 2 -I 192.168.50.1 192.168.10.100")
|
||||
|
||||
with subtest("provider tunnel: only the listed destinations, masqueraded"):
|
||||
out = main.succeed("curl -sf --max-time 5 http://198.18.0.1/")
|
||||
assert out.strip() == "10.200.0.2", f"expected masquerade to 10.200.0.2, got {out!r}"
|
||||
# Everything else still leaves via the WAN.
|
||||
out = main.succeed("curl -sf --max-time 5 http://203.0.113.1/")
|
||||
assert out.strip() == "203.0.113.100", f"expected the WAN address, got {out!r}"
|
||||
# IPv6 over the dual-stack transfer net (router-originated).
|
||||
router.succeed("ping -6 -c 1 -W 2 fd42:200::1")
|
||||
out = router.succeed("curl -sf -g --max-time 5 'http://[fd00:ff::1]/'")
|
||||
assert out.strip() == "fd42:200::2", f"expected the IPv6 transfer address, got {out!r}"
|
||||
|
||||
with subtest("zones apply to tunnels: the road warrior may not use the provider tunnel"):
|
||||
laptop.fail("curl -sf --max-time 3 http://198.18.0.1/")
|
||||
|
||||
with subtest("kill switch: no leak to the WAN when the provider tunnel is down"):
|
||||
router.succeed("ip link set dev wg-ff down")
|
||||
# The route is gone, so the destination would now follow the default route.
|
||||
router.succeed("ip route get 198.18.0.1 | grep -q 'dev eth1'")
|
||||
main.fail("curl -sf --max-time 3 http://198.18.0.1/")
|
||||
# Ordinary destinations are not affected.
|
||||
main.succeed("curl -sf --max-time 5 http://203.0.113.1/")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user