From aadf40aa30ebf83321568e5e8d1bcf797933a048 Mon Sep 17 00:00:00 2001 From: zy860 Date: Tue, 18 Aug 2026 14:50:24 +0800 Subject: [PATCH] feat: remove wifi and external vpn requirements --- README.md | 12 +- StikDebug.xcodeproj/project.pbxproj | 2 + StikDebugTunnel/PacketTunnelProvider.swift | 1 + StikJIT/Resources/Localizable.xcstrings | 108 ++++++++++++++- StikJIT/StikJIT.entitlements | 4 + StikJIT/StikJITApp.swift | 143 +------------------- StikJIT/Utilities/DeviceTransport.swift | 10 +- StikJIT/Utilities/StikDebugVPNManager.swift | 3 + StikJIT/Utilities/StikDebugVPNStatus.swift | 6 +- StikJIT/Views/SettingsView.swift | 92 ++++++++++++- StikJITTests/StikJITTests.swift | 1 + 11 files changed, 219 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 8a4f8bd..e6098f4 100644 --- a/README.md +++ b/README.md @@ -62,22 +62,22 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com - StikDebug installed (via AltSource, direct .ipa, or self-built) - A valid **pairing file** (.plist / .mobiledevicepairing) for your device - SideStore / AltStore / similar sideload tool (for app refreshing) -- A loopback VPN such as [LocalDevVPN](https://apps.apple.com/us/app/localdevvpn/id6755608044) +- Permission to install and run StikDebug's embedded Network Extension VPN ### Steps 1. **Obtain a pairing file** - Detailed guide: [Pairing File Instructions](https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord). -2. **Set up VPN** - - Launch LocalDevVPN and enable the VPN. +2. **Set up the embedded VPN** + - Open StikDebug's Settings and tap **Start VPN**. The app creates and controls its own local loopback tunnel. -4. **Enable JIT for an app** - - Launch StikDebug and tapp the `Enable JIT` button. +3. **Enable JIT for an app** + - Launch StikDebug and tap the `Enable JIT` button. - Select your sideloaded app from the list in StikDebug. **Troubleshooting** - "Connection dropped" or loopback errors → Check iOS version compatibility / beta warnings. -- Heartbeat errors → Ensure that the VPN is on and that you are connecected to Wi-Fi. It may be a pairing file issue. +- Heartbeat errors → Ensure that the embedded VPN is connected. It may be a pairing file issue. - Pairing file issues → Replace file with device unlocked & trusted. - Still stuck? Join the [Discord](https://discord.gg/ZnNcrRT3M8) with logs/screenshots. diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj index d3727c3..e28285d 100644 --- a/StikDebug.xcodeproj/project.pbxproj +++ b/StikDebug.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ A1B2C3D40123456789ABC002 /* IPv4PacketRewriter.swift in StikDebugTunnel Sources */ = {isa = PBXBuildFile; fileRef = A3B2C3D40123456789ABC004 /* IPv4PacketRewriter.swift */; }; A1B2C3D40123456789ABC003 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3B2C3D40123456789ABC002 /* NetworkExtension.framework */; }; A1B2C3D40123456789ABC004 /* StikDebugTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A3B2C3D40123456789ABC001 /* StikDebugTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + A1B2C3D40123456789ABC005 /* NetworkExtension.framework in StikDebug Frameworks */ = {isa = PBXBuildFile; fileRef = A3B2C3D40123456789ABC002 /* NetworkExtension.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -108,6 +109,7 @@ files = ( 68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */, 68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */, + A1B2C3D40123456789ABC005 /* NetworkExtension.framework in StikDebug Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/StikDebugTunnel/PacketTunnelProvider.swift b/StikDebugTunnel/PacketTunnelProvider.swift index 7407468..1edd98a 100644 --- a/StikDebugTunnel/PacketTunnelProvider.swift +++ b/StikDebugTunnel/PacketTunnelProvider.swift @@ -3,6 +3,7 @@ // StikDebugTunnel // +import Darwin import NetworkExtension final class PacketTunnelProvider: NEPacketTunnelProvider { diff --git a/StikJIT/Resources/Localizable.xcstrings b/StikJIT/Resources/Localizable.xcstrings index 64eff22..e713a29 100644 --- a/StikJIT/Resources/Localizable.xcstrings +++ b/StikJIT/Resources/Localizable.xcstrings @@ -664,19 +664,121 @@ } } }, - "Download LocalDevVPN" : { + "Embedded VPN" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Download LocalDevVPN" + "value" : "Embedded VPN" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "下载 LocalDevVPN" + "value" : "内置 VPN" + } + } + } + }, + "Start VPN" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start VPN" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启动 VPN" + } + } + } + }, + "Stop VPN" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stop VPN" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止 VPN" + } + } + } + }, + "VPN connected" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN connected" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN 已连接" + } + } + } + }, + "VPN connecting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN connecting" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN 连接中" + } + } + } + }, + "VPN disconnected" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN disconnected" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN 未连接" + } + } + } + }, + "VPN failed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN failed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN 错误" } } } diff --git a/StikJIT/StikJIT.entitlements b/StikJIT/StikJIT.entitlements index 3dabfcc..9b362a9 100644 --- a/StikJIT/StikJIT.entitlements +++ b/StikJIT/StikJIT.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.networking.networkextension + + packet-tunnel-provider + com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index d817944..713211e 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Network import idevice // Register default settings before the app starts @@ -20,101 +19,6 @@ private func registerAdvancedOptionsDefault() { UserDefaults.standard.register(defaults: ["keepAliveLocation": true]) } -// MARK: - DNS Checker - -class DNSChecker: ObservableObject { - @Published var appleIP: String? - @Published var controlIP: String? - @Published var dnsError: String? - - func checkDNS() { - checkIfConnectedToWifi { [weak self] wifiConnected in - guard let self = self else { return } - if wifiConnected { - let group = DispatchGroup() - - group.enter() - self.lookupIPAddress(for: "gs.apple.com") { ip in - DispatchQueue.main.async { - self.appleIP = ip - } - group.leave() - } - - group.enter() - self.lookupIPAddress(for: "google.com") { ip in - DispatchQueue.main.async { - self.controlIP = ip - } - group.leave() - } - - group.notify(queue: .main) { - if self.controlIP == nil { - self.dnsError = "No internet connection." - } else if self.appleIP == nil { - self.dnsError = "Apple DNS blocked. Your network might be filtering Apple traffic." - } else { - self.dnsError = nil - } - } - } else { - DispatchQueue.main.async { - self.dnsError = nil - } - } - } - } - - private func checkIfConnectedToWifi(completion: @escaping (Bool) -> Void) { - let monitor = NWPathMonitor(requiredInterfaceType: .wifi) - monitor.pathUpdateHandler = { path in - completion(path.status == .satisfied) - monitor.cancel() - } - let queue = DispatchQueue.global(qos: .background) - monitor.start(queue: queue) - } - - private func lookupIPAddress(for host: String, completion: @escaping (String?) -> Void) { - DispatchQueue.global(qos: .background).async { - var hints = addrinfo( - ai_flags: 0, - ai_family: AF_UNSPEC, - ai_socktype: SOCK_STREAM, - ai_protocol: 0, - ai_addrlen: 0, - ai_canonname: nil, - ai_addr: nil, - ai_next: nil - ) - var res: UnsafeMutablePointer? - let err = getaddrinfo(host, nil, &hints, &res) - if err != 0 { - DispatchQueue.main.async { completion(nil) } - return - } - - var ipAddress: String? - var ptr = res - while ptr != nil { - if let addr = ptr?.pointee.ai_addr { - var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - if getnameinfo(addr, ptr!.pointee.ai_addrlen, - &hostBuffer, socklen_t(hostBuffer.count), - nil, 0, NI_NUMERICHOST) == 0 { - ipAddress = String(cString: hostBuffer) - break - } - } - ptr = ptr?.pointee.ai_next - } - freeaddrinfo(res) - DispatchQueue.main.async { completion(ipAddress) } - } - } -} - // MARK: - Main App // Global state variable for the tunnel connection. @@ -342,7 +246,7 @@ func startTunnelInBackground(showErrorUI: Bool = true) { } else { showAlert( title: "Connection Error", - message: "\(error.localizedDescription)\n\nMake sure Wi‑Fi and LocalDevVPN are connected and that the device is reachable.", + message: "\(error.localizedDescription)\n\nCheck that the embedded VPN is connected and that the pairing file is current.", showOk: false, showTryAgain: true ) { shouldTryAgain in @@ -360,51 +264,6 @@ func startTunnelInBackground(showErrorUI: Bool = true) { } -func checkDeviceConnection(callback: @escaping (Bool, String?) -> Void) { - let targetIP = DeviceConnectionContext.targetIPAddress - let host = NWEndpoint.Host(targetIP) - let port = NWEndpoint.Port(rawValue: 62078)! - let connection = NWConnection(host: host, port: port, using: .tcp) - var timeoutWorkItem: DispatchWorkItem? - - timeoutWorkItem = DispatchWorkItem { [weak connection] in - if connection?.state != .ready { - connection?.cancel() - DispatchQueue.main.async { - if timeoutWorkItem?.isCancelled == false { - let message = "[TIMEOUT] Could not reach the device at \(targetIP). Make sure it’s online and on the same network." - callback(false, message) - } - } - } - } - - connection.stateUpdateHandler = { [weak connection] state in - switch state { - case .ready: - timeoutWorkItem?.cancel() - connection?.cancel() - DispatchQueue.main.async { - callback(true, nil) - } - case .failed(let error): - timeoutWorkItem?.cancel() - connection?.cancel() - DispatchQueue.main.async { - let message = "Could not reach the device at \(targetIP): \(error.localizedDescription)" - callback(false, message) - } - default: - break - } - } - - connection.start(queue: .global()) - if let workItem = timeoutWorkItem { - DispatchQueue.global().asyncAfter(deadline: .now() + 20, execute: workItem) - } -} - public func showAlert(title: String, message: String, showOk: Bool, showTryAgain: Bool = false, primaryButtonText: String? = nil, completion: ((Bool) -> Void)? = nil) { DispatchQueue.main.async { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, diff --git a/StikJIT/Utilities/DeviceTransport.swift b/StikJIT/Utilities/DeviceTransport.swift index f1f1aa2..63a1730 100644 --- a/StikJIT/Utilities/DeviceTransport.swift +++ b/StikJIT/Utilities/DeviceTransport.swift @@ -69,15 +69,15 @@ enum DeviceTransportError: LocalizedError, Equatable { var errorDescription: String? { switch self { case .invalidIPAddress(let ip): - return "Invalid RPPairing IPv4 address: (ip)" + return "Invalid RPPairing IPv4 address: \(ip)" case .pairingFileMissing(let path): - return "Pairing file not found at (path)" + return "Pairing file not found at \(path)" case .pairingFileReadFailed(let message): - return "Failed to read the RPPairing file: (message)" + return "Failed to read the RPPairing file: \(message)" case .vpnUnavailable(let message): - return "Embedded VPN unavailable: (message)" + return "Embedded VPN unavailable: \(message)" case .ffiFailure(let message): - return "RPPairing tunnel creation failed: (message)" + return "RPPairing tunnel creation failed: \(message)" case .incompleteTunnel: return "RPPairing returned incomplete tunnel handles" } diff --git a/StikJIT/Utilities/StikDebugVPNManager.swift b/StikJIT/Utilities/StikDebugVPNManager.swift index 4cd1c42..87d6081 100644 --- a/StikJIT/Utilities/StikDebugVPNManager.swift +++ b/StikJIT/Utilities/StikDebugVPNManager.swift @@ -53,6 +53,9 @@ final class StikDebugVPNManager: ObservableObject { lastEnsureError = ensureError startingWaiter = nil stateLock.unlock() + if ensureError != nil { + publish(.failed) + } waiter.signal() } diff --git a/StikJIT/Utilities/StikDebugVPNStatus.swift b/StikJIT/Utilities/StikDebugVPNStatus.swift index d5a05b6..5d504ca 100644 --- a/StikJIT/Utilities/StikDebugVPNStatus.swift +++ b/StikJIT/Utilities/StikDebugVPNStatus.swift @@ -44,11 +44,11 @@ enum StikDebugVPNError: LocalizedError, Equatable { var errorDescription: String? { switch self { case .profileLoadFailed(let message): - return "Unable to load the embedded VPN profile: (message)" + return "Unable to load the embedded VPN profile: \(message)" case .profileSaveFailed(let message): - return "Unable to save the embedded VPN profile: (message)" + return "Unable to save the embedded VPN profile: \(message)" case .permissionDenied(let message): - return "The embedded VPN permission was denied: (message)" + return "The embedded VPN permission was denied: \(message)" case .competingVPN: return "Another VPN is active. Stop it before starting the StikDebug local tunnel." case .timeout: diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 94a5dde..5021def 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -9,7 +9,6 @@ import UIKit private enum SettingsLinks { static let githubStars = URL(string: "https://github.com/StephenDev0/StikDebug/stargazers")! static let pairingFileGuide = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")! - static let localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")! static let discord = URL(string: "https://discord.gg/qahjXNTDwS")! } @@ -19,6 +18,7 @@ struct SettingsView: View { @AppStorage("keepAliveLocation") private var keepAliveLocation = true @AppStorage("customTargetIP") private var customTargetIP = "" @Environment(LanguageManager.self) private var langManager + @StateObject private var vpnManager = StikDebugVPNManager.shared @State private var isShowingPairingFilePicker = false @State private var isImportingFile = false @@ -28,6 +28,7 @@ struct SettingsView: View { @State private var ddiDownloadProgress: Double = 0.0 @State private var ddiStatusMessage: String = "" @State private var ddiResultMessage: (text: String, isError: Bool)? + @State private var vpnErrorMessage: String? private var appVersion: String { let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" @@ -96,6 +97,38 @@ struct SettingsView: View { } } + Section("Embedded VPN".localized) { + HStack { + Label(vpnStatusTitle.localized, systemImage: vpnStatusIcon) + Spacer() + Circle() + .fill(vpnStatusColor) + .frame(width: 9, height: 9) + } + + if vpnManager.currentStatus == .connected || vpnManager.currentStatus == .connecting { + Button { + vpnManager.stop() + vpnErrorMessage = nil + } label: { + Label("Stop VPN".localized, systemImage: "stop.circle") + } + } else { + Button { + startEmbeddedVPN() + } label: { + Label("Start VPN".localized, systemImage: "play.circle") + } + .disabled(vpnManager.currentStatus == .connecting) + } + + if let vpnErrorMessage { + Text(vpnErrorMessage) + .font(.caption) + .foregroundStyle(.red) + } + } + Section { Toggle(isOn: $keepAliveAudio) { VStack(alignment: .leading, spacing: 2) { @@ -163,9 +196,6 @@ struct SettingsView: View { Link(destination: SettingsLinks.pairingFileGuide) { Label("Pairing File Guide".localized, systemImage: "questionmark.circle") } - Link(destination: SettingsLinks.localDevVPN) { - Label("Download LocalDevVPN".localized, systemImage: "arrow.down.circle") - } Link(destination: SettingsLinks.discord) { Label("Discord Support".localized, systemImage: "bubble.left.and.bubble.right") } @@ -231,6 +261,60 @@ struct SettingsView: View { return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)" } + private var vpnStatusTitle: String { + switch vpnManager.currentStatus { + case .connected: + return "VPN connected" + case .connecting: + return "VPN connecting" + case .disconnecting: + return "VPN connecting" + case .failed: + return "VPN failed" + case .disconnected: + return "VPN disconnected" + } + } + + private var vpnStatusIcon: String { + switch vpnManager.currentStatus { + case .connected: + return "checkmark.shield.fill" + case .connecting, .disconnecting: + return "arrow.triangle.2.circlepath" + case .failed: + return "exclamationmark.shield.fill" + case .disconnected: + return "shield.slash" + } + } + + private var vpnStatusColor: Color { + switch vpnManager.currentStatus { + case .connected: + return .green + case .connecting, .disconnecting: + return .orange + case .failed: + return .red + case .disconnected: + return .secondary + } + } + + private func startEmbeddedVPN() { + vpnErrorMessage = nil + DispatchQueue.global(qos: .userInitiated).async { + do { + try vpnManager.start() + } catch { + DispatchQueue.main.async { + vpnErrorMessage = error.localizedDescription + } + } + } + } + // MARK: - Business Logic private func openAppFolder() { diff --git a/StikJITTests/StikJITTests.swift b/StikJITTests/StikJITTests.swift index 29c1516..d3a980f 100644 --- a/StikJITTests/StikJITTests.swift +++ b/StikJITTests/StikJITTests.swift @@ -59,6 +59,7 @@ struct StikJITTests { @Test func ipv4PacketRewriterSwapsPacketEndpoints() { var packet = [UInt8](repeating: 0, count: 20) + packet[0] = 0x45 packet.replaceSubrange(12..<16, with: [10, 7, 0, 2]) packet.replaceSubrange(16..<20, with: [10, 7, 0, 1])