From b86f6d9f24937cf14a859b0fb77aca1b85524146 Mon Sep 17 00:00:00 2001 From: zy860 Date: Tue, 18 Aug 2026 14:42:31 +0800 Subject: [PATCH] refactor: route rsd connections through embedded vpn --- StikJIT/Utilities/DeviceTransport.swift | 180 +++++++++++++++++++++++ StikJIT/Utilities/IdeviceFFIBridge.swift | 60 +++----- StikJIT/Utilities/JITEnableContext.swift | 96 ++---------- StikJITTests/StikJITTests.swift | 20 +++ 4 files changed, 232 insertions(+), 124 deletions(-) create mode 100644 StikJIT/Utilities/DeviceTransport.swift diff --git a/StikJIT/Utilities/DeviceTransport.swift b/StikJIT/Utilities/DeviceTransport.swift new file mode 100644 index 0000000..f1f1aa2 --- /dev/null +++ b/StikJIT/Utilities/DeviceTransport.swift @@ -0,0 +1,180 @@ +// +// DeviceTransport.swift +// StikDebug +// + +import Darwin +import Foundation +import idevice + +struct RPPairingEndpoint { + static let port: UInt16 = 49152 + + let ip: String + let port: UInt16 + + init(ip: String) throws { + var address = in_addr() + let result = ip.withCString { pointer in + inet_pton(AF_INET, pointer, &address) + } + guard result == 1 else { + throw DeviceTransportError.invalidIPAddress(ip) + } + + self.ip = ip + self.port = Self.port + } + + var sockaddr: sockaddr_in { + var address = sockaddr_in() + address.sin_family = sa_family_t(AF_INET) + address.sin_port = port.bigEndian + _ = ip.withCString { pointer in + inet_pton(AF_INET, pointer, &address.sin_addr) + } + return address + } +} + +struct RPPairingTunnel { + var adapter: OpaquePointer? + var handshake: OpaquePointer? + + init(adapter: OpaquePointer? = nil, handshake: OpaquePointer? = nil) { + self.adapter = adapter + self.handshake = handshake + } + + mutating func free() { + if let handshake { + rsd_handshake_free(handshake) + self.handshake = nil + } + if let adapter { + adapter_free(adapter) + self.adapter = nil + } + } +} + +enum DeviceTransportError: LocalizedError, Equatable { + case invalidIPAddress(String) + case pairingFileMissing(String) + case pairingFileReadFailed(String) + case vpnUnavailable(String) + case ffiFailure(String) + case incompleteTunnel + + var errorDescription: String? { + switch self { + case .invalidIPAddress(let ip): + return "Invalid RPPairing IPv4 address: (ip)" + case .pairingFileMissing(let path): + return "Pairing file not found at (path)" + case .pairingFileReadFailed(let message): + return "Failed to read the RPPairing file: (message)" + case .vpnUnavailable(let message): + return "Embedded VPN unavailable: (message)" + case .ffiFailure(let message): + return "RPPairing tunnel creation failed: (message)" + case .incompleteTunnel: + return "RPPairing returned incomplete tunnel handles" + } + } +} + +protocol DeviceTransport { + func ensureReady() throws + func makeRPPairingTunnel( + hostname: String, + targetIPAddress: String, + pairingFileURL: URL + ) throws -> RPPairingTunnel +} + +final class EmbeddedDeviceTransport: DeviceTransport { + static let shared = EmbeddedDeviceTransport() + + private init() {} + + func ensureReady() throws { + do { + try StikDebugVPNManager.shared.ensureReady() + } catch let error as StikDebugVPNError { + throw DeviceTransportError.vpnUnavailable(error.localizedDescription) + } catch { + throw DeviceTransportError.vpnUnavailable(error.localizedDescription) + } + } + + func makeRPPairingTunnel( + hostname: String, + targetIPAddress: String, + pairingFileURL: URL + ) throws -> RPPairingTunnel { + try ensureReady() + + guard FileManager.default.fileExists(atPath: pairingFileURL.path) else { + throw DeviceTransportError.pairingFileMissing(pairingFileURL.path) + } + + let endpoint = try RPPairingEndpoint(ip: targetIPAddress) + + var pairingFile: OpaquePointer? + let pairingError = pairingFileURL.path.withCString { path in + rp_pairing_file_read(path, &pairingFile) + } + if let pairingError { + throw DeviceTransportError.pairingFileReadFailed( + consumeFFIError(pairingError, fallback: "unknown pairing-file error") + ) + } + + guard let pairingFile else { + throw DeviceTransportError.pairingFileReadFailed("empty pairing handle") + } + defer { rp_pairing_file_free(pairingFile) } + + var address = endpoint.sockaddr + var tunnel = RPPairingTunnel() + let ffiError = hostname.withCString { hostnamePointer in + withUnsafePointer(to: &address) { addressPointer in + addressPointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { + tunnel_create_rppairing( + $0, + socklen_t(MemoryLayout.stride), + hostnamePointer, + pairingFile, + nil, + nil, + &tunnel.adapter, + &tunnel.handshake + ) + } + } + } + + if let ffiError { + let message = consumeFFIError(ffiError, fallback: "unknown RPPairing error") + tunnel.free() + throw DeviceTransportError.ffiFailure(message) + } + + guard tunnel.adapter != nil, tunnel.handshake != nil else { + tunnel.free() + throw DeviceTransportError.incompleteTunnel + } + + return tunnel + } + + private func consumeFFIError( + _ ffiError: UnsafeMutablePointer, + fallback: String + ) -> String { + let message = String(validatingUTF8: ffiError.pointee.message) ?? fallback + idevice_error_free(ffiError) + return message + } +} diff --git a/StikJIT/Utilities/IdeviceFFIBridge.swift b/StikJIT/Utilities/IdeviceFFIBridge.swift index 6871c79..0690705 100644 --- a/StikJIT/Utilities/IdeviceFFIBridge.swift +++ b/StikJIT/Utilities/IdeviceFFIBridge.swift @@ -764,49 +764,33 @@ func simulate_location(_ deviceIP: String, _ latitude: Double, _ longitude: Doub } } - var address = sockaddr_in() - address.sin_family = sa_family_t(AF_INET) - address.sin_port = in_port_t(49152).bigEndian - - let inetResult = deviceIP.withCString { inet_pton(AF_INET, $0, &address.sin_addr) } - guard inetResult == 1 else { - return LocationSimulationStatus.invalidIP - } - - var pairingHandle: OpaquePointer? - let pairingError = pairingFile.withCString { rp_pairing_file_read($0, &pairingHandle) } - if let pairingError { - idevice_error_free(pairingError) - return LocationSimulationStatus.pairingRead - } - - guard let pairingHandle else { - return LocationSimulationStatus.pairingRead - } - - defer { rp_pairing_file_free(pairingHandle) } - - let providerError = withUnsafePointer(to: &address) { pointer in - pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { - tunnel_create_rppairing( - $0, - socklen_t(MemoryLayout.stride), - "StikDebugLocation", - pairingHandle, - nil, - nil, - &LocationSimulationState.adapter, - &LocationSimulationState.handshake - ) + var tunnel: RPPairingTunnel + do { + tunnel = try EmbeddedDeviceTransport.shared.makeRPPairingTunnel( + hostname: "StikDebugLocation", + targetIPAddress: deviceIP, + pairingFileURL: URL(fileURLWithPath: pairingFile) + ) + } catch let error as DeviceTransportError { + switch error { + case .invalidIPAddress(_): + return LocationSimulationStatus.invalidIP + case .pairingFileMissing(_), .pairingFileReadFailed(_): + return LocationSimulationStatus.pairingRead + case .vpnUnavailable(_), .ffiFailure(_), .incompleteTunnel: + LocationSimulationState.cleanup() + return LocationSimulationStatus.providerCreate } - } - - if let providerError { - idevice_error_free(providerError) + } catch { LocationSimulationState.cleanup() return LocationSimulationStatus.providerCreate } + LocationSimulationState.adapter = tunnel.adapter + LocationSimulationState.handshake = tunnel.handshake + tunnel.adapter = nil + tunnel.handshake = nil + let remoteServerError = remote_server_connect_rsd( LocationSimulationState.adapter, LocationSimulationState.handshake, diff --git a/StikJIT/Utilities/JITEnableContext.swift b/StikJIT/Utilities/JITEnableContext.swift index 04308c0..afc02ff 100644 --- a/StikJIT/Utilities/JITEnableContext.swift +++ b/StikJIT/Utilities/JITEnableContext.swift @@ -7,7 +7,6 @@ import Foundation import idevice -import Darwin typealias LogFunc = (String?) -> Void typealias DebugAppCallback = (_ pid: Int32, _ debugProxy: OpaquePointer?, _ remoteServer: OpaquePointer?, _ semaphore: DispatchSemaphore) -> Void @@ -17,24 +16,9 @@ typealias SyslogErrorHandler = (NSError?) -> Void final class JITEnableContext { static let shared = JITEnableContext() - private struct TunnelHandles { - var adapter: OpaquePointer? - var handshake: OpaquePointer? - - mutating func free() { - if let handshake { - rsd_handshake_free(handshake) - self.handshake = nil - } - if let adapter { - adapter_free(adapter) - self.adapter = nil - } - } - } - private var adapter: OpaquePointer? private var handshake: OpaquePointer? + private let transport: DeviceTransport = EmbeddedDeviceTransport.shared private let tunnelLock = NSLock() private var tunnelConnecting = false @@ -113,72 +97,12 @@ final class JITEnableContext { logger?(message) } - private func getPairingFile() throws -> OpaquePointer { - let pairingFileURL = PairingFileStore.prepareURL() - - guard FileManager.default.fileExists(atPath: pairingFileURL.path) else { - throw makeError("Pairing file not found!", code: -17) - } - - var pairingFile: OpaquePointer? - let ffiError = pairingFileURL.path.withCString { path in - rp_pairing_file_read(path, &pairingFile) - } - - if let ffiError { - throw error(from: ffiError, fallback: "Failed to read pairing file!") - } - - guard let pairingFile else { - throw makeError("Failed to read pairing file!", code: -17) - } - - return pairingFile - } - - private func createTunnel(hostname: String) throws -> TunnelHandles { - let pairingFile = try getPairingFile() - defer { rp_pairing_file_free(pairingFile) } - - var addr = sockaddr_in() - addr.sin_family = sa_family_t(AF_INET) - addr.sin_port = in_port_t(49152).bigEndian - - let deviceIP = DeviceConnectionContext.targetIPAddress - let parseResult = deviceIP.withCString { inet_pton(AF_INET, $0, &addr.sin_addr) } - guard parseResult == 1 else { - throw makeError("Failed to parse target IP address.", code: -18) - } - - var tunnel = TunnelHandles() - let ffiError = hostname.withCString { hostname in - withUnsafePointer(to: &addr) { pointer in - pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { - tunnel_create_rppairing( - $0, - socklen_t(MemoryLayout.stride), - hostname, - pairingFile, - nil, - nil, - &tunnel.adapter, - &tunnel.handshake - ) - } - } - } - - if let ffiError { - throw error(from: ffiError, fallback: "Failed to create tunnel") - } - - guard tunnel.adapter != nil, tunnel.handshake != nil else { - var incompleteTunnel = tunnel - incompleteTunnel.free() - throw makeError("Tunnel was created without valid handles") - } - - return tunnel + private func createTunnel(hostname: String) throws -> RPPairingTunnel { + try transport.makeRPPairingTunnel( + hostname: hostname, + targetIPAddress: DeviceConnectionContext.targetIPAddress, + pairingFileURL: PairingFileStore.prepareURL() + ) } func startTunnel() throws { @@ -281,16 +205,16 @@ final class JITEnableContext { private let startupSemaphore = DispatchSemaphore(value: 0) private let stoppedSemaphore = DispatchSemaphore(value: 0) private let logger: LogFunc? - private let makeClient: () throws -> (client: OpaquePointer, tunnel: TunnelHandles) + private let makeClient: () throws -> (client: OpaquePointer, tunnel: RPPairingTunnel) private let errorBuilder: (UnsafeMutablePointer?, String) -> NSError private var startupError: NSError? private var client: OpaquePointer? - private var tunnel: TunnelHandles? + private var tunnel: RPPairingTunnel? private var stopRequested = false init( logger: LogFunc?, - makeClient: @escaping () throws -> (client: OpaquePointer, tunnel: TunnelHandles), + makeClient: @escaping () throws -> (client: OpaquePointer, tunnel: RPPairingTunnel), errorBuilder: @escaping (UnsafeMutablePointer?, String) -> NSError ) { self.logger = logger diff --git a/StikJITTests/StikJITTests.swift b/StikJITTests/StikJITTests.swift index 5ac1b26..29c1516 100644 --- a/StikJITTests/StikJITTests.swift +++ b/StikJITTests/StikJITTests.swift @@ -11,6 +11,26 @@ import Testing struct StikJITTests { + @Test func transportEndpointUsesRPPairingPort() throws { + let endpoint = try RPPairingEndpoint(ip: "10.7.0.1") + + #expect(endpoint.port == 49152) + #expect(endpoint.ip == "10.7.0.1") + } + + @Test func transportEndpointRejectsMalformedIPv4() { + #expect(throws: DeviceTransportError.self) { + try RPPairingEndpoint(ip: "not-an-ip") + } + } + + @Test func transportErrorsDoNotReportWiFiAsTheRootCause() { + let message = DeviceTransportError.vpnUnavailable("permission denied").errorDescription ?? "" + + #expect(!message.localizedCaseInsensitiveContains("wifi")) + #expect(message.localizedCaseInsensitiveContains("vpn")) + } + @Test func vpnStatusMappingRecognizesConnectedAndReasserting() { #expect(StikDebugVPNStatus(neStatus: .connected) == .connected) #expect(StikDebugVPNStatus(neStatus: .reasserting) == .connecting)