182 lines
4.7 KiB
Bash
Executable file
182 lines
4.7 KiB
Bash
Executable file
#!/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="<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
|