Loading nixos/modules/virtualisation/nixos-containers.nix +25 −2 Original line number Diff line number Diff line Loading @@ -56,8 +56,13 @@ let # Initialise the container side of the veth pair. if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]] || [[ -n "''${HOST_BRIDGE-}" ]]; then [[ -n "''${HOST_BRIDGE-}" ]] || [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then ip link set host0 name eth0 if [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then ip link set dev eth0 address "$LOCAL_MAC_ADDRESS" fi ip link set dev eth0 up if [[ -n "''${LOCAL_ADDRESS-}" ]]; then Loading Loading @@ -140,7 +145,8 @@ let fi if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${LOCAL_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]]; then [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]] || [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then extraFlags+=("--network-veth") fi Loading Loading @@ -207,6 +213,7 @@ let --setenv LOCAL_ADDRESS="''${LOCAL_ADDRESS-}" \ --setenv HOST_ADDRESS6="''${HOST_ADDRESS6-}" \ --setenv LOCAL_ADDRESS6="''${LOCAL_ADDRESS6-}" \ --setenv LOCAL_MAC_ADDRESS="''${LOCAL_MAC_ADDRESS-}" \ --setenv HOST_PORT="''${HOST_PORT-}" \ --setenv PATH="$PATH" \ ${optionalString cfg.ephemeral "--ephemeral"} \ Loading Loading @@ -489,6 +496,18 @@ let ''; }; localMacAddress = mkOption { type = types.nullOr (lib.types.strMatching "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}"); default = null; example = "de:b7:73:01:10:90"; description = '' The MAC address assigned to the interface in the container. This address is assigned early during container boot, and can thus be reliably used for setups like IPv6 SLAAC with router advertisements. If this option is not specified, the veth devices gets assigned a random, locally-administered unicast MAC address. ''; }; }; dummyConfig = { Loading @@ -501,6 +520,7 @@ let hostAddress6 = null; localAddress = null; localAddress6 = null; localmacAddress = null; tmpfs = null; }; Loading Loading @@ -1115,6 +1135,9 @@ in ${optionalString (cfg.localAddress6 != null) '' LOCAL_ADDRESS6=${cfg.localAddress6} ''} ${optionalString (cfg.localMacAddress != null) '' LOCAL_MAC_ADDRESS=${cfg.localMacAddress} ''} ''} ${optionalString (cfg.networkNamespace != null) '' NETWORK_NAMESPACE_PATH=${cfg.networkNamespace} Loading nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -387,6 +387,7 @@ in containers-hosts = runTest ./containers-hosts.nix; containers-imperative = runTest ./containers-imperative.nix; containers-ip = runTest ./containers-ip.nix; containers-ipv6-slaac = runTest ./containers-ipv6-slaac.nix; containers-macvlans = runTest ./containers-macvlans.nix; containers-names = runTest ./containers-names.nix; containers-nested = runTest ./containers-nested.nix; Loading nixos/tests/containers-ipv6-slaac.nix 0 → 100644 +178 −0 Original line number Diff line number Diff line let ulaPrefix = "fd5f:e1a2:4f0c::/64"; hostMAC = "72:ec:00:8b:75:44"; hostSLAACv6 = "fd5f:e1a2:4f0c:0:70ec:ff:fe8b:7544/64"; containerMAC = "b2:65:3f:c9:6b:10"; containerSLAACv6 = "fd5f:e1a2:4f0c:0:b065:3fff:fec9:6b10/64"; in { pkgs, lib, ... }: { name = "containers-ipv6-slaac"; meta = { maintainers = with lib.maintainers; [ lschuermann ]; }; nodes.machine = { pkgs, ... }: { networking.useNetworkd = true; networking.useDHCP = false; systemd.network.netdevs."br0".netdevConfig = { Name = "br0"; Kind = "bridge"; MACAddress = hostMAC; }; systemd.network.networks."br0" = { name = "br0"; networkConfig = { IPv6SendRA = true; # Disable privacy extensions, which would assign the host a random # address in the ULA prefix (defeating the purpose of setting the # bridge's `MACAddress` to assign it a stable address explicitly): IPv6PrivacyExtensions = false; }; ipv6SendRAConfig = { # We assign addresses exclusively through SLAAC, not via DHCPv6: Managed = false; # This router is not a default gateway, as we don't have an IPv6 # upstream. This causes no default route to be inserted with the RA. RouterLifetimeSec = 0; UplinkInterface = ":none"; }; ipv6Prefixes = [ { # Assign addresses out of the configured ULA prefix: Prefix = ulaPrefix; AddressAutoconfiguration = true; # All other addresses in this subnet are reachable via Layer 2 (don't # need to go through the host as a router): OnLink = true; # Assign the host an address out of this subnet: Assign = true; # Use MAC address as the basis for SLAAC address generation: Token = "eui64"; } ]; # The router doesn't advertise itself as a default gateway, so we # announce our ULA prefix explicitly: ipv6RoutePrefixes = [ { Route = ulaPrefix; LifetimeSec = 1800; } ]; }; containers.webserver = { autoStart = true; privateNetwork = true; hostBridge = "br0"; localMacAddress = containerMAC; config = { networking.useNetworkd = true; networking.useHostResolvConf = false; systemd.network.networks."eth0" = { name = "eth0"; DHCP = "no"; # Assign an IPv6 address out of the host-advertised prefix, disable # privacy extensions (which would assign a random address in the # announced prefix, defeating the purpose of setting the # `localMacAddress` option to assign the container a stable # address): networkConfig = { IPv6AcceptRA = true; IPv6PrivacyExtensions = false; }; }; services.httpd.enable = true; networking.firewall.allowedTCPPorts = [ 80 ]; }; }; }; testScript = '' import time machine.wait_for_unit("default.target") assert "webserver" in machine.succeed("nixos-container list") with subtest("Start the webserver container"): assert "up" in machine.succeed("nixos-container status webserver") with subtest("veth in container has correct MAC address"): assert "${containerMAC}" in machine.succeed( "nixos-container run webserver -- ip link show eth0", ) with subtest("Host gets assigned IPv6 in and route for ULA prefix"): # This is done by systemd-network internally, so should be available # instantly: assert "${hostSLAACv6}" in machine.succeed( "ip addr show br0" ) assert "${ulaPrefix}" in machine.succeed( "ip -6 route show" ) with subtest("Container gets assigned IPv6 in and route for ULA prefix"): # Give the container a few seconds to assign itself a v6 out of and set # up a route for the ULA prefix from the router advertisement: for _ in range(3): iface_ips = machine.succeed( "nixos-container run webserver -- ip addr show eth0", ) v6_routes = machine.succeed( "nixos-container run webserver -- ip -6 route show", ) if "${containerSLAACv6}" in iface_ips and "${ulaPrefix}" in v6_routes: break else: time.sleep(1) else: raise AssertionError( "Container either did not assign itself the expected SLAAC " + "v6 out of the announced ULA prefix (${containerSLAACv6}) " + "or did not assign a route for the URL prefix " + f"(${ulaPrefix}).\n\n==> ip addr show eth0:\n{iface_ips}" + f"\n\n==> ip -6 route show:\n{v6_routes}" ) ip6 = "${containerSLAACv6}".split("/")[0] with subtest("Container reponds to ICMPv6 echo requests"): # IPv6 ND can take some time, so try at most 30 times: for i in range(30): print(f"Sending ICMP echo request, attempt #{i}") exit_status, _out = machine.execute(f"ping -n -c 1 {ip6}") if exit_status == 0: break else: time.sleep(1) else: raise AssertionError("Container doesn't respond to pings!") with subtest("Container responds to HTTP requests"): machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null") # Destroying a declarative container should fail. machine.fail("nixos-container destroy webserver") ''; } Loading
nixos/modules/virtualisation/nixos-containers.nix +25 −2 Original line number Diff line number Diff line Loading @@ -56,8 +56,13 @@ let # Initialise the container side of the veth pair. if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]] || [[ -n "''${HOST_BRIDGE-}" ]]; then [[ -n "''${HOST_BRIDGE-}" ]] || [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then ip link set host0 name eth0 if [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then ip link set dev eth0 address "$LOCAL_MAC_ADDRESS" fi ip link set dev eth0 up if [[ -n "''${LOCAL_ADDRESS-}" ]]; then Loading Loading @@ -140,7 +145,8 @@ let fi if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${LOCAL_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]]; then [[ -n "''${HOST_ADDRESS6-}" ]] || [[ -n "''${LOCAL_ADDRESS6-}" ]] || [[ -n "''${LOCAL_MAC_ADDRESS-}" ]]; then extraFlags+=("--network-veth") fi Loading Loading @@ -207,6 +213,7 @@ let --setenv LOCAL_ADDRESS="''${LOCAL_ADDRESS-}" \ --setenv HOST_ADDRESS6="''${HOST_ADDRESS6-}" \ --setenv LOCAL_ADDRESS6="''${LOCAL_ADDRESS6-}" \ --setenv LOCAL_MAC_ADDRESS="''${LOCAL_MAC_ADDRESS-}" \ --setenv HOST_PORT="''${HOST_PORT-}" \ --setenv PATH="$PATH" \ ${optionalString cfg.ephemeral "--ephemeral"} \ Loading Loading @@ -489,6 +496,18 @@ let ''; }; localMacAddress = mkOption { type = types.nullOr (lib.types.strMatching "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}"); default = null; example = "de:b7:73:01:10:90"; description = '' The MAC address assigned to the interface in the container. This address is assigned early during container boot, and can thus be reliably used for setups like IPv6 SLAAC with router advertisements. If this option is not specified, the veth devices gets assigned a random, locally-administered unicast MAC address. ''; }; }; dummyConfig = { Loading @@ -501,6 +520,7 @@ let hostAddress6 = null; localAddress = null; localAddress6 = null; localmacAddress = null; tmpfs = null; }; Loading Loading @@ -1115,6 +1135,9 @@ in ${optionalString (cfg.localAddress6 != null) '' LOCAL_ADDRESS6=${cfg.localAddress6} ''} ${optionalString (cfg.localMacAddress != null) '' LOCAL_MAC_ADDRESS=${cfg.localMacAddress} ''} ''} ${optionalString (cfg.networkNamespace != null) '' NETWORK_NAMESPACE_PATH=${cfg.networkNamespace} Loading
nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -387,6 +387,7 @@ in containers-hosts = runTest ./containers-hosts.nix; containers-imperative = runTest ./containers-imperative.nix; containers-ip = runTest ./containers-ip.nix; containers-ipv6-slaac = runTest ./containers-ipv6-slaac.nix; containers-macvlans = runTest ./containers-macvlans.nix; containers-names = runTest ./containers-names.nix; containers-nested = runTest ./containers-nested.nix; Loading
nixos/tests/containers-ipv6-slaac.nix 0 → 100644 +178 −0 Original line number Diff line number Diff line let ulaPrefix = "fd5f:e1a2:4f0c::/64"; hostMAC = "72:ec:00:8b:75:44"; hostSLAACv6 = "fd5f:e1a2:4f0c:0:70ec:ff:fe8b:7544/64"; containerMAC = "b2:65:3f:c9:6b:10"; containerSLAACv6 = "fd5f:e1a2:4f0c:0:b065:3fff:fec9:6b10/64"; in { pkgs, lib, ... }: { name = "containers-ipv6-slaac"; meta = { maintainers = with lib.maintainers; [ lschuermann ]; }; nodes.machine = { pkgs, ... }: { networking.useNetworkd = true; networking.useDHCP = false; systemd.network.netdevs."br0".netdevConfig = { Name = "br0"; Kind = "bridge"; MACAddress = hostMAC; }; systemd.network.networks."br0" = { name = "br0"; networkConfig = { IPv6SendRA = true; # Disable privacy extensions, which would assign the host a random # address in the ULA prefix (defeating the purpose of setting the # bridge's `MACAddress` to assign it a stable address explicitly): IPv6PrivacyExtensions = false; }; ipv6SendRAConfig = { # We assign addresses exclusively through SLAAC, not via DHCPv6: Managed = false; # This router is not a default gateway, as we don't have an IPv6 # upstream. This causes no default route to be inserted with the RA. RouterLifetimeSec = 0; UplinkInterface = ":none"; }; ipv6Prefixes = [ { # Assign addresses out of the configured ULA prefix: Prefix = ulaPrefix; AddressAutoconfiguration = true; # All other addresses in this subnet are reachable via Layer 2 (don't # need to go through the host as a router): OnLink = true; # Assign the host an address out of this subnet: Assign = true; # Use MAC address as the basis for SLAAC address generation: Token = "eui64"; } ]; # The router doesn't advertise itself as a default gateway, so we # announce our ULA prefix explicitly: ipv6RoutePrefixes = [ { Route = ulaPrefix; LifetimeSec = 1800; } ]; }; containers.webserver = { autoStart = true; privateNetwork = true; hostBridge = "br0"; localMacAddress = containerMAC; config = { networking.useNetworkd = true; networking.useHostResolvConf = false; systemd.network.networks."eth0" = { name = "eth0"; DHCP = "no"; # Assign an IPv6 address out of the host-advertised prefix, disable # privacy extensions (which would assign a random address in the # announced prefix, defeating the purpose of setting the # `localMacAddress` option to assign the container a stable # address): networkConfig = { IPv6AcceptRA = true; IPv6PrivacyExtensions = false; }; }; services.httpd.enable = true; networking.firewall.allowedTCPPorts = [ 80 ]; }; }; }; testScript = '' import time machine.wait_for_unit("default.target") assert "webserver" in machine.succeed("nixos-container list") with subtest("Start the webserver container"): assert "up" in machine.succeed("nixos-container status webserver") with subtest("veth in container has correct MAC address"): assert "${containerMAC}" in machine.succeed( "nixos-container run webserver -- ip link show eth0", ) with subtest("Host gets assigned IPv6 in and route for ULA prefix"): # This is done by systemd-network internally, so should be available # instantly: assert "${hostSLAACv6}" in machine.succeed( "ip addr show br0" ) assert "${ulaPrefix}" in machine.succeed( "ip -6 route show" ) with subtest("Container gets assigned IPv6 in and route for ULA prefix"): # Give the container a few seconds to assign itself a v6 out of and set # up a route for the ULA prefix from the router advertisement: for _ in range(3): iface_ips = machine.succeed( "nixos-container run webserver -- ip addr show eth0", ) v6_routes = machine.succeed( "nixos-container run webserver -- ip -6 route show", ) if "${containerSLAACv6}" in iface_ips and "${ulaPrefix}" in v6_routes: break else: time.sleep(1) else: raise AssertionError( "Container either did not assign itself the expected SLAAC " + "v6 out of the announced ULA prefix (${containerSLAACv6}) " + "or did not assign a route for the URL prefix " + f"(${ulaPrefix}).\n\n==> ip addr show eth0:\n{iface_ips}" + f"\n\n==> ip -6 route show:\n{v6_routes}" ) ip6 = "${containerSLAACv6}".split("/")[0] with subtest("Container reponds to ICMPv6 echo requests"): # IPv6 ND can take some time, so try at most 30 times: for i in range(30): print(f"Sending ICMP echo request, attempt #{i}") exit_status, _out = machine.execute(f"ping -n -c 1 {ip6}") if exit_status == 0: break else: time.sleep(1) else: raise AssertionError("Container doesn't respond to pings!") with subtest("Container responds to HTTP requests"): machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null") # Destroying a declarative container should fail. machine.fail("nixos-container destroy webserver") ''; }