84689b208e
- 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>
583 lines
20 KiB
Markdown
583 lines
20 KiB
Markdown
# 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/)
|