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