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>
9.8 KiB
9.8 KiB
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:
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:
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"
}
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. 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);
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)
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 |
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);
Data Structures
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;
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;
Error Handling
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;
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:
// 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:
// 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