This repository has no description
0

Configure Feed

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

superconfig / tools / pxe-share / pxe-share.sh
8.1 kB 283 lines
1#!/usr/bin/env bash 2set -euo pipefail 3 4PROG="pxe-share" 5 6# Defaults 7MODE="proxy" 8HTTP_WANTED="" 9HTTP_ROOT="" 10HTTP_PORT="8079" 11PXE_SCRIPT="netboot.ipxe" 12BACKEND="auto" 13IFACE="" 14ROOT="" 15 16usage() { 17 cat >&2 <<EOF 18Usage: $PROG [OPTIONS] <dir> <interface> 19 20Serve PXE network boot from <dir> on <interface>. 21 22By default this is a *proxy* DHCP server: it answers only the PXE boot 23questions (which boot file to fetch) and never hands out addresses or DNS, so 24it can run on a network whose DHCP server belongs to somebody else — a router, 25an office LAN — without fighting it. The existing server keeps assigning IPs. 26 27Nothing about the interface is reconfigured; only firewall rules are added, 28and they are removed again on exit. 29 30Runs in the foreground. Press Ctrl+C to stop and restore. 31 32Options: 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 47for other client architectures, and ${PXE_SCRIPT}. 48EOF 49} 50 51while [[ $# -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 97done 98 99if [[ -z $ROOT || -z $IFACE ]]; then 100 echo "$PROG: need both <dir> and <interface>" >&2 101 usage 102 exit 1 103fi 104 105case $BACKEND in 106 auto | iptables | nft) ;; 107 *) 108 echo "$PROG: invalid backend '$BACKEND' (expected auto|iptables|nft)" >&2 109 exit 1 110 ;; 111esac 112 113if [[ ! -d $ROOT ]]; then 114 echo "$PROG: '$ROOT' is not a directory" >&2 115 exit 1 116fi 117# Absolute, because we re-exec through sudo from an unknown cwd. 118ROOT=$(cd "$ROOT" && pwd -P) 119 120if [[ -n $HTTP_WANTED ]]; then 121 HTTP_ROOT="$ROOT" 122fi 123 124if ! ip link show "$IFACE" >/dev/null 2>&1; then 125 echo "$PROG: interface '$IFACE' does not exist" >&2 126 exit 1 127fi 128 129# Re-exec as root, preserving arguments. 130if [[ $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[@]}" 140fi 141 142if [[ $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 148fi 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. 152ADDR="" 153if [[ $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 159fi 160 161# Track what we changed so cleanup only reverts our own state. 162IPT_RULES=() 163FW_HANDLES=() # "chain handle" pairs inserted into inet nixos-fw 164HTTP_PID="" 165 166# Name of the "inet nixos-fw" base chain registered to the given hook, if any. 167nft_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. 175fw_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. 185open_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 197cleanup() { 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} 221trap 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. 226open_port udp 69 227if [[ $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 231fi 232if [[ -n $HTTP_ROOT ]]; then 233 open_port tcp "$HTTP_PORT" 234fi 235 236DNSMASQ_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 248if [[ $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" 268else 269 echo "$PROG: serving files only on ${IFACE} (no DHCP)" 270fi 271 272echo "$PROG: serving TFTP from ${ROOT} (boot script ${PXE_SCRIPT})" 273 274if [[ -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=$! 280fi 281 282echo "$PROG: press Ctrl+C to stop" 283dnsmasq "${DNSMASQ_OPTS[@]}"