From 84689b208e892c4a1538a1be36d5c240c5baf1f8 Mon Sep 17 00:00:00 2001 From: datadunia Date: Mon, 6 Jul 2026 14:20:10 +0700 Subject: [PATCH] docs: add ESP32 and iOS agent architecture reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- reference/esp32-agent/README.md | 103 ++++ reference/esp32-agent/docs/API_COMPAT.md | 335 ++++++++++++ reference/esp32-agent/docs/ARCHITECTURE.md | 387 ++++++++++++++ reference/esp32-agent/docs/DESIGN.md | 582 +++++++++++++++++++++ reference/esp32-agent/docs/HARDWARE.md | 330 ++++++++++++ reference/ios-agent/README.md | 58 ++ reference/ios-agent/docs/API_COMPAT.md | 365 +++++++++++++ reference/ios-agent/docs/ARCHITECTURE.md | 474 +++++++++++++++++ reference/ios-agent/docs/DESIGN.md | 425 +++++++++++++++ reference/ios-agent/docs/UI_DESIGN.md | 506 ++++++++++++++++++ 10 files changed, 3565 insertions(+) create mode 100644 reference/esp32-agent/README.md create mode 100644 reference/esp32-agent/docs/API_COMPAT.md create mode 100644 reference/esp32-agent/docs/ARCHITECTURE.md create mode 100644 reference/esp32-agent/docs/DESIGN.md create mode 100644 reference/esp32-agent/docs/HARDWARE.md create mode 100644 reference/ios-agent/README.md create mode 100644 reference/ios-agent/docs/API_COMPAT.md create mode 100644 reference/ios-agent/docs/ARCHITECTURE.md create mode 100644 reference/ios-agent/docs/DESIGN.md create mode 100644 reference/ios-agent/docs/UI_DESIGN.md diff --git a/reference/esp32-agent/README.md b/reference/esp32-agent/README.md new file mode 100644 index 0000000..9644edf --- /dev/null +++ b/reference/esp32-agent/README.md @@ -0,0 +1,103 @@ +# ESP32 Agent — Architecture Reference + +**Status**: ARCHITECTURE ONLY (no code) +**Location**: `reference/esp32-agent/` (NOT a submodule) + +## Overview + +This directory contains the architecture design and documentation for the ESP32-based WireGuard agent. It is **NOT a git submodule** — it is an architecture reference for future implementation. + +## Why `reference/` not `apps/`? + +If ESP32 agent is implemented as a submodule in the future, it will be added to `apps/device-agent-embedded/`. This `reference/` directory stores the architecture docs separately to avoid conflicts. + +## Purpose + +- Document the ESP32 agent architecture +- Define the API contract (shared with server-core) +- Hardware reference for LW840X module +- Track implementation status and TODOs + +## When to Implement + +When ready to build the ESP32 agent: + +1. Create a new repo: `nexus-agent-embedded` +2. Add as submodule: `apps/device-agent-embedded/` +3. Use ESP-IDF v5.2+ toolchain +4. Follow the architecture in `docs/DESIGN.md` + +## Files + +| File | Description | +|------|-------------| +| `docs/DESIGN.md` | Technical design document (full architecture) | +| `docs/HARDWARE.md` | ESP32/LW840X hardware reference | +| `docs/API_COMPAT.md` | Server API contract (shared protocol) | +| `docs/ARCHITECTURE.md` | Module architecture + implementation status | + +## Architecture Summary + +``` +┌─────────────────────────────────────────────────────────┐ +│ 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 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Implementation Status + +| Module | Status | Notes | +|--------|--------|-------| +| `tunnel.c` | ❌ STUB | Needs `wireguard-esp32` component integration | +| `heartbeat.c` | ✅ Real | HTTP loop working, config sync TODO | +| `provision.c` | ✅ Real | HTTP provisioning working | +| `crypto.c` | ✅ Real | mbedtls AES-256-GCM | +| `config.c` | ✅ Real | NVS storage | +| `wifi.c` | ✅ Real | WiFi STA management | + +## Dependencies + +- **ESP-IDF**: v5.2+ +- **mbedtls**: AES-256-GCM, SHA-256 (built-in) +- **lwIP**: TCP/IP stack (built-in) +- **FreeRTOS**: RTOS (built-in) +- **wireguard-esp32**: WireGuard tunnel library (NOT YET INTEGRATED) + +## API Contract (Shared with Server) + +``` +POST /api/v1/provision → { token, hwid } → { encrypted_config } +POST /api/v1/heartbeat → { device_id, tunnel_up, last_handshake } → { config sync } +``` + +See `docs/API_COMPAT.md` for full specification. + +## Related + +- [Server Core API](../../server-core/docs/) +- [Device Agent (Go)](../../apps/device-agent/) +- [Android Agent](../../apps/android-agent/) + +## License + +Proprietary - NexusGuard diff --git a/reference/esp32-agent/docs/API_COMPAT.md b/reference/esp32-agent/docs/API_COMPAT.md new file mode 100644 index 0000000..21f79ab --- /dev/null +++ b/reference/esp32-agent/docs/API_COMPAT.md @@ -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 diff --git a/reference/esp32-agent/docs/ARCHITECTURE.md b/reference/esp32-agent/docs/ARCHITECTURE.md new file mode 100644 index 0000000..1863039 --- /dev/null +++ b/reference/esp32-agent/docs/ARCHITECTURE.md @@ -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/) diff --git a/reference/esp32-agent/docs/DESIGN.md b/reference/esp32-agent/docs/DESIGN.md new file mode 100644 index 0000000..4c08474 --- /dev/null +++ b/reference/esp32-agent/docs/DESIGN.md @@ -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/) diff --git a/reference/esp32-agent/docs/HARDWARE.md b/reference/esp32-agent/docs/HARDWARE.md new file mode 100644 index 0000000..23a8820 --- /dev/null +++ b/reference/esp32-agent/docs/HARDWARE.md @@ -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/) diff --git a/reference/ios-agent/README.md b/reference/ios-agent/README.md new file mode 100644 index 0000000..bb990e0 --- /dev/null +++ b/reference/ios-agent/README.md @@ -0,0 +1,58 @@ +# iOS Agent — Architecture Reference + +**Status**: ARCHITECTURE ONLY (no code) +**Location**: `reference/ios-agent/` (NOT a submodule) +**Mirror**: Android Agent (`apps/android-agent/`) + +## Overview + +This directory contains the architecture design for an iOS WireGuard agent that mirrors the Android agent's architecture, UI design, colors, and heartbeat flow. + +## Why `reference/` not `apps/`? + +If iOS agent is implemented in the future, it will be added as `apps/ios-agent/`. This `reference/` directory stores the architecture docs separately to avoid conflicts. + +## Purpose + +- Document the iOS agent architecture (mirrors Android) +- Define UI design specs (colors, layout, components) +- API contract (shared with server-core) +- Track implementation status and TODOs + +## Architecture Mirror: Android → iOS + +| Android Component | iOS Equivalent | Notes | +|-------------------|----------------|-------| +| `AgentService.kt` | `PacketTunnelProvider` | Foreground service → NetworkExtension | +| `TunnelManager.kt` | `WireGuardAdapter` | VPNService → NEPacketTunnelProvider | +| `Heartbeat.kt` | `HeartbeatService` | HTTP loop (same) | +| `Provisioning.kt` | `ProvisioningService` | HTTP provisioning (same) | +| `ConfigStorage.kt` | `KeychainStorage` | SharedPreferences → Keychain | +| `Encryptor.kt` | `CryptoManager` | JCE → CryptoKit | +| `MainActivity.kt` | `ContentView` | Activity → SwiftUI | +| `SettingsActivity.kt` | `SettingsView` | Activity → SwiftUI | +| `BootReceiver.kt` | `BGAppRefreshTask` | Boot broadcast → Background Tasks | +| `LogBuffer.kt` | `LogStore` | In-memory → CoreData | + +## Files + +| File | Description | +|------|-------------| +| `docs/DESIGN.md` | Technical design + UI specs | +| `docs/ARCHITECTURE.md` | Module architecture | +| `docs/API_COMPAT.md` | Server API contract | +| `docs/UI_DESIGN.md` | UI design specs (colors, layout) | + +## Quick Start + +When implementing: + +1. Create Xcode project: `ios-agent` +2. Add NetworkExtension capability +3. Use `wireguard-apple` library for WireGuard +4. Follow architecture in `docs/ARCHITECTURE.md` +5. Match UI design in `docs/UI_DESIGN.md` + +## License + +Proprietary - NexusGuard diff --git a/reference/ios-agent/docs/API_COMPAT.md b/reference/ios-agent/docs/API_COMPAT.md new file mode 100644 index 0000000..69435a3 --- /dev/null +++ b/reference/ios-agent/docs/API_COMPAT.md @@ -0,0 +1,365 @@ +# iOS Agent — API Compatibility Reference + +**Status**: API CONTRACT +**Mirror**: Android Agent (`apps/android-agent/`) + +## Overview + +The iOS 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 (iOS identifierForVendor) | + +### 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 (iOS `identifierForVendor`) +- `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) + +```swift +let combined = Data(base64Encoded: encryptedConfig)! +let nonce = combined.prefix(12) +let ciphertext = combined.dropFirst(12) + +let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext) +let plaintext = try AES.GCM.open(sealedBox, using: key) +let json = try JSONSerialization.jsonObject(with: plaintext) +``` + +### Implementation Notes + +- Use `CryptoKit` for AES-256-GCM +- Use `CryptoKit` for SHA-256 key derivation +- 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 UserDefaults +2. Compare with current hash +3. If different, trigger config reload + +```swift +let lastHash = UserDefaults.standard.string(forKey: "config_hash") ?? "" + +if lastHash != response.forwardsHash { + // Config changed, rebuild tunnel + tunnelManager.rebuildTunnel(with: newConfig) + UserDefaults.standard.set(response.forwardsHash, forKey: "config_hash") +} +``` + +## 5. Hardware ID + +### iOS identifierForVendor + +```swift +import UIKit + +let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown" +``` + +### Alternative: Custom Serial + +```swift +func getHardwareId() -> String { + // Use Keychain to persist custom HWID + if let savedHwid = KeychainManager.get(key: "hwid") { + return savedHwid + } + + let newHwid = UUID().uuidString + KeychainManager.set(key: "hwid", value: newHwid) + return newHwid +} +``` + +## 6. 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 + +## 7. Examples + +### Provisioning Flow + +```swift +// 1. Get hardware ID +let hwid = UIDevice.current.identifierForVendor?.uuidString ?? "unknown" + +// 2. Send provisioning request +let body = ["token": token, "hwid": hwid] +let request = URLRequest(url: URL(string: "\(serverUrl)/api/v1/provision")!) + +// 3. Parse encrypted config +let encrypted = try JSONDecoder().decode([String: String].self, from: data)["encrypted_config"]! + +// 4. Decrypt config +let config = try CryptoManager.decryptConfig(encrypted: encrypted, hwid: hwid, salt: salt) + +// 5. Store in Keychain +KeychainStorage.save(config) + +// 6. Apply to WireGuard +try await tunnelManager.startTunnel(config: config) +``` + +### Heartbeat Flow + +```swift +// 1. Get tunnel status +let tunnelUp = tunnelManager.isTunnelUp() +let lastHandshake = tunnelManager.getLastHandshake() + +// 2. Send heartbeat +let body: [String: Any] = [ + "device_id": config.deviceId, + "tunnel_up": tunnelUp, + "last_handshake": lastHandshake?.iso8601String ?? "1970-01-01T00:00:00Z" +] + +// 3. Check for config changes +let response = try JSONDecoder().decode(HeartbeatResponse.self, from: data) + +if config.configHash != response.forwardsHash { + // Config changed, reload + let newConfig = try parseConfig(from: response) + tunnelManager.rebuildTunnel(with: newConfig) + KeychainStorage.save(newConfig) +} +``` + +## 8. Error Handling + +### HTTP Retries + +```swift +func sendRequest(url: URL, body: Data, maxRetries: Int = 3) async throws -> Data { + for attempt in 1...maxRetries { + do { + let (data, response) = try await URLSession.shared.data(for: request) + if let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 { + return data + } + } catch { + if attempt == maxRetries { + throw error + } + try await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000)) + } + } + throw AgentError.heartbeatFailed("Max retries exceeded") +} +``` + +### Timeout Settings + +- Connect timeout: 10 seconds +- Read timeout: 30 seconds +- Write timeout: 10 seconds + +## 9. 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 +- WireGuard keys stored in iOS Keychain (hardware-backed) diff --git a/reference/ios-agent/docs/ARCHITECTURE.md b/reference/ios-agent/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c753742 --- /dev/null +++ b/reference/ios-agent/docs/ARCHITECTURE.md @@ -0,0 +1,474 @@ +# iOS Agent — Module Architecture + +**Status**: ARCHITECTURE ONLY +**Mirror**: Android Agent (`apps/android-agent/`) + +## Overview + +This document defines the module architecture for the iOS WireGuard agent. The agent uses NetworkExtension (NEPacketTunnelProvider) for tunnel management and mirrors the Android agent's architecture. + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ iOS (SwiftUI) │ +├─────────────────────────────────────────────────────────┤ +│ App Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │ +│ │ AgentManager │ │ +│ │ (orchestrates all components) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Service Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │ +│ │ CryptoManager │ │ +│ │ AES-256-GCM (CryptoKit) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ NetworkExtension Layer │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ PacketTunnelProvider │ │ +│ │ (WireGuard tunnel via wireguard-apple) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Storage Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Task Architecture + +``` +iOS Threads/Tasks +├── Main Thread # UI updates +├── AgentManager # Orchestration (async/await) +├── HeartbeatTask # HTTP heartbeat (30s interval) +├── TunnelManager # WireGuard tunnel management +└── BackgroundTask # BGAppRefreshTask (background refresh) +``` + +## State Machine + +``` +┌─────────────┐ +│ BOOT │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ APP_INIT │ ◄──────────────────────────────┐ +└──────┬──────┘ │ + │ loaded │ + ▼ │ +┌─────────────┐ fail ┌─────────────┐│ +│ PROVISION │───────────────►│ APP_RETRY ││ +└──────┬──────┘ └──────┬──────┘│ + │ success │ │ + ▼ └───────┘ +┌─────────────┐ +│ TUNNEL_UP │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ HEARTBEAT │ ◄─── 30s interval +└──────┬──────┘ + │ config_changed + ▼ +┌─────────────┐ +│ TUNNEL_REBUILD │ +└──────┬──────┘ + │ + └──► HEARTBEAT +``` + +## Module Design + +### 1. AgentManager + +**Responsibilities**: +- Orchestrate all agent components +- Manage agent lifecycle (start/stop) +- Handle state transitions + +**Key Properties**: +```swift +class AgentManager: ObservableObject { + @Published var state: AgentState = .disconnected + @Published var tunnelInfo: TunnelInfo? + @Published var logs: [LogEntry] = [] + + private let provisioning: ProvisioningService + private let heartbeat: HeartbeatService + private let tunnel: TunnelManager + private let config: KeychainStorage +} +``` + +**Key Methods**: +```swift +func startAgent() async +func stopAgent() +func rebuildTunnel(with config: WireGuardConfig) async +``` + +### 2. PacketTunnelProvider + +**Responsibilities**: +- Handle NetworkExtension tunnel lifecycle +- Start/stop WireGuard tunnel +- Report tunnel status + +**Key Methods**: +```swift +override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) +override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) +override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) +``` + +**WireGuard Integration**: +```swift +// Uses wireguard-apple library +let adapter = WireGuardAdapter(with: self) { logLevel, message in + Logger.log(level: logLevel, message: message) +} + +adapter.start(tunnelConfiguration: tunnelConfig) { error in + if let error = error { + completionHandler(error) + } else { + completionHandler(nil) + } +} +``` + +### 3. TunnelManager + +**Responsibilities**: +- Build WireGuard config from stored keys +- Start/stop tunnel via PacketTunnelProvider +- Monitor tunnel state +- Track last handshake time + +**Key Methods**: +```swift +func startTunnel(config: WireGuardConfig) async throws +func stopTunnel() +func isTunnelUp() -> Bool +func getLastHandshake() -> Date? +``` + +**Config Build**: +```swift +func buildTunnelConfig(from config: WireGuardConfig) -> TunnelConfiguration { + let interface = InterfaceConfiguration( + privateKey: config.privateKey, + addresses: [config.internalIp], + dns: [config.dns] + ) + + let peer = PeerConfiguration( + publicKey: config.serverPub, + allowedIPs: config.allowedIps, + endpoint: config.endpoint, + preSharedKey: config.presharedKey, + persistentKeepAlive: 25 + ) + + return TunnelConfiguration(interface: interface, peers: [peer]) +} +``` + +### 4. HeartbeatService + +**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 Methods**: +```swift +func startHeartbeatLoop() +func stopHeartbeat() +func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig? +``` + +**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" +} +``` + +### 5. ProvisioningService + +**Responsibilities**: +- HTTP POST to `/api/v1/provision` +- Parse encrypted config response +- Decrypt config via CryptoManager +- Store config in Keychain + +**Key Methods**: +```swift +func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig? +func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig? +``` + +**HTTP Request**: +```json +POST /api/v1/provision +Content-Type: application/json + +{ + "token": "registration-token", + "hwid": "ios-hardware-id" +} +``` + +**HTTP Response**: +```json +{ + "encrypted_config": "base64-encoded-bytes" +} +``` + +### 6. CryptoManager + +**Responsibilities**: +- Key derivation: SHA-256(hwid + salt) +- AES-256-GCM decryption +- Secure memory handling + +**Key Methods**: +```swift +func deriveKey(token: String, hwid: String) -> SymmetricKey +func decrypt(encoded: String, key: SymmetricKey) -> Data? +``` + +**Algorithm**: +```swift +// Key Derivation +let input = "\(token)\(hwid)".data(using: .utf8)! +let hash = SHA256.hash(data: input) +let key = SymmetricKey(data: hash) + +// Decryption +let combined = Data(base64Encoded: encoded)! +let nonce = combined.prefix(12) +let ciphertext = combined.dropFirst(12) + +let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext) +let plaintext = try AES.GCM.open(sealedBox, using: key) +``` + +### 7. KeychainStorage + +**Responsibilities**: +- Store WireGuard keys in Keychain +- Store agent config in UserDefaults +- Secure key access + +**Key Methods**: +```swift +func saveConfig(_ config: AgentConfig) +func loadConfig() -> AgentConfig? +func saveWireGuardConfig(_ config: WireGuardConfig) +func loadWireGuardConfig() -> WireGuardConfig? +func clearAll() +``` + +**Storage Strategy**: +- **Keychain**: WireGuard keys (private_key, preshared_key) +- **UserDefaults**: Non-sensitive config (server_url, device_id, endpoint) +- **File**: Log entries (optional) + +### 8. LogStore + +**Responsibilities**: +- Buffer log entries in memory +- Persist to CoreData (optional) +- Provide log stream for UI + +**Key Methods**: +```swift +func add(level: LogLevel, tag: String, message: String) +func clear() +func getLogs() -> [LogEntry] +``` + +**Log Levels**: +```swift +enum LogLevel: String { + case debug = "DEBUG" + case info = "INFO" + case warn = "WARN" + case error = "ERROR" +} +``` + +## Data Structures + +### WireGuardConfig + +```swift +struct WireGuardConfig: Codable { + let deviceId: String + let privateKey: String + let presharedKey: String + let internalIp: String + let serverPub: String + let endpoint: String + let dns: String + let allowedIps: String + let serverWgIp: String + let configHash: String + let forwardsHash: String +} +``` + +### AgentConfig + +```swift +struct AgentConfig: Codable { + let serverUrl: String + let registrationToken: String + let isProvisioned: Bool +} +``` + +### AgentState + +```swift +enum AgentState: String { + case disconnected = "disconnected" + case connecting = "connecting" + case connected = "connected" + case failed = "failed" +} +``` + +### LogEntry + +```swift +struct LogEntry: Identifiable { + let id = UUID() + let timestamp: Date + let level: LogLevel + let tag: String + let message: String +} +``` + +## Error Handling + +### Error Types + +```swift +enum AgentError: Error { + case wifiNotConnected + case provisioningFailed(String) + case heartbeatFailed(String) + case cryptoDecryptionFailed + case configInvalid + case tunnelInitFailed + case keychainError(OSStatus) +} +``` + +### Recovery Strategy + +| Error | Recovery | +|-------|----------| +| WiFi not connected | Retry 3x, then pause heartbeat | +| HTTP provision fail | Retry 3x, then notify user | +| HTTP heartbeat fail | Log only, continue | +| Crypto decrypt fail | Clear config, re-provision | +| Tunnel init fail | Retry 3x, then notify user | +| Keychain error | Use UserDefaults fallback | + +## Background Tasks + +### BGAppRefreshTask + +iOS doesn't have boot receiver like Android. Use Background Tasks: + +```swift +// Register task +BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) { + task in + self.handleBackgroundRefresh(task: task as! BGAppRefreshTask) +} + +// Schedule task +let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh") +request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes +try BGTaskScheduler.shared.submit(request) +``` + +### Background Modes + +Add to `Info.plist`: +```xml +UIBackgroundModes + + fetch + processing + vpn-api + +``` + +## Dependencies + +- **wireguard-apple**: WireGuard tunnel library +- **CryptoKit**: AES-256-GCM encryption +- **NetworkExtension**: VPN tunnel management +- **BackgroundTasks**: Background refresh +- **SwiftUI**: UI framework +- **Combine**: Reactive state management + +## Android → iOS Mapping + +| Android | iOS | Notes | +|---------|-----|-------| +| `LifecycleService` | `PacketTunnelProvider` | Background service | +| `startForeground()` | `NEPacketTunnelProvider` | Always-on tunnel | +| `SharedPreferences` | `Keychain + UserDefaults` | Config storage | +| `BroadcastReceiver` | `BGAppRefreshTask` | Background triggers | +| `lifecycleScope` | `Task {}` | Coroutine/async | +| `OkHttp` | `URLSession` | HTTP client | +| `Base64` | `Data(base64Encoded:)` | Encoding | +| `Log.d/i/w/e` | `os_log` or `print` | Logging | diff --git a/reference/ios-agent/docs/DESIGN.md b/reference/ios-agent/docs/DESIGN.md new file mode 100644 index 0000000..0d4f213 --- /dev/null +++ b/reference/ios-agent/docs/DESIGN.md @@ -0,0 +1,425 @@ +# iOS Agent — Technical Design Document + +**Version**: 1.0.0 +**Date**: 2026-07-06 +**Status**: Draft +**Mirror**: Android Agent (`apps/android-agent/`) + +## 1. Executive Summary + +NexusGuard iOS Agent is an iOS-based WireGuard client for mobile devices. It mirrors the Android agent's architecture, UI design, and heartbeat flow, using iOS-native APIs (NetworkExtension, CryptoKit, Keychain). + +## 2. Goals & Non-Goals + +### Goals +- iOS-based WireGuard tunnel (NetworkExtension) +- Auto-provisioning via HTTP (same API as Android/ESP32) +- Heartbeat monitoring with config sync (30s interval) +- Keychain-based config storage (hardware-backed encryption) +- SwiftUI UI matching Android agent design +- Background operation via NetworkExtension + +### Non-Goals +- gRPC support (not needed for mobile) +- Port forwarding (userspace agent only) +- Jailbroken device support +- macOS/iPadOS support (iPhone only for v1) + +## 3. Architecture + +### 3.1 System Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ iOS (SwiftUI) │ +├─────────────────────────────────────────────────────────┤ +│ App Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │ +│ │ AgentManager │ │ +│ │ (orchestrates all components) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Service Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │ +│ │ CryptoManager │ │ +│ │ AES-256-GCM (CryptoKit) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ NetworkExtension Layer │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ PacketTunnelProvider │ │ +│ │ (WireGuard tunnel via wireguard-apple) │ │ +│ └──────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Storage Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.2 Task Architecture + +``` +iOS Threads/Tasks +├── Main Thread # UI updates +├── AgentManager # Orchestration (async/await) +├── HeartbeatTask # HTTP heartbeat (30s interval) +├── TunnelManager # WireGuard tunnel management +└── BackgroundTask # BGAppRefreshTask (background refresh) +``` + +### 3.3 State Machine + +``` +┌─────────────┐ +│ BOOT │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ APP_INIT │ ◄──────────────────────────────┐ +└──────┬──────┘ │ + │ loaded │ + ▼ │ +┌─────────────┐ fail ┌─────────────┐│ +│ PROVISION │───────────────►│ APP_RETRY ││ +└──────┬──────┘ └──────┬──────┘│ + │ success │ │ + ▼ └───────┘ +┌─────────────┐ +│ TUNNEL_UP │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ HEARTBEAT │ ◄─── 30s interval +└──────┬──────┘ + │ config_changed + ▼ +┌─────────────┐ +│ TUNNEL_REBUILD │ +└──────┬──────┘ + │ + └──► HEARTBEAT +``` + +## 4. Module Design + +### 4.1 AgentManager + +**Responsibilities**: +- Orchestrate all agent components +- Manage agent lifecycle (start/stop) +- Handle state transitions + +**Key Properties**: +```swift +class AgentManager: ObservableObject { + @Published var state: AgentState = .disconnected + @Published var tunnelInfo: TunnelInfo? + @Published var logs: [LogEntry] = [] + + private let provisioning: ProvisioningService + private let heartbeat: HeartbeatService + private let tunnel: TunnelManager + private let config: KeychainStorage +} +``` + +**Key Methods**: +```swift +func startAgent() async +func stopAgent() +func rebuildTunnel(with config: WireGuardConfig) async +``` + +### 4.2 PacketTunnelProvider + +**Responsibilities**: +- Handle NetworkExtension tunnel lifecycle +- Start/stop WireGuard tunnel +- Report tunnel status + +**Key Methods**: +```swift +override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) +override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) +override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) +``` + +### 4.3 TunnelManager + +**Responsibilities**: +- Build WireGuard config from stored keys +- Start/stop tunnel via PacketTunnelProvider +- Monitor tunnel state +- Track last handshake time + +**Key Methods**: +```swift +func startTunnel(config: WireGuardConfig) async throws +func stopTunnel() +func isTunnelUp() -> Bool +func getLastHandshake() -> Date? +``` + +### 4.4 HeartbeatService + +**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 Methods**: +```swift +func startHeartbeatLoop() +func stopHeartbeat() +func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig? +``` + +### 4.5 ProvisioningService + +**Responsibilities**: +- HTTP POST to `/api/v1/provision` +- Parse encrypted config response +- Decrypt config via CryptoManager +- Store config in Keychain + +**Key Methods**: +```swift +func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig? +func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig? +``` + +### 4.6 CryptoManager + +**Responsibilities**: +- Key derivation: SHA-256(hwid + salt) +- AES-256-GCM decryption +- Secure memory handling + +**Key Methods**: +```swift +func deriveKey(token: String, hwid: String) -> SymmetricKey +func decrypt(encoded: String, key: SymmetricKey) -> Data? +``` + +### 4.7 KeychainStorage + +**Responsibilities**: +- Store WireGuard keys in Keychain +- Store agent config in UserDefaults +- Secure key access + +**Key Methods**: +```swift +func saveConfig(_ config: AgentConfig) +func loadConfig() -> AgentConfig? +func saveWireGuardConfig(_ config: WireGuardConfig) +func loadWireGuardConfig() -> WireGuardConfig? +func clearAll() +``` + +### 4.8 LogStore + +**Responsibilities**: +- Buffer log entries in memory +- Persist to CoreData (optional) +- Provide log stream for UI + +**Key Methods**: +```swift +func add(level: LogLevel, tag: String, message: String) +func clear() +func getLogs() -> [LogEntry] +``` + +## 5. Data Structures + +### 5.1 WireGuardConfig + +```swift +struct WireGuardConfig: Codable { + let deviceId: String + let privateKey: String + let presharedKey: String + let internalIp: String + let serverPub: String + let endpoint: String + let dns: String + let allowedIps: String + let serverWgIp: String + let configHash: String + let forwardsHash: String +} +``` + +### 5.2 AgentConfig + +```swift +struct AgentConfig: Codable { + let serverUrl: String + let registrationToken: String + let isProvisioned: Bool +} +``` + +### 5.3 AgentState + +```swift +enum AgentState: String { + case disconnected = "disconnected" + case connecting = "connecting" + case connected = "connected" + case failed = "failed" +} +``` + +### 5.4 LogEntry + +```swift +struct LogEntry: Identifiable { + let id = UUID() + let timestamp: Date + let level: LogLevel + let tag: String + let message: String +} +``` + +## 6. Error Handling + +### 6.1 Error Types + +```swift +enum AgentError: Error { + case wifiNotConnected + case provisioningFailed(String) + case heartbeatFailed(String) + case cryptoDecryptionFailed + case configInvalid + case tunnelInitFailed + case keychainError(OSStatus) +} +``` + +### 6.2 Recovery Strategy + +| Error | Recovery | +|-------|----------| +| WiFi not connected | Retry 3x, then pause heartbeat | +| HTTP provision fail | Retry 3x, then notify user | +| HTTP heartbeat fail | Log only, continue | +| Crypto decrypt fail | Clear config, re-provision | +| Tunnel init fail | Retry 3x, then notify user | +| Keychain error | Use UserDefaults fallback | + +## 7. Background Tasks + +### 7.1 BGAppRefreshTask + +iOS doesn't have boot receiver like Android. Use Background Tasks: + +```swift +// Register task +BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) { + task in + self.handleBackgroundRefresh(task: task as! BGAppRefreshTask) +} + +// Schedule task +let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh") +request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes +try BGTaskScheduler.shared.submit(request) +``` + +### 7.2 Background Modes + +Add to `Info.plist`: +```xml +UIBackgroundModes + + fetch + processing + vpn-api + +``` + +## 8. Security Considerations + +### 8.1 Key Storage + +- WireGuard private keys stored in iOS Keychain (hardware-backed) +- Non-sensitive config in UserDefaults +- Never log keys or tokens + +### 8.2 TLS + +- Server communication over HTTPS (TLS 1.2+) +- Use `URLSession` with default TLS settings +- Certificate pinning (optional, future) + +### 8.3 Hardware ID + +- Use `identifierForVendor` for device identification +- Persists across app reinstalls until device factory reset +- Used for key derivation (HWID + salt) + +## 9. Testing Strategy + +### 9.1 Unit Tests + +- Crypto: Decrypt known ciphertext with known key +- Config: Keychain 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 UI Tests + +- VPN toggle interaction +- Tab switching +- Settings save/load + +### 9.4 Device Tests + +- iPhone 12+ running iOS 16+ +- NetworkExtension background operation +- Keychain access in background + +## 10. Future Enhancements + +### 10.1 Phase 2 + +- iPadOS support +- Widget for quick status +- Shortcuts integration +- Siri commands + +### 10.2 Phase 3 + +- macOS Catalyst support +- Apple Watch companion +- CarPlay integration + +## 11. References + +- [Apple NetworkExtension Documentation](https://developer.apple.com/documentation/networkextension) +- [WireGuard for iOS](https://github.com/WireGuard/wireguard-apple) +- [CryptoKit Documentation](https://developer.apple.com/cryptokit/) +- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks) +- [NexusGuard Server API](../../server-core/docs/) diff --git a/reference/ios-agent/docs/UI_DESIGN.md b/reference/ios-agent/docs/UI_DESIGN.md new file mode 100644 index 0000000..72c860c --- /dev/null +++ b/reference/ios-agent/docs/UI_DESIGN.md @@ -0,0 +1,506 @@ +# iOS Agent — UI Design Specs + +**Status**: DESIGN ONLY +**Mirror**: Android Agent (`apps/android-agent/`) + +## Overview + +This document defines the UI design for the iOS WireGuard agent. It mirrors the Android agent's design, colors, layout, and user experience. + +## Color Palette + +### Status Colors + +| Name | Hex | RGB | Usage | +|------|-----|-----|-------| +| `status_disconnected` | `#FFB0B0B0` | (176, 176, 176) | Disconnected state | +| `status_connecting` | `#FFFFD93D` | (255, 217, 61) | Connecting state | +| `status_connected` | `#FF4CAF50` | (76, 175, 80) | Connected state | +| `status_failed` | `#FFFF6B6B` | (255, 107, 107) | Failed state | + +### UI Colors + +| Name | Hex | RGB | Usage | +|------|-----|-----|-------| +| `card_bg` | `#FF1A1A2E` | (26, 26, 46) | Card background | +| `card_bg_dark` | `#FF0F0F1A` | (15, 15, 26) | Badge background | +| `text_primary` | `#FFFFFFFF` | (255, 255, 255) | Primary text | +| `text_secondary` | `#FFB0B0B0` | (176, 176, 176) | Secondary text | +| `accent_green` | `#FF4CAF50` | (76, 175, 80) | Accent/highlight | +| `accent_blue` | `#FF2196F3` | (33, 150, 243) | Links/actions | +| `border_color` | `#FF2A2A3E` | (42, 42, 62) | Card borders | + +### Log Colors + +| Level | Hex | RGB | +|-------|-----|-----| +| `DEBUG` | `#FF6B6B6B` | (107, 107, 107) | +| `INFO` | `#FFB0B0B0` | (176, 176, 176) | +| `WARN` | `#FFFFD93D` | (255, 217, 61) | +| `ERROR` | `#FFFF6B6B` | (255, 107, 107) | + +## Color Definitions (Swift) + +```swift +import SwiftUI + +extension Color { + // Status colors + static let statusDisconnected = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 + static let statusConnecting = Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D + static let statusConnected = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50 + static let statusFailed = Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B + + // UI colors + static let cardBackground = Color(red: 0.10, green: 0.10, blue: 0.18) // #1A1A2E + static let cardBackgroundDark = Color(red: 0.06, green: 0.06, blue: 0.10) // #0F0F1A + static let textPrimary = Color.white // #FFFFFF + static let textSecondary = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 + static let accentGreen = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50 + static let accentBlue = Color(red: 0.13, green: 0.59, blue: 0.95) // #2196F3 + static let borderColor = Color(red: 0.16, green: 0.16, blue: 0.24) // #2A2A3E +} +``` + +## Layout Structure + +### Main Screen (ContentView) + +``` +┌─────────────────────────────────────┐ +│ NexusGuard [⚙️ Settings]│ +├─────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ ● STATUS: Connected │ │ +│ │ │ │ +│ │ IP Address 10.172.21.2 │ │ +│ │ Admin Web https://... │ │ +│ │ Device ID abc-123-... │ │ +│ │ Handshake 14:32:05 │ │ +│ │ Allowed IPs 0.0.0.0/0 │ │ +│ │ Transport HTTPS │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ [Status] [Ports] [Log] │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Port Forwards: 2 │ │ +│ │ TCP :8080 → 10.172.21.2:80│ │ +│ │ UDP :53 → 10.172.21.2:53│ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ [Copy Log] [Clear Log] │ │ +│ │ │ │ +│ │ 14:32:05 [INFO] Agent... │ │ +│ │ 14:32:05 [INFO] Tunnel... │ │ +│ │ 14:32:05 [WARN] Retry... │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ ══════════════════ │ │ +│ │ VPN Toggle │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────┘ +``` + +### Settings Screen (SettingsView) + +``` +┌─────────────────────────────────────┐ +│ ← Settings │ +├─────────────────────────────────────┤ +│ │ +│ Server URL │ +│ ┌─────────────────────────────┐ │ +│ │ https://api-nexus.datadunia.com│ │ +│ └─────────────────────────────┘ │ +│ │ +│ Registration Token │ +│ ┌─────────────────────────────┐ │ +│ │ •••••••••••••••• │ │ +│ └─────────────────────────────┘ │ +│ │ +│ Auto-Start on Boot │ +│ ┌─────────────────────────────┐ │ +│ │ [Toggle] │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Save Settings │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Export Config │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Import Config │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Paste Config │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────┘ +``` + +## Component Specs + +### Status Card + +```swift +struct StatusCard: View { + let state: AgentState + let config: WireGuardConfig? + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Status indicator + HStack { + Circle() + .fill(statusColor) + .frame(width: 12, height: 12) + Text(statusText) + .font(.headline) + .foregroundColor(statusColor) + } + + // Info fields + if state == .connected { + InfoRow(label: "IP Address", value: config?.internalIp ?? "-") + InfoRow(label: "Admin Web", value: "https://api-nexus.datadunia.com") + InfoRow(label: "Device ID", value: config?.deviceId ?? "-") + InfoRow(label: "Handshake", value: lastHandshake ?? "-") + InfoRow(label: "Allowed IPs", value: config?.allowedIps ?? "-") + InfoRow(label: "Transport", value: transport) + } + } + .padding() + .background(Color.cardBackground) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.borderColor, lineWidth: 1) + ) + } + + private var statusColor: Color { + switch state { + case .disconnected: return .statusDisconnected + case .connecting: return .statusConnecting + case .connected: return .statusConnected + case .failed: return .statusFailed + } + } +} +``` + +### Info Row + +```swift +struct InfoRow: View { + let label: String + let value: String + + var body: some View { + HStack { + Text(label) + .font(.subheadline) + .foregroundColor(.textSecondary) + Spacer() + Text(value) + .font(.subheadline) + .foregroundColor(.textPrimary) + .lineLimit(1) + } + } +} +``` + +### Port Forward Row + +```swift +struct PortForwardRow: View { + let forward: PortForward + + var body: some View { + HStack { + Text(forward.protocol.uppercased()) + .font(.caption) + .fontWeight(.bold) + .foregroundColor(.cardBackgroundDark) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.accentGreen) + .cornerRadius(4) + + Text(":\(forward.publicPort)") + .font(.subheadline) + .foregroundColor(.textPrimary) + .font(.system(.body, design: .monospaced)) + + Spacer() + + Text("\(forward.targetIp):\(forward.targetPort)") + .font(.subheadline) + .foregroundColor(.textSecondary) + .font(.system(.body, design: .monospaced)) + } + } +} +``` + +### Log Entry + +```swift +struct LogEntryView: View { + let entry: LogEntry + + var body: some View { + HStack(alignment: .top) { + Text(entry.timestamp, style: .time) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(entry.level.color) + + Text("[\(entry.level.rawValue)]") + .font(.system(.caption, design: .monospaced)) + .foregroundColor(entry.level.color) + + Text("\(entry.tag): \(entry.message)") + .font(.system(.caption, design: .monospaced)) + .foregroundColor(entry.level.color) + } + } +} + +extension LogLevel { + var color: Color { + switch self { + case .debug: return Color(red: 0.42, green: 0.42, blue: 0.42) // #6B6B6B + case .info: return Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 + case .warn: return Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D + case .error: return Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B + } + } +} +``` + +### VPN Toggle + +```swift +struct VPNToggle: View { + @Binding var isOn: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + RoundedRectangle(cornerRadius: 25) + .fill(isOn ? Color.accentGreen : Color.statusDisconnected) + .frame(height: 50) + .overlay( + HStack { + Circle() + .fill(Color.white) + .frame(width: 40, height: 40) + .offset(x: isOn ? 40 : -40) + Spacer() + } + ) + } + .buttonStyle(PlainButtonStyle()) + } +} +``` + +## Tab Layout + +```swift +struct TabBar: View { + @Binding var selectedTab: Int + + var body: some View { + HStack(spacing: 0) { + TabButton(title: "Status", isSelected: selectedTab == 0) { + selectedTab = 0 + } + TabButton(title: "Ports", isSelected: selectedTab == 1) { + selectedTab = 1 + } + TabButton(title: "Log", isSelected: selectedTab == 2) { + selectedTab = 2 + } + } + .background(Color.cardBackground) + .cornerRadius(8) + } +} + +struct TabButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.subheadline) + .foregroundColor(isSelected ? .textPrimary : .textSecondary) + .padding(.vertical, 12) + .frame(maxWidth: .infinity) + .background(isSelected ? Color.accentGreen : Color.clear) + .cornerRadius(8) + } + } +} +``` + +## Typography + +### Font Sizes + +| Element | Size | Weight | Design | +|---------|------|--------|--------| +| Status text | 18pt | Semibold | Default | +| Info label | 14pt | Regular | Default | +| Info value | 14pt | Regular | Monospaced | +| Tab text | 14pt | Medium | Default | +| Log text | 12pt | Regular | Monospaced | +| Badge text | 10pt | Bold | Default | + +### Font Definitions + +```swift +extension Font { + static let statusText = Font.headline + static let infoLabel = Font.subheadline + static let infoValue = Font.subheadline.monospaced() + static let tabText = Font.subheadline.weight(.medium) + static let logText = Font.caption.monospaced() + static let badgeText = Font.caption2.bold() +} +``` + +## Spacing + +| Element | Value | +|---------|-------| +| Card padding | 16pt | +| Card corner radius | 12pt | +| Card border width | 1pt | +| Row spacing | 12pt | +| Badge padding | 8pt horizontal, 4pt vertical | +| Badge corner radius | 4pt | +| Button height | 50pt | +| Button corner radius | 25pt | + +## Animation + +### Status Change + +```swift +withAnimation(.easeInOut(duration: 0.3)) { + state = .connected +} +``` + +### Tab Switch + +```swift +withAnimation(.easeInOut(duration: 0.2)) { + selectedTab = 1 +} +``` + +### VPN Toggle + +```swift +withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) { + isVPNOn.toggle() +} +``` + +## Haptic Feedback + +```swift +// Success +UIImpactFeedbackGenerator(style: .medium).impactOccurred() + +// Error +UINotificationFeedbackGenerator().notificationOccurred(.error) + +// Selection +UISelectionFeedbackGenerator().selectionChanged() +``` + +## Dark Mode + +iOS agent uses dark mode only (matches Android design). Force dark mode: + +```swift +@main +struct NexusGuardApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .preferredColorScheme(.dark) + } + } +} +``` + +## Responsive Design + +### iPhone SE + +- Compact layout +- Smaller font sizes +- Reduced padding + +### iPhone 14/15 + +- Standard layout +- Default font sizes +- Standard padding + +### iPhone 14/15 Pro Max + +- Expanded layout +- Larger font sizes +- Increased padding + +## Accessibility + +### VoiceOver + +```swift +Text("Connected") + .accessibilityLabel("VPN Status: Connected") + .accessibilityHint("Double tap to toggle VPN") + +InfoRow(label: "IP Address", value: "10.172.21.2") + .accessibilityElement(children: .combine) + .accessibilityLabel("IP Address: 10.172.21.2") +``` + +### Dynamic Type + +```swift +Text("Status") + .font(.headline) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) +``` + +### Reduce Motion + +```swift +@Environment(\.accessibilityReduceMotion) var reduceMotion + +withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) { + state = .connected +} +```