2024-06-17 08:34:39 +00:00
{ 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 } / b i n / i p t a b l e s " ;
in-ns = " i p n e t n s e x e c ${ 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 } a l r e a d y e x i s t s " )
# DOCS:
# - some of this approach is described here: <https://josephmuia.ca/2018-05-16-net-namespaces-veth-nat/>
# - iptables primer: <https://danielmiessler.com/study/iptables/>
# create veth pair
ip link add $ { name } - veth-a type veth peer name $ { name } - veth-b || echo " ${ name } - v e t h - { a , b } a l e a d y e x i s t s "
ip addr add $ { hostVethIpv4 } /24 dev $ { name } - veth-a || echo " ${ name } - v e t h - a a l e a d y h a s I P a d d r e s s "
ip link set $ { name } - veth-a up
# move veth-b into the namespace
ip link set $ { name } - veth-b netns $ { name } || echo " ${ name } - v e t h - b w a s a l r e a d y m o v e d i n t o i t s n e t n s "
$ { in-ns } ip addr add $ { netnsVethIpv4 } /24 dev $ { name } - veth-b || echo " ${ name } - v e t h - b a l e a d y h a s I P a d d r e s s "
$ { 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 } a l r e a d y h a s i p r u l e s ( p r e f 5 0 ) "
# 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 } a l r e a d y h a s d e f a u l t r o u t e "
# give the default route lower priority
ip rule add from all lookup local pref 100 || echo " ${ name } : a l r e a d y h a s i p r u l e s ( p r e f 1 0 0 ) "
ip rule del from all lookup local pref 0 || echo " ${ name } : a l r e a d y r e m o v e d i p r u l e o f d e f a u l t l o o k u p ( p r e f 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
'' + ( l i b . c o n c a t S t r i n g s S e p " \ n " b r i d g e S t a t e m e n t s ) ;
# 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 = [ " n e t w o r k - p r e . t a r g e t " ] ;
systemd . services . network-local-commands . before = [ " n e t w o r k - p r e . t a r g e t " ] ;
2024-06-18 09:07:40 +00:00
systemd . targets . network-pre . wantedBy = [ " n e t w o r k . t a r g e t " ] ;
systemd . targets . network-pre . before = [ " n e t w o r k . t a r g e t " ] ;
2024-06-17 08:34:39 +00:00
# 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 ;
2024-06-18 09:07:40 +00:00
systemd . targets . network-pre = f . systemd . targets . network-pre ;
2024-06-17 08:34:39 +00:00
} ;
in take ( sane-lib . mkTypedMerge take configs ) ;
}