远程唤醒 Wake-on-LAN

七月 18, 2025 [network] #wol #network #ethernet #router

远程唤醒 Wake-on-LAN

原理

WOL 通过发送 Magic Packet 唤醒目标计算机。Magic Packet 是一个广播帧,由以下组成:

通过 UDP 端口 7 或 9(默认)发送,也可以是二层帧(EtherType 0x0842)。

关机状态下网卡(NIC)保持低功耗待机,监听 Magic Packet,收到后触发开机。

Linux 被唤醒端配置

# 查看是否支持 WOL
ethtool eth0 | grep -C 3 -i wake
# Supports Wake-on: pumbg  表示支持
# Wake-on: d               d=禁用, g=已开启

# 开启 WOL
ethtool -s eth0 wol g

WOL 选项含义:

含义
d禁用
p物理活动唤醒
u单播消息唤醒
m多播(组播)消息唤醒
b广播消息唤醒
aARP 唤醒
gMagic Packet 唤醒
s带密码的 Magic Packet 唤醒

推荐使用 g(Magic Packet),避免路由器广播误唤醒。

确保重启后自动开启

方法一:添加到 /etc/rc.local

/sbin/ethtool -s eth0 wol g

方法二:创建 systemd 服务:

[Unit]
Description=Enable Wake On Lan

[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool --change enp3s0 wol g

[Install]
WantedBy=basic.target

发送 Magic Packet

# wakeonlan(UDP,无需 root)
apt install wakeonlan
wakeonlan <MAC-address>
wakeonlan -i 192.168.1.255 <MAC>      # 指定广播地址
wakeonlan -i 192.168.1.255 -p 4242 <MAC>  # 指定端口

# etherwake(二层帧,需 root)
apt install etherwake
sudo etherwake <MAC-address>
sudo etherwake -i eno1 <MAC-address>   # 指定接口

抓包验证

# 监听 Magic Packet(UDP 端口 9)
sudo tcpdump -vvnn -i eno1 'ether dst host ff:ff:ff:ff:ff:ff and udp port 9'

# 监听二层帧 WOL
sudo tcpdump -vvnn -i eno1 'ether[12:2] == 0x0842'

# Wireshark 过滤
udp.port == 9 or udp.port == 7 or eth.type == 0x0842

WOL 报文类型总结

工具以太网地址IP 层地址协议
wakeonlan单播单播/网段广播/255.255.255.255UDP
etherwake单播/广播无(纯二层)EtherType 0x0842
Windows WOL App广播网段广播 + 255.255.255.255UDP

公网唤醒

必要条件

  1. 公网 IPDDNS
  2. 路由器端口转发:将外网端口映射到目标机器的 IP:9(UDP)
  3. ARP 绑定:路由器中绑定目标机器的 MAC-IP,防止关机后 ARP 表丢失

常见失败原因

  1. 路由器端口转发不支持映射为广播地址(如华为路由只支持已获取 IP 的设备)
  2. 路由器在设备关机后检测到 MAC 下线,ARP 绑定失效,丢包
  3. 源地址经 DNAT(端口转发)后变为公网 IP,某些网卡/NIC 对单播 Magic Packet 的处理不一致

绕行方案

通过内网一台始终在线的机器(如 NAS)作为跳板,由 NAS 发送 wakeonlan 命令唤醒目标机器。

WOL 报文发送方式对比

方式优点缺点
单播安全、低带宽IP 变化后失效
子网广播兼容性好、成功率高需要路由器支持;仅 IPv4;安全性低

其他远程开机方案

  1. 智能插座 + BIOS "通电自动开机":最简单,适用于任何机器
  2. PCI-E 开机卡:通过 WiFi 发送开机信号,需 PCIE 插槽带电
  3. 短接 POWER SW 跳线:通过外部设备短接主板的电源跳线

示例 Python 脚本

import socket

def wol(mac: bytes):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    magic = b'\xff' * 6 + mac * 16
    s.sendto(magic, ('<broadcast>', 7))

wol(b'\x00\x15\xB2\xAA\x5B\x00')