# 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 |