From 47e19f5f91a9d3eebd7a6e5f117fba494e56a8c1 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Fri, 22 May 2026 11:54:13 +0200 Subject: [PATCH] nixos/containers: fix default gateway with privateNetwork Fixes issue reported here https://github.com/NixOS/nixpkgs/pull/515773#issuecomment-4501563586 Containers with privateNetwork have an eth0 interface configured imperatively by the container setup script, so the networking-interfaces.nix module doesn't know about it. Specifying the default gateway then fails silently, unless this setup is mirrored in networking.interfaces.eth0 inside the container. --- .../virtualisation/nixos-containers.nix | 21 ++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/containers-gateway.nix | 67 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 nixos/tests/containers-gateway.nix diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix index a952e64c5b65..84bb0b6c4856 100644 --- a/nixos/modules/virtualisation/nixos-containers.nix +++ b/nixos/modules/virtualisation/nixos-containers.nix @@ -524,6 +524,19 @@ let tmpfs = null; }; + # Parses an IPv4 address with an optional prefix + ipv4FromString = + str: + let + segments = lib.splitString "/" str; + prefix = lib.elemAt segments 1; + hasPrefix = builtins.length segments == 2; + in + { + address = lib.head segments; + prefixLength = if hasPrefix then builtins.fromJSON prefix else 32; + }; + in { @@ -594,6 +607,14 @@ in boot.isNspawnContainer = true; networking.hostName = mkDefault name; networking.useDHCP = false; + networking.interfaces = lib.mkIf config.privateNetwork { + eth0.ipv4.addresses = lib.optional (config.localAddress != null) ( + ipv4FromString config.localAddress + ); + eth0.ipv6.addresses = lib.optional (config.localAddress6 != null) ( + lib.network.ipv6.fromString config.localAddress6 + ); + }; assertions = [ { assertion = diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e653dc42b9c2..5a8b72970a68 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -385,6 +385,7 @@ in containers-custom-pkgs = runTest ./containers-custom-pkgs.nix; containers-ephemeral = runTest ./containers-ephemeral.nix; containers-extra_veth = runTest ./containers-extra_veth.nix; + containers-gateway = runTest ./containers-gateway.nix; containers-hosts = runTest ./containers-hosts.nix; containers-imperative = runTest ./containers-imperative.nix; containers-ip = runTest ./containers-ip.nix; diff --git a/nixos/tests/containers-gateway.nix b/nixos/tests/containers-gateway.nix new file mode 100644 index 000000000000..48a29f6c99dc --- /dev/null +++ b/nixos/tests/containers-gateway.nix @@ -0,0 +1,67 @@ +let + hostIp4 = "192.168.0.1"; + containerIp4 = "192.168.0.100/24"; + hostIp6 = "fc00::1"; + containerIp6 = "fc00::2/7"; +in + +{ lib, ... }: +{ + name = "containers-gateway"; + meta = { + maintainers = with lib.maintainers; [ + rnhmjoj + ]; + }; + + nodes.machine = { + networking.bridges = { + br0.interfaces = [ ]; + }; + networking.interfaces = { + br0.ipv4.addresses = [ + { + address = hostIp4; + prefixLength = 24; + } + ]; + br0.ipv6.addresses = [ + { + address = hostIp6; + prefixLength = 7; + } + ]; + }; + + containers.test = { + autoStart = true; + privateNetwork = true; + hostBridge = "br0"; + localAddress = containerIp4; + localAddress6 = containerIp6; + config.networking = { + defaultGateway.address = hostIp4; + defaultGateway6.address = hostIp6; + }; + }; + }; + + testScript = '' + def container_succeed(command: str): + machine.succeed(f"nixos-container run test -- {command}") + + machine.wait_for_unit("default.target") + assert "test" in machine.succeed("nixos-container list") + + with subtest("Container has started"): + assert "up" in machine.succeed("nixos-container status test") + + with subtest("Container can ping the host"): + container_succeed("ping -n -c 1 ${hostIp4}") + container_succeed("ping -n -c 1 ${hostIp6}") + + with subtest("Container default gateways are set"): + container_succeed("ip -4 route show default | grep 'via ${hostIp4}'") + container_succeed("ip -6 route show default | grep 'via ${hostIp6}'") + ''; +}