1import ./make-test-python.nix ({ pkgs, lib, ... }: let
2 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
3
4 mkNode = vlan: id: {
5 virtualisation.vlans = [ vlan ];
6 networking = {
7 useDHCP = false;
8 useNetworkd = true;
9 };
10
11 systemd.network = {
12 enable = true;
13
14 networks."10-eth${toString vlan}" = {
15 matchConfig.Name = "eth${toString vlan}";
16 linkConfig.RequiredForOnline = "no";
17 networkConfig = {
18 Address = "192.168.${toString vlan}.${toString id}/24";
19 IPForward = "yes";
20 };
21 };
22 };
23 };
24in {
25 name = "systemd-networkd-vrf";
26 meta.maintainers = with lib.maintainers; [ ma27 ];
27
28 nodes = {
29 client = { pkgs, ... }: {
30 virtualisation.vlans = [ 1 2 ];
31
32 networking = {
33 useDHCP = false;
34 useNetworkd = true;
35 firewall.checkReversePath = "loose";
36 };
37
38 systemd.network = {
39 enable = true;
40
41 netdevs."10-vrf1" = {
42 netdevConfig = {
43 Kind = "vrf";
44 Name = "vrf1";
45 MTUBytes = "1300";
46 };
47 vrfConfig.Table = 23;
48 };
49 netdevs."10-vrf2" = {
50 netdevConfig = {
51 Kind = "vrf";
52 Name = "vrf2";
53 MTUBytes = "1300";
54 };
55 vrfConfig.Table = 42;
56 };
57
58 networks."10-vrf1" = {
59 matchConfig.Name = "vrf1";
60 networkConfig.IPForward = "yes";
61 routes = [
62 { routeConfig = { Destination = "192.168.1.2"; Metric = 100; }; }
63 ];
64 };
65 networks."10-vrf2" = {
66 matchConfig.Name = "vrf2";
67 networkConfig.IPForward = "yes";
68 routes = [
69 { routeConfig = { Destination = "192.168.2.3"; Metric = 100; }; }
70 ];
71 };
72
73 networks."10-eth1" = {
74 matchConfig.Name = "eth1";
75 linkConfig.RequiredForOnline = "no";
76 networkConfig = {
77 VRF = "vrf1";
78 Address = "192.168.1.1/24";
79 IPForward = "yes";
80 };
81 };
82 networks."10-eth2" = {
83 matchConfig.Name = "eth2";
84 linkConfig.RequiredForOnline = "no";
85 networkConfig = {
86 VRF = "vrf2";
87 Address = "192.168.2.1/24";
88 IPForward = "yes";
89 };
90 };
91 };
92 };
93
94 node1 = lib.mkMerge [
95 (mkNode 1 2)
96 {
97 services.openssh.enable = true;
98 users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
99 }
100 ];
101
102 node2 = mkNode 2 3;
103 node3 = mkNode 2 4;
104 };
105
106 testScript = ''
107 import json
108
109 def compare(raw_json, to_compare):
110 data = json.loads(raw_json)
111 assert len(raw_json) >= len(to_compare)
112 for i, row in enumerate(to_compare):
113 actual = data[i]
114 assert len(row.keys()) > 0
115 for key, value in row.items():
116 assert value == actual[key], f"""
117 In entry {i}, value {key}: got: {actual[key]}, expected {value}
118 """
119
120
121 start_all()
122
123 client.wait_for_unit("network.target")
124 node1.wait_for_unit("network.target")
125 node2.wait_for_unit("network.target")
126 node3.wait_for_unit("network.target")
127
128 # Check that networkd properly configures the main routing table
129 # and the routing tables for the VRF.
130 with subtest("check vrf routing tables"):
131 compare(
132 client.succeed("ip --json -4 route list"),
133 [
134 {"dst": "192.168.1.2", "dev": "vrf1", "metric": 100},
135 {"dst": "192.168.2.3", "dev": "vrf2", "metric": 100}
136 ]
137 )
138 compare(
139 client.succeed("ip --json -4 route list table 23"),
140 [
141 {"dst": "192.168.1.0/24", "dev": "eth1", "prefsrc": "192.168.1.1"},
142 {"type": "local", "dst": "192.168.1.1", "dev": "eth1", "prefsrc": "192.168.1.1"},
143 {"type": "broadcast", "dev": "eth1", "prefsrc": "192.168.1.1", "dst": "192.168.1.255"}
144 ]
145 )
146 compare(
147 client.succeed("ip --json -4 route list table 42"),
148 [
149 {"dst": "192.168.2.0/24", "dev": "eth2", "prefsrc": "192.168.2.1"},
150 {"type": "local", "dst": "192.168.2.1", "dev": "eth2", "prefsrc": "192.168.2.1"},
151 {"type": "broadcast", "dev": "eth2", "prefsrc": "192.168.2.1", "dst": "192.168.2.255"}
152 ]
153 )
154
155 # Ensure that other nodes are reachable via ICMP through the VRF.
156 with subtest("icmp through vrf works"):
157 client.succeed("ping -c5 192.168.1.2")
158 client.succeed("ping -c5 192.168.2.3")
159
160 # Test whether TCP through a VRF IP is possible.
161 with subtest("tcp traffic through vrf works"):
162 node1.wait_for_open_port(22)
163 client.succeed(
164 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
165 )
166 client.succeed("chmod 600 privkey.snakeoil")
167 client.succeed(
168 "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
169 )
170
171 # Only configured routes through the VRF from the main routing table should
172 # work. Additional IPs are only reachable when binding to the vrf interface.
173 with subtest("only routes from main routing table work by default"):
174 client.fail("ping -c5 192.168.2.4")
175 client.succeed("ping -I vrf2 -c5 192.168.2.4")
176
177 client.shutdown()
178 node1.shutdown()
179 node2.shutdown()
180 node3.shutdown()
181 '';
182})