Files
Nexus-Guard-Suite/reference/esp32-agent/docs/API_COMPAT.md
T
datadunia 84689b208e 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>
2026-07-06 14:20:10 +07:00

8.1 KiB

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
{
  "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
{
  "encrypted_config": "base64-encoded-bytes"
}
Field Type Description
encrypted_config string AES-256-GCM encrypted ConfigPayload

Response (Error)

{
  "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:

{
  "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
{
  "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
{
  "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)

{
  "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
// 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

// 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

// 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

#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