#!/usr/bin/env bash set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${root}" netboot_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}" http_dir="${netboot_root}/http" tftp_dir="${netboot_root}/tftp" listen_ip="${EVERY_CHANNEL_NETBOOT_LISTEN_IP:-}" interface_name="${EVERY_CHANNEL_NETBOOT_INTERFACE:-}" proxy_subnet="${EVERY_CHANNEL_NETBOOT_PROXY_SUBNET:-}" netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-}" http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}" dnsmasq_port="${EVERY_CHANNEL_NETBOOT_DNS_PORT:-0}" proxy_dhcp="${EVERY_CHANNEL_NETBOOT_PROXY_DHCP:-true}" tftp_boot_filename="${EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME:-ipxe.efi}" http_allowed_cidrs="${EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS:-}" chain_token="${EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN:-}" need_cmd() { local name="$1" if ! command -v "${name}" >/dev/null 2>&1; then echo "error: required command not found: ${name}" >&2 exit 2 fi } need_cmd dnsmasq need_cmd python3 bool_norm() { local raw raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" case "${raw}" in ''|true|1|yes|y|on) echo "true" ;; false|0|no|n|off) echo "false" ;; *) echo "error: invalid boolean value '${1}'" >&2 exit 2 ;; esac } trim_ws() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "${value}" } validate_chain_token() { local value="$1" if [[ -z "${value}" ]]; then return 0 fi if [[ ! "${value}" =~ ^[A-Za-z0-9._~-]{16,128}$ ]]; then echo "error: EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN must match [A-Za-z0-9._~-]{16,128}" >&2 exit 2 fi } if [[ "$(id -u)" -ne 0 ]]; then echo "error: netboot-serve requires root (TFTP + ProxyDHCP ports)." >&2 echo "hint: run with sudo and pass env vars. Example (UniFi-only):" >&2 echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel EVERY_CHANNEL_NETBOOT_PROXY_DHCP=false EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME=ec-ipxe.efi ./scripts/netboot-serve.sh" >&2 echo "hint: Example (ProxyDHCP):" >&2 echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel EVERY_CHANNEL_NETBOOT_PROXY_DHCP=true ./scripts/netboot-serve.sh" >&2 exit 2 fi if [[ -z "${listen_ip}" ]]; then echo "error: set EVERY_CHANNEL_NETBOOT_LISTEN_IP (boot server IP on NUC VLAN)" >&2 exit 2 fi if [[ -z "${interface_name}" ]]; then echo "error: set EVERY_CHANNEL_NETBOOT_INTERFACE (interface on NUC VLAN)" >&2 exit 2 fi if [[ -z "${netboot_hostname}" ]]; then netboot_hostname="${listen_ip}" fi proxy_dhcp="$(bool_norm "${proxy_dhcp}")" validate_chain_token "${chain_token}" if [[ "${proxy_dhcp}" == "true" && -z "${proxy_subnet}" ]]; then echo "error: set EVERY_CHANNEL_NETBOOT_PROXY_SUBNET (for example 10.20.30.0/24) when proxy mode is enabled" >&2 exit 2 fi if [[ -z "${http_allowed_cidrs}" && "${proxy_dhcp}" == "true" ]]; then http_allowed_cidrs="${proxy_subnet}" fi netboot_chain_url="http://${netboot_hostname}:${http_port}/netboot.ipxe" if [[ -n "${chain_token}" ]]; then netboot_chain_url="${netboot_chain_url}?token=${chain_token}" fi for required in "${http_dir}/kernel" "${http_dir}/initrd" "${http_dir}/netboot.ipxe" "${tftp_dir}/${tftp_boot_filename}"; do if [[ ! -f "${required}" ]]; then echo "error: missing required staged file: ${required}" >&2 echo "hint: run ./scripts/netboot-stage.sh first" >&2 exit 2 fi done run_dir="$(mktemp -d)" cleanup() { if [[ -n "${http_pid:-}" ]] && kill -0 "${http_pid}" >/dev/null 2>&1; then kill "${http_pid}" >/dev/null 2>&1 || true wait "${http_pid}" 2>/dev/null || true fi rm -rf "${run_dir}" } trap cleanup EXIT INT TERM cat > "${run_dir}/dnsmasq.conf" <> "${run_dir}/dnsmasq.conf" <&2 exit 2 fi http_args=(python3 "${http_server_script}" --bind-ip "${listen_ip}" --port "${http_port}" --root "${http_dir}") if [[ -n "${chain_token}" ]]; then http_args+=(--netboot-token "${chain_token}") fi if [[ -n "${http_allowed_cidrs}" ]]; then IFS=',' read -r -a cidr_raw <<< "${http_allowed_cidrs}" for raw in "${cidr_raw[@]}"; do cidr="$(trim_ws "${raw}")" if [[ -n "${cidr}" ]]; then http_args+=(--allow-cidr "${cidr}") fi done fi http_log="${run_dir}/http.log" "${http_args[@]}" >"${http_log}" 2>&1 & http_pid="$!" sleep 0.2 if ! kill -0 "${http_pid}" >/dev/null 2>&1; then echo "error: HTTP server failed to start; see ${http_log}" >&2 cat "${http_log}" >&2 || true exit 2 fi echo "ok: HTTP serving ${http_dir} on http://${listen_ip}:${http_port}/" echo "ok: advertised netboot host: ${netboot_hostname}" echo "ok: TFTP serving ${tftp_dir} on ${listen_ip}:69" echo "ok: TFTP boot filename: ${tftp_boot_filename}" if [[ -n "${chain_token}" ]]; then echo "ok: chain token enabled" fi if [[ -n "${http_allowed_cidrs}" ]]; then echo "ok: HTTP allowed CIDRs: ${http_allowed_cidrs}" else echo "warning: HTTP CIDR allowlist is disabled; set EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS to lock this down" fi if [[ "${proxy_dhcp}" == "true" ]]; then echo "ok: ProxyDHCP active for ${proxy_subnet} on interface ${interface_name}" echo "ok: Use normal Unifi DHCP for IP assignment; do not configure Unifi DHCP bootfile while proxy mode is active." else echo "ok: ProxyDHCP disabled." echo "ok: Configure UniFi DHCP option 66=${netboot_hostname}, option 67=${tftp_boot_filename}" fi echo "ok: chain URL: ${netboot_chain_url}" echo echo "Press Ctrl+C to stop." dnsmasq --no-daemon --conf-file="${run_dir}/dnsmasq.conf"