refactor: route rsd connections through embedded vpn

This commit is contained in:
zy860 2026-08-18 14:42:31 +08:00
parent 3fa5d488c1
commit b86f6d9f24
4 changed files with 232 additions and 124 deletions

View file

@ -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<sockaddr_in>.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<IdeviceFfiError>,
fallback: String
) -> String {
let message = String(validatingUTF8: ffiError.pointee.message) ?? fallback
idevice_error_free(ffiError)
return message
}
}

View file

@ -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<sockaddr_in>.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,

View file

@ -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<sockaddr_in>.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<IdeviceFfiError>?, 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<IdeviceFfiError>?, String) -> NSError
) {
self.logger = logger

View file

@ -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)