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