{ config, lib, pkgs, sane-lib, ... }: let cfg = config.sane.netns; netnsOpts = with lib; types.submodule { options = { dns = mkOption { type = types.str; default = "1.1.1.1"; #< TODO: make the default be to forward DNS queries to the init namespace. }; hostVethIpv4 = mkOption { type = types.str; }; netnsVethIpv4 = mkOption { type = types.str; }; netnsPubIpv4 = mkOption { type = types.str; }; routeTable = mkOption { type = types.int; description = '' numeric ID for iproute2 (0-255?). each netns gets its own routing table so that i can route a packet out by placing it in the table. ''; }; }; }; mkNetNsConfig = name: opts: with opts; { networking.localCommands = let iptables = "${pkgs.iptables}/bin/iptables"; in-ns = "ip netns exec ${name}"; bridgePort = port: proto: '' ${in-ns} ${iptables} -A PREROUTING -t nat -p ${proto} --dport ${port} -m iprange --dst-range ${netnsPubIpv4} \ -j DNAT --to-destination ${hostVethIpv4} ''; bridgeStatements = lib.foldlAttrs (acc: port: portCfg: acc ++ (builtins.map (bridgePort port) portCfg.protocol)) [] (lib.filterAttrs (port: portCfg: portCfg.visibleTo."${name}") config.sane.ports.ports ) ; in '' ip netns add ${name} || (test -e /run/netns/${name} && echo "${name} already exists") # DOCS: # - some of this approach is described here: # - iptables primer: # create veth pair ip link add ${name}-veth-a type veth peer name ${name}-veth-b || echo "${name}-veth-{a,b} aleady exists" ip addr add ${hostVethIpv4}/24 dev ${name}-veth-a || echo "${name}-veth-a aleady has IP address" ip link set ${name}-veth-a up # move veth-b into the namespace ip link set ${name}-veth-b netns ${name} || echo "${name}-veth-b was already moved into its netns" ${in-ns} ip addr add ${netnsVethIpv4}/24 dev ${name}-veth-b || echo "${name}-veth-b aleady has IP address" ${in-ns} ip link set ${name}-veth-b up # make it so traffic originating from the host side of the veth # is sent over the veth no matter its destination. ip rule add from ${hostVethIpv4} lookup ${name} pref 50 || echo "${name} already has ip rules (pref 50)" # for traffic originating at the host veth to the WAN, use the veth as our gateway # not sure if the metric 1002 matters. ip route add default via ${netnsVethIpv4} dev ${name}-veth-a proto kernel src ${hostVethIpv4} metric 1002 table ${name} || \ echo "${name} already has default route" # give the default route lower priority ip rule add from all lookup local pref 100 || echo "${name}: already has ip rules (pref 100)" ip rule del from all lookup local pref 0 || echo "${name}: already removed ip rule of default lookup (pref 0)" # in order to access DNS in this netns, we need to route it to the VPN's nameservers # - alternatively, we could fix DNS servers like 1.1.1.1. ${in-ns} ${iptables} -A OUTPUT -t nat -p udp --dport 53 -m iprange --dst-range 127.0.0.53 \ -j DNAT --to-destination ${dns}:53 '' + (lib.concatStringsSep "\n" bridgeStatements); # postShutdown = '' # ${in-ns} ip link del ${name}-veth-b || echo "couldn't delete ${name}-veth-b" # ip link del ${name}-veth-a || echo "couldn't delete ${name}-veth-a" # ip netns delete ${name} || echo "couldn't delete ${name}" # # restore rules/routes # ip rule del from ${veth-host-ip} lookup ${name} pref 50 || echo "couldn't delete init -> ${name} rule" # ip route del default via ${veth-local-ip} dev ${name}-veth-a proto kernel src ${veth-host-ip} metric 1002 table ${name} || echo "couldn't delete init > #{name} route" # ip rule add from all lookup local pref 0 # ip rule del from all lookup local pref 100 # ''; # specifically, we need to set these up before wireguard-wg-*, # whose unit files are BEFORE "network.target", and therefore # ordered ambiguously w.r.t. network-local-commands (a dep of "network.target"). systemd.services.network-local-commands.wantedBy = [ "network-pre.target" ]; systemd.services.network-local-commands.before = [ "network-pre.target" ]; # create a new routing table that we can use to proxy traffic out of the root namespace # through the wireguard namespaces, and to the WAN via VPN. # i think the numbers here aren't particularly important. networking.iproute2.rttablesExtraConfig = '' ${builtins.toString routeTable} ${name} ''; networking.iproute2.enable = true; }; in { options = with lib; { sane.netns = mkOption { type = types.attrsOf netnsOpts; default = {}; }; }; config = let configs = lib.mapAttrsToList mkNetNsConfig cfg; take = f: { networking.localCommands = f.networking.localCommands; networking.iproute2.rttablesExtraConfig = f.networking.iproute2.rttablesExtraConfig; networking.iproute2.enable = f.networking.iproute2.enable; systemd.services.network-local-commands = f.systemd.services.network-local-commands; }; in take (sane-lib.mkTypedMerge take configs); }