1import ./make-test-python.nix ({ pkgs, lib, ... }: let
2 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
3in {
4 name = "systemd-networkd-vrf";
5 meta.maintainers = with lib.maintainers; [ ma27 ];
6
7 nodes = {
8 client = { pkgs, ... }: {
9 virtualisation.vlans = [ 1 2 ];
10
11 networking = {
12 useDHCP = false;
13 useNetworkd = true;
14 firewall.checkReversePath = "loose";
15 };
16
17 systemd.network = {
18 enable = true;
19
20 netdevs."10-vrf1" = {
21 netdevConfig = {
22 Kind = "vrf";
23 Name = "vrf1";
24 MTUBytes = "1300";
25 };
26 vrfConfig.Table = 23;
27 };
28 netdevs."10-vrf2" = {
29 netdevConfig = {
30 Kind = "vrf";
31 Name = "vrf2";
32 MTUBytes = "1300";
33 };
34 vrfConfig.Table = 42;
35 };
36
37 networks."10-vrf1" = {
38 matchConfig.Name = "vrf1";
39 networkConfig.IPForward = "yes";
40 routes = [
41 { routeConfig = { Destination = "192.168.1.2"; Metric = 100; }; }
42 ];
43 };
44 networks."10-vrf2" = {
45 matchConfig.Name = "vrf2";
46 networkConfig.IPForward = "yes";
47 routes = [
48 { routeConfig = { Destination = "192.168.2.3"; Metric = 100; }; }
49 ];
50 };
51
52 networks."10-eth1" = {
53 matchConfig.Name = "eth1";
54 linkConfig.RequiredForOnline = "no";
55 networkConfig = {
56 VRF = "vrf1";
57 Address = "192.168.1.1";
58 IPForward = "yes";
59 };
60 };
61 networks."10-eth2" = {
62 matchConfig.Name = "eth2";
63 linkConfig.RequiredForOnline = "no";
64 networkConfig = {
65 VRF = "vrf2";
66 Address = "192.168.2.1";
67 IPForward = "yes";
68 };
69 };
70 };
71 };
72
73 node1 = { pkgs, ... }: {
74 virtualisation.vlans = [ 1 ];
75 networking = {
76 useDHCP = false;
77 useNetworkd = true;
78 };
79
80 services.openssh.enable = true;
81 users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
82
83 systemd.network = {
84 enable = true;
85
86 networks."10-eth1" = {
87 matchConfig.Name = "eth1";
88 linkConfig.RequiredForOnline = "no";
89 networkConfig = {
90 Address = "192.168.1.2";
91 IPForward = "yes";
92 };
93 };
94 };
95 };
96
97 node2 = { pkgs, ... }: {
98 virtualisation.vlans = [ 2 ];
99 networking = {
100 useDHCP = false;
101 useNetworkd = true;
102 };
103
104 systemd.network = {
105 enable = true;
106
107 networks."10-eth2" = {
108 matchConfig.Name = "eth2";
109 linkConfig.RequiredForOnline = "no";
110 networkConfig = {
111 Address = "192.168.2.3";
112 IPForward = "yes";
113 };
114 };
115 };
116 };
117
118 node3 = { pkgs, ... }: {
119 virtualisation.vlans = [ 2 ];
120 networking = {
121 useDHCP = false;
122 useNetworkd = true;
123 };
124
125 systemd.network = {
126 enable = true;
127
128 networks."10-eth2" = {
129 matchConfig.Name = "eth2";
130 linkConfig.RequiredForOnline = "no";
131 networkConfig = {
132 Address = "192.168.2.4";
133 IPForward = "yes";
134 };
135 };
136 };
137 };
138 };
139
140 testScript = ''
141 def compare_tables(expected, actual):
142 assert (
143 expected == actual
144 ), """
145 Routing tables don't match!
146 Expected:
147 {}
148 Actual:
149 {}
150 """.format(
151 expected, actual
152 )
153
154
155 start_all()
156
157 client.wait_for_unit("network.target")
158 node1.wait_for_unit("network.target")
159 node2.wait_for_unit("network.target")
160 node3.wait_for_unit("network.target")
161
162 # NOTE: please keep in mind that the trailing whitespaces in the following strings
163 # are intentional as the output is compared against the raw `iproute2`-output.
164 client_ipv4_table = """
165 192.168.1.2 dev vrf1 proto static metric 100
166 192.168.2.3 dev vrf2 proto static metric 100
167 """.strip()
168 vrf1_table = """
169 broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.1
170 192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1
171 local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1
172 broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.1
173 """.strip()
174 vrf2_table = """
175 broadcast 192.168.2.0 dev eth2 proto kernel scope link src 192.168.2.1
176 192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1
177 local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1
178 broadcast 192.168.2.255 dev eth2 proto kernel scope link src 192.168.2.1
179 """.strip()
180
181 # Check that networkd properly configures the main routing table
182 # and the routing tables for the VRF.
183 with subtest("check vrf routing tables"):
184 compare_tables(
185 client_ipv4_table, client.succeed("ip -4 route list | head -n2").strip()
186 )
187 compare_tables(
188 vrf1_table, client.succeed("ip -4 route list table 23 | head -n4").strip()
189 )
190 compare_tables(
191 vrf2_table, client.succeed("ip -4 route list table 42 | head -n4").strip()
192 )
193
194 # Ensure that other nodes are reachable via ICMP through the VRF.
195 with subtest("icmp through vrf works"):
196 client.succeed("ping -c5 192.168.1.2")
197 client.succeed("ping -c5 192.168.2.3")
198
199 # Test whether TCP through a VRF IP is possible.
200 with subtest("tcp traffic through vrf works"):
201 node1.wait_for_open_port(22)
202 client.succeed(
203 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
204 )
205 client.succeed("chmod 600 privkey.snakeoil")
206 client.succeed(
207 "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
208 )
209
210 # Only configured routes through the VRF from the main routing table should
211 # work. Additional IPs are only reachable when binding to the vrf interface.
212 with subtest("only routes from main routing table work by default"):
213 client.fail("ping -c5 192.168.2.4")
214 client.succeed("ping -I vrf2 -c5 192.168.2.4")
215
216 client.shutdown()
217 node1.shutdown()
218 node2.shutdown()
219 node3.shutdown()
220 '';
221})