Wake-On-Lan程序解析

组装好NAS之后,我一直在考虑如何让其实现定时开机与关机,其中后者可以通过cron或者systemd-timer轻松解决,问题是开机,想到的方案主要有2个:

  1. 在机箱内安装一块Arduino/ESP开发板,配合时钟模块和锂电,定时接通PWR。
  2. 依靠网卡唤醒,也就是WOL(Wake-On-Lan),通过路由器实现定时唤醒(OpenWRT)。

对于我个人而言,2个方案都可以实现,但后者显然耗时更少,近期忙着毕业,没法抽出大段时间折腾。在编写LuCI插件过程中遇到了一点麻烦,单纯的cbi无法满足需求,所有这部分内容只能滞后了。

WOL很简单,只需要知道以下几点内容即可:

  1. 采用的是UDP协议
  2. 需要广播
  3. 特定的报文结构

WOL报文的组成为:6 * 0xFF + 16 * MAC(6B),我给出了程序源码可以直接编译运行,其中有一半代码是在解析MAC地址,并且程序参数处理部分存在一个bug,我没有fix。

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <stdint.h>
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <errno.h>  
#include <unistd.h>
  
#define PORT 53
#define MAC_STRLEN 18

static inline int is_hex(char ch) {
    if(ch >= '0' && ch <= '9' ||
       ch >= 'a' && ch <= 'f' ||
       ch >= 'A' && ch <= 'F') {
        return 1;
    }
    return 0;
}

int parse_mac(const char* mac_str, char buf[]) {
    static uint8_t HEX[128] = {
        ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
        ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
        ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15,
        ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15,
    };
    if(strlen(mac_str) != 17) {
        return 0;
    }
    const char* p = mac_str;
    uint8_t i = 0;
    do {
        if(p[2] != ':') {
            return 0;
        }
        buf[i++] = (HEX[p[0]] << 4) + HEX[p[1]];
        p = mac_str + i * 3;
    } while(i < 5);
    if(p[2] != '\0') {
        return 0;
    }
    buf[i] = (HEX[p[0]] << 4) + HEX[p[1]];
    return 1;
}

void mac_to_string(char* buf, char res[]) {
    snprintf(res, MAC_STRLEN, "%02hhX:%02hhX:%02hhX:%02hhX:%02hhX:%02hhX",
        buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
}

int create_socket() {
    int s = socket(AF_INET, SOCK_DGRAM, 0);  
    if (s == -1) {  
        perror("socket");  
        return -1;
    }  
    int optval = 1;  
    int ret = setsockopt(s, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(int));   
    if (ret == -1) {  
        perror("setsockopt");  
        return -1;
    }  
    return s;
}

void init_sockaddr(struct sockaddr_in* addr) {
    memset(addr, 0, sizeof(struct sockaddr_in));  
    addr->sin_family = AF_INET;  
    addr->sin_addr.s_addr = htonl(INADDR_BROADCAST);  
    addr->sin_port = htons(PORT);
}

int parse_args(int argc, char** argv, char* mac) {
    int ch;
    while((ch = getopt(argc, argv, "hm:")) != -1) {
        switch(ch) {
            case 'h':
                fprintf(stdout,
                    "Usage: \n"
                    "  ./wol -h\n"
                    "  ./wol -m XX:XX:XX:XX:XX:XX\n"
                );
                return -1;
            case 'm':
                if(parse_mac(optarg, mac) != 1) {
                    fprintf(stderr, "Invalid MAC!\n");
                    return -1;
                }
                break;
            default:
                fprintf(stderr, "Invalid option!\n");
                return -1;
        }
    }
    return 0;
}

int main(int argc, char **argv) {  
    char mac[6] = {0};
    if(parse_args(argc, argv, mac) == -1) {
        return -1;
    }

    printf("Creating Socket...");
    int s = create_socket();
    if(s == -1) {
        return -1;
    }
    printf("Done\n");

    struct sockaddr_in addr;  
    init_sockaddr(&addr);

    printf("Creating Packet...");
    char pkt[6 + 16 * 6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
    for(uint8_t i = 0; i < 16; ++i) {
        memcpy(pkt + 6 + i * 6, mac, 6);
    }
    printf("Done\n");

    printf("Sending Packet...");
    int len = sendto(s, pkt, sizeof(pkt), 0, (struct sockaddr*)&addr, sizeof(addr));  
    if (len == -1) {  
        perror("sendto");  
        return -1;
    }
    printf("Done\n");

    char mac_str[MAC_STRLEN] = {0};
    mac_to_string(mac, mac_str);
    printf("Wake %s OK!\n", mac_str);  
    return 0;  
}

WOL唤醒的缺点在于,需要网卡支持唤醒功能,例如对于树莓派则是没有效果的。此外也没法确认报文发送后机器是否有被唤醒,除非能够ping MAC。。。

如果BIOS支持,可以设置RTC Alarm Power On,这个时间是以BIOS内的时钟为准的。

说两句: