C++20 Lightweight Wake-on-LAN Client for Embedded MQTT-Free Cloud Integration
This article documents the design and implementation of a minimal, statically linked Wake-on-LAN client written in C++20, targeting resource-constrained embedded devices such as OpenWrt-based routers. Unlike typical implementations relying on heavy runtimes (e.g., Go) or external libraries (e.g., Paho MQTT), this project communicates directly with the Bemfa cloud platform using its raw TCP protocol—eliminating dependencies while achieving sub-1MB binary size after compression.
Design Rationale
The primary constraint was flash storage: many consumer-grade routers offer only 4–8 MB of writable space, often insufficient for Go binaries (>5 MB even after UPX). To meet strict size and portability requirements, the solution avoids:
- Third-party networking or serialization libraries
- Dynamic linking against glibc
- Protocol abstractions (e.g., MQTT brokers or WebSocket layers)
Instead, it leverages only POSIX socket APIs and standard C++20 featrues—including std::jthread, std::stop_token, and structured bindings—to implement robust connection management, timeout-aware I/O, and automatic thread cleanup.
Core Networking Layer
Domain resolution and socket setup use getaddrinfo() for IPv4-only compatibility and portability across musl-based systems:
addrinfo hints{}, *result;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int err = getaddrinfo("bemfa.com", "8344", &hints, &result);
if (err != 0) {
std::cerr << "[ERR] DNS resolution failed: " << gai_strerror(err) << '\n';
return false;
}
int sock_fd = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (sock_fd == -1) {
std::cerr << "[ERR] Socket creation failed\n";
freeaddrinfo(result);
return false;
}
// Apply 5-second I/O timeout
timeval tv{5, 0};
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sock_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
bool connected = connect(sock_fd, result->ai_addr, result->ai_addrlen) == 0;
freeaddrinfo(result); // Critical: prevent memory leak
Thread-Safe Lifecycle Management
Two long-running tasks—message reception and perioidc heartbeat transmission—are delegated to std::jthread instances, which automatically join on destruction and accept cancellation tokens:
void listen_for_commands(const std::stop_token& stopper) {
char buffer[256];
while (!stopper.stop_requested()) {
ssize_t n = recv(sock_fd, buffer, sizeof(buffer) - 1, MSG_DONTWAIT);
if (n > 0) {
buffer[n] = '\0';
handle_incoming_message(std::string_view{buffer, static_cast<size_t>(n)});
} else if (n == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
break; // Permanent error → trigger reconnect
}
std::this_thread::sleep_for(100ms);
}
}
void send_heartbeat(const std::stop_token& stopper) {
const std::string ping = "ping";
while (!stopper.stop_requested()) {
if (send(sock_fd, ping.data(), ping.size(), 0) <= 0) break;
std::this_thread::sleep_for(60s);
}
}
Idiomatic Reconnection Logic
A compact, self-synchronizing loop uses short-circuit evaluation to avoid unnecessary work:
while (running.load()) {
// Only attempt reconnection if socket is invalid
if (sock_fd == -1 && establish_connection()) {
subscribe_to_topic();
listener = std::jthread{listen_for_commands};
pinger = std::jthread{send_heartbeat};
}
std::this_thread::sleep_for(30s);
}
This pattern ensures establish_connection() is invoked exclusively when needed—and never during active operation—reducing race conditions and simplifying state tracking.
Cross-Compilation Strategy
Targeting MIPS32 little-endian (mipsel) with musl libc required static linking to avoid runtime dependency mismatches. The final CMake configuration includes:
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
Binaries were built remotely via CLion’s SSH toolchain integration, using the same GCC/musl cross-compilation suite used for OpenWrt firmware builds—ensuring ABI consistency and eliminating "not found" errors from dynamic loader mismatches.
Size Optimization Results
- Uncompressed ELF: ~1.9 MB
- UPX-compressed: 687 KB
- No external runtime or shared library dependencies