Created
March 5, 2026 12:30
-
-
Save jvarn/c685c6dee6741eb20d379c28f825faf3 to your computer and use it in GitHub Desktop.
Send a Wake-on-LAN (WOL) packet to a target machine via an always-on Linux host
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env zsh | |
| # ----------------------------------------------------------------------------- | |
| # wakeonlan.sh | |
| # | |
| # Send a Wake-on-LAN (WOL) packet to a target machine via an always-on | |
| # Linux host. | |
| # | |
| # Many computers that are powered off cannot receive WOL packets from | |
| # outside their local network, e.g. when using Zerotier or Tailscale | |
| # VPN. This script solves that problem by SSHing into a machine that | |
| # is always running (for example a home server, Raspberry Pi, or NAS | |
| # host) and sending the WOL packet from there. | |
| # | |
| # Behaviour: | |
| # - A target hostname is looked up in an internal host → MAC address map. | |
| # - The script connects via SSH to the configured remote host. | |
| # - The remote host sends the WOL magic packet using the `wakeonlan` tool. | |
| # | |
| # If the requested host is not found in the map, the script prints the list | |
| # of available hosts. | |
| # | |
| # Options: | |
| # --list | |
| # Show all configured hosts and their MAC addresses. | |
| # | |
| # --remote HOST | |
| # Override the default remote machine used to send the WOL packet. | |
| # | |
| # --dry-run | |
| # Print the SSH command that would be executed without running it. | |
| # | |
| # Usage: | |
| # wakeonlan.sh [--remote HOST] [--dry-run] [--list] <target-host> | |
| # | |
| # Examples: | |
| # wakeonlan.sh computer1.local | |
| # wakeonlan.sh --remote linuxhost.local computer2.local | |
| # wakeonlan.sh --dry-run computer3.local | |
| # wakeonlan.sh --list | |
| # | |
| # Requirements: | |
| # - zsh | |
| # - ssh client | |
| # - `wakeonlan` installed on the remote host | |
| # | |
| # Remote host setup example (Ubuntu/Debian): | |
| # | |
| # sudo apt update | |
| # sudo apt install wakeonlan | |
| # | |
| # It is recommended to configure passwordless SSH authentication: | |
| # | |
| # ssh-copy-id linuxhost.local | |
| # | |
| # so the script can run without prompting for a password. | |
| # | |
| # Notes: | |
| # - Hostnames are matched case-insensitively. | |
| # - The remote host must be on the same LAN as the target machines | |
| # for Wake-on-LAN to work. | |
| # | |
| # Installation (optional): | |
| # Copy the script to a directory on your PATH: | |
| # | |
| # cp wakeonlan.sh /usr/local/bin/wakeonlan | |
| # chmod +x /usr/local/bin/wakeonlan | |
| # | |
| # You can then run it from anywhere: | |
| # | |
| # wakeonlan computer1.local | |
| # | |
| # Author: Jeremy Varnham https://github.com/jvarn | |
| # License: MIT | |
| # ----------------------------------------------------------------------------- | |
| set -euo pipefail | |
| # ----------------------------------------------------------------------------- | |
| # Configuration (edit this section) | |
| # ----------------------------------------------------------------------------- | |
| typeset -A HOSTS | |
| HOSTS=( | |
| computer1.local a0:b1:c2:d3:e4:f5 | |
| computer2.local a1:b2:c3:d4:e5:f6 | |
| computer3.local a2:b3:c4:d5:e6:f7 | |
| computer4.local a3:b4:c5:d6:e7:f8 | |
| ) | |
| REMOTE_HOST="linuxhost.local" | |
| DRY_RUN=0 | |
| # ----------------------------------------------------------------------------- | |
| # Functions | |
| # ----------------------------------------------------------------------------- | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| wakeonlan.sh [--remote HOST] [--dry-run] [--list] <target-host> | |
| Examples: | |
| wakeonlan.sh computer1.local | |
| wakeonlan.sh --remote linuxhost.local computer2.local | |
| wakeonlan.sh --dry-run computer3.local | |
| wakeonlan.sh --list | |
| EOF | |
| } | |
| list_hosts() { | |
| echo "Known hosts:" | |
| for k in ${(ok)HOSTS}; do | |
| printf " %-10s %s\n" "$k" "$HOSTS[$k]" | |
| done | sort | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Argument parsing | |
| # ----------------------------------------------------------------------------- | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -h|--help) usage; exit 0 ;; | |
| --list) list_hosts; exit 0 ;; | |
| --dry-run) DRY_RUN=1; shift ;; | |
| --remote) | |
| [[ $# -ge 2 ]] || { echo "Error: --remote requires a host."; usage; exit 2; } | |
| REMOTE_HOST="$2" | |
| shift 2 | |
| ;; | |
| --) shift; break ;; | |
| -*) echo "Error: Unknown option: $1"; usage; exit 2 ;; | |
| *) break ;; | |
| esac | |
| done | |
| # ----------------------------------------------------------------------------- | |
| # Main logic | |
| # ----------------------------------------------------------------------------- | |
| TARGET_HOST_RAW="${1:-}" | |
| [[ -n "$TARGET_HOST_RAW" ]] || { echo "Error: Missing <target-host>."; usage; exit 2; } | |
| TARGET_HOST="${TARGET_HOST_RAW:l}" # lowercase in zsh | |
| MAC="${HOSTS[$TARGET_HOST]:-}" | |
| if [[ -z "$MAC" ]]; then | |
| echo "Error: Host '$TARGET_HOST_RAW' not found." | |
| echo | |
| list_hosts | |
| exit 1 | |
| fi | |
| # Basic MAC sanity check | |
| if ! [[ "$MAC" =~ '^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$' ]]; then | |
| echo "Error: Stored MAC for '$TARGET_HOST' doesn't look valid: '$MAC'" | |
| exit 1 | |
| fi | |
| REMOTE_CMD="command -v wakeonlan >/dev/null 2>&1 && wakeonlan '$MAC'" | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| echo "Dry run:" | |
| echo " ssh \"$REMOTE_HOST\" $REMOTE_CMD" | |
| exit 0 | |
| fi | |
| ssh -o BatchMode=yes "$REMOTE_HOST" "$REMOTE_CMD" \ | |
| || { echo "Error: Failed to send WOL via '$REMOTE_HOST' for '$TARGET_HOST' ($MAC)."; exit 1; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment