Proxmox-WOL-Server/pve-wol-server.sh
2025-12-11 04:27:50 +01:00

182 lines
4.7 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 dont 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="<unknown>"
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="<unknown>"
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