Files
vpn/ios/PacketTunnel/PacketTunnelProvider.swift
2026-04-18 01:06:18 +03:00

328 lines
11 KiB
Swift

import Libbox
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
private var platformInterface: ExtensionPlatformInterface?
private var config: String?
private var appGroupId: String {
if let value = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String,
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
return value
}
let extensionBundleId = Bundle.main.bundleIdentifier ?? "com.example.vpn.PacketTunnel"
let mainBundleId = extensionBundleId.replacingOccurrences(of: ".PacketTunnel", with: "")
return "group.\(mainBundleId)"
}
override func startTunnel(
options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void
) {
NSLog("[PacketTunnel] Starting tunnel...")
guard let configString = options?["Config"] as? String else {
NSLog("[PacketTunnel] Config not provided")
completionHandler(
NSError(
domain: "V2rayBox",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Config not provided"]))
return
}
let coreEngine = (options?["CoreEngine"] as? String) ?? "singbox"
NSLog("[PacketTunnel] Config received, length: \(configString.count), core: \(coreEngine)")
config = configString
if coreEngine == "xray" {
startXrayTunnel(config: configString, completionHandler: completionHandler)
} else {
startSingboxTunnel(config: configString, completionHandler: completionHandler)
}
}
private func startSingboxTunnel(
config configString: String, completionHandler: @escaping (Error?) -> Void
) {
do {
let fileManager = FileManager.default
guard let sharedDir = fileManager.containerURL(
forSecurityApplicationGroupIdentifier: appGroupId)
else {
NSLog("[PacketTunnel] Failed to get shared container")
completionHandler(
NSError(
domain: "V2rayBox",
code: -2,
userInfo: [NSLocalizedDescriptionKey: "Failed to get shared container"]))
return
}
let workingDir = sharedDir.appendingPathComponent("working", isDirectory: true)
let cacheDir = sharedDir.appendingPathComponent("Library/Caches", isDirectory: true)
try fileManager.createDirectory(at: workingDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: cacheDir, withIntermediateDirectories: true)
NSLog("[PacketTunnel] Directories created")
platformInterface = ExtensionPlatformInterface(tunnel: self)
let opts = MobileSetupOptions()
opts.basePath = sharedDir.path
opts.workingDir = workingDir.path
opts.tempDir = cacheDir.path
opts.listen = ""
opts.secret = ""
opts.debug = false
opts.mode = 0
opts.fixAndroidStack = false
var error: NSError?
MobileSetup(opts, platformInterface, &error)
if let error = error {
NSLog("[PacketTunnel] MobileSetup error: \(error.localizedDescription)")
throw error
}
NSLog("[PacketTunnel] MobileSetup completed")
MobileStart(nil, configString, &error)
if let error = error {
NSLog("[PacketTunnel] MobileStart error: \(error.localizedDescription)")
throw error
}
NSLog("[PacketTunnel] sing-box started successfully")
completionHandler(nil)
} catch {
NSLog("[PacketTunnel] Error: \(error.localizedDescription)")
completionHandler(error)
}
}
private func startXrayTunnel(
config configString: String, completionHandler: @escaping (Error?) -> Void
) {
NSLog("[PacketTunnel] Xray-core tunnel start requested")
NSLog(
"[PacketTunnel] Note: Xray-core requires libXray framework integration in the PacketTunnel extension"
)
NSLog("[PacketTunnel] Falling back to sing-box core")
startSingboxTunnel(config: configString, completionHandler: completionHandler)
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
{
NSLog("[PacketTunnel] Stopping tunnel, reason: \(reason.rawValue)")
var error: NSError?
MobileStop(&error)
if let error = error {
NSLog("[PacketTunnel] MobileStop error: \(error.localizedDescription)")
}
MobileClose(0)
platformInterface?.reset()
NSLog("[PacketTunnel] Tunnel stopped")
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
guard let message = String(data: messageData, encoding: .utf8) else {
completionHandler?(nil)
return
}
if message == "stats" {
completionHandler?("0,0".data(using: .utf8))
} else {
completionHandler?(nil)
}
}
}
class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
private weak var tunnel: PacketTunnelProvider?
private var networkSettings: NEPacketTunnelNetworkSettings?
init(tunnel: PacketTunnelProvider) {
self.tunnel = tunnel
super.init()
}
func reset() {
networkSettings = nil
}
func openTun(_ options: (any LibboxTunOptionsProtocol)?, ret0_: UnsafeMutablePointer<Int32>?) throws {
NSLog("[PlatformInterface] openTun called")
guard let options = options, let ret0_ = ret0_, let tunnel = tunnel else {
throw NSError(
domain: "V2rayBox", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid parameters"])
}
let semaphore = DispatchSemaphore(value: 0)
var resultError: Error?
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
if options.getAutoRoute() {
settings.mtu = NSNumber(value: options.getMTU())
var dnsServers = ["8.8.8.8", "8.8.4.4"]
do {
let dnsBox = try options.getDNSServerAddress()
let dnsStr = dnsBox.value
if !dnsStr.isEmpty {
dnsServers = dnsStr.components(separatedBy: "\n").filter { !$0.isEmpty }
}
} catch {}
settings.dnsSettings = NEDNSSettings(servers: dnsServers)
var ipv4Addr: [String] = []
var ipv4Mask: [String] = []
if let iter = options.getInet4Address() {
while iter.hasNext() {
if let p = iter.next() {
ipv4Addr.append(p.address())
ipv4Mask.append(p.mask())
}
}
}
if !ipv4Addr.isEmpty {
let ipv4 = NEIPv4Settings(addresses: ipv4Addr, subnetMasks: ipv4Mask)
ipv4.includedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
} else {
let ipv4 = NEIPv4Settings(addresses: ["172.19.0.1"], subnetMasks: ["255.255.255.252"])
ipv4.includedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
}
var ipv6Addr: [String] = []
var ipv6Prefix: [NSNumber] = []
if let iter = options.getInet6Address() {
while iter.hasNext() {
if let p = iter.next() {
ipv6Addr.append(p.address())
ipv6Prefix.append(NSNumber(value: p.prefix()))
}
}
}
if !ipv6Addr.isEmpty {
let ipv6 = NEIPv6Settings(addresses: ipv6Addr, networkPrefixLengths: ipv6Prefix)
ipv6.includedRoutes = [NEIPv6Route.default()]
settings.ipv6Settings = ipv6
}
let proxyServer = options.getHTTPProxyServer()
let proxyPort = options.getHTTPProxyServerPort()
if !proxyServer.isEmpty && proxyPort > 0 {
let proxySettings = NEProxySettings()
proxySettings.httpServer = NEProxyServer(address: proxyServer, port: Int(proxyPort))
proxySettings.httpsServer = NEProxyServer(address: proxyServer, port: Int(proxyPort))
proxySettings.httpEnabled = true
proxySettings.httpsEnabled = true
var bypassDomains: [String] = []
if let iter = options.getHTTPProxyBypassDomain() {
while iter.hasNext() {
bypassDomains.append(iter.next())
}
}
proxySettings.exceptionList = bypassDomains
var matchDomains: [String] = []
if let iter = options.getHTTPProxyMatchDomain() {
while iter.hasNext() {
matchDomains.append(iter.next())
}
}
proxySettings.matchDomains = matchDomains.isEmpty ? nil : matchDomains
settings.proxySettings = proxySettings
}
} else {
settings.mtu = NSNumber(value: 9000)
settings.dnsSettings = NEDNSSettings(servers: ["8.8.8.8"])
let ipv4 = NEIPv4Settings(addresses: ["172.19.0.1"], subnetMasks: ["255.255.255.252"])
ipv4.includedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
}
networkSettings = settings
NSLog("[PlatformInterface] Setting tunnel network settings...")
tunnel.setTunnelNetworkSettings(settings) { error in
if let error = error {
NSLog("[PlatformInterface] setTunnelNetworkSettings error: \(error.localizedDescription)")
}
resultError = error
semaphore.signal()
}
semaphore.wait()
if let error = resultError {
throw error
}
let fd = LibboxGetTunnelFileDescriptor()
NSLog("[PlatformInterface] Got file descriptor: \(fd)")
if fd != -1 {
ret0_.pointee = fd
} else {
throw NSError(
domain: "V2rayBox", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Missing file descriptor"])
}
}
func usePlatformAutoDetectControl() -> Bool { false }
func autoDetectControl(_ fd: Int32) throws {}
func findConnectionOwner(
_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, destinationAddress: String?,
destinationPort: Int32
) throws -> LibboxConnectionOwner {
throw NSError(
domain: "V2rayBox", code: -1,
userInfo: [NSLocalizedDescriptionKey: "not supported on iOS"])
}
func useProcFS() -> Bool { false }
func startDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws
{}
func closeDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws
{}
func getInterfaces() throws -> any LibboxNetworkInterfaceIteratorProtocol {
throw NSError(
domain: "V2rayBox", code: -1, userInfo: [NSLocalizedDescriptionKey: "not implemented"])
}
func underNetworkExtension() -> Bool { true }
func includeAllNetworks() -> Bool { false }
func localDNSTransport() -> (any LibboxLocalDNSTransportProtocol)? { nil }
func systemCertificates() -> (any LibboxStringIteratorProtocol)? { nil }
func clearDNSCache() {
guard let s = networkSettings, let t = tunnel else { return }
t.reasserting = true
t.setTunnelNetworkSettings(nil) { _ in }
t.setTunnelNetworkSettings(s) { _ in }
t.reasserting = false
}
func readWIFIState() -> LibboxWIFIState? { nil }
func send(_ notification: LibboxNotification?) throws {}
}