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
This commit is contained in:
@@ -1,18 +1,55 @@
|
||||
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 {
|
||||
// Start the local proxy server
|
||||
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()
|
||||
try await server.start()
|
||||
proxyServer = server
|
||||
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
|
||||
}
|
||||
|
||||
// Configure tunnel to redirect HTTP/HTTPS to our local proxy
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||
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,
|
||||
@@ -24,27 +61,66 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
|
||||
)
|
||||
proxySettings.httpEnabled = true
|
||||
proxySettings.httpsEnabled = true
|
||||
proxySettings.matchDomains = [""] // Match all domains
|
||||
proxySettings.matchDomains = [""]
|
||||
proxySettings.excludeSimpleHostnames = true
|
||||
settings.proxySettings = proxySettings
|
||||
|
||||
// DNS settings to ensure proper resolution
|
||||
let dnsSettings = NEDNSSettings(servers: ["8.8.8.8", "8.8.4.4"])
|
||||
dnsSettings.matchDomains = [""] // Match all
|
||||
settings.dnsSettings = dnsSettings
|
||||
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
|
||||
}
|
||||
|
||||
try await setTunnelNetworkSettings(settings)
|
||||
// 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? {
|
||||
// Handle IPC messages from the main app
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user