diff --git a/flake/nixpkgs.nix b/flake/nixpkgs.nix index 03a3be0..f4a232a 100644 --- a/flake/nixpkgs.nix +++ b/flake/nixpkgs.nix @@ -4,6 +4,7 @@ }: let overlays = [ + inputs.rebmit.overlays.default inputs.ranet.overlays.default (final: prev: { diff --git a/nixos/modules/networking/netns.nix b/nixos/modules/networking/netns.nix new file mode 100644 index 0000000..c8b4078 --- /dev/null +++ b/nixos/modules/networking/netns.nix @@ -0,0 +1,473 @@ +{ + config, + lib, + pkgs, + mylib, + ... +}: +with lib; +let + allNetns = config.networking.netns; + nonDefaultNetns = filterAttrs (_name: cfg: cfg.netns != "default") allNetns; + + mkService = + cfg: + let + inherit (cfg) + netns + interface + address + ; + enableIPv4Forwarding = if cfg.enableIPv4Forwarding then "1" else "0"; + enableIPv6Forwarding = if cfg.enableIPv6Forwarding then "1" else "0"; + in + { + path = with pkgs; [ + coreutils + iproute2 + procps + ]; + script = '' + ip netns add ${netns} + ip -n ${netns} link add ${interface} type dummy + ip -n ${netns} link set lo up + ip -n ${netns} link set ${interface} up + ip netns exec ${netns} sysctl -w net.ipv4.conf.default.forwarding=${enableIPv4Forwarding} + ip netns exec ${netns} sysctl -w net.ipv4.conf.all.forwarding=${enableIPv4Forwarding} + ip netns exec ${netns} sysctl -w net.ipv6.conf.default.forwarding=${enableIPv6Forwarding} + ip netns exec ${netns} sysctl -w net.ipv6.conf.all.forwarding=${enableIPv6Forwarding} + ${concatMapStringsSep "\n" (addr: "ip -n ${netns} addr add ${addr} dev ${interface}") address} + ''; + preStop = '' + ip netns del ${netns} + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + }; + + # https://flokli.de/posts/2022-11-18-nsncd + mkNscdService = + cfg: + let + inherit (cfg) netns netnsPath; + in + { + serviceConfig = mylib.misc.serviceHardened // { + NetworkNamespacePath = netnsPath; + BindReadOnlyPaths = [ + "/etc/netns/${netns}/resolv.conf:/etc/resolv.conf:norbind" + "/etc/netns/${netns}/nsswitch.conf:/etc/nsswitch.conf:norbind" + ]; + BindPaths = [ "/run/netns-${netns}/nscd:/run/nscd:norbind" ]; + Type = "notify"; + Restart = "always"; + RestartSec = 5; + DynamicUser = true; + RuntimeDirectory = "netns-${netns}/nscd"; + ExecStart = "${pkgs.nsncd}/bin/nsncd"; + }; + environment.LD_LIBRARY_PATH = config.system.nssModules.path; + after = [ + "netns-${netns}.service" + "network.target" + ]; + partOf = [ "netns-${netns}.service" ]; + wantedBy = [ + "multi-user.target" + "netns-${netns}.service" + ]; + }; + + mkAuxMntnsService = + cfg: + let + inherit (cfg) + netns + mntnsPath + ; + in + { + path = with pkgs; [ + coreutils + util-linux + ]; + script = '' + touch ${mntnsPath} || echo "${mntnsPath} already exists" + unshare --mount=${mntnsPath} --propagation slave mount --bind --read-only /etc/netns/${netns}/resolv.conf /etc/resolv.conf + nsenter --mount=${mntnsPath} mount --bind --read-only /etc/netns/${netns}/nsswitch.conf /etc/nsswitch.conf + nsenter --mount=${mntnsPath} mount --bind --read-only /run/netns-${netns}/nscd /run/nscd + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + after = [ + "netns-${netns}.service" + "netns-${netns}-nscd.service" + "network.target" + ]; + partOf = [ "netns-${netns}.service" ]; + wantedBy = [ + "multi-user.target" + "netns-${netns}.service" + ]; + }; + + mkPortForwardService = + cfg: fp: + let + inherit (fp) protocol source target; + sourceNetns = fp.netns; + sourceNetnsPath = config.networking.netns.${sourceNetns}.netnsPath; + targetNetns = cfg.netns; + targetNetnsConfig = cfg.serviceConfig; + serviceDeps = map (ns: "netns-${ns}.service") ( + filter (ns: ns != "default") [ + sourceNetns + targetNetns + ] + ); + in + { + serviceConfig = + mylib.misc.serviceHardened + // targetNetnsConfig + // { + Type = "simple"; + Restart = "on-failure"; + RestartSec = 5; + DynamicUser = true; + ExecStart = "${pkgs.netns-proxy}/bin/netns-proxy ${sourceNetnsPath} ${source} -b ${target} -p ${protocol} -v"; + ProtectProc = false; + RestrictNamespaces = "net"; + AmbientCapabilities = [ + "CAP_SYS_ADMIN" + "CAP_SYS_PTRACE" + ]; + CapabilityBoundingSet = [ + "CAP_SYS_ADMIN" + "CAP_SYS_PTRACE" + ]; + }; + after = [ + "network.target" + ] ++ serviceDeps; + partOf = serviceDeps; + wantedBy = [ + "multi-user.target" + ] ++ serviceDeps; + }; + + mkExtraVethService = + cfg: ev: + let + inherit (ev) + sourceInterface + targetInterface + ; + sourceNetns = cfg.netns; + sourceNetnsPath = cfg.netnsPath; + targetNetns = ev.netns; + targetNetnsPath = config.networking.netns.${targetNetns}.netnsPath; + serviceDeps = map (ns: "netns-${ns}.service") ( + filter (ns: ns != "default") [ + sourceNetns + targetNetns + ] + ); + mkSetup = + netns: _netnsPath: interface: + if netns == "default" then "ip link set ${interface} up" else "ip -n ${netns} link set ${interface} up"; + mkDrop = + netns: _netnsPath: interface: + if netns == "default" then "ip link del ${interface}" else "ip -n ${netns} link del ${interface}"; + in + { + path = with pkgs; [ + coreutils + iproute2 + procps + ]; + script = '' + ip link add ${sourceInterface} mtu 1400 address 02:00:00:00:00:01 netns ${sourceNetnsPath} type veth \ + peer ${targetInterface} mtu 1400 address 02:00:00:00:00:00 netns ${targetNetnsPath} + ${mkSetup sourceNetns sourceNetnsPath sourceInterface} + ${mkSetup targetNetns targetNetnsPath targetInterface} + ''; + preStop = '' + ${mkDrop sourceNetns sourceNetnsPath sourceInterface} + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + after = [ + "network.target" + ] ++ serviceDeps; + partOf = serviceDeps; + wantedBy = [ + "multi-user.target" + ] ++ serviceDeps; + }; + + defaultResolv = pkgs.writeText "netns-default-resolv-conf" '' + nameserver 2606:4700:4700::1111 + nameserver 2001:4860:4860::8888 + nameserver 1.1.1.1 + nameserver 8.8.8.8 + ''; + + defaultNsswitch = pkgs.writeText "netns-default-nsswitch-conf" '' + passwd: ${concatStringsSep " " config.system.nssDatabases.passwd} + group: ${concatStringsSep " " config.system.nssDatabases.group} + shadow: ${concatStringsSep " " config.system.nssDatabases.shadow} + sudoers: ${concatStringsSep " " config.system.nssDatabases.sudoers} + + hosts: ${concatStringsSep " " (remove "resolve [!UNAVAIL=return]" config.system.nssDatabases.hosts)} + networks: files + + ethers: files + services: ${concatStringsSep " " config.system.nssDatabases.services} + protocols: files + rpc: files + ''; +in +{ + options.networking.netns = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, config, ... }: + let + netnsConfig = config; + in + { + options = { + netns = mkOption { + type = types.str; + default = name; + readOnly = true; + description = '' + Name of the network namespace. + ''; + }; + netnsPath = mkOption { + type = types.str; + default = if name == "default" then "/proc/1/ns/net" else "/run/netns/${name}"; + readOnly = true; + description = '' + Path to the network namespace. + ''; + }; + mntnsPath = mkOption { + type = types.str; + default = if name == "default" then "/proc/1/ns/mnt" else "/run/netns-${name}/mntns"; + readOnly = true; + description = '' + Path to the auxiliary mount namespace. + ''; + }; + serviceConfig = mkOption { + type = types.attrs; + default = + if config.netns == "default" then + { } + else + { + NetworkNamespacePath = config.netnsPath; + BindReadOnlyPaths = [ + "/etc/netns/${config.netns}/resolv.conf:/etc/resolv.conf:norbind" + "/etc/netns/${config.netns}/nsswitch.conf:/etc/nsswitch.conf:norbind" + "/run/netns-${config.netns}/nscd:/run/nscd:norbind" + ]; + }; + readOnly = true; + description = '' + Systemd service configuration for entering the network namespace. + ''; + }; + interface = mkOption { + type = types.str; + default = name; + description = '' + Name of the dummy interface to add the address. + ''; + }; + address = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Address to be added into the network namespace as source address. + ''; + }; + enableIPv4Forwarding = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable IPv4 packet forwarding in the network namespace. + ''; + }; + enableIPv6Forwarding = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable IPv6 packet forwarding in the network namespace. + ''; + }; + forwardPorts = mkOption { + type = types.listOf ( + types.submodule { + options = { + protocol = mkOption { + type = types.enum [ + "tcp" + "udp" + ]; + default = "tcp"; + description = '' + The protocol specifier for port forwarding between network namespaces. + ''; + }; + netns = mkOption { + type = types.str; + default = "default"; + description = '' + The network namespace to forward ports from. + ''; + }; + source = mkOption { + type = types.str; + description = '' + The source endpoint in the specified network namespace to forward. + ''; + }; + target = mkOption { + type = types.str; + description = '' + The target endpoint in the current network namespace to listen on. + ''; + }; + }; + } + ); + default = [ ]; + description = '' + List of forwarded ports from another network namespace to this + network namespace. + ''; + }; + extraVeths = mkOption { + type = types.listOf ( + types.submodule ( + { config, ... }: + { + options = { + netns = mkOption { + type = types.str; + default = "default"; + description = '' + The network namespace to connect to. + ''; + }; + sourceInterface = mkOption { + type = types.str; + default = if config.netns == "default" then "host" else config.netns; + description = '' + The interface name in current network namespace; + ''; + }; + targetInterface = mkOption { + type = types.str; + default = if netnsConfig.netns == "default" then "host" else netnsConfig.netns; + description = '' + The interface name in the other network namespace; + ''; + }; + }; + } + ) + ); + default = [ ]; + description = '' + Extra veth-pairs to be created for enabling link-scope connectivity + between inter-network namespaces. + Note that a veth-pair only needs to be defined on one end. + ''; + }; + }; + } + ) + ); + description = '' + Network namespace configuration. + ''; + }; + + config = { + networking.netns.default = { }; + + systemd.services = listToAttrs ( + mapAttrsToList (_name: cfg: nameValuePair "netns-${cfg.netns}" (mkService cfg)) nonDefaultNetns + ++ mapAttrsToList ( + _name: cfg: nameValuePair "netns-${cfg.netns}-nscd" (mkNscdService cfg) + ) nonDefaultNetns + ++ mapAttrsToList ( + _name: cfg: nameValuePair "netns-${cfg.netns}-mntns" (mkAuxMntnsService cfg) + ) nonDefaultNetns + ++ flatten ( + mapAttrsToList ( + _name: cfg: + (imap ( + index: fp: + nameValuePair "netns-${cfg.netns}-port-forward-${fp.netns}-${fp.protocol}-${toString index}" ( + mkPortForwardService cfg fp + ) + ) cfg.forwardPorts) + ) allNetns + ) + ++ flatten ( + mapAttrsToList ( + _name: cfg: + (imap ( + index: ev: + nameValuePair "netns-${cfg.netns}-extra-veth-${ev.netns}-${toString index}" ( + mkExtraVethService cfg ev + ) + ) cfg.extraVeths) + ) allNetns + ) + ); + + environment.etc = listToAttrs ( + mapAttrsToList ( + _name: cfg: + nameValuePair "netns/${cfg.netns}/resolv.conf" { + source = mkDefault defaultResolv; + } + ) nonDefaultNetns + ++ mapAttrsToList ( + _name: cfg: + nameValuePair "netns/${cfg.netns}/nsswitch.conf" { + source = mkDefault defaultNsswitch; + } + ) nonDefaultNetns + ); + + environment.systemPackages = mapAttrsToList ( + name: cfg: + let + inherit (cfg) netns netnsPath mntnsPath; + in + pkgs.writeShellApplication { + name = "netns-run-${netns}"; + runtimeInputs = with pkgs; [ util-linux ]; + text = '' + pkexec nsenter -t $$ -e --mount=${mntnsPath} --net=${netnsPath} -S "$(id -u)" -G "$(id -g)" --wdns="$PWD" "$@" + ''; + } + ) allNetns; + }; +} diff --git a/nixos/modules/services/enthalpy/services.nix b/nixos/modules/services/enthalpy/services.nix index 9b9524e..1c4bd6c 100644 --- a/nixos/modules/services/enthalpy/services.nix +++ b/nixos/modules/services/enthalpy/services.nix @@ -118,17 +118,5 @@ in wantedBy = [ "multi-user.target" ]; }; }) - - (mkIf (cfg.users != { }) { - environment.systemPackages = with pkgs; [ - (pkgs.writeShellApplication { - name = "netns-run-default"; - runtimeInputs = with pkgs; [ util-linux ]; - text = '' - pkexec nsenter -t $$ -e --mount=/proc/1/ns/mnt --net=/proc/1/ns/net -S "$(id -u)" -G "$(id -g)" --wdns="$PWD" "$@" - ''; - }) - ]; - }) ]); }