nixos-containers: allow hard-coding container veth MAC address & add IPv6 SLAAC test case (#462775)
This commit is contained in:
@@ -56,8 +56,13 @@ let
|
|||||||
# Initialise the container side of the veth pair.
|
# Initialise the container side of the veth pair.
|
||||||
if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] ||
|
if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${HOST_ADDRESS6-}" ]] ||
|
||||||
[[ -n "''${LOCAL_ADDRESS-}" ]] || [[ -n "''${LOCAL_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
|
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
|
ip link set dev eth0 up
|
||||||
|
|
||||||
if [[ -n "''${LOCAL_ADDRESS-}" ]]; then
|
if [[ -n "''${LOCAL_ADDRESS-}" ]]; then
|
||||||
@@ -140,7 +145,8 @@ let
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "''${HOST_ADDRESS-}" ]] || [[ -n "''${LOCAL_ADDRESS-}" ]] ||
|
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")
|
extraFlags+=("--network-veth")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -207,6 +213,7 @@ let
|
|||||||
--setenv LOCAL_ADDRESS="''${LOCAL_ADDRESS-}" \
|
--setenv LOCAL_ADDRESS="''${LOCAL_ADDRESS-}" \
|
||||||
--setenv HOST_ADDRESS6="''${HOST_ADDRESS6-}" \
|
--setenv HOST_ADDRESS6="''${HOST_ADDRESS6-}" \
|
||||||
--setenv LOCAL_ADDRESS6="''${LOCAL_ADDRESS6-}" \
|
--setenv LOCAL_ADDRESS6="''${LOCAL_ADDRESS6-}" \
|
||||||
|
--setenv LOCAL_MAC_ADDRESS="''${LOCAL_MAC_ADDRESS-}" \
|
||||||
--setenv HOST_PORT="''${HOST_PORT-}" \
|
--setenv HOST_PORT="''${HOST_PORT-}" \
|
||||||
--setenv PATH="$PATH" \
|
--setenv PATH="$PATH" \
|
||||||
${optionalString cfg.ephemeral "--ephemeral"} \
|
${optionalString cfg.ephemeral "--ephemeral"} \
|
||||||
@@ -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 = {
|
dummyConfig = {
|
||||||
@@ -501,6 +520,7 @@ let
|
|||||||
hostAddress6 = null;
|
hostAddress6 = null;
|
||||||
localAddress = null;
|
localAddress = null;
|
||||||
localAddress6 = null;
|
localAddress6 = null;
|
||||||
|
localmacAddress = null;
|
||||||
tmpfs = null;
|
tmpfs = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1115,6 +1135,9 @@ in
|
|||||||
${optionalString (cfg.localAddress6 != null) ''
|
${optionalString (cfg.localAddress6 != null) ''
|
||||||
LOCAL_ADDRESS6=${cfg.localAddress6}
|
LOCAL_ADDRESS6=${cfg.localAddress6}
|
||||||
''}
|
''}
|
||||||
|
${optionalString (cfg.localMacAddress != null) ''
|
||||||
|
LOCAL_MAC_ADDRESS=${cfg.localMacAddress}
|
||||||
|
''}
|
||||||
''}
|
''}
|
||||||
${optionalString (cfg.networkNamespace != null) ''
|
${optionalString (cfg.networkNamespace != null) ''
|
||||||
NETWORK_NAMESPACE_PATH=${cfg.networkNamespace}
|
NETWORK_NAMESPACE_PATH=${cfg.networkNamespace}
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ in
|
|||||||
containers-hosts = runTest ./containers-hosts.nix;
|
containers-hosts = runTest ./containers-hosts.nix;
|
||||||
containers-imperative = runTest ./containers-imperative.nix;
|
containers-imperative = runTest ./containers-imperative.nix;
|
||||||
containers-ip = runTest ./containers-ip.nix;
|
containers-ip = runTest ./containers-ip.nix;
|
||||||
|
containers-ipv6-slaac = runTest ./containers-ipv6-slaac.nix;
|
||||||
containers-macvlans = runTest ./containers-macvlans.nix;
|
containers-macvlans = runTest ./containers-macvlans.nix;
|
||||||
containers-names = runTest ./containers-names.nix;
|
containers-names = runTest ./containers-names.nix;
|
||||||
containers-nested = runTest ./containers-nested.nix;
|
containers-nested = runTest ./containers-nested.nix;
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
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")
|
||||||
|
'';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user