- reference/esp32-agent/: DESIGN, HARDWARE, API_COMPAT, ARCHITECTURE, README - reference/ios-agent/: DESIGN, API_COMPAT, UI_DESIGN, ARCHITECTURE, README - Both mirror Android agent architecture, UI design, and heartbeat flow - Architecture-only (no code) — avoids submodule conflicts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
20 KiB
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:
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:
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:
POST /api/v1/provision
Content-Type: application/json
{
"token": "registration-token",
"hwid": "esp32-hardware-id"
}
HTTP Response:
{
"encrypted_config": "base64-encoded-bytes"
}
4.3 heartbeat.c
Responsibilities:
- HTTP POST to
/api/v1/heartbeatevery 30s - Send device status + tunnel state
- Receive config sync (detect changes)
- Trigger tunnel rebuild if config changed
Key Functions:
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:
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:
{
"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:
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:
// 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:
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:
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:
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
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
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
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
// 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
// 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