refactor: Fix macOS standalone application
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
private let PROC_PIDPATHINFO_MAXSIZE: Int32 = 4096
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConnectionOwnerLookup")
|
||||
|
||||
enum ConnectionOwnerLookup {
|
||||
struct Result {
|
||||
let userId: Int32
|
||||
let userName: String
|
||||
let processPath: String
|
||||
}
|
||||
|
||||
static func find(
|
||||
ipProtocol: Int32,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int32,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int32
|
||||
) -> Result? {
|
||||
let sourceAddr = parseAddress(sourceAddress)
|
||||
let destAddr = parseAddress(destinationAddress)
|
||||
|
||||
guard let sourceAddr, let destAddr else {
|
||||
logger.error("find: failed to parse addresses")
|
||||
return nil
|
||||
}
|
||||
|
||||
let pidCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
|
||||
guard pidCount > 0 else {
|
||||
logger.error("find: no processes found")
|
||||
return nil
|
||||
}
|
||||
|
||||
let pidBufferSize = Int(pidCount) * MemoryLayout<pid_t>.size
|
||||
let pids = UnsafeMutablePointer<pid_t>.allocate(capacity: Int(pidCount))
|
||||
defer { pids.deallocate() }
|
||||
|
||||
let actualCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, pids, Int32(pidBufferSize))
|
||||
guard actualCount > 0 else {
|
||||
logger.error("find: failed to list processes")
|
||||
return nil
|
||||
}
|
||||
|
||||
let numPids = Int(actualCount) / MemoryLayout<pid_t>.size
|
||||
|
||||
for i in 0 ..< numPids {
|
||||
let pid = pids[i]
|
||||
if pid == 0 { continue }
|
||||
|
||||
if let result = checkProcessForConnection(
|
||||
pid: pid,
|
||||
ipProtocol: ipProtocol,
|
||||
sourceAddr: sourceAddr,
|
||||
sourcePort: UInt16(sourcePort),
|
||||
destAddr: destAddr,
|
||||
destPort: UInt16(destinationPort)
|
||||
) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func checkProcessForConnection(
|
||||
pid: pid_t,
|
||||
ipProtocol: Int32,
|
||||
sourceAddr: Data,
|
||||
sourcePort: UInt16,
|
||||
destAddr: Data,
|
||||
destPort: UInt16
|
||||
) -> Result? {
|
||||
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
|
||||
guard bufferSize > 0 else { return nil }
|
||||
|
||||
let fdBuffer = UnsafeMutableRawPointer.allocate(byteCount: Int(bufferSize), alignment: MemoryLayout<proc_fdinfo>.alignment)
|
||||
defer { fdBuffer.deallocate() }
|
||||
|
||||
let actualSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fdBuffer, bufferSize)
|
||||
guard actualSize > 0 else { return nil }
|
||||
|
||||
let fdCount = Int(actualSize) / MemoryLayout<proc_fdinfo>.size
|
||||
|
||||
for i in 0 ..< fdCount {
|
||||
let fd = fdBuffer.load(fromByteOffset: i * MemoryLayout<proc_fdinfo>.size, as: proc_fdinfo.self)
|
||||
|
||||
guard fd.proc_fdtype == PROX_FDTYPE_SOCKET else { continue }
|
||||
|
||||
var socketInfo = socket_fdinfo()
|
||||
let socketInfoSize = Int32(MemoryLayout<socket_fdinfo>.size)
|
||||
|
||||
let result = proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDSOCKETINFO, &socketInfo, socketInfoSize)
|
||||
guard result == socketInfoSize else { continue }
|
||||
|
||||
let soi: in_sockinfo
|
||||
if ipProtocol == IPPROTO_TCP {
|
||||
guard socketInfo.psi.soi_kind == SOCKINFO_TCP else { continue }
|
||||
soi = socketInfo.psi.soi_proto.pri_tcp.tcpsi_ini
|
||||
} else if ipProtocol == IPPROTO_UDP {
|
||||
guard socketInfo.psi.soi_kind == SOCKINFO_IN else { continue }
|
||||
soi = socketInfo.psi.soi_proto.pri_in
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if matchesConnection(
|
||||
socketInfo: soi,
|
||||
sourceAddr: sourceAddr,
|
||||
sourcePort: sourcePort,
|
||||
destAddr: destAddr,
|
||||
destPort: destPort
|
||||
) {
|
||||
return getProcessInfo(pid: pid)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func matchesConnection(
|
||||
socketInfo: in_sockinfo,
|
||||
sourceAddr: Data,
|
||||
sourcePort: UInt16,
|
||||
destAddr: Data,
|
||||
destPort: UInt16
|
||||
) -> Bool {
|
||||
let localPort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_lport))
|
||||
let remotePort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_fport))
|
||||
|
||||
guard localPort == sourcePort, remotePort == destPort else {
|
||||
return false
|
||||
}
|
||||
|
||||
var localAddr = socketInfo.insi_laddr
|
||||
var remoteAddr = socketInfo.insi_faddr
|
||||
|
||||
let localData: Data
|
||||
let remoteData: Data
|
||||
|
||||
if sourceAddr.count == 4 {
|
||||
localData = Data(bytes: &localAddr.ina_46.i46a_addr4, count: 4)
|
||||
remoteData = Data(bytes: &remoteAddr.ina_46.i46a_addr4, count: 4)
|
||||
} else {
|
||||
localData = Data(bytes: &localAddr.ina_6, count: 16)
|
||||
remoteData = Data(bytes: &remoteAddr.ina_6, count: 16)
|
||||
}
|
||||
|
||||
return localData == sourceAddr && remoteData == destAddr
|
||||
}
|
||||
|
||||
private static func getProcessInfo(pid: pid_t) -> Result? {
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(PROC_PIDPATHINFO_MAXSIZE))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
let pathLength = proc_pidpath(pid, pathBuffer, UInt32(PROC_PIDPATHINFO_MAXSIZE))
|
||||
let processPath = pathLength > 0 ? String(cString: pathBuffer) : ""
|
||||
|
||||
var info = proc_bsdinfo()
|
||||
let infoSize = Int32(MemoryLayout<proc_bsdinfo>.size)
|
||||
let result = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, infoSize)
|
||||
|
||||
guard result == infoSize else { return nil }
|
||||
|
||||
let uid = Int32(info.pbi_uid)
|
||||
let userName: String
|
||||
|
||||
if let pw = getpwuid(info.pbi_uid) {
|
||||
userName = String(cString: pw.pointee.pw_name)
|
||||
} else {
|
||||
userName = String(uid)
|
||||
}
|
||||
|
||||
return Result(userId: uid, userName: userName, processPath: processPath)
|
||||
}
|
||||
|
||||
private static func parseAddress(_ address: String) -> Data? {
|
||||
var addr4 = in_addr()
|
||||
if inet_pton(AF_INET, address, &addr4) == 1 {
|
||||
return Data(bytes: &addr4, count: 4)
|
||||
}
|
||||
|
||||
var addr6 = in6_addr()
|
||||
if inet_pton(AF_INET6, address, &addr6) == 1 {
|
||||
return Data(bytes: &addr6, count: 16)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>BasePackageIdentifier</key>
|
||||
<string>$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||
<key>AppGroupIdentifier</key>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>io.nekohasekai.sfavt.helper</string>
|
||||
<key>BundleProgram</key>
|
||||
<string>Contents/Helpers/RootHelper</string>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>287TTNZF8L.io.nekohasekai.sfavt.helper</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>AssociatedBundleIdentifiers</key>
|
||||
<array>
|
||||
<string>io.nekohasekai.sfavt.standalone</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
import Library
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "RootHelper")
|
||||
|
||||
class RootHelperService: NSObject {
|
||||
private var listener: NSXPCListener?
|
||||
|
||||
func start() {
|
||||
setupLogging()
|
||||
startXPCListener()
|
||||
}
|
||||
|
||||
private func setupLogging() {
|
||||
let basePath = "/var/log/sing-box"
|
||||
try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true)
|
||||
|
||||
let logPath = basePath + "/roothelper.log"
|
||||
freopen(logPath, "a", stderr)
|
||||
}
|
||||
|
||||
private func startXPCListener() {
|
||||
let machServiceName = getMachServiceName()
|
||||
listener = NSXPCListener(machServiceName: machServiceName)
|
||||
listener?.delegate = self
|
||||
listener?.resume()
|
||||
}
|
||||
|
||||
private func getMachServiceName() -> String {
|
||||
if let identifier = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String {
|
||||
return "\(identifier).helper"
|
||||
}
|
||||
fatalError("Missing AppGroupIdentifier in Info.plist")
|
||||
}
|
||||
}
|
||||
|
||||
extension RootHelperService: NSXPCListenerDelegate {
|
||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
let allowedBundleIDs = [
|
||||
AppConfiguration.systemExtensionBundleID,
|
||||
AppConfiguration.packageName + ".standalone",
|
||||
]
|
||||
guard XPCConnectionValidator.validateConnection(
|
||||
newConnection,
|
||||
teamID: AppConfiguration.teamID,
|
||||
allowedBundleIDs: allowedBundleIDs
|
||||
) else {
|
||||
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
|
||||
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
|
||||
return false
|
||||
}
|
||||
|
||||
let exportedInterface = NSXPCInterface(with: RootHelperProtocol.self)
|
||||
RootHelperXPC.configureInterface(exportedInterface)
|
||||
newConnection.exportedInterface = exportedInterface
|
||||
newConnection.exportedObject = self
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension RootHelperService: RootHelperProtocol {
|
||||
func findConnectionOwner(
|
||||
ipProtocol: Int32,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int32,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int32,
|
||||
reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void
|
||||
) {
|
||||
guard let result = ConnectionOwnerLookup.find(
|
||||
ipProtocol: ipProtocol,
|
||||
sourceAddress: sourceAddress,
|
||||
sourcePort: sourcePort,
|
||||
destinationAddress: destinationAddress,
|
||||
destinationPort: destinationPort
|
||||
) else {
|
||||
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Connection owner not found",
|
||||
])
|
||||
logger.error("findConnectionOwner: \(error.localizedDescription)")
|
||||
reply(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
let ownerResult = ConnectionOwnerResult(
|
||||
userId: result.userId,
|
||||
userName: result.userName,
|
||||
processPath: result.processPath
|
||||
)
|
||||
reply(ownerResult, nil)
|
||||
}
|
||||
|
||||
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void) {
|
||||
let size = WorkingDirectoryManager.getSize()
|
||||
reply(size, nil)
|
||||
}
|
||||
|
||||
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void) {
|
||||
do {
|
||||
try WorkingDirectoryManager.clean()
|
||||
reply(nil)
|
||||
} catch {
|
||||
logger.error("cleanWorkingDirectory error: \(error.localizedDescription)")
|
||||
reply(error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import Library
|
||||
|
||||
enum WorkingDirectoryManager {
|
||||
private static var workingDirectoryPath: String {
|
||||
"/var/root/Library/Containers/\(AppConfiguration.systemExtensionBundleID)/Data/Working"
|
||||
}
|
||||
|
||||
static func getSize() -> Int64 {
|
||||
let path = workingDirectoryPath
|
||||
guard FileManager.default.fileExists(atPath: path) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
var totalSize: Int64 = 0
|
||||
let enumerator = FileManager.default.enumerator(atPath: path)
|
||||
|
||||
while let file = enumerator?.nextObject() as? String {
|
||||
let filePath = (path as NSString).appendingPathComponent(file)
|
||||
if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath),
|
||||
let size = attrs[.size] as? Int64
|
||||
{
|
||||
totalSize += size
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
static func clean() throws {
|
||||
let path = workingDirectoryPath
|
||||
guard FileManager.default.fileExists(atPath: path) else {
|
||||
return
|
||||
}
|
||||
|
||||
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
|
||||
for item in contents {
|
||||
let itemPath = (path as NSString).appendingPathComponent(item)
|
||||
try FileManager.default.removeItem(atPath: itemPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
let service = RootHelperService()
|
||||
service.start()
|
||||
dispatchMain()
|
||||
Reference in New Issue
Block a user