Wake on LAN from WAN using Chromecast

最近想給租屋處的 PC 弄 Wake on LAN (WoL),但是中華電信的小烏龜 (ZyXEL) 很可惜沒有提供 WoL 的功能,所以想辦法透過現有裝置的情況下嘗試達成能從 WAN 對我的 PC send magic packet 的目標。

Wake on LAN

Wake on LAN 是透過 LAN 的其他裝置發送一個 UDP broadcast 的 magic packet,指定目標裝置的 MAC Address,讓支援 WoL 的裝置從睡眠狀態中喚醒。然而它這個也代表它只能從 LAN 發送而已,因為 WAN 沒辦法直接發送 UDP broadcast。

Port Forwarding (failed)

一個方法是嘗試將 UDP 9 port forward 到我的 PC,而這個方法在電腦剛進入睡眠時確實可以 work,但過一段時間之後就會失效。這是因為一段時間後 router 的 ARP table 會把 ip 和 MAC Address 的對應關係刪除,所以 router 沒辦法幫我把 magic packet 轉發到我的 PC。

這有個解決辦法是在 router 上的 static ARP table 綁死我的 PC 的 MAC Address 和 IP,但可惜我的 router 只能設定 static DHCP,沒有 static ARP table 的功能。

而上網查了一些資料可知也能透過 LAN 上的其他裝置,如 Raspberry Pi 之類的來幫忙轉送 magic packet 來達成 WoL 的目標。但我不希望就為了這個去多買一個裝置回來,所以就想想目前租屋處的 LAN 有沒有什麼其他裝置可以利用。

WoL with Chromecast

想了一下就想到我還有個 Chromecast (4th gen),它隨時都連著網路且上面跑的是 Android,因此我就想說如果能在上面跑點東西是不是就能達成 WoL 的目標了呢? 要達成這個目標需要先有辦法在上面執行 code 才行。

我是先用 Send files to TV 透過手機把 Termux apk sideload 到 Chromecast 上,看看能不能執行指令。因為要控制鍵盤所以我是用 Google Home 的 app 當作鍵盤去控制,但是在 Termux 的 shell 中雖然可以輸入指令,但我手機用的 GBoard 這邊都把輸入的地方當作是 single line input,所以換行符號的按鍵都被當成 Done 了。我這邊是改用了 Unexpected Keyboard,它可以讓我在單行模式下送出換行符號,也就可以在上面執行指令了。

能執行指令後就透過參考 Termux - Remote Access 去啟動 ssh server,然後從 PC ssh 進去,然後在上面裝上 python。接下來就用 Python 寫個簡單的 HTTP server 讓我能控制 WoL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import socket, threading

TARGET_MAC = "11:22:33:44:55:66"
LISTEN_PORT = 1234


def wol(mac_address: str):
mac_address_bytes = bytes.fromhex(
mac_address.replace(":", "").replace("-", "").lower()
)
magic_packet = b"\xFF" * 6 + mac_address_bytes * 16
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(magic_packet, ("255.255.255.255", 9))
sock.close()


def http_response(sock, resp, type="text/plain"):
sock.send(
f"HTTP/1.0 200 OK\r\nContent-Length: {len(resp)}\r\nContent-Type: {type}\r\n\r\n".encode()
+ resp
)


def handle(client, address):
print(f"Connection from {address}")
client.settimeout(3)
try:
reqline = client.recv(128).split(b"\r\n")[0]
ar = reqline.split(b" ")
if len(ar) == 3 and ar[1] == b"/wol":
if ar[0] == b"POST":
wol(TARGET_MAC)
http_response(client, b"Magic packet sent")
else:
http_response(
client,
b"<form method=post><button>Wake up</button></form>",
"text/html",
)
else:
http_response(client, b"It works!")
except socket.timeout:
http_response(client, b"Timeout")


def listen():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("", LISTEN_PORT))
server.listen()
while True:
client, address = server.accept()
threading.Thread(target=handle, args=(client, address)).start()


if __name__ == "__main__":
listen()

寫好之後 scp 上去,用 nohup 跑起來就完成了:

1
2
scp -P 8022 wol.py termux@192.168.x.y:
nohup python3 -u wol.py &

最後在 router 上 port forward 想要的 port 到這個 server listen 的 port 上,然後確定能從 WAN 存取就代表成功了。

Optimization: Using C

有個小問題是 Chromecast 的 storage size 只有 8GB,而 Termux 安裝 python 就佔了 500 多 MB (會安裝 clang, llvm ...),因此我就把上面那個讓 ChatGPT 改寫成 C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#include <arpa/inet.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define TARGET_MAC "11:22:33:44:55:66"
#define LISTEN_PORT 48763

void wol(const char *mac_address) {
unsigned char mac_address_bytes[6];
unsigned char magic_packet[102];
struct sockaddr_in addr;
int sock, i;

sscanf(mac_address, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac_address_bytes[0],
&mac_address_bytes[1], &mac_address_bytes[2], &mac_address_bytes[3],
&mac_address_bytes[4], &mac_address_bytes[5]);

memset(magic_packet, 0xFF, 6);
for (i = 1; i <= 16; i++) {
memcpy(magic_packet + i * 6, mac_address_bytes, 6);
}

sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
perror("socket");
return;
}

int broadcast = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

addr.sin_family = AF_INET;
addr.sin_port = htons(9);
addr.sin_addr.s_addr = inet_addr("255.255.255.255");

sendto(sock, magic_packet, sizeof(magic_packet), 0,
(struct sockaddr *)&addr, sizeof(addr));
close(sock);
}

void http_response(int client_sock, const char *resp, const char *type) {
char buffer[256];
size_t resplen = strlen(resp);
size_t buflen = snprintf(
buffer, sizeof(buffer),
"HTTP/1.0 200 OK\r\nContent-Length: %zu\r\nContent-Type: %s\r\n\r\n",
resplen, type);
send(client_sock, buffer, buflen, 0);
send(client_sock, resp, resplen, 0);
}

void *handle(void *arg) {
int client_sock = *(int *)arg;
free(arg);
char buffer[128];
int bytes_received = recv(client_sock, buffer, sizeof(buffer) - 1, 0);

if (bytes_received > 0) {
buffer[bytes_received] = '\0';
char *first_line = strtok(buffer, "\r\n");
char method[8], path[16], version[16];
sscanf(first_line, "%7s %15s %15s", method, path, version);

if (strcmp(path, "/wol") == 0) {
if (strcmp(method, "POST") == 0) {
wol(TARGET_MAC);
http_response(client_sock, "Magic packet sent", "text/plain");
} else {
http_response(
client_sock,
"<form method=post><button>Wake up</button></form>",
"text/html");
}
} else {
http_response(client_sock, "It works!", "text/plain");
}
} else {
http_response(client_sock, "Timeout", "text/plain");
}

return NULL;
}

void listen_for_connections() {
int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock < 0) {
perror("socket");
exit(EXIT_FAILURE);
}

int opt = 1;
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in server_addr = {.sin_family = AF_INET,
.sin_port = htons(LISTEN_PORT),
.sin_addr.s_addr = INADDR_ANY};

if (bind(server_sock, (struct sockaddr *)&server_addr,
sizeof(server_addr)) < 0) {
perror("bind");
close(server_sock);
exit(EXIT_FAILURE);
}

if (listen(server_sock, 10) < 0) {
perror("listen");
close(server_sock);
exit(EXIT_FAILURE);
}

while (1) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_sock =
accept(server_sock, (struct sockaddr *)&client_addr, &addr_len);
if (client_sock < 0) {
perror("accept");
continue;
}

pthread_t thread;
int *pclient = malloc(sizeof(int));
*pclient = client_sock;
pthread_create(&thread, NULL, handle, pclient);
pthread_detach(thread);
}
}

int main() {
listen_for_connections();
return 0;
}

然後編譯執行:

1
2
3
# kill existing python server with `pkill python`
clang wol.c -o wol -Wall
nohup ./wol &

最後再把安裝的 python 刪掉釋放空間:

1
2
3
pkg uninstall python
apt autoremove
apt autoclean