docs: add ESP32 and iOS agent architecture reference
- reference/esp32-agent/: DESIGN, HARDWARE, API_COMPAT, ARCHITECTURE, README - reference/ios-agent/: DESIGN, API_COMPAT, UI_DESIGN, ARCHITECTURE, README - Both mirror Android agent architecture, UI design, and heartbeat flow - Architecture-only (no code) — avoids submodule conflicts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
# iOS Agent — Module Architecture
|
||||
|
||||
**Status**: ARCHITECTURE ONLY
|
||||
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the module architecture for the iOS WireGuard agent. The agent uses NetworkExtension (NEPacketTunnelProvider) for tunnel management and mirrors the Android agent's architecture.
|
||||
|
||||
## System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ iOS (SwiftUI) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ App Layer │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ContentView│ │SettingsView│ │LogView │ │PortView │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||
│ │ AgentManager │ │
|
||||
│ │ (orchestrates all components) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │Provisioning│ │Heartbeat│ │TunnelMgr │ │ConfigMgr│ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
|
||||
│ │ CryptoManager │ │
|
||||
│ │ AES-256-GCM (CryptoKit) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ NetworkExtension Layer │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ PacketTunnelProvider │ │
|
||||
│ │ (WireGuard tunnel via wireguard-apple) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Storage Layer │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Keychain │ │ UserDefaults│ │ CoreData│ │ FileMgr │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Task Architecture
|
||||
|
||||
```
|
||||
iOS Threads/Tasks
|
||||
├── Main Thread # UI updates
|
||||
├── AgentManager # Orchestration (async/await)
|
||||
├── HeartbeatTask # HTTP heartbeat (30s interval)
|
||||
├── TunnelManager # WireGuard tunnel management
|
||||
└── BackgroundTask # BGAppRefreshTask (background refresh)
|
||||
```
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ BOOT │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ APP_INIT │ ◄──────────────────────────────┐
|
||||
└──────┬──────┘ │
|
||||
│ loaded │
|
||||
▼ │
|
||||
┌─────────────┐ fail ┌─────────────┐│
|
||||
│ PROVISION │───────────────►│ APP_RETRY ││
|
||||
└──────┬──────┘ └──────┬──────┘│
|
||||
│ success │ │
|
||||
▼ └───────┘
|
||||
┌─────────────┐
|
||||
│ TUNNEL_UP │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ HEARTBEAT │ ◄─── 30s interval
|
||||
└──────┬──────┘
|
||||
│ config_changed
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ TUNNEL_REBUILD │
|
||||
└──────┬──────┘
|
||||
│
|
||||
└──► HEARTBEAT
|
||||
```
|
||||
|
||||
## Module Design
|
||||
|
||||
### 1. AgentManager
|
||||
|
||||
**Responsibilities**:
|
||||
- Orchestrate all agent components
|
||||
- Manage agent lifecycle (start/stop)
|
||||
- Handle state transitions
|
||||
|
||||
**Key Properties**:
|
||||
```swift
|
||||
class AgentManager: ObservableObject {
|
||||
@Published var state: AgentState = .disconnected
|
||||
@Published var tunnelInfo: TunnelInfo?
|
||||
@Published var logs: [LogEntry] = []
|
||||
|
||||
private let provisioning: ProvisioningService
|
||||
private let heartbeat: HeartbeatService
|
||||
private let tunnel: TunnelManager
|
||||
private let config: KeychainStorage
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func startAgent() async
|
||||
func stopAgent()
|
||||
func rebuildTunnel(with config: WireGuardConfig) async
|
||||
```
|
||||
|
||||
### 2. PacketTunnelProvider
|
||||
|
||||
**Responsibilities**:
|
||||
- Handle NetworkExtension tunnel lifecycle
|
||||
- Start/stop WireGuard tunnel
|
||||
- Report tunnel status
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void)
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
|
||||
```
|
||||
|
||||
**WireGuard Integration**:
|
||||
```swift
|
||||
// Uses wireguard-apple library
|
||||
let adapter = WireGuardAdapter(with: self) { logLevel, message in
|
||||
Logger.log(level: logLevel, message: message)
|
||||
}
|
||||
|
||||
adapter.start(tunnelConfiguration: tunnelConfig) { error in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. TunnelManager
|
||||
|
||||
**Responsibilities**:
|
||||
- Build WireGuard config from stored keys
|
||||
- Start/stop tunnel via PacketTunnelProvider
|
||||
- Monitor tunnel state
|
||||
- Track last handshake time
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func startTunnel(config: WireGuardConfig) async throws
|
||||
func stopTunnel()
|
||||
func isTunnelUp() -> Bool
|
||||
func getLastHandshake() -> Date?
|
||||
```
|
||||
|
||||
**Config Build**:
|
||||
```swift
|
||||
func buildTunnelConfig(from config: WireGuardConfig) -> TunnelConfiguration {
|
||||
let interface = InterfaceConfiguration(
|
||||
privateKey: config.privateKey,
|
||||
addresses: [config.internalIp],
|
||||
dns: [config.dns]
|
||||
)
|
||||
|
||||
let peer = PeerConfiguration(
|
||||
publicKey: config.serverPub,
|
||||
allowedIPs: config.allowedIps,
|
||||
endpoint: config.endpoint,
|
||||
preSharedKey: config.presharedKey,
|
||||
persistentKeepAlive: 25
|
||||
)
|
||||
|
||||
return TunnelConfiguration(interface: interface, peers: [peer])
|
||||
}
|
||||
```
|
||||
|
||||
### 4. HeartbeatService
|
||||
|
||||
**Responsibilities**:
|
||||
- HTTP POST to `/api/v1/heartbeat` every 30s
|
||||
- Send device status + tunnel state
|
||||
- Receive config sync (detect changes)
|
||||
- Trigger tunnel rebuild if config changed
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func startHeartbeatLoop()
|
||||
func stopHeartbeat()
|
||||
func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?
|
||||
```
|
||||
|
||||
**HTTP Request**:
|
||||
```json
|
||||
POST /api/v1/heartbeat
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": "uuid",
|
||||
"status": "connected",
|
||||
"tunnel_up": true,
|
||||
"last_handshake": "2026-06-26T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Response**:
|
||||
```json
|
||||
{
|
||||
"device_id": "uuid",
|
||||
"status": "ok",
|
||||
"internal_ip": "10.172.21.2",
|
||||
"private_key": "hex",
|
||||
"preshared_key": "hex",
|
||||
"server_pub": "hex",
|
||||
"endpoint": "italy-twenty.gl.at.ply.gg:59750",
|
||||
"dns": "1.1.1.1",
|
||||
"allowed_ips": "0.0.0.0/0",
|
||||
"server_wg_ip": "10.172.21.1",
|
||||
"forwards_hash": "hash-string"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. ProvisioningService
|
||||
|
||||
**Responsibilities**:
|
||||
- HTTP POST to `/api/v1/provision`
|
||||
- Parse encrypted config response
|
||||
- Decrypt config via CryptoManager
|
||||
- Store config in Keychain
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
|
||||
func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?
|
||||
```
|
||||
|
||||
**HTTP Request**:
|
||||
```json
|
||||
POST /api/v1/provision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"token": "registration-token",
|
||||
"hwid": "ios-hardware-id"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Response**:
|
||||
```json
|
||||
{
|
||||
"encrypted_config": "base64-encoded-bytes"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. CryptoManager
|
||||
|
||||
**Responsibilities**:
|
||||
- Key derivation: SHA-256(hwid + salt)
|
||||
- AES-256-GCM decryption
|
||||
- Secure memory handling
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func deriveKey(token: String, hwid: String) -> SymmetricKey
|
||||
func decrypt(encoded: String, key: SymmetricKey) -> Data?
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
```swift
|
||||
// Key Derivation
|
||||
let input = "\(token)\(hwid)".data(using: .utf8)!
|
||||
let hash = SHA256.hash(data: input)
|
||||
let key = SymmetricKey(data: hash)
|
||||
|
||||
// Decryption
|
||||
let combined = Data(base64Encoded: encoded)!
|
||||
let nonce = combined.prefix(12)
|
||||
let ciphertext = combined.dropFirst(12)
|
||||
|
||||
let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext)
|
||||
let plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||
```
|
||||
|
||||
### 7. KeychainStorage
|
||||
|
||||
**Responsibilities**:
|
||||
- Store WireGuard keys in Keychain
|
||||
- Store agent config in UserDefaults
|
||||
- Secure key access
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func saveConfig(_ config: AgentConfig)
|
||||
func loadConfig() -> AgentConfig?
|
||||
func saveWireGuardConfig(_ config: WireGuardConfig)
|
||||
func loadWireGuardConfig() -> WireGuardConfig?
|
||||
func clearAll()
|
||||
```
|
||||
|
||||
**Storage Strategy**:
|
||||
- **Keychain**: WireGuard keys (private_key, preshared_key)
|
||||
- **UserDefaults**: Non-sensitive config (server_url, device_id, endpoint)
|
||||
- **File**: Log entries (optional)
|
||||
|
||||
### 8. LogStore
|
||||
|
||||
**Responsibilities**:
|
||||
- Buffer log entries in memory
|
||||
- Persist to CoreData (optional)
|
||||
- Provide log stream for UI
|
||||
|
||||
**Key Methods**:
|
||||
```swift
|
||||
func add(level: LogLevel, tag: String, message: String)
|
||||
func clear()
|
||||
func getLogs() -> [LogEntry]
|
||||
```
|
||||
|
||||
**Log Levels**:
|
||||
```swift
|
||||
enum LogLevel: String {
|
||||
case debug = "DEBUG"
|
||||
case info = "INFO"
|
||||
case warn = "WARN"
|
||||
case error = "ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### WireGuardConfig
|
||||
|
||||
```swift
|
||||
struct WireGuardConfig: Codable {
|
||||
let deviceId: String
|
||||
let privateKey: String
|
||||
let presharedKey: String
|
||||
let internalIp: String
|
||||
let serverPub: String
|
||||
let endpoint: String
|
||||
let dns: String
|
||||
let allowedIps: String
|
||||
let serverWgIp: String
|
||||
let configHash: String
|
||||
let forwardsHash: String
|
||||
}
|
||||
```
|
||||
|
||||
### AgentConfig
|
||||
|
||||
```swift
|
||||
struct AgentConfig: Codable {
|
||||
let serverUrl: String
|
||||
let registrationToken: String
|
||||
let isProvisioned: Bool
|
||||
}
|
||||
```
|
||||
|
||||
### AgentState
|
||||
|
||||
```swift
|
||||
enum AgentState: String {
|
||||
case disconnected = "disconnected"
|
||||
case connecting = "connecting"
|
||||
case connected = "connected"
|
||||
case failed = "failed"
|
||||
}
|
||||
```
|
||||
|
||||
### LogEntry
|
||||
|
||||
```swift
|
||||
struct LogEntry: Identifiable {
|
||||
let id = UUID()
|
||||
let timestamp: Date
|
||||
let level: LogLevel
|
||||
let tag: String
|
||||
let message: String
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
```swift
|
||||
enum AgentError: Error {
|
||||
case wifiNotConnected
|
||||
case provisioningFailed(String)
|
||||
case heartbeatFailed(String)
|
||||
case cryptoDecryptionFailed
|
||||
case configInvalid
|
||||
case tunnelInitFailed
|
||||
case keychainError(OSStatus)
|
||||
}
|
||||
```
|
||||
|
||||
### Recovery Strategy
|
||||
|
||||
| Error | Recovery |
|
||||
|-------|----------|
|
||||
| WiFi not connected | Retry 3x, then pause heartbeat |
|
||||
| HTTP provision fail | Retry 3x, then notify user |
|
||||
| HTTP heartbeat fail | Log only, continue |
|
||||
| Crypto decrypt fail | Clear config, re-provision |
|
||||
| Tunnel init fail | Retry 3x, then notify user |
|
||||
| Keychain error | Use UserDefaults fallback |
|
||||
|
||||
## Background Tasks
|
||||
|
||||
### BGAppRefreshTask
|
||||
|
||||
iOS doesn't have boot receiver like Android. Use Background Tasks:
|
||||
|
||||
```swift
|
||||
// Register task
|
||||
BGAppRefreshTaskRequest.register(forTaskWithIdentifier: "com.nexusguard.agent.refresh", using: nil) {
|
||||
task in
|
||||
self.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
|
||||
// Schedule task
|
||||
let request = BGAppRefreshTaskRequest(identifier: "com.nexusguard.agent.refresh")
|
||||
request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 minutes
|
||||
try BGTaskScheduler.shared.submit(request)
|
||||
```
|
||||
|
||||
### Background Modes
|
||||
|
||||
Add to `Info.plist`:
|
||||
```xml
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>vpn-api</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **wireguard-apple**: WireGuard tunnel library
|
||||
- **CryptoKit**: AES-256-GCM encryption
|
||||
- **NetworkExtension**: VPN tunnel management
|
||||
- **BackgroundTasks**: Background refresh
|
||||
- **SwiftUI**: UI framework
|
||||
- **Combine**: Reactive state management
|
||||
|
||||
## Android → iOS Mapping
|
||||
|
||||
| Android | iOS | Notes |
|
||||
|---------|-----|-------|
|
||||
| `LifecycleService` | `PacketTunnelProvider` | Background service |
|
||||
| `startForeground()` | `NEPacketTunnelProvider` | Always-on tunnel |
|
||||
| `SharedPreferences` | `Keychain + UserDefaults` | Config storage |
|
||||
| `BroadcastReceiver` | `BGAppRefreshTask` | Background triggers |
|
||||
| `lifecycleScope` | `Task {}` | Coroutine/async |
|
||||
| `OkHttp` | `URLSession` | HTTP client |
|
||||
| `Base64` | `Data(base64Encoded:)` | Encoding |
|
||||
| `Log.d/i/w/e` | `os_log` or `print` | Logging |
|
||||
Reference in New Issue
Block a user