docs: add ESP32 and iOS agent architecture reference
- reference/esp32-agent/: DESIGN, HARDWARE, API_COMPAT, ARCHITECTURE, README - reference/ios-agent/: DESIGN, API_COMPAT, UI_DESIGN, ARCHITECTURE, README - Both mirror Android agent architecture, UI design, and heartbeat flow - Architecture-only (no code) — avoids submodule conflicts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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/)
|
||||||
@@ -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/)
|
||||||
@@ -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/)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
<string>vpn-api</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -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
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
<string>vpn-api</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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/)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user