fix(client/macos+server): 隧道运行时打通——DNS 劫持 + 死锁/空指针修复
接续 447f3f4(系统扩展可加载),修复"扩展能起但连上无法上网"的运行时问题,
现已在 macOS 15(cara)端到端连通:出口=节点 IP、国外站可达、DNS 经隧道解析。
服务端(clientconfig.go):
- route.rules 首条加 {"action":"hijack-dns","port":[53]}(排在 LAN 直连规则前)。
否则发往隧道 DNS(172.19.0.2:53)的查询被 172.16.0.0/12 吞去直连,域名解析失败。
sing-box 1.13 按端口劫持(protocol:dns 需先 sniff,不稳)。
客户端——保留三个真 bug 修复:
- startTunnel 的 libbox 启动移到后台队列:避免在 provider 队列同步阻塞,与
openTun→setTunnelNetworkSettings 回调三方死锁(隧道永远卡 connecting)。
- startOrReloadService(options:) 传非空 LibboxOverrideOptions():传 nil 致空指针 SIGSEGV。
- startDefaultInterfaceMonitor 阻塞到首个 path 更新再返回:修 "no available network interface"。
- 清除排障期临时诊断代码;CFBundleVersion 递增(sysextd 按版本去重)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEHzjEcFzvGwgbxT6Wbt6c
This commit is contained in:
@@ -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 脱敏成 <private>。
|
||||
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?) {}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user