This commit is contained in:
h@x 2025-12-11 04:27:50 +01:00
commit 82bca58738
5 changed files with 474 additions and 0 deletions

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
Copyright (c) 2025 h@x
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the " Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

208
README.md Normal file
View file

@ -0,0 +1,208 @@
# PVE-VM-WOL
Helper scripts to integrate **Wake-on-LAN** with **Proxmox VE VMs** and to manage / inspect VM MAC addresses from the host.
This repository currently provides:
- `pve-list-mac-addresses.sh` — list all VM NICs and MAC addresses on a PVE node
- (optional) `pve-wol-server.sh` — listen for WOL packets on a bridge and start matching VMs automatically
---
## Overview
Typical use case:
- You want to **wake a specific Proxmox VM** from a client (e.g. Moonlight, another PC, NAS, etc.).
- The client sends a **WOL magic packet** to your Proxmox bridge/VLAN.
- Proxmox host runs a small script (`pve-wol-server.sh`) that:
- listens for these packets on an interface (e.g. `vmbr0`, `vmbr1`, `ovs`, etc.),
- extracts the **target MAC**,
- finds a matching VM,
- and **starts** the corresponding VM.
`pve-list-mac-addresses.sh` helps with the annoying part: **figuring out which VM has which MAC on which interface/bridge** directly from PVEs configs.
---
## Requirements
- Proxmox VE node (Debian-based host with `qm` available)
- `bash`
- `awk`, `printf`, standard coreutils
- Root or sufficient privileges to run `qm list` / `qm config`
For `wol_hack.sh` (if used):
- `tcpdump` (or similar packet capture tool)
- Access to the relevant bridge/interface that receives WOL/magic packets
---
## Scripts
### `list-pve-vm-macs.sh`
Lists all **QEMU VMs** on the node and prints:
- VMID
- VM name
- NIC (`net0`, `net1`, …)
- MAC address
- Attached bridge (e.g. `vmbr0`, `vmbr1`, `ovs`, …)
#### Installation
On the Proxmox node:
```bash
cd /root/PVE-VM-WOL # or any directory you prefer
nano pve-list-mac-addresses.sh # paste the script content
chmod +x pve-list-mac-addresses.sh
```
#### Usage
Basic usage:
```bash
./pve-list-mac-addresses.sh
```
Example output:
```text
ID NAME IF MAC BRIDGE
---------------------------------------------------------------------
100 datacenter-vm net0 DE:AD:BE:EF:01:02 vmbr0
101 jellyfin net0 DE:AD:BE:EF:01:03 vmbr1
102 vaultwarden net0 DE:AD:BE:EF:01:04 ovs
```
Only MAC addresses (e.g. for mapping files, WOL tools, etc.):
```bash
./pve-list-mac-addresses.sh | awk 'NR>2 {print $4}'
```
Export to CSV:
```bash
./pve-list-mac-addresses.sh | awk 'NR>2 {print $1","$2","$3","$4","$5}' > vm-macs.csv
```
#### How it works (short version)
- Uses `qm list` to obtain VMIDs.
- For each VMID, queries `qm config <vmid>`:
- reads `name:` for the VM name,
- parses all `netX:` lines,
- splits the NIC options into `key=value` pairs,
- identifies the MAC by pattern (`aa:bb:cc:dd:ee:ff`),
- extracts the `bridge=` field.
The script intentionally uses **very basic `awk` features** so that it works on older Proxmox/Debian versions without GNU-specific extensions.
---
### `wol_hack.sh` (optional listener)
This script is typically used to:
1. Listen on a given interface/bridge (e.g. `vmbr1`) for:
- WOL packets (`ether proto 0x0842`) or
- UDP port 9 (traditional WOL port).
2. Extract the MAC address from the packet.
3. Find a VM with that MAC in its `netX:` config.
4. `qm start <vmid>` for the matching VM.
Typical setup:
```bash
cd /root/PVE-VM-WOL
nano pve-wol-server.sh
chmod +x pve-wol-server.sh
```
Then either run it manually:
```bash
./pve-wol-server.sh
```
Or create a systemd service to run it in the background on boot.
> **Warning:**
> If misused or flooded with packets, this script can lead to a lot of VM start attempts.
> You should add:
> - rate limiting (e.g. sleep between loops),
> - basic logging,
> - and maybe IP/MAC allow-lists.
Use `pve-list-mac-addresses.sh` to verify the VM MACs that `pve-wol-server.sh` should respond to.
---
## Extending the Toolkit
Planned / possible additions:
- **LXC support:**
Parse `/etc/pve/lxc/*.conf` for `hwaddr=XX:XX:XX:XX:XX:XX` and print them in the same table format.
- **Static mapping file generator:**
Export VMID → MAC mapping into a flat file, JSON, or key-value format for other tools.
- **Systemd units:**
Ready-made `systemd` service files for:
- WOL listener (`pve-wakeonlan.service`),
- Periodic MAC export (`list-pve-vm-macs.timer` → writes CSV to a shared location).
---
## Troubleshooting
- **No output / empty table**
- Check that VMs exist:
```bash
qm list
```
- Ensure you run the script **on the Proxmox host**, not inside a guest.
- **Script errors for `qm` or `awk`**
- Make sure `qm` is in `$PATH` (it should be on PVE hosts).
- On non-PVE Debian/Ubuntu, `qm` is not available → this script is Proxmox-specific.
- **Wrong or missing names**
- Names are read from `name:` in `qm config <vmid>`.
If no `name:` is set, the script will display `<no-name>`.
---
## Security Notes
- The MAC listing script itself is read-only (only reads VM configs).
- The WOL listener script potentially **starts VMs based on external packets**:
- Use firewalling/VLANs to limit who can send WOL packets.
- Consider rate limiting and logging.
- Be careful exposing the listening interface directly to untrusted networks.
---
## License
```text
SPDX-License-Identifier: MIT
```
---
## Summary
- `pve-list-mac-addresses.sh` gives you a clean, script-friendly view of **all VM NICs and MACs** directly from Proxmox configs.
- `pve-wol-server.sh` (or a similar listener) can then use this information to **start VMs via WOL** packets hitting your PVE bridges.
Drop these into `/root/PVE-VM-WOL` on your node, wire it into systemd, and your Proxmox starts behaving a lot more like a physical machine with proper Wake-on-LAN support.

56
pve-list-mac-addresses.sh Normal file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# list-pve-vm-macs.sh
# List MAC addresses for all QEMU VMs on this PVE node.
set -Eeuo pipefail
# Header
printf '%-5s %-25s %-5s %-17s %-10s\n' "ID" "NAME" "IF" "MAC" "BRIDGE"
printf '%s\n' "---------------------------------------------------------------------"
# Alle VMIDs holen (erste Zeile ist Header)
qm list | awk 'NR>1 {print $1}' | while read -r vmid; do
# Name aus der VM-Config holen
name="$(qm config "$vmid" | awk -F': ' '/^name:/ {print $2; exit}')"
[ -z "${name:-}" ] && name="<no-name>"
# Netzwerkkarten aus der Config parsen
qm config "$vmid" | awk -v vmid="$vmid" -v name="$name" '
/^net[0-9]+:/ {
# Beispiel:
# net0: virtio=DE:AD:BE:EF:01:02,bridge=vmbr0,firewall=1
net = $1
sub(":", "", net) # "net0:" -> "net0"
line = $0
sub(/^net[0-9]+:[[:space:]]*/, "", line)
mac = ""
bridge = ""
# In Komma-getrennte Optionen splitten
n = split(line, parts, ",")
for (i = 1; i <= n; i++) {
# führende Spaces entfernen
gsub(/^[[:space:]]+/, "", parts[i])
if (index(parts[i], "=") == 0)
continue
split(parts[i], kv, "=")
key = kv[1]
val = kv[2]
# MAC erkennen: 6x 2 Hex-Zeichen mit :
if (val ~ /^[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]$/) {
mac = val
} else if (key == "bridge") {
bridge = val
}
}
printf "%-5s %-25s %-5s %-17s %-10s\n", vmid, name, net, mac, bridge
}
'
done

19
pve-wakeonlan.service Normal file
View file

@ -0,0 +1,19 @@
# Put it in /etc/systemd/system/<projectname>.service
[Unit]
Description=Wake-on-LAN listener for Proxmox VMs/LXCs
After=network-online.target pve-cluster.service
Wants=network-online.target
[Service]
Type=simple
User=root
# Set IFACE here if you don't want vmbr0:
# Environment=IFACE=vmbr1
# or for OVS: Environment=IFACE=ovs
Environment=IFACE=ovs
ExecStart=/root/PVE-VM-WOL/pve-wol-server.sh
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target

182
pve-wol-server.sh Executable file
View file

@ -0,0 +1,182 @@
#!/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