Unverified Commit a13b4230 authored by Lucas Savva's avatar Lucas Savva Committed by GitHub
Browse files

nixos-containers: allow hard-coding container veth MAC address & add IPv6 SLAAC test case (#462775)

parents b6d12e93 dee54f29
Loading
Loading
Loading
Loading
+25 −2
Original line number Diff line number Diff line
@@ -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
@@ -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

@@ -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"} \
@@ -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 = {
@@ -501,6 +520,7 @@ let
    hostAddress6 = null;
    localAddress = null;
    localAddress6 = null;
    localmacAddress = null;
    tmpfs = null;
  };

@@ -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}
+1 −0
Original line number Diff line number Diff line
@@ -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;
+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")
  '';
}