From 4c54e18421fd7dbddfd312a967161461e9609a18 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 28 Jul 2025 21:04:52 +0200 Subject: [PATCH 1/3] python3Packages.wgnlpy: fix exception when WGPEER_A_LAST_HANDSHAKE_TIME is None Cherry-picked from https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/83019: > The value of a peer's last handshake NLA can be None which triggers an TypeError > exception inside wgnlpy. This breaks ifstate from time to time when it is used > to configure wireguard VPNs and this bug is triggered. Although a PR[1] was > opened longer time ago the issue had not been fixed upstream, yet. > > [1]: https://github.com/ArgosyLabs/wgnlpy/pull/5 --- pkgs/development/python-modules/wgnlpy/default.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkgs/development/python-modules/wgnlpy/default.nix b/pkgs/development/python-modules/wgnlpy/default.nix index 11cb4e38b046..c50f80cd3db1 100644 --- a/pkgs/development/python-modules/wgnlpy/default.nix +++ b/pkgs/development/python-modules/wgnlpy/default.nix @@ -2,6 +2,7 @@ lib, buildPythonPackage, fetchFromGitHub, + fetchpatch, setuptools, cryptography, pyroute2, @@ -19,6 +20,16 @@ buildPythonPackage rec { hash = "sha256-5XAfBiKx4SqouA57PxmaCb0ea7mT2VeUI1tgnQE/ZwQ="; }; + patches = [ + # see https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/83019 + # Required for ifstate + # Upstream Issue/PR: https://github.com/ArgosyLabs/wgnlpy/pull/5 + (fetchpatch { + url = "https://gitlab.alpinelinux.org/alpine/aports/-/raw/1f78a31dc3e8d7ffd4ff4b8c32fabc3ad0265ae2/community/py3-wgnlpy/0001-fix-exception-when-WGPEER_A_LAST_HANDSHAKE_TIME-is-N.patch"; + hash = "sha256-MO5MMDXnaCPdakMlxCkiCBCDCiTFdG3V66l+AKb95X4="; + }) + ]; + build-system = [ setuptools ]; dependencies = [ From a956dc122c3c73cd89c5eb42cc8518f9941467d3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 28 Jul 2025 21:06:12 +0200 Subject: [PATCH 2/3] ifstate: init at 2.0.0 --- pkgs/by-name/if/ifstate/package.nix | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 pkgs/by-name/if/ifstate/package.nix diff --git a/pkgs/by-name/if/ifstate/package.nix b/pkgs/by-name/if/ifstate/package.nix new file mode 100644 index 000000000000..4d92b86701d4 --- /dev/null +++ b/pkgs/by-name/if/ifstate/package.nix @@ -0,0 +1,87 @@ +{ + lib, + python3Packages, + fetchFromGitea, + iproute2, + libbpf, + nixosTests, + withBpf ? false, + withConfigValidation ? true, + withShellColor ? false, + withWireguard ? true, +}: + +let + self = python3Packages.buildPythonApplication rec { + pname = "ifstate"; + version = "2.0.0"; + pyproject = true; + + src = fetchFromGitea { + domain = "codeberg.org"; + owner = "liske"; + repo = "ifstate"; + tag = version; + hash = "sha256-YxLyiTVLN4nxc2ppqGGnYCGudbdPLSLV8EwDURtpO0U="; + }; + + postPatch = '' + substituteInPlace libifstate/routing/__init__.py \ + --replace-fail '/usr/share/iproute2' '${iproute2}/share/iproute2' + '' + + lib.optionalString withBpf '' + substituteInPlace libifstate/bpf/ctypes.py \ + --replace-fail 'libbpf.so.1' '${libbpf}/lib/libbpf.so.1' + ''; + + build-system = with python3Packages; [ + setuptools + ]; + + dependencies = + with python3Packages; + [ + pyroute2 + pyyaml + setproctitle + ] + ++ lib.optional withConfigValidation jsonschema + ++ lib.optional withShellColor pygments + ++ lib.optional withWireguard wgnlpy; + + pythonRemoveDeps = lib.optional (!withConfigValidation) "jsonschema"; + + # has no unit tests + doCheck = false; + + pythonImportsCheck = [ + "libifstate" + "ifstate" + ]; + + passthru = { + tests = nixosTests.ifstate; + features = { + inherit + withBpf + withConfigValidation + withShellColor + withWireguard + ; + }; + # needed for access in schema validaten in module + jsonschema = "${self}/${python3Packages.python.sitePackages}/libifstate/schema/2/ifstate.conf.schema.json"; + }; + + meta = { + description = "Manage host interface settings in a declarative manner"; + homepage = "https://ifstate.net"; + changelog = "https://codeberg.org/liske/ifstate/src/tag/${src.tag}/CHANGELOG.md"; + platforms = lib.platforms.linux; + license = lib.licenses.gpl3Plus; + maintainers = with lib.maintainers; [ marcel ]; + mainProgram = "ifstatecli"; + }; + }; +in +self From 8d8c9633bf0a62167418d2259e5697496a97fd22 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 5 Aug 2025 01:01:48 +0200 Subject: [PATCH 3/3] nixos/ifstate: init --- .../manual/release-notes/rl-2511.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/networking/ifstate.nix | 311 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/ifstate/default.nix | 10 + .../ifstate/initrd-partial-broken-config.nix | 79 +++++ nixos/tests/ifstate/initrd-wireguard.nix | 121 +++++++ nixos/tests/ifstate/initrd.nix | 65 ++++ nixos/tests/ifstate/partial-broken-config.nix | 39 +++ nixos/tests/ifstate/ping.nix | 38 +++ nixos/tests/ifstate/wireguard.nix | 88 +++++ 11 files changed, 755 insertions(+) create mode 100644 nixos/modules/services/networking/ifstate.nix create mode 100644 nixos/tests/ifstate/default.nix create mode 100644 nixos/tests/ifstate/initrd-partial-broken-config.nix create mode 100644 nixos/tests/ifstate/initrd-wireguard.nix create mode 100644 nixos/tests/ifstate/initrd.nix create mode 100644 nixos/tests/ifstate/partial-broken-config.nix create mode 100644 nixos/tests/ifstate/ping.nix create mode 100644 nixos/tests/ifstate/wireguard.nix diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 4f51b1e520e6..692cfb5a8362 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -60,6 +60,8 @@ - [Newt](https://github.com/fosrl/newt), a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. Available as [services.newt](options.html#opt-services.newt.enable). +- [IfState](https://ifstate.net), manage host interface settings in a declarative manner. Available as [networking.ifstate](options.html#opt-networking.ifstate.enable) and [boot.initrd.network.ifstate](options.html#opt-boot.initrd.network.ifstate.enable). + - [qBittorrent](https://www.qbittorrent.org/), is a bittorrent client programmed in C++ / Qt that uses libtorrent by Arvid Norberg. Available as [services.qbittorrent](#opt-services.qbittorrent.enable). - [Speedify](https://speedify.com/), a proprietary VPN which allows combining multiple internet connections (Wi-Fi, 4G, 5G, Ethernet, Starlink, Satellite, and more) to improve the stability, speed, and security of online experiences. Available as [services.speedify](#opt-services.speedify.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index eb52b87a22b5..8c7cb7626405 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1180,6 +1180,7 @@ ./services/networking/i2pd.nix ./services/networking/icecream/daemon.nix ./services/networking/icecream/scheduler.nix + ./services/networking/ifstate.nix ./services/networking/imaginary.nix ./services/networking/inadyn.nix ./services/networking/inspircd.nix diff --git a/nixos/modules/services/networking/ifstate.nix b/nixos/modules/services/networking/ifstate.nix new file mode 100644 index 000000000000..f382f28efbe9 --- /dev/null +++ b/nixos/modules/services/networking/ifstate.nix @@ -0,0 +1,311 @@ +{ + lib, + config, + pkgs, + ... +}: + +let + cfg = config.networking.ifstate; + initrdCfg = config.boot.initrd.network.ifstate; + settingsFormat = { + # override generator in order to: + # - use yq and not remarshal because it matches yaml datatype handling with IfState + # - validate json schema + generate = + name: value: package: + pkgs.runCommand name + { + nativeBuildInputs = with pkgs; [ + yq + check-jsonschema + ]; + value = builtins.toJSON value; + passAsFile = [ "value" ]; + } + '' + yq --yaml-output . $valuePath > $out + check-jsonschema --schemafile "${cfg.package.passthru.jsonschema}" "$out" + sed -i $'s|\'!include |!include \'|' $out + ''; + + inherit (pkgs.formats.yaml { }) type; + }; + initrdInterfaceTypes = builtins.map (interface: interface.link.kind) ( + builtins.attrValues initrdCfg.settings.interfaces + ); + # IfState interface kind to kernel modules mapping + interfaceKernelModules = { + "ifb" = [ "ifb" ]; + "ip6tnl" = [ "ip6tnl" ]; + "ipoib" = [ "ib_ipoib" ]; + "ipvlan" = [ "ipvlan" ]; + "macvlan" = [ "macvlan" ]; + "macvtap" = [ "macvtap" ]; + "team" = [ "team" ]; + "tun" = [ "tun" ]; + "vrf" = [ "vrf" ]; + "vti" = [ "ip_vti" ]; + "vti6" = [ "ip6_vti" ]; + "bond" = [ "bonding" ]; + "bridge" = [ "bridge" ]; + # "physical" = ...; + "dsa" = [ "dsa_core" ]; + "dummy" = [ "dummy" ]; + "veth" = [ "veth" ]; + "vxcan" = [ "vxcan" ]; + "vlan" = [ "8021q" ]; + "vxlan" = [ "vxlan" ]; + "ipip" = [ "ipip" ]; + "sit" = [ "sit" ]; + "gre" = [ "ip_gre" ]; + "gretap" = [ "ip_gre" ]; + "ip6gre" = [ "ip6_gre" ]; + "ip6gretap" = [ "ip6_gre" ]; + "geneve" = [ "geneve" ]; + "wireguard" = [ "wireguard" ]; + "xfrm" = [ "xfrm_interface" ]; + }; +in +{ + meta.maintainers = with lib.maintainers; [ marcel ]; + + options = { + networking.ifstate = { + enable = lib.mkEnableOption "networking using IfState"; + + package = lib.mkPackageOption pkgs "ifstate" { }; + + settings = lib.mkOption { + inherit (settingsFormat) type; + default = { }; + description = "Content of IfState's configuration file. See for details."; + }; + }; + + boot.initrd.network.ifstate = { + enable = lib.mkEnableOption "initrd networking using IfState"; + + allowIfstateToDrasticlyIncreaseInitrdSize = lib.mkOption { + type = lib.types.bool; + default = false; + description = "IfState in initrd drastically increases the size of initrd, your boot partition may be too small and/or you may have significantly fewer generations. By setting this option, you acknowledge this fact and keep it in mind when reporting issues."; + }; + + package = lib.mkOption { + type = lib.types.package; + default = cfg.package.override { + withConfigValidation = false; + withWireguard = false; + }; + defaultText = lib.literalExpression "pkgs.ifstate.override { withConfigValidation = false; withWireguard = false; }"; + description = "The initrd IfState package to use."; + }; + + settings = lib.mkOption { + inherit (settingsFormat) type; + default = { }; + description = "Content of IfState's initrd configuration file. See for details."; + }; + + cleanupSettings = lib.mkOption { + inherit (settingsFormat) type; + default = { + # required by json schema + interfaces = { }; + # https://codeberg.org/liske/ifstate/issues/118 + namespaces = { }; + }; + description = "Content of IfState's initrd cleanup configuration file. See for details. This configuration gets applied before systemd switches to stage two. The goas is to deconfigurate the whole network in order to prevent access to services, before the firewall is configured. The stage two IfState configuration will start after the firewall is configured."; + }; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.enable { + assertions = [ + { + assertion = !config.networking.networkmanager.enable; + message = "IfState and NetworkManager cannot be used at the same time, as both configure the network in a conflicting manner."; + } + { + assertion = !config.networking.useDHCP; + message = "IfState and networking.useDHCP cannot be used at the same time, as both configure the network. Please look into IfState hooks to integrate DHCP: https://codeberg.org/liske/ifstate/issues/111"; + } + ]; + + networking.useDHCP = lib.mkDefault false; + + # sane defaults to not let IfState work against the kernel + boot.extraModprobeConfig = '' + options bonding max_bonds=0 + options dummy numdummies=0 + options ifb numifbs=0 + ''; + + environment = { + # ifstatecli command should be available to use user, there are other useful subcommands like check or show + systemPackages = [ cfg.package ]; + # match the default value of the --config flag of IfState + etc."ifstate/ifstate.yaml".source = settingsFormat.generate "ifstate.yaml" cfg.settings cfg.package; + }; + + systemd.services.ifstate = { + description = "IfState"; + + wantedBy = [ + "multi-user.target" + ]; + after = [ + "systemd-udevd.service" + "network-pre.target" + "systemd-sysusers.service" + "systemd-sysctl.service" + ]; + before = [ + "network.target" + "multi-user.target" + "shutdown.target" + "initrd-switch-root.target" + ]; + conflicts = [ + "shutdown.target" + "initrd-switch-root.target" + ]; + wants = [ + "network.target" + ]; + + # mount is always available on nixos, avoid adding additional store paths to the closure + path = [ "/run/wrappers" ]; + + serviceConfig = { + Type = "oneshot"; + ExecStart = "${lib.getExe cfg.package} --config ${ + config.environment.etc."ifstate/ifstate.yaml".source + } apply"; + # because oneshot services do not have a timeout by default + TimeoutStartSec = "2min"; + }; + }; + }) + (lib.mkIf initrdCfg.enable { + assertions = [ + { + assertion = + initrdCfg.package.passthru.features.withWireguard + || !(builtins.any (kind: kind == "wireguard") initrdInterfaceTypes); + message = "IfState initrd package is configured without the `wireguard` feature, but wireguard interfaces are configured. Please see the `boot.initrd.network.ifstate.package` option."; + } + { + assertion = initrdCfg.allowIfstateToDrasticlyIncreaseInitrdSize; + message = "IfState in initrd drastically increases the size of initrd, your boot partition may be too small and/or you may have significantly fewer generations. By setting boot.initrd.network.initrd.allowIfstateToDrasticlyIncreaseInitrdSize to true, you acknowledge this fact and keep it in mind when reporting issues."; + } + { + assertion = cfg.enable; + message = "If IfState is used in initrd, it should also be used for the stage 2 system (networking.ifstate), as initrd IfState does not clean up the network stack like it was before after execution."; + } + { + assertion = config.boot.initrd.systemd.enable; + message = "IfState only supports systemd stage one. See `boot.initrd.systemd.enable` option."; + } + ]; + + environment.etc = { + "ifstate/ifstate.initrd.yaml".source = + settingsFormat.generate "ifstate.initrd.yaml" initrdCfg.settings + initrdCfg.package; + "ifstate/ifstate.initrd-cleanup.yaml".source = + settingsFormat.generate "ifstate.initrd-cleanup.yaml" initrdCfg.cleanupSettings + initrdCfg.package; + }; + + boot.initrd = { + network.udhcpc.enable = lib.mkDefault false; + + # automatic configuration of kernel modules of virtual interface types + availableKernelModules = + let + enableModule = + type: + if builtins.hasAttr type interfaceKernelModules then interfaceKernelModules."${type}" else [ ]; + in + lib.flatten (builtins.map enableModule initrdInterfaceTypes); + + systemd = { + storePaths = [ + (pkgs.runCommand "ifstate-closure" + { + info = pkgs.closureInfo { + rootPaths = [ + initrdCfg.package + # copy whole config closure, because it can reference other files using !include + config.environment.etc."ifstate/ifstate.initrd.yaml".source + config.environment.etc."ifstate/ifstate.initrd-cleanup.yaml".source + ]; + }; + } + '' + mkdir $out + cat "$info"/store-paths | while read path; do + ln -s "$path" "$out/$(basename "$path")" + done + '' + ) + ]; + + services.ifstate-initrd = { + description = "IfState initrd"; + + wantedBy = [ + "initrd.target" + ]; + after = [ + "systemd-udevd.service" + "network-pre.target" + "systemd-sysusers.service" + "systemd-sysctl.service" + ]; + before = [ + "network.target" + "multi-user.target" + "shutdown.target" + "initrd-switch-root.target" + ]; + conflicts = [ + "shutdown.target" + "initrd-switch-root.target" + ]; + wants = [ + "network.target" + ]; + + # mount is always available on nixos, avoid adding additional store paths to the closure + # https://github.com/NixOS/nixpkgs/blob/2b8e2457ebe576ebf41ddfa8452b5b07a8d493ad/nixos/modules/system/boot/systemd/initrd.nix#L550-L551 + path = [ + config.boot.initrd.systemd.package.util-linux + ]; + + serviceConfig = { + Type = "oneshot"; + # Otherwise systemd starts ifstate again, after the encryption password was entered by the user + # and we are able to implement the cleanup using ExecStop rather than a separate unit. + RemainAfterExit = true; + # When using network namespaces pyroute2 expects this directory to exists. + # @liske is currently investigating whether this should be considered a bug in pyroute2. + ExecStartPre = "${lib.getExe' pkgs.coreutils "mkdir"} /var/run"; + ExecStart = "${lib.getExe initrdCfg.package} --config ${ + config.environment.etc."ifstate/ifstate.initrd.yaml".source + } apply"; + ExecStop = "${lib.getExe initrdCfg.package} --config ${ + config.environment.etc."ifstate/ifstate.initrd-cleanup.yaml".source + } apply"; + # because oneshot services do not have a timeout by default + TimeoutStartSec = "2min"; + }; + }; + }; + }; + }) + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 76867b74b49d..2cffed7d9443 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -723,6 +723,7 @@ in i3wm = runTest ./i3wm.nix; icingaweb2 = runTest ./icingaweb2.nix; ifm = runTest ./ifm.nix; + ifstate = import ./ifstate { inherit runTest; }; iftop = runTest ./iftop.nix; immich = runTest ./web-apps/immich.nix; immich-public-proxy = runTest ./web-apps/immich-public-proxy.nix; diff --git a/nixos/tests/ifstate/default.nix b/nixos/tests/ifstate/default.nix new file mode 100644 index 000000000000..db6985f15064 --- /dev/null +++ b/nixos/tests/ifstate/default.nix @@ -0,0 +1,10 @@ +{ runTest }: + +{ + initrd = runTest ./initrd.nix; + initrd-partial-broken-config = runTest ./initrd-partial-broken-config.nix; + initrd-wireguard = runTest ./initrd-wireguard.nix; + partial-broken-config = runTest ./partial-broken-config.nix; + ping = runTest ./ping.nix; + wireguard = runTest ./wireguard.nix; +} diff --git a/nixos/tests/ifstate/initrd-partial-broken-config.nix b/nixos/tests/ifstate/initrd-partial-broken-config.nix new file mode 100644 index 000000000000..2acd9226dde9 --- /dev/null +++ b/nixos/tests/ifstate/initrd-partial-broken-config.nix @@ -0,0 +1,79 @@ +let + mkIfStateConfig = id: { + enable = true; + settings.interfaces.eth1 = { + addresses = [ "2001:0db8::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + }; +in +{ + name = "ifstate-initrd-partial-broken-config"; + + nodes = { + server = + { lib, ... }: + { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + # Initrd IfState enforces stage 2 ifstate using assertion. + networking.ifstate = { + enable = true; + settings.interfaces = { }; + }; + + boot.initrd = { + network = { + enable = true; + ifstate = lib.mkMerge [ + (mkIfStateConfig 1) + { + allowIfstateToDrasticlyIncreaseInitrdSize = true; + + # non-existent interface; ifstate should apply eth1 and do not distrupt the boot process + settings.interfaces.eth2 = { + addresses = [ "2001:0db8:b::dead:beef/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + } + ]; + }; + systemd = { + enable = true; + network.enable = false; + services.boot-blocker = { + before = [ "initrd.target" ]; + wantedBy = [ "initrd.target" ]; + script = "sleep infinity"; + serviceConfig.Type = "oneshot"; + }; + }; + }; + }; + + client = { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking.ifstate = mkIfStateConfig 2; + }; + }; + + testScript = # python + '' + start_all() + client.wait_for_unit("network.target") + + # try to ping the server from the client + client.wait_until_succeeds("ping -c 1 2001:0db8::1") + ''; +} diff --git a/nixos/tests/ifstate/initrd-wireguard.nix b/nixos/tests/ifstate/initrd-wireguard.nix new file mode 100644 index 000000000000..7fe99a5a2646 --- /dev/null +++ b/nixos/tests/ifstate/initrd-wireguard.nix @@ -0,0 +1,121 @@ +let + mkNodeIfStateConfig = + { + pkgs, + id, + wgPriv, + wgPeerPubKey, + wgPeerId, + }: + { + enable = true; + settings = { + namespaces.outside.interfaces.eth1 = { + addresses = [ "2001:0db8:a::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + interfaces = { + wg0 = { + addresses = [ "2001:0db8:b::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "wireguard"; + bind_netns = "outside"; + }; + wireguard = { + private_key = "!include ${pkgs.writeText "wg_priv.key" wgPriv}"; + listen_port = 51820; + peers."${wgPeerPubKey}" = { + endpoint = "[2001:0db8:a::${builtins.toString wgPeerId}]:51820"; + allowedips = [ "::/0" ]; + }; + }; + }; + }; + routing.routes = [ + { + to = "2001:0db8:b::/64"; + dev = "wg0"; + } + ]; + }; + }; +in +{ + name = "ifstate-initrd-wireguard"; + + nodes = { + foo = + { pkgs, ... }: + { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + # Initrd IfState enforces stage 2 ifstate using assertion. + networking.ifstate = { + enable = true; + settings.interfaces = { }; + }; + + boot.initrd = { + network = { + enable = true; + ifstate = + mkNodeIfStateConfig { + inherit pkgs; + id = 1; + wgPriv = "6KmLyTyrN9OZIOCkdpiAwoVoeSiwvyI+mtn1wooKSEU="; + wgPeerPubKey = "olFuE7u5pVwSeWLFtrXSvD8+aCDBiKNKCLjLb/dgXiA="; + wgPeerId = 2; + } + // { + package = pkgs.ifstate.override { + withConfigValidation = false; + }; + allowIfstateToDrasticlyIncreaseInitrdSize = true; + }; + }; + systemd = { + enable = true; + network.enable = false; + services.boot-blocker = { + before = [ "initrd.target" ]; + wantedBy = [ "initrd.target" ]; + script = "sleep infinity"; + serviceConfig.Type = "oneshot"; + }; + }; + }; + }; + + bar = + { pkgs, ... }: + { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking = { + ifstate = mkNodeIfStateConfig { + inherit pkgs; + id = 2; + wgPriv = "QN89cvFD0C8z1MSpUaJa1YBXt2MaIQegVkEYROi71Fg="; + wgPeerPubKey = "5qeKbAGc7wh9Xg0MoMXqXCSmp9TawmtI1bVk/vp3Cn4="; + wgPeerId = 1; + }; + }; + }; + }; + testScript = # python + '' + start_all() + + bar.wait_for_unit("default.target") + + bar.wait_until_succeeds("ping -c 1 2001:0db8:b::1") + ''; +} diff --git a/nixos/tests/ifstate/initrd.nix b/nixos/tests/ifstate/initrd.nix new file mode 100644 index 000000000000..01ed9a513ccd --- /dev/null +++ b/nixos/tests/ifstate/initrd.nix @@ -0,0 +1,65 @@ +let + mkIfStateConfig = id: { + enable = true; + settings.interfaces.eth1 = { + addresses = [ "2001:0db8::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + }; +in +{ + name = "ifstate-initrd"; + + nodes = { + server = { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + # Initrd IfState enforces stage 2 ifstate using assertion. + networking.ifstate = { + enable = true; + settings.interfaces = { }; + }; + + boot.initrd = { + network = { + enable = true; + ifstate = mkIfStateConfig 1 // { + allowIfstateToDrasticlyIncreaseInitrdSize = true; + }; + }; + systemd = { + enable = true; + network.enable = false; + services.boot-blocker = { + before = [ "initrd.target" ]; + wantedBy = [ "initrd.target" ]; + script = "sleep infinity"; + serviceConfig.Type = "oneshot"; + }; + }; + }; + }; + + client = { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking.ifstate = mkIfStateConfig 2; + }; + }; + + testScript = # python + '' + start_all() + client.wait_for_unit("network.target") + + # try to ping the server from the client + client.wait_until_succeeds("ping -c 1 2001:0db8::1") + ''; +} diff --git a/nixos/tests/ifstate/partial-broken-config.nix b/nixos/tests/ifstate/partial-broken-config.nix new file mode 100644 index 000000000000..fe9c8edbe270 --- /dev/null +++ b/nixos/tests/ifstate/partial-broken-config.nix @@ -0,0 +1,39 @@ +{ + name = "ifstate-partial-broken-config"; + + nodes.machine = { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking.ifstate = { + enable = true; + settings.interfaces = { + eth1 = { + addresses = [ "2001:0db8:a::1/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + # non-existent interface; ifstate should apply eth1 and do not distrupt the boot process + eth2 = { + addresses = [ "2001:0db8:b::1/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + }; + }; + }; + + testScript = # python + '' + start_all() + + machine.wait_for_unit("default.target") + + machine.wait_until_succeeds("ping -c 1 2001:0db8:a::1") + ''; +} diff --git a/nixos/tests/ifstate/ping.nix b/nixos/tests/ifstate/ping.nix new file mode 100644 index 000000000000..92dab9becf9c --- /dev/null +++ b/nixos/tests/ifstate/ping.nix @@ -0,0 +1,38 @@ +let + mkNode = id: { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking.ifstate = { + enable = true; + settings.interfaces.eth1 = { + addresses = [ "2001:0db8::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + }; + }; +in + +{ + name = "ifstate-ping"; + + nodes = { + foo = mkNode 1; + bar = mkNode 2; + }; + + testScript = # python + '' + start_all() + + foo.wait_for_unit("default.target") + bar.wait_for_unit("default.target") + + foo.wait_until_succeeds("ping -c 1 2001:0db8::2") + bar.wait_until_succeeds("ping -c 1 2001:0db8::1") + ''; +} diff --git a/nixos/tests/ifstate/wireguard.nix b/nixos/tests/ifstate/wireguard.nix new file mode 100644 index 000000000000..3c822c346330 --- /dev/null +++ b/nixos/tests/ifstate/wireguard.nix @@ -0,0 +1,88 @@ +let + mkNode = + { + id, + wgPriv, + wgPeerPubKey, + wgPeerId, + }: + ( + { pkgs, ... }: + { + imports = [ ../../modules/profiles/minimal.nix ]; + + virtualisation.interfaces.eth1.vlan = 1; + + networking = { + firewall.interfaces.eth1.allowedUDPPorts = [ 51820 ]; + + ifstate = { + enable = true; + settings = { + namespaces.outside.interfaces.eth1 = { + addresses = [ "2001:0db8:a::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "physical"; + }; + }; + interfaces = { + wg0 = { + addresses = [ "2001:0db8:b::${builtins.toString id}/64" ]; + link = { + state = "up"; + kind = "wireguard"; + bind_netns = "outside"; + }; + wireguard = { + private_key = "!include ${pkgs.writeText "wg_priv.key" wgPriv}"; + listen_port = 51820; + peers."${wgPeerPubKey}" = { + endpoint = "[2001:0db8:a::${builtins.toString wgPeerId}]:51820"; + allowedips = [ "::/0" ]; + }; + }; + }; + }; + routing.routes = [ + { + to = "2001:0db8:b::/64"; + dev = "wg0"; + } + ]; + }; + }; + }; + } + ); +in + +{ + name = "ifstate-wireguard"; + + nodes = { + foo = mkNode { + id = 1; + wgPriv = "6KmLyTyrN9OZIOCkdpiAwoVoeSiwvyI+mtn1wooKSEU="; + wgPeerPubKey = "olFuE7u5pVwSeWLFtrXSvD8+aCDBiKNKCLjLb/dgXiA="; + wgPeerId = 2; + }; + bar = mkNode { + id = 2; + wgPriv = "QN89cvFD0C8z1MSpUaJa1YBXt2MaIQegVkEYROi71Fg="; + wgPeerPubKey = "5qeKbAGc7wh9Xg0MoMXqXCSmp9TawmtI1bVk/vp3Cn4="; + wgPeerId = 1; + }; + }; + + testScript = # python + '' + start_all() + + foo.wait_for_unit("default.target") + bar.wait_for_unit("default.target") + + foo.wait_until_succeeds("ping -c 1 2001:0db8:b::2") + bar.wait_until_succeeds("ping -c 1 2001:0db8:b::1") + ''; +}