import ./make-test-python.nix ({ pkgs, lib, ... }: let inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; in { name = "systemd-networkd-vrf"; meta.maintainers = with lib.maintainers; [ ma27 ]; nodes = { client = { pkgs, ... }: { virtualisation.vlans = [ 1 2 ]; networking = { useDHCP = false; useNetworkd = true; firewall.checkReversePath = "loose"; }; systemd.network = { enable = true; netdevs."10-vrf1" = { netdevConfig = { Kind = "vrf"; Name = "vrf1"; MTUBytes = "1300"; }; vrfConfig.Table = 23; }; netdevs."10-vrf2" = { netdevConfig = { Kind = "vrf"; Name = "vrf2"; MTUBytes = "1300"; }; vrfConfig.Table = 42; }; networks."10-vrf1" = { matchConfig.Name = "vrf1"; networkConfig.IPForward = "yes"; routes = [ { routeConfig = { Destination = "192.168.1.2"; Metric = 100; }; } ]; }; networks."10-vrf2" = { matchConfig.Name = "vrf2"; networkConfig.IPForward = "yes"; routes = [ { routeConfig = { Destination = "192.168.2.3"; Metric = 100; }; } ]; }; networks."10-eth1" = { matchConfig.Name = "eth1"; linkConfig.RequiredForOnline = "no"; networkConfig = { VRF = "vrf1"; Address = "192.168.1.1"; IPForward = "yes"; }; }; networks."10-eth2" = { matchConfig.Name = "eth2"; linkConfig.RequiredForOnline = "no"; networkConfig = { VRF = "vrf2"; Address = "192.168.2.1"; IPForward = "yes"; }; }; }; }; node1 = { pkgs, ... }: { virtualisation.vlans = [ 1 ]; networking = { useDHCP = false; useNetworkd = true; }; services.openssh.enable = true; users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; systemd.network = { enable = true; networks."10-eth1" = { matchConfig.Name = "eth1"; linkConfig.RequiredForOnline = "no"; networkConfig = { Address = "192.168.1.2"; IPForward = "yes"; }; }; }; }; node2 = { pkgs, ... }: { virtualisation.vlans = [ 2 ]; networking = { useDHCP = false; useNetworkd = true; }; systemd.network = { enable = true; networks."10-eth2" = { matchConfig.Name = "eth2"; linkConfig.RequiredForOnline = "no"; networkConfig = { Address = "192.168.2.3"; IPForward = "yes"; }; }; }; }; node3 = { pkgs, ... }: { virtualisation.vlans = [ 2 ]; networking = { useDHCP = false; useNetworkd = true; }; systemd.network = { enable = true; networks."10-eth2" = { matchConfig.Name = "eth2"; linkConfig.RequiredForOnline = "no"; networkConfig = { Address = "192.168.2.4"; IPForward = "yes"; }; }; }; }; }; testScript = '' def compare_tables(expected, actual): assert ( expected == actual ), """ Routing tables don't match! Expected: {} Actual: {} """.format( expected, actual ) start_all() client.wait_for_unit("network.target") node1.wait_for_unit("network.target") node2.wait_for_unit("network.target") node3.wait_for_unit("network.target") # NOTE: please keep in mind that the trailing whitespaces in the following strings # are intentional as the output is compared against the raw `iproute2`-output. # editorconfig-checker-disable client_ipv4_table = """ 192.168.1.2 dev vrf1 proto static metric 100 192.168.2.3 dev vrf2 proto static metric 100 """.strip() vrf1_table = """ broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.1 192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1 local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1 broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.1 """.strip() vrf2_table = """ broadcast 192.168.2.0 dev eth2 proto kernel scope link src 192.168.2.1 192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1 local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1 broadcast 192.168.2.255 dev eth2 proto kernel scope link src 192.168.2.1 """.strip() # editorconfig-checker-enable # Check that networkd properly configures the main routing table # and the routing tables for the VRF. with subtest("check vrf routing tables"): compare_tables( client_ipv4_table, client.succeed("ip -4 route list | head -n2").strip() ) compare_tables( vrf1_table, client.succeed("ip -4 route list table 23 | head -n4").strip() ) compare_tables( vrf2_table, client.succeed("ip -4 route list table 42 | head -n4").strip() ) # Ensure that other nodes are reachable via ICMP through the VRF. with subtest("icmp through vrf works"): client.succeed("ping -c5 192.168.1.2") client.succeed("ping -c5 192.168.2.3") # Test whether TCP through a VRF IP is possible. with subtest("tcp traffic through vrf works"): node1.wait_for_open_port(22) client.succeed( "cat ${snakeOilPrivateKey} > privkey.snakeoil" ) client.succeed("chmod 600 privkey.snakeoil") client.succeed( "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true" ) # Only configured routes through the VRF from the main routing table should # work. Additional IPs are only reachable when binding to the vrf interface. with subtest("only routes from main routing table work by default"): client.fail("ping -c5 192.168.2.4") client.succeed("ping -I vrf2 -c5 192.168.2.4") client.shutdown() node1.shutdown() node2.shutdown() node3.shutdown() ''; })