diff --git a/StikDebug/Device/IdeviceFFIBridge.swift b/StikDebug/Device/IdeviceFFIBridge.swift
index 3e1c98e..a21329a 100644
--- a/StikDebug/Device/IdeviceFFIBridge.swift
+++ b/StikDebug/Device/IdeviceFFIBridge.swift
@@ -791,7 +791,7 @@ 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
+ address.sin_port = in_port_t(RemotePairingPortResolver.resolve()).bigEndian
let inetResult = deviceIP.withCString { inet_pton(AF_INET, $0, &address.sin_addr) }
guard inetResult == 1 else {
diff --git a/StikDebug/Device/JITEnableContext.swift b/StikDebug/Device/JITEnableContext.swift
index 9a37cf2..d2a0e78 100644
--- a/StikDebug/Device/JITEnableContext.swift
+++ b/StikDebug/Device/JITEnableContext.swift
@@ -9,6 +9,99 @@ import Foundation
import idevice
import Darwin
+final class RemotePairingPortResolver: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
+ static let fallback: UInt16 = 49152
+ private static let overrideKey = "TunnelRemotePairingPort"
+ private static let stateLock = NSLock()
+ private static var selectedEndpoint = "Not attempted yet"
+
+ /// The precise port and discovery method used for the latest connection attempt.
+ static var lastAttemptDescription: String {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ return selectedEndpoint
+ }
+
+ private static func record(port: UInt16, source: String) {
+ let endpoint = "\(DeviceConnectionContext.targetIPAddress):\(port) [\(source)]"
+ stateLock.lock()
+ selectedEndpoint = endpoint
+ stateLock.unlock()
+ NSLog("[StikDebug] Remote Pairing selected endpoint: %@", endpoint)
+ }
+
+ private let browser = NetServiceBrowser()
+ private var services: [NetService] = []
+ private var resolvedPort: UInt16?
+ private var resolvedServiceName: String?
+ private var discoveryError: String?
+
+ /// Resolve on a worker thread with an active run loop. An explicit Settings
+ /// override takes precedence when multicast DNS cannot see the local service.
+ static func resolve(timeout: TimeInterval = 5) -> UInt16 {
+ let override = (UserDefaults.standard.string(forKey: overrideKey) ?? "")
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ if !override.isEmpty, let port = UInt16(override), port != 0 {
+ record(port: port, source: "manual override")
+ return port
+ }
+ if !override.isEmpty {
+ NSLog("[StikDebug] Ignoring invalid manual Remote Pairing port: %@", override)
+ }
+
+ let worker = RemotePairingPortResolver()
+ let completion = DispatchSemaphore(value: 0)
+ DispatchQueue.global(qos: .utility).async {
+ worker.search(timeout: timeout)
+ completion.signal()
+ }
+ guard completion.wait(timeout: .now() + timeout + 1) == .success else {
+ record(port: fallback, source: "Bonjour timeout; fallback")
+ return fallback
+ }
+ if let port = worker.resolvedPort {
+ record(port: port, source: "Bonjour: \(worker.resolvedServiceName ?? "unknown service")")
+ return port
+ }
+ let detail = worker.discoveryError ?? "no _remotepairing._tcp service resolved"
+ record(port: fallback, source: "Bonjour unavailable (\(detail)); fallback")
+ return fallback
+ }
+
+ private func search(timeout: TimeInterval) {
+ browser.delegate = self
+ browser.schedule(in: .current, forMode: .default)
+ browser.searchForServices(ofType: "_remotepairing._tcp.", inDomain: "local.")
+ let deadline = Date().addingTimeInterval(timeout)
+ while resolvedPort == nil && Date() < deadline {
+ _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.1))
+ }
+ browser.stop()
+ services.forEach { $0.stop() }
+ }
+
+ func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
+ services.append(service)
+ service.delegate = self
+ service.resolve(withTimeout: 4)
+ }
+
+ func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) {
+ discoveryError = "browser error \(errorDict)"
+ }
+
+ func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
+ discoveryError = "service \(sender.name): \(errorDict)"
+ }
+
+ func netServiceDidResolveAddress(_ sender: NetService) {
+ if (1...65535).contains(sender.port), resolvedPort == nil {
+ resolvedPort = UInt16(sender.port)
+ resolvedServiceName = sender.name
+ }
+ }
+}
+
typealias LogFunc = (String?) -> Void
typealias DebugAppCallback = (_ pid: Int32, _ debugProxy: OpaquePointer?, _ remoteServer: OpaquePointer?, _ semaphore: DispatchSemaphore) -> Void
typealias SyslogLineHandler = (String) -> Void
@@ -169,7 +262,8 @@ final class JITEnableContext {
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
- addr.sin_port = in_port_t(49152).bigEndian
+ let remotePairingPort = RemotePairingPortResolver.resolve()
+ addr.sin_port = in_port_t(remotePairingPort).bigEndian
let deviceIP = DeviceConnectionContext.targetIPAddress
let parseResult = deviceIP.withCString { inet_pton(AF_INET, $0, &addr.sin_addr) }
diff --git a/StikDebug/Info.plist b/StikDebug/Info.plist
index 60fb164..62c20a9 100644
--- a/StikDebug/Info.plist
+++ b/StikDebug/Info.plist
@@ -16,8 +16,11 @@
+ NSLocalNetworkUsageDescription
+ StikDebug discovers this iPhone's Remote Pairing service to enable JIT.
NSBonjourServices
+ _remotepairing._tcp
_stikdebug._tcp
_stikdebug._udp
diff --git a/StikDebug/Services/TunnelManager.swift b/StikDebug/Services/TunnelManager.swift
index 444b88f..944725c 100644
--- a/StikDebug/Services/TunnelManager.swift
+++ b/StikDebug/Services/TunnelManager.swift
@@ -129,7 +129,7 @@ func markTunnelDisconnected() {
}
private func tunnelConnectionLogMessage(for error: NSError) -> String {
- let target = "\(DeviceConnectionContext.targetIPAddress):49152"
+ let target = RemotePairingPortResolver.lastAttemptDescription
return "Tunnel connection failed for \(target): \(error.localizedDescription) (Domain: \(error.domain), Code: \(error.code), Raw: \(String(describing: error)))"
}
@@ -157,6 +157,14 @@ private func tunnelConnectionAlertMessage(for error: NSError) -> String {
"Reconnect Wi-Fi and LocalDevVPN, then try again.",
"If this keeps happening, select a fresh pairing file."
]
+ } else if error.code == 16 || lowercasedMessage.contains("device socket io failed") {
+ likelyCause = "The Remote Pairing tunnel could not connect to the selected endpoint."
+ recoverySteps = [
+ "Check the Target attempted line below (including whether Bonjour or fallback selected the port).",
+ "In SideStore Network Discovery, find the _remotepairing._tcp service port.",
+ "If it differs, set Settings > Advanced > Remote Pairing Port to SideStore's discovered port, then reconnect.",
+ "If the addresses and ports already match, check LocalDevVPN routing and the pairing file."
+ ]
} else if error.code == -18 || lowercasedMessage.contains("parse target ip") {
likelyCause = "The configured target IP address is not valid."
recoverySteps = [
@@ -193,7 +201,7 @@ private func tunnelConnectionAlertMessage(for error: NSError) -> String {
return """
\(likelyCause)
- Target: \(targetIP):49152
+ Target attempted: \(RemotePairingPortResolver.lastAttemptDescription)
Expected LocalDevVPN IP: \(DeviceConnectionContext.defaultTargetIPAddress)
Try this:
diff --git a/StikDebug/Views/SettingsView.swift b/StikDebug/Views/SettingsView.swift
index 871fe31..9906b0e 100644
--- a/StikDebug/Views/SettingsView.swift
+++ b/StikDebug/Views/SettingsView.swift
@@ -19,6 +19,7 @@ struct SettingsView: View {
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
@AppStorage(UserDefaults.Keys.targetDeviceIP) private var targetDeviceIP = DeviceConnectionContext.defaultTargetIPAddress
+ @AppStorage("TunnelRemotePairingPort") private var remotePairingPortOverride = ""
@AppStorage(UserDefaults.Keys.mallocDebug) private var mallocDebug = false
@State private var isShowingPairingFilePicker = false
@@ -153,6 +154,18 @@ struct SettingsView: View {
.keyboardType(.numbersAndPunctuation)
.frame(maxWidth: 160)
}
+ HStack {
+ Text("Remote Pairing Port")
+ Spacer()
+ TextField("Auto (Bonjour)", text: $remotePairingPortOverride)
+ .multilineTextAlignment(.trailing)
+ .foregroundStyle(.secondary)
+ .keyboardType(.numberPad)
+ .frame(maxWidth: 160)
+ }
+ Text("Leave blank for automatic discovery. Use the port reported by SideStore Network Discovery if Bonjour fails. Restart the tunnel after changing this value.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
Button { openAppFolder() } label: {
Label("App Folder", systemImage: "folder")
}.foregroundStyle(.primary)