Debug pt 2

This commit is contained in:
Stephen 2025-04-16 00:27:13 -04:00
parent 50b77252b1
commit 8bcd12f4bc
8 changed files with 763 additions and 114 deletions

View file

@ -6,6 +6,11 @@
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
DCA690122DAF660E007C91A8 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA690112DAF660E007C91A8 /* NetworkExtension.framework */; };
DCA6901A2DAF660E007C91A8 /* TunnelProv.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCA690102DAF660E007C91A8 /* TunnelProv.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
DC6F1D492D94EADF0071B2B6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@ -21,13 +26,36 @@
remoteGlobalIDString = DC6F1D362D94EADD0071B2B6;
remoteInfo = StikJIT;
};
DCA690182DAF660E007C91A8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = DCA6900F2DAF660E007C91A8;
remoteInfo = TunnelProv;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
DCA6901F2DAF660E007C91A8 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
DCA6901A2DAF660E007C91A8 /* TunnelProv.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
DC5716242D9564760099302E /* em_proxy.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = em_proxy.xcframework; sourceTree = "<group>"; };
DC6F1D372D94EADD0071B2B6 /* StikDebug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StikDebug.app; sourceTree = BUILT_PRODUCTS_DIR; };
DC6F1D482D94EADF0071B2B6 /* StikDebugTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StikDebugTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StikDebugUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DCA690102DAF660E007C91A8 /* TunnelProv.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TunnelProv.appex; sourceTree = BUILT_PRODUCTS_DIR; };
DCA690112DAF660E007C91A8 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -38,6 +66,13 @@
);
target = DC6F1D362D94EADD0071B2B6 /* StikDebug */;
};
DCA6901E2DAF660E007C91A8 /* Exceptions for "TunnelProv" folder in "TunnelProv" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = DCA6900F2DAF660E007C91A8 /* TunnelProv */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@ -59,6 +94,14 @@
path = StikJITUITests;
sourceTree = "<group>";
};
DCA690132DAF660E007C91A8 /* TunnelProv */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
DCA6901E2DAF660E007C91A8 /* Exceptions for "TunnelProv" folder in "TunnelProv" target */,
);
path = TunnelProv;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@ -83,6 +126,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900D2DAF660E007C91A8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DCA690122DAF660E007C91A8 /* NetworkExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -92,6 +143,7 @@
DC6F1D392D94EADD0071B2B6 /* StikJIT */,
DC6F1D4B2D94EADF0071B2B6 /* StikJITTests */,
DC6F1D552D94EADF0071B2B6 /* StikJITUITests */,
DCA690132DAF660E007C91A8 /* TunnelProv */,
DC6F1D752D94EB620071B2B6 /* Frameworks */,
DC6F1D382D94EADD0071B2B6 /* Products */,
);
@ -103,6 +155,7 @@
DC6F1D372D94EADD0071B2B6 /* StikDebug.app */,
DC6F1D482D94EADF0071B2B6 /* StikDebugTests.xctest */,
DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */,
DCA690102DAF660E007C91A8 /* TunnelProv.appex */,
);
name = Products;
sourceTree = "<group>";
@ -111,6 +164,7 @@
isa = PBXGroup;
children = (
DC5716242D9564760099302E /* em_proxy.xcframework */,
DCA690112DAF660E007C91A8 /* NetworkExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -125,10 +179,12 @@
DC6F1D332D94EADD0071B2B6 /* Sources */,
DC6F1D342D94EADD0071B2B6 /* Frameworks */,
DC6F1D352D94EADD0071B2B6 /* Resources */,
DCA6901F2DAF660E007C91A8 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
DCA690192DAF660E007C91A8 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
DC6F1D392D94EADD0071B2B6 /* StikJIT */,
@ -186,6 +242,28 @@
productReference = DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
DCA6900F2DAF660E007C91A8 /* TunnelProv */ = {
isa = PBXNativeTarget;
buildConfigurationList = DCA6901B2DAF660E007C91A8 /* Build configuration list for PBXNativeTarget "TunnelProv" */;
buildPhases = (
DCA6900C2DAF660E007C91A8 /* Sources */,
DCA6900D2DAF660E007C91A8 /* Frameworks */,
DCA6900E2DAF660E007C91A8 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
DCA690132DAF660E007C91A8 /* TunnelProv */,
);
name = TunnelProv;
packageProductDependencies = (
);
productName = TunnelProv;
productReference = DCA690102DAF660E007C91A8 /* TunnelProv.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@ -208,6 +286,9 @@
CreatedOnToolsVersion = 16.2;
TestTargetID = DC6F1D362D94EADD0071B2B6;
};
DCA6900F2DAF660E007C91A8 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = DC6F1D322D94EADD0071B2B6 /* Build configuration list for PBXProject "StikDebug" */;
@ -227,6 +308,7 @@
DC6F1D362D94EADD0071B2B6 /* StikDebug */,
DC6F1D472D94EADF0071B2B6 /* StikDebugTests */,
DC6F1D512D94EADF0071B2B6 /* StikDebugUITests */,
DCA6900F2DAF660E007C91A8 /* TunnelProv */,
);
};
/* End PBXProject section */
@ -253,6 +335,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900E2DAF660E007C91A8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -277,6 +366,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900C2DAF660E007C91A8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -290,6 +386,11 @@
target = DC6F1D362D94EADD0071B2B6 /* StikDebug */;
targetProxy = DC6F1D532D94EADF0071B2B6 /* PBXContainerItemProxy */;
};
DCA690192DAF660E007C91A8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DCA6900F2DAF660E007C91A8 /* TunnelProv */;
targetProxy = DCA690182DAF660E007C91A8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@ -592,6 +693,63 @@
};
name = Release;
};
DCA6901C2DAF660E007C91A8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = TunnelProv/TunnelProv.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TunnelProv/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TunnelProv;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.TunnelProv;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
DCA6901D2DAF660E007C91A8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = TunnelProv/TunnelProv.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TunnelProv/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TunnelProv;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.TunnelProv;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -631,6 +789,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DCA6901B2DAF660E007C91A8 /* Build configuration list for PBXNativeTarget "TunnelProv" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DCA6901C2DAF660E007C91A8 /* Debug */,
DCA6901D2DAF660E007C91A8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = DC6F1D2F2D94EADD0071B2B6 /* Project object */;

View file

@ -2,9 +2,15 @@
<!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.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<string>allow-vpn</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View file

@ -8,6 +8,268 @@
import SwiftUI
import Network
import UniformTypeIdentifiers
import NetworkExtension
// MARK: - Welcome Sheet
struct WelcomeSheetView: View {
// A callback to dismiss the sheet and trigger VPN setup.
var onDismiss: (() -> Void)?
var body: some View {
VStack(spacing: 24) {
Text("Welcome!")
.font(.system(.largeTitle, design: .rounded).weight(.bold))
.foregroundColor(.primary)
.padding(.top)
Text("Thanks for installing the app. This brief introduction will help you get started.")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Text("StikDebug is an on-device debugger designed specifically for self-developed apps. It helps streamline testing and troubleshooting without sending any data to external servers.")
.font(.callout)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Text("The next step will prompt you to allow VPN permissions. This is necessary for the app to function properly. The VPN configuration allows your device to securely connect to itself — nothing more. Rest assured, no data is collected or sent externally. Everything stays on your device.")
.font(.callout)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Text("You might encounter a -17 error after your initial launch. Simply tap 'OK' and the issue will be resolved once a valid pairing file has been imported.")
.font(.callout)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Button(action: {
onDismiss?()
}) {
Text("Continue")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(RoundedRectangle(cornerRadius: 12).fill(Color.accentColor))
.foregroundColor(.white)
}
.padding(.horizontal)
.padding(.bottom)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
.shadow(radius: 20)
)
.padding()
}
}
// MARK: - VPN Logger
class VPNLogger: ObservableObject {
@Published var logs: [String] = []
static var shared = VPNLogger()
private init() {}
func log(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
let fileName = (file as NSString).lastPathComponent
print("[\(fileName):\(line)] \(function): \(message)")
#endif
logs.append("\(message)")
}
}
// MARK: - Tunnel Manager
class TunnelManager: ObservableObject {
@Published var tunnelStatus: TunnelStatus = .disconnected
static var shared = TunnelManager()
private var vpnManager: NETunnelProviderManager?
private var tunnelDeviceIp: String {
UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.0"
}
private var tunnelFakeIp: String {
UserDefaults.standard.string(forKey: "TunnelFakeIP") ?? "10.7.0.1"
}
private var tunnelSubnetMask: String {
UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0"
}
private var tunnelBundleId: String {
Bundle.main.bundleIdentifier!.appending(".TunnelProv")
}
enum TunnelStatus: String {
case disconnected = "Disconnected"
case connecting = "Connecting"
case connected = "Connected"
case disconnecting = "Disconnecting"
case error = "Error"
}
private init() {
loadTunnelPreferences()
NotificationCenter.default.addObserver(self, selector: #selector(statusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil)
}
private func loadTunnelPreferences() {
NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in
guard let self = self else { return }
DispatchQueue.main.async {
if let error = error {
VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)")
self.tunnelStatus = .error
return
}
if let managers = managers, !managers.isEmpty {
for manager in managers {
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
proto.providerBundleIdentifier == self.tunnelBundleId {
self.vpnManager = manager
self.updateTunnelStatus(from: manager.connection.status)
VPNLogger.shared.log("Loaded existing tunnel configuration")
break
}
}
if self.vpnManager == nil, let firstManager = managers.first {
self.vpnManager = firstManager
self.updateTunnelStatus(from: firstManager.connection.status)
VPNLogger.shared.log("Using existing tunnel configuration")
}
} else {
VPNLogger.shared.log("No existing tunnel configuration found")
}
}
}
}
@objc private func statusDidChange(_ notification: Notification) {
if let connection = notification.object as? NEVPNConnection {
updateTunnelStatus(from: connection.status)
}
}
private func updateTunnelStatus(from connectionStatus: NEVPNStatus) {
DispatchQueue.main.async {
switch connectionStatus {
case .invalid, .disconnected:
self.tunnelStatus = .disconnected
case .connecting:
self.tunnelStatus = .connecting
case .connected:
self.tunnelStatus = .connected
case .disconnecting:
self.tunnelStatus = .disconnecting
case .reasserting:
self.tunnelStatus = .connecting
@unknown default:
self.tunnelStatus = .error
}
VPNLogger.shared.log("VPN status updated: \(self.tunnelStatus.rawValue)")
}
}
private func createOrUpdateTunnelConfiguration(completion: @escaping (Bool) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in
guard let self = self else { return completion(false) }
if let error = error {
VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)")
return completion(false)
}
let manager: NETunnelProviderManager
if let existingManagers = managers, !existingManagers.isEmpty {
if let matchingManager = existingManagers.first(where: {
($0.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.tunnelBundleId
}) {
manager = matchingManager
VPNLogger.shared.log("Updating existing tunnel configuration")
} else {
manager = existingManagers[0]
VPNLogger.shared.log("Using first available tunnel configuration")
}
} else {
manager = NETunnelProviderManager()
VPNLogger.shared.log("Creating new tunnel configuration")
}
manager.localizedDescription = "StikDebug"
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = self.tunnelBundleId
proto.serverAddress = "StikDebug's Local Network Tunnel"
manager.protocolConfiguration = proto
manager.isOnDemandEnabled = true
manager.isEnabled = true
manager.saveToPreferences { [weak self] error in
guard let self = self else { return completion(false) }
DispatchQueue.main.async {
if let error = error {
VPNLogger.shared.log("Error saving tunnel configuration: \(error.localizedDescription)")
completion(false)
return
}
self.vpnManager = manager
VPNLogger.shared.log("Tunnel configuration saved successfully")
completion(true)
}
}
}
}
func startVPN() {
if let manager = vpnManager {
startExistingVPN(manager: manager)
} else {
createOrUpdateTunnelConfiguration { [weak self] success in
guard let self = self, success else { return }
self.loadTunnelPreferences()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let manager = self.vpnManager {
self.startExistingVPN(manager: manager)
}
}
}
}
}
private func startExistingVPN(manager: NETunnelProviderManager) {
guard tunnelStatus != .connected else {
VPNLogger.shared.log("Network tunnel is already connected")
return
}
tunnelStatus = .connecting
let options: [String: NSObject] = [
"TunnelDeviceIP": tunnelDeviceIp as NSObject,
"TunnelFakeIP": tunnelFakeIp as NSObject,
"TunnelSubnetMask": tunnelSubnetMask as NSObject
]
do {
try manager.connection.startVPNTunnel(options: options)
VPNLogger.shared.log("Network tunnel start initiated")
} catch {
tunnelStatus = .error
VPNLogger.shared.log("Failed to start tunnel: \(error.localizedDescription)")
}
}
func stopVPN() {
guard let manager = vpnManager else { return }
tunnelStatus = .disconnecting
manager.connection.stopVPNTunnel()
VPNLogger.shared.log("Network tunnel stop initiated")
}
}
// MARK: - AccentColor Environment Key
struct AccentColorKey: EnvironmentKey {
static let defaultValue: Color = .blue
@ -20,6 +282,8 @@ extension EnvironmentValues {
}
}
// MARK: - Helper Functions and Globals
let fileManager = FileManager.default
func httpGet(_ urlString: String, result: @escaping (String?) -> Void) {
@ -63,6 +327,8 @@ func UpdateRetrieval() -> Bool {
return res
}
// MARK: - DNS Checker
class DNSChecker: ObservableObject {
@Published var appleIP: String?
@Published var controlIP: String?
@ -168,8 +434,15 @@ class DNSChecker: ObservableObject {
}
}
// MARK: - Main App
// Global state variable for the heartbeat response.
var pubHeartBeat = false
@main
struct HeartbeatApp: App {
@AppStorage("hasLaunchedBefore") var hasLaunchedBefore: Bool = false
@State private var showWelcomeSheet: Bool = false
@State private var isLoading2 = true
@State private var isPairing = false
@State private var heartBeat = false
@ -249,109 +522,132 @@ struct HeartbeatApp: App {
var body: some Scene {
WindowGroup {
if isLoading2 {
// Pass bindings to LoadingView for alert handling and version check.
LoadingView(showAlert: $show_alert, alertTitle: $alert_title, alertMessage: $alert_string)
.onAppear {
dnsChecker.checkDNS()
startProxy() { result, error in
if result {
checkVPNConnection() { result, vpn_error in
if result {
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if pubHeartBeat {
isLoading2 = false
timer.invalidate()
} else {
if let error {
if error == -9 { // InvalidHostID is -9
isPairing = true
} else {
startHeartbeatInBackground()
Group {
if isLoading2 {
LoadingView(showAlert: $show_alert, alertTitle: $alert_title, alertMessage: $alert_string)
.onAppear {
dnsChecker.checkDNS()
startProxy() { result, error in
if result {
checkVPNConnection() { result, vpn_error in
if result {
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if pubHeartBeat {
isLoading2 = false
timer.invalidate()
} else {
if let error {
if error == -9 { // InvalidHostID is -9
isPairing = true
} else {
startHeartbeatInBackground()
}
self.error = nil
}
self.error = nil
}
}
startHeartbeatInBackground()
} else {
isLoading2 = false
}
} else if let vpn_error {
showAlert(title: "Error", message: "EM Proxy failed to connect: \(vpn_error)", showOk: true) { _ in
exit(0)
}
startHeartbeatInBackground()
} else {
isLoading2 = false
}
} else if let vpn_error {
showAlert(title: "Error", message: "EM Proxy failed to connect: \(vpn_error)", showOk: true) { _ in
exit(0)
}
}
} else if let error {
showAlert(title: "Error", message: "EM Proxy Failed to start \(error)", showOk: true) { _ in }
}
} else if let error {
showAlert(title: "Error", message: "EM Proxy Failed to start \(error)", showOk: true) { _ in }
}
}
}
.fileImporter(isPresented: $isPairing, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) { result in
switch result {
case .success(let url):
.fileImporter(
isPresented: $isPairing,
allowedContentTypes: [
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
.propertyList
]
) { result in
switch result {
case .success(let url):
let fileManager = FileManager.default
let accessing = url.startAccessingSecurityScopedResource()
if fileManager.fileExists(atPath: url.path) {
do {
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
}
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!")
startHeartbeatInBackground()
} catch {
print("Error copying file: \(error)")
}
} else {
print("Source file does not exist.")
}
if accessing {
url.stopAccessingSecurityScopedResource()
}
case .failure(_):
print("Failed")
}
}
} else {
MainTabView()
.onAppear {
applyTheme()
let fileManager = FileManager.default
let accessing = url.startAccessingSecurityScopedResource()
if fileManager.fileExists(atPath: url.path) {
do {
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
}
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!")
startHeartbeatInBackground()
} catch {
print("Error copying file: \(error)")
}
} else {
print("Source file does not exist.")
}
if accessing {
url.stopAccessingSecurityScopedResource()
}
case .failure(_):
print("Failed")
}
}
} else {
MainTabView()
.onAppear {
applyTheme()
let fileManager = FileManager.default
for (index, urlString) in urls.enumerated() {
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
if !fileManager.fileExists(atPath: destinationURL.path) {
downloadFile(from: urlString, to: destinationURL) { result in
if (result != "") {
alert_title = "An Error has Occurred"
alert_string = "[Download DDI Error]: " + result
show_alert = true
for (index, urlString) in urls.enumerated() {
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
if !fileManager.fileExists(atPath: destinationURL.path) {
downloadFile(from: urlString, to: destinationURL) { result in
if (result != "") {
alert_title = "An Error has Occurred"
alert_string = "[Download DDI Error]: " + result
show_alert = true
}
}
}
}
}
}
.overlay(
ZStack {
if show_alert {
CustomErrorView(
title: alert_title,
message: alert_string,
onDismiss: {
show_alert = false
},
showButton: true,
primaryButtonText: "OK"
)
.overlay(
ZStack {
if show_alert {
CustomErrorView(
title: alert_title,
message: alert_string,
onDismiss: {
show_alert = false
},
showButton: true,
primaryButtonText: "OK"
)
}
}
}
)
)
}
}
.onAppear {
// On first launch, present the welcome sheet.
// Otherwise, start the VPN automatically.
if !hasLaunchedBefore {
showWelcomeSheet = true
} else {
TunnelManager.shared.startVPN()
}
}
.sheet(isPresented: $showWelcomeSheet) {
WelcomeSheetView {
// When the user taps "Continue", mark the app as launched and start the VPN.
hasLaunchedBefore = true
showWelcomeSheet = false
TunnelManager.shared.startVPN()
}
}
}
.onChange(of: scenePhase) { newPhase in
@ -436,7 +732,7 @@ struct HeartbeatApp: App {
}
}
var pubHeartBeat = false
// MARK: - Additional Helpers
actor FunctionGuard<T> {
private var runningTask: Task<T, Never>?
@ -479,7 +775,6 @@ class MountingProgress: ObservableObject {
private func mount() {
self.coolisMounted = isMounted()
let fileManager = FileManager.default
let pairingpath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path
if isPairing(), !isMounted() {
@ -544,8 +839,7 @@ func startHeartbeatInBackground() {
print("Error: \(message ?? "") (Code: \(result))")
DispatchQueue.main.async {
// Special handling for InvalidHostID error (code -9)
if result == -9 { // InvalidHostID is -9
// Delete the invalid pairing file
if result == -9 {
do {
try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("Removed invalid pairing file")
@ -553,7 +847,6 @@ func startHeartbeatInBackground() {
print("Error removing invalid pairing file: \(error)")
}
// Show alert with option to select a new pairing file
showAlert(
title: "Invalid Pairing File",
message: "The pairing file is invalid or expired. Please select a new pairing file.",
@ -561,11 +854,9 @@ func startHeartbeatInBackground() {
showTryAgain: false,
primaryButtonText: "Select New File"
) { _ in
// This will be handled by the HomeView's fileImporter
NotificationCenter.default.post(name: NSNotification.Name("ShowPairingFilePicker"), object: nil)
}
} else {
// Handle other errors as before
showAlert(
title: "Heartbeat Error",
message: "Failed to connect to Heartbeat (\(result))",
@ -645,16 +936,14 @@ struct LoadingView: View {
.shadow(color: accentColor.opacity(0.4), radius: 10, x: 0, y: 0)
.onAppear {
animate = true
let os = ProcessInfo.processInfo.operatingSystemVersion
if os.majorVersion < 17 || (os.majorVersion == 17 && os.minorVersion < 4) {
// Show alert for unsupported host iOS version
alertTitle = "Unsupported OS Version"
alertMessage = "StikJIT only supports 17.4 and above. Your device is running iOS/iPadOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
showAlert = true
}
}
let os = ProcessInfo.processInfo.operatingSystemVersion
if os.majorVersion < 17 || (os.majorVersion == 17 && os.minorVersion < 4) {
alertTitle = "Unsupported OS Version"
alertMessage = "StikJIT only supports 17.4 and above. Your device is running iOS/iPadOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
showAlert = true
}
}
Text("Loading...")
.font(.system(size: 20, weight: .medium, design: .rounded))
.foregroundColor(isDarkMode ? .white.opacity(0.8) : .black.opacity(0.8))

View file

@ -84,7 +84,7 @@ struct HomeView: View {
}
}) {
HStack {
Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus")
Image(systemName: pairingFileExists ? "cable.connector.horizontal" : "doc.badge.plus")
.font(.system(size: 20))
Text(pairingFileExists ? "Connect" : "Select Pairing File")
.font(.system(.title3, design: .rounded))

View file

@ -358,10 +358,8 @@ struct SettingsView: View {
// Vertical stack of collaborators
VStack(spacing: 12) {
CollaboratorRow(name: "Stossy11", url: "https://github.com/Stossy11", imageUrl: developerProfiles["Stossy11"] ?? "", quote: "I helped I guess :3")
CollaboratorRow(name: "Neo", url: "https://neoarz.xyz/", imageUrl: developerProfiles["Neo"] ?? "", quote: "Chicken Jockey")
CollaboratorRow(name: "Stossy11", url: "https://github.com/Stossy11", imageUrl: developerProfiles["Stossy11"] ?? "")
CollaboratorRow(name: "Neo", url: "https://neoarz.xyz/", imageUrl: developerProfiles["Neo"] ?? "")
CollaboratorRow(name: "Se2crid", url: "https://github.com/Se2crid", imageUrl: developerProfiles["Se2crid"] ?? "")
CollaboratorRow(name: "Huge_Black", url: "https://github.com/HugeBlack", imageUrl: developerProfiles["Huge_Black"] ?? "")

13
TunnelProv/Info.plist Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,162 @@
//
// PacketTunnelProvider.swift
// TunnelProv
//
// Created by Stephen on 4/16/25.
//
//
// PacketTunnelProvider.swift
// TunnelProv
//
// Created by Stossy11 on 28/03/2025.
//
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
var tunnelDeviceIp: String = "10.7.0.0"
var tunnelFakeIp: String = "10.7.0.1"
var tunnelSubnetMask: String = "255.255.255.0"
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
if let deviceIp = options?["TunnelDeviceIP"] as? String {
tunnelDeviceIp = deviceIp
}
if let fakeIp = options?["TunnelFakeIP"] as? String {
tunnelFakeIp = fakeIp
}
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIp)
let ipv4 = NEIPv4Settings(addresses: [tunnelDeviceIp], subnetMasks: [tunnelSubnetMask])
ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)]
ipv4.excludedRoutes = [.default()]
settings.ipv4Settings = ipv4
setTunnelNetworkSettings(settings) { error in
if error == nil {
self.readPackets()
}
completionHandler(error)
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
// Add code here to start the process of stopping the tunnel.
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
// Add code here to handle the message.
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
completionHandler()
}
override func wake() {
// Add code here to wake up.
}
private func readPackets() {
packetFlow.readPackets { packets, protocols in
var output: [Data] = []
for (i, packet) in packets.enumerated() {
var modifiedPacket = packet
if protocols[i].int32Value == AF_INET {
modifiedPacket = self.packetReplaceIp(packet, self.tunnelDeviceIp, self.tunnelFakeIp, self.tunnelFakeIp, self.tunnelDeviceIp)
}
NSLog("wow \(modifiedPacket)")
// Ensure packet has at least 20 bytes before swapping
if modifiedPacket.count >= 20 {
var mutableBytes = [UInt8](modifiedPacket)
// Swap bytes
(mutableBytes[12], mutableBytes[16]) = (mutableBytes[16], mutableBytes[12])
(mutableBytes[13], mutableBytes[17]) = (mutableBytes[17], mutableBytes[13])
(mutableBytes[14], mutableBytes[18]) = (mutableBytes[18], mutableBytes[14])
(mutableBytes[15], mutableBytes[19]) = (mutableBytes[19], mutableBytes[15])
modifiedPacket = Data(mutableBytes)
}
output.append(modifiedPacket)
}
self.packetFlow.writePackets(output, withProtocols: protocols)
self.readPackets()
}
}
private func packetReplaceIp(_ data: Data, _ sourceSearch: String, _ sourceReplace: String, _ destSearch: String, _ destReplace: String) -> Data {
// Check if packet is too small for IPv4 header
if data.count < 20 {
return data
}
// Convert IP strings to Data with network byte order (big-endian)
func ipToUInt32(_ ipString: String) -> UInt32 {
let components = ipString.split(separator: ".")
var result: UInt32 = 0
if components.count == 4,
let byte1 = UInt32(components[0]),
let byte2 = UInt32(components[1]),
let byte3 = UInt32(components[2]),
let byte4 = UInt32(components[3]) {
result = (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4
}
return result
}
// Convert IP strings to UInt32
let sourceSearchIP = ipToUInt32(sourceSearch)
let sourceReplaceIP = ipToUInt32(sourceReplace)
let destSearchIP = ipToUInt32(destSearch)
let destReplaceIP = ipToUInt32(destReplace)
// Extract source and destination IPs from packet
var sourcePacketIP: UInt32 = 0
var destPacketIP: UInt32 = 0
(data as NSData).getBytes(&sourcePacketIP, range: NSRange(location: 12, length: 4))
(data as NSData).getBytes(&destPacketIP, range: NSRange(location: 16, length: 4))
if sourceSearchIP != sourcePacketIP && destSearchIP != destPacketIP {
return data
}
let mutableData = NSMutableData(data: data)
if sourceSearchIP == sourcePacketIP {
var sourceIP = sourceReplaceIP
mutableData.replaceBytes(in: NSRange(location: 12, length: 4), withBytes: &sourceIP)
}
if destSearchIP == destPacketIP {
var destIP = destReplaceIP
mutableData.replaceBytes(in: NSRange(location: 16, length: 4), withBytes: &destIP)
}
return mutableData as Data
}
// Helper function to convert IP string to Data
private func ipToData(_ ip: String) -> Data {
let components = ip.split(separator: ".")
var data = Data(capacity: 4)
for component in components {
if let byte = UInt8(component) {
data.append(byte)
}
}
return data
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!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.application-groups</key>
<array>
<string>group.com.stik.sj</string>
</array>
</dict>
</plist>