84689b208e
- 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>
475 lines
15 KiB
Markdown
475 lines
15 KiB
Markdown
# 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 |
|