{ pkgs, lib, config, options, ... }: let cfg = config.services.netns; inherit (config) networking; # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html escapeUnitName = name: lib.concatMapStrings (s: if lib.isList s then "-" else s) ( builtins.split "[^a-zA-Z0-9_.\\-]+" name ); in { options.services.netns = { namespaces = lib.mkOption { description = '' Network namespaces to create. Other services can join a network namespace named `netns` with: ``` PrivateNetwork=true; JoinsNamespaceOf="netns-''${netns}.service"; ``` So can `iproute2` with: `ip -n ''${netns}` ::: {.warning} You should usually create (or update via your VPN configuration's up script) a file named `/etc/netns/''${netns}/resolv.conf` that will be bind-mounted by `ip -n ''${netns}` onto `/etc/resolv.conf`, which you'll also want to configure in the services joining this network namespace: ``` BindReadOnlyPaths = ["/etc/netns/''${netns}/resolv.conf:/etc/resolv.conf"]; ``` ::: ''; default = { }; type = lib.types.attrsOf ( lib.types.submodule { options.nftables = lib.mkOption { description = "Nftables ruleset within the network namespace."; type = lib.types.lines; default = networking.nftables.ruleset; defaultText = "config.networking.nftables.ruleset"; }; options.sysctl = options.boot.kernel.sysctl // { description = "sysctl within the network namespace."; default = config.boot.kernel.sysctl; defaultText = lib.literalExpression "config.boot.kernel.sysctl"; }; options.service = lib.mkOption { description = "Systemd configuration specific to this netns service"; type = lib.types.attrs; default = { }; }; } ); }; }; config = { systemd.services = lib.mapAttrs' ( name: conf: lib.nameValuePair "netns-${escapeUnitName name}" ( lib.mkMerge [ { description = "${name} network namespace"; before = [ "network.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; # Explanation: let systemd create the netns # so that PrivateNetwork=true # with JoinsNamespaceOf="netns-${name}.service" works. PrivateNetwork = true; # Explanation: using PrivateMounts=true would prevent # the sharing of the mount bind /var/run/netns/$name # done by `ip netns attach`, # causing outside `ip netns exec $name $SHELL` to fail with: # Error: Peer netns reference is invalid. PrivateMounts = false; ExecStart = [ (pkgs.writeShellScript "ip-netns-attach" ( lib.concatStringsSep "\n" [ # Explanation: for convenience, register the netns # to the tracking mechanism of iproute2, # and make sure resolv.conf can be used in BindReadOnlyPaths= # For propagating changes in that file to the services bind mounting it, # updating must not remove the file, but only truncate it. '' set -eux ${lib.getExe' pkgs.iproute2 "ip"} netns delete ${lib.escapeShellArg name} || true ${lib.getExe' pkgs.iproute2 "ip"} netns attach ${lib.escapeShellArg name} $$ mkdir -p /etc/netns/${lib.escapeShellArg name} touch /etc/netns/${lib.escapeShellArg name}/resolv.conf '' # Explanation: bringing the loopback interface is almost always a good thing. "${lib.getExe' pkgs.iproute2 "ip"} link set dev lo up" # Explanation: use --ignore because some keys # may no longer exist in that new namespace, # like net.ipv6.conf.eth0.addr_gen_mode or net.core.rmem_max . '' ${lib.getExe' pkgs.procps "sysctl"} --ignore -p ${ pkgs.writeScript "sysctl" ( lib.concatStrings ( lib.mapAttrsToList ( n: v: lib.optionalString (v != null) "${n}=${if v == false then "0" else toString v}\n" ) conf.sysctl ) ) } || true '' ] )) ] ++ # Load the nftables ruleset of this netns. lib.optional networking.nftables.enable ( pkgs.writeScript "nftables-ruleset" '' #!${lib.getExe' pkgs.nftables "nft"} -f flush ruleset ${conf.nftables} '' ); # Unregister the netns from the tracking mechanism of iproute2. ExecStop = "${lib.getExe' pkgs.iproute2 "ip"} netns delete ${lib.escapeShellArg name}"; }; } conf.service ] ) ) cfg.namespaces; meta.maintainers = with lib.maintainers; [ julm ]; }; }