1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 # The container's init script, a small wrapper around the regular
8 # NixOS stage-2 init script.
9 containerInit = (cfg:
10 let
11 renderExtraVeth = (name: cfg:
12 ''
13 echo "Bringing ${name} up"
14 ip link set dev ${name} up
15 ${optionalString (cfg.localAddress != null) ''
16 echo "Setting ip for ${name}"
17 ip addr add ${cfg.localAddress} dev ${name}
18 ''}
19 ${optionalString (cfg.localAddress6 != null) ''
20 echo "Setting ip6 for ${name}"
21 ip -6 addr add ${cfg.localAddress6} dev ${name}
22 ''}
23 ${optionalString (cfg.hostAddress != null) ''
24 echo "Setting route to host for ${name}"
25 ip route add ${cfg.hostAddress} dev ${name}
26 ''}
27 ${optionalString (cfg.hostAddress6 != null) ''
28 echo "Setting route6 to host for ${name}"
29 ip -6 route add ${cfg.hostAddress6} dev ${name}
30 ''}
31 ''
32 );
33 in
34 pkgs.writeScript "container-init"
35 ''
36 #! ${pkgs.runtimeShell} -e
37
38 # Initialise the container side of the veth pair.
39 if [ "$PRIVATE_NETWORK" = 1 ]; then
40
41 ip link set host0 name eth0
42 ip link set dev eth0 up
43
44 if [ -n "$LOCAL_ADDRESS" ]; then
45 ip addr add $LOCAL_ADDRESS dev eth0
46 fi
47 if [ -n "$LOCAL_ADDRESS6" ]; then
48 ip -6 addr add $LOCAL_ADDRESS6 dev eth0
49 fi
50 if [ -n "$HOST_ADDRESS" ]; then
51 ip route add $HOST_ADDRESS dev eth0
52 ip route add default via $HOST_ADDRESS
53 fi
54 if [ -n "$HOST_ADDRESS6" ]; then
55 ip -6 route add $HOST_ADDRESS6 dev eth0
56 ip -6 route add default via $HOST_ADDRESS6
57 fi
58
59 ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
60 fi
61
62 # Start the regular stage 1 script.
63 exec "$1"
64 ''
65 );
66
67 nspawnExtraVethArgs = (name: cfg: "--network-veth-extra=${name}");
68
69 startScript = cfg:
70 ''
71 mkdir -p -m 0755 "$root/etc" "$root/var/lib"
72 mkdir -p -m 0700 "$root/var/lib/private" "$root/root" /run/containers
73 if ! [ -e "$root/etc/os-release" ]; then
74 touch "$root/etc/os-release"
75 fi
76
77 if ! [ -e "$root/etc/machine-id" ]; then
78 touch "$root/etc/machine-id"
79 fi
80
81 mkdir -p -m 0755 \
82 "/nix/var/nix/profiles/per-container/$INSTANCE" \
83 "/nix/var/nix/gcroots/per-container/$INSTANCE"
84
85 cp --remove-destination /etc/resolv.conf "$root/etc/resolv.conf"
86
87 if [ "$PRIVATE_NETWORK" = 1 ]; then
88 extraFlags+=" --network-veth"
89 if [ -n "$HOST_BRIDGE" ]; then
90 extraFlags+=" --network-bridge=$HOST_BRIDGE"
91 fi
92 if [ -n "$HOST_PORT" ]; then
93 OIFS=$IFS
94 IFS=","
95 for i in $HOST_PORT
96 do
97 extraFlags+=" --port=$i"
98 done
99 IFS=$OIFS
100 fi
101 fi
102
103 extraFlags+=" ${concatStringsSep " " (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)}"
104
105 for iface in $INTERFACES; do
106 extraFlags+=" --network-interface=$iface"
107 done
108
109 for iface in $MACVLANS; do
110 extraFlags+=" --network-macvlan=$iface"
111 done
112
113 # If the host is 64-bit and the container is 32-bit, add a
114 # --personality flag.
115 ${optionalString (config.nixpkgs.localSystem.system == "x86_64-linux") ''
116 if [ "$(< ''${SYSTEM_PATH:-/nix/var/nix/profiles/per-container/$INSTANCE/system}/system)" = i686-linux ]; then
117 extraFlags+=" --personality=x86"
118 fi
119 ''}
120
121 # Run systemd-nspawn without startup notification (we'll
122 # wait for the container systemd to signal readiness).
123 exec ${config.systemd.package}/bin/systemd-nspawn \
124 --keep-unit \
125 -M "$INSTANCE" -D "$root" $extraFlags \
126 $EXTRA_NSPAWN_FLAGS \
127 --notify-ready=yes \
128 --bind-ro=/nix/store \
129 --bind-ro=/nix/var/nix/db \
130 --bind-ro=/nix/var/nix/daemon-socket \
131 --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \
132 --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \
133 --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \
134 --setenv HOST_BRIDGE="$HOST_BRIDGE" \
135 --setenv HOST_ADDRESS="$HOST_ADDRESS" \
136 --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \
137 --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \
138 --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \
139 --setenv HOST_PORT="$HOST_PORT" \
140 --setenv PATH="$PATH" \
141 ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then
142 ''--capability="${concatStringsSep " " cfg.additionalCapabilities}"'' else ""
143 } \
144 ${if cfg.tmpfs != null && cfg.tmpfs != [] then
145 ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else ""
146 } \
147 ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
148 '';
149
150 preStartScript = cfg:
151 ''
152 # Clean up existing machined registration and interfaces.
153 machinectl terminate "$INSTANCE" 2> /dev/null || true
154
155 if [ "$PRIVATE_NETWORK" = 1 ]; then
156 ip link del dev "ve-$INSTANCE" 2> /dev/null || true
157 ip link del dev "vb-$INSTANCE" 2> /dev/null || true
158 fi
159
160 ${concatStringsSep "\n" (
161 mapAttrsToList (name: cfg:
162 ''ip link del dev ${name} 2> /dev/null || true ''
163 ) cfg.extraVeths
164 )}
165 '';
166
167 postStartScript = (cfg:
168 let
169 ipcall = cfg: ipcmd: variable: attribute:
170 if cfg.${attribute} == null then
171 ''
172 if [ -n "${variable}" ]; then
173 ${ipcmd} add ${variable} dev $ifaceHost
174 fi
175 ''
176 else
177 ''${ipcmd} add ${cfg.${attribute}} dev $ifaceHost'';
178 renderExtraVeth = name: cfg:
179 if cfg.hostBridge != null then
180 ''
181 # Add ${name} to bridge ${cfg.hostBridge}
182 ip link set dev ${name} master ${cfg.hostBridge} up
183 ''
184 else
185 ''
186 # Set IPs and routes for ${name}
187 ${optionalString (cfg.hostAddress != null) ''
188 ip addr add ${cfg.hostAddress} dev ${name}
189 ''}
190 ${optionalString (cfg.hostAddress6 != null) ''
191 ip -6 addr add ${cfg.hostAddress6} dev ${name}
192 ''}
193 ${optionalString (cfg.localAddress != null) ''
194 ip route add ${cfg.localAddress} dev ${name}
195 ''}
196 ${optionalString (cfg.localAddress6 != null) ''
197 ip -6 route add ${cfg.localAddress6} dev ${name}
198 ''}
199 '';
200 in
201 ''
202 if [ "$PRIVATE_NETWORK" = 1 ]; then
203 if [ -z "$HOST_BRIDGE" ]; then
204 ifaceHost=ve-$INSTANCE
205 ip link set dev $ifaceHost up
206
207 ${ipcall cfg "ip addr" "$HOST_ADDRESS" "hostAddress"}
208 ${ipcall cfg "ip -6 addr" "$HOST_ADDRESS6" "hostAddress6"}
209 ${ipcall cfg "ip route" "$LOCAL_ADDRESS" "localAddress"}
210 ${ipcall cfg "ip -6 route" "$LOCAL_ADDRESS6" "localAddress6"}
211 fi
212 ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
213 fi
214
215 # Get the leader PID so that we can signal it in
216 # preStop. We can't use machinectl there because D-Bus
217 # might be shutting down. FIXME: in systemd 219 we can
218 # just signal systemd-nspawn to do a clean shutdown.
219 machinectl show "$INSTANCE" | sed 's/Leader=\(.*\)/\1/;t;d' > "/run/containers/$INSTANCE.pid"
220 ''
221 );
222
223 serviceDirectives = cfg: {
224 ExecReload = pkgs.writeScript "reload-container"
225 ''
226 #! ${pkgs.runtimeShell} -e
227 ${pkgs.nixos-container}/bin/nixos-container run "$INSTANCE" -- \
228 bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test"
229 '';
230
231 SyslogIdentifier = "container %i";
232
233 EnvironmentFile = "-/etc/containers/%i.conf";
234
235 Type = "notify";
236
237 # Note that on reboot, systemd-nspawn returns 133, so this
238 # unit will be restarted. On poweroff, it returns 0, so the
239 # unit won't be restarted.
240 RestartForceExitStatus = "133";
241 SuccessExitStatus = "133";
242
243 Restart = "on-failure";
244
245 # Hack: we don't want to kill systemd-nspawn, since we call
246 # "machinectl poweroff" in preStop to shut down the
247 # container cleanly. But systemd requires sending a signal
248 # (at least if we want remaining processes to be killed
249 # after the timeout). So send an ignored signal.
250 KillMode = "mixed";
251 KillSignal = "WINCH";
252
253 DevicePolicy = "closed";
254 DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
255 };
256
257
258 system = config.nixpkgs.localSystem.system;
259
260 bindMountOpts = { name, ... }: {
261
262 options = {
263 mountPoint = mkOption {
264 example = "/mnt/usb";
265 type = types.str;
266 description = "Mount point on the container file system.";
267 };
268 hostPath = mkOption {
269 default = null;
270 example = "/home/alice";
271 type = types.nullOr types.str;
272 description = "Location of the host path to be mounted.";
273 };
274 isReadOnly = mkOption {
275 default = true;
276 type = types.bool;
277 description = "Determine whether the mounted path will be accessed in read-only mode.";
278 };
279 };
280
281 config = {
282 mountPoint = mkDefault name;
283 };
284
285 };
286
287 allowedDeviceOpts = { ... }: {
288 options = {
289 node = mkOption {
290 example = "/dev/net/tun";
291 type = types.str;
292 description = "Path to device node";
293 };
294 modifier = mkOption {
295 example = "rw";
296 type = types.str;
297 description = ''
298 Device node access modifier. Takes a combination
299 <literal>r</literal> (read), <literal>w</literal> (write), and
300 <literal>m</literal> (mknod). See the
301 <literal>systemd.resource-control(5)</literal> man page for more
302 information.'';
303 };
304 };
305 };
306
307
308 mkBindFlag = d:
309 let flagPrefix = if d.isReadOnly then " --bind-ro=" else " --bind=";
310 mountstr = if d.hostPath != null then "${d.hostPath}:${d.mountPoint}" else "${d.mountPoint}";
311 in flagPrefix + mountstr ;
312
313 mkBindFlags = bs: concatMapStrings mkBindFlag (lib.attrValues bs);
314
315 networkOptions = {
316 hostBridge = mkOption {
317 type = types.nullOr types.string;
318 default = null;
319 example = "br0";
320 description = ''
321 Put the host-side of the veth-pair into the named bridge.
322 Only one of hostAddress* or hostBridge can be given.
323 '';
324 };
325
326 forwardPorts = mkOption {
327 type = types.listOf (types.submodule {
328 options = {
329 protocol = mkOption {
330 type = types.str;
331 default = "tcp";
332 description = "The protocol specifier for port forwarding between host and container";
333 };
334 hostPort = mkOption {
335 type = types.int;
336 description = "Source port of the external interface on host";
337 };
338 containerPort = mkOption {
339 type = types.nullOr types.int;
340 default = null;
341 description = "Target port of container";
342 };
343 };
344 });
345 default = [];
346 example = [ { protocol = "tcp"; hostPort = 8080; containerPort = 80; } ];
347 description = ''
348 List of forwarded ports from host to container. Each forwarded port
349 is specified by protocol, hostPort and containerPort. By default,
350 protocol is tcp and hostPort and containerPort are assumed to be
351 the same if containerPort is not explicitly given.
352 '';
353 };
354
355
356 hostAddress = mkOption {
357 type = types.nullOr types.str;
358 default = null;
359 example = "10.231.136.1";
360 description = ''
361 The IPv4 address assigned to the host interface.
362 (Not used when hostBridge is set.)
363 '';
364 };
365
366 hostAddress6 = mkOption {
367 type = types.nullOr types.string;
368 default = null;
369 example = "fc00::1";
370 description = ''
371 The IPv6 address assigned to the host interface.
372 (Not used when hostBridge is set.)
373 '';
374 };
375
376 localAddress = mkOption {
377 type = types.nullOr types.str;
378 default = null;
379 example = "10.231.136.2";
380 description = ''
381 The IPv4 address assigned to the interface in the container.
382 If a hostBridge is used, this should be given with netmask to access
383 the whole network. Otherwise the default netmask is /32 and routing is
384 set up from localAddress to hostAddress and back.
385 '';
386 };
387
388 localAddress6 = mkOption {
389 type = types.nullOr types.string;
390 default = null;
391 example = "fc00::2";
392 description = ''
393 The IPv6 address assigned to the interface in the container.
394 If a hostBridge is used, this should be given with netmask to access
395 the whole network. Otherwise the default netmask is /128 and routing is
396 set up from localAddress6 to hostAddress6 and back.
397 '';
398 };
399
400 };
401
402 dummyConfig =
403 {
404 extraVeths = {};
405 additionalCapabilities = [];
406 allowedDevices = [];
407 hostAddress = null;
408 hostAddress6 = null;
409 localAddress = null;
410 localAddress6 = null;
411 tmpfs = null;
412 };
413
414in
415
416{
417 options = {
418
419 boot.isContainer = mkOption {
420 type = types.bool;
421 default = false;
422 description = ''
423 Whether this NixOS machine is a lightweight container running
424 in another NixOS system.
425 '';
426 };
427
428 boot.enableContainers = mkOption {
429 type = types.bool;
430 default = !config.boot.isContainer;
431 description = ''
432 Whether to enable support for nixos containers.
433 '';
434 };
435
436 containers = mkOption {
437 type = types.attrsOf (types.submodule (
438 { config, options, name, ... }:
439 {
440 options = {
441
442 config = mkOption {
443 description = ''
444 A specification of the desired configuration of this
445 container, as a NixOS module.
446 '';
447 type = lib.mkOptionType {
448 name = "Toplevel NixOS config";
449 merge = loc: defs: (import ../../lib/eval-config.nix {
450 inherit system;
451 modules =
452 let extraConfig =
453 { boot.isContainer = true;
454 networking.hostName = mkDefault name;
455 networking.useDHCP = false;
456 };
457 in [ extraConfig ] ++ (map (x: x.value) defs);
458 prefix = [ "containers" name ];
459 }).config;
460 };
461 };
462
463 path = mkOption {
464 type = types.path;
465 example = "/nix/var/nix/profiles/containers/webserver";
466 description = ''
467 As an alternative to specifying
468 <option>config</option>, you can specify the path to
469 the evaluated NixOS system configuration, typically a
470 symlink to a system profile.
471 '';
472 };
473
474 additionalCapabilities = mkOption {
475 type = types.listOf types.str;
476 default = [];
477 example = [ "CAP_NET_ADMIN" "CAP_MKNOD" ];
478 description = ''
479 Grant additional capabilities to the container. See the
480 capabilities(7) and systemd-nspawn(1) man pages for more
481 information.
482 '';
483 };
484 enableTun = mkOption {
485 type = types.bool;
486 default = false;
487 description = ''
488 Allows the container to create and setup tunnel interfaces
489 by granting the <literal>NET_ADMIN</literal> capability and
490 enabling access to <literal>/dev/net/tun</literal>.
491 '';
492 };
493
494 privateNetwork = mkOption {
495 type = types.bool;
496 default = false;
497 description = ''
498 Whether to give the container its own private virtual
499 Ethernet interface. The interface is called
500 <literal>eth0</literal>, and is hooked up to the interface
501 <literal>ve-<replaceable>container-name</replaceable></literal>
502 on the host. If this option is not set, then the
503 container shares the network interfaces of the host,
504 and can bind to any port on any interface.
505 '';
506 };
507
508 interfaces = mkOption {
509 type = types.listOf types.string;
510 default = [];
511 example = [ "eth1" "eth2" ];
512 description = ''
513 The list of interfaces to be moved into the container.
514 '';
515 };
516
517 macvlans = mkOption {
518 type = types.listOf types.str;
519 default = [];
520 example = [ "eth1" "eth2" ];
521 description = ''
522 The list of host interfaces from which macvlans will be
523 created. For each interface specified, a macvlan interface
524 will be created and moved to the container.
525 '';
526 };
527
528 extraVeths = mkOption {
529 type = with types; attrsOf (submodule { options = networkOptions; });
530 default = {};
531 description = ''
532 Extra veth-pairs to be created for the container
533 '';
534 };
535
536 autoStart = mkOption {
537 type = types.bool;
538 default = false;
539 description = ''
540 Whether the container is automatically started at boot-time.
541 '';
542 };
543
544 bindMounts = mkOption {
545 type = with types; loaOf (submodule bindMountOpts);
546 default = {};
547 example = { "/home" = { hostPath = "/home/alice";
548 isReadOnly = false; };
549 };
550
551 description =
552 ''
553 An extra list of directories that is bound to the container.
554 '';
555 };
556
557 allowedDevices = mkOption {
558 type = with types; listOf (submodule allowedDeviceOpts);
559 default = [];
560 example = [ { node = "/dev/net/tun"; modifier = "rw"; } ];
561 description = ''
562 A list of device nodes to which the containers has access to.
563 '';
564 };
565
566 tmpfs = mkOption {
567 type = types.listOf types.str;
568 default = [];
569 example = [ "/var" ];
570 description = ''
571 Mounts a set of tmpfs file systems into the container.
572 Multiple paths can be specified.
573 Valid items must conform to the --tmpfs argument
574 of systemd-nspawn. See systemd-nspawn(1) for details.
575 '';
576 };
577
578 extraFlags = mkOption {
579 type = types.listOf types.str;
580 default = [];
581 example = [ "--drop-capability=CAP_SYS_CHROOT" ];
582 description = ''
583 Extra flags passed to the systemd-nspawn command.
584 See systemd-nspawn(1) for details.
585 '';
586 };
587
588 } // networkOptions;
589
590 config = mkMerge
591 [
592 (mkIf options.config.isDefined {
593 path = config.config.system.build.toplevel;
594 })
595 ];
596 }));
597
598 default = {};
599 example = literalExample
600 ''
601 { webserver =
602 { path = "/nix/var/nix/profiles/webserver";
603 };
604 database =
605 { config =
606 { config, pkgs, ... }:
607 { services.postgresql.enable = true;
608 services.postgresql.package = pkgs.postgresql96;
609
610 system.stateVersion = "17.03";
611 };
612 };
613 }
614 '';
615 description = ''
616 A set of NixOS system configurations to be run as lightweight
617 containers. Each container appears as a service
618 <literal>container-<replaceable>name</replaceable></literal>
619 on the host system, allowing it to be started and stopped via
620 <command>systemctl</command>.
621 '';
622 };
623
624 };
625
626
627 config = mkIf (config.boot.enableContainers) (let
628
629 unit = {
630 description = "Container '%i'";
631
632 unitConfig.RequiresMountsFor = [ "/var/lib/containers/%i" ];
633
634 path = [ pkgs.iproute ];
635
636 environment.INSTANCE = "%i";
637 environment.root = "/var/lib/containers/%i";
638
639 preStart = preStartScript dummyConfig;
640
641 script = startScript dummyConfig;
642
643 postStart = postStartScript dummyConfig;
644
645 preStop =
646 ''
647 pid="$(cat /run/containers/$INSTANCE.pid)"
648 if [ -n "$pid" ]; then
649 kill -RTMIN+4 "$pid"
650 fi
651 rm -f "/run/containers/$INSTANCE.pid"
652 '';
653
654 restartIfChanged = false;
655
656 serviceConfig = serviceDirectives dummyConfig;
657 };
658 in {
659 systemd.services = listToAttrs (filter (x: x.value != null) (
660 # The generic container template used by imperative containers
661 [{ name = "container@"; value = unit; }]
662 # declarative containers
663 ++ (mapAttrsToList (name: cfg: nameValuePair "container@${name}" (let
664 config = cfg // (
665 if cfg.enableTun then
666 {
667 allowedDevices = cfg.allowedDevices
668 ++ [ { node = "/dev/net/tun"; modifier = "rw"; } ];
669 additionalCapabilities = cfg.additionalCapabilities
670 ++ [ "CAP_NET_ADMIN" ];
671 }
672 else {});
673 in
674 unit // {
675 preStart = preStartScript config;
676 script = startScript config;
677 postStart = postStartScript config;
678 serviceConfig = serviceDirectives config;
679 } // (
680 if config.autoStart then
681 {
682 wantedBy = [ "multi-user.target" ];
683 wants = [ "network.target" ];
684 after = [ "network.target" ];
685 restartTriggers = [ config.path ];
686 reloadIfChanged = true;
687 }
688 else {})
689 )) config.containers)
690 ));
691
692 # Generate a configuration file in /etc/containers for each
693 # container so that container@.target can get the container
694 # configuration.
695 environment.etc =
696 let mkPortStr = p: p.protocol + ":" + (toString p.hostPort) + ":" + (if p.containerPort == null then toString p.hostPort else toString p.containerPort);
697 in mapAttrs' (name: cfg: nameValuePair "containers/${name}.conf"
698 { text =
699 ''
700 SYSTEM_PATH=${cfg.path}
701 ${optionalString cfg.privateNetwork ''
702 PRIVATE_NETWORK=1
703 ${optionalString (cfg.hostBridge != null) ''
704 HOST_BRIDGE=${cfg.hostBridge}
705 ''}
706 ${optionalString (length cfg.forwardPorts > 0) ''
707 HOST_PORT=${concatStringsSep "," (map mkPortStr cfg.forwardPorts)}
708 ''}
709 ${optionalString (cfg.hostAddress != null) ''
710 HOST_ADDRESS=${cfg.hostAddress}
711 ''}
712 ${optionalString (cfg.hostAddress6 != null) ''
713 HOST_ADDRESS6=${cfg.hostAddress6}
714 ''}
715 ${optionalString (cfg.localAddress != null) ''
716 LOCAL_ADDRESS=${cfg.localAddress}
717 ''}
718 ${optionalString (cfg.localAddress6 != null) ''
719 LOCAL_ADDRESS6=${cfg.localAddress6}
720 ''}
721 ''}
722 INTERFACES="${toString cfg.interfaces}"
723 MACVLANS="${toString cfg.macvlans}"
724 ${optionalString cfg.autoStart ''
725 AUTO_START=1
726 ''}
727 EXTRA_NSPAWN_FLAGS="${mkBindFlags cfg.bindMounts +
728 optionalString (cfg.extraFlags != [])
729 (" " + concatStringsSep " " cfg.extraFlags)}"
730 '';
731 }) config.containers;
732
733 # Generate /etc/hosts entries for the containers.
734 networking.extraHosts = concatStrings (mapAttrsToList (name: cfg: optionalString (cfg.localAddress != null)
735 ''
736 ${head (splitString "/" cfg.localAddress)} ${name}.containers
737 '') config.containers);
738
739 networking.dhcpcd.denyInterfaces = [ "ve-*" "vb-*" ];
740
741 services.udev.extraRules = optionalString config.networking.networkmanager.enable ''
742 # Don't manage interfaces created by nixos-container.
743 ENV{INTERFACE}=="v[eb]-*", ENV{NM_UNMANAGED}="1"
744 '';
745
746 environment.systemPackages = [ pkgs.nixos-container ];
747 });
748}