This repository has no description
0

Configure Feed

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

tools.pxe-share: init proxy-DHCP PXE boot server

+316
+21
tools/pxe-share/flake-module.nix
··· 1 + { ... }: 2 + { 3 + perSystem = 4 + { pkgs, ... }: 5 + { 6 + packages.pxe-share = 7 + (pkgs.writeShellApplication { 8 + name = "pxe-share"; 9 + runtimeInputs = [ 10 + pkgs.dnsmasq 11 + pkgs.iptables 12 + pkgs.nftables 13 + pkgs.iproute2 14 + pkgs.gawk 15 + pkgs.darkhttpd 16 + ]; 17 + text = builtins.readFile ./pxe-share.sh; 18 + }).overrideAttrs 19 + { passthru.usage = builtins.readFile ./usage.kdl; }; 20 + }; 21 + }
+283
tools/pxe-share/pxe-share.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + PROG="pxe-share" 5 + 6 + # Defaults 7 + MODE="proxy" 8 + HTTP_WANTED="" 9 + HTTP_ROOT="" 10 + HTTP_PORT="8079" 11 + PXE_SCRIPT="netboot.ipxe" 12 + BACKEND="auto" 13 + IFACE="" 14 + ROOT="" 15 + 16 + usage() { 17 + cat >&2 <<EOF 18 + Usage: $PROG [OPTIONS] <dir> <interface> 19 + 20 + Serve PXE network boot from <dir> on <interface>. 21 + 22 + By default this is a *proxy* DHCP server: it answers only the PXE boot 23 + questions (which boot file to fetch) and never hands out addresses or DNS, so 24 + it can run on a network whose DHCP server belongs to somebody else — a router, 25 + an office LAN — without fighting it. The existing server keeps assigning IPs. 26 + 27 + Nothing about the interface is reconfigured; only firewall rules are added, 28 + and they are removed again on exit. 29 + 30 + Runs in the foreground. Press Ctrl+C to stop and restore. 31 + 32 + Options: 33 + --no-dhcp Do not answer DHCP at all; only serve the files 34 + (TFTP, and HTTP with --http). Use when another DHCP 35 + server on the link already points clients at us, 36 + e.g. \`nat-share --pxe\`, which owns port 67 itself. 37 + --http Also serve <dir> over HTTP, on --http-port. Needed 38 + for netboot payloads: a kernel+initrd carrying a 39 + nix store is far too big for TFTP. 40 + --http-port <port> Port for --http (default: ${HTTP_PORT}) 41 + --script <name> iPXE script handed to iPXE (default: ${PXE_SCRIPT}) 42 + -b, --backend <name> Firewall backend: auto | iptables | nft 43 + (default: auto-detect) 44 + -h, --help Show this help 45 + 46 + <dir> should hold ipxe.efi (UEFI x64), optionally ipxe32.efi / undionly.kpxe 47 + for other client architectures, and ${PXE_SCRIPT}. 48 + EOF 49 + } 50 + 51 + while [[ $# -gt 0 ]]; do 52 + case $1 in 53 + --no-dhcp) 54 + MODE="serve-only" 55 + shift 56 + ;; 57 + --http) 58 + # A plain flag on purpose: an optional argument here is ambiguous next 59 + # to the positional <dir> <interface> and silently eats the interface. 60 + HTTP_WANTED=1 61 + shift 62 + ;; 63 + --http-port) 64 + HTTP_PORT="$2" 65 + shift 2 66 + ;; 67 + --script) 68 + PXE_SCRIPT="$2" 69 + shift 2 70 + ;; 71 + -b | --backend) 72 + BACKEND="$2" 73 + shift 2 74 + ;; 75 + -h | --help) 76 + usage 77 + exit 0 78 + ;; 79 + -*) 80 + echo "$PROG: unknown option: $1" >&2 81 + usage 82 + exit 1 83 + ;; 84 + *) 85 + if [[ -z $ROOT ]]; then 86 + ROOT="$1" 87 + elif [[ -z $IFACE ]]; then 88 + IFACE="$1" 89 + else 90 + echo "$PROG: unexpected argument: $1" >&2 91 + usage 92 + exit 1 93 + fi 94 + shift 95 + ;; 96 + esac 97 + done 98 + 99 + if [[ -z $ROOT || -z $IFACE ]]; then 100 + echo "$PROG: need both <dir> and <interface>" >&2 101 + usage 102 + exit 1 103 + fi 104 + 105 + case $BACKEND in 106 + auto | iptables | nft) ;; 107 + *) 108 + echo "$PROG: invalid backend '$BACKEND' (expected auto|iptables|nft)" >&2 109 + exit 1 110 + ;; 111 + esac 112 + 113 + if [[ ! -d $ROOT ]]; then 114 + echo "$PROG: '$ROOT' is not a directory" >&2 115 + exit 1 116 + fi 117 + # Absolute, because we re-exec through sudo from an unknown cwd. 118 + ROOT=$(cd "$ROOT" && pwd -P) 119 + 120 + if [[ -n $HTTP_WANTED ]]; then 121 + HTTP_ROOT="$ROOT" 122 + fi 123 + 124 + if ! ip link show "$IFACE" >/dev/null 2>&1; then 125 + echo "$PROG: interface '$IFACE' does not exist" >&2 126 + exit 1 127 + fi 128 + 129 + # Re-exec as root, preserving arguments. 130 + if [[ $EUID -ne 0 ]]; then 131 + reexec=() 132 + if [[ $MODE == serve-only ]]; then 133 + reexec+=(--no-dhcp) 134 + fi 135 + if [[ -n $HTTP_WANTED ]]; then 136 + reexec+=(--http --http-port "$HTTP_PORT") 137 + fi 138 + reexec+=(--script "$PXE_SCRIPT" --backend "$BACKEND" "$ROOT" "$IFACE") 139 + exec sudo -- "$0" "${reexec[@]}" 140 + fi 141 + 142 + if [[ $BACKEND == auto ]]; then 143 + if command -v nft >/dev/null 2>&1 && nft list table inet nixos-fw >/dev/null 2>&1; then 144 + BACKEND=nft 145 + else 146 + BACKEND=iptables 147 + fi 148 + fi 149 + 150 + # Proxy-DHCP has to answer from the address the clients can reach us on, and 151 + # dnsmasq wants that subnet in --dhcp-range to scope the proxy offer. 152 + ADDR="" 153 + if [[ $MODE == proxy ]]; then 154 + ADDR=$(ip -4 -brief addr show dev "$IFACE" | awk '{print $3}' | cut -d/ -f1 | head -n1) 155 + if [[ -z $ADDR ]]; then 156 + echo "$PROG: $IFACE has no IPv4 address; proxy DHCP needs one (or use --no-dhcp)" >&2 157 + exit 1 158 + fi 159 + fi 160 + 161 + # Track what we changed so cleanup only reverts our own state. 162 + IPT_RULES=() 163 + FW_HANDLES=() # "chain handle" pairs inserted into inet nixos-fw 164 + HTTP_PID="" 165 + 166 + # Name of the "inet nixos-fw" base chain registered to the given hook, if any. 167 + nft_base_chain() { 168 + nft list table inet nixos-fw 2>/dev/null | awk -v h="hook $1 " ' 169 + /chain [A-Za-z0-9_.-]+ \{/ { name = $2 } 170 + index($0, h) { print name; exit } 171 + ' 172 + } 173 + 174 + # Insert a rule at the top of an "inet nixos-fw" chain, remembering its handle. 175 + fw_insert() { 176 + local chain=$1 177 + shift 178 + local out handle 179 + out=$(nft --handle --echo insert rule inet nixos-fw "$chain" "$@" 2>/dev/null) || return 1 180 + handle=$(printf '%s\n' "$out" | grep -oE 'handle [0-9]+' | grep -oE '[0-9]+' | tail -n1) 181 + [[ -n $handle ]] && FW_HANDLES+=("$chain $handle") 182 + } 183 + 184 + # Open a port on the served interface in whichever backend is active. 185 + open_port() { 186 + local proto=$1 port=$2 187 + if [[ $BACKEND == iptables ]]; then 188 + iptables -I INPUT -i "$IFACE" -p "$proto" --dport "$port" -j ACCEPT 189 + IPT_RULES+=("$proto $port") 190 + else 191 + local chain 192 + chain=$(nft_base_chain input) 193 + [[ -n $chain ]] && fw_insert "$chain" iifname "$IFACE" "$proto" dport "$port" accept 194 + fi 195 + } 196 + 197 + cleanup() { 198 + set +e 199 + echo "" 200 + echo "$PROG: restoring..." 201 + 202 + if [[ -n $HTTP_PID ]]; then 203 + kill "$HTTP_PID" 2>/dev/null 204 + wait "$HTTP_PID" 2>/dev/null 205 + fi 206 + 207 + local entry 208 + for entry in "${IPT_RULES[@]}"; do 209 + # shellcheck disable=SC2086 210 + set -- $entry 211 + iptables -D INPUT -i "$IFACE" -p "$1" --dport "$2" -j ACCEPT 2>/dev/null 212 + done 213 + for entry in "${FW_HANDLES[@]}"; do 214 + # shellcheck disable=SC2086 215 + set -- $entry 216 + nft delete rule inet nixos-fw "$1" handle "$2" 2>/dev/null 217 + done 218 + 219 + echo "$PROG: done" 220 + } 221 + trap cleanup EXIT INT TERM 222 + 223 + # TFTP is always ours. Only the initial request hits UDP/69; dnsmasq then 224 + # transfers from an ephemeral port, whose return traffic conntrack already 225 + # accepts as ESTABLISHED. 226 + open_port udp 69 227 + if [[ $MODE == proxy ]]; then 228 + # 67 for the proxy offer, 4011 for the PXE client's follow-up request. 229 + open_port udp 67 230 + open_port udp 4011 231 + fi 232 + if [[ -n $HTTP_ROOT ]]; then 233 + open_port tcp "$HTTP_PORT" 234 + fi 235 + 236 + DNSMASQ_OPTS=( 237 + --keep-in-foreground 238 + --log-facility=- 239 + --interface="$IFACE" 240 + --bind-interfaces 241 + --except-interface=lo 242 + # No DNS: somebody else's network already has one. 243 + --port=0 244 + --enable-tftp 245 + "--tftp-root=${ROOT}" 246 + ) 247 + 248 + if [[ $MODE == proxy ]]; then 249 + # Chainloaded in two steps: bare firmware is matched on its client 250 + # architecture and handed an iPXE binary; iPXE itself (which sets DHCP 251 + # option 175) is then handed the boot script. Without that split the 252 + # firmware would be pointed back at iPXE forever. 253 + # 254 + # ",proxy" is what makes this coexist with the network's real DHCP server: 255 + # dnsmasq replies with boot information only, never with a lease. 256 + DNSMASQ_OPTS+=( 257 + "--dhcp-range=${ADDR},proxy" 258 + "--dhcp-match=set:ipxe,175" 259 + "--dhcp-match=set:efi64,option:client-arch,7" 260 + "--dhcp-match=set:efi64,option:client-arch,9" 261 + "--dhcp-match=set:efi32,option:client-arch,6" 262 + "--pxe-service=tag:!ipxe,x86-64_EFI,Network boot,ipxe.efi" 263 + "--pxe-service=tag:!ipxe,IA32_EFI,Network boot,ipxe32.efi" 264 + "--pxe-service=tag:!ipxe,x86PC,Network boot,undionly.kpxe" 265 + "--dhcp-boot=tag:ipxe,${PXE_SCRIPT}" 266 + ) 267 + echo "$PROG: proxy DHCP on ${IFACE} (${ADDR}) — not assigning addresses" 268 + else 269 + echo "$PROG: serving files only on ${IFACE} (no DHCP)" 270 + fi 271 + 272 + echo "$PROG: serving TFTP from ${ROOT} (boot script ${PXE_SCRIPT})" 273 + 274 + if [[ -n $HTTP_ROOT ]]; then 275 + # darkhttpd follows the symlinks in the served root into /nix/store, so the 276 + # tree can point at the store instead of copying a multi-GB initrd around. 277 + echo "$PROG: serving HTTP from ${HTTP_ROOT} on port ${HTTP_PORT}" 278 + darkhttpd "$HTTP_ROOT" --port "$HTTP_PORT" & 279 + HTTP_PID=$! 280 + fi 281 + 282 + echo "$PROG: press Ctrl+C to stop" 283 + dnsmasq "${DNSMASQ_OPTS[@]}"
+12
tools/pxe-share/usage.kdl
··· 1 + bin "pxe-share" 2 + about "Serve PXE network boot, by default as a proxy DHCP server that coexists with somebody else's DHCP" 3 + 4 + flag "--no-dhcp" help="Don't answer DHCP; only serve the files (use alongside nat-share --pxe)" 5 + flag "--http" help="Also serve <dir> over HTTP (kernel/initrd are too big for TFTP)" 6 + flag "--http-port <port>" help="Port for --http (default: 8079)" 7 + flag "--script <name>" help="iPXE script handed to iPXE (default: netboot.ipxe)" 8 + flag "-b --backend <name>" help="Firewall backend: auto | iptables | nft (default: auto-detect)" 9 + flag "-h --help" help="Show help message" 10 + 11 + arg "<dir>" help="Directory holding ipxe.efi / undionly.kpxe and the iPXE script" 12 + arg "<interface>" help="Interface to serve PXE on"