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() } } }