diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index 4f950fa..02ba177 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -26,48 +26,48 @@ final class PacketTunnelProvider: NEPacketTunnelProvider { override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { log.info("startTunnel") - do { - let rawConfig = try resolveConfig(options) + // 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") + 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 + + log.info("startTunnel: service started") + completionHandler(nil) + } catch { + log.error("startTunnel failed: \(error.localizedDescription, privacy: .public)") + completionHandler(error) } - let work = base.appendingPathComponent("work", isDirectory: true) - try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true) - - // 【临时】剥掉远程 rule-set(#5);并把 sing-box 日志直写容器 box.log(debug)以定位启动卡点。 - let configContent = Self.stripRemoteRuleSets( - rawConfig, logOutput: base.appendingPathComponent("box.log").path) - - 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 - - log.info("startTunnel: service started") - completionHandler(nil) - } catch { - // 【临时诊断】完整错误写进 stderr.log;并设 public 避免 os_log 脱敏成 。 - FileHandle.standardError.write("startTunnel failed: \(error)\n".data(using: .utf8)!) - log.error("startTunnel failed: \(error.localizedDescription, privacy: .public)") - completionHandler(error) } } @@ -100,26 +100,6 @@ final class PacketTunnelProvider: NEPacketTunnelProvider { guard !content.isEmpty else { throw simpleError("empty sing-box config") } return content } - - // 【临时】移除配置里的远程 rule-set 定义,以及 route.rules 中引用了 rule_set 的规则。 - // 解析失败则原样返回。正式分流方案应改用本地 .srs(客户端预取)。 - static func stripRemoteRuleSets(_ json: String, logOutput: String) -> String { - guard let data = json.data(using: .utf8), - var root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any], - var route = root["route"] as? [String: Any] else { - return json - } - if route["rule_set"] != nil { route.removeValue(forKey: "rule_set") } - if let rules = route["rules"] as? [[String: Any]] { - route["rules"] = rules.filter { $0["rule_set"] == nil } - } - root["route"] = route - // 【临时诊断】sing-box 日志直写容器 box.log(debug),定位启动卡点。 - root["log"] = ["level": "debug", "timestamp": true, "output": logOutput] - guard let out = try? JSONSerialization.data(withJSONObject: root), - let s = String(data: out, encoding: .utf8) else { return json } - return s - } } // ── LibboxCommandServerHandler:服务端事件回调 ────────────────────────────── @@ -136,15 +116,7 @@ extension PacketTunnelProvider: LibboxCommandServerHandlerProtocol { } func setSystemProxyEnabled(_ enabled: Bool) throws {} func writeStatus(_ message: LibboxStatusMessage?) {} - func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) { - // 【临时诊断】把 sing-box 日志写进 stderr.log,定位启动卡点。 - guard let it = messageList else { return } - while it.hasNext() { - guard let entry = it.next() else { continue } - let line = "[box] \(entry.message)\n" - FileHandle.standardError.write(line.data(using: .utf8) ?? Data()) - } - } + func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {} func writeConnectionEvents(_ events: LibboxConnectionEvents?) {} func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {} func writeDebugMessage(_ message: String?) {} diff --git a/client/macos/PacketTunnel/main.swift b/client/macos/PacketTunnel/main.swift index 9680633..c1fdc59 100644 --- a/client/macos/PacketTunnel/main.swift +++ b/client/macos/PacketTunnel/main.swift @@ -7,17 +7,6 @@ import Foundation import NetworkExtension -// 【临时诊断】把 stderr/stdout 重定向到 App Group 容器文件,捕获 libbox(Go)的 -// fatal error / panic——系统扩展默认不把 stderr 收进统一日志。诊断完移除。 -if let c = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: "BYL4KQHMTN.com.pangolin.pangolin") { - let p = c.appendingPathComponent("stderr.log").path - freopen(p, "a", stderr) - freopen(p, "a", stdout) - setvbuf(stderr, nil, _IONBF, 0) - FileHandle.standardError.write("=== boot \(Date()) ===\n".data(using: .utf8)!) -} - autoreleasepool { NEProvider.startSystemExtensionMode() } diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index 88e9832..aca38ec 100644 --- a/client/macos/Runner.xcodeproj/project.pbxproj +++ b/client/macos/Runner.xcodeproj/project.pbxproj @@ -634,7 +634,7 @@ baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -649,7 +649,7 @@ baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -664,7 +664,7 @@ baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -935,7 +935,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -985,7 +985,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1034,7 +1034,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; diff --git a/server/internal/httpapi/clientconfig.go b/server/internal/httpapi/clientconfig.go index 0882398..246ce13 100644 --- a/server/internal/httpapi/clientconfig.go +++ b/server/internal/httpapi/clientconfig.go @@ -126,8 +126,13 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien "tolerance": 50, } - // Route: LAN direct;(可选)国内 IP/域名直连;其余 via auto。 + // Route: DNS 劫持 → LAN direct →(可选)国内直连 → 其余 via auto。 routeRules := []any{ + // DNS 劫持(sing-box 1.13 action=hijack-dns,按目的端口 53 匹配,不依赖 sniff): + // 把发往隧道 DNS(172.19.0.2:53)的查询交给 sing-box DNS 模块解析。必须排在 LAN + // 直连规则之前——否则 172.19.x 落在下面的 172.16.0.0/12 里,DNS 会被吞去直连、解析 + // 失败导致打不开网站(TUN 模式 DNS 劫持是必需项,缺失则隧道连上也无法上网)。 + map[string]any{"action": "hijack-dns", "port": []int{53}}, map[string]any{ "ip_cidr": []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"}, "outbound": "direct",