This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.nat-share: init

+437
+21
tools/nat-share/flake-module.nix
··· 1 + { ... }: 2 + { 3 + perSystem = 4 + { pkgs, config, ... }: 5 + { 6 + packages.nat-share = 7 + (pkgs.writeShellApplication { 8 + name = "nat-share"; 9 + runtimeInputs = [ 10 + pkgs.dnsmasq 11 + pkgs.iptables 12 + pkgs.nftables 13 + pkgs.iproute2 14 + pkgs.gawk 15 + config.packages.pxe-share 16 + ]; 17 + text = builtins.readFile ./nat-share.sh; 18 + }).overrideAttrs 19 + { passthru.usage = builtins.readFile ./usage.kdl; }; 20 + }; 21 + }
+401
tools/nat-share/nat-share.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + PROG="nat-share" 5 + 6 + # Defaults 7 + SUBNET_PREFIX="10.42.0" 8 + NETMASK="24" 9 + DHCP_NETMASK="255.255.255.0" 10 + DHCP_START_HOST="10" 11 + DHCP_END_HOST="254" 12 + LEASE_TIME="12h" 13 + UPSTREAM="" 14 + IFACE="" 15 + DNS_SERVER="" 16 + BACKEND="auto" 17 + PXE_ROOT="" 18 + PXE_SCRIPT="netboot.ipxe" 19 + HTTP="" 20 + HTTP_PORT="8079" 21 + 22 + usage() { 23 + cat >&2 <<EOF 24 + Usage: $PROG [OPTIONS] <interface> 25 + 26 + Start a DHCP + DNS (dnsmasq) NAT server on <interface>, sharing the host's 27 + upstream internet connection with clients on that link. The interface is 28 + unmanaged from NetworkManager while the server runs; every change (IP, 29 + NetworkManager state, forwarding sysctl, firewall rules) is reverted on exit. 30 + 31 + Runs in the foreground. Press Ctrl+C to stop and restore. 32 + 33 + Options: 34 + -u, --upstream <iface> Upstream/WAN interface to NAT through 35 + (default: interface of the current default route) 36 + -s, --subnet <prefix> /24 subnet prefix for the served link 37 + (default: ${SUBNET_PREFIX}, gateway <prefix>.1) 38 + --range <start-end> DHCP host range within the /24 39 + (default: ${DHCP_START_HOST}-${DHCP_END_HOST}) 40 + --lease <time> DHCP lease time (default: ${LEASE_TIME}) 41 + --dns <ip> Upstream DNS server to forward to 42 + (default: system resolv.conf) 43 + --pxe <dir> Additionally boot PXE clients from <dir>. Handing 44 + out the boot filename happens here (only the DHCP 45 + server can); serving the files is delegated to 46 + \`pxe-share --no-dhcp\`. <dir> should hold ipxe.efi 47 + (UEFI x64), optionally ipxe32.efi / 48 + undionly.kpxe, and ${PXE_SCRIPT}. 49 + --http Have pxe-share serve the --pxe dir over HTTP as 50 + well; requires --pxe. Needed for netboot payloads: 51 + a kernel+initrd carrying a nix store is far too big 52 + for TFTP. 53 + --http-port <port> Port for --http (default: ${HTTP_PORT}) 54 + -b, --backend <name> Firewall backend: auto | iptables | nft 55 + (default: auto-detect) 56 + -h, --help Show this help 57 + EOF 58 + } 59 + 60 + # Parse arguments 61 + while [[ $# -gt 0 ]]; do 62 + case $1 in 63 + -u | --upstream) 64 + UPSTREAM="$2" 65 + shift 2 66 + ;; 67 + -s | --subnet) 68 + SUBNET_PREFIX="$2" 69 + shift 2 70 + ;; 71 + --range) 72 + DHCP_START_HOST="${2%%-*}" 73 + DHCP_END_HOST="${2##*-}" 74 + shift 2 75 + ;; 76 + --lease) 77 + LEASE_TIME="$2" 78 + shift 2 79 + ;; 80 + --dns) 81 + DNS_SERVER="$2" 82 + shift 2 83 + ;; 84 + --pxe) 85 + PXE_ROOT="$2" 86 + shift 2 87 + ;; 88 + --http) 89 + HTTP=1 90 + shift 91 + ;; 92 + --http-port) 93 + HTTP_PORT="$2" 94 + shift 2 95 + ;; 96 + -b | --backend) 97 + BACKEND="$2" 98 + shift 2 99 + ;; 100 + -h | --help) 101 + usage 102 + exit 0 103 + ;; 104 + -*) 105 + echo "$PROG: unknown option: $1" >&2 106 + usage 107 + exit 1 108 + ;; 109 + *) 110 + if [[ -n $IFACE ]]; then 111 + echo "$PROG: unexpected argument: $1" >&2 112 + usage 113 + exit 1 114 + fi 115 + IFACE="$1" 116 + shift 117 + ;; 118 + esac 119 + done 120 + 121 + if [[ -z $IFACE ]]; then 122 + echo "$PROG: no interface specified" >&2 123 + usage 124 + exit 1 125 + fi 126 + 127 + case $BACKEND in 128 + auto | iptables | nft) ;; 129 + *) 130 + echo "$PROG: invalid backend '$BACKEND' (expected auto|iptables|nft)" >&2 131 + exit 1 132 + ;; 133 + esac 134 + 135 + if ! ip link show "$IFACE" >/dev/null 2>&1; then 136 + echo "$PROG: interface '$IFACE' does not exist" >&2 137 + exit 1 138 + fi 139 + 140 + if [[ -n $PXE_ROOT ]]; then 141 + if [[ ! -d $PXE_ROOT ]]; then 142 + echo "$PROG: pxe root '$PXE_ROOT' is not a directory" >&2 143 + exit 1 144 + fi 145 + # Absolute, because we re-exec through sudo from an unknown cwd. 146 + PXE_ROOT=$(cd "$PXE_ROOT" && pwd -P) 147 + fi 148 + 149 + if [[ -n $HTTP && -z $PXE_ROOT ]]; then 150 + echo "$PROG: --http only makes sense together with --pxe" >&2 151 + exit 1 152 + fi 153 + 154 + # Re-exec as root, preserving arguments. 155 + if [[ $EUID -ne 0 ]]; then 156 + exec sudo -- "$0" \ 157 + ${UPSTREAM:+--upstream "$UPSTREAM"} \ 158 + --subnet "$SUBNET_PREFIX" \ 159 + --range "${DHCP_START_HOST}-${DHCP_END_HOST}" \ 160 + --lease "$LEASE_TIME" \ 161 + ${DNS_SERVER:+--dns "$DNS_SERVER"} \ 162 + --backend "$BACKEND" \ 163 + ${PXE_ROOT:+--pxe "$PXE_ROOT"} \ 164 + ${HTTP:+--http --http-port "$HTTP_PORT"} \ 165 + "$IFACE" 166 + fi 167 + 168 + GATEWAY="${SUBNET_PREFIX}.1" 169 + DHCP_START="${SUBNET_PREFIX}.${DHCP_START_HOST}" 170 + DHCP_END="${SUBNET_PREFIX}.${DHCP_END_HOST}" 171 + 172 + # Autodetect the upstream interface from the default route. 173 + if [[ -z $UPSTREAM ]]; then 174 + UPSTREAM=$(ip -4 route show default | awk '/default/ {print $5; exit}') 175 + if [[ -z $UPSTREAM ]]; then 176 + echo "$PROG: could not detect upstream interface; pass --upstream <iface>" >&2 177 + exit 1 178 + fi 179 + fi 180 + 181 + if [[ $UPSTREAM == "$IFACE" ]]; then 182 + echo "$PROG: upstream interface must differ from the served interface" >&2 183 + exit 1 184 + fi 185 + 186 + # --------------------------------------------------------------------------- 187 + # Firewall / NAT backend. 188 + # 189 + # iptables backend: INPUT/FORWARD are single chains, so inserting ACCEPT rules 190 + # at the top preempts the firewall's reject. 191 + # 192 + # nftables backend: an ACCEPT in a separate base chain does NOT override a DROP 193 + # in "inet nixos-fw" (only DROP is terminal across chains). So DHCP/DNS accepts 194 + # must be inserted directly into the firewall's own chains, and NAT lives in a 195 + # private table we can drop wholesale. 196 + # --------------------------------------------------------------------------- 197 + if [[ $BACKEND == auto ]]; then 198 + if command -v nft >/dev/null 2>&1 && nft list table inet nixos-fw >/dev/null 2>&1; then 199 + BACKEND=nft 200 + else 201 + BACKEND=iptables 202 + fi 203 + fi 204 + 205 + # Track what we changed so cleanup only reverts our own state. 206 + NM_UNMANAGED=false 207 + IP_ADDED=false 208 + FORWARD_WAS="" 209 + IPT_NAT_ADDED=false 210 + IPT_FWD_IN_ADDED=false 211 + IPT_FWD_OUT_ADDED=false 212 + IPT_INPUT_OPENED=false 213 + PXE_PID="" 214 + DNSMASQ_PID="" 215 + NFT_NAT_TABLE=false 216 + FW_HANDLES=() # "chain handle" pairs inserted into inet nixos-fw 217 + 218 + # Name of the "inet nixos-fw" base chain registered to the given hook, if any. 219 + nft_base_chain() { 220 + nft list table inet nixos-fw 2>/dev/null | awk -v h="hook $1 " ' 221 + /chain [A-Za-z0-9_.-]+ \{/ { name = $2 } 222 + index($0, h) { print name; exit } 223 + ' 224 + } 225 + 226 + # Insert a rule at the top of an "inet nixos-fw" chain, remembering its handle. 227 + fw_insert() { 228 + local chain=$1 229 + shift 230 + local out handle 231 + out=$(nft --handle --echo insert rule inet nixos-fw "$chain" "$@" 2>/dev/null) || return 1 232 + handle=$(printf '%s\n' "$out" | grep -oE 'handle [0-9]+' | grep -oE '[0-9]+' | tail -n1) 233 + [[ -n $handle ]] && FW_HANDLES+=("$chain $handle") 234 + } 235 + 236 + cleanup() { 237 + set +e 238 + echo "" 239 + echo "$PROG: restoring..." 240 + 241 + # dnsmasq ignores SIGINT outside debug mode, so stop it with SIGTERM. 242 + if [[ -n $DNSMASQ_PID ]]; then 243 + kill "$DNSMASQ_PID" 2>/dev/null 244 + wait "$DNSMASQ_PID" 2>/dev/null 245 + fi 246 + 247 + if [[ -n $PXE_PID ]]; then 248 + kill "$PXE_PID" 2>/dev/null 249 + wait "$PXE_PID" 2>/dev/null 250 + fi 251 + 252 + if [[ $BACKEND == iptables ]]; then 253 + $IPT_NAT_ADDED && iptables -t nat -D POSTROUTING -o "$UPSTREAM" -j MASQUERADE 2>/dev/null 254 + $IPT_FWD_IN_ADDED && iptables -D FORWARD -i "$UPSTREAM" -o "$IFACE" \ 255 + -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null 256 + $IPT_FWD_OUT_ADDED && iptables -D FORWARD -i "$IFACE" -o "$UPSTREAM" -j ACCEPT 2>/dev/null 257 + if $IPT_INPUT_OPENED; then 258 + iptables -D INPUT -i "$IFACE" -p udp --dport 67 -j ACCEPT 2>/dev/null 259 + iptables -D INPUT -i "$IFACE" -p udp --dport 53 -j ACCEPT 2>/dev/null 260 + iptables -D INPUT -i "$IFACE" -p tcp --dport 53 -j ACCEPT 2>/dev/null 261 + fi 262 + 263 + else 264 + local entry 265 + for entry in "${FW_HANDLES[@]}"; do 266 + # shellcheck disable=SC2086 267 + set -- $entry 268 + nft delete rule inet nixos-fw "$1" handle "$2" 2>/dev/null 269 + done 270 + $NFT_NAT_TABLE && nft delete table ip nat_share 2>/dev/null 271 + fi 272 + 273 + if [[ -n $FORWARD_WAS ]]; then 274 + echo "$FORWARD_WAS" >/proc/sys/net/ipv4/ip_forward 2>/dev/null 275 + fi 276 + 277 + $IP_ADDED && ip addr del "${GATEWAY}/${NETMASK}" dev "$IFACE" 2>/dev/null 278 + 279 + if $NM_UNMANAGED && command -v nmcli >/dev/null 2>&1; then 280 + nmcli device set "$IFACE" managed yes 2>/dev/null 281 + fi 282 + 283 + echo "$PROG: done" 284 + } 285 + # Signals only trigger a normal exit; cleanup runs exactly once via EXIT. 286 + # (Ctrl+C reaches dnsmasq too, but it ignores SIGINT; bash's `wait` below is 287 + # interruptible, so the INT trap fires immediately and cleanup SIGTERMs it.) 288 + trap cleanup EXIT 289 + trap 'exit 130' INT 290 + trap 'exit 143' TERM 291 + 292 + # Hand the interface off from NetworkManager, if present. 293 + if command -v nmcli >/dev/null 2>&1 && nmcli -t -f RUNNING general >/dev/null 2>&1; then 294 + echo "$PROG: unmanaging $IFACE from NetworkManager" 295 + nmcli device set "$IFACE" managed no || true 296 + NM_UNMANAGED=true 297 + fi 298 + 299 + # Bring up the link with the gateway address. 300 + echo "$PROG: configuring ${IFACE} as ${GATEWAY}/${NETMASK}" 301 + ip link set "$IFACE" up 302 + ip addr flush dev "$IFACE" 303 + ip addr add "${GATEWAY}/${NETMASK}" dev "$IFACE" 304 + IP_ADDED=true 305 + 306 + # Enable IPv4 forwarding. 307 + FORWARD_WAS=$(cat /proc/sys/net/ipv4/ip_forward) 308 + echo 1 >/proc/sys/net/ipv4/ip_forward 309 + 310 + # NAT + firewall openings. 311 + echo "$PROG: enabling NAT via ${UPSTREAM} (${BACKEND} backend)" 312 + if [[ $BACKEND == iptables ]]; then 313 + iptables -t nat -A POSTROUTING -o "$UPSTREAM" -j MASQUERADE 314 + IPT_NAT_ADDED=true 315 + # Insert at the top of FORWARD so a DROP policy / reject can't shadow us. 316 + iptables -I FORWARD -i "$UPSTREAM" -o "$IFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 317 + IPT_FWD_IN_ADDED=true 318 + iptables -I FORWARD -i "$IFACE" -o "$UPSTREAM" -j ACCEPT 319 + IPT_FWD_OUT_ADDED=true 320 + # Let clients reach our DHCP (67) and DNS (53) despite the host firewall. 321 + iptables -I INPUT -i "$IFACE" -p udp --dport 67 -j ACCEPT 322 + iptables -I INPUT -i "$IFACE" -p udp --dport 53 -j ACCEPT 323 + iptables -I INPUT -i "$IFACE" -p tcp --dport 53 -j ACCEPT 324 + IPT_INPUT_OPENED=true 325 + 326 + else 327 + # NAT masquerade in a private table (torn down wholesale on exit). 328 + nft add table ip nat_share 329 + nft add chain ip nat_share postrouting '{ type nat hook postrouting priority 100 ; }' 330 + nft add rule ip nat_share postrouting oifname "$UPSTREAM" masquerade 331 + NFT_NAT_TABLE=true 332 + 333 + # DHCP/DNS accepts must live inside the firewall's own input chain to beat 334 + # its drop; likewise forward accepts if the firewall filters forwarding. 335 + in_chain=$(nft_base_chain input) 336 + fwd_chain=$(nft_base_chain forward) 337 + if [[ -n $in_chain ]]; then 338 + fw_insert "$in_chain" iifname "$IFACE" udp dport '{ 53, 67 }' accept 339 + fw_insert "$in_chain" iifname "$IFACE" tcp dport 53 accept 340 + 341 + fi 342 + if [[ -n $fwd_chain ]]; then 343 + fw_insert "$fwd_chain" iifname "$UPSTREAM" oifname "$IFACE" ct state related,established accept 344 + fw_insert "$fwd_chain" iifname "$IFACE" oifname "$UPSTREAM" accept 345 + fi 346 + fi 347 + 348 + # DHCP + DNS via dnsmasq, bound only to the served interface/address. 349 + DNS_OPTS=() 350 + if [[ -n $DNS_SERVER ]]; then 351 + DNS_OPTS+=(--no-resolv "--server=${DNS_SERVER}") 352 + fi 353 + 354 + # PXE. Only the DHCP half belongs here: nothing but the DHCP server can tell a 355 + # client what to boot. Serving the files (TFTP, and HTTP for payloads too big 356 + # for TFTP) is pxe-share's job — the same tool that can do this on networks we 357 + # don't own, where it answers as a proxy DHCP server instead. Here we already 358 + # own port 67, so it runs with --no-dhcp and just serves, and opens its own 359 + # firewall ports. 360 + # 361 + # Chainloaded in two steps: bare firmware is matched on its client 362 + # architecture and handed an iPXE binary; iPXE itself (which sets DHCP option 363 + # 175) is then handed the boot script. Without that split the firmware would 364 + # be pointed back at iPXE forever. 365 + PXE_OPTS=() 366 + if [[ -n $PXE_ROOT ]]; then 367 + PXE_OPTS+=( 368 + "--dhcp-match=set:ipxe,175" 369 + "--dhcp-match=set:efi64,option:client-arch,7" 370 + "--dhcp-match=set:efi64,option:client-arch,9" 371 + "--dhcp-match=set:efi32,option:client-arch,6" 372 + "--dhcp-boot=tag:ipxe,${PXE_SCRIPT}" 373 + "--dhcp-boot=tag:!ipxe,tag:efi64,ipxe.efi" 374 + "--dhcp-boot=tag:!ipxe,tag:efi32,ipxe32.efi" 375 + "--dhcp-boot=tag:!ipxe,tag:!efi64,tag:!efi32,undionly.kpxe" 376 + ) 377 + pxe-share --no-dhcp \ 378 + ${HTTP:+--http --http-port "$HTTP_PORT"} \ 379 + --script "$PXE_SCRIPT" \ 380 + --backend "$BACKEND" \ 381 + "$PXE_ROOT" "$IFACE" & 382 + PXE_PID=$! 383 + fi 384 + 385 + echo "$PROG: serving DHCP ${DHCP_START}-${DHCP_END} (lease ${LEASE_TIME}) on ${IFACE}" 386 + echo "$PROG: press Ctrl+C to stop" 387 + dnsmasq \ 388 + --keep-in-foreground \ 389 + --log-facility=- \ 390 + --interface="$IFACE" \ 391 + --bind-interfaces \ 392 + --except-interface=lo \ 393 + --listen-address="$GATEWAY" \ 394 + --dhcp-authoritative \ 395 + --dhcp-range="${DHCP_START},${DHCP_END},${DHCP_NETMASK},${LEASE_TIME}" \ 396 + --dhcp-option=option:router,"$GATEWAY" \ 397 + --dhcp-option=option:dns-server,"$GATEWAY" \ 398 + "${DNS_OPTS[@]}" \ 399 + "${PXE_OPTS[@]}" & 400 + DNSMASQ_PID=$! 401 + wait "$DNSMASQ_PID"
+15
tools/nat-share/usage.kdl
··· 1 + bin "nat-share" 2 + about "Start a DHCP + dnsmasq NAT server on an interface, sharing the upstream connection" 3 + 4 + flag "-u --upstream" help="Upstream/WAN interface to NAT through (default: default route)" 5 + flag "-s --subnet" help="/24 subnet prefix for the served link (default: 10.42.0)" 6 + flag "--range" help="DHCP host range within the /24 (default: 10-254)" 7 + flag "--lease" help="DHCP lease time (default: 12h)" 8 + flag "--dns" help="Upstream DNS server to forward to (default: system resolv.conf)" 9 + flag "-h --help" help="Show help message" 10 + flag "-b --backend" help="Firewall backend: auto | iptables | nft (default: auto-detect)" 11 + flag "--pxe <dir>" help="Also boot PXE clients from <dir> (runs pxe-share --no-dhcp to serve it)" 12 + flag "--http" help="Serve the --pxe dir over HTTP too; requires --pxe (netboot kernel/initrd are too big for TFTP)" 13 + flag "--http-port <port>" help="Port for --http (default: 8079)" 14 + 15 + arg "<interface>" help="Interface to serve DHCP/DNS/NAT on"