#!/usr/bin/env bash # pve-wol-server.sh — Wake-on-LAN → Proxmox VM/LXC starter # # Listens for UDP/9 magic packets on a given interface, extracts the # target MAC from the payload, finds the matching Proxmox guest and # starts it if it is stopped. set -Eeuo pipefail # ===================== Config ===================== # Interface that sees the WOL packets (IP of the node) # Override via environment: IFACE=mgmt1264 ./pve-wol-server.sh IFACE="${IFACE:-proxmox}" QEMU_CFG_DIR="/etc/pve/qemu-server" LXC_CFG_DIR="/etc/pve/lxc" DEBUG="${DEBUG:-1}" # ===================== Helpers ==================== #log() { printf '%s %s\n' "$(date -Is)" "$*" >&1; } #debug() { [[ "$DEBUG" = "1" ]] && log "[DEBUG] $*"; } log() { # send logs to stderr so they don’t corrupt command substitutions printf '%s %s\n' "$(date -Is)" "$*" >&2 } debug() { [[ "$DEBUG" = "1" ]] && log "[DEBUG] $*" } get_wake_mac() { # Capture one UDP/9 packet on $IFACE, dump raw bytes, # locate 6x 0xff (ffffffffffff) and read the next 6 bytes as MAC. tcpdump -i "$IFACE" -c 1 -U -n -w - 'udp port 9' 2>/dev/null \ | hexdump -ve '1/1 "%02x"' \ | awk ' { hex = $0 # 6x ff = 12x "f" pos = index(hex, "ffffffffffff") if (pos == 0) { exit 1 } # Immediately after that comes the 6-byte target MAC (12 hex chars) start = pos + 12 mac = substr(hex, start, 12) printf("%s:%s:%s:%s:%s:%s\n", substr(mac, 1,2), substr(mac, 3,2), substr(mac, 5,2), substr(mac, 7,2), substr(mac, 9,2), substr(mac,11,2)) }' } find_guest_by_mac() { local mac="$1" local -a matches=() debug "Searching configs for MAC $mac" if [[ -d "$QEMU_CFG_DIR" ]]; then mapfile -t matches < <(grep -iRl -- "$mac" "$QEMU_CFG_DIR" 2>/dev/null || true) if (( ${#matches[@]} > 0 )); then [[ ${#matches[@]} -gt 1 ]] && log "Multiple QEMU configs match $mac, using ${matches[0]}" printf 'qemu|%s\n' "${matches[0]}" return 0 fi fi if [[ -d "$LXC_CFG_DIR" ]]; then mapfile -t matches < <(grep -iRl -- "$mac" "$LXC_CFG_DIR" 2>/dev/null || true) if (( ${#matches[@]} > 0 )); then [[ ${#matches[@]} -gt 1 ]] && log "Multiple LXC configs match $mac, using ${matches[0]}" printf 'lxc|%s\n' "${matches[0]}" return 0 fi fi return 1 } start_qemu_vm() { local vm_id="$1" local status name status=$(qm status "$vm_id" --verbose 2>/dev/null | awk -F': *' '/^status:/{print $2; exit}') name=$(qm config "$vm_id" 2>/dev/null | awk -F': *' '/^name:/{print $2; exit}') [[ -z "$name" ]] && name="" if [[ -z "$status" ]]; then log "VM $vm_id: unable to read status (wrong node or missing config?)" return 1 fi log "VM $vm_id ($name) current status: $status" if [[ "$status" != "stopped" ]]; then log "SKIP VM $vm_id ($name): already $status" return 0 fi log "START VM $vm_id ($name)" qm start "$vm_id" } start_lxc_ct() { local ct_id="$1" local status name status=$(pct status "$ct_id" --verbose 2>/dev/null | awk -F': *' '/^status:/{print $2; exit}') name=$(pct config "$ct_id" 2>/dev/null | awk -F': *' '/^hostname:/{print $2; exit}') [[ -z "$name" ]] && name="" if [[ -z "$status" ]]; then log "CT $ct_id ($name) current status: $status" if [[ -z "$status" ]]; then log "CT $ct_id: unable to read status (wrong node or missing config?)" return 1 fi fi if [[ "$status" != "stopped" ]]; then log "SKIP CT $ct_id ($name): already $status" return 0 fi log "START CT $ct_id ($name)" pct start "$ct_id" } handle_mac() { local mac="$1" mac="${mac,,}" log "Captured magic packet for MAC: $mac" local guest_info guest_type cfg_path vm_file vm_id if ! guest_info="$(find_guest_by_mac "$mac")"; then log "No VM/LXC config contains MAC $mac" return 0 fi guest_type="${guest_info%%|*}" cfg_path="${guest_info#*|}" vm_file="$(basename "$cfg_path")" vm_id="${vm_file%%.*}" log "Match: type=$guest_type id=$vm_id config=$cfg_path" case "$guest_type" in qemu) start_qemu_vm "$vm_id" ;; lxc) start_lxc_ct "$vm_id" ;; *) log "Internal error: unknown guest type '$guest_type'" ;; esac } # Test mode: simulate WOL for a given MAC if [[ "${1:-}" == "--test" && -n "${2:-}" ]]; then handle_mac "${2,,}" exit 0 fi log "WOL listener starting on interface $IFACE (UDP/9)" while true; do debug "Waiting for magic packet on $IFACE ..." wake_mac="$(get_wake_mac || true)" if [[ -z "${wake_mac:-}" ]]; then debug "No MAC parsed from packet, retrying…" sleep 1 continue fi handle_mac "$wake_mac" sleep 5 # simple rate limit done