mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
feat: remove wifi and external vpn requirements
This commit is contained in:
parent
b86f6d9f24
commit
aadf40aa30
11 changed files with 219 additions and 163 deletions
12
README.md
12
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// StikDebugTunnel
|
||||
//
|
||||
|
||||
import Darwin
|
||||
import NetworkExtension
|
||||
|
||||
final class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
|
|
|||
|
|
@ -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 错误"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
|
|
|
|||
|
|
@ -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<addrinfo>?
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ final class StikDebugVPNManager: ObservableObject {
|
|||
lastEnsureError = ensureError
|
||||
startingWaiter = nil
|
||||
stateLock.unlock()
|
||||
if ensureError != nil {
|
||||
publish(.failed)
|
||||
}
|
||||
waiter.signal()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue