Files
ProxyIOS/PacketTunnel/PacketTunnelProvider.swift
Trey t 148bc3887c Add iPad support, auto-pinning, and comprehensive logging
- Adaptive iPhone/iPad layout with NavigationSplitView sidebar
- Auto-detect SSL-pinned domains, fall back to passthrough
- Certificate install via local HTTP server (Safari profile flow)
- App Group-backed CA, per-domain leaf cert LRU cache
- DB-backed config repository, Darwin notification throttling
- Rules engine, breakpoint rules, pinned domain tracking
- os.Logger instrumentation across tunnel/proxy/mitm/capture/cert/rules/db/ipc/ui
- Fix dyld framework embed, race conditions, thread safety
2026-04-11 12:52:18 -05:00

127 lines
5.1 KiB
Swift

import NetworkExtension
import ProxyCore
import os
private let log = Logger(subsystem: "com.treyt.proxyapp", category: "tunnel")
class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private var proxyServer: ProxyServer?
private let runtimeStatusRepo = RuntimeStatusRepository()
override func startTunnel(options: [String: NSObject]? = nil) async throws {
log.info("========== TUNNEL STARTING ==========")
CertificateManager.shared.reloadSharedCA()
runtimeStatusRepo.update {
$0.tunnelState = ProxyRuntimeState.starting.rawValue
$0.proxyHost = nil
$0.proxyPort = nil
$0.caFingerprint = CertificateManager.shared.caFingerprint
$0.lastProxyError = nil
$0.lastConnectError = nil
$0.lastMITMError = nil
$0.lastExtensionStartAt = Date().timeIntervalSince1970
}
let server = ProxyServer()
do {
try await server.start()
proxyServer = server
log.info("ProxyServer started on \(ProxyConstants.proxyHost):\(ProxyConstants.proxyPort)")
} catch {
log.error("ProxyServer FAILED to start: \(error.localizedDescription)")
runtimeStatusRepo.update {
$0.tunnelState = ProxyRuntimeState.failed.rawValue
$0.lastProxyError = error.localizedDescription
$0.caFingerprint = CertificateManager.shared.caFingerprint
}
throw error
}
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "192.0.2.1")
// Assign a dummy IP to the tunnel interface.
// This is required for NEPacketTunnelProvider to function.
let ipv4 = NEIPv4Settings(addresses: ["198.51.100.1"], subnetMasks: ["255.255.255.0"])
// Do NOT add includedRoutes we don't want to route IP packets through the tunnel.
// Only the proxy settings below handle traffic redirection for HTTP/HTTPS.
ipv4.includedRoutes = []
ipv4.excludedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
// HTTP/HTTPS proxy this is the actual interception mechanism.
// Apps using NSURLSession/WKWebView will route through our proxy.
let proxySettings = NEProxySettings()
proxySettings.httpServer = NEProxyServer(
address: ProxyConstants.proxyHost,
port: ProxyConstants.proxyPort
)
proxySettings.httpsServer = NEProxyServer(
address: ProxyConstants.proxyHost,
port: ProxyConstants.proxyPort
)
proxySettings.httpEnabled = true
proxySettings.httpsEnabled = true
proxySettings.matchDomains = [""]
proxySettings.excludeSimpleHostnames = true
settings.proxySettings = proxySettings
log.info("Applying tunnel settings")
do {
try await setTunnelNetworkSettings(settings)
} catch {
runtimeStatusRepo.update {
$0.tunnelState = ProxyRuntimeState.failed.rawValue
$0.lastProxyError = "Tunnel settings: \(error.localizedDescription)"
}
await proxyServer?.stop()
proxyServer = nil
throw error
}
log.info("Tunnel settings applied successfully")
runtimeStatusRepo.update {
$0.tunnelState = ProxyRuntimeState.running.rawValue
$0.proxyHost = ProxyConstants.proxyHost
$0.proxyPort = ProxyConstants.proxyPort
$0.caFingerprint = CertificateManager.shared.caFingerprint
}
// Start reading packets from the tunnel to prevent the queue from filling up.
// We don't need to process them just drain them. All real traffic goes
// through the HTTP proxy, not the IP tunnel.
startPacketDrain()
IPCManager.shared.post(.extensionStarted)
log.info("========== TUNNEL STARTED ==========")
}
override func stopTunnel(with reason: NEProviderStopReason) async {
log.info("========== TUNNEL STOPPING (reason: \(String(describing: reason))) ==========")
await proxyServer?.stop()
proxyServer = nil
runtimeStatusRepo.update {
$0.tunnelState = ProxyRuntimeState.stopped.rawValue
$0.proxyHost = nil
$0.proxyPort = nil
$0.lastExtensionStopAt = Date().timeIntervalSince1970
}
IPCManager.shared.post(.extensionStopped)
log.info("========== TUNNEL STOPPED ==========")
}
override func handleAppMessage(_ messageData: Data) async -> Data? {
log.debug("Received app message: \(messageData.count) bytes")
return nil
}
/// Continuously drain packets from the tunnel interface.
/// Since we exclude all IP routes, very few packets should arrive here.
/// But we must read them to prevent the tunnel from backing up.
private func startPacketDrain() {
packetFlow.readPackets { [weak self] packets, protocols in
// Discard packets all real traffic goes through the HTTP proxy
// Re-arm the read
self?.startPacketDrain()
}
}
}