From 61542ce0f6aeae6dd29da2c5d9da3c62af8b5917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=8E=E5=AE=87?= <69169091+XMing673@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:18:18 +0800 Subject: [PATCH] fix: dynamically resolve Remote Pairing ports for JIT and device tools Discover _remotepairing._tcp via Bonjour, retain the 49152 fallback, expose a manual port override, and report the actual selected endpoint on connection failure. --- StikDebug/Device/IdeviceFFIBridge.swift | 2 +- StikDebug/Device/JITEnableContext.swift | 96 ++++++++++++++++++++++++- StikDebug/Info.plist | 3 + StikDebug/Services/TunnelManager.swift | 12 +++- StikDebug/Views/SettingsView.swift | 13 ++++ 5 files changed, 122 insertions(+), 4 deletions(-) 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)