From 52600dbb0052e6c2246f66390d799ab189b2d977 Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Tue, 10 Dec 2024 20:54:41 +0800 Subject: [PATCH] networking/netns: prefer ipv6 by introducing gai.conf --- nixos/modules/networking/netns/common.nix | 140 ++++++-------------- nixos/modules/networking/netns/mntns.nix | 149 ++++++++++++++++++++++ nixos/modules/networking/netns/nscd.nix | 91 ++++++++----- 3 files changed, 243 insertions(+), 137 deletions(-) create mode 100644 nixos/modules/networking/netns/mntns.nix diff --git a/nixos/modules/networking/netns/common.nix b/nixos/modules/networking/netns/common.nix index d5eec8e..230a321 100644 --- a/nixos/modules/networking/netns/common.nix +++ b/nixos/modules/networking/netns/common.nix @@ -13,7 +13,7 @@ in options.networking.netns = mkOption { type = types.attrsOf ( types.submodule ( - { name, config, ... }: + { name, ... }: { options = { netnsPath = mkOption { @@ -24,14 +24,6 @@ in Path to the network namespace. ''; }; - mntnsPath = mkOption { - type = types.str; - default = if name == "default" then "/proc/1/ns/mnt" else "/run/${name}/mntns/${name}"; - readOnly = true; - description = '' - Path to the auxiliary mount namespace. - ''; - }; interface = mkOption { type = types.str; default = name; @@ -60,25 +52,6 @@ in Whether to enable IPv6 packet forwarding in the network namespace. ''; }; - serviceConfig = mkOption { - type = types.attrs; - default = - if name == "default" then - { } - else - { - NetworkNamespacePath = config.netnsPath; - BindReadOnlyPaths = optionals config.enableDNSIsolation [ - "/etc/netns/${name}/resolv.conf:/etc/resolv.conf:norbind" - "/etc/netns/${name}/nsswitch.conf:/etc/nsswitch.conf:norbind" - "/run/${name}/nscd:/run/nscd:norbind" - ]; - }; - readOnly = true; - description = '' - Systemd service configuration for entering the network namespace. - ''; - }; }; } ) @@ -91,82 +64,41 @@ in config = { networking.netns.default = { }; - systemd.services = listToAttrs ( - mapAttrsToList ( - name: cfg: - let - inherit (cfg) interface address; - enableIPv4Forwarding = if cfg.enableIPv4Forwarding then "1" else "0"; - enableIPv6Forwarding = if cfg.enableIPv6Forwarding then "1" else "0"; - in - nameValuePair "netns-${name}" { - path = with pkgs; [ - coreutils - iproute2 - procps - ]; - script = '' - ip netns add ${name} - ip -n ${name} link add ${interface} type dummy - ip -n ${name} link set lo up - ip -n ${name} link set ${interface} up - ip netns exec ${name} sysctl -w net.ipv4.conf.default.forwarding=${enableIPv4Forwarding} - ip netns exec ${name} sysctl -w net.ipv4.conf.all.forwarding=${enableIPv4Forwarding} - ip netns exec ${name} sysctl -w net.ipv6.conf.default.forwarding=${enableIPv6Forwarding} - ip netns exec ${name} sysctl -w net.ipv6.conf.all.forwarding=${enableIPv6Forwarding} - ${concatMapStringsSep "\n" (addr: "ip -n ${name} addr add ${addr} dev ${interface}") address} - ''; - preStop = '' - ip netns del ${name} - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - } - ) nonDefaultNetns - ++ mapAttrsToList ( - name: cfg: - let - inherit (cfg) mntnsPath enableDNSIsolation; - in - nameValuePair "netns-${name}-mntns" { - path = with pkgs; [ - coreutils - util-linux - ]; - script = '' - touch ${mntnsPath} || echo "${mntnsPath} already exists" - unshare --mount=${mntnsPath} --propagation slave true - ${optionalString enableDNSIsolation '' - nsenter --mount=${mntnsPath} mount --bind --read-only /etc/netns/${name}/resolv.conf /etc/resolv.conf - nsenter --mount=${mntnsPath} mount --bind --read-only /etc/netns/${name}/nsswitch.conf /etc/nsswitch.conf - nsenter --mount=${mntnsPath} mount --bind --read-only /run/${name}/nscd /run/nscd - ''} - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - RuntimeDirectory = "${name}/mntns"; - }; - after = - [ - "netns-${name}.service" - "network.target" - ] - ++ optionals enableDNSIsolation [ - "netns-${name}-nscd.service" - ]; - partOf = [ "netns-${name}.service" ]; - wantedBy = [ - "multi-user.target" - "netns-${name}.service" - ]; - } - ) nonDefaultNetns - ); + systemd.services = mapAttrs' ( + name: cfg: + let + inherit (cfg) interface address; + enableIPv4Forwarding = if cfg.enableIPv4Forwarding then "1" else "0"; + enableIPv6Forwarding = if cfg.enableIPv6Forwarding then "1" else "0"; + in + nameValuePair "netns-${name}" { + path = with pkgs; [ + coreutils + iproute2 + procps + ]; + script = '' + ip netns add ${name} + ip -n ${name} link add ${interface} type dummy + ip -n ${name} link set lo up + ip -n ${name} link set ${interface} up + ip netns exec ${name} sysctl -w net.ipv4.conf.default.forwarding=${enableIPv4Forwarding} + ip netns exec ${name} sysctl -w net.ipv4.conf.all.forwarding=${enableIPv4Forwarding} + ip netns exec ${name} sysctl -w net.ipv6.conf.default.forwarding=${enableIPv6Forwarding} + ip netns exec ${name} sysctl -w net.ipv6.conf.all.forwarding=${enableIPv6Forwarding} + ${concatMapStringsSep "\n" (addr: "ip -n ${name} addr add ${addr} dev ${interface}") address} + ''; + preStop = '' + ip netns del ${name} + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + } + ) nonDefaultNetns; environment.systemPackages = mkIf (nonDefaultNetns != { }) ( mapAttrsToList ( diff --git a/nixos/modules/networking/netns/mntns.nix b/nixos/modules/networking/netns/mntns.nix new file mode 100644 index 0000000..ef80eea --- /dev/null +++ b/nixos/modules/networking/netns/mntns.nix @@ -0,0 +1,149 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + allNetns = config.networking.netns; + nonDefaultNetns = filterAttrs (name: _cfg: name != "default") allNetns; +in +{ + options.networking.netns = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, config, ... }: + { + options = { + mntnsPath = mkOption { + type = types.str; + default = if name == "default" then "/proc/1/ns/mnt" else "/run/${name}/mntns/${name}"; + readOnly = true; + description = '' + Path to the auxiliary mount namespace. + ''; + }; + bindMounts = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + mountPoint = mkOption { + type = types.str; + default = name; + description = '' + Mount point on the auxiliary mount namespace. + ''; + }; + hostPath = mkOption { + type = types.str; + description = '' + Location of the path to be mounted in the default mount namespace. + ''; + }; + isReadOnly = mkOption { + type = types.bool; + default = true; + description = '' + Determine whether the mounted path will be accessed in read-only mode. + ''; + }; + }; + } + ) + ); + default = { }; + description = '' + A extra list of bind mounts that is bound to the network namespace. + ''; + }; + serviceConfig = mkOption { + type = types.attrs; + default = + if name == "default" then + { } + else + let + rwBinds = filter (d: d.isReadOnly == false) (attrValues config.bindMounts); + roBinds = filter (d: d.isReadOnly == true) (attrValues config.bindMounts); + in + { + NetworkNamespacePath = config.netnsPath; + BindPaths = map (d: "${d.hostPath}:${d.mountPoint}:norbind") rwBinds; + BindReadOnlyPaths = map (d: "${d.hostPath}:${d.mountPoint}:norbind") roBinds; + }; + readOnly = true; + description = '' + Systemd service configuration for entering the network namespace. + ''; + }; + }; + } + ) + ); + }; + + config = { + systemd.services = mapAttrs' ( + name: cfg: + let + inherit (cfg) mntnsPath bindMounts; + in + nameValuePair "netns-${name}-mntns" { + path = with pkgs; [ + coreutils + util-linux + bash + ]; + script = '' + [ ! -e "${mntnsPath}" ] && touch ${mntnsPath} + unshare --mount=${mntnsPath} --propagation slave true + nsenter --mount=${mntnsPath} bash ${pkgs.writeShellScript "netns-${name}-mntns-bind-mount" '' + declare -A bind_mounts=( + ${ + concatMapStringsSep "\n" (d: '' + ["${d.mountPoint}"]="${d.hostPath}:${if d.isReadOnly then "ro" else "rw"}" + '') (attrValues bindMounts) + } + ) + + for mount_point in "''${!bind_mounts[@]}"; do + IFS=':' read -r host_path mount_option <<< "''${bind_mounts[$mount_point]}" + + if [ -f "$host_path" ]; then + [ ! -e "$mount_point" ] && touch "$mount_point" + elif [ -d "$host_path" ]; then + [ ! -e "$mount_point" ] && mkdir -p "$mount_point" + else + echo "Error: $host_path is neither a file nor a directory" + continue + fi + + if [ "$mount_option" = "ro" ]; then + mount --bind --read-only "$host_path" "$mount_point" + else + mount --bind "$host_path" "$mount_point" + fi + done + ''} + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + RuntimeDirectory = "${name}/mntns"; + }; + after = [ + "netns-${name}.service" + "network.target" + ]; + partOf = [ "netns-${name}.service" ]; + wantedBy = [ + "multi-user.target" + "netns-${name}.service" + ]; + } + ) nonDefaultNetns; + }; +} diff --git a/nixos/modules/networking/netns/nscd.nix b/nixos/modules/networking/netns/nscd.nix index 0b74837..5aa1040 100644 --- a/nixos/modules/networking/netns/nscd.nix +++ b/nixos/modules/networking/netns/nscd.nix @@ -14,7 +14,7 @@ in options.networking.netns = mkOption { type = types.attrsOf ( types.submodule ( - { ... }: + { name, config, ... }: { options.enableDNSIsolation = mkOption { type = types.bool; @@ -24,6 +24,15 @@ in DNS requests in this namespace may be exposed to other namespaces. ''; }; + + config = mkIf config.enableDNSIsolation { + bindMounts = { + "/run/nscd".hostPath = "/run/${name}/nscd"; + "/etc/resolv.conf".hostPath = "/etc/netns/${name}/resolv.conf"; + "/etc/nsswitch.conf".hostPath = "/etc/netns/${name}/nsswitch.conf"; + "/etc/gai.conf".hostPath = "/etc/netns/${name}/gai.conf"; + }; + }; } ) ); @@ -42,6 +51,7 @@ in BindReadOnlyPaths = [ "/etc/netns/${name}/resolv.conf:/etc/resolv.conf:norbind" "/etc/netns/${name}/nsswitch.conf:/etc/nsswitch.conf:norbind" + "/etc/netns/${name}/gai.conf:/etc/gai.conf:norbind" ]; BindPaths = [ "/run/${name}/nscd:/run/nscd:norbind" ]; Type = "notify"; @@ -53,6 +63,7 @@ in ExecStart = "${pkgs.nsncd}/bin/nsncd"; }; environment.LD_LIBRARY_PATH = config.system.nssModules.path; + before = [ "netns-${name}-mntns.service" ]; after = [ "netns-${name}.service" "network.target" @@ -76,40 +87,54 @@ in users.groups = mapAttrs' (name: _cfg: nameValuePair "${name}-nscd" { }) dnsIsolatedNetns; environment.etc = listToAttrs ( - mapAttrsToList ( - name: _cfg: - nameValuePair "netns/${name}/resolv.conf" { - source = mkDefault ( - 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 - '' - ); - } - ) dnsIsolatedNetns - ++ mapAttrsToList ( - name: _cfg: - nameValuePair "netns/${name}/nsswitch.conf" { - source = mkDefault ( - 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} + flatten ( + mapAttrsToList (name: _cfg: [ + (nameValuePair "netns/${name}/resolv.conf" { + source = mkDefault ( + 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 + '' + ); + }) + (nameValuePair "netns/${name}/nsswitch.conf" { + source = mkDefault ( + 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 + hosts: ${concatStringsSep " " (remove "resolve [!UNAVAIL=return]" config.system.nssDatabases.hosts)} + networks: files - ethers: files - services: ${concatStringsSep " " config.system.nssDatabases.services} - protocols: files - rpc: files - '' - ); - } - ) dnsIsolatedNetns + ethers: files + services: ${concatStringsSep " " config.system.nssDatabases.services} + protocols: files + rpc: files + '' + ); + }) + (nameValuePair "netns/${name}/gai.conf" { + source = mkDefault ( + pkgs.writeText "netns-default-gai-conf" '' + label ::1/128 0 + label ::/0 1 + label 2002::/16 2 + label ::/96 3 + label ::ffff:0:0/96 4 + precedence ::1/128 50 + precedence ::/0 40 + precedence 2002::/16 30 + precedence ::/96 20 + precedence ::ffff:0:0/96 10 + '' + ); + }) + ]) dnsIsolatedNetns + ) ); }; }