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:
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 跑起來就完成了:
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:
#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;
}然後編譯執行:
# kill existing python server with `pkill python`
clang wol.c -o wol -Wall
nohup ./wol &最後再把安裝的 python 刪掉釋放空間:
pkg uninstall python
apt autoremove
apt autoclean