This commit is contained in:
Stephen B 2025-12-17 21:21:47 -05:00
parent d0c86c40d1
commit 6a6411a1cf
20 changed files with 253 additions and 1103 deletions

View file

@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.stik.sj</string>
<string>group.com.stik.stikdebug</string>
</array>
</dict>
</plist>

View file

@ -49,10 +49,9 @@
## Features
- On-device debugging/Just-In-Time (JIT) compilation for supported apps via [`idevice`](https://github.com/jkcoxson/idevice).
- Seamless integration with our custom-built loopback vpn.
- No special VPN/Network Extension entitlements required.
- Native UI for managing debugging/JIT-enabling.
- No data collection—ensuring full privacy.
## License
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.

View file

@ -8,15 +8,12 @@
/* Begin PBXBuildFile section */
17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; };
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */; };
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; };
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; };
68E714E62E6AA2B00025610F /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 68E714E52E6AA2B00025610F /* ZIPFoundation */; };
DC139F6E2DE97EA400F63846 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC139F6D2DE97EA400F63846 /* WidgetKit.framework */; };
DC139F702DE97EA400F63846 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC139F6F2DE97EA400F63846 /* SwiftUI.framework */; };
DC139F812DE97EA600F63846 /* DebugWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
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, ); }; };
DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */ = {isa = PBXBuildFile; productRef = DCBA85852E3897BD00E88C06 /* StikImporter */; };
/* End PBXBuildFile section */
@ -42,13 +39,6 @@
remoteGlobalIDString = DC6F1D362D94EADD0071B2B6;
remoteInfo = StikJIT;
};
DCA690182DAF660E007C91A8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = DCA6900F2DAF660E007C91A8;
remoteInfo = TunnelProv;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -59,7 +49,6 @@
dstSubfolderSpec = 13;
files = (
DC139F812DE97EA600F63846 /* DebugWidgetExtension.appex in Embed Foundation Extensions */,
DCA6901A2DAF660E007C91A8 /* TunnelProv.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@ -67,7 +56,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DebugWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
DC139F6D2DE97EA400F63846 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
DC139F6F2DE97EA400F63846 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
@ -75,8 +63,6 @@
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 */
@ -94,13 +80,6 @@
);
target = DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */;
};
DCA6901E2DAF660E007C91A8 /* Exceptions for "TunnelProv" folder in "TunnelProv" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = DCA6900F2DAF660E007C91A8 /* TunnelProv */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@ -130,14 +109,6 @@
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 */
@ -157,7 +128,6 @@
17C744F02E20BED000834F17 /* Pipify in Frameworks */,
DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */,
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */,
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */,
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */,
68E714E62E6AA2B00025610F /* ZIPFoundation in Frameworks */,
);
@ -177,14 +147,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900D2DAF660E007C91A8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DCA690122DAF660E007C91A8 /* NetworkExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -195,7 +157,6 @@
DC6F1D392D94EADD0071B2B6 /* StikJIT */,
DC6F1D4B2D94EADF0071B2B6 /* StikJITTests */,
DC6F1D552D94EADF0071B2B6 /* StikJITUITests */,
DCA690132DAF660E007C91A8 /* TunnelProv */,
DC139F712DE97EA400F63846 /* DebugWidget */,
DC6F1D752D94EB620071B2B6 /* Frameworks */,
DC6F1D382D94EADD0071B2B6 /* Products */,
@ -208,7 +169,6 @@
DC6F1D372D94EADD0071B2B6 /* StikDebug.app */,
DC6F1D482D94EADF0071B2B6 /* StikDebugTests.xctest */,
DC6F1D522D94EADF0071B2B6 /* StikDebugUITests.xctest */,
DCA690102DAF660E007C91A8 /* TunnelProv.appex */,
DC139F6C2DE97EA400F63846 /* DebugWidgetExtension.appex */,
);
name = Products;
@ -217,8 +177,6 @@
DC6F1D752D94EB620071B2B6 /* Frameworks */ = {
isa = PBXGroup;
children = (
68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */,
DCA690112DAF660E007C91A8 /* NetworkExtension.framework */,
DC139F6D2DE97EA400F63846 /* WidgetKit.framework */,
DC139F6F2DE97EA400F63846 /* SwiftUI.framework */,
);
@ -262,7 +220,6 @@
buildRules = (
);
dependencies = (
DCA690192DAF660E007C91A8 /* PBXTargetDependency */,
DC139F802DE97EA600F63846 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
@ -326,28 +283,6 @@
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 */
@ -373,9 +308,6 @@
CreatedOnToolsVersion = 16.2;
TestTargetID = DC6F1D362D94EADD0071B2B6;
};
DCA6900F2DAF660E007C91A8 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = DC6F1D322D94EADD0071B2B6 /* Build configuration list for PBXProject "StikDebug" */;
@ -403,7 +335,6 @@
DC6F1D362D94EADD0071B2B6 /* StikDebug */,
DC6F1D472D94EADF0071B2B6 /* StikDebugTests */,
DC6F1D512D94EADF0071B2B6 /* StikDebugUITests */,
DCA6900F2DAF660E007C91A8 /* TunnelProv */,
DC139F6B2DE97EA400F63846 /* DebugWidgetExtension */,
);
};
@ -438,13 +369,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900E2DAF660E007C91A8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -476,13 +400,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DCA6900C2DAF660E007C91A8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -501,11 +418,6 @@
target = DC6F1D362D94EADD0071B2B6 /* StikDebug */;
targetProxy = DC6F1D532D94EADF0071B2B6 /* PBXContainerItemProxy */;
};
DCA690192DAF660E007C91A8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DCA6900F2DAF660E007C91A8 /* TunnelProv */;
targetProxy = DCA690182DAF660E007C91A8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@ -517,7 +429,7 @@
CODE_SIGN_ENTITLEMENTS = DebugWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
DEVELOPMENT_TEAM = 2743MYJ5UC;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = DebugWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DebugWidget;
@ -529,7 +441,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.DebugWidget;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.DebugWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@ -547,7 +459,7 @@
CODE_SIGN_ENTITLEMENTS = DebugWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SZ977XLF24;
DEVELOPMENT_TEAM = 2743MYJ5UC;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = DebugWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = DebugWidget;
@ -559,7 +471,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj.DebugWidget;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.DebugWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@ -695,7 +607,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"StikJIT/Preview Content\"";
DEVELOPMENT_TEAM = SZ977XLF24;
DEVELOPMENT_TEAM = 2743MYJ5UC;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -726,8 +638,8 @@
"$(PROJECT_DIR)/StikJIT/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 2.3.5;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
MARKETING_VERSION = 2.3.6;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -753,7 +665,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"StikJIT/Preview Content\"";
DEVELOPMENT_TEAM = SZ977XLF24;
DEVELOPMENT_TEAM = 2743MYJ5UC;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -784,8 +696,8 @@
"$(PROJECT_DIR)/StikJIT/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 2.3.5;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
MARKETING_VERSION = 2.3.6;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -889,63 +801,6 @@
};
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 = 17.4;
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 = 17.4;
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 */
@ -994,15 +849,6 @@
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 */
/* Begin XCRemoteSwiftPackageReference section */

View file

@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DCA6900F2DAF660E007C91A8"
BuildableName = "TunnelProv.appex"
BlueprintName = "TunnelProv"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC6F1D362D94EADD0071B2B6"
BuildableName = "StikDebug.app"
BlueprintName = "StikDebug"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC6F1D472D94EADF0071B2B6"
BuildableName = "StikDebugTests.xctest"
BlueprintName = "StikDebugTests"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC6F1D512D94EADF0071B2B6"
BuildableName = "StikDebugUITests.xctest"
BlueprintName = "StikDebugUITests"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC6F1D362D94EADD0071B2B6"
BuildableName = "StikDebug.app"
BlueprintName = "StikDebug"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC6F1D362D94EADD0071B2B6"
BuildableName = "StikDebug.app"
BlueprintName = "StikDebug"
ReferencedContainer = "container:StikDebug.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -2,17 +2,11 @@
<!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.developer.networking.vpn.api</key>
<string>allow-vpn</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.stik.sj</string>
<string>group.com.stik.stikdebug</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>

View file

@ -8,7 +8,6 @@
import SwiftUI
import Network
import UniformTypeIdentifiers
import NetworkExtension
// Register default settings before the app starts
private func registerAdvancedOptionsDefault() {
@ -64,17 +63,6 @@ struct WelcomeSheetView: View {
.fixedSize(horizontal: false, vertical: true)
}
// VPN explanation
VStack(alignment: .leading, spacing: 6) {
Label("Why VPN permission?", systemImage: "lock.shield.fill")
.foregroundColor(accent)
.font(.headline)
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. No data is collected or sent externally; everything stays on your device.")
.font(.callout)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
// Continue button
Button(action: { onDismiss?() }) {
Text("Continue")
@ -124,221 +112,6 @@ struct WelcomeSheetView: View {
}
}
// 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 pendingStopRequest = false
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")
}
if self.pendingStopRequest, let manager = self.vpnManager {
VPNLogger.shared.log("Pending stop request detected during preference load")
self.stopVPN(with: manager)
}
} 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)")
if connectionStatus == .connected && heartbeatStartPending {
startHeartbeatInBackground(showErrorUI: heartbeatPendingShowUI)
}
}
}
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 == .disconnected || tunnelStatus == .error else {
VPNLogger.shared.log("Ignoring VPN start; current status: \(tunnelStatus.rawValue)")
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 {
pendingStopRequest = true
loadTunnelPreferences()
return
}
pendingStopRequest = false
stopVPN(with: manager)
}
private func stopVPN(with manager: NETunnelProviderManager) {
tunnelStatus = .disconnecting
manager.connection.stopVPNTunnel()
VPNLogger.shared.log("Network tunnel stop initiated")
}
}
// MARK: - AccentColor Environment Key (leave available but unused)
struct AccentColorKey: EnvironmentKey {
@ -509,7 +282,6 @@ struct HeartbeatApp: App {
@AppStorage("hasLaunchedBefore") var hasLaunchedBefore: Bool = false
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@AppStorage("autoStartVPN") private var autoStartVPN = true
@State private var showWelcomeSheet: Bool = false
@State private var show_alert = false
@State private var alert_string = ""
@ -559,14 +331,6 @@ struct HeartbeatApp: App {
}
}
private func triggerAutoVPNStartIfNeeded() {
guard autoStartVPN, DeviceConnectionContext.requiresLoopbackVPN else { return }
let manager = TunnelManager.shared
if manager.tunnelStatus == .disconnected || manager.tunnelStatus == .error {
manager.startVPN()
}
}
private func handleScenePhaseChange(_ newPhase: ScenePhase) {
switch newPhase {
case .background:
@ -629,11 +393,8 @@ struct HeartbeatApp: App {
.tint(globalAccent)
.onAppear {
// On first launch, present the welcome sheet.
// Otherwise, start the VPN automatically.
if !hasLaunchedBefore {
showWelcomeSheet = true
} else {
triggerAutoVPNStartIfNeeded()
}
HeartbeatApp.updateUIKitTint(customHex: customAccentColorHex,
hasAccess: themeExpansionManager.hasThemeExpansion)
@ -650,10 +411,9 @@ struct HeartbeatApp: App {
}
.sheet(isPresented: $showWelcomeSheet) {
WelcomeSheetView {
// When the user taps "Continue", mark the app as launched and start the VPN if allowed.
// When the user taps "Continue", mark the app as launched.
hasLaunchedBefore = true
showWelcomeSheet = false
triggerAutoVPNStartIfNeeded()
}
}
}
@ -706,16 +466,6 @@ class MountingProgress: ObservableObject {
}
private func mount() {
if DeviceConnectionContext.requiresLoopbackVPN {
guard TunnelManager.shared.tunnelStatus == .connected else {
DispatchQueue.main.async {
self.coolisMounted = false
self.mountingThread = nil
}
return
}
}
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = currentlyMounted
@ -772,7 +522,7 @@ func isPairing() -> Bool {
return true
}
func startHeartbeatInBackground(requireVPNConnection: Bool? = nil, showErrorUI: Bool = true) {
func startHeartbeatInBackground(showErrorUI: Bool = true) {
assert(Thread.isMainThread, "startHeartbeatInBackground must be called on the main thread")
let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
@ -782,17 +532,6 @@ func startHeartbeatInBackground(requireVPNConnection: Bool? = nil, showErrorUI:
return
}
let shouldRequireVPN = requireVPNConnection ?? DeviceConnectionContext.requiresLoopbackVPN
let vpnConnected = TunnelManager.shared.tunnelStatus == .connected
if shouldRequireVPN && !vpnConnected {
if !heartbeatStartPending {
print("Heartbeat start deferred until VPN connects")
}
heartbeatPendingShowUI = showErrorUI
heartbeatStartPending = true
return
}
guard !heartbeatStartInProgress else {
return
}
@ -845,7 +584,7 @@ func startHeartbeatInBackground(requireVPNConnection: Bool? = nil, showErrorUI:
} else {
showAlert(
title: "Heartbeat Error",
message: "Failed to connect to Heartbeat (\(code)). Are you connected to WiFi or is Airplane Mode enabled? Cellular data isn’t supported. Please launch the app at least once with WiFi enabled. After that, you can switch to cellular data to turn on the VPN, and once the VPN is active you can use Airplane Mode.",
message: "Failed to connect to Heartbeat (\(code)). Make sure Wi‑Fi is enabled and the device is reachable. Launch the app at least once while online before trying again.",
showOk: false,
showTryAgain: true
) { shouldTryAgain in
@ -863,7 +602,7 @@ func startHeartbeatInBackground(requireVPNConnection: Bool? = nil, showErrorUI:
}
func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) {
func checkDeviceConnection(callback: @escaping (Bool, String?) -> Void) {
let targetIP = DeviceConnectionContext.targetIPAddress
let host = NWEndpoint.Host(targetIP)
let port = NWEndpoint.Port(rawValue: 62078)!
@ -875,12 +614,7 @@ func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) {
connection?.cancel()
DispatchQueue.main.async {
if timeoutWorkItem?.isCancelled == false {
let message: String
if DeviceConnectionContext.requiresLoopbackVPN {
message = "[TIMEOUT] The loopback VPN is not connected. Try closing this app, turn it off and back on."
} else {
message = "[TIMEOUT] Could not reach the device at \(targetIP). Make sure it’s online and on the same network."
}
let message = "[TIMEOUT] Could not reach the device at \(targetIP). Make sure it’s online and on the same network."
callback(false, message)
}
}
@ -899,18 +633,7 @@ func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) {
timeoutWorkItem?.cancel()
connection?.cancel()
DispatchQueue.main.async {
let message: String
if DeviceConnectionContext.requiresLoopbackVPN {
if error == NWError.posix(.ETIMEDOUT) {
message = "The loopback VPN is not connected. Try closing the app, turn it off and back on."
} else if error == NWError.posix(.ECONNREFUSED) {
message = "Wi-Fi is not connected. StikDebug can't connect over cellular data while in loopback mode."
} else {
message = "VPN check error: \(error.localizedDescription)"
}
} else {
message = "Could not reach the device at \(targetIP): \(error.localizedDescription)"
}
let message = "Could not reach the device at \(targetIP): \(error.localizedDescription)"
callback(false, message)
}
default:

View file

@ -13,13 +13,13 @@ enum DeviceConnectionContext {
}
static var requiresLoopbackVPN: Bool {
!isUsingExternalDevice
false
}
static var targetIPAddress: String {
if let device = DeviceLibraryStore.shared.activeDevice {
return device.ipAddress
}
return UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.2"
return "127.0.0.1"
}
}

View file

@ -103,7 +103,7 @@ final class DeviceLibraryStore: ObservableObject {
DeviceProfileEntry(
id: localLoopbackID,
name: "This Device",
ipAddress: "10.7.0.2",
ipAddress: "10.7.0.1",
pairingRelativePath: "",
pairingFilename: "pairingFile.plist",
dateAdded: Date.distantPast,

View file

@ -2,11 +2,11 @@ import Foundation
enum FeatureFlags {
/// Global toggle for exposing location spoofing UI/logic.
static let isLocationSpoofingEnabled = false
static let isLocationSpoofingEnabled = true
/// Controls visibility of beta-quality tabs and UI.
static let showBetaTabs = false
static let showBetaTabs = true
/// Forces the Theme Expansion to be unlocked and visible everywhere.
static let alwaysUnlockThemeExpansion = false
static let alwaysUnlockThemeExpansion = true
/// Controls visibility of the Mini Tools tab and related UI.
static let isMiniToolsEnabled = false
}

View file

@ -19,12 +19,6 @@ func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRaw
}
func isMounted() -> Bool {
if DeviceConnectionContext.requiresLoopbackVPN {
guard TunnelManager.shared.tunnelStatus == .connected else {
return false
}
}
do {
let result = try JITEnableContext.shared.getMountedDeviceCount()
return result > 0

View file

@ -115,7 +115,7 @@ struct DeviceLibraryView: View {
if let refreshedDevice = store.devices.first(where: { $0.id == originalDevice.id }) {
do {
try store.activate(device: refreshedDevice)
startHeartbeatInBackground(requireVPNConnection: false)
startHeartbeatInBackground()
alert = DeviceAlert(title: "Pairing Updated",
message: "\(refreshedDevice.name)'s pairing file was refreshed.",
isError: false)
@ -164,10 +164,10 @@ struct DeviceLibraryView: View {
let activatingDefault = store.isDefaultDevice(device)
do {
try store.activate(device: device)
startHeartbeatInBackground(requireVPNConnection: false)
let title = activatingDefault ? "Loopback Active" : "Device Activated"
startHeartbeatInBackground()
let title = activatingDefault ? "Local Device Active" : "Device Activated"
let message = activatingDefault
? "Switched back to debugging this device over the loopback VPN."
? "Switched back to debugging on this device."
: "\(device.name) is now active. The heartbeat will refresh automatically."
alert = DeviceAlert(title: title, message: message, isError: false)
} catch {

View file

@ -33,8 +33,8 @@ struct HomeView: View {
@State private var isProcessing = false
@State private var isShowingInstalledApps = false
@State private var isShowingPairingFilePicker = false
@State private var pairingFileExists: Bool = false
@State private var pairingFilePresentOnDisk: Bool = false
@State private var pairingFileExists: Bool = true
@State private var pairingFilePresentOnDisk: Bool = true
@State private var isValidatingPairingFile = false
@State private var lastValidatedPairingSignature: PairingFileSignature? = nil
@State private var showPairingFileMessage = false
@ -56,7 +56,6 @@ struct HomeView: View {
@AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js"
@State var jsModel: RunJSViewModel?
@StateObject private var tunnel = TunnelManager.shared
@ObservedObject private var mounting = MountingProgress.shared
@ObservedObject private var deviceStore = DeviceLibraryStore.shared
@State private var heartbeatOK = false
@ -75,8 +74,6 @@ struct HomeView: View {
@State private var cellularMonitor: NWPathMonitor? = nil
@State private var isSchedulingInitialSetup = false
@AppStorage("cachedAppNamesData") private var cachedAppNamesData: Data?
@AppStorage("autoStartVPN") private var autoStartVPN = true
@State private var lastDDIIndicatorStatus: StartupIndicatorStatus = .idle
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@Environment(\.themeExpansionManager) private var themeExpansion
@ -87,15 +84,10 @@ struct HomeView: View {
themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue
}
private var ddiMounted: Bool { mounting.coolisMounted }
private var ddiMounted: Bool { true }
private var canConnectByApp: Bool { pairingFileExists && ddiMounted }
private var requiresLoopbackVPN: Bool { !deviceStore.isUsingExternalDevice }
private var pairingFileLikelyInvalid: Bool {
(pairingFileExists || pairingFilePresentOnDisk) &&
!isValidatingPairingFile &&
!ddiMounted &&
!heartbeatOK
}
private var requiresLoopbackVPN: Bool { DeviceConnectionContext.requiresLoopbackVPN }
private var pairingFileLikelyInvalid: Bool { false }
private var sanitizedUsername: String {
let trimmed = username.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "there" : trimmed
@ -122,9 +114,7 @@ struct HomeView: View {
default: return "Hello"
}
}
private var shouldPromptForWiFi: Bool {
pairingFileLikelyInvalid && !wifiConnected && isCellularActive
}
private var shouldPromptForWiFi: Bool { false }
private let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
@ -132,7 +122,7 @@ struct HomeView: View {
private var homeContent: some View {
VStack(spacing: 20) {
welcomeCard
setupCard
heartbeatCard
connectCard
// if pairingFileExists {
// quickConnectCard
@ -189,14 +179,11 @@ struct HomeView: View {
scheduleInitialSetupWork()
startWiFiMonitoring()
startCellularMonitoring()
if requiresLoopbackVPN && autoStartVPN && tunnel.tunnelStatus == .disconnected {
TunnelManager.shared.startVPN()
}
if !hasAutoStartedConnectionCheck {
hasAutoStartedConnectionCheck = true
runConnectionDiagnostics(autoStart: true)
}
lastDDIIndicatorStatus = ddiIndicatorStatus
startHeartbeatInBackground()
NotificationCenter.default.addObserver(
forName: NSNotification.Name("ShowPairingFilePicker"),
object: nil,
@ -226,21 +213,10 @@ struct HomeView: View {
cachedAppNames = [:]
}
}
.onChange(of: tunnel.tunnelStatus) { _, newStatus in
guard requiresLoopbackVPN else { return }
if newStatus == .connected {
loadAppListIfNeeded(force: cachedAppNames.isEmpty)
runConnectionDiagnostics()
MountingProgress.shared.checkforMounted()
}
}
.onChange(of: favoriteApps) { _, _ in
loadAppListIfNeeded()
syncFavoriteAppNamesWithCache()
}
.onChange(of: ddiIndicatorStatus) { _, newStatus in
handleDDIIndicatorChange(newStatus)
}
.onChange(of: recentApps) { _, _ in
loadAppListIfNeeded()
}
@ -265,7 +241,7 @@ struct HomeView: View {
}
DispatchQueue.main.async {
startHeartbeatInBackground(requireVPNConnection: false)
startHeartbeatInBackground()
}
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { t in
@ -420,20 +396,31 @@ struct HomeView: View {
}
}
private var setupCard: some View {
private var heartbeatCard: some View {
homeCard {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Setup")
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Heartbeat")
.font(.headline.weight(.semibold))
Spacer()
connectionStatusBadge
Text(heartbeatSubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
HStack(spacing: 6) {
pillIconButton(icon: "play.fill", title: "Start") {
startHeartbeatInBackground()
}
pillIconButton(icon: "arrow.clockwise", title: "Restart") {
pubHeartBeat = false
heartbeatOK = false
startHeartbeatInBackground()
}
}
}
Spacer(minLength: 0)
VStack(alignment: .trailing, spacing: 6) {
heartbeatStatusBadge
statusLightsRow
}
statusLightsRow
vpnControls
}
}
}
@ -451,12 +438,6 @@ struct HomeView: View {
text: "Wi-Fi required",
color: .orange
)
} else if pairingFileLikelyInvalid {
statusBadge(
icon: "xmark.octagon.fill",
text: "Pairing file expired",
color: .red
)
}
primaryActionControls
@ -499,6 +480,22 @@ struct HomeView: View {
}
}
@ViewBuilder
private var heartbeatStatusBadge: some View {
switch heartbeatIndicatorStatus {
case .success:
statusBadge(icon: "checkmark.circle.fill", text: "Connected", color: .green)
case .running:
statusBadge(icon: "clock.arrow.circlepath", text: "Starting…", color: .orange)
case .warning:
statusBadge(icon: "exclamationmark.triangle.fill", text: "Waiting", color: .yellow)
case .error:
statusBadge(icon: "xmark.circle.fill", text: "Error", color: .red)
case .idle:
statusBadge(icon: "pause.circle", text: "Idle", color: .secondary)
}
}
private func statusBadge(icon: String, text: String, color: Color) -> some View {
Label(text, systemImage: icon)
.font(.footnote.weight(.semibold))
@ -524,15 +521,9 @@ struct HomeView: View {
.labelStyle(.iconOnly)
}
private var connectionHasError: Bool {
if case .failure = connectionCheckState { return true }
if case .timeout = connectionCheckState { return true }
return false
}
private var connectionHasError: Bool { false }
private var allStatusIndicatorsGreen: Bool {
ddiIndicatorStatus == .success &&
wifiIndicatorStatus == .success &&
heartbeatIndicatorStatus == .success
}
@ -554,20 +545,6 @@ struct HomeView: View {
private var statusLights: [StatusLightData] {
[
StatusLightData(
type: .ddi,
title: "DDI",
icon: "externaldrive",
status: ddiIndicatorStatus,
detail: ddiDetailText
),
StatusLightData(
type: .wifi,
title: "Wi-Fi",
icon: "wifi",
status: wifiIndicatorStatus,
detail: wifiDetailText
),
StatusLightData(
type: .heartbeat,
title: "Heartbeat",
@ -578,47 +555,16 @@ struct HomeView: View {
]
}
private var ddiDetailText: String {
if ddiMounted { return "Mounted" }
if pairingFileLikelyInvalid { return "Error" }
return pairingFileExists ? "Mount required" : "Not ready"
}
private var wifiDetailText: String {
if isConnectionCheckRunning { return "Checking…" }
return wifiConnected ? "Connected" : "Offline"
}
private var wifiDetailText: String { "Connected" }
private var heartbeatDetailText: String {
if heartbeatOK { return "Active" }
if pairingFileExists {
if requiresLoopbackVPN {
return tunnel.tunnelStatus == .connected ? "Waiting" : "VPN required"
} else {
return "Waiting"
}
}
if pairingFileExists { return "Waiting" }
return "Pair first"
}
private var refreshIndicatorStatus: StartupIndicatorStatus {
switch connectionCheckState {
case .running:
return .running
case .success:
return .success
case .failure, .timeout:
return .warning
case .idle:
return .idle
}
}
private var ddiIndicatorStatus: StartupIndicatorStatus {
if ddiMounted { return .success }
if pairingFileLikelyInvalid { return .warning }
if pairingFileExists { return .warning }
return .idle
.success
}
private func color(for indicator: StartupIndicatorStatus) -> Color {
@ -660,12 +606,6 @@ struct HomeView: View {
isCellularActive = false
}
private func handleDDIIndicatorChange(_ newStatus: StartupIndicatorStatus) {
defer { lastDDIIndicatorStatus = newStatus }
guard lastDDIIndicatorStatus == .success, newStatus == .warning else { return }
startHeartbeatInBackground(showErrorUI: false)
}
private var pairingStatusDescription: String {
if isValidatingPairingFile { return "Validating pairing file…" }
if pairingFileExists {
@ -678,62 +618,19 @@ struct HomeView: View {
}
private var wifiStatusDescription: String {
if isConnectionCheckRunning { return "Checking Wi-Fi status…" }
return wifiConnected ? "Wi-Fi connected and ready." : "Connect to Wi-Fi."
"Wi-Fi connected and ready."
}
private var isConnectionCheckRunning: Bool {
if case .running = connectionCheckState { return true }
return false
}
private var isConnectionCheckRunning: Bool { false }
private var vpnStatusSubtitle: String {
if !requiresLoopbackVPN {
return "External device selected. VPN tunnel not required."
}
if isConnectionCheckRunning {
return "Checking the VPN/loopback tunnel…"
}
switch tunnel.tunnelStatus {
case .connected:
return "VPN is connected; loopback traffic is ready."
case .connecting:
return "Connecting… allow the VPN prompt if it appears."
case .disconnecting:
return "Disconnecting from the VPN."
case .disconnected:
return "VPN is off. Connect before running diagnostics."
case .error:
return "VPN configuration error. Try reconnecting."
}
}
private var vpnIndicatorStatus: StartupIndicatorStatus {
if !requiresLoopbackVPN { return .success }
if isConnectionCheckRunning { return .running }
switch tunnel.tunnelStatus {
case .connected:
return .success
case .connecting, .disconnecting:
return .running
case .disconnected:
return pairingFileExists ? .warning : .idle
case .error:
return .error
}
}
private var wifiIndicatorStatus: StartupIndicatorStatus {
if isConnectionCheckRunning { return .running }
return wifiConnected ? .success : .warning
}
private var wifiIndicatorStatus: StartupIndicatorStatus { .success }
private var heartbeatSubtitle: String {
if heartbeatOK {
return "Heartbeat is responding."
}
if !requiresLoopbackVPN && pairingFileExists {
return "Waiting for the remote device to respond."
return "Waiting for a response."
}
if pairingFileLikelyInvalid {
return "Heartbeat is blocked because the pairing file looks invalid."
@ -752,11 +649,7 @@ struct HomeView: View {
private var heartbeatIndicatorStatus: StartupIndicatorStatus {
if heartbeatOK { return .success }
if pairingFileLikelyInvalid { return .warning }
if !pairingFileExists { return .idle }
if case .running = connectionCheckState { return .running }
if case .success = connectionCheckState { return .warning }
return .idle
return .warning
}
private var connectionCheckButtonLabel: some View {
@ -787,144 +680,27 @@ struct HomeView: View {
}
}
@ViewBuilder
private var vpnControls: some View {
if !requiresLoopbackVPN {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("VPN Tunnel")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Spacer()
statusBadge(icon: "checkmark.circle.fill", text: "Not needed", color: .green)
}
Text("You’re targeting an external device. StikDebug connects directly to \(DeviceConnectionContext.targetIPAddress).")
.font(.footnote)
.foregroundStyle(.secondary)
}
} else {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("VPN Tunnel")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Spacer()
statusBadge(icon: "shield.lefthalf.filled", text: tunnel.tunnelStatus.rawValue, color: vpnStatusColor)
}
Text(vpnStatusSubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
Button(action: { TunnelManager.shared.startVPN() }) {
compactControlButton(icon: "lock.open", title: "Connect")
}
.buttonStyle(.plain)
.disabled(!canStartVPN)
Button(action: { TunnelManager.shared.stopVPN() }) {
compactControlButton(icon: "lock.fill", title: "Disconnect")
}
.buttonStyle(.plain)
.disabled(!canStopVPN)
}
Button {
autoStartVPN.toggle()
if requiresLoopbackVPN && autoStartVPN && tunnel.tunnelStatus == .disconnected {
TunnelManager.shared.startVPN()
}
} label: {
compactControlButton(
icon: autoStartVPN ? "lock.circle" : "lock.slash",
title: autoStartVPN ? "Disable Auto VPN" : "Enable Auto VPN"
)
}
.buttonStyle(.plain)
}
}
}
private var vpnStatusColor: Color {
if !requiresLoopbackVPN { return .green }
switch tunnel.tunnelStatus {
case .connected: return .green
case .connecting, .disconnecting: return .orange
case .error: return .red
case .disconnected: return .yellow
}
}
private var canStartVPN: Bool {
guard requiresLoopbackVPN else { return false }
switch tunnel.tunnelStatus {
case .disconnected, .error:
return true
default:
return false
}
}
private var canStopVPN: Bool {
guard requiresLoopbackVPN else { return false }
switch tunnel.tunnelStatus {
case .connected, .connecting, .disconnecting:
return true
default:
return false
}
}
private func refreshStatusTapped() {
runConnectionDiagnostics()
if pairingFileExists {
startHeartbeatInBackground(requireVPNConnection: requiresLoopbackVPN)
startHeartbeatInBackground()
}
}
private func runConnectionDiagnostics(autoStart: Bool = false) {
guard !isConnectionCheckRunning else { return }
connectionTimeoutTask?.cancel()
connectionTimeoutTask = nil
connectionInfoMessage = autoStart ? "Checking connection…" : nil
connectionCheckState = .running
let timeout = DispatchWorkItem {
DispatchQueue.main.async {
if case .running = connectionCheckState {
connectionCheckState = .timeout
connectionInfoMessage = requiresLoopbackVPN
? "Connection timed out. Check the VPN and pairing file, then try again."
: "Connection timed out. Make sure the device at \(DeviceConnectionContext.targetIPAddress) is reachable."
connectionTimeoutTask = nil
}
}
}
connectionTimeoutTask = timeout
DispatchQueue.main.asyncAfter(deadline: .now() + 7, execute: timeout)
checkVPNConnection { success, error in
connectionTimeoutTask?.cancel()
connectionTimeoutTask = nil
if success {
connectionCheckState = .success
connectionInfoMessage = nil
if pairingFileExists && !heartbeatOK {
startHeartbeatInBackground()
}
} else {
let fallback = requiresLoopbackVPN ? "VPN tunnel is not connected." : "Unable to reach the device."
connectionCheckState = .failure(error ?? fallback)
connectionInfoMessage = error ?? fallback
}
}
connectionTimeoutTask?.cancel()
connectionTimeoutTask = nil
connectionCheckState = .success
connectionInfoMessage = nil
if pairingFileExists && !heartbeatOK {
startHeartbeatInBackground()
}
}
private var primaryActionTitle: String {
if isValidatingPairingFile { return "Validating…" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "Import New Pairing File" : "Import Pairing File" }
if shouldPromptForWiFi { return "Connect to Wi-Fi" }
if pairingFileLikelyInvalid { return "New Pairing File Needed" }
if !ddiMounted { return "Mount Developer Disk Image" }
return "Connect by App"
}
@ -933,97 +709,120 @@ struct HomeView: View {
if isValidatingPairingFile { return "hourglass" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" }
if shouldPromptForWiFi { return "wifi.slash" }
if pairingFileLikelyInvalid { return "arrow.clockwise" }
if !ddiMounted { return "externaldrive" }
return "cable.connector.horizontal"
}
private var pairingImportProgressView: some View {
VStack(spacing: 8) {
HStack {
Text("Processing pairing file…")
.font(.system(.caption, design: .rounded))
.foregroundStyle(.secondary)
Spacer()
Text("\(Int(importProgress * 100))%")
.font(.system(.caption, design: .rounded))
.foregroundStyle(.secondary)
}
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(Color(UIColor.tertiarySystemFill))
.frame(height: 8)
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(accentColor)
.frame(width: geo.size.width * CGFloat(importProgress), height: 8)
.animation(.linear(duration: 0.25), value: importProgress)
}
}
.frame(height: 8)
}
.accessibilityElement(children: .combine)
}
private var pairingSuccessMessage: some View {
HStack(spacing: 10) {
StatusDot(color: .green)
Text("Pairing file successfully imported")
.font(.system(.callout, design: .rounded))
.foregroundStyle(.green)
Spacer(minLength: 0)
}
.padding(.top, 4)
.transition(.opacity)
}
private func whiteCardButtonLabel(icon: String, title: String, isLoading: Bool = false) -> some View {
HStack(spacing: 10) {
if isLoading {
ProgressView()
.progressViewStyle(.circular)
.tint(accentColor.contrastText())
.frame(width: 20, height: 20)
} else {
Image(systemName: icon)
.font(.system(size: 20, weight: .semibold, design: .rounded))
}
Text(title)
.font(.system(.title3, design: .rounded).weight(.semibold))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(accentColor.contrastText())
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
)
.animation(.easeInOut(duration: 0.2), value: isLoading)
}
private func secondaryButtonLabel(icon: String, title: String) -> some View {
private var pairingImportProgressView: some View {
VStack(spacing: 8) {
HStack {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold, design: .rounded))
Text(title)
.font(.system(.title3, design: .rounded).weight(.semibold))
Text("Processing pairing file…")
.font(.system(.caption, design: .rounded))
.foregroundStyle(.secondary)
Spacer()
Text("\(Int(importProgress * 100))%")
.font(.system(.caption, design: .rounded))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground).opacity(0.6))
)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.12), lineWidth: 1)
)
.foregroundStyle(.primary)
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(Color(UIColor.tertiarySystemFill))
.frame(height: 8)
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(accentColor)
.frame(width: geo.size.width * CGFloat(importProgress), height: 8)
.animation(.linear(duration: 0.25), value: importProgress)
}
}
.frame(height: 8)
}
.accessibilityElement(children: .combine)
}
private var pairingSuccessMessage: some View {
HStack(spacing: 10) {
StatusDot(color: .green)
Text("Pairing file successfully imported")
.font(.system(.callout, design: .rounded))
.foregroundStyle(.green)
Spacer(minLength: 0)
}
.padding(.top, 4)
.transition(.opacity)
}
private func whiteCardButtonLabel(icon: String, title: String, isLoading: Bool = false) -> some View {
HStack(spacing: 10) {
if isLoading {
ProgressView()
.progressViewStyle(.circular)
.tint(accentColor.contrastText())
.frame(width: 20, height: 20)
} else {
Image(systemName: icon)
.font(.system(size: 20, weight: .semibold, design: .rounded))
}
Text(title)
.font(.system(.title3, design: .rounded).weight(.semibold))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.foregroundColor(accentColor.contrastText())
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.2), lineWidth: 0.5)
)
.animation(.easeInOut(duration: 0.2), value: isLoading)
}
private func secondaryButtonLabel(icon: String, title: String) -> some View {
HStack {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold, design: .rounded))
Text(title)
.font(.system(.title3, design: .rounded).weight(.semibold))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground).opacity(0.6))
)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.12), lineWidth: 1)
)
.foregroundStyle(.primary)
}
private func pillIconButton(icon: String, title: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 14, weight: .semibold, design: .rounded))
Text(title)
.font(.system(.footnote, design: .rounded).weight(.semibold))
.lineLimit(1)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(minWidth: 92, alignment: .center)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(Color.white.opacity(0.12), lineWidth: 1)
)
)
}
.buttonStyle(.plain)
}
private var quickConnectCard: some View {
homeCard {
@ -1193,9 +992,7 @@ struct HomeView: View {
refreshBackground()
checkPairingFileExists()
loadAppListIfNeeded()
if tunnel.tunnelStatus == .connected || !requiresLoopbackVPN {
MountingProgress.shared.checkforMounted()
}
MountingProgress.shared.checkforMounted()
}
}
}
@ -1207,8 +1004,6 @@ struct HomeView: View {
return
}
guard tunnel.tunnelStatus == .connected else { return }
if !force && !cachedAppNames.isEmpty { return }
DispatchQueue.global(qos: .userInitiated).async {
@ -1390,15 +1185,11 @@ struct HomeView: View {
}
return
}
if pairingFileExists {
if !ddiMounted {
showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in }
return
}
isShowingInstalledApps = true
} else {
isShowingPairingFilePicker = true
if !ddiMounted {
showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in }
return
}
isShowingInstalledApps = true
}
private func showCopiedToast() {
@ -1425,31 +1216,10 @@ struct HomeView: View {
}
private func checkPairingFileExists() {
let fileExists = FileManager.default.fileExists(atPath: pairingFileURL.path)
pairingFilePresentOnDisk = fileExists
guard fileExists else {
pairingFileExists = false
lastValidatedPairingSignature = nil
isValidatingPairingFile = false
return
}
let signature = pairingFileSignature(for: pairingFileURL)
guard needsValidation(for: signature) else { return }
guard !isValidatingPairingFile else { return }
isValidatingPairingFile = true
DispatchQueue.global(qos: .utility).async {
let valid = isPairing()
DispatchQueue.main.async {
pairingFileExists = valid
lastValidatedPairingSignature = signature
isValidatingPairingFile = false
}
}
// Home screen no longer blocks on pairing file checks; assume available.
pairingFileExists = true
pairingFilePresentOnDisk = true
isValidatingPairingFile = false
}
private func needsValidation(for signature: PairingFileSignature) -> Bool {

View file

@ -122,7 +122,7 @@ struct LocationSimulationView: View {
}
private var deviceIP: String {
UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.2"
UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.1"
}
var body: some View {

View file

@ -127,7 +127,7 @@ private extension ProcessInspectorView {
isConfirming: killCandidate?.pid == process.pid,
onKillTap: { handleKillTap(for: $0) }
)
.padding(.vertical, 10)
.padding(.vertical, 6)
if process.id != viewModel.filteredProcesses.last?.id {
Divider()
@ -170,23 +170,23 @@ private struct ProcessRow: View {
let onKillTap: (ProcessInfoEntry) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 6) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(process.displayName)
.font(.headline)
.font(.subheadline.weight(.semibold))
Spacer()
Text("PID \(process.pid)")
.font(.caption)
.font(.caption2)
.foregroundStyle(.secondary)
}
if let bundle = process.bundleID, !bundle.isEmpty {
Text(bundle)
.font(.caption)
.font(.caption2)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
Text(process.executablePath)
.font(.caption)
.font(.caption2)
.foregroundStyle(.secondary)
.textSelection(.enabled)
HStack {
@ -209,12 +209,13 @@ private struct ProcessRow: View {
}
}
.buttonStyle(.bordered)
.controlSize(.small)
.tint(isConfirming ? .green : .red)
.labelStyle(.iconOnly)
}
}
}
.padding(.vertical, 8)
.padding(.vertical, 4)
}
}

View file

@ -127,6 +127,7 @@ struct ProfileView: View {
@State private var alertSuccess = false
@State private var profiles: [Profile] = []
@AppStorage("cachedProfilesData") private var cachedProfilesData: Data?
@State private var confirmRemove = false
@State private var removeTargetName: String = ""
@State private var removeTargetUUID: String = ""
@ -208,7 +209,7 @@ struct ProfileView: View {
}
Button {
Task {await loadProfiles()}
Task { await loadProfiles(force: true) }
} label: {
Label("Reload", systemImage: "arrow.clockwise")
}
@ -348,15 +349,42 @@ struct ProfileView: View {
confirmRemove = true
}
func loadProfiles() async {
private func restoreCachedProfilesIfNeeded() async -> Bool {
guard profiles.isEmpty, let cachedProfilesData else { return false }
let cachedDatas: [Data]? = try? await Task.detached(priority: .userInitiated) {
try? JSONDecoder().decode([Data].self, from: cachedProfilesData)
}.value
guard let cachedDatas, !cachedDatas.isEmpty else { return false }
let restoredProfiles = cachedDatas.map { Profile(data: $0) }
await MainActor.run {
guard self.profiles.isEmpty else { return }
self.profiles = restoredProfiles
self.working = false
}
return true
}
func loadProfiles(force: Bool = false) async {
if !force {
if !profiles.isEmpty {
await MainActor.run { working = false }
return
}
if await restoreCachedProfilesIfNeeded() { return }
}
await MainActor.run { working = true }
do {
let profileDatas = try await Task.detached(priority: .userInitiated) {
try JITEnableContext.shared.fetchAllProfiles()
}.value
let parsedProfiles = profileDatas.map { Profile(data: $0) }
let encodedProfiles = try? JSONEncoder().encode(profileDatas)
await MainActor.run {
self.profiles = parsedProfiles
self.cachedProfilesData = encodedProfiles
self.working = false
}
} catch {
@ -385,7 +413,7 @@ struct ProfileView: View {
alertSuccess = true
alert = true
// Reload profiles after removal
await loadProfiles()
await loadProfiles(force: true)
} catch {
alertMsg = error.localizedDescription
alertTitle = "Failed to Remove Profile"
@ -420,7 +448,7 @@ struct ProfileView: View {
alert = true
// Reload profiles after adding
await loadProfiles()
await loadProfiles(force: true)
} catch {
alertMsg = error.localizedDescription
alertTitle = "Failed to Add Profile"

View file

@ -231,7 +231,7 @@ struct SettingsView: View {
RunLoop.current.add(progressTimer, forMode: .common)
DispatchQueue.main.async {
startHeartbeatInBackground(requireVPNConnection: false)
startHeartbeatInBackground()
}
} catch {

View file

@ -27,7 +27,7 @@ void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** pr
addr.sin_port = htons(LOCKDOWN_PORT);
NSString* deviceIP = [[NSUserDefaults standardUserDefaults] stringForKey:@"TunnelDeviceIP"];
inet_pton(AF_INET, deviceIP ? [deviceIP UTF8String] : "10.7.0.2", &addr.sin_addr);
inet_pton(AF_INET, deviceIP ? [deviceIP UTF8String] : "10.7.0.1", &addr.sin_addr);
err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file,

View file

@ -1,13 +0,0 @@
<?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

@ -1,57 +0,0 @@
//
// PacketTunnelProvider.swift
// TunnelProv
//
// Created by Stephen on 06/01/2025.
//
// Inspired by Stossy & StosVPN
//
import NetworkExtension
import Darwin
class PacketTunnelProvider: NEPacketTunnelProvider {
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
let deviceIP = options?["TunnelDeviceIP"] as? String ?? "10.7.0.0"
let fakeIP = options?["TunnelFakeIP"] as? String ?? "10.7.0.1"
let toNetwork: (String) -> UInt32 = { address in
var addr = in_addr()
inet_pton(AF_INET, address, &addr)
return addr.s_addr.bigEndian
}
let deviceNet = toNetwork(deviceIP), fakeNet = toNetwork(fakeIP)
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: deviceIP)
let ipv4 = NEIPv4Settings(addresses: [deviceIP], subnetMasks: ["255.255.255.0"])
ipv4.includedRoutes = [NEIPv4Route(destinationAddress: deviceIP, subnetMask: "255.255.255.0")]
ipv4.excludedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
setTunnelNetworkSettings(settings) { error in
guard error == nil else { return completionHandler(error) }
func process() {
self.packetFlow.readPackets { packets, protocols in
var modified = packets
for i in modified.indices where protocols[i].int32Value == AF_INET && modified[i].count >= 20 {
modified[i].withUnsafeMutableBytes { buffer in
let ptr = buffer.baseAddress!.assumingMemoryBound(to: UInt32.self)
let src = ptr[3], dst = ptr[4]
ptr[3] = src == deviceNet ? fakeNet : dst
ptr[4] = dst == fakeNet ? deviceNet : src
}
}
self.packetFlow.writePackets(modified, withProtocols: protocols)
process()
}
}
process()
completionHandler(nil)
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
}

View file

@ -1,14 +0,0 @@
<?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>