1{ config, lib, pkgs, utils, ... }:
2
3with utils;
4with lib;
5
6let
7
8 cfg = config.networking;
9 interfaces = attrValues cfg.interfaces;
10 hasVirtuals = any (i: i.virtual) interfaces;
11
12 # We must escape interfaces due to the systemd interpretation
13 subsystemDevice = interface:
14 "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
15
16 interfaceIps = i:
17 i.ip4 ++ optionals cfg.enableIPv6 i.ip6
18 ++ optional (i.ipAddress != null) {
19 address = i.ipAddress;
20 prefixLength = i.prefixLength;
21 } ++ optional (cfg.enableIPv6 && i.ipv6Address != null) {
22 address = i.ipv6Address;
23 prefixLength = i.ipv6PrefixLength;
24 };
25
26 destroyBond = i: ''
27 while true; do
28 UPDATED=1
29 SLAVES=$(ip link | grep 'master ${i}' | awk -F: '{print $2}')
30 for I in $SLAVES; do
31 UPDATED=0
32 ip link set "$I" nomaster
33 done
34 [ "$UPDATED" -eq "1" ] && break
35 done
36 ip link set "${i}" down 2>/dev/null || true
37 ip link del "${i}" 2>/dev/null || true
38 '';
39
40 # warn that these attributes are deprecated (2017-2-2)
41 # Should be removed in the release after next
42 bondDeprecation = rec {
43 deprecated = [ "lacp_rate" "miimon" "mode" "xmit_hash_policy" ];
44 filterDeprecated = bond: (filterAttrs (attrName: attr:
45 elem attrName deprecated && attr != null) bond);
46 };
47
48 bondWarnings =
49 let oneBondWarnings = bondName: bond:
50 mapAttrsToList (bondText bondName) (bondDeprecation.filterDeprecated bond);
51 bondText = bondName: optName: _:
52 "${bondName}.${optName} is deprecated, use ${bondName}.driverOptions";
53 in {
54 warnings = flatten (mapAttrsToList oneBondWarnings cfg.bonds);
55 };
56
57 normalConfig = {
58
59 systemd.services =
60 let
61
62 deviceDependency = dev:
63 # Use systemd service if we manage device creation, else
64 # trust udev when not in a container
65 if (hasAttr dev (filterAttrs (k: v: v.virtual) cfg.interfaces)) ||
66 (hasAttr dev cfg.bridges) ||
67 (hasAttr dev cfg.bonds) ||
68 (hasAttr dev cfg.macvlans) ||
69 (hasAttr dev cfg.sits) ||
70 (hasAttr dev cfg.vlans) ||
71 (hasAttr dev cfg.vswitches) ||
72 (hasAttr dev cfg.wlanInterfaces)
73 then [ "${dev}-netdev.service" ]
74 else optional (dev != null && dev != "lo" && !config.boot.isContainer) (subsystemDevice dev);
75
76 networkLocalCommands = {
77 after = [ "network-setup.service" ];
78 bindsTo = [ "network-setup.service" ];
79 };
80
81 networkSetup =
82 { description = "Networking Setup";
83
84 after = [ "network-pre.target" "systemd-udevd.service" "systemd-sysctl.service" ];
85 before = [ "network.target" "shutdown.target" ];
86 wants = [ "network.target" ];
87 conflicts = [ "shutdown.target" ];
88 wantedBy = [ "multi-user.target" ];
89
90 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
91
92 path = [ pkgs.iproute ];
93
94 serviceConfig = {
95 Type = "oneshot";
96 RemainAfterExit = true;
97 };
98
99 unitConfig.DefaultDependencies = false;
100
101 script =
102 ''
103 # Set the static DNS configuration, if given.
104 ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
105 ${optionalString (cfg.nameservers != [] && cfg.domain != null) ''
106 domain ${cfg.domain}
107 ''}
108 ${optionalString (cfg.search != []) ("search " + concatStringsSep " " cfg.search)}
109 ${flip concatMapStrings cfg.nameservers (ns: ''
110 nameserver ${ns}
111 '')}
112 EOF
113
114 # Set the default gateway.
115 ${optionalString (cfg.defaultGateway != null && cfg.defaultGateway.address != "") ''
116 # FIXME: get rid of "|| true" (necessary to make it idempotent).
117 ip route add default ${optionalString (cfg.defaultGateway.metric != null)
118 "metric ${toString cfg.defaultGateway.metric}"
119 } via "${cfg.defaultGateway.address}" ${
120 optionalString (cfg.defaultGatewayWindowSize != null)
121 "window ${toString cfg.defaultGatewayWindowSize}"} ${
122 optionalString (cfg.defaultGateway.interface != null)
123 "dev ${cfg.defaultGateway.interface}"} proto static || true
124 ''}
125 ${optionalString (cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "") ''
126 # FIXME: get rid of "|| true" (necessary to make it idempotent).
127 ip -6 route add ::/0 ${optionalString (cfg.defaultGateway6.metric != null)
128 "metric ${toString cfg.defaultGateway6.metric}"
129 } via "${cfg.defaultGateway6.address}" ${
130 optionalString (cfg.defaultGatewayWindowSize != null)
131 "window ${toString cfg.defaultGatewayWindowSize}"} ${
132 optionalString (cfg.defaultGateway6.interface != null)
133 "dev ${cfg.defaultGateway6.interface}"} proto static || true
134 ''}
135 '';
136 };
137
138 # For each interface <foo>, create a job ‘network-addresses-<foo>.service"
139 # that performs static address configuration. It has a "wants"
140 # dependency on ‘<foo>.service’, which is supposed to create
141 # the interface and need not exist (i.e. for hardware
142 # interfaces). It has a binds-to dependency on the actual
143 # network device, so it only gets started after the interface
144 # has appeared, and it's stopped when the interface
145 # disappears.
146 configureAddrs = i:
147 let
148 ips = interfaceIps i;
149 in
150 nameValuePair "network-addresses-${i.name}"
151 { description = "Address configuration of ${i.name}";
152 wantedBy = [ "network-setup.service" ];
153 # propagate stop and reload from network-setup
154 partOf = [ "network-setup.service" ];
155 # order before network-setup because the routes that are configured
156 # there may need ip addresses configured
157 before = [ "network-setup.service" ];
158 bindsTo = deviceDependency i.name;
159 after = [ "network-pre.target" ] ++ (deviceDependency i.name);
160 serviceConfig.Type = "oneshot";
161 serviceConfig.RemainAfterExit = true;
162 # Restart rather than stop+start this unit to prevent the
163 # network from dying during switch-to-configuration.
164 stopIfChanged = false;
165 path = [ pkgs.iproute ];
166 script =
167 ''
168 # FIXME: shouldn't this be done in network-link?
169 echo "bringing up interface..."
170 ip link set "${i.name}" up
171
172 state="/run/nixos/network/addresses/${i.name}"
173
174 mkdir -p $(dirname "$state")
175
176 '' + flip concatMapStrings (ips) (ip:
177 let
178 address = "${ip.address}/${toString ip.prefixLength}";
179 in
180 ''
181 echo "${address}" >> $state
182 if out=$(ip addr add "${address}" dev "${i.name}" 2>&1); then
183 echo "added ip ${address}"
184 elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then
185 echo "failed to add ${address}"
186 exit 1
187 fi
188 '');
189 preStop = ''
190 state="/run/nixos/network/addresses/${i.name}"
191 while read address; do
192 echo -n "deleting $address..."
193 ip addr del "$address" dev "${i.name}" >/dev/null 2>&1 || echo -n " Failed"
194 echo ""
195 done < "$state"
196 rm -f "$state"
197 '';
198 };
199
200 createTunDevice = i: nameValuePair "${i.name}-netdev"
201 { description = "Virtual Network Interface ${i.name}";
202 bindsTo = [ "dev-net-tun.device" ];
203 after = [ "dev-net-tun.device" "network-pre.target" ];
204 wantedBy = [ "network-setup.service" (subsystemDevice i.name) ];
205 partOf = [ "network-setup.service" ];
206 before = [ "network-setup.service" (subsystemDevice i.name) ];
207 path = [ pkgs.iproute ];
208 serviceConfig = {
209 Type = "oneshot";
210 RemainAfterExit = true;
211 };
212 script = ''
213 ip tuntap add dev "${i.name}" \
214 ${optionalString (i.virtualType != null) "mode ${i.virtualType}"} \
215 user "${i.virtualOwner}"
216 '';
217 postStop = ''
218 ip link del ${i.name} || true
219 '';
220 };
221
222 createBridgeDevice = n: v: nameValuePair "${n}-netdev"
223 (let
224 deps = concatLists (map deviceDependency v.interfaces);
225 in
226 { description = "Bridge Interface ${n}";
227 wantedBy = [ "network-setup.service" (subsystemDevice n) ];
228 bindsTo = deps ++ optional v.rstp "mstpd.service";
229 partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service";
230 after = [ "network-pre.target" ] ++ deps ++ optional v.rstp "mstpd.service"
231 ++ concatMap (i: [ "network-addresses-${i}.service" "network-link-${i}.service" ]) v.interfaces;
232 before = [ "network-setup.service" (subsystemDevice n) ];
233 serviceConfig.Type = "oneshot";
234 serviceConfig.RemainAfterExit = true;
235 path = [ pkgs.iproute ];
236 script = ''
237 # Remove Dead Interfaces
238 echo "Removing old bridge ${n}..."
239 ip link show "${n}" >/dev/null 2>&1 && ip link del "${n}"
240
241 echo "Adding bridge ${n}..."
242 ip link add name "${n}" type bridge
243
244 # Enslave child interfaces
245 ${flip concatMapStrings v.interfaces (i: ''
246 ip link set "${i}" master "${n}"
247 ip link set "${i}" up
248 '')}
249 # Save list of enslaved interfaces
250 echo "${flip concatMapStrings v.interfaces (i: ''
251 ${i}
252 '')}" > /run/${n}.interfaces
253
254 # Enable stp on the interface
255 ${optionalString v.rstp ''
256 echo 2 >/sys/class/net/${n}/bridge/stp_state
257 ''}
258
259 ip link set "${n}" up
260 '';
261 postStop = ''
262 ip link set "${n}" down || true
263 ip link del "${n}" || true
264 rm -f /run/${n}.interfaces
265 '';
266 reload = ''
267 # Un-enslave child interfaces (old list of interfaces)
268 for interface in `cat /run/${n}.interfaces`; do
269 ip link set "$interface" nomaster up
270 done
271
272 # Enslave child interfaces (new list of interfaces)
273 ${flip concatMapStrings v.interfaces (i: ''
274 ip link set "${i}" master "${n}"
275 ip link set "${i}" up
276 '')}
277 # Save list of enslaved interfaces
278 echo "${flip concatMapStrings v.interfaces (i: ''
279 ${i}
280 '')}" > /run/${n}.interfaces
281
282 # (Un-)set stp on the bridge
283 echo ${if v.rstp then "2" else "0"} > /sys/class/net/${n}/bridge/stp_state
284 '';
285 reloadIfChanged = true;
286 });
287
288 createVswitchDevice = n: v: nameValuePair "${n}-netdev"
289 (let
290 deps = concatLists (map deviceDependency v.interfaces);
291 ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
292 in
293 { description = "Open vSwitch Interface ${n}";
294 wantedBy = [ "network-setup.service" "vswitchd.service" ] ++ deps;
295 bindsTo = [ "vswitchd.service" (subsystemDevice n) ] ++ deps;
296 partOf = [ "network-setup.service" "vswitchd.service" ];
297 after = [ "network-pre.target" "vswitchd.service" ] ++ deps;
298 before = [ "network-setup.service" ];
299 serviceConfig.Type = "oneshot";
300 serviceConfig.RemainAfterExit = true;
301 path = [ pkgs.iproute config.virtualisation.vswitch.package ];
302 script = ''
303 echo "Removing old Open vSwitch ${n}..."
304 ovs-vsctl --if-exists del-br ${n}
305
306 echo "Adding Open vSwitch ${n}..."
307 ovs-vsctl -- add-br ${n} ${concatMapStrings (i: " -- add-port ${n} ${i}") v.interfaces} \
308 ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \
309 ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
310
311 echo "Adding OpenFlow rules for Open vSwitch ${n}..."
312 ovs-ofctl add-flows ${n} ${ofRules}
313 '';
314 postStop = ''
315 ip link set ${n} down || true
316 ovs-ofctl del-flows ${n} || true
317 ovs-vsctl --if-exists del-br ${n}
318 '';
319 });
320
321 createBondDevice = n: v: nameValuePair "${n}-netdev"
322 (let
323 deps = concatLists (map deviceDependency v.interfaces);
324 in
325 { description = "Bond Interface ${n}";
326 wantedBy = [ "network-setup.service" (subsystemDevice n) ];
327 bindsTo = deps;
328 partOf = [ "network-setup.service" ];
329 after = [ "network-pre.target" ] ++ deps
330 ++ concatMap (i: [ "network-addresses-${i}.service" "network-link-${i}.service" ]) v.interfaces;
331 before = [ "network-setup.service" (subsystemDevice n) ];
332 serviceConfig.Type = "oneshot";
333 serviceConfig.RemainAfterExit = true;
334 path = [ pkgs.iproute pkgs.gawk ];
335 script = ''
336 echo "Destroying old bond ${n}..."
337 ${destroyBond n}
338
339 echo "Creating new bond ${n}..."
340 ip link add name "${n}" type bond \
341 ${let opts = (mapAttrs (const toString)
342 (bondDeprecation.filterDeprecated v))
343 // v.driverOptions;
344 in concatStringsSep "\n"
345 (mapAttrsToList (set: val: " ${set} ${val} \\") opts)}
346
347 # !!! There must be a better way to wait for the interface
348 while [ ! -d "/sys/class/net/${n}" ]; do sleep 0.1; done;
349
350 # Bring up the bond and enslave the specified interfaces
351 ip link set "${n}" up
352 ${flip concatMapStrings v.interfaces (i: ''
353 ip link set "${i}" down
354 ip link set "${i}" master "${n}"
355 '')}
356 '';
357 postStop = destroyBond n;
358 });
359
360 createMacvlanDevice = n: v: nameValuePair "${n}-netdev"
361 (let
362 deps = deviceDependency v.interface;
363 in
364 { description = "Vlan Interface ${n}";
365 wantedBy = [ "network-setup.service" (subsystemDevice n) ];
366 bindsTo = deps;
367 partOf = [ "network-setup.service" ];
368 after = [ "network-pre.target" ] ++ deps;
369 before = [ "network-setup.service" (subsystemDevice n) ];
370 serviceConfig.Type = "oneshot";
371 serviceConfig.RemainAfterExit = true;
372 path = [ pkgs.iproute ];
373 script = ''
374 # Remove Dead Interfaces
375 ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
376 ip link add link "${v.interface}" name "${n}" type macvlan \
377 ${optionalString (v.mode != null) "mode ${v.mode}"}
378 ip link set "${n}" up
379 '';
380 postStop = ''
381 ip link delete "${n}" || true
382 '';
383 });
384
385 createSitDevice = n: v: nameValuePair "${n}-netdev"
386 (let
387 deps = deviceDependency v.dev;
388 in
389 { description = "6-to-4 Tunnel Interface ${n}";
390 wantedBy = [ "network-setup.service" (subsystemDevice n) ];
391 bindsTo = deps;
392 partOf = [ "network-setup.service" ];
393 after = [ "network-pre.target" ] ++ deps;
394 before = [ "network-setup.service" (subsystemDevice n) ];
395 serviceConfig.Type = "oneshot";
396 serviceConfig.RemainAfterExit = true;
397 path = [ pkgs.iproute ];
398 script = ''
399 # Remove Dead Interfaces
400 ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
401 ip link add name "${n}" type sit \
402 ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
403 ${optionalString (v.local != null) "local \"${v.local}\""} \
404 ${optionalString (v.ttl != null) "ttl ${toString v.ttl}"} \
405 ${optionalString (v.dev != null) "dev \"${v.dev}\""}
406 ip link set "${n}" up
407 '';
408 postStop = ''
409 ip link delete "${n}" || true
410 '';
411 });
412
413 createVlanDevice = n: v: nameValuePair "${n}-netdev"
414 (let
415 deps = deviceDependency v.interface;
416 in
417 { description = "Vlan Interface ${n}";
418 wantedBy = [ "network-setup.service" (subsystemDevice n) ];
419 bindsTo = deps;
420 partOf = [ "network-setup.service" ];
421 after = [ "network-pre.target" ] ++ deps;
422 before = [ "network-setup.service" (subsystemDevice n) ];
423 serviceConfig.Type = "oneshot";
424 serviceConfig.RemainAfterExit = true;
425 path = [ pkgs.iproute ];
426 script = ''
427 # Remove Dead Interfaces
428 ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
429 ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}"
430 ip link set "${n}" up
431 '';
432 postStop = ''
433 ip link delete "${n}" || true
434 '';
435 });
436
437 in listToAttrs (
438 map configureAddrs interfaces ++
439 map createTunDevice (filter (i: i.virtual) interfaces))
440 // mapAttrs' createBridgeDevice cfg.bridges
441 // mapAttrs' createVswitchDevice cfg.vswitches
442 // mapAttrs' createBondDevice cfg.bonds
443 // mapAttrs' createMacvlanDevice cfg.macvlans
444 // mapAttrs' createSitDevice cfg.sits
445 // mapAttrs' createVlanDevice cfg.vlans
446 // {
447 "network-setup" = networkSetup;
448 "network-local-commands" = networkLocalCommands;
449 };
450
451 services.udev.extraRules =
452 ''
453 KERNEL=="tun", TAG+="systemd"
454 '';
455
456
457 };
458
459in
460
461{
462 config = mkMerge [
463 bondWarnings
464 (mkIf (!cfg.useNetworkd) normalConfig)
465 ];
466}