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