# Technical Design Document **Version**: 1.0.0 **Date**: 2026-06-26 **Status**: Draft ## 1. Executive Summary NexusGuard Embedded Agent is an ESP32-based WireGuard client designed for IoT and edge devices. It provides zero-trust VPN tunneling with auto-provisioning and heartbeat monitoring, connecting to the existing NexusGuard Server Core infrastructure. ## 2. Goals & Non-Goals ### Goals - ESP32-based WireGuard tunnel (LW840X compatible) - Auto-provisioning via HTTP (same API as Go agent) - Heartbeat monitoring with config sync - NVS-based config persistence (encrypted) - FreeRTOS task-based architecture - Low power consumption (WiFi sleep modes) - **Independent build** — no dependency on main NexusGuard repo - **Server discovery** — configurable server URL + registration token ### Non-Goals - Port forwarding (userspace agent only) - gRPC support (too heavy for ESP32) - Zephyr RTOS support (ESP-IDF uses FreeRTOS) - Multi-peer support (single tunnel only) - Kernel WireGuard (lwIP userspace only) ## 3. Architecture ### 3.1 System Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ ESP32 (FreeRTOS) │ ├─────────────────────────────────────────────────────────┤ │ Application Layer │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ provision│ │ heartbeat│ │ tunnel │ │ config │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ ┌────┴─────────────┴────────────┴─────────────┴────┐ │ │ │ crypto.c │ │ │ │ AES-256-GCM decrypt (mbedtls) │ │ │ └──────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ Network Stack (lwIP) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ WireGuard (lwIP socket API) │ │ │ └──────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ Hardware Layer │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ WiFi │ │ NVS │ │ UART │ │ GPIO │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 3.2 Task Architecture ``` FreeRTOS Tasks ├── main_task # Initialization, event loop ├── wifi_task # WiFi STA management (priority: 5) ├── http_task # HTTP client (provisioning, heartbeat) (priority: 4) ├── wg_task # WireGuard tunnel management (priority: 3) └── led_task # Status LED indication (priority: 1) ``` ### 3.3 State Machine ``` ┌─────────────┐ │ BOOT │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ WIFI_INIT │ ◄──────────────────────────────┐ └──────┬──────┘ │ │ connected │ ▼ │ ┌─────────────┐ fail ┌─────────────┐│ │ PROVISION │───────────────►│ WIFI_RETRY ││ └──────┬──────┘ └──────┬──────┘│ │ success │ │ ▼ └───────┘ ┌─────────────┐ │ TUNNEL_UP │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ HEARTBEAT │ ◄─── 30s interval └──────┬──────┘ │ config_changed ▼ ┌─────────────┐ │ TUNNEL_REBUILD │ └──────┬──────┘ │ └──► HEARTBEAT ``` ## 4. Server Discovery & Provisioning ### 4.1 Overview The embedded agent connects to NexusGuard Server Core via HTTP. The agent must be configured with: - **Server URL**: The API endpoint (e.g., `https://api-nexus.datadunia.com`) - **Registration Token**: Single-use token for initial provisioning - **Server Salt**: For AES-256-GCM key derivation (shared secret) ### 4.2 Provisioning Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ PROVISIONING FLOW │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. Device boots │ │ │ │ │ ▼ │ │ 2. Check NVS for saved config │ │ │ │ │ ├─► Config exists ──► Load config ──► Skip to TUNNEL │ │ │ │ │ └─► No config ──► Continue to provisioning │ │ │ │ 3. Connect to WiFi │ │ │ │ │ ▼ │ │ 4. HTTP POST /api/v1/provision │ │ │ Request: { "token": "...", "hwid": "..." } │ │ │ │ │ ▼ │ │ 5. Server validates token, generates WireGuard keys │ │ │ │ │ ▼ │ │ 6. Server encrypts config with AES-256-GCM │ │ │ Key = SHA256(hwid + salt) │ │ │ │ │ ▼ │ │ 7. Server responds with encrypted_config │ │ │ │ │ ▼ │ │ 8. Agent decrypts config using mbedtls │ │ │ │ │ ▼ │ │ 9. Store config in NVS │ │ │ │ │ ▼ │ │ 10. Apply WireGuard tunnel │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 4.3 Server Configuration Sources The embedded agent can obtain server configuration from: | Source | Priority | Use Case | |--------|----------|----------| | NVS (saved config) | 1 | After initial provisioning | | sdkconfig (menuconfig) | 2 | Build-time configuration | | SmartConfig / BLE | 3 | WiFi provisioning (future) | | QR Code | 4 | Manual provisioning (future) | ### 4.4 Build-Time Configuration Configure server URL and token via `idf.py menuconfig`: ``` Component config → NexusGuard Agent ├── Server URL (NEXUS_SERVER_URL) ├── Registration Token (NEXUS_REG_TOKEN) ├── Server Salt (NEXUS_SERVER_SALT) └── WiFi SSID (NEXUS_WIFI_SSID) └── WiFi Password (NEXUS_WIFI_PASSWORD) ``` ### 4.5 Runtime Configuration (NVS) After first provisioning, config is stored in NVS: | Key | Description | |-----|-------------| | `server_url` | Server API URL | | `device_id` | Assigned device UUID | | `private_key` | WireGuard private key | | `preshared_key` | Pre-shared key | | `internal_ip` | Device IP address | | `server_pub` | Server public key | | `endpoint` | Server WireGuard endpoint | | `dns` | DNS server | | `allowed_ips` | Allowed IPs | | `server_wg_ip` | Server WireGuard IP | | `config_hash` | Last config hash | ### 4.6 Server API Contract The embedded agent uses the **same HTTP API** as the Go device-agent: | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/provision` | POST | Initial key exchange | | `/api/v1/heartbeat` | POST | Periodic status + config sync | **Key**: The API contract is stable and documented in `docs/API_COMPAT.md`. The server does not need to know whether the client is Go agent or ESP32 agent — it treats them identically. ### 4.7 Independence from Main Repo The embedded agent is **fully independent**: ``` nexus-agent-embedded/ # Standalone repo ├── main/ # C source code ├── components/ # ESP-IDF components ├── docs/ # Documentation ├── CMakeLists.txt # Build system └── sdkconfig.defaults # Configuration ``` **No dependency on**: - `server-core` (Go backend) - `device-agent` (Go agent) - `dashboard-ui` (Vue frontend) **Build anywhere** with ESP-IDF toolchain — no Go, no Node.js, no Docker. ## 5. Module Design ### 4.1 main.c **Responsibilities**: - Hardware initialization (NVS, WiFi, GPIO) - Task creation and event loop - Signal handling (Ctrl+C graceful shutdown) **Key Functions**: ```c void app_main(void); // Entry point void shutdown_handler(void); // Graceful shutdown ``` ### 4.2 provision.c **Responsibilities**: - HTTP POST to `/api/v1/provision` - Parse encrypted config response - Decrypt config via crypto.c - Store config in NVS **Key Functions**: ```c esp_err_t provision_device(const char *server_url, const char *token, const char *hwid, wg_config_t *config); esp_err_t decrypt_config(const uint8_t *encrypted, size_t len, const char *hwid, const char *salt, wg_config_t *config); ``` **HTTP Request**: ```json POST /api/v1/provision Content-Type: application/json { "token": "registration-token", "hwid": "esp32-hardware-id" } ``` **HTTP Response**: ```json { "encrypted_config": "base64-encoded-bytes" } ``` ### 4.3 heartbeat.c **Responsibilities**: - HTTP POST to `/api/v1/heartbeat` every 30s - Send device status + tunnel state - Receive config sync (detect changes) - Trigger tunnel rebuild if config changed **Key Functions**: ```c esp_err_t heartbeat_send(const wg_config_t *config, const char *status, bool tunnel_up, int64_t last_handshake); bool heartbeat_config_changed(const char *old_hash, const char *new_hash); ``` **HTTP Request**: ```json POST /api/v1/heartbeat Content-Type: application/json { "device_id": "uuid", "status": "connected", "tunnel_up": true, "last_handshake": "2026-06-26T09:00:00Z" } ``` **HTTP Response**: ```json { "device_id": "uuid", "status": "ok", "internal_ip": "10.172.21.2", "private_key": "hex", "preshared_key": "hex", "server_pub": "hex", "endpoint": "italy-twenty.gl.at.ply.gg:59750", "dns": "1.1.1.1", "allowed_ips": "0.0.0.0/0", "server_wg_ip": "10.172.21.1", "forwards_hash": "hash-string" } ``` ### 4.4 tunnel.c **Responsibilities**: - Initialize WireGuard tunnel via lwIP - Apply config (keys, endpoint, allowed IPs) - Monitor tunnel state (handshake time) - Rebuild tunnel on config change **Key Functions**: ```c esp_err_t tunnel_init(const wg_config_t *config); esp_err_t tunnel_apply_config(const wg_config_t *config); esp_err_t tunnel_rebuild(const wg_config_t *config); int64_t tunnel_get_last_handshake(void); bool tunnel_is_up(void); ``` **WireGuard Config Application**: ```c // Pseudo-code wg_device wg = { .private_key = config->private_key, .listen_port = 0, // ephemeral }; wg_peer peer = { .public_key = config->server_pub, .preshared_key = config->preshared_key, .endpoint = config->endpoint, .allowed_ips = config->allowed_ips, }; wg_set_device(&wg); wg_add_peer(&wg, &peer); wg_set_peer_allowed_ips(&wg, &peer, config->allowed_ips); ``` ### 4.5 crypto.c **Responsibilities**: - Key derivation: SHA256(hwid + salt) - AES-256-GCM decryption - Secure memory handling **Key Functions**: ```c esp_err_t crypto_derive_key(const char *hwid, const char *salt, uint8_t key[32]); esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len, const uint8_t key[32], uint8_t **plaintext, size_t *plaintext_len); ``` **Algorithm**: ``` Key Derivation: key = SHA256(hwid + salt) // 32 bytes Decryption: nonce = ciphertext[0:12] // first 12 bytes encrypted = ciphertext[12:] // rest plaintext = AES-256-GCM-Decrypt(key, nonce, encrypted) ``` ### 4.6 config.c **Responsibilities**: - NVS read/write for config fields - Config validation - Config versioning **Key Functions**: ```c esp_err_t config_save(const wg_config_t *config); esp_err_t config_load(wg_config_t *config); esp_err_t config_clear(void); bool config_is_valid(const wg_config_t *config); ``` **NVS Keys**: | Key | Max Size | Description | |-----|----------|-------------| | `device_id` | 37 | UUID string | | `private_key` | 64 | WG private key hex | | `preshared_key` | 64 | PSK hex | | `internal_ip` | 18 | IP address | | `server_pub` | 64 | Server public key hex | | `endpoint` | 128 | Server endpoint | | `dns` | 64 | DNS server | | `allowed_ips` | 128 | Allowed IPs | | `config_hash` | 64 | Last config hash | ### 4.7 wifi.c **Responsibilities**: - WiFi STA initialization - Connection management - Reconnection handling **Key Functions**: ```c esp_err_t wifi_init_sta(const char *ssid, const char *password); esp_err_t wifi_connect(void); esp_err_t wifi_disconnect(void); bool wifi_is_connected(void); ``` ## 5. Data Structures ### 5.1 wg_config_t ```c typedef struct { char device_id[37]; // UUID char private_key[64]; // WG private key hex char preshared_key[64]; // PSK hex char internal_ip[18]; // e.g. "10.172.21.2" char server_pub[64]; // Server WG public key hex char endpoint[128]; // e.g. "italy-twenty.gl.at.ply.gg:59750" char dns[64]; // DNS server char allowed_ips[128]; // e.g. "0.0.0.0/0" char server_wg_ip[18]; // e.g. "10.172.21.1" char config_hash[64]; // Last config hash } wg_config_t; ``` ### 5.2 device_state_t ```c typedef enum { STATE_BOOT, STATE_WIFI_INIT, STATE_WIFI_CONNECTED, STATE_PROVISIONING, STATE_PROVISIONED, STATE_TUNNEL_UP, STATE_TUNNEL_DOWN, STATE_HEARTBEAT, STATE_ERROR } device_state_t; ``` ## 6. Error Handling ### 6.1 Error Codes ```c typedef enum { ERR_OK = 0, ERR_WIFI_CONNECT = -1, ERR_HTTP_PROVISION = -2, ERR_HTTP_HEARTBEAT = -3, ERR_CRYPTO_DECRYPT = -4, ERR_CONFIG_INVALID = -5, ERR_TUNNEL_INIT = -6, ERR_TUNNEL_APPLY = -7, ERR_NVS_READ = -8, ERR_NVS_WRITE = -9, } agent_error_t; ``` ### 6.2 Recovery Strategy | Error | Recovery | |-------|----------| | WiFi connect fail | Retry 3x, then reboot | | HTTP provision fail | Retry 3x, then reboot | | HTTP heartbeat fail | Log only, continue | | Crypto decrypt fail | Clear config, re-provision | | Tunnel init fail | Retry 3x, then reboot | | NVS read fail | Use defaults, re-provision | ## 7. Power Management ### 7.1 WiFi Sleep ```c // Enable WiFi sleep when tunnel is up esp_wifi_set_ps(WIFI_PS_MIN_MODEM); // Disable sleep during provisioning/heartbeat esp_wifi_set_ps(WIFI_PS_NONE); ``` ### 7.2 Light Sleep ```c // Enter light sleep between heartbeats (30s) esp_light_sleep_start(); // Wake on: WiFi event, GPIO interrupt, timer ``` ## 8. Security Considerations ### 8.1 Key Storage - WireGuard keys stored in NVS (flash, encrypted by NVS encryption) - Never log keys or tokens - Clear memory after use: `memset_s(key, 0, sizeof(key))` ### 8.2 TLS - Server communication over HTTPS (TLS 1.2+) - Skip certificate verification for internal network (configurable) - Future: Support custom CA certificate ### 8.3 Hardware ID - Derived from ESP32 eFuse MAC or custom serial - Used for key derivation (HWID + salt) - Never transmitted in plaintext ## 9. Testing Strategy ### 9.1 Unit Tests - Crypto: Decrypt known ciphertext with known key - Config: NVS read/write cycle - Heartbeat: Parse server response ### 9.2 Integration Tests - Provision → Tunnel → Heartbeat cycle - Config change detection - Tunnel rebuild on config change ### 9.3 Hardware Tests - ESP32-S3 DevKit flashing - WiFi connection stability - WireGuard handshake verification - Power consumption measurement ## 10. Future Enhancements ### 10.1 Phase 2 - OTA firmware updates - Custom CA certificate support - Power optimization (deep sleep) - Multiple server support ### 10.2 Phase 3 - Zephyr RTOS port - ESP32-S2 support - Ethernet (SPI) support - Bluetooth provisioning ## 11. References - [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/) - [WireGuard Protocol](https://www.wireguard.com/protocol/) - [lwIP Documentation](https://www.nongnu.org/lwip/) - [mbedtls Documentation](https://mbed-tls.readthedocs.io/) - [NexusGuard Server API](../../server-core/docs/)