docs: add ESP32 and iOS agent architecture reference

- 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>
This commit is contained in:
datadunia
2026-07-06 14:20:10 +07:00
parent 4af46dadad
commit 84689b208e
10 changed files with 3565 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
# API Compatibility Reference
**Version**: 1.0.0
**Date**: 2026-06-26
This document defines the HTTP API contract between NexusGuard Server Core and Embedded Agent (ESP32).
## Overview
The embedded agent communicates with the server via two HTTP endpoints:
1. **Provisioning**: `POST /api/v1/provision` — Initial key exchange
2. **Heartbeat**: `POST /api/v1/heartbeat` — Periodic status + config sync
## 1. Provisioning
### Request
```
POST /api/v1/provision
Content-Type: application/json
```
```json
{
"token": "string (required)",
"hwid": "string (required)"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | string | Yes | Registration token (single-use) |
| `hwid` | string | Yes | Hardware ID (ESP32 MAC or serial) |
### Response (Success)
```
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"encrypted_config": "base64-encoded-bytes"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `encrypted_config` | string | AES-256-GCM encrypted ConfigPayload |
### Response (Error)
```json
{
"error": "string"
}
```
| Status | Error | Description |
|--------|-------|-------------|
| 400 | `"invalid token format"` | Token is not valid UUID |
| 400 | `"token already used"` | Token was already consumed |
| 400 | `"invalid hardware id"` | HWID is empty or invalid |
| 403 | `"token expired"` | Token has expired (24h TTL) |
| 500 | `"provisioning failed"` | Server internal error |
### Decrypted ConfigPayload
After decrypting `encrypted_config`, the agent receives:
```json
{
"device_id": "string (UUID)",
"private_key": "string (hex, 64 chars)",
"preshared_key": "string (hex, 64 chars)",
"internal_ip": "string (e.g. 10.172.21.2)",
"server_pub": "string (hex, 64 chars)",
"endpoint": "string (e.g. italy-twenty.gl.at.ply.gg:59750)",
"dns": "string (e.g. 1.1.1.1)",
"allowed_ips": "string (e.g. 0.0.0.0/0)",
"server_wg_ip": "string (e.g. 10.172.21.1)"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `device_id` | string | Device UUID (use in heartbeat) |
| `private_key` | string | WireGuard private key (hex) |
| `preshared_key` | string | Pre-shared key (hex) |
| `internal_ip` | string | Device IP address |
| `server_pub` | string | Server WireGuard public key (hex) |
| `endpoint` | string | Server WireGuard endpoint |
| `dns` | string | DNS server IP |
| `allowed_ips` | string | Allowed IPs (e.g. "0.0.0.0/0" for full tunnel) |
| `server_wg_ip` | string | Server WireGuard IP |
## 2. Heartbeat
### Request
```
POST /api/v1/heartbeat
Content-Type: application/json
```
```json
{
"device_id": "string (required)",
"status": "string (optional)",
"state": "string (optional)",
"tunnel_up": "boolean (optional)",
"last_handshake": "string (optional, ISO 8601)"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `device_id` | string | Yes | Device UUID |
| `status` | string | No | "connected", "recovering", "stopped" |
| `state` | string | No | Free-form state description |
| `tunnel_up` | boolean | No | WireGuard tunnel status |
| `last_handshake` | string | No | ISO 8601 timestamp of last handshake |
### Response (Success)
```
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"device_id": "string",
"status": "ok",
"internal_ip": "string",
"private_key": "string (hex)",
"preshared_key": "string (hex)",
"server_pub": "string (hex)",
"endpoint": "string",
"server_wg_ip": "string",
"dns": "string",
"allowed_ips": "string",
"forwards_hash": "string"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `device_id` | string | Device UUID |
| `status` | string | "ok" or error |
| `internal_ip` | string | Device IP (may change) |
| `private_key` | string | WireGuard private key (may rotate) |
| `preshared_key` | string | Pre-shared key (may rotate) |
| `server_pub` | string | Server public key |
| `endpoint` | string | Server endpoint |
| `server_wg_ip` | string | Server WireGuard IP |
| `dns` | string | DNS server |
| `allowed_ips` | string | Allowed IPs |
| `forwards_hash` | string | Hash of port forwards (for change detection) |
### Response (Error)
```json
{
"error": "string"
}
```
| Status | Error | Description |
|--------|-------|-------------|
| 400 | `"invalid device id"` | Device ID is not valid UUID |
| 403 | `"device is suspended"` | Device is suspended by admin |
| 500 | `"heartbeat failed"` | Server internal error |
## 3. Crypto Protocol
### Key Derivation
```
key = SHA256(hwid + salt)
```
- `hwid`: Hardware ID (ESP32 MAC or serial)
- `salt`: Server's `SERVER_SALT` environment variable
- Output: 32-byte AES key
### Encryption (Server Side)
```
plaintext = ConfigPayload JSON
nonce = random 12 bytes
ciphertext = AES-256-GCM-Encrypt(key, nonce, plaintext)
encrypted_config = nonce + ciphertext
```
### Decryption (Agent Side)
```
encrypted_config = response.encrypted_config (base64 decoded)
nonce = encrypted_config[0:12]
ciphertext = encrypted_config[12:]
plaintext = AES-256-GCM-Decrypt(key, nonce, ciphertext)
ConfigPayload = JSON.parse(plaintext)
```
### Implementation Notes
- Use `mbedtls_sha256()` for key derivation
- Use `mbedtls_gcm_*()` for AES-256-GCM
- Nonce size: 12 bytes (fixed for GCM)
- Tag size: 16 bytes (default for GCM)
## 4. Config Change Detection
### Heartbeat Hash Comparison
The server returns `forwards_hash` in heartbeat response. Agent should:
1. Store last received `forwards_hash` in NVS
2. Compare with current hash
3. If different, trigger config reload
```c
// Pseudo-code
char last_hash[64];
nvs_get_str(nvs_handle, "config_hash", last_hash, sizeof(last_hash));
if (strcmp(last_hash, response.forwards_hash) != 0) {
// Config changed, rebuild tunnel
tunnel_rebuild(&config);
nvs_set_str(nvs_handle, "config_hash", response.forwards_hash);
}
```
## 5. Versioning
### Current Version
- API Version: v1 (implicit, no version in URL)
- Agent Version: 1.0.0
### Compatibility Policy
- **Breaking Changes**: Major version bump (v2)
- **New Fields**: Added without version bump (agent ignores unknown fields)
- **Deprecation**: 6-month notice before removal
### Agent Behavior
- Agent ignores unknown fields in server response
- Agent sends only required fields in request
- Agent handles missing optional fields gracefully
## 6. Examples
### Provisioning Flow
```c
// 1. Get hardware ID
char hwid[32];
get_hardware_id(hwid, sizeof(hwid));
// 2. Send provisioning request
char *response = http_post(server_url, "/api/v1/provision",
"{\"token\":\"%s\",\"hwid\":\"%s\"}", token, hwid);
// 3. Parse encrypted config
cJSON *json = cJSON_Parse(response);
char *encrypted = cJSON_GetObjectItem(json, "encrypted_config")->valuestring;
// 4. Decrypt config
wg_config_t config;
decrypt_config(encrypted, strlen(encrypted), hwid, salt, &config);
// 5. Store in NVS
config_save(&config);
// 6. Apply to WireGuard
tunnel_init(&config);
```
### Heartbeat Flow
```c
// 1. Get tunnel status
bool tunnel_up = tunnel_is_up();
int64_t last_hs = tunnel_get_last_handshake();
// 2. Send heartbeat
char *response = http_post(server_url, "/api/v1/heartbeat",
"{\"device_id\":\"%s\",\"tunnel_up\":%s,\"last_handshake\":\"%s\"}",
config.device_id, tunnel_up ? "true" : "false",
format_timestamp(last_hs));
// 3. Check for config changes
cJSON *json = cJSON_Parse(response);
const char *new_hash = cJSON_GetObjectItem(json, "forwards_hash")->valuestring;
if (strcmp(config.config_hash, new_hash) != 0) {
// Config changed, reload
load_config_from_response(json, &config);
tunnel_rebuild(&config);
config_save(&config);
}
```
## 7. Error Handling
### HTTP Retries
```c
#define MAX_RETRIES 3
#define RETRY_DELAY_MS 1000
for (int i = 0; i < MAX_RETRIES; i++) {
esp_err_t err = http_post(...);
if (err == ESP_OK) break;
vTaskDelay(RETRY_DELAY_MS * (i + 1) / portTICK_PERIOD_MS);
}
```
### Timeout Settings
- Connect timeout: 10 seconds
- Read timeout: 30 seconds
- Write timeout: 10 seconds
## 8. Security Notes
- All communication over HTTPS (TLS 1.2+)
- Registration token is single-use (consumed on first provisioning)
- Device can re-provision by obtaining new token
- Keys never logged or transmitted in plaintext
+387
View File
@@ -0,0 +1,387 @@
# ESP32 Agent — Module Architecture
**Status**: ARCHITECTURE ONLY
**Last Updated**: 2026-07-06
## Overview
This document defines the module architecture for the ESP32-based WireGuard agent. The agent runs on FreeRTOS + ESP-IDF and connects to NexusGuard Server Core via HTTP.
## 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)
```
## State Machine
```
┌─────────────┐
│ BOOT │
└──────┬──────┘
┌─────────────┐
│ WIFI_INIT │ ◄──────────────────────────────┐
└──────┬──────┘ │
│ connected │
▼ │
┌─────────────┐ fail ┌─────────────┐│
│ PROVISION │───────────────►│ WIFI_RETRY ││
└──────┬──────┘ └──────┬──────┘│
│ success │ │
▼ └───────┘
┌─────────────┐
│ TUNNEL_UP │
└──────┬──────┘
┌─────────────┐
│ HEARTBEAT │ ◄─── 30s interval
└──────┬──────┘
│ config_changed
┌─────────────┐
│ TUNNEL_REBUILD │
└──────┬──────┘
└──► HEARTBEAT
```
## Module Design
### 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
```
### 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"
}
```
### 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. 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);
```
### 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)
```
### 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 |
### 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);
```
## Data Structures
### 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;
```
### 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;
```
## Error Handling
### 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;
```
### 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 |
## Implementation Status
### ✅ Implemented (Real Code)
| Module | File | Status |
|--------|------|--------|
| Provisioning | `provision.c` | HTTP POST working |
| Heartbeat | `heartbeat.c` | HTTP loop working |
| Crypto | `crypto.c` | mbedtls AES-256-GCM |
| Config | `config.c` | NVS read/write |
| WiFi | `wifi.c` | STA connection |
### ❌ STUB (Needs Implementation)
| Module | File | What's Missing |
|--------|------|----------------|
| Tunnel | `tunnel.c` | `wireguard-esp32` component integration |
| Heartbeat | `heartbeat.c` | Config sync parsing from response |
### TODO: tunnel.c Integration
When integrating `wireguard-esp32` component:
```c
// 1. Parse private_key hex to binary
// 2. Parse server_pub hex to binary
// 3. Parse preshared_key hex to binary
// 4. Create WireGuard device with private_key
// 5. Add peer with server_pub, preshared_key, endpoint
// 6. Set peer allowed_ips
// 7. Start WireGuard tunnel
```
### TODO: heartbeat.c Config Sync
When parsing heartbeat response:
```c
// 1. Parse response JSON
// 2. Extract config_hash
// 3. Compare with stored config_hash
// 4. If different → parse new config fields
// 5. Save to NVS
// 6. Call tunnel_rebuild()
```
## Future Enhancements
### Phase 2
- OTA firmware updates
- Custom CA certificate support
- Power optimization (deep sleep)
- Multiple server support
### Phase 3
- Zephyr RTOS port
- ESP32-S2 support
- Ethernet (SPI) support
- Bluetooth provisioning
## 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/)
+582
View File
@@ -0,0 +1,582 @@
# 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/)
+330
View File
@@ -0,0 +1,330 @@
# Hardware Reference
**Version**: 1.0.0
**Date**: 2026-06-26
## 1. Target Hardware
### 1.1 LW840X Module
The LW840X is an ESP32-based WiFi module designed for IoT applications.
| Specification | Value |
|---------------|-------|
| MCU | ESP32 (Xtensa LX7 dual-core) |
| Clock | 240 MHz |
| RAM | 520 KB SRAM |
| Flash | 4 MB (external) |
| WiFi | 802.11 b/g/n (2.4 GHz) |
| Bluetooth | None (LW840X) |
| GPIO | 20+ (depends on module) |
| ADC | 18 channels (12-bit) |
| Operating Temp | -40°C to +85°C |
| Voltage | 3.0V to 3.6V |
### 1.2 Pin Mapping
```
LW840X Module Pinout
┌─────────────────────────────────────┐
│ │
│ 3V3 ─┐ │
│ GND ─┤ │
│ EN ─┤ │
│ IO0 ─┤ Boot/Flash mode │
│ IO1 ─┤ TX0 (UART0) │
│ IO3 ─┤ RX0 (UART0) │
│ IO4 ─┤ Status LED │
│ IO5 ─┤ (reserved) │
│ IO12 ─┤ (reserved) │
│ IO13 ─┤ (reserved) │
│ IO14 ─┤ (reserved) │
│ IO15 ─┤ (reserved) │
│ IO16 ─┤ (reserved) │
│ IO17 ─┤ (reserved) │
│ IO18 ─┤ (reserved) │
│ IO19 ─┤ (reserved) │
│ IO21 ─┤ (reserved) │
│ IO22 ─┤ (reserved) │
│ IO23 ─┤ (reserved) │
│ IO25 ─┤ (reserved) │
│ IO26 ─┤ (reserved) │
│ IO27 ─┤ (reserved) │
│ IO32 ─┤ (reserved) │
│ IO33 ─┤ (reserved) │
│ IO34 ─┤ (reserved) │
│ IO35 ─┤ (reserved) │
│ │
└─────────────────────────────────────┘
```
### 1.3 Minimal Circuit
```
Power Supply:
3.3V ──── LW840X 3V3
GND ──── LW840X GND
Decoupling:
100nF ceramic capacitor between 3V3 and GND (close to module)
Boot/Flash:
IO0 ──── 10K pull-up to 3V3 (normal boot)
IO0 ──── GND (flash mode)
Status LED:
IO4 ──── 220Ω ──── LED ──── GND
UART (for debugging):
IO1 (TX0) ──── USB-UART RX
IO3 (RX0) ──── USB-UART TX
GND ──── USB-UART GND
```
## 2. Power Requirements
### 2.1 Current Consumption
| Mode | Current | Notes |
|------|---------|-------|
| Active (WiFi TX) | 130-170 mA | Transmitting data |
| Active (WiFi RX) | 80-100 mA | Receiving data |
| Modem Sleep | 15-20 mA | WiFi connected, low duty cycle |
| Light Sleep | 0.8-1.5 mA | CPU paused, WiFi wake |
| Deep Sleep | 5-10 µA | RTC only, wake on GPIO |
| Off | 0 µA | No power |
### 2.2 Power Supply Design
```
Recommended:
Input: 5V USB or 12V DC
Regulator: AMS1117-3.3 or similar LDO
Capacity: 500mA minimum
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 5V USB │────▶│ LDO │────▶│ LW840X │
│ │ │ 3.3V │ │ │
└─────────┘ └─────────┘ └─────────┘
GND
```
### 2.3 Battery Operation
For battery-powered applications:
| Battery | Capacity | Runtime (Active) | Runtime (Sleep) |
|---------|----------|------------------|-----------------|
| CR2032 | 225 mAh | ~1.5 hours | ~2 years |
| 18650 | 3400 mAh | ~20 hours | ~30 years |
| LiPo 1000mAh | 1000 mAh | ~6 hours | ~10 years |
**Note**: Deep sleep with periodic wake (e.g., every 5 minutes) is recommended for battery operation.
## 3. Antenna Options
### 3.1 PCB Trace Antenna
- **Pros**: Low cost, compact
- **Cons**: Lower gain, sensitive to placement
- **Range**: 10-30m (indoor)
- **Use Case**: Short-range, cost-sensitive
### 3.2 U.FL Connector
- **Pros**: Higher gain, flexible placement
- **Cons**: Additional cost, larger size
- **Range**: 50-100m (indoor)
- **Use Case**: Longer range, industrial
### 3.3 External Antenna
- **Pros**: Best performance, directional options
- **Cons**: Highest cost, largest size
- **Range**: 100m+ (outdoor)
- **Use Case**: Outdoor, long-range
## 4. Development Board
### 4.1 ESP32-DevKitC
Recommended for development and prototyping.
| Feature | Specification |
|---------|---------------|
| MCU | ESP32-WROOM-32 |
| Flash | 4 MB |
| RAM | 520 KB SRAM |
| WiFi | 802.11 b/g/n |
| Bluetooth | BT 4.2 + BLE |
| USB | Micro-USB (CP2102) |
| GPIO | 38 pins |
| Price | ~$5-10 |
### 4.2 Flashing
```bash
# Install ESP-IDF
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/
# Set target
idf.py set-target esp32
# Build
idf.py build
# Flash (auto-detect port)
idf.py -p /dev/ttyUSB0 flash
# Monitor
idf.py -p /dev/ttyUSB0 monitor
```
### 4.3 Pin Connections (DevKit)
```
Status LED:
GPIO4 ──── 220Ω ──── LED ──── GND
Debug UART (optional):
GPIO1 (TX0) ──── USB-UART RX
GPIO3 (RX0) ──── USB-UART TX
GND ──── USB-UART GND
```
## 5. Production Board
### 5.1 Design Guidelines
1. **Power**:
- Use 3.3V LDO with 500mA capacity
- Add 100nF ceramic capacitor close to 3V3 pin
- Add 10µF tantalum capacitor for bulk decoupling
2. **Antenna**:
- Keep antenna area clear of copper pours
- Minimum 10mm clearance around antenna
- Use U.FL connector for external antenna
3. **Layout**:
- Route UART traces away from antenna
- Keep crystal traces short
- Ground plane under module
4. **Enclosure**:
- Use non-metallic enclosure (plastic)
- Ensure antenna is not shielded
- Provide mounting holes
### 5.2 Bill of Materials (BOM)
| Component | Quantity | Package | Notes |
|-----------|----------|---------|-------|
| LW840X | 1 | Module | ESP32-based |
| AMS1117-3.3 | 1 | SOT-223 | 3.3V LDO |
| 100nF | 2 | 0402 | Decoupling |
| 10µF | 1 | 0805 | Bulk cap |
| 220Ω | 1 | 0402 | LED resistor |
| LED | 1 | 0603 | Status indicator |
| U.FL | 1 | SMD | Antenna connector |
| Header | 1 | 2.54mm | Debug UART |
### 5.3 Schematic
```
┌─────────────────┐
│ LW840X │
3.3V ────────────┤ 3V3 GND ├──── GND
│ │
IO4 ──── 220Ω ───┤ IO4 TX0 ├──── UART RX
└── LED ──┤ RX0 ├──── UART TX
GND │ │
│ EN ├──── 10K ──── 3.3V
│ │
│ IO0 ├──── 10K ──── 3.3V
│ │ (flash: GND)
└─────────────────┘
```
## 6. Testing & Validation
### 6.1 Hardware Tests
1. **Power-on Test**:
- Verify 3.3V at module pin
- Check current consumption (~50mA idle)
- Confirm LED blinks on boot
2. **WiFi Test**:
- Scan for WiFi networks
- Connect to test AP
- Measure RSSI at distance
3. **UART Test**:
- Send AT commands (if firmware supports)
- Verify debug output
- Check baud rate (115200)
4. **Flash Test**:
- Write/read NVS data
- Verify flash size
- Test wear leveling
### 6.2 Production Test
1. **Functional Test**:
- Provision with test server
- Verify WireGuard handshake
- Check heartbeat response
2. **Stress Test**:
- Run for 24+ hours
- Monitor memory leaks
- Verify reconnection after WiFi drop
3. **Environmental Test**:
- Operating temperature range
- Humidity resistance
- Vibration resistance
## 7. Troubleshooting
### 7.1 Common Issues
| Symptom | Cause | Solution |
|---------|-------|----------|
| No boot | IO0 held low | Remove flash jumper |
| No WiFi | Antenna issue | Check antenna connection |
| High current | WiFi always on | Use modem sleep |
| Crash on boot | Stack overflow | Increase task stack size |
| Provision fail | Token expired | Get new token from server |
### 7.2 Debug Tools
- **Serial Monitor**: `idf.py monitor`
- **JTAG Debugger**: OpenOCD + GDB
- **Logic Analyzer**: Saleae or similar
- **Power Analyzer**: Monsoon or similar
## 8. Compliance
### 8.1 Certifications
- **FCC**: Required for US market
- **CE**: Required for EU market
- **IC**: Required for Canada market
- **TELEC**: Required for Japan market
### 8.2 Testing
- EMC testing (emissions and immunity)
- SAR testing (if applicable)
- Environmental testing (temperature, humidity)
## 9. References
- [ESP32 Hardware Design Guidelines](https://www.espressif.com/sites/default/files/documentation/esp32_hardware_design_guidelines_en.pdf)
- [ESP32 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf)
- [LW840X Module Datasheet](link-to-datasheet)
- [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/)