fix(client/macos): sysext 把 command.sock 建在登录用户容器并 chown,修实时统计断流

根因:系统扩展以 root 运行,其 App Group 容器是 /var/root/Library/Group Containers/...,
而用户 app 的是 /Users/<user>/...(容器 home 相对)。libbox 把 command.sock 建在 basePath
下 → 落在 root 容器;无 root 权限的 app 永远 connect 不上 → 连接页 — KB/s + 延迟不刷新。
(经 sudo ls 实测确认:socket 在 /var/root 容器,用户容器里没有。)

- PacketTunnelProvider 用 SCDynamicStoreCopyConsoleUser 取登录用户,把 libbox basePath/
  workingPath 指向该用户的同名容器(root 可写),command.sock 即落在 app 查找的路径。
- server.start() 后 chown 容器/work/command.sock 给登录用户:Unix socket connect() 需写
  权限,仅 owner 满足,否则 app 仍 EACCES。拿不到登录用户时回退 root 容器(隧道可用、仅统计不可用)。
- sysext 代码改动 → CURRENT_PROJECT_VERSION 43→44,让 sysextd 重装新扩展。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-30 11:43:41 +08:00
parent 4f727beb60
commit 71132df669
2 changed files with 52 additions and 8 deletions
@@ -13,6 +13,7 @@ import Libbox
import Network
import NetworkExtension
import os
import SystemConfiguration
private let log = Logger(subsystem: "com.pangolin.pangolin.PacketTunnel", category: "provider")
// macOS App Group <TeamID>.<name>( iOS group. )
@@ -34,8 +35,24 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
do {
let configContent = try self.resolveConfig(options)
guard let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
// root , App Group /var/root/Library/Group
// Containers/..., app /Users/<user>/... ( home
// )libbox command.sock basePath root , root
// app connect ( KB/s + )
// : libbox (root ),
// socket chown ,使 app connect退
// root (,)
let consoleUser = Self.consoleUser()
let base: URL
if let u = consoleUser {
base = u.home
.appendingPathComponent("Library/Group Containers/\(appGroup)", isDirectory: true)
log.info("libbox base → 登录用户容器 \(base.path, privacy: .public)")
} else if let root = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
base = root
log.error("拿不到登录用户,回退 root 容器 \(base.path, privacy: .public)(app 连不上 command.sock)")
} else {
throw simpleError("no app group container")
}
let work = base.appendingPathComponent("work", isDirectory: true)
@@ -57,6 +74,16 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
throw newErr ?? simpleError("LibboxNewCommandServer returned nil")
}
try server.start()
// server.start() base command.sock(root )Unix socket
// connect() socket , owner /work/socket
// chown , root app root home root
// , chown app connect EACCES
if let u = consoleUser {
chown(base.path, u.uid, u.gid)
chown(work.path, u.uid, u.gid)
chown(base.appendingPathComponent("command.sock").path, u.uid, u.gid)
}
// options: libbox StartOrReloadService options,
// nil command_server.go:175 SIGSEGV()
try server.startOrReloadService(configContent, options: LibboxOverrideOptions())
@@ -87,6 +114,23 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
completionHandler?(nil)
}
// (GUI console user) home libbox
// , root(/var/root)(/) nil
private static func consoleUser() -> (uid: uid_t, gid: gid_t, home: URL)? {
guard let store = SCDynamicStoreCreate(nil, "com.pangolin.pangolin.PacketTunnel" as CFString, nil, nil) else {
return nil
}
var uid: uid_t = 0
var gid: gid_t = 0
guard let name = SCDynamicStoreCopyConsoleUser(store, &uid, &gid) as String?,
!name.isEmpty, name != "loginwindow", uid >= 500,
let pw = getpwuid(uid) else {
return nil
}
let home = URL(fileURLWithPath: String(cString: pw.pointee.pw_dir), isDirectory: true)
return (uid, gid, home)
}
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
if let inline = options?["configContent"] as? String, !inline.isEmpty {
return inline
@@ -659,7 +659,7 @@
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
@@ -674,7 +674,7 @@
baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
@@ -689,7 +689,7 @@
baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
@@ -960,7 +960,7 @@
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1010,7 +1010,7 @@
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 43;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;