From 8bcd12f4bcafc1ef4fb630c15908fb88c5eea36e Mon Sep 17 00:00:00 2001 From: Stephen <158498287+0-Blu@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:27:13 -0400 Subject: [PATCH] Debug pt 2 --- StikDebug.xcodeproj/project.pbxproj | 167 +++++++++ StikJIT/StikJIT.entitlements | 14 +- StikJIT/StikJITApp.swift | 499 ++++++++++++++++++++------ StikJIT/Views/HomeView.swift | 2 +- StikJIT/Views/SettingsView.swift | 6 +- TunnelProv/Info.plist | 13 + TunnelProv/PacketTunnelProvider.swift | 162 +++++++++ TunnelProv/TunnelProv.entitlements | 14 + 8 files changed, 763 insertions(+), 114 deletions(-) create mode 100644 TunnelProv/Info.plist create mode 100644 TunnelProv/PacketTunnelProvider.swift create mode 100644 TunnelProv/TunnelProv.entitlements diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj index c5089c3..c4c207a 100644 --- a/StikDebug.xcodeproj/project.pbxproj +++ b/StikDebug.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + DCA690132DAF660E007C91A8 /* TunnelProv */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + DCA6901E2DAF660E007C91A8 /* Exceptions for "TunnelProv" folder in "TunnelProv" target */, + ); + path = TunnelProv; + sourceTree = ""; + }; /* 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 = ""; @@ -111,6 +164,7 @@ isa = PBXGroup; children = ( DC5716242D9564760099302E /* em_proxy.xcframework */, + DCA690112DAF660E007C91A8 /* NetworkExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -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 */; diff --git a/StikJIT/StikJIT.entitlements b/StikJIT/StikJIT.entitlements index f2ef3ae..a5dceae 100644 --- a/StikJIT/StikJIT.entitlements +++ b/StikJIT/StikJIT.entitlements @@ -2,9 +2,15 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.developer.networking.vpn.api + allow-vpn + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index ea5bff9..148d885 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -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 { private var runningTask: Task? @@ -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)) diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 515c552..067b1cc 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -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)) diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 8e5beae..753533d 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -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"] ?? "") diff --git a/TunnelProv/Info.plist b/TunnelProv/Info.plist new file mode 100644 index 0000000..3059459 --- /dev/null +++ b/TunnelProv/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/TunnelProv/PacketTunnelProvider.swift b/TunnelProv/PacketTunnelProvider.swift new file mode 100644 index 0000000..c57de2b --- /dev/null +++ b/TunnelProv/PacketTunnelProvider.swift @@ -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 + } +} diff --git a/TunnelProv/TunnelProv.entitlements b/TunnelProv/TunnelProv.entitlements new file mode 100644 index 0000000..bbcb3da --- /dev/null +++ b/TunnelProv/TunnelProv.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.application-groups + + group.com.stik.sj + + +