mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
refactor: route rsd connections through embedded vpn
This commit is contained in:
parent
3fa5d488c1
commit
b86f6d9f24
4 changed files with 232 additions and 124 deletions
180
StikJIT/Utilities/DeviceTransport.swift
Normal file
180
StikJIT/Utilities/DeviceTransport.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue