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