Wake on LAN from WAN using Chromecast
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