Files
datadunia 84689b208e 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>
2026-07-06 14:20:10 +07:00

15 KiB

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:

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:

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:

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:

// 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:

func startTunnel(config: WireGuardConfig) async throws
func stopTunnel()
func isTunnelUp() -> Bool
func getLastHandshake() -> Date?

Config Build:

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:

func startHeartbeatLoop()
func stopHeartbeat()
func sendHeartbeat(config: WireGuardConfig, tunnelUp: Bool, lastHandshake: Date?) async -> WireGuardConfig?

HTTP Request:

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:

{
  "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:

func provision(serverUrl: String, token: String, hwid: String) async -> WireGuardConfig?
func decryptConfig(encrypted: String, hwid: String, salt: String) -> WireGuardConfig?

HTTP Request:

POST /api/v1/provision
Content-Type: application/json

{
  "token": "registration-token",
  "hwid": "ios-hardware-id"
}

HTTP Response:

{
  "encrypted_config": "base64-encoded-bytes"
}

6. CryptoManager

Responsibilities:

  • Key derivation: SHA-256(hwid + salt)
  • AES-256-GCM decryption
  • Secure memory handling

Key Methods:

func deriveKey(token: String, hwid: String) -> SymmetricKey
func decrypt(encoded: String, key: SymmetricKey) -> Data?

Algorithm:

// 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:

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:

func add(level: LogLevel, tag: String, message: String)
func clear()
func getLogs() -> [LogEntry]

Log Levels:

enum LogLevel: String {
    case debug = "DEBUG"
    case info = "INFO"
    case warn = "WARN"
    case error = "ERROR"
}

Data Structures

WireGuardConfig

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

struct AgentConfig: Codable {
    let serverUrl: String
    let registrationToken: String
    let isProvisioned: Bool
}

AgentState

enum AgentState: String {
    case disconnected = "disconnected"
    case connecting = "connecting"
    case connected = "connected"
    case failed = "failed"
}

LogEntry

struct LogEntry: Identifiable {
    let id = UUID()
    let timestamp: Date
    let level: LogLevel
    let tag: String
    let message: String
}

Error Handling

Error Types

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:

// 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:

<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