diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9505958 Binary files /dev/null and b/.DS_Store differ diff --git a/TetheringHider.xcodeproj/project.pbxproj b/TetheringHider.xcodeproj/project.pbxproj new file mode 100644 index 0000000..494db1c --- /dev/null +++ b/TetheringHider.xcodeproj/project.pbxproj @@ -0,0 +1,393 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0ED897B912F95BB51D023D21 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC8C459FAEBF6E4430F356C /* ContentView.swift */; }; + 1996632E29C85D523F161D27 /* GlassModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1E4DD25148C4B779DD7AB /* GlassModifiers.swift */; }; + 21C59AA87880BE42C84FA5AF /* StatusIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD85A3510C05A9BCF2AE3967 /* StatusIndicator.swift */; }; + 26F71842BA0787F021CAA6B2 /* SystemCommandService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F78DC8BF18792636E9816D1 /* SystemCommandService.swift */; }; + 2771ADA7302CEF6100A74E0C /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2771ADA6302CEF6100A74E0C /* Localizable.xcstrings */; }; + 46B0F9471E81979738AF3B77 /* TTLControlPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F622A688DC3081B00C57AF0 /* TTLControlPanel.swift */; }; + C2D263A68C7C3715C72D43E0 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303AC637A762389B92B1789E /* AuthenticationService.swift */; }; + C6FE29A2579CCD7D90FD002A /* TetheringHiderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF970E56192546A80A72095 /* TetheringHiderApp.swift */; }; + F85087C3ACC19F8A9811FF55 /* TTLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10849CA4224D433EE2901B17 /* TTLManager.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 10849CA4224D433EE2901B17 /* TTLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTLManager.swift; sourceTree = ""; }; + 1F622A688DC3081B00C57AF0 /* TTLControlPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTLControlPanel.swift; sourceTree = ""; }; + 2771ADA6302CEF6100A74E0C /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 2BF970E56192546A80A72095 /* TetheringHiderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TetheringHiderApp.swift; sourceTree = ""; }; + 303AC637A762389B92B1789E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; + 7F78DC8BF18792636E9816D1 /* SystemCommandService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemCommandService.swift; sourceTree = ""; }; + 8BC8C459FAEBF6E4430F356C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A8C3A702F0D5E0791E68D310 /* TetheringHider.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TetheringHider.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E8A1E4DD25148C4B779DD7AB /* GlassModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassModifiers.swift; sourceTree = ""; }; + FD85A3510C05A9BCF2AE3967 /* StatusIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusIndicator.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 09EB3BF2D219481B672AD590 /* Models */ = { + isa = PBXGroup; + children = ( + 10849CA4224D433EE2901B17 /* TTLManager.swift */, + ); + path = Models; + sourceTree = ""; + }; + 0E12970F7ADCC0D0DDFBEA23 /* Services */ = { + isa = PBXGroup; + children = ( + 303AC637A762389B92B1789E /* AuthenticationService.swift */, + 7F78DC8BF18792636E9816D1 /* SystemCommandService.swift */, + ); + path = Services; + sourceTree = ""; + }; + B96BAE44BD18AD7208D3DA80 /* Products */ = { + isa = PBXGroup; + children = ( + A8C3A702F0D5E0791E68D310 /* TetheringHider.app */, + ); + name = Products; + sourceTree = ""; + }; + BF87D2EA1A464B929DA82A81 /* Views */ = { + isa = PBXGroup; + children = ( + 8BC8C459FAEBF6E4430F356C /* ContentView.swift */, + 2771ADA6302CEF6100A74E0C /* Localizable.xcstrings */, + E8A1E4DD25148C4B779DD7AB /* GlassModifiers.swift */, + FD85A3510C05A9BCF2AE3967 /* StatusIndicator.swift */, + 1F622A688DC3081B00C57AF0 /* TTLControlPanel.swift */, + ); + path = Views; + sourceTree = ""; + }; + C0FBE1EA54AD5144BFFD3461 = { + isa = PBXGroup; + children = ( + D61DD9130F3129C572A0794F /* TetheringHider */, + B96BAE44BD18AD7208D3DA80 /* Products */, + ); + sourceTree = ""; + }; + D61DD9130F3129C572A0794F /* TetheringHider */ = { + isa = PBXGroup; + children = ( + 09EB3BF2D219481B672AD590 /* Models */, + 0E12970F7ADCC0D0DDFBEA23 /* Services */, + BF87D2EA1A464B929DA82A81 /* Views */, + 2BF970E56192546A80A72095 /* TetheringHiderApp.swift */, + ); + path = TetheringHider; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 522E0329F51D9AE0DD4F79C2 /* TetheringHider */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2DDA7E49014C25A2097D74CA /* Build configuration list for PBXNativeTarget "TetheringHider" */; + buildPhases = ( + 0C4BD1F074A5C215A10B53C9 /* Sources */, + 2771ADA8302CEF6100A74E0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TetheringHider; + packageProductDependencies = ( + ); + productName = TetheringHider; + productReference = A8C3A702F0D5E0791E68D310 /* TetheringHider.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B2FDB901F8B4728B7FE78327 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 2660; + TargetAttributes = { + 522E0329F51D9AE0DD4F79C2 = { + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 91E0115F13D05E40BA171708 /* Build configuration list for PBXProject "TetheringHider" */; + developmentRegion = es; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + es, + ); + mainGroup = C0FBE1EA54AD5144BFFD3461; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = B96BAE44BD18AD7208D3DA80 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 522E0329F51D9AE0DD4F79C2 /* TetheringHider */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2771ADA8302CEF6100A74E0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2771ADA7302CEF6100A74E0C /* Localizable.xcstrings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0C4BD1F074A5C215A10B53C9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C2D263A68C7C3715C72D43E0 /* AuthenticationService.swift in Sources */, + 0ED897B912F95BB51D023D21 /* ContentView.swift in Sources */, + 1996632E29C85D523F161D27 /* GlassModifiers.swift in Sources */, + 21C59AA87880BE42C84FA5AF /* StatusIndicator.swift in Sources */, + 26F71842BA0787F021CAA6B2 /* SystemCommandService.swift in Sources */, + 46B0F9471E81979738AF3B77 /* TTLControlPanel.swift in Sources */, + F85087C3ACC19F8A9811FF55 /* TTLManager.swift in Sources */, + C6FE29A2579CCD7D90FD002A /* TetheringHiderApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 11AA62F6617B022CFD404264 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_APP_SANDBOX = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; + 31E4FAB6483D842FA3A3FCDE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = TetheringHider/TetheringHider.entitlements; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDevelopmentRegion = es; + INFOPLIST_KEY_CFBundleDisplayName = "Tethering Hider"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tetheringhider.app; + PRODUCT_NAME = TetheringHider; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7EADBBF3285DFAA6D9F296D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = TetheringHider/TetheringHider.entitlements; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDevelopmentRegion = es; + INFOPLIST_KEY_CFBundleDisplayName = "Tethering Hider"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tetheringhider.app; + PRODUCT_NAME = TetheringHider; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + B9CCE047ED62954275DBEDE8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_APP_SANDBOX = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2DDA7E49014C25A2097D74CA /* Build configuration list for PBXNativeTarget "TetheringHider" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 31E4FAB6483D842FA3A3FCDE /* Debug */, + 7EADBBF3285DFAA6D9F296D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 91E0115F13D05E40BA171708 /* Build configuration list for PBXProject "TetheringHider" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9CCE047ED62954275DBEDE8 /* Debug */, + 11AA62F6617B022CFD404264 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = B2FDB901F8B4728B7FE78327 /* Project object */; +} diff --git a/TetheringHider.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TetheringHider.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/TetheringHider.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TetheringHider/Models/TTLManager.swift b/TetheringHider/Models/TTLManager.swift new file mode 100644 index 0000000..11f66c4 --- /dev/null +++ b/TetheringHider/Models/TTLManager.swift @@ -0,0 +1,64 @@ +import Foundation +import Observation + +@Observable +@MainActor +final class TTLManager { + private(set) var currentTTL: Int = 64 + private(set) var currentIPv6HopLimit: Int = 64 + private(set) var isProtected: Bool = false + private(set) var lastError: String? + + var onStatusChanged: (() -> Void)? + + let defaultTTL: Int = 64 + let targetTTL: Int = 65 + + init() { + refreshStatus() + } + + /// Reads the current system IPv4 TTL value using sysctlbyname (no root required) + func readCurrentTTL() -> Int { + var ttl: Int32 = 0 + var size = MemoryLayout.size + let result = sysctlbyname("net.inet.ip.ttl", &ttl, &size, nil, 0) + if result == 0 { + return Int(ttl) + } + return defaultTTL + } + + /// Reads the current system IPv6 Hop Limit value using sysctlbyname (no root required) + func readCurrentIPv6HopLimit() -> Int { + var hlim: Int32 = 0 + var size = MemoryLayout.size + let result = sysctlbyname("net.inet6.ip6.hlim", &hlim, &size, nil, 0) + if result == 0 { + return Int(hlim) + } + return defaultTTL + } + + /// Refreshes current IPv4 TTL, IPv6 Hop Limit, and protection status + func refreshStatus() { + currentTTL = readCurrentTTL() + currentIPv6HopLimit = readCurrentIPv6HopLimit() + isProtected = (currentTTL == targetTTL) && (currentIPv6HopLimit == targetTTL) + lastError = nil + onStatusChanged?() + } + + func didSetTTL(_ value: Int) { + currentTTL = value + currentIPv6HopLimit = value + isProtected = value == targetTTL + lastError = nil + onStatusChanged?() + } + + func setError(_ message: String) { + lastError = message + onStatusChanged?() + } +} diff --git a/TetheringHider/Services/AuthenticationService.swift b/TetheringHider/Services/AuthenticationService.swift new file mode 100644 index 0000000..0f86cf8 --- /dev/null +++ b/TetheringHider/Services/AuthenticationService.swift @@ -0,0 +1,53 @@ +import Foundation +import LocalAuthentication + +enum AuthenticationError: LocalizedError { + case biometricsNotAvailable + case authenticationFailed(String) + case userCancelled + + var errorDescription: String? { + switch self { + case .biometricsNotAvailable: + return "Touch ID is not available on this device." + case .authenticationFailed(let reason): + return "Authentication failed: \(reason)" + case .userCancelled: + return "Authentication was cancelled." + } + } +} + +struct AuthenticationService: Sendable { + + /// Attempts biometric authentication (Touch ID) before proceeding. + /// Falls back gracefully if Touch ID is not available. + /// Returns true if authenticated, false if biometrics unavailable (will proceed to admin dialog). + func authenticateWithBiometrics() async throws -> Bool { + let context = LAContext() + var error: NSError? + + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + // Touch ID not available — skip biometric step, will use admin dialog + return true + } + + do { + let success = try await context.evaluatePolicy( + .deviceOwnerAuthenticationWithBiometrics, + localizedReason: "Authenticate to modify system TTL value" + ) + return success + } catch let authError as LAError { + switch authError.code { + case .userCancel, .appCancel: + throw AuthenticationError.userCancelled + case .userFallback: + // User chose to use password — allow proceeding to admin dialog + return true + default: + throw AuthenticationError.authenticationFailed(authError.localizedDescription) + } + } + } +} diff --git a/TetheringHider/Services/SystemCommandService.swift b/TetheringHider/Services/SystemCommandService.swift new file mode 100644 index 0000000..6502c00 --- /dev/null +++ b/TetheringHider/Services/SystemCommandService.swift @@ -0,0 +1,56 @@ +import Foundation + +enum SystemCommandError: LocalizedError { + case executionFailed(String) + case userCancelledAuth + case scriptError(String) + + var errorDescription: String? { + switch self { + case .executionFailed(let reason): + return "Command failed: \(reason)" + case .userCancelledAuth: + return "Administrator authentication was cancelled." + case .scriptError(let detail): + return "Script error: \(detail)" + } + } +} + +struct SystemCommandService: Sendable { + + /// Sets IPv4 TTL, IPv6 Hop Limit, and flushes system DNS cache with administrator privileges. + func setTTL(_ value: Int) async throws { + let command = "sysctl -w net.inet.ip.ttl=\(value) && sysctl -w net.inet6.ip6.hlim=\(value) && dscacheutil -flushcache && killall -HUP mDNSResponder" + try await executeWithAdminPrivileges(command) + } + + /// Resets IPv4 TTL and IPv6 Hop Limit to default (64) and flushes DNS cache. + func resetTTL() async throws { + try await setTTL(64) + } + + /// Executes a shell command with administrator privileges via NSAppleScript + @MainActor + private func executeWithAdminPrivileges(_ command: String) async throws { + let scriptSource = "do shell script \"\(command)\" with administrator privileges" + + guard let script = NSAppleScript(source: scriptSource) else { + throw SystemCommandError.scriptError("Failed to create AppleScript.") + } + + var errorDict: NSDictionary? + script.executeAndReturnError(&errorDict) + + if let error = errorDict { + let errorNumber = error[NSAppleScript.errorNumber] as? Int ?? -1 + let errorMessage = error[NSAppleScript.errorMessage] as? String ?? "Unknown error" + + if errorNumber == -128 { + throw SystemCommandError.userCancelledAuth + } + + throw SystemCommandError.executionFailed(errorMessage) + } + } +} diff --git a/TetheringHider/TetheringHider.entitlements b/TetheringHider/TetheringHider.entitlements new file mode 100644 index 0000000..e89b7f3 --- /dev/null +++ b/TetheringHider/TetheringHider.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/TetheringHider/TetheringHiderApp.swift b/TetheringHider/TetheringHiderApp.swift new file mode 100644 index 0000000..00a56e8 --- /dev/null +++ b/TetheringHider/TetheringHiderApp.swift @@ -0,0 +1,183 @@ +// +// TetheringHiderApp.swift +// +// +// Created by Jesús David Chapman Vélez on 12/08/26. +// + +import SwiftUI +import AppKit + +@MainActor +final class StatusBarController: NSObject, NSMenuDelegate { + private var statusItem: NSStatusItem? + private let ttlManager: TTLManager + private let authService = AuthenticationService() + private let commandService = SystemCommandService() + + init(ttlManager: TTLManager) { + self.ttlManager = ttlManager + super.init() + setupStatusItem() + + self.ttlManager.onStatusChanged = { [weak self] in + Task { @MainActor in + self?.updateMenu() + } + } + } + + func setupStatusItem() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + updateMenu() + } + + func updateMenu() { + guard let statusItem = statusItem, let button = statusItem.button else { return } + + // Status Bar Icon + let imageName = ttlManager.isProtected + ? "antenna.radiowaves.left.and.right" + : "antenna.radiowaves.left.and.right.slash" + + button.image = NSImage(systemSymbolName: imageName, accessibilityDescription: "Tethering Hider") + + let menu = NSMenu() + menu.delegate = self + + let statusTitle = ttlManager.isProtected + ? String(localized: "Tethering Hider — Hotspot Cloaked") + : String(localized: "Tethering Hider — Standard Tethering") + + let headerItem = NSMenuItem(title: statusTitle, action: nil, keyEquivalent: "") + headerItem.isEnabled = false + headerItem.image = NSImage( + systemSymbolName: ttlManager.isProtected ? "eye.slash.fill" : "eye.fill", + accessibilityDescription: nil + ) + menu.addItem(headerItem) + + menu.addItem(NSMenuItem.separator()) + + let toggleTitle = ttlManager.isProtected + ? String(localized: "Disable Hotspot Cloak (Set TTL 64)") + : String(localized: "Enable Hotspot Cloak (Set TTL 65)") + + let toggleItem = NSMenuItem(title: toggleTitle, action: #selector(toggleAction), keyEquivalent: "") + toggleItem.target = self + toggleItem.image = NSImage( + systemSymbolName: ttlManager.isProtected ? "eye.fill" : "eye.slash.fill", + accessibilityDescription: nil + ) + menu.addItem(toggleItem) + + menu.addItem(NSMenuItem.separator()) + + let ipv4Format = String(localized: "IPv4 TTL: %d") + let ipv4Item = NSMenuItem(title: String(format: ipv4Format, ttlManager.currentTTL), action: nil, keyEquivalent: "") + ipv4Item.isEnabled = false + ipv4Item.image = NSImage(systemSymbolName: "network", accessibilityDescription: nil) + menu.addItem(ipv4Item) + + let ipv6Format = String(localized: "IPv6 HLIM: %d") + let ipv6Item = NSMenuItem(title: String(format: ipv6Format, ttlManager.currentIPv6HopLimit), action: nil, keyEquivalent: "") + ipv6Item.isEnabled = false + ipv6Item.image = NSImage(systemSymbolName: "globe", accessibilityDescription: nil) + menu.addItem(ipv6Item) + + let dnsTitle = String(localized: "DNS Flush: AUTO") + let dnsItem = NSMenuItem(title: dnsTitle, action: nil, keyEquivalent: "") + dnsItem.isEnabled = false + dnsItem.image = NSImage(systemSymbolName: "arrow.triangle.2.circlepath", accessibilityDescription: nil) + menu.addItem(dnsItem) + + menu.addItem(NSMenuItem.separator()) + + let aboutTitle = String(localized: "About Tethering Hider...") + let aboutItem = NSMenuItem(title: aboutTitle, action: #selector(aboutAction), keyEquivalent: "") + aboutItem.target = self + aboutItem.image = NSImage(systemSymbolName: "info.circle.fill", accessibilityDescription: nil) + menu.addItem(aboutItem) + + menu.addItem(NSMenuItem.separator()) + + let quitTitle = String(localized: "Quit Tethering Hider") + let quitItem = NSMenuItem(title: quitTitle, action: #selector(quitAction), keyEquivalent: "q") + quitItem.target = self + quitItem.image = NSImage(systemSymbolName: "rectangle.portrait.and.arrow.right", accessibilityDescription: nil) + menu.addItem(quitItem) + + statusItem.menu = menu + } + + func menuWillOpen(_ menu: NSMenu) { + ttlManager.refreshStatus() + } + + @objc private func toggleAction() { + Task { @MainActor in + do { + let authenticated = try await authService.authenticateWithBiometrics() + guard authenticated else { return } + + if ttlManager.isProtected { + try await commandService.resetTTL() + ttlManager.didSetTTL(ttlManager.defaultTTL) + } else { + try await commandService.setTTL(ttlManager.targetTTL) + ttlManager.didSetTTL(ttlManager.targetTTL) + } + updateMenu() + } catch { + ttlManager.setError(error.localizedDescription) + } + } + } + + @objc private func aboutAction() { + let creditsString = String(localized: "Bypasses mobile carrier hotspot tethering detection by adjusting outgoing IPv4 TTL to 65, IPv6 Hop Limit to 65, and flushing system DNS caches.\n\nDesigned for macOS.") + let options: [NSApplication.AboutPanelOptionKey: Any] = [ + .credits: NSAttributedString( + string: creditsString, + attributes: [ + .font: NSFont.systemFont(ofSize: 11), + .foregroundColor: NSColor.secondaryLabelColor + ] + ), + .applicationName: "Tethering Hider", + .version: "1.0.0" + ] + NSApp.orderFrontStandardAboutPanel(options: options) + NSApp.activate(ignoringOtherApps: true) + } + + @objc private func quitAction() { + NSApp.terminate(nil) + } +} + +@MainActor +final class AppDelegate: NSObject, NSApplicationDelegate { + var statusBarController: StatusBarController? + let ttlManager = TTLManager() + + func applicationWillFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.accessory) + } + + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.accessory) + self.statusBarController = StatusBarController(ttlManager: ttlManager) + } +} + +@main +struct TetheringHiderApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + Settings { + EmptyView() + } + } +} diff --git a/TetheringHider/Views/ContentView.swift b/TetheringHider/Views/ContentView.swift new file mode 100644 index 0000000..df6d8a8 --- /dev/null +++ b/TetheringHider/Views/ContentView.swift @@ -0,0 +1,218 @@ +// +// ContentView.swift +// +// +// Created by Jesús David Chapman Vélez on 12/08/26. +// + +import SwiftUI + +struct ContentView: View { + @Environment(TTLManager.self) private var ttlManager + @AppStorage("startMinimized") private var startMinimized = false + @State private var hasHandledInitialLaunch = false + @State private var isProcessing = false + @State private var showError = false + @State private var errorMessage = "" + @Namespace private var shieldNamespace + + private let authService = AuthenticationService() + private let commandService = SystemCommandService() + + @Environment(\.colorScheme) private var colorScheme + + private var accentColor: Color { + if colorScheme == .light { + return ttlManager.isProtected + ? Color(hue: 0.38, saturation: 0.85, brightness: 0.40) + : Color(hue: 0.05, saturation: 0.90, brightness: 0.45) + } else { + return ttlManager.isProtected + ? Color(hue: 0.38, saturation: 0.70, brightness: 0.85) + : Color(hue: 0.06, saturation: 0.70, brightness: 0.90) + } + } + + var body: some View { + ZStack { + // Background gradient + backgroundGradient + + VStack(spacing: 0) { + // Header with glass + headerSection + .padding(.horizontal, 20) + .padding(.vertical, 10) + .modifier(GlassInteractiveModifier(cornerRadius: 14, tintColor: accentColor)) + .padding(.top, 14) + + Spacer(minLength: 12) + + // Central antenna indicator + StatusIndicator( + isProtected: ttlManager.isProtected, + isProcessing: isProcessing + ) + .padding(.vertical, 10) + + Spacer(minLength: 12) + + // Control panel + TTLControlPanel( + currentTTL: ttlManager.currentTTL, + currentIPv6HopLimit: ttlManager.currentIPv6HopLimit, + targetTTL: ttlManager.targetTTL, + defaultTTL: ttlManager.defaultTTL, + isProtected: ttlManager.isProtected, + isProcessing: isProcessing, + onActivate: activateProtection, + onDeactivate: deactivateProtection + ) + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + } + .frame(width: 400, height: 580) + .alert("Error", isPresented: $showError) { + Button("OK", role: .cancel) { } + } message: { + Text(errorMessage) + } + .task { + ttlManager.refreshStatus() + } + } + + private var backgroundGradient: some View { + MeshGradient( + width: 3, height: 3, + points: [ + [0.0, 0.0], [0.5, 0.0], [1.0, 0.0], + [0.0, 0.5], [0.5, 0.5], [1.0, 0.5], + [0.0, 1.0], [0.5, 1.0], [1.0, 1.0] + ], + colors: meshColors + ) + .ignoresSafeArea() + .animation(.easeInOut(duration: 1.2), value: ttlManager.isProtected) + .animation(.easeInOut(duration: 0.5), value: colorScheme) + } + + private var meshColors: [Color] { + if colorScheme == .light { + return ttlManager.isProtected ? [ + Color(hue: 0.44, saturation: 0.22, brightness: 0.95), + Color(hue: 0.48, saturation: 0.18, brightness: 0.98), + Color(hue: 0.52, saturation: 0.22, brightness: 0.94), + Color(hue: 0.40, saturation: 0.16, brightness: 0.96), + Color(hue: 0.46, saturation: 0.26, brightness: 0.93), + Color(hue: 0.50, saturation: 0.18, brightness: 0.97), + Color(hue: 0.42, saturation: 0.20, brightness: 0.95), + Color(hue: 0.45, saturation: 0.16, brightness: 0.98), + Color(hue: 0.51, saturation: 0.22, brightness: 0.94) + ] : [ + Color(hue: 0.04, saturation: 0.18, brightness: 0.96), + Color(hue: 0.07, saturation: 0.14, brightness: 0.98), + Color(hue: 0.09, saturation: 0.18, brightness: 0.95), + Color(hue: 0.02, saturation: 0.14, brightness: 0.97), + Color(hue: 0.05, saturation: 0.24, brightness: 0.94), + Color(hue: 0.08, saturation: 0.16, brightness: 0.98), + Color(hue: 0.03, saturation: 0.18, brightness: 0.95), + Color(hue: 0.06, saturation: 0.14, brightness: 0.98), + Color(hue: 0.08, saturation: 0.18, brightness: 0.94) + ] + } else { + return ttlManager.isProtected ? [ + Color(hue: 0.45, saturation: 0.4, brightness: 0.08), + Color(hue: 0.50, saturation: 0.3, brightness: 0.10), + Color(hue: 0.55, saturation: 0.4, brightness: 0.08), + Color(hue: 0.40, saturation: 0.3, brightness: 0.12), + Color(hue: 0.48, saturation: 0.5, brightness: 0.14), + Color(hue: 0.52, saturation: 0.3, brightness: 0.10), + Color(hue: 0.42, saturation: 0.4, brightness: 0.06), + Color(hue: 0.47, saturation: 0.3, brightness: 0.10), + Color(hue: 0.53, saturation: 0.4, brightness: 0.08) + ] : [ + Color(hue: 0.02, saturation: 0.3, brightness: 0.08), + Color(hue: 0.05, saturation: 0.2, brightness: 0.10), + Color(hue: 0.08, saturation: 0.3, brightness: 0.08), + Color(hue: 0.00, saturation: 0.2, brightness: 0.12), + Color(hue: 0.04, saturation: 0.4, brightness: 0.14), + Color(hue: 0.07, saturation: 0.2, brightness: 0.10), + Color(hue: 0.01, saturation: 0.3, brightness: 0.06), + Color(hue: 0.03, saturation: 0.2, brightness: 0.10), + Color(hue: 0.06, saturation: 0.3, brightness: 0.08) + ] + } + } + + // MARK: - Header + + private var headerSection: some View { + VStack(spacing: 6) { + Text("TETHERING HIDER") + .font(.system(size: 14, weight: .bold, design: .rounded)) + .tracking(4) + .foregroundStyle(.secondary) + + Text(ttlManager.isProtected ? "Tethering Hidden" : "Tethering Exposed") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(ttlManager.isProtected + ? Color(hue: 0.38, saturation: 0.7, brightness: 0.85) + : Color(hue: 0.06, saturation: 0.7, brightness: 0.90) + ) + .contentTransition(.numericText()) + .animation(.easeInOut, value: ttlManager.isProtected) + } + } + + private func activateProtection() { + Task { + isProcessing = true + defer { isProcessing = false } + + do { + // Authenticate with Touch ID + let authenticated = try await authService.authenticateWithBiometrics() + guard authenticated else { return } + + // Execute privileged command + try await commandService.setTTL(ttlManager.targetTTL) + + // Refresh status + ttlManager.didSetTTL(ttlManager.targetTTL) + } catch is AuthenticationError { + + } catch SystemCommandError.userCancelledAuth { + + } catch { + errorMessage = error.localizedDescription + showError = true + ttlManager.setError(error.localizedDescription) + } + } + } + + private func deactivateProtection() { + Task { + isProcessing = true + defer { isProcessing = false } + + do { + let authenticated = try await authService.authenticateWithBiometrics() + guard authenticated else { return } + + try await commandService.resetTTL() + ttlManager.didSetTTL(ttlManager.defaultTTL) + } catch is AuthenticationError { + + } catch SystemCommandError.userCancelledAuth { + + } catch { + errorMessage = error.localizedDescription + showError = true + ttlManager.setError(error.localizedDescription) + } + } + } +} diff --git a/TetheringHider/Views/GlassModifiers.swift b/TetheringHider/Views/GlassModifiers.swift new file mode 100644 index 0000000..7742340 --- /dev/null +++ b/TetheringHider/Views/GlassModifiers.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct GlassCardModifier: ViewModifier { + let tintColor: Color + + func body(content: Content) -> some View { + if #available(macOS 26, *) { + content + .glassEffect(.regular.tint(tintColor.opacity(0.15)), in: .rect(cornerRadius: 16)) + } else { + content + } + } +} + +struct GlassButtonModifier: ViewModifier { + var isActive: Bool = false + + func body(content: Content) -> some View { + if #available(macOS 26, *) { + content + .buttonStyle(.glassProminent) + } else { + content + .buttonStyle(.borderedProminent) + } + } +} + +struct GlassInteractiveModifier: ViewModifier { + var cornerRadius: CGFloat = 20 + var tintColor: Color = .clear + + func body(content: Content) -> some View { + if #available(macOS 26, *) { + content + .glassEffect( + .regular.tint(tintColor.opacity(0.1)).interactive(), + in: .rect(cornerRadius: cornerRadius) + ) + } else { + content + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) + } + } +} diff --git a/TetheringHider/Views/Localizable.xcstrings b/TetheringHider/Views/Localizable.xcstrings new file mode 100644 index 0000000..18a9c0f --- /dev/null +++ b/TetheringHider/Views/Localizable.xcstrings @@ -0,0 +1,146 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "" : { + + }, + "%@: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@: %2$@" + } + } + } + }, + "%lld" : { + + }, + "About Tethering Hider..." : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de Tethering Hider..." + } + } + } + }, + "Activate Hider" : { + + }, + "Bypasses mobile carrier hotspot tethering detection by adjusting outgoing IPv4 TTL to 65, IPv6 Hop Limit to 65, and flushing system DNS caches.\n\nDesigned for macOS." : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evita la detección de anclaje de datos (hotspot) del operador móvil ajustando el TTL de IPv4 a 65, el Hop Limit de IPv6 a 65 y vaciando la caché DNS del sistema.\n\nDiseñado para macOS." + } + } + } + }, + "Deactivate Hider" : { + + }, + "Disable Hotspot Cloak (Set TTL 64)" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactivar Ocultación de Anclaje (TTL 64)" + } + } + } + }, + "DNS Flush: AUTO" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaciado DNS: AUTO" + } + } + } + }, + "Enable Hotspot Cloak (Set TTL 65)" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar Ocultación de Anclaje (TTL 65)" + } + } + } + }, + "Error" : { + + }, + "IPv4 TTL: %d" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IPv4 TTL: %d" + } + } + } + }, + "IPv6 HLIM: %d" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IPv6 HLIM: %d" + } + } + } + }, + "OK" : { + + }, + "Quit Tethering Hider" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir de Tethering Hider" + } + } + } + }, + "Start minimized in menu bar" : { + + }, + "Tethering Exposed" : { + + }, + "Tethering Hidden" : { + + }, + "TETHERING HIDER" : { + + }, + "Tethering Hider — Hotspot Cloaked" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tethering Hider — Ocultación Activa" + } + } + } + }, + "Tethering Hider — Standard Tethering" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tethering Hider — Modo Estándar" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/TetheringHider/Views/MenuBarView.swift b/TetheringHider/Views/MenuBarView.swift new file mode 100644 index 0000000..7f32484 --- /dev/null +++ b/TetheringHider/Views/MenuBarView.swift @@ -0,0 +1,181 @@ +import SwiftUI + +struct MenuBarView: View { + @Environment(TTLManager.self) private var ttlManager + @Environment(\.openWindow) private var openWindow + @Environment(\.colorScheme) private var colorScheme + + @State private var isProcessing = false + @State private var showError = false + @State private var errorMessage = "" + + private let authService = AuthenticationService() + private let commandService = SystemCommandService() + + private var accentColor: Color { + if colorScheme == .light { + return ttlManager.isProtected + ? Color(hue: 0.38, saturation: 0.85, brightness: 0.40) + : Color(hue: 0.05, saturation: 0.90, brightness: 0.45) + } else { + return ttlManager.isProtected + ? Color(hue: 0.38, saturation: 0.70, brightness: 0.85) + : Color(hue: 0.06, saturation: 0.70, brightness: 0.90) + } + } + + var body: some View { + VStack(spacing: 14) { + // Header + HStack(spacing: 10) { + Image(systemName: ttlManager.isProtected ? "antenna.radiowaves.left.and.right" : "antenna.radiowaves.left.and.right.slash") + .font(.system(size: 20, weight: .medium)) + .foregroundStyle(accentColor) + .symbolEffect(.bounce, value: ttlManager.isProtected) + + VStack(alignment: .leading, spacing: 2) { + Text("Tethering Hider") + .font(.system(size: 13, weight: .bold, design: .rounded)) + + Text(ttlManager.isProtected ? "Protected (TTL 65)" : "Exposed (TTL 64)") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(accentColor) + } + + Spacer() + + // Toggle Switch + Toggle("", isOn: Binding( + get: { ttlManager.isProtected }, + set: { newValue in + if newValue { + activateProtection() + } else { + deactivateProtection() + } + } + )) + .toggleStyle(.switch) + .disabled(isProcessing) + .tint(accentColor) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(accentColor.opacity(0.2), lineWidth: 0.5) + ) + ) + .modifier(GlassCardModifier(tintColor: accentColor)) + + // Status Badges + HStack(spacing: 6) { + badgeItem(title: "IPv4", val: "\(ttlManager.currentTTL)", active: ttlManager.currentTTL == 65) + badgeItem(title: "IPv6", val: "\(ttlManager.currentIPv6HopLimit)", active: ttlManager.currentIPv6HopLimit == 65) + badgeItem(title: "DNS", val: "FLUSH", active: ttlManager.isProtected) + } + + Divider() + .opacity(0.3) + + // Action Links + VStack(spacing: 6) { + Button { + NSApp.activate(ignoringOtherApps: true) + openWindow(id: "mainWindow") + } label: { + Label("Open Dashboard", systemImage: "macwindow") + .font(.system(size: 12, weight: .medium)) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(RoundedRectangle(cornerRadius: 6).fill(Color.primary.opacity(0.05))) + + Button(role: .destructive) { + NSApp.terminate(nil) + } label: { + Label("Quit Tethering Hider", systemImage: "power") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(.red) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, 10) + .padding(.vertical, 6) + } + } + .padding(14) + .frame(width: 280) + .alert("Error", isPresented: $showError) { + Button("OK", role: .cancel) { } + } message: { + Text(errorMessage) + } + .task { + ttlManager.refreshStatus() + } + } + + private func badgeItem(title: String, val: String, active: Bool) -> some View { + HStack(spacing: 3) { + Circle() + .fill(active ? Color.green : Color.orange) + .frame(width: 4, height: 4) + Text("\(title): \(val)") + .font(.system(size: 9, weight: .medium, design: .monospaced)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background(Capsule().fill(.ultraThinMaterial)) + } + + private func activateProtection() { + Task { + isProcessing = true + defer { isProcessing = false } + + do { + let authenticated = try await authService.authenticateWithBiometrics() + guard authenticated else { return } + + try await commandService.setTTL(ttlManager.targetTTL) + ttlManager.didSetTTL(ttlManager.targetTTL) + } catch is AuthenticationError { + } catch SystemCommandError.userCancelledAuth { + } catch { + errorMessage = error.localizedDescription + showError = true + ttlManager.setError(error.localizedDescription) + } + } + } + + private func deactivateProtection() { + Task { + isProcessing = true + defer { isProcessing = false } + + do { + let authenticated = try await authService.authenticateWithBiometrics() + guard authenticated else { return } + + try await commandService.resetTTL() + ttlManager.didSetTTL(ttlManager.defaultTTL) + } catch is AuthenticationError { + } catch SystemCommandError.userCancelledAuth { + } catch { + errorMessage = error.localizedDescription + showError = true + ttlManager.setError(error.localizedDescription) + } + } + } +} diff --git a/TetheringHider/Views/StatusIndicator.swift b/TetheringHider/Views/StatusIndicator.swift new file mode 100644 index 0000000..0c13ab0 --- /dev/null +++ b/TetheringHider/Views/StatusIndicator.swift @@ -0,0 +1,110 @@ +import SwiftUI + +struct StatusIndicator: View { + let isProtected: Bool + let isProcessing: Bool + + @Environment(\.colorScheme) private var colorScheme + @State private var pulseScale: CGFloat = 1.0 + @State private var glowOpacity: Double = 0.4 + @State private var rotationAngle: Double = 0 + + private var accentColor: Color { + if colorScheme == .light { + return isProtected + ? Color(hue: 0.38, saturation: 0.85, brightness: 0.40) + : Color(hue: 0.05, saturation: 0.90, brightness: 0.45) + } else { + return isProtected + ? Color(hue: 0.38, saturation: 0.75, brightness: 0.80) + : Color(hue: 0.06, saturation: 0.75, brightness: 0.85) + } + } + + private var statusText: String { + if isProcessing { return "Processing..." } + return isProtected ? "PROTECTED" : "EXPOSED" + } + + var body: some View { + VStack(spacing: 20) { + ZStack { + + ForEach(0..<3, id: \.self) { ring in + Circle() + .stroke( + accentColor.opacity(colorScheme == .light ? (0.28 - Double(ring) * 0.07) : (0.15 - Double(ring) * 0.04)), + lineWidth: colorScheme == .light ? 2.0 : 1.5 + ) + .frame( + width: 140 + CGFloat(ring) * 22, + height: 140 + CGFloat(ring) * 22 + ) + .scaleEffect(pulseScale + CGFloat(ring) * 0.02) + } + + Circle() + .fill( + RadialGradient( + colors: [ + accentColor.opacity(0.25), + accentColor.opacity(0.08), + .clear + ], + center: .center, + startRadius: 20, + endRadius: 75 + ) + ) + .frame(width: 130, height: 130) + .scaleEffect(pulseScale) + + // Antenna icon + Image(systemName: isProtected ? "antenna.radiowaves.left.and.right" : "antenna.radiowaves.left.and.right.slash") + .font(.system(size: 52, weight: .light)) + .foregroundStyle(accentColor) + .symbolEffect(.bounce, value: isProtected) + .contentTransition(.symbolEffect(.replace.magic(fallback: .replace))) + .shadow(color: accentColor.opacity(0.5), radius: 16) + + // Processing spinner + if isProcessing { + Circle() + .trim(from: 0, to: 0.7) + .stroke( + accentColor, + style: StrokeStyle(lineWidth: 2, lineCap: .round) + ) + .frame(width: 125, height: 125) + .rotationEffect(.degrees(rotationAngle)) + } + } + .animation(.easeInOut(duration: 0.8), value: isProtected) + .animation(.easeInOut(duration: 0.4), value: isProcessing) + + // Status text + Text(statusText) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .tracking(3) + .foregroundStyle(accentColor) + .contentTransition(.numericText()) + .animation(.easeInOut, value: statusText) + } + .onAppear { + withAnimation( + .easeInOut(duration: 2.0) + .repeatForever(autoreverses: true) + ) { + pulseScale = 1.04 + glowOpacity = 0.7 + } + + withAnimation( + .linear(duration: 1.0) + .repeatForever(autoreverses: false) + ) { + rotationAngle = 360 + } + } + } +} diff --git a/TetheringHider/Views/TTLControlPanel.swift b/TetheringHider/Views/TTLControlPanel.swift new file mode 100644 index 0000000..f8b8c62 --- /dev/null +++ b/TetheringHider/Views/TTLControlPanel.swift @@ -0,0 +1,207 @@ +import SwiftUI + +struct TTLControlPanel: View { + let currentTTL: Int + let currentIPv6HopLimit: Int + let targetTTL: Int + let defaultTTL: Int + let isProtected: Bool + let isProcessing: Bool + let onActivate: () -> Void + let onDeactivate: () -> Void + + @AppStorage("startMinimized") private var startMinimized = false + @Environment(\.colorScheme) private var colorScheme + @Namespace private var controlNamespace + + @State private var isHovering = false + + private var accentColor: Color { + if colorScheme == .light { + return isProtected + ? Color(hue: 0.38, saturation: 0.85, brightness: 0.40) + : Color(hue: 0.05, saturation: 0.90, brightness: 0.45) + } else { + return isProtected + ? Color(hue: 0.38, saturation: 0.70, brightness: 0.80) + : Color(hue: 0.06, saturation: 0.70, brightness: 0.85) + } + } + + private var buttonTint: Color { + if colorScheme == .light { + return isProtected + ? Color(hue: 0.05, saturation: 0.85, brightness: isHovering ? 0.60 : 0.50) + : Color(hue: 0.38, saturation: 0.85, brightness: isHovering ? 0.50 : 0.42) + } else { + return isProtected + ? Color(hue: 0.06, saturation: 0.65, brightness: isHovering ? 0.90 : 0.80) + : Color(hue: 0.38, saturation: 0.65, brightness: isHovering ? 0.90 : 0.80) + } + } + + var body: some View { + VStack(spacing: 12) { + ttlInfoCard + protocolBadges + actionButton + settingsCard + } + } + + private var ttlInfoCard: some View { + HStack(spacing: 20) { + ttlValueDisplay( + label: "CURRENT", + value: currentTTL, + highlight: isProtected + ) + + Image(systemName: "arrow.right") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.tertiary) + + ttlValueDisplay( + label: "TARGET", + value: targetTTL, + highlight: true + ) + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 16) + .fill(.ultraThinMaterial) + .overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(accentColor.opacity(0.15), lineWidth: 0.5) + } + } + .modifier(GlassCardModifier(tintColor: accentColor)) + } + + private func ttlValueDisplay(label: String, value: Int, highlight: Bool) -> some View { + VStack(spacing: 4) { + Text(label) + .font(.system(size: 10, weight: .semibold, design: .rounded)) + .tracking(2) + .foregroundStyle(.secondary) + + Text("\(value)") + .font(.system(size: 30, weight: .bold, design: .monospaced)) + .foregroundStyle(highlight ? accentColor : .primary) + .contentTransition(.numericText()) + .animation(.spring(duration: 0.5), value: value) + } + } + + // Protocol Badges + + private var protocolBadges: some View { + HStack(spacing: 8) { + badgeView(title: "IPv4 TTL", value: "\(currentTTL)", active: currentTTL == targetTTL) + badgeView(title: "IPv6 HLIM", value: "\(currentIPv6HopLimit)", active: currentIPv6HopLimit == targetTTL) + badgeView(title: "DNS FLUSH", value: "AUTO", active: isProtected) + } + } + + private func badgeView(title: String, value: String, active: Bool) -> some View { + HStack(spacing: 4) { + Circle() + .fill(active ? Color.green : Color.orange) + .frame(width: 5, height: 5) + + Text("\(title): \(value)") + .font(.system(size: 9, weight: .medium, design: .monospaced)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(.ultraThinMaterial) + .overlay( + Capsule().stroke(active ? Color.green.opacity(0.3) : Color.orange.opacity(0.2), lineWidth: 0.5) + ) + ) + } + + // Action Button + + private var actionButton: some View { + Button { + if isProtected { + onDeactivate() + } else { + onActivate() + } + } label: { + HStack(spacing: 10) { + if isProcessing { + ProgressView() + .controlSize(.small) + .tint(.white) + } else { + Image(systemName: isProtected ? "antenna.radiowaves.left.and.right.slash" : "antenna.radiowaves.left.and.right") + .font(.system(size: 16, weight: .semibold)) + .symbolEffect(.bounce, value: isHovering) + .contentTransition(.symbolEffect(.replace.magic(fallback: .replace))) + } + + Text(isProtected ? "Deactivate Hider" : "Activate Hider") + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .contentTransition(.numericText()) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + } + .modifier(GlassButtonModifier()) + .tint(buttonTint) + .scaleEffect(isHovering ? 1.025 : 1.0) + .shadow( + color: (isProtected + ? Color(hue: 0.06, saturation: 0.8, brightness: colorScheme == .light ? 0.5 : 0.9) + : Color(hue: 0.38, saturation: 0.8, brightness: colorScheme == .light ? 0.4 : 0.9) + ).opacity(isHovering ? (colorScheme == .light ? 0.35 : 0.45) : 0.0), + radius: isHovering ? 12 : 0, + x: 0, + y: isHovering ? 4 : 0 + ) + .onHover { hovering in + withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) { + isHovering = hovering + } + } + .disabled(isProcessing) + .animation(.easeInOut(duration: 0.3), value: isProtected) + .animation(.easeInOut(duration: 0.3), value: isProcessing) + } + + // Settings Card + + private var settingsCard: some View { + HStack { + Label("Start minimized in menu bar", systemImage: "macwindow.on.rectangle") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.secondary) + + Spacer() + + Toggle("", isOn: $startMinimized) + .toggleStyle(.switch) + .controlSize(.small) + .tint(accentColor) + } + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(accentColor.opacity(0.12), lineWidth: 0.5) + ) + ) + } +}