feat: add ESP32 embedded agent + namespace VRF reference docs + update android-agent submodule
- apps/device-agent-embedded/: ESP32 WireGuard agent (C, ESP-IDF, mbedTLS) - reference/namespace-vrf/: Linux namespace/VRF network isolation docs - apps/android-agent: VPN permission check, StartResult sealed class
This commit is contained in:
+1
-1
Submodule apps/android-agent updated: ce70c3e197...8faf87a71c
@@ -0,0 +1,22 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# ESP-IDF
|
||||
dependencies.lock
|
||||
managed_components/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,134 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-06-26
|
||||
**Repository:** `nexus-agent-embedded`
|
||||
|
||||
## OVERVIEW
|
||||
NexusGuard Embedded Agent: ESP32-based WireGuard client for IoT/edge devices. Runs on FreeRTOS + ESP-IDF, connects to NexusGuard Server Core via HTTP provisioning + heartbeat, manages WireGuard tunnel via lwIP stack.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
nexus-agent-embedded/
|
||||
├── main/
|
||||
│ ├── main.c # Entry point, initialization
|
||||
│ ├── provision.c/.h # HTTP provisioning (POST /api/v1/provision)
|
||||
│ ├── heartbeat.c/.h # HTTP heartbeat loop (POST /api/v1/heartbeat)
|
||||
│ ├── tunnel.c/.h # WireGuard tunnel management via lwIP
|
||||
│ ├── crypto.c/.h # AES-256-GCM decrypt (mbedtls)
|
||||
│ ├── config.c/.h # NVS config storage
|
||||
│ └── wifi.c/.h # WiFi STA management
|
||||
├── components/
|
||||
│ └── wireguard-esp32/ # WireGuard library (lwIP-based)
|
||||
├── docs/
|
||||
│ ├── DESIGN.md # Technical design document
|
||||
│ ├── API_COMPAT.md # Server API compatibility
|
||||
│ └── HARDWARE.md # ESP32/LW840X hardware reference
|
||||
├── CMakeLists.txt # ESP-IDF build system
|
||||
├── sdkconfig.defaults # ESP-IDF configuration
|
||||
├── AGENTS.md # This file
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API PROTOCOL (Shared with Server)
|
||||
|
||||
### Provisioning
|
||||
- **Endpoint**: `POST /api/v1/provision`
|
||||
- **Request**: `{ "token": "string", "hwid": "string" }`
|
||||
- **Response**: `{ "encrypted_config": "base64-bytes" }`
|
||||
- **ConfigPayload** (decrypted): device_id, private_key, preshared_key, internal_ip, server_pub, endpoint, dns, allowed_ips, server_wg_ip
|
||||
|
||||
### Heartbeat
|
||||
- **Endpoint**: `POST /api/v1/heartbeat`
|
||||
- **Request**: `{ "device_id": "UUID", "status": "connected|recovering|stopped", "tunnel_up": bool, "last_handshake": "ISO8601" }`
|
||||
- **Response**: Full config sync (same fields as ConfigPayload + forwards_hash)
|
||||
- **Interval**: 30 seconds
|
||||
|
||||
### Crypto
|
||||
- **Algorithm**: AES-256-GCM
|
||||
- **Key Derivation**: SHA256(hwid + salt)
|
||||
- **Format**: nonce (12 bytes) + ciphertext
|
||||
- **Implementation**: mbedtls (built-in ESP-IDF)
|
||||
|
||||
## HARDWARE PLATFORM
|
||||
|
||||
### Target: ESP32 (LW840X)
|
||||
- **MCU**: ESP32 Xtensa LX7 dual-core
|
||||
- **RAM**: 320KB SRAM
|
||||
- **Flash**: 4MB
|
||||
- **WiFi**: 802.11 b/g/n
|
||||
- **OS**: FreeRTOS via ESP-IDF
|
||||
- **Network Stack**: lwIP
|
||||
|
||||
### Build Toolchain
|
||||
- **ESP-IDF**: v5.2+
|
||||
- **Compiler**: Xtensa GCC
|
||||
- **Build System**: CMake
|
||||
- **Debug**: JTAG + OpenOCD
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
### Code Style
|
||||
- C99 standard
|
||||
- Snake_case for functions and variables
|
||||
- UPPER_SNAKE_CASE for macros and constants
|
||||
- Doxygen-style comments for public API
|
||||
- Error handling: return esp_err_t or NULL on failure
|
||||
|
||||
### Memory Management
|
||||
- Use `heap_caps_malloc(MALLOC_CAP_SPIRAM)` for large buffers
|
||||
- Free all allocated memory in error paths
|
||||
- No static buffers > 1KB on stack
|
||||
|
||||
### Network
|
||||
- Use `esp_http_client` for HTTP requests
|
||||
- Timeout: 10s connect, 30s read
|
||||
- Retry: 3 attempts with exponential backoff
|
||||
- TLS: Skip verify for now (internal network only)
|
||||
|
||||
### Storage
|
||||
- NVS namespace: "nexusguard"
|
||||
- Keys: "device_id", "private_key", "preshared_key", "internal_ip", "server_pub", "endpoint", "dns", "allowed_ips"
|
||||
- Max key size: 15 chars (NVS limit)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **NEVER** store WireGuard keys in plaintext on flash (always encrypted via NVS)
|
||||
- **NEVER** use `printf` for sensitive data (keys, tokens)
|
||||
- **NEVER** hardcode WiFi credentials (use menuconfig or provisioning)
|
||||
- **NEVER** block main task for > 100ms (use timers or tasks)
|
||||
- **NEVER** use `malloc()` in interrupt context
|
||||
|
||||
## COMMANDS
|
||||
|
||||
```bash
|
||||
# Build
|
||||
idf.py set-target esp32
|
||||
idf.py build
|
||||
|
||||
# Flash & Monitor
|
||||
idf.py -p /dev/ttyUSB0 flash monitor
|
||||
|
||||
# Clean
|
||||
idf.py fullclean
|
||||
|
||||
# Menuconfig
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
## DEPENDENCIES
|
||||
|
||||
- **ESP-IDF**: v5.2+ (framework)
|
||||
- **mbedtls**: AES-256-GCM, SHA-256 (built-in)
|
||||
- **lwIP**: TCP/IP stack (built-in)
|
||||
- **FreeRTOS**: RTOS (built-in)
|
||||
- **wireguard-esp32**: WireGuard tunnel library (component)
|
||||
|
||||
## NOTES
|
||||
|
||||
- ESP32 uses FreeRTOS, NOT Zephyr RTOS
|
||||
- lwIP is the network stack, NOT kernel networking
|
||||
- WireGuard runs in userspace via lwIP socket API
|
||||
- Config is stored in NVS (flash), not filesystem
|
||||
- No gRPC support (too heavy for ESP32) — HTTP only
|
||||
- No port forwarding (userspace agent only)
|
||||
- Agent version: 1.0.0 (initial release)
|
||||
@@ -0,0 +1,40 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# ESP-IDF component registration
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"provision.c"
|
||||
"heartbeat.c"
|
||||
"tunnel.c"
|
||||
"crypto.c"
|
||||
"config.c"
|
||||
"wifi.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES
|
||||
esp_wifi
|
||||
esp_event
|
||||
esp_netif
|
||||
nvs_flash
|
||||
esp_http_client
|
||||
esp_tls
|
||||
mbedtls
|
||||
json
|
||||
lwip
|
||||
)
|
||||
|
||||
# WireGuard component (external)
|
||||
# Add wireguard-esp32 as component
|
||||
set(COMPONENTS_DIR ${CMAKE_CURRENT_LIST_DIR}/../components)
|
||||
if(EXISTS ${COMPONENTS_DIR}/wireguard-esp32)
|
||||
set(EXTRA_COMPONENT_DIRS ${COMPONENTS_DIR}/wireguard-esp32)
|
||||
endif()
|
||||
|
||||
# Build flags
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror)
|
||||
|
||||
# Version
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE
|
||||
AGENT_VERSION="1.0.0"
|
||||
BUILD_DATE="${BUILD_DATE}"
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
menu "NexusGuard Agent Configuration"
|
||||
|
||||
config NEXUS_SERVER_URL
|
||||
string "Server URL"
|
||||
default "https://api-nexus.datadunia.com"
|
||||
help
|
||||
NexusGuard Server Core API URL.
|
||||
Example: https://api-nexus.datadunia.com
|
||||
|
||||
config NEXUS_REG_TOKEN
|
||||
string "Registration Token"
|
||||
default ""
|
||||
help
|
||||
Single-use registration token for initial provisioning.
|
||||
Obtain from server admin dashboard.
|
||||
|
||||
config NEXUS_SERVER_SALT
|
||||
string "Server Salt"
|
||||
default ""
|
||||
help
|
||||
Server salt for AES-256-GCM key derivation.
|
||||
Must match SERVER_SALT on server.
|
||||
|
||||
config NEXUS_WIFI_SSID
|
||||
string "WiFi SSID"
|
||||
default ""
|
||||
help
|
||||
WiFi network name (SSID).
|
||||
|
||||
config NEXUS_WIFI_PASSWORD
|
||||
string "WiFi Password"
|
||||
default ""
|
||||
help
|
||||
WiFi network password.
|
||||
|
||||
config NEXUS_AGENT_VERSION
|
||||
string "Agent Version"
|
||||
default "1.0.0"
|
||||
help
|
||||
Embedded agent version string.
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,126 @@
|
||||
# NexusGuard Embedded Agent
|
||||
|
||||
ESP32-based WireGuard agent for IoT/edge devices. Connects to NexusGuard Server Core via HTTP provisioning + heartbeat, manages WireGuard tunnel via lwIP stack.
|
||||
|
||||
## Overview
|
||||
|
||||
This agent runs on ESP32 (LW840X and compatible) with FreeRTOS + ESP-IDF. It provides:
|
||||
|
||||
- **Zero-Trust VPN Tunnel**: WireGuard over lwIP
|
||||
- **Auto-Provisioning**: HTTP-based key exchange with server
|
||||
- **Heartbeat Monitoring**: Periodic status reports + config sync
|
||||
- **NVS Config Persistence**: WireGuard keys stored in flash (never disk)
|
||||
- **Independent Build**: No dependency on main NexusGuard repo
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
| Component | Specification |
|
||||
|-----------|---------------|
|
||||
| MCU | ESP32 (Xtensa LX7) or ESP32-S3 |
|
||||
| RAM | ≥ 320KB SRAM |
|
||||
| Flash | ≥ 4MB |
|
||||
| WiFi | 802.11 b/g/n |
|
||||
| antenna | U.FL or PCB trace |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install ESP-IDF
|
||||
|
||||
```bash
|
||||
# Follow official guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/
|
||||
# ESP-IDF v5.2+ required
|
||||
```
|
||||
|
||||
### 2. Configure Server
|
||||
|
||||
```bash
|
||||
# Set server URL, registration token, and WiFi credentials
|
||||
idf.py menuconfig
|
||||
# Navigate to: Component config → NexusGuard Agent
|
||||
```
|
||||
|
||||
Or edit `sdkconfig.defaults` directly:
|
||||
|
||||
```
|
||||
CONFIG_NEXUS_SERVER_URL="https://api-nexus.datadunia.com"
|
||||
CONFIG_NEXUS_REG_TOKEN="your-registration-token"
|
||||
CONFIG_NEXUS_SERVER_SALT="your-server-salt"
|
||||
CONFIG_NEXUS_WIFI_SSID="your-wifi-ssid"
|
||||
CONFIG_NEXUS_WIFI_PASSWORD="your-wifi-password"
|
||||
```
|
||||
|
||||
### 3. Build & Flash
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash monitor
|
||||
```
|
||||
|
||||
## Server Configuration
|
||||
|
||||
The agent requires the following server configuration:
|
||||
|
||||
| Parameter | Description | Where to Find |
|
||||
|-----------|-------------|---------------|
|
||||
| Server URL | API endpoint | `https://api-nexus.datadunia.com` |
|
||||
| Registration Token | Single-use provisioning token | Server admin dashboard |
|
||||
| Server Salt | AES-256-GCM key derivation | Server `.env` file (`SERVER_SALT`) |
|
||||
|
||||
### Provisioning Flow
|
||||
|
||||
1. Create device in server dashboard
|
||||
2. Copy registration token
|
||||
3. Configure agent with token (menuconfig or NVS)
|
||||
4. Agent boots → connects WiFi → provisions → builds tunnel
|
||||
|
||||
### Heartbeat
|
||||
|
||||
Agent sends heartbeat every 30 seconds:
|
||||
|
||||
```json
|
||||
POST /api/v1/heartbeat
|
||||
{
|
||||
"device_id": "uuid",
|
||||
"status": "connected",
|
||||
"tunnel_up": true,
|
||||
"last_handshake": "2026-06-26T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Server responds with config sync (detects changes, triggers rebuild).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
nexus-agent-embedded/
|
||||
├── main/
|
||||
│ ├── main.c # Entry point
|
||||
│ ├── provision.c # HTTP provisioning
|
||||
│ ├── heartbeat.c # HTTP heartbeat loop
|
||||
│ ├── tunnel.c # WireGuard via lwIP
|
||||
│ ├── crypto.c # AES-256-GCM decrypt
|
||||
│ ├── config.c # NVS config storage
|
||||
│ └── wifi.c # WiFi management
|
||||
├── components/
|
||||
│ └── wireguard-esp32/ # WireGuard library
|
||||
├── docs/
|
||||
│ ├── DESIGN.md # Technical design
|
||||
│ ├── API_COMPAT.md # Server API compatibility
|
||||
│ └── HARDWARE.md # Hardware reference
|
||||
├── CMakeLists.txt
|
||||
├── Kconfig # Menuconfig options
|
||||
├── sdkconfig.defaults
|
||||
├── AGENTS.md # AI agent knowledge base
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Technical Design](docs/DESIGN.md)
|
||||
- [API Compatibility](docs/API_COMPAT.md)
|
||||
- [Hardware Reference](docs/HARDWARE.md)
|
||||
|
||||
## 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,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,264 @@
|
||||
/**
|
||||
* @file config.c
|
||||
* @brief NVS Configuration Management
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "config.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "nexus_config";
|
||||
|
||||
/* NVS namespace */
|
||||
static const char *NVS_NAMESPACE = "nexusguard";
|
||||
|
||||
/* NVS keys (all < 15 chars per NVS limit) */
|
||||
static const char *KEY_DEVICE_ID = "device_id";
|
||||
static const char *KEY_PRIVATE_KEY = "private_key";
|
||||
static const char *KEY_PRESHARED_KEY = "preshared_key";
|
||||
static const char *KEY_INTERNAL_IP = "internal_ip";
|
||||
static const char *KEY_SERVER_PUB = "server_pub";
|
||||
static const char *KEY_ENDPOINT = "endpoint";
|
||||
static const char *KEY_DNS = "dns";
|
||||
static const char *KEY_ALLOWED_IPS = "allowed_ips";
|
||||
static const char *KEY_SERVER_WG_IP = "server_wgip";
|
||||
static const char *KEY_CONFIG_HASH = "config_hash";
|
||||
|
||||
/* NVS handle (open once, reuse) */
|
||||
static nvs_handle_t s_nvs_handle = 0;
|
||||
static bool s_nvs_open = false;
|
||||
|
||||
/**
|
||||
* @brief Helper: write string to NVS
|
||||
*/
|
||||
static esp_err_t nvs_write_str(const char *key, const char *value)
|
||||
{
|
||||
if (!s_nvs_open) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
esp_err_t err = nvs_set_str(s_nvs_handle, key, value);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write NVS key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper: read string from NVS into buffer
|
||||
*/
|
||||
static esp_err_t nvs_read_str(const char *key, char *buf, size_t buf_len)
|
||||
{
|
||||
if (!s_nvs_open) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_str(s_nvs_handle, key, NULL, &required_size);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (required_size > buf_len) {
|
||||
ESP_LOGE(TAG, "NVS key '%s' value too large (%zu > %zu)", key, required_size, buf_len);
|
||||
return ESP_ERR_NVS_INVALID_LENGTH;
|
||||
}
|
||||
err = nvs_get_str(s_nvs_handle, key, buf, &buf_len);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t config_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing NVS config module...");
|
||||
|
||||
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &s_nvs_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open NVS namespace '%s': %s", NVS_NAMESPACE, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
s_nvs_open = true;
|
||||
|
||||
ESP_LOGI(TAG, "NVS config module initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t config_save(const wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Saving config for device: %s", config->device_id);
|
||||
|
||||
esp_err_t err;
|
||||
esp_err_t final_err = ESP_OK;
|
||||
|
||||
err = nvs_write_str(KEY_DEVICE_ID, config->device_id);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_PRIVATE_KEY, config->private_key);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_PRESHARED_KEY, config->preshared_key);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_INTERNAL_IP, config->internal_ip);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_SERVER_PUB, config->server_pub);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_ENDPOINT, config->endpoint);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_DNS, config->dns);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_ALLOWED_IPS, config->allowed_ips);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_SERVER_WG_IP, config->server_wg_ip);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_write_str(KEY_CONFIG_HASH, config->config_hash);
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
if (final_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to save config");
|
||||
return final_err;
|
||||
}
|
||||
|
||||
/* Commit to flash */
|
||||
err = nvs_commit(s_nvs_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config saved successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t config_load(wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Zero out the struct first */
|
||||
memset(config, 0, sizeof(wg_config_t));
|
||||
|
||||
ESP_LOGI(TAG, "Loading config from NVS...");
|
||||
|
||||
esp_err_t err;
|
||||
esp_err_t final_err = ESP_OK;
|
||||
|
||||
err = nvs_read_str(KEY_DEVICE_ID, config->device_id, sizeof(config->device_id));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "No device_id in NVS (first boot?)");
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_read_str(KEY_PRIVATE_KEY, config->private_key, sizeof(config->private_key));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_PRESHARED_KEY, config->preshared_key, sizeof(config->preshared_key));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_INTERNAL_IP, config->internal_ip, sizeof(config->internal_ip));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_SERVER_PUB, config->server_pub, sizeof(config->server_pub));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_ENDPOINT, config->endpoint, sizeof(config->endpoint));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_DNS, config->dns, sizeof(config->dns));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_ALLOWED_IPS, config->allowed_ips, sizeof(config->allowed_ips));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_SERVER_WG_IP, config->server_wg_ip, sizeof(config->server_wg_ip));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
err = nvs_read_str(KEY_CONFIG_HASH, config->config_hash, sizeof(config->config_hash));
|
||||
if (err != ESP_OK) final_err = err;
|
||||
|
||||
if (final_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Config loaded with errors");
|
||||
return final_err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config loaded successfully for device: %s", config->device_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t config_clear(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Clearing all NexusGuard NVS keys...");
|
||||
|
||||
if (!s_nvs_open) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t err;
|
||||
esp_err_t final_err = ESP_OK;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_DEVICE_ID);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_PRIVATE_KEY);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_PRESHARED_KEY);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_INTERNAL_IP);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_SERVER_PUB);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_ENDPOINT);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_DNS);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_ALLOWED_IPS);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_SERVER_WG_IP);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
err = nvs_erase_key(s_nvs_handle, KEY_CONFIG_HASH);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
|
||||
|
||||
/* Commit erases to flash */
|
||||
err = nvs_commit(s_nvs_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit NVS erase: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config cleared");
|
||||
return final_err;
|
||||
}
|
||||
|
||||
bool config_is_valid(const wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* device_id and private_key must be non-empty */
|
||||
if (strlen(config->device_id) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (strlen(config->private_key) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @file config.h
|
||||
* @brief NVS Configuration Management
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
/* WireGuard configuration structure */
|
||||
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 */
|
||||
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;
|
||||
|
||||
/**
|
||||
* @brief Initialize configuration module
|
||||
*/
|
||||
esp_err_t config_init(void);
|
||||
|
||||
/**
|
||||
* @brief Save configuration to NVS
|
||||
*/
|
||||
esp_err_t config_save(const wg_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Load configuration from NVS
|
||||
*/
|
||||
esp_err_t config_load(wg_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Clear configuration from NVS
|
||||
*/
|
||||
esp_err_t config_clear(void);
|
||||
|
||||
/**
|
||||
* @brief Check if configuration is valid
|
||||
*/
|
||||
bool config_is_valid(const wg_config_t *config);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @file crypto.c
|
||||
* @brief AES-256-GCM Crypto Operations (mbedtls)
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "crypto.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "mbedtls/sha256.h"
|
||||
#include "mbedtls/cipher.h"
|
||||
#include "mbedtls/error.h"
|
||||
|
||||
static const char *TAG = "nexus_crypto";
|
||||
|
||||
/* AES-256-GCM constants */
|
||||
#define GCM_NONCE_SIZE 12
|
||||
#define GCM_TAG_SIZE 16
|
||||
#define KEY_SIZE 32
|
||||
|
||||
esp_err_t crypto_init(void)
|
||||
{
|
||||
/* mbedtls auto-initializes on first use, nothing needed */
|
||||
ESP_LOGI(TAG, "Crypto module initialized (mbedtls)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t crypto_derive_key(const char *hwid, const char *salt, uint8_t key[32])
|
||||
{
|
||||
if (hwid == NULL || salt == NULL || key == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Build input: hwid + salt (concatenation, NOT HMAC) */
|
||||
size_t hwid_len = strlen(hwid);
|
||||
size_t salt_len = strlen(salt);
|
||||
size_t input_len = hwid_len + salt_len;
|
||||
|
||||
char *input = malloc(input_len);
|
||||
if (input == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate key derivation input");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
memcpy(input, hwid, hwid_len);
|
||||
memcpy(input + hwid_len, salt, salt_len);
|
||||
|
||||
/* SHA-256 hash */
|
||||
int ret = mbedtls_sha256((const unsigned char *)input, input_len, key, 0);
|
||||
free(input);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "SHA-256 failed: -0x%04x", -ret);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Key derived from HWID+salt");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t key[32], uint8_t **plaintext,
|
||||
size_t *plaintext_len)
|
||||
{
|
||||
if (ciphertext == NULL || key == NULL || plaintext == NULL || plaintext_len == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Minimum: nonce(12) + tag(16) = 28 bytes minimum */
|
||||
if (len < GCM_NONCE_SIZE + GCM_TAG_SIZE) {
|
||||
ESP_LOGE(TAG, "Ciphertext too short: %zu bytes (min %d)", len, GCM_NONCE_SIZE + GCM_TAG_SIZE);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Split: nonce(12) + encrypted_data + tag(16) */
|
||||
const uint8_t *nonce = ciphertext;
|
||||
const uint8_t *encrypted = ciphertext + GCM_NONCE_SIZE;
|
||||
size_t encrypted_len = len - GCM_NONCE_SIZE - GCM_TAG_SIZE;
|
||||
const uint8_t *tag = ciphertext + len - GCM_TAG_SIZE;
|
||||
|
||||
/* Allocate output buffer */
|
||||
uint8_t *out = malloc(encrypted_len + 1); /* +1 for null terminator (JSON payload) */
|
||||
if (out == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate plaintext buffer");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Initialize mbedtls cipher context */
|
||||
mbedtls_cipher_context_t ctx;
|
||||
mbedtls_cipher_init(&ctx);
|
||||
|
||||
ret = mbedtls_cipher_setup(&ctx, mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_256_GCM));
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Cipher setup failed: -0x%04x", -ret);
|
||||
mbedtls_cipher_free(&ctx);
|
||||
free(out);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = mbedtls_cipher_setkey(&ctx, key, KEY_SIZE * 8, MBEDTLS_DECRYPT);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Cipher setkey failed: -0x%04x", -ret);
|
||||
mbedtls_cipher_free(&ctx);
|
||||
free(out);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = mbedtls_cipher_set_iv(&ctx, nonce, GCM_NONCE_SIZE);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Cipher setiv failed: -0x%04x", -ret);
|
||||
mbedtls_cipher_free(&ctx);
|
||||
free(out);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Start decrypt */
|
||||
ret = mbedtls_cipher_auth_decrypt(&ctx, tag, GCM_TAG_SIZE,
|
||||
NULL, 0,
|
||||
encrypted, encrypted_len,
|
||||
out, plaintext_len);
|
||||
mbedtls_cipher_free(&ctx);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "AES-256-GCM decrypt failed: -0x%04x", -ret);
|
||||
free(out);
|
||||
return ESP_ERR_AUTH_CRYPTO;
|
||||
}
|
||||
|
||||
/* Null-terminate for JSON parsing */
|
||||
out[*plaintext_len] = '\0';
|
||||
*plaintext = out;
|
||||
|
||||
ESP_LOGI(TAG, "Decrypted %zu bytes", *plaintext_len);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file crypto.h
|
||||
* @brief AES-256-GCM Crypto Operations
|
||||
*/
|
||||
|
||||
#ifndef CRYPTO_H
|
||||
#define CRYPTO_H
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Initialize crypto module
|
||||
*/
|
||||
esp_err_t crypto_init(void);
|
||||
|
||||
/**
|
||||
* @brief Derive key from HWID + salt
|
||||
*
|
||||
* @param hwid Hardware ID
|
||||
* @param salt Salt string
|
||||
* @param key Output 32-byte key
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t crypto_derive_key(const char *hwid, const char *salt, uint8_t key[32]);
|
||||
|
||||
/**
|
||||
* @brief Decrypt AES-256-GCM ciphertext
|
||||
*
|
||||
* @param ciphertext Input ciphertext (nonce + encrypted)
|
||||
* @param len Ciphertext length
|
||||
* @param key 32-byte key
|
||||
* @param plaintext Output plaintext (caller must free)
|
||||
* @param plaintext_len Output plaintext length
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t key[32], uint8_t **plaintext,
|
||||
size_t *plaintext_len);
|
||||
|
||||
#endif /* CRYPTO_H */
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @file heartbeat.c
|
||||
* @brief HTTP Heartbeat Loop
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "heartbeat.h"
|
||||
#include "config.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "nexus_heartbeat";
|
||||
|
||||
/* Retry config */
|
||||
#define MAX_RETRIES 3
|
||||
#define CONNECT_TIMEOUT_MS 10000
|
||||
#define READ_TIMEOUT_MS 30000
|
||||
|
||||
/**
|
||||
* @brief Convert epoch seconds to ISO8601 string
|
||||
* @param epoch Unix timestamp
|
||||
* @param buf Output buffer (at least 25 bytes)
|
||||
* @param buf_len Buffer length
|
||||
*/
|
||||
static void epoch_to_iso8601(int64_t epoch, char *buf, size_t buf_len)
|
||||
{
|
||||
time_t t = (time_t)epoch;
|
||||
struct tm tm_info;
|
||||
gmtime_r(&t, &tm_info);
|
||||
strftime(buf, buf_len, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief HTTP event handler for heartbeat
|
||||
*/
|
||||
static esp_err_t heartbeat_event_handler(esp_http_client_event_t *evt)
|
||||
{
|
||||
switch (evt->event_id) {
|
||||
case HTTP_EVENT_ERROR:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
|
||||
break;
|
||||
case HTTP_EVENT_ON_CONNECTED:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
|
||||
break;
|
||||
case HTTP_EVENT_ON_HEADER:
|
||||
break;
|
||||
case HTTP_EVENT_ON_DATA:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
|
||||
break;
|
||||
case HTTP_EVENT_ON_FINISH:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
|
||||
break;
|
||||
case HTTP_EVENT_DISCONNECTED:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper: copy cJSON string to char buffer
|
||||
*/
|
||||
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL) {
|
||||
strncpy(buf, item->valuestring, buf_len - 1);
|
||||
buf[buf_len - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t heartbeat_send(const wg_config_t *config, const char *status,
|
||||
bool tunnel_up, int64_t last_handshake)
|
||||
{
|
||||
if (config == NULL || status == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Convert last_handshake to ISO8601 */
|
||||
char handshake_str[25] = "1970-01-01T00:00:00Z";
|
||||
if (last_handshake > 0) {
|
||||
epoch_to_iso8601(last_handshake, handshake_str, sizeof(handshake_str));
|
||||
}
|
||||
|
||||
/* Build request JSON */
|
||||
cJSON *req_json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(req_json, "device_id", config->device_id);
|
||||
cJSON_AddStringToObject(req_json, "status", status);
|
||||
cJSON_AddBoolToObject(req_json, "tunnel_up", tunnel_up);
|
||||
cJSON_AddStringToObject(req_json, "last_handshake", handshake_str);
|
||||
char *req_body = cJSON_PrintUnformatted(req_json);
|
||||
cJSON_Delete(req_json);
|
||||
|
||||
if (req_body == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create heartbeat JSON");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Build URL — use server_wg_ip if available, else we need external URL
|
||||
* For simplicity, we store server_url elsewhere or use the configured endpoint.
|
||||
* Since we don't have server_url in wg_config_t, we construct from endpoint.
|
||||
* TODO: store server_url in config or pass as param */
|
||||
char url[192];
|
||||
/* Use server_wg_ip to build the heartbeat URL */
|
||||
if (strlen(config->server_wg_ip) > 0) {
|
||||
snprintf(url, sizeof(url), "http://%s:8080/api/v1/heartbeat", config->server_wg_ip);
|
||||
} else {
|
||||
/* Fallback — shouldn't happen in normal operation */
|
||||
ESP_LOGE(TAG, "No server_wg_ip configured");
|
||||
free(req_body);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t final_err = ESP_FAIL;
|
||||
|
||||
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
ESP_LOGD(TAG, "Heartbeat attempt %d/%d...", attempt, MAX_RETRIES);
|
||||
|
||||
esp_http_client_config_t http_config = {
|
||||
.url = url,
|
||||
.event_handler = heartbeat_event_handler,
|
||||
.timeout_ms = CONNECT_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&http_config);
|
||||
if (client == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to init HTTP client");
|
||||
continue;
|
||||
}
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
esp_http_client_set_post_field(client, req_body, strlen(req_body));
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, strlen(req_body));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
int written = esp_http_client_write(client, req_body, strlen(req_body));
|
||||
if (written < 0) {
|
||||
ESP_LOGE(TAG, "HTTP write failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
int content_length = esp_http_client_fetch_headers(client);
|
||||
int status_code = esp_http_client_get_status_code(client);
|
||||
|
||||
if (status_code != 200) {
|
||||
ESP_LOGW(TAG, "Heartbeat failed, HTTP %d", status_code);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Read response body for config sync */
|
||||
if (content_length > 0) {
|
||||
char *response_buf = malloc(content_length + 1);
|
||||
if (response_buf != NULL) {
|
||||
int read_len = esp_http_client_read(client, response_buf, content_length);
|
||||
response_buf[read_len] = '\0';
|
||||
|
||||
/* TODO: Parse config sync fields if config_changed is detected
|
||||
* For now, log the response size */
|
||||
ESP_LOGD(TAG, "Heartbeat response: %d bytes", read_len);
|
||||
|
||||
free(response_buf);
|
||||
}
|
||||
}
|
||||
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
final_err = ESP_OK;
|
||||
break;
|
||||
}
|
||||
|
||||
free(req_body);
|
||||
return final_err;
|
||||
}
|
||||
|
||||
bool heartbeat_config_changed(const char *old_hash, const char *new_hash)
|
||||
{
|
||||
if (old_hash == NULL || new_hash == NULL) {
|
||||
/* If either is NULL, treat as changed */
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Empty new hash means no hash provided — treat as changed */
|
||||
if (strlen(new_hash) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Simple string comparison */
|
||||
return (strcmp(old_hash, new_hash) != 0);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file heartbeat.h
|
||||
* @brief HTTP Heartbeat Loop
|
||||
*/
|
||||
|
||||
#ifndef HEARTBEAT_H
|
||||
#define HEARTBEAT_H
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "config.h"
|
||||
|
||||
/**
|
||||
* @brief Send heartbeat to server
|
||||
*
|
||||
* @param config Device configuration
|
||||
* @param status Device status ("connected", "recovering", "stopped")
|
||||
* @param tunnel_up WireGuard tunnel status
|
||||
* @param last_handshake Last handshake timestamp (epoch seconds)
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t heartbeat_send(const wg_config_t *config, const char *status,
|
||||
bool tunnel_up, int64_t last_handshake);
|
||||
|
||||
/**
|
||||
* @brief Check if config changed based on hash comparison
|
||||
*
|
||||
* @param old_hash Previous config hash
|
||||
* @param new_hash New config hash
|
||||
* @return true if config changed, false otherwise
|
||||
*/
|
||||
bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
|
||||
|
||||
#endif /* HEARTBEAT_H */
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief NexusGuard Embedded Agent - Main Entry Point
|
||||
*
|
||||
* ESP32-based WireGuard agent for IoT/edge devices.
|
||||
* Connects to NexusGuard Server Core via HTTP provisioning + heartbeat.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @date 2026-06-26
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
/* Agent modules */
|
||||
#include "config.h"
|
||||
#include "wifi.h"
|
||||
#include "provision.h"
|
||||
#include "heartbeat.h"
|
||||
#include "tunnel.h"
|
||||
#include "crypto.h"
|
||||
|
||||
static const char *TAG = "nexusguard";
|
||||
|
||||
/* Event bits */
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define PROVISIONED_BIT BIT1
|
||||
#define TUNNEL_UP_BIT BIT2
|
||||
#define HEARTBEAT_OK_BIT BIT3
|
||||
|
||||
static EventGroupHandle_t s_event_group;
|
||||
|
||||
/* Agent configuration */
|
||||
static wg_config_t s_config;
|
||||
static device_state_t s_state = STATE_BOOT;
|
||||
|
||||
/**
|
||||
* @brief Get hardware ID from ESP32 eFuse MAC
|
||||
*/
|
||||
static void get_hardware_id(char *hwid, size_t len)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_efuse_mac_get_default(mac);
|
||||
snprintf(hwid, len, "%02x%02x%02x%02x%02x%02x",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief WiFi event handler
|
||||
*/
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
ESP_LOGW(TAG, "WiFi disconnected, reconnecting...");
|
||||
esp_wifi_connect();
|
||||
xEventGroupClearBits(s_event_group, WIFI_CONNECTED_BIT);
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
xEventGroupSetBits(s_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize WiFi
|
||||
*/
|
||||
static esp_err_t init_wifi(const char *ssid, const char *password)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing WiFi...");
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler, NULL, &instance_any_id));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler, NULL, &instance_got_ip));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
|
||||
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
|
||||
strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "WiFi initialized, connecting to %s...", ssid);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main entry point
|
||||
*/
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "=== NexusGuard Embedded Agent v%s ===", CONFIG_NEXUS_AGENT_VERSION);
|
||||
ESP_LOGI(TAG, "Hardware: %s", CONFIG_IDF_TARGET);
|
||||
ESP_LOGI(TAG, "IDF Version: %s", esp_get_idf_version());
|
||||
ESP_LOGI(TAG, "Server: %s", CONFIG_NEXUS_SERVER_URL);
|
||||
|
||||
/* Initialize NVS */
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
/* Create event group */
|
||||
s_event_group = xEventGroupCreate();
|
||||
|
||||
/* Get hardware ID */
|
||||
char hwid[32];
|
||||
get_hardware_id(hwid, sizeof(hwid));
|
||||
ESP_LOGI(TAG, "Hardware ID: %s", hwid);
|
||||
|
||||
/* Initialize modules */
|
||||
ESP_ERROR_CHECK(config_init());
|
||||
ESP_ERROR_CHECK(crypto_init());
|
||||
|
||||
/* Load saved config */
|
||||
ret = config_load(&s_config);
|
||||
if (ret == ESP_OK && config_is_valid(&s_config)) {
|
||||
ESP_LOGI(TAG, "Loaded saved config for device: %s", s_config.device_id);
|
||||
xEventGroupSetBits(s_event_group, PROVISIONED_BIT);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No valid config found, will provision...");
|
||||
}
|
||||
|
||||
/* Initialize WiFi */
|
||||
const char *ssid = CONFIG_NEXUS_WIFI_SSID;
|
||||
const char *password = CONFIG_NEXUS_WIFI_PASSWORD;
|
||||
|
||||
if (strlen(ssid) == 0) {
|
||||
ESP_LOGE(TAG, "WiFi SSID not configured! Set via menuconfig or NVS.");
|
||||
ESP_LOGE(TAG, "Run: idf.py menuconfig → Component config → NexusGuard Agent");
|
||||
return;
|
||||
}
|
||||
|
||||
init_wifi(ssid, password);
|
||||
|
||||
/* Wait for WiFi connection */
|
||||
ESP_LOGI(TAG, "Waiting for WiFi connection...");
|
||||
xEventGroupWaitBits(s_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE,
|
||||
pdMS_TO_TICKS(30000));
|
||||
|
||||
if (!(xEventGroupGetBits(s_event_group) & WIFI_CONNECTED_BIT)) {
|
||||
ESP_LOGE(TAG, "WiFi connection failed, restarting...");
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "WiFi connected!");
|
||||
|
||||
/* Provision if needed */
|
||||
if (!(xEventGroupGetBits(s_event_group) & PROVISIONED_BIT)) {
|
||||
ESP_LOGI(TAG, "Provisioning device...");
|
||||
s_state = STATE_PROVISIONING;
|
||||
|
||||
const char *server_url = CONFIG_NEXUS_SERVER_URL;
|
||||
const char *token = CONFIG_NEXUS_REG_TOKEN;
|
||||
|
||||
if (strlen(server_url) == 0 || strlen(token) == 0) {
|
||||
ESP_LOGE(TAG, "Server URL or registration token not configured!");
|
||||
ESP_LOGE(TAG, "Run: idf.py menuconfig → Component config → NexusGuard Agent");
|
||||
return;
|
||||
}
|
||||
|
||||
ret = provision_device(server_url, token, hwid, &s_config);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Provisioning failed: %s", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "Restarting in 5 seconds...");
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Provisioning successful!");
|
||||
xEventGroupSetBits(s_event_group, PROVISIONED_BIT);
|
||||
}
|
||||
|
||||
/* Initialize WireGuard tunnel */
|
||||
ESP_LOGI(TAG, "Initializing WireGuard tunnel...");
|
||||
s_state = STATE_TUNNEL_UP;
|
||||
|
||||
ret = tunnel_init(&s_config);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Tunnel initialization failed: %s", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "Restarting in 5 seconds...");
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "WireGuard tunnel initialized!");
|
||||
xEventGroupSetBits(s_event_group, TUNNEL_UP_BIT);
|
||||
|
||||
/* Main heartbeat loop */
|
||||
ESP_LOGI(TAG, "Starting heartbeat loop...");
|
||||
s_state = STATE_HEARTBEAT;
|
||||
|
||||
while (1) {
|
||||
/* Wait for 30 seconds */
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
|
||||
/* Check WiFi */
|
||||
if (!(xEventGroupGetBits(s_event_group) & WIFI_CONNECTED_BIT)) {
|
||||
ESP_LOGW(TAG, "WiFi not connected, skipping heartbeat");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Send heartbeat */
|
||||
bool tunnel_up = tunnel_is_up();
|
||||
int64_t last_hs = tunnel_get_last_handshake();
|
||||
|
||||
ret = heartbeat_send(&s_config, "connected", tunnel_up, last_hs);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Heartbeat failed: %s", esp_err_to_name(ret));
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Heartbeat sent (tunnel=%s)", tunnel_up ? "up" : "down");
|
||||
|
||||
/* Check for config changes */
|
||||
// TODO: Compare config hash and rebuild tunnel if needed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* @file provision.c
|
||||
* @brief HTTP Provisioning
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "provision.h"
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
static const char *TAG = "nexus_provision";
|
||||
|
||||
/* Retry config */
|
||||
#define MAX_RETRIES 3
|
||||
#define CONNECT_TIMEOUT_MS 10000
|
||||
#define READ_TIMEOUT_MS 30000
|
||||
|
||||
/* Config key name for decryption (must match server) */
|
||||
#define CONFIG_NEXUS_SALT CONFIG_NEXUS_SERVER_SALT
|
||||
|
||||
/**
|
||||
* @brief Helper: copy cJSON string to char buffer with bounds check
|
||||
*/
|
||||
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL) {
|
||||
strncpy(buf, item->valuestring, buf_len - 1);
|
||||
buf[buf_len - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief HTTP event handler for provisioning
|
||||
*/
|
||||
static esp_err_t provision_event_handler(esp_http_client_event_t *evt)
|
||||
{
|
||||
switch (evt->event_id) {
|
||||
case HTTP_EVENT_ERROR:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
|
||||
break;
|
||||
case HTTP_EVENT_ON_CONNECTED:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
|
||||
break;
|
||||
case HTTP_EVENT_ON_HEADER:
|
||||
break;
|
||||
case HTTP_EVENT_ON_DATA:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
|
||||
break;
|
||||
case HTTP_EVENT_ON_FINISH:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
|
||||
break;
|
||||
case HTTP_EVENT_DISCONNECTED:
|
||||
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t provision_device(const char *server_url, const char *token,
|
||||
const char *hwid, wg_config_t *config)
|
||||
{
|
||||
if (server_url == NULL || token == NULL || hwid == NULL || config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting provisioning...");
|
||||
|
||||
/* Build request JSON */
|
||||
cJSON *req_json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(req_json, "token", token);
|
||||
cJSON_AddStringToObject(req_json, "hwid", hwid);
|
||||
char *req_body = cJSON_PrintUnformatted(req_json);
|
||||
cJSON_Delete(req_json);
|
||||
|
||||
if (req_body == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create request JSON");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Build URL */
|
||||
char url[192];
|
||||
snprintf(url, sizeof(url), "%s/api/v1/provision", server_url);
|
||||
|
||||
esp_err_t final_err = ESP_FAIL;
|
||||
|
||||
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
ESP_LOGI(TAG, "Provisioning attempt %d/%d...", attempt, MAX_RETRIES);
|
||||
|
||||
/* Configure HTTP client */
|
||||
esp_http_client_config_t http_config = {
|
||||
.url = url,
|
||||
.event_handler = provision_event_handler,
|
||||
.timeout_ms = CONNECT_TIMEOUT_MS,
|
||||
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
||||
.skip_cert_common_name_check = true, /* TODO: use proper CA cert */
|
||||
};
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&http_config);
|
||||
if (client == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to init HTTP client");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Set POST data */
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
esp_http_client_set_post_field(client, req_body, strlen(req_body));
|
||||
|
||||
/* Execute request */
|
||||
esp_err_t err = esp_http_client_open(client, strlen(req_body));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
int written = esp_http_client_write(client, req_body, strlen(req_body));
|
||||
if (written < 0) {
|
||||
ESP_LOGE(TAG, "HTTP write failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
int content_length = esp_http_client_fetch_headers(client);
|
||||
int status_code = esp_http_client_get_status_code(client);
|
||||
|
||||
if (status_code != 200) {
|
||||
ESP_LOGE(TAG, "Provisioning failed, HTTP %d", status_code);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Read response body */
|
||||
char *response_buf = malloc(content_length + 1);
|
||||
if (response_buf == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate response buffer");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
final_err = ESP_ERR_NO_MEM;
|
||||
break;
|
||||
}
|
||||
|
||||
int read_len = esp_http_client_read(client, response_buf, content_length);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
if (read_len < 0) {
|
||||
ESP_LOGE(TAG, "HTTP read failed");
|
||||
free(response_buf);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
response_buf[read_len] = '\0';
|
||||
|
||||
/* Parse response JSON */
|
||||
cJSON *resp_json = cJSON_Parse(response_buf);
|
||||
free(response_buf);
|
||||
|
||||
if (resp_json == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to parse response JSON");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract encrypted_config (base64) */
|
||||
cJSON *encrypted_item = cJSON_GetObjectItemCaseSensitive(resp_json, "encrypted_config");
|
||||
if (!cJSON_IsString(encrypted_item) || encrypted_item->valuestring == NULL) {
|
||||
ESP_LOGE(TAG, "Response missing 'encrypted_config'");
|
||||
cJSON_Delete(resp_json);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *b64_data = encrypted_item->valuestring;
|
||||
size_t b64_len = strlen(b64_data);
|
||||
|
||||
/* Base64 decode */
|
||||
size_t decoded_max_len = (b64_len * 3) / 4 + 4;
|
||||
uint8_t *decoded_buf = malloc(decoded_max_len);
|
||||
if (decoded_buf == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate base64 decode buffer");
|
||||
cJSON_Delete(resp_json);
|
||||
final_err = ESP_ERR_NO_MEM;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t decoded_len = 0;
|
||||
int b64_ret = mbedtls_base64_decode(decoded_buf, decoded_max_len, &decoded_len,
|
||||
(const unsigned char *)b64_data, b64_len);
|
||||
if (b64_ret != 0) {
|
||||
ESP_LOGE(TAG, "Base64 decode failed: -0x%04x", -b64_ret);
|
||||
free(decoded_buf);
|
||||
cJSON_Delete(resp_json);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Derive decryption key: SHA256(hwid + salt) */
|
||||
uint8_t key[32];
|
||||
err = crypto_derive_key(hwid, CONFIG_NEXUS_SALT, key);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Key derivation failed");
|
||||
explicit_bzero(key, sizeof(key));
|
||||
free(decoded_buf);
|
||||
cJSON_Delete(resp_json);
|
||||
final_err = err;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Decrypt config */
|
||||
uint8_t *plaintext = NULL;
|
||||
size_t plaintext_len = 0;
|
||||
err = crypto_decrypt(decoded_buf, decoded_len, key, &plaintext, &plaintext_len);
|
||||
explicit_bzero(key, sizeof(key));
|
||||
free(decoded_buf);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Decryption failed");
|
||||
cJSON_Delete(resp_json);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Parse decrypted JSON payload */
|
||||
cJSON *config_json = cJSON_Parse((char *)plaintext);
|
||||
free(plaintext);
|
||||
|
||||
if (config_json == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to parse decrypted config JSON");
|
||||
cJSON_Delete(resp_json);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract config fields */
|
||||
copy_json_str(config_json, "device_id", config->device_id, sizeof(config->device_id));
|
||||
copy_json_str(config_json, "private_key", config->private_key, sizeof(config->private_key));
|
||||
copy_json_str(config_json, "preshared_key", config->preshared_key, sizeof(config->preshared_key));
|
||||
copy_json_str(config_json, "internal_ip", config->internal_ip, sizeof(config->internal_ip));
|
||||
copy_json_str(config_json, "server_pub", config->server_pub, sizeof(config->server_pub));
|
||||
copy_json_str(config_json, "endpoint", config->endpoint, sizeof(config->endpoint));
|
||||
copy_json_str(config_json, "dns", config->dns, sizeof(config->dns));
|
||||
copy_json_str(config_json, "allowed_ips", config->allowed_ips, sizeof(config->allowed_ips));
|
||||
copy_json_str(config_json, "server_wg_ip", config->server_wg_ip, sizeof(config->server_wg_ip));
|
||||
copy_json_str(config_json, "config_hash", config->config_hash, sizeof(config->config_hash));
|
||||
|
||||
cJSON_Delete(config_json);
|
||||
cJSON_Delete(resp_json);
|
||||
|
||||
/* Save config to NVS */
|
||||
err = config_save(config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err));
|
||||
final_err = err;
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Provisioning complete, device: %s", config->device_id);
|
||||
final_err = ESP_OK;
|
||||
break;
|
||||
}
|
||||
|
||||
free(req_body);
|
||||
return final_err;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file provision.h
|
||||
* @brief HTTP Provisioning
|
||||
*/
|
||||
|
||||
#ifndef PROVISION_H
|
||||
#define PROVISION_H
|
||||
|
||||
#include <esp_err.h>
|
||||
#include "config.h"
|
||||
|
||||
/**
|
||||
* @brief Provision device with server
|
||||
*
|
||||
* @param server_url Server URL (e.g., "https://api-nexus.datadunia.com")
|
||||
* @param token Registration token
|
||||
* @param hwid Hardware ID
|
||||
* @param config Configuration to populate
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t provision_device(const char *server_url, const char *token,
|
||||
const char *hwid, wg_config_t *config);
|
||||
|
||||
#endif /* PROVISION_H */
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @file tunnel.c
|
||||
* @brief WireGuard Tunnel Management (Stub)
|
||||
*
|
||||
* TODO: Integrate wireguard-esp32 component for actual WireGuard tunnel.
|
||||
* These are stubs that log and maintain state for the heartbeat loop.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "tunnel.h"
|
||||
#include "config.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "nexus_tunnel";
|
||||
|
||||
/* Tunnel state */
|
||||
static bool s_tunnel_up = false;
|
||||
static int64_t s_last_handshake = 0;
|
||||
|
||||
esp_err_t tunnel_init(const wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initializing WireGuard tunnel...");
|
||||
ESP_LOGI(TAG, " Device: %s", config->device_id);
|
||||
ESP_LOGI(TAG, " Endpoint: %s", config->endpoint);
|
||||
ESP_LOGI(TAG, " Internal IP: %s", config->internal_ip);
|
||||
ESP_LOGI(TAG, " Allowed IPs: %s", config->allowed_ips);
|
||||
|
||||
/* TODO: integrate wireguard-esp32 component */
|
||||
/* Steps when integrated:
|
||||
* 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
|
||||
*/
|
||||
|
||||
/* Stub: mark tunnel as up and record handshake time */
|
||||
s_tunnel_up = true;
|
||||
s_last_handshake = (int64_t)time(NULL);
|
||||
|
||||
ESP_LOGI(TAG, "WireGuard tunnel initialized (stub)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tunnel_apply_config(const wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Applying WireGuard config...");
|
||||
ESP_LOGI(TAG, " Endpoint: %s", config->endpoint);
|
||||
|
||||
/* TODO: integrate wireguard-esp32 component */
|
||||
/* Steps when integrated:
|
||||
* 1. Stop existing tunnel
|
||||
* 2. Reconfigure device with new keys
|
||||
* 3. Reconfigure peer with new endpoint/allowed_ips
|
||||
* 4. Restart tunnel
|
||||
*/
|
||||
|
||||
s_tunnel_up = true;
|
||||
s_last_handshake = (int64_t)time(NULL);
|
||||
|
||||
ESP_LOGI(TAG, "WireGuard config applied (stub)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tunnel_rebuild(const wg_config_t *config)
|
||||
{
|
||||
if (config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Rebuilding WireGuard tunnel...");
|
||||
|
||||
/* Mark tunnel down during rebuild */
|
||||
s_tunnel_up = false;
|
||||
|
||||
/* Apply new config */
|
||||
esp_err_t err = tunnel_apply_config(config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Tunnel rebuild failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "WireGuard tunnel rebuilt successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int64_t tunnel_get_last_handshake(void)
|
||||
{
|
||||
return s_last_handshake;
|
||||
}
|
||||
|
||||
bool tunnel_is_up(void)
|
||||
{
|
||||
return s_tunnel_up;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @file tunnel.h
|
||||
* @brief WireGuard Tunnel Management
|
||||
*/
|
||||
|
||||
#ifndef TUNNEL_H
|
||||
#define TUNNEL_H
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "config.h"
|
||||
|
||||
/**
|
||||
* @brief Initialize WireGuard tunnel
|
||||
*/
|
||||
esp_err_t tunnel_init(const wg_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Apply configuration to tunnel
|
||||
*/
|
||||
esp_err_t tunnel_apply_config(const wg_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Rebuild tunnel with new configuration
|
||||
*/
|
||||
esp_err_t tunnel_rebuild(const wg_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Get last handshake timestamp (epoch seconds)
|
||||
*/
|
||||
int64_t tunnel_get_last_handshake(void);
|
||||
|
||||
/**
|
||||
* @brief Check if tunnel is up
|
||||
*/
|
||||
bool tunnel_is_up(void);
|
||||
|
||||
#endif /* TUNNEL_H */
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @file wifi.c
|
||||
* @brief WiFi STA Management
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "wifi.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "nexus_wifi";
|
||||
|
||||
/* Event group bits */
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
/* Static state */
|
||||
static EventGroupHandle_t s_wifi_event_group = NULL;
|
||||
static esp_netif_t *s_sta_netif = NULL;
|
||||
static esp_event_handler_instance_t s_any_id_instance = NULL;
|
||||
static esp_event_handler_instance_t s_got_ip_instance = NULL;
|
||||
static bool s_initialized = false;
|
||||
|
||||
/**
|
||||
* @brief WiFi event handler
|
||||
*/
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT) {
|
||||
switch (event_id) {
|
||||
case WIFI_EVENT_STA_START:
|
||||
ESP_LOGI(TAG, "WiFi STA started, connecting...");
|
||||
esp_wifi_connect();
|
||||
break;
|
||||
|
||||
case WIFI_EVENT_STA_DISCONNECTED:
|
||||
ESP_LOGW(TAG, "WiFi disconnected");
|
||||
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
/* Auto-reconnect is handled by esp_wifi_connect() below */
|
||||
esp_wifi_connect();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t wifi_init_sta(const char *ssid, const char *password)
|
||||
{
|
||||
if (ssid == NULL || password == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (s_initialized) {
|
||||
ESP_LOGW(TAG, "WiFi already initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initializing WiFi STA...");
|
||||
|
||||
/* Create event group */
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
/* Initialize network interface */
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
s_sta_netif = esp_netif_create_default_wifi_sta();
|
||||
|
||||
/* Initialize WiFi */
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
/* Register event handlers */
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler, NULL, &s_any_id_instance));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler, NULL, &s_got_ip_instance));
|
||||
|
||||
/* Configure WiFi */
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.pmf_cfg = {
|
||||
.capable = true,
|
||||
.required = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
|
||||
strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "WiFi STA initialized, will connect to: %s", ssid);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_connect(void)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
ESP_LOGE(TAG, "WiFi not initialized, call wifi_init_sta first");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to WiFi...");
|
||||
esp_err_t err = esp_wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi connect failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t wifi_disconnect(void)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Disconnecting from WiFi...");
|
||||
return esp_wifi_disconnect();
|
||||
}
|
||||
|
||||
bool wifi_is_connected(void)
|
||||
{
|
||||
if (s_wifi_event_group == NULL) {
|
||||
return false;
|
||||
}
|
||||
return (xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT) != 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file wifi.h
|
||||
* @brief WiFi STA Management
|
||||
*/
|
||||
|
||||
#ifndef WIFI_H
|
||||
#define WIFI_H
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
/**
|
||||
* @brief Initialize WiFi in STA mode
|
||||
*/
|
||||
esp_err_t wifi_init_sta(const char *ssid, const char *password);
|
||||
|
||||
/**
|
||||
* @brief Connect to WiFi
|
||||
*/
|
||||
esp_err_t wifi_connect(void);
|
||||
|
||||
/**
|
||||
* @brief Disconnect from WiFi
|
||||
*/
|
||||
esp_err_t wifi_disconnect(void);
|
||||
|
||||
/**
|
||||
* @brief Check if WiFi is connected
|
||||
*/
|
||||
bool wifi_is_connected(void);
|
||||
|
||||
#endif /* WIFI_H */
|
||||
@@ -0,0 +1,77 @@
|
||||
# NexusGuard Embedded Agent Configuration
|
||||
|
||||
# Target
|
||||
CONFIG_IDF_TARGET="esp32"
|
||||
|
||||
# WiFi
|
||||
CONFIG_ESP_WIFI_SSID=""
|
||||
CONFIG_ESP_WIFI_PASSWORD=""
|
||||
CONFIG_ESP_WIFI_AUTHMODE=WPA2_PSK
|
||||
CONFIG_ESP_WIFI_SCAN_TYPE=ACTIVE
|
||||
CONFIG_ESP_WIFI_CONNECT_TIMEOUT_MS=10000
|
||||
|
||||
# HTTP Client
|
||||
CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y
|
||||
CONFIG_ESP_HTTP_CLIENT_ENABLE_TLS=y
|
||||
CONFIG_ESP_TLS_USING_MBEDTLS=y
|
||||
|
||||
# NVS
|
||||
CONFIG_NVS_ENCRYPTION=n
|
||||
CONFIG_NVS_PARTITION_ENCRYPTED=n
|
||||
|
||||
# FreeRTOS
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
|
||||
CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=n
|
||||
|
||||
# LWIP
|
||||
CONFIG_LWIP_IP4_NAPT=n
|
||||
CONFIG_LWIP_NETIF_LOOPBACK=y
|
||||
|
||||
# mbedTLS
|
||||
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y
|
||||
CONFIG_MBEDTLS_AES_C=y
|
||||
CONFIG_MBEDTLS_GCM_C=y
|
||||
CONFIG_MBEDTLS_SHA256_C=y
|
||||
|
||||
# Log Level
|
||||
CONFIG_LOG_DEFAULT_LEVEL=3
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
# Stack Size
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
|
||||
|
||||
# Power Management
|
||||
CONFIG_PM_ENABLE=n
|
||||
CONFIG_PM_DFS_INIT_AUTO=y
|
||||
CONFIG_PM_DFS_APB_MAX_FREQ=80
|
||||
|
||||
# Flash
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHMODE=QIO
|
||||
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
|
||||
|
||||
# Partition Table
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=n
|
||||
|
||||
# Watchdog
|
||||
CONFIG_ESP_TASK_WDT=y
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
|
||||
|
||||
# HTTPS Server (for OTA in future)
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
CONFIG_HTTPD_MAX_URI_LEN=512
|
||||
|
||||
# Application Version
|
||||
CONFIG_NEXUSGUARD_AGENT_VERSION="1.0.0"
|
||||
|
||||
# NexusGuard Server Configuration
|
||||
CONFIG_NEXUS_SERVER_URL="https://api-nexus.datadunia.com"
|
||||
CONFIG_NEXUS_REG_TOKEN=""
|
||||
CONFIG_NEXUS_SERVER_SALT=""
|
||||
CONFIG_NEXUS_WIFI_SSID=""
|
||||
CONFIG_NEXUS_WIFI_PASSWORD=""
|
||||
Reference in New Issue
Block a user