1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7
8 cfg = config.networking;
9 interfaces = attrValues cfg.interfaces;
10
11 interfaceIps = i:
12 i.ipv4.addresses
13 ++ optionals cfg.enableIPv6 i.ipv6.addresses;
14
15 interfaceRoutes = i:
16 i.ipv4.routes
17 ++ optionals cfg.enableIPv6 i.ipv6.routes;
18
19 dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no";
20
21 slaves =
22 concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds))
23 ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges))
24 ++ map (sit: sit.dev) (attrValues cfg.sits)
25 ++ map (gre: gre.dev) (attrValues cfg.greTunnels)
26 ++ map (vlan: vlan.interface) (attrValues cfg.vlans)
27 # add dependency to physical or independently created vswitch member interface
28 # TODO: warn the user that any address configured on those interfaces will be useless
29 ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
30
31 defaultGateways = mkMerge (forEach [ cfg.defaultGateway cfg.defaultGateway6 ] (gateway:
32 optionalAttrs (gateway != null && gateway.interface != null) {
33 networks."40-${gateway.interface}" = {
34 matchConfig.Name = gateway.interface;
35 routes = [{
36 routeConfig = {
37 Gateway = gateway.address;
38 } // optionalAttrs (gateway.metric != null) {
39 Metric = gateway.metric;
40 };
41 }];
42 };
43 }
44 ));
45
46 genericDhcpNetworks = initrd: mkIf cfg.useDHCP {
47 networks."99-ethernet-default-dhcp" = {
48 # We want to match physical ethernet interfaces as commonly
49 # found on laptops, desktops and servers, to provide an
50 # "out-of-the-box" setup that works for common cases. This
51 # heuristic isn't perfect (it could match interfaces with
52 # custom names that _happen_ to start with en or eth), but
53 # should be good enough to make the common case easy and can
54 # be overridden on a case-by-case basis using
55 # higher-priority networks or by disabling useDHCP.
56
57 # Type=ether matches veth interfaces as well, and this is
58 # more likely to result in interfaces being configured to
59 # use DHCP when they shouldn't.
60
61 matchConfig.Name = ["en*" "eth*"];
62 DHCP = "yes";
63 networkConfig.IPv6PrivacyExtensions = "kernel";
64 };
65 networks."99-wireless-client-dhcp" = {
66 # Like above, but this is much more likely to be correct.
67 matchConfig.WLANInterfaceType = "station";
68 DHCP = "yes";
69 networkConfig.IPv6PrivacyExtensions = "kernel";
70 # We also set the route metric to one more than the default
71 # of 1024, so that Ethernet is preferred if both are
72 # available.
73 dhcpV4Config.RouteMetric = 1025;
74 ipv6AcceptRAConfig.RouteMetric = 1025;
75 };
76 };
77
78
79 interfaceNetworks = mkMerge (forEach interfaces (i: {
80 netdevs = mkIf i.virtual ({
81 "40-${i.name}" = {
82 netdevConfig = {
83 Name = i.name;
84 Kind = i.virtualType;
85 };
86 "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
87 User = i.virtualOwner;
88 };
89 };
90 });
91 networks."40-${i.name}" = {
92 name = mkDefault i.name;
93 DHCP = mkForce (dhcpStr
94 (if i.useDHCP != null then i.useDHCP else (config.networking.useDHCP && i.ipv4.addresses == [ ])));
95 address = forEach (interfaceIps i)
96 (ip: "${ip.address}/${toString ip.prefixLength}");
97 routes = forEach (interfaceRoutes i)
98 (route: {
99 # Most of these route options have not been tested.
100 # Please fix or report any mistakes you may find.
101 routeConfig =
102 optionalAttrs (route.address != null && route.prefixLength != null) {
103 Destination = "${route.address}/${toString route.prefixLength}";
104 } //
105 optionalAttrs (route.options ? fastopen_no_cookie) {
106 FastOpenNoCookie = route.options.fastopen_no_cookie;
107 } //
108 optionalAttrs (route.via != null) {
109 Gateway = route.via;
110 } //
111 optionalAttrs (route.type != null) {
112 Type = route.type;
113 } //
114 optionalAttrs (route.options ? onlink) {
115 GatewayOnLink = true;
116 } //
117 optionalAttrs (route.options ? initrwnd) {
118 InitialAdvertisedReceiveWindow = route.options.initrwnd;
119 } //
120 optionalAttrs (route.options ? initcwnd) {
121 InitialCongestionWindow = route.options.initcwnd;
122 } //
123 optionalAttrs (route.options ? pref) {
124 IPv6Preference = route.options.pref;
125 } //
126 optionalAttrs (route.options ? mtu) {
127 MTUBytes = route.options.mtu;
128 } //
129 optionalAttrs (route.options ? metric) {
130 Metric = route.options.metric;
131 } //
132 optionalAttrs (route.options ? src) {
133 PreferredSource = route.options.src;
134 } //
135 optionalAttrs (route.options ? protocol) {
136 Protocol = route.options.protocol;
137 } //
138 optionalAttrs (route.options ? quickack) {
139 QuickAck = route.options.quickack;
140 } //
141 optionalAttrs (route.options ? scope) {
142 Scope = route.options.scope;
143 } //
144 optionalAttrs (route.options ? from) {
145 Source = route.options.from;
146 } //
147 optionalAttrs (route.options ? table) {
148 Table = route.options.table;
149 } //
150 optionalAttrs (route.options ? advmss) {
151 TCPAdvertisedMaximumSegmentSize = route.options.advmss;
152 } //
153 optionalAttrs (route.options ? ttl-propagate) {
154 TTLPropagate = route.options.ttl-propagate == "enabled";
155 };
156 });
157 networkConfig.IPv6PrivacyExtensions = "kernel";
158 linkConfig = optionalAttrs (i.macAddress != null) {
159 MACAddress = i.macAddress;
160 } // optionalAttrs (i.mtu != null) {
161 MTUBytes = toString i.mtu;
162 };
163 };
164 }));
165
166 bridgeNetworks = mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
167 netdevs."40-${name}" = {
168 netdevConfig = {
169 Name = name;
170 Kind = "bridge";
171 };
172 };
173 networks = listToAttrs (forEach bridge.interfaces (bi:
174 nameValuePair "40-${bi}" {
175 DHCP = mkOverride 0 (dhcpStr false);
176 networkConfig.Bridge = name;
177 }));
178 }));
179
180 vlanNetworks = mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: {
181 netdevs."40-${name}" = {
182 netdevConfig = {
183 Name = name;
184 Kind = "vlan";
185 };
186 vlanConfig.Id = vlan.id;
187 };
188 networks."40-${vlan.interface}" = {
189 vlan = [ name ];
190 };
191 }));
192
193in
194
195{
196 config = mkMerge [
197
198 (mkIf config.boot.initrd.network.enable {
199 # Note this is if initrd.network.enable, not if
200 # initrd.systemd.network.enable. By setting the latter and not the
201 # former, the user retains full control over the configuration.
202 boot.initrd.systemd.network = mkMerge [
203 defaultGateways
204 (genericDhcpNetworks true)
205 interfaceNetworks
206 bridgeNetworks
207 vlanNetworks
208 ];
209 boot.initrd.availableKernelModules =
210 optional (cfg.bridges != {}) "bridge" ++
211 optional (cfg.vlans != {}) "8021q";
212 })
213
214 (mkIf cfg.useNetworkd {
215
216 assertions = [ {
217 assertion = cfg.defaultGatewayWindowSize == null;
218 message = "networking.defaultGatewayWindowSize is not supported by networkd.";
219 } {
220 assertion = cfg.defaultGateway != null -> cfg.defaultGateway.interface != null;
221 message = "networking.defaultGateway.interface is not optional when using networkd.";
222 } {
223 assertion = cfg.defaultGateway6 != null -> cfg.defaultGateway6.interface != null;
224 message = "networking.defaultGateway6.interface is not optional when using networkd.";
225 } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: {
226 assertion = !rstp;
227 message = "networking.bridges.${n}.rstp is not supported by networkd.";
228 }) ++ flip mapAttrsToList cfg.fooOverUDP (n: { local, ... }: {
229 assertion = local == null;
230 message = "networking.fooOverUDP.${n}.local is not supported by networkd.";
231 });
232
233 networking.dhcpcd.enable = mkDefault false;
234
235 systemd.network =
236 mkMerge [ {
237 enable = true;
238 }
239 defaultGateways
240 (genericDhcpNetworks false)
241 interfaceNetworks
242 bridgeNetworks
243 (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: {
244 netdevs."40-${name}" = {
245 netdevConfig = {
246 Name = name;
247 Kind = "bond";
248 };
249 bondConfig = let
250 # manual mapping as of 2017-02-03
251 # man 5 systemd.netdev [BOND]
252 # to https://www.kernel.org/doc/Documentation/networking/bonding.txt
253 # driver options.
254 driverOptionMapping = let
255 trans = f: optName: { valTransform = f; optNames = [optName]; };
256 simp = trans id;
257 ms = trans (v: v + "ms");
258 in {
259 Mode = simp "mode";
260 TransmitHashPolicy = simp "xmit_hash_policy";
261 LACPTransmitRate = simp "lacp_rate";
262 MIIMonitorSec = ms "miimon";
263 UpDelaySec = ms "updelay";
264 DownDelaySec = ms "downdelay";
265 LearnPacketIntervalSec = simp "lp_interval";
266 AdSelect = simp "ad_select";
267 FailOverMACPolicy = simp "fail_over_mac";
268 ARPValidate = simp "arp_validate";
269 # apparently in ms for this value?! Upstream bug?
270 ARPIntervalSec = simp "arp_interval";
271 ARPIPTargets = simp "arp_ip_target";
272 ARPAllTargets = simp "arp_all_targets";
273 PrimaryReselectPolicy = simp "primary_reselect";
274 ResendIGMP = simp "resend_igmp";
275 PacketsPerSlave = simp "packets_per_slave";
276 GratuitousARP = { valTransform = id;
277 optNames = [ "num_grat_arp" "num_unsol_na" ]; };
278 AllSlavesActive = simp "all_slaves_active";
279 MinLinks = simp "min_links";
280 };
281
282 do = bond.driverOptions;
283 assertNoUnknownOption = let
284 knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames)
285 driverOptionMapping);
286 # options that apparently don’t exist in the networkd config
287 unknownOptions = [ "primary" ];
288 assertTrace = bool: msg: if bool then true else builtins.trace msg false;
289 in assert all (driverOpt: assertTrace
290 (elem driverOpt (knownOptions ++ unknownOptions))
291 "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.")
292 (mapAttrsToList (k: _: k) do); "";
293 # get those driverOptions that have been set
294 filterSystemdOptions = filterAttrs (sysDOpt: kOpts:
295 any (kOpt: do ? ${kOpt}) kOpts.optNames);
296 # build final set of systemd options to bond values
297 buildOptionSet = mapAttrs (_: kOpts: with kOpts;
298 # we simply take the first set kernel bond option
299 # (one option has multiple names, which is silly)
300 head (map (optN: valTransform (do.${optN}))
301 # only map those that exist
302 (filter (o: do ? ${o}) optNames)));
303 in seq assertNoUnknownOption
304 (buildOptionSet (filterSystemdOptions driverOptionMapping));
305
306 };
307
308 networks = listToAttrs (forEach bond.interfaces (bi:
309 nameValuePair "40-${bi}" {
310 DHCP = mkOverride 0 (dhcpStr false);
311 networkConfig.Bond = name;
312 }));
313 })))
314 (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: {
315 netdevs."40-${name}" = {
316 netdevConfig = {
317 Name = name;
318 Kind = "macvlan";
319 };
320 macvlanConfig = optionalAttrs (macvlan.mode != null) { Mode = macvlan.mode; };
321 };
322 networks."40-${macvlan.interface}" = {
323 macvlan = [ name ];
324 };
325 })))
326 (mkMerge (flip mapAttrsToList cfg.fooOverUDP (name: fou: {
327 netdevs."40-${name}" = {
328 netdevConfig = {
329 Name = name;
330 Kind = "fou";
331 };
332 # unfortunately networkd cannot encode dependencies of netdevs on addresses/routes,
333 # so we cannot specify Local=, Peer=, PeerPort=. this looks like a missing feature
334 # in networkd.
335 fooOverUDPConfig = {
336 Port = fou.port;
337 Encapsulation = if fou.protocol != null then "FooOverUDP" else "GenericUDPEncapsulation";
338 } // (optionalAttrs (fou.protocol != null) {
339 Protocol = fou.protocol;
340 });
341 };
342 })))
343 (mkMerge (flip mapAttrsToList cfg.sits (name: sit: {
344 netdevs."40-${name}" = {
345 netdevConfig = {
346 Name = name;
347 Kind = "sit";
348 };
349 tunnelConfig =
350 (optionalAttrs (sit.remote != null) {
351 Remote = sit.remote;
352 }) // (optionalAttrs (sit.local != null) {
353 Local = sit.local;
354 }) // (optionalAttrs (sit.ttl != null) {
355 TTL = sit.ttl;
356 }) // (optionalAttrs (sit.encapsulation != null) (
357 {
358 FooOverUDP = true;
359 Encapsulation =
360 if sit.encapsulation.type == "fou"
361 then "FooOverUDP"
362 else "GenericUDPEncapsulation";
363 FOUDestinationPort = sit.encapsulation.port;
364 } // (optionalAttrs (sit.encapsulation.sourcePort != null) {
365 FOUSourcePort = sit.encapsulation.sourcePort;
366 })));
367 };
368 networks = mkIf (sit.dev != null) {
369 "40-${sit.dev}" = {
370 tunnel = [ name ];
371 };
372 };
373 })))
374 (mkMerge (flip mapAttrsToList cfg.greTunnels (name: gre: {
375 netdevs."40-${name}" = {
376 netdevConfig = {
377 Name = name;
378 Kind = gre.type;
379 };
380 tunnelConfig =
381 (optionalAttrs (gre.remote != null) {
382 Remote = gre.remote;
383 }) // (optionalAttrs (gre.local != null) {
384 Local = gre.local;
385 }) // (optionalAttrs (gre.ttl != null) {
386 TTL = gre.ttl;
387 });
388 };
389 networks = mkIf (gre.dev != null) {
390 "40-${gre.dev}" = {
391 tunnel = [ name ];
392 };
393 };
394 })))
395 vlanNetworks
396 ];
397
398 # We need to prefill the slaved devices with networking options
399 # This forces the network interface creator to initialize slaves.
400 networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves);
401
402 systemd.services = let
403 # We must escape interfaces due to the systemd interpretation
404 subsystemDevice = interface:
405 "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
406 # support for creating openvswitch switches
407 createVswitchDevice = n: v: nameValuePair "${n}-netdev"
408 (let
409 deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces));
410 ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
411 in
412 { description = "Open vSwitch Interface ${n}";
413 wantedBy = [ "network.target" (subsystemDevice n) ];
414 # and create bridge before systemd-networkd starts because it might create internal interfaces
415 before = [ "systemd-networkd.service" ];
416 # shutdown the bridge when network is shutdown
417 partOf = [ "network.target" ];
418 # requires ovs-vswitchd to be alive at all times
419 bindsTo = [ "ovs-vswitchd.service" ];
420 # start switch after physical interfaces and vswitch daemon
421 after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps;
422 wants = deps; # if one or more interface fails, the switch should continue to run
423 serviceConfig.Type = "oneshot";
424 serviceConfig.RemainAfterExit = true;
425 path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
426 preStart = ''
427 echo "Resetting Open vSwitch ${n}..."
428 ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
429 -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
430 '';
431 script = ''
432 echo "Configuring Open vSwitch ${n}..."
433 ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
434 ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
435 ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \
436 ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
437
438
439 echo "Adding OpenFlow rules for Open vSwitch ${n}..."
440 ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
441 '';
442 postStop = ''
443 echo "Cleaning Open vSwitch ${n}"
444 echo "Shutting down internal ${n} interface"
445 ip link set dev ${n} down || true
446 echo "Deleting flows for ${n}"
447 ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
448 echo "Deleting Open vSwitch ${n}"
449 ovs-vsctl --if-exists del-br ${n} || true
450 '';
451 });
452 in mapAttrs' createVswitchDevice cfg.vswitches
453 // {
454 "network-local-commands" = {
455 after = [ "systemd-networkd.service" ];
456 bindsTo = [ "systemd-networkd.service" ];
457 };
458 };
459 })
460
461 ];
462}