Release
This commit is contained in:
commit
82bca58738
5 changed files with 474 additions and 0 deletions
9
LICENSE
Normal file
9
LICENSE
Normal 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
208
README.md
Normal 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 PVE’s 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
56
pve-list-mac-addresses.sh
Normal 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
19
pve-wakeonlan.service
Normal 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
182
pve-wol-server.sh
Executable 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 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
|
||||
Loading…
Add table
Reference in a new issue