Wake on LAN from WAN using Chromecast

發表於
分類於 心得
This article is LLM-translated by GPT-4o, so the translation may be inaccurate or complete. If you find any mistake, please let me know.

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