[TUTORIAL] Wake on Lan on VMs. :-)

tikilou

New Member
Apr 1, 2025
2
0
1
Hi guys, this is my firt contribution for the community, a script for install/uninstall a service wich gonna run your VM when your wakeonlan the MAC adress of your VM !

This script is fully interactive, under GPLV3 licence, in french, feel free to rewrite/share.

Proxmox Wake-on-LAN VM Listener Installer

This Bash script allows you to associate physical MAC addresses to Proxmox VM IDs and automatically boot those VMs when a Wake-on-LAN (WOL) magic packet is received. It uses `tcpdump` and `awk` to detect the packets and maps them through a configuration file.

Configuration paths:
Code:
CONFIG_FILE="/etc/proxmox-wol-vm.conf"
LISTENER_SCRIPT_PATH="/usr/local/bin/proxmox-wol-listener.sh"
SERVICE_NAME="proxmox-wol-listener.service"
SERVICE_PATH="/etc/systemd/system/proxmox-wol-listener.service"
LOG_TAG="proxmox-wol"

Root check:
Bash:
if [[ $(id -u) -ne 0 ]]; then
  echo "[!] This script must be run as root."
  exec sudo bash "$0" "$@"
  exit 1
fi

⚙️ Listener Script Generator:
Bash:
function regenerate_listener_script() {
  cat << EOF > "$LISTENER_SCRIPT_PATH"
#!/bin/bash

CONFIG_FILE="$CONFIG_FILE"
LOG_TAG="$LOG_TAG"

[[ ! -f "\$CONFIG_FILE" ]] && echo "[\$(date)] [!] Configuration file not found." && exit 1

exec tcpdump -lnXX -i any 'udp port 9' 2>/dev/null | awk -v config_file="\$CONFIG_FILE" -v log_tag="\$LOG_TAG" '
BEGIN {
  rule_count = 0
  while ((getline < config_file) > 0) {
    if (\$0 ~ /^\s*#/ || \$0 ~ /^\s*\$/) continue
    if (split(\$0, parts, /[[:space:]]+/) == 2) {
      mac_orig = parts[1]
      mac_clean = tolower(gensub(":", "", "g", mac_orig))
      vmid = parts[2]
      if (mac_clean ~ /^[0-9a-f]{12}\$/ && vmid ~ /^[0-9]+\$/) {
        patterns[mac_clean] = "ffffffffffff"
        for (i = 1; i <= 16; i++) {
          patterns[mac_clean] = patterns[mac_clean] mac_clean
        }
        vmids[mac_clean] = vmid
        mac_display[mac_clean] = mac_orig
        rule_count++
      }
    }
  }
  close(config_file)
  buffer = ""
}

/^[[:space:]]*0x[0-9a-f]+:/ {
  line = \$0
  sub(/^[^:]*:[[:space:]]*/, "", line)
  sub(/[[:space:]]+[^ ]*\$/, "", line)
  gsub(/[[:space:]]/, "", line)
  buffer = buffer line
}

/^[^[:space:]]/ {
  if (buffer != "") {
    buffer_lower = tolower(buffer)
    for (mac_key in patterns) {
      if (buffer_lower ~ patterns[mac_key]) {
        target_vmid = vmids[mac_key]
        system("qm status " target_vmid " > /dev/null 2>&1")
        if (system("qm status " target_vmid " | grep -q 'status:.*stopped'") == 0) {
          system("qm start " target_vmid)
        }
      }
    }
  }
  buffer = ""
}
'
EOF
  chmod +x "$LISTENER_SCRIPT_PATH"
}

️ Systemd Service Generator:
Bash:
function regenerate_service_file() {
  cat << EOF > "$SERVICE_PATH"
[Unit]
Description=Proxmox VM Wake-on-LAN Listener
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=$LISTENER_SCRIPT_PATH
User=root
Restart=always
RestartSec=5
SyslogIdentifier=$LOG_TAG

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
}

Enable and Start Service:
Bash:
function enable_and_start_service() {
  systemctl enable "$SERVICE_NAME"
  systemctl restart "$SERVICE_NAME"
}

Main Interactive Menu:
Bash:
function main_menu() {
  PS3="Choose an option: "
  select opt in "Install/Update" "Add Rule" "Remove Rule" "View Logs" "Uninstall" "Exit"; do
    case $REPLY in
      1)
        touch "$CONFIG_FILE"
        regenerate_listener_script
        regenerate_service_file
        enable_and_start_service
        ;;
      2)
        read -rp "MAC Address: " mac
        read -rp "VM ID: " vmid
        echo "${mac,,} ${vmid}" >> "$CONFIG_FILE"
        regenerate_listener_script
        systemctl restart "$SERVICE_NAME"
        ;;
      3)
        read -rp "MAC Address to remove: " mac
        sed -i "/^${mac,,} /d" "$CONFIG_FILE"
        regenerate_listener_script
        systemctl restart "$SERVICE_NAME"
        ;;
      4)
        journalctl -u "$SERVICE_NAME" -f --no-pager
        ;;
      5)
        systemctl stop "$SERVICE_NAME"
        systemctl disable "$SERVICE_NAME"
        read -rp "Delete config file too? (y/N): " del
        [[ "$del" =~ ^[yY]$ ]] && rm -f "$CONFIG_FILE"
        rm -f "$LISTENER_SCRIPT_PATH" "$SERVICE_PATH"
        systemctl daemon-reload
        ;;
      6)
        exit 0
        ;;
      *) echo "Invalid choice.";;
    esac
  done
}

Bash:
main_menu

Summary:
  • Listens for UDP port 9 WOL packets
  • Matches target MAC against known VM ID mapping
  • Starts the VM if it's currently stopped
  • Configurable and fully managed via interactive Bash menu
  • Integrated with systemd for auto-start and background operation

To use:
Code:
chmod +x ./install_proxmox_wol_vm.sh
sudo ./install_proxmox_wol_vm.sh




"install_proxmox_wol_vm.sh"
Bash:
#!/bin/bash

# --- Configuration des chemins ---
CONFIG_FILE="/etc/proxmox-wol-vm.conf"
LISTENER_SCRIPT_PATH="/usr/local/bin/proxmox-wol-listener.sh"
SERVICE_NAME="proxmox-wol-listener.service"
SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}"
LOG_TAG="proxmox-wol"

if [[ $(id -u) -ne 0 ]]; then
  echo "[!] Ce script nécessite les droits administrateur (root)."
  echo "    Tentative de ré-exécution avec sudo..."
  exec sudo bash "$0" "$@"
  exit 1
fi

function regenerate_listener_script() {
  echo "[+] Régénération du script d'écoute : ${LISTENER_SCRIPT_PATH}"
  cat << EOF > "${LISTENER_SCRIPT_PATH}"
#!/bin/bash

CONFIG_FILE="${CONFIG_FILE}"
LOG_TAG="${LOG_TAG}"

[[ ! -f "\$CONFIG_FILE" ]] && echo "[\$(date)] [!] Fichier de configuration \$CONFIG_FILE introuvable" && exit 1

exec tcpdump -lnXX -i any 'udp port 9' 2>/dev/null | awk -v config_file="\$CONFIG_FILE" -v log_tag="\$LOG_TAG" '
BEGIN {
  rule_count = 0
  while ((getline < config_file) > 0) {
    if (\$0 ~ /^\s*#/ || \$0 ~ /^\s*$/) continue
    if (split(\$0, parts, /[[:space:]]+/) == 2) {
      mac_orig = parts[1]
      mac_clean = tolower(gensub(":", "", "g", mac_orig))
      vmid = parts[2]
      if (mac_clean ~ /^[0-9a-f]{12}$/ && vmid ~ /^[0-9]+$/) {
        patterns[mac_clean] = "ffffffffffff"
        for (i = 1; i <= 16; i++) {
          patterns[mac_clean] = patterns[mac_clean] mac_clean
        }
        vmids[mac_clean] = vmid
        mac_display[mac_clean] = mac_orig
        rule_count++
      }
    }
  }
  close(config_file)
  buffer = ""
}

/^[[:space:]]*0x[0-9a-f]+:/ {
  line = \$0
  sub(/^[^:]*:[[:space:]]*/, "", line)
  sub(/[[:space:]]+[^ ]*$/, "", line)
  gsub(/[[:space:]]/, "", line)
  buffer = buffer line
}

/^[^[:space:]]/ {
  if (buffer != "") {
    buffer_lower = tolower(buffer)
    for (mac_key in patterns) {
      if (buffer_lower ~ patterns[mac_key]) {
        target_vmid = vmids[mac_key]
        system("qm status " target_vmid " > /dev/null 2>&1")
        if (system("qm status " target_vmid " | grep -q 'status:.*stopped'") == 0) {
          system("qm start " target_vmid)
        }
      }
    }
  }
  buffer = ""
}
'
EOF
  chmod +x "${LISTENER_SCRIPT_PATH}"
}

function regenerate_service_file() {
  cat << EOF > "$SERVICE_PATH"
[Unit]
Description=Proxmox VM Wake-on-LAN Listener
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=$LISTENER_SCRIPT_PATH
User=root
Restart=always
RestartSec=5
SyslogIdentifier=$LOG_TAG

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
}

function enable_and_start_service() {
  systemctl enable "$SERVICE_NAME"
  systemctl restart "$SERVICE_NAME"
}

function main_menu() {
  PS3="Choisissez une option : "
  select opt in "Installer/Mettre à jour" "Ajouter une règle" "Supprimer une règle" "Voir les logs" "Désinstaller" "Quitter"; do
    case $REPLY in
      1)
        touch "$CONFIG_FILE"
        regenerate_listener_script
        regenerate_service_file
        enable_and_start_service
        ;;
      2)
        read -rp "Adresse MAC : " mac
        read -rp "ID de VM : " vmid
        echo "${mac,,} ${vmid}" >> "$CONFIG_FILE"
        regenerate_listener_script
        systemctl restart "$SERVICE_NAME"
        ;;
      3)
        read -rp "Adresse MAC à supprimer : " mac
        sed -i "/^${mac,,} /d" "$CONFIG_FILE"
        regenerate_listener_script
        systemctl restart "$SERVICE_NAME"
        ;;
      4)
        journalctl -u "$SERVICE_NAME" -f --no-pager
        ;;
      5)
        systemctl stop "$SERVICE_NAME"
        systemctl disable "$SERVICE_NAME"
        read -rp "Supprimer aussi le fichier de config ? (o/N) : " del
        [[ "$del" =~ ^[oOyY]$ ]] && rm -f "$CONFIG_FILE"
        rm -f "$LISTENER_SCRIPT_PATH" "$SERVICE_PATH"
        systemctl daemon-reload
        ;;
      6)
        exit 0
        ;;
      *) echo "Choix invalide.";;
    esac
  done
}

main_menu
 
Last edited: