Files
pangolin/client/ios/PacketTunnel/PacketTunnelProvider.swift
T
wangjia bd984feff9 fix(client/ios): iOS 接通原生隧道——切真桥 + save-before-start + DNS matchDomains
根因:iOS 端 vpn_bridge_provider 把 iOS 归入'其他平台 → VpnBridgeMock',
整个 App 用假桥,伪造已连接状态与写死的 URLTest 延迟(18/32/54ms),从不调用
B1/C1 打通的原生 NEPacketTunnelProvider。表现为 UI 显示已连接但流量全直连
(apple 通/google 被墙、RTT 20ms),设备日志无任何隧道/sing-box/节点痕迹。

三处修复:
1. vpn_bridge_provider.dart: iOS → VpnNativeBridge(真原生 MethodChannel)。
2. VpnManager.start(): 先 saveToPreferences+重载(首启触发系统授权、持久化配置)
   再 startVPNTunnel,否则对未持久化 manager 调用抛 NEVPNError。对齐 macOS。
3. PacketTunnelProvider.openTun: DNS 补 matchDomains=[""],强制所有 DNS 走隧道,
   iOS 不设会把隧道 DNS 当补充解析器、查询仍走系统直连 DNS 致污染打不开站点。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:28:53 +08:00

343 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PacketTunnelProvider.swift Pangolin iOS
//
// NEPacketTunnelProvider , sing-box(libbox) TUN, root
// macOS (LibboxNewCommandServer + LibboxPlatformInterfaceProtocol
// + utun fd)
//
// gomobile : Go protocol + class;Swift `XxxProtocol`,
// / classNE override completion-handler( Swift 6.2
// asyncObjC thunk )
import Darwin
import Foundation
import Libbox
import Network
import NetworkExtension
import os
private let log = Logger(subsystem: "com.pangolin.pangolinVpn.PacketTunnel", category: "provider")
// iOS App Group group. macOS <TeamID>.
private let appGroup = "group.com.pangolin.pangolinVpn"
final class PacketTunnelProvider: NEPacketTunnelProvider {
private var commandServer: LibboxCommandServer?
private var platform: PangolinPlatformInterface?
private let memoryMonitor = MemoryMonitor() // iOS
override func startTunnel(options: [String: NSObject]?,
completionHandler: @escaping (Error?) -> Void) {
log.info("startTunnel")
// libbox :startOrReloadService openTun
// setTunnelNetworkSettings, provider ; provider
// (NE startTunnel ) startOrReloadService
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self else { return }
do {
let configContent = try self.resolveConfig(options)
guard let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw simpleError("no app group container")
}
let work = base.appendingPathComponent("work", isDirectory: true)
try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true)
let setup = LibboxSetupOptions()
setup.basePath = base.path
setup.workingPath = work.path
setup.tempPath = NSTemporaryDirectory()
var setupErr: NSError?
LibboxSetup(setup, &setupErr)
if let setupErr { throw setupErr }
let platform = PangolinPlatformInterface(provider: self)
self.platform = platform
var newErr: NSError?
guard let server = LibboxNewCommandServer(self, platform, &newErr) else {
throw newErr ?? simpleError("LibboxNewCommandServer returned nil")
}
try server.start()
// options: libbox StartOrReloadService options,
// nil command_server.go:175 SIGSEGV()
try server.startOrReloadService(configContent, options: LibboxOverrideOptions())
self.commandServer = server
self.memoryMonitor.start() // iOS
log.info("startTunnel: service started")
completionHandler(nil)
} catch {
log.error("startTunnel failed: \(error.localizedDescription, privacy: .public)")
completionHandler(error)
}
}
}
override func stopTunnel(with reason: NEProviderStopReason,
completionHandler: @escaping () -> Void) {
log.info("stopTunnel reason=\(reason.rawValue)")
memoryMonitor.stop() // iOS
log.info("memory peak: \(self.memoryMonitor.summaryString(), privacy: .public)") // iOS
try? commandServer?.closeService()
commandServer?.close()
commandServer = nil
platform?.stopMonitor()
platform = nil
completionHandler()
}
override func handleAppMessage(_ messageData: Data,
completionHandler: ((Data?) -> Void)?) {
guard let req = try? JSONSerialization.jsonObject(with: messageData) as? [String: Any],
let action = req["action"] as? String else { completionHandler?(nil); return }
var resp: [String: Any] = [:]
switch action {
case "getStatus":
resp["status"] = (commandServer != nil) ? "on" : "error"
case "getMemory":
resp["memory"] = memoryMonitor.summaryString()
resp["peakRssBytes"] = memoryMonitor.peakResidentBytes
default:
resp["error"] = "unknown action: \(action)"
}
completionHandler?((try? JSONSerialization.data(withJSONObject: resp)) ?? nil)
}
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
if let inline = options?["config"] as? String, !inline.isEmpty {
return inline
}
if let shared = UserDefaults(suiteName: appGroup),
let cached = shared.string(forKey: "lastConfigJson"), !cached.isEmpty {
return cached
}
guard let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw simpleError("no app group container")
}
let content = (try? String(contentsOf: base.appendingPathComponent("config.json"),
encoding: .utf8)) ?? ""
guard !content.isEmpty else { throw simpleError("empty sing-box config") }
return content
}
}
// LibboxCommandServerHandler:
extension PacketTunnelProvider: LibboxCommandServerHandlerProtocol {
func serviceReload() throws {
if let cfg = try? resolveConfig(nil) {
try commandServer?.startOrReloadService(cfg, options: LibboxOverrideOptions())
}
}
func serviceStop() throws { cancelTunnelWithError(nil) }
func postServiceClose() {}
func getSystemProxyStatus() throws -> LibboxSystemProxyStatus {
let s = LibboxSystemProxyStatus(); s.available = false; s.enabled = false; return s
}
func setSystemProxyEnabled(_ enabled: Bool) throws {}
func writeStatus(_ message: LibboxStatusMessage?) {}
func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {}
func writeConnectionEvents(_ events: LibboxConnectionEvents?) {}
func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {}
func writeDebugMessage(_ message: String?) {}
func setDefaultLogLevel(_ level: Int32) {}
func initializeClashMode(_ modeList: (any LibboxStringIteratorProtocol)?, currentMode: String?) {}
func updateClashMode(_ newMode: String?) {}
func clearLogs() {}
func connected() {}
func disconnected(_ message: String?) {}
}
// libbox NetworkExtension (LibboxPlatformInterface)
final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
private weak var provider: NEPacketTunnelProvider?
private var monitor: NWPathMonitor?
private var defaultInterfaceIndex: Int32 = -1
private let monitorQueue = DispatchQueue(label: "pangolin.tunnel.pathmonitor")
init(provider: NEPacketTunnelProvider) {
self.provider = provider
super.init()
}
func stopMonitor() { monitor?.cancel(); monitor = nil }
// MARK: openTun TunOptions NEPacketTunnelNetworkSettings, utun fd
func openTun(_ options: (any LibboxTunOptionsProtocol)?, ret0_: UnsafeMutablePointer<Int32>?) throws {
guard let options, let provider else { throw simpleError("openTun: no options/provider") }
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
settings.mtu = NSNumber(value: options.getMTU())
let v4addrs = routePrefixes(options.getInet4Address())
if !v4addrs.isEmpty {
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
subnetMasks: v4addrs.map(\.mask))
ipv4.includedRoutes = includedRoutes4(options)
settings.ipv4Settings = ipv4
}
let v6addrs = routePrefixes(options.getInet6Address())
if !v6addrs.isEmpty {
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
ipv6.includedRoutes = includedRoutes6(options)
settings.ipv6Settings = ipv6
}
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
let dnsSettings = NEDNSSettings(servers: [dns.value])
// iOS matchDomains=[""] DNS ;
// DNS , DNS(en0),
// DNS "" =
dnsSettings.matchDomains = [""]
settings.dnsSettings = dnsSettings
}
let sem = DispatchSemaphore(value: 0)
var applyError: Error?
provider.setTunnelNetworkSettings(settings) { err in applyError = err; sem.signal() }
sem.wait()
if let applyError { throw applyError }
guard let fd = Self.tunFd() else { throw simpleError("openTun: utun fd not found") }
ret0_?.pointee = fd
}
// MARK:
func startDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws {
let m = NWPathMonitor()
monitor = m
// path : sing-box rule-set
// defaultInterfaceIndex -1,autoDetectControl "no available
// network interface" sing-box-for-apple
let firstUpdate = DispatchSemaphore(value: 0)
var signaled = false
m.pathUpdateHandler = { [weak self] path in
guard let self else { return }
let iface = path.availableInterfaces.first { path.usesInterfaceType($0.type) }
?? path.availableInterfaces.first
let name = iface?.name ?? ""
let index = Int32(iface?.index ?? 0)
self.defaultInterfaceIndex = index
listener?.updateDefaultInterface(name, interfaceIndex: index,
isExpensive: path.isExpensive,
isConstrained: path.isConstrained)
if !signaled { signaled = true; firstUpdate.signal() }
}
m.start(queue: monitorQueue)
_ = firstUpdate.wait(timeout: .now() + 5)
}
func closeDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws {
stopMonitor()
}
// MARK: socket
func autoDetectControl(_ fd: Int32) throws {
guard defaultInterfaceIndex > 0 else { return }
var idx = defaultInterfaceIndex
_ = setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &idx, socklen_t(MemoryLayout<Int32>.size))
_ = setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &idx, socklen_t(MemoryLayout<Int32>.size))
}
func usePlatformAutoDetectControl() -> Bool { true }
// MARK:
func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
InterfaceIterator(items: Self.enumerateInterfaces())
}
// MARK: ()
func underNetworkExtension() -> Bool { true }
func useProcFS() -> Bool { false }
func includeAllNetworks() -> Bool { false }
func clearDNSCache() {}
func readWIFIState() -> LibboxWIFIState? { nil }
func systemCertificates() -> (any LibboxStringIteratorProtocol)? { nil }
func localDNSTransport() -> (any LibboxLocalDNSTransportProtocol)? { nil }
func send(_ notification: LibboxNotification?) throws {}
func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32,
destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner {
throw simpleError("findConnectionOwner: unsupported")
}
//
private func routePrefixes(_ it: (any LibboxRoutePrefixIteratorProtocol)?) -> [(address: String, mask: String, prefix: Int32)] {
var out: [(address: String, mask: String, prefix: Int32)] = []
guard let it else { return out }
while it.hasNext() {
if let p = it.next() { out.append((address: p.address(), mask: p.mask(), prefix: p.prefix())) }
}
return out
}
private func includedRoutes4(_ o: any LibboxTunOptionsProtocol) -> [NEIPv4Route] {
let r = routePrefixes(o.getInet4RouteAddress())
if r.isEmpty { return [NEIPv4Route.default()] }
return r.map { NEIPv4Route(destinationAddress: $0.address, subnetMask: $0.mask) }
}
private func includedRoutes6(_ o: any LibboxTunOptionsProtocol) -> [NEIPv6Route] {
let r = routePrefixes(o.getInet6RouteAddress())
if r.isEmpty { return [NEIPv6Route.default()] }
return r.map { NEIPv6Route(destinationAddress: $0.address,
networkPrefixLength: NSNumber(value: $0.prefix)) }
}
// utun fd:getpeername sockaddr_ctl, utun id(WireGuard )
private static func tunFd() -> Int32? {
var ctlInfo = ctl_info()
withUnsafeMutableBytes(of: &ctlInfo.ctl_name) { ptr in
_ = "com.apple.net.utun_control".withCString {
strncpy(ptr.baseAddress!.assumingMemoryBound(to: CChar.self), $0, ptr.count - 1)
}
}
// CTLIOCGINFO = _IOWR('N', 3, struct ctl_info)(Swift ,)
let ctliocginfo: UInt = 0x8000_0000 | 0x4000_0000
| ((UInt(MemoryLayout<ctl_info>.stride) & 0x1fff) << 16)
| (UInt(UInt8(ascii: "N")) << 8) | 3
for fd in Int32(0)...1024 {
var addr = sockaddr_ctl()
var len = socklen_t(MemoryLayout<sockaddr_ctl>.size)
let ok = withUnsafeMutablePointer(to: &addr) { p in
p.withMemoryRebound(to: sockaddr.self, capacity: 1) { getpeername(fd, $0, &len) }
}
if ok == 0, addr.sc_family == AF_SYSTEM {
if ioctl(fd, ctliocginfo, &ctlInfo) == 0, addr.sc_id == ctlInfo.ctl_id {
return fd
}
}
}
return nil
}
private static func enumerateInterfaces() -> [LibboxNetworkInterface] {
var result: [String: LibboxNetworkInterface] = [:]
var head: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&head) == 0 else { return [] }
defer { freeifaddrs(head) }
var cur = head
while let p = cur {
let name = String(cString: p.pointee.ifa_name)
if result[name] == nil {
let i = LibboxNetworkInterface()
i.name = name
i.index = Int32(if_nametoindex(p.pointee.ifa_name))
result[name] = i
}
cur = p.pointee.ifa_next
}
return Array(result.values)
}
}
// LibboxNetworkInterfaceIterator
final class InterfaceIterator: NSObject, LibboxNetworkInterfaceIteratorProtocol {
private var idx = 0
private let items: [LibboxNetworkInterface]
init(items: [LibboxNetworkInterface]) { self.items = items }
func hasNext() -> Bool { idx < items.count }
func next() -> LibboxNetworkInterface? {
guard idx < items.count else { return nil }
defer { idx += 1 }
return items[idx]
}
}
private func simpleError(_ msg: String) -> NSError {
NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg])
}