Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

C++20 Lightweight Wake-on-LAN Client for Embedded MQTT-Free Cloud Integration

Tech Nov 3 1

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

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.