Wake on LAN from WAN using Chromecast
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
Recently, I wanted to set up Wake on LAN (WoL) for the PC in my rental place. Unfortunately, the ZyXEL router provided by Chunghwa Telecom does not support WoL. Therefore, I tried to find a way to send a magic packet to my PC from the WAN using the existing devices.
Wake on LAN
Wake on LAN is a feature that allows other devices on the LAN to send a UDP broadcast magic packet to a specified target device's MAC Address, waking up the WoL-supported device from sleep mode. However, this also means it can only be sent from the LAN, as the WAN cannot directly send a UDP broadcast.
Port Forwarding (failed)
One method is to try forwarding UDP port 9 to my PC. This method works when the computer has just entered sleep mode, but it fails after a while. This is because the router's ARP table will delete the IP and MAC Address mapping after some time, so the router cannot forward the magic packet to my PC.
A solution to this is to bind my PC's MAC Address and IP in the router's static ARP table. Unfortunately, my router only supports static DHCP and does not have a static ARP table feature.
Researching online, I found that other devices on the LAN, such as a Raspberry Pi, can help forward the magic packet to achieve WoL. However, I didn't want to buy an additional device just for this purpose, so I thought about whether there were any other devices in my rental place's LAN that I could use.
WoL with Chromecast
After some thought, I remembered I have a Chromecast (4th gen), which is always connected to the network and runs on Android. I wondered if I could run something on it to achieve WoL. To achieve this, I first needed a way to execute code on it.
I used Send files to TV to sideload the Termux apk onto the Chromecast via my phone to see if I could execute commands. To control the keyboard, I used the Google Home app as a keyboard. However, in Termux's shell, although I could input commands, my phone's GBoard treated the input area as single-line input, so the newline key was treated as Done. I switched to Unexpected Keyboard, which allowed me to send newline characters in single-line mode, enabling command execution.
After being able to execute commands, I referred to Termux - Remote Access to start the SSH server, then SSHed into it from my PC and installed Python. Next, I wrote a simple HTTP server in Python to control 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()
After writing it, I SCPed it onto the Chromecast and ran it using nohup:
scp -P 8022 wol.py termux@192.168.x.y:
nohup python3 -u wol.py &
Finally, I port forwarded the desired port on the router to the server's listening port and confirmed that it could be accessed from the WAN, indicating success.
Optimization: Using C
One small issue is that the Chromecast's storage size is only 8GB, and installing Python on Termux takes up over 500MB (including clang, llvm, etc.). Therefore, I had ChatGPT rewrite the above code in 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;
}
Then compiled and executed it:
# kill existing python server with `pkill python`
clang wol.c -o wol -Wall
nohup ./wol &
Finally, I deleted the installed Python to free up space:
pkg uninstall python
apt autoremove
apt autoclean