diff --git a/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift index 4ff9afdda..3d10f012b 100644 --- a/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift +++ b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift @@ -16,10 +16,13 @@ extension SwiftConnlibError: @unchecked Sendable {} extension SwiftConnlibError: Error {} public protocol CallbackHandlerDelegate: AnyObject { - func onConnect(tunnelAddressIPv4: String, tunnelAddressIPv6: String) + func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String) + func onTunnelReady() + func onAddRoute(_: String) + func onRemoveRoute(_: String) func onUpdateResources(resourceList: String) - func onDisconnect() - func onError(error: Error, isRecoverable: Bool) + func onDisconnect(error: Error) + func onError(error: Error) } public class CallbackHandler { @@ -28,22 +31,26 @@ public class CallbackHandler { func onSetInterfaceConfig(tunnelAddresses: TunnelAddresses, dnsAddress: RustString) { logger.debug("CallbackHandler.onSetInterfaceConfig: IPv4: \(tunnelAddresses.address4.toString(), privacy: .public), IPv6: \(tunnelAddresses.address6.toString(), privacy: .public), DNS: \(dnsAddress.toString(), privacy: .public)") - // Unimplemented + delegate?.onSetInterfaceConfig( + tunnelAddressIPv4: tunnelAddresses.address4.toString(), + tunnelAddressIPv6: tunnelAddresses.address6.toString(), + dnsAddress: dnsAddress.toString() + ) } func onTunnelReady() { logger.debug("CallbackHandler.onTunnelReady") - // Unimplemented + delegate?.onTunnelReady() } func onAddRoute(route: RustString) { logger.debug("CallbackHandler.onAddRoute: \(route.toString(), privacy: .public)") - // Unimplemented + delegate?.onAddRoute(route.toString()) } func onRemoveRoute(route: RustString) { logger.debug("CallbackHandler.onRemoveRoute: \(route.toString(), privacy: .public)") - // Unimplemented + delegate?.onRemoveRoute(route.toString()) } func onUpdateResources(resourceList: ResourceList) { @@ -54,11 +61,11 @@ public class CallbackHandler { func onDisconnect(error: SwiftConnlibError) { logger.debug("CallbackHandler.onDisconnect: \(error, privacy: .public)") // TODO: convert `error` to `Optional` by checking for `None` case - delegate?.onDisconnect() + delegate?.onDisconnect(error: error) } func onError(error: SwiftConnlibError) { logger.debug("CallbackHandler.onError: \(error, privacy: .public)") - delegate?.onError(error: error, isRecoverable: true) + delegate?.onError(error: error) } } diff --git a/swift/apple/Firezone.xcodeproj/project.pbxproj b/swift/apple/Firezone.xcodeproj/project.pbxproj index ea4a50f21..544d4b937 100644 --- a/swift/apple/Firezone.xcodeproj/project.pbxproj +++ b/swift/apple/Firezone.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; }; 05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; }; 6F68E44C2A588522003C7D08 /* AllConfigs.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 6F68E44B2A588522003C7D08 /* AllConfigs.xcconfig */; }; + 6FA39A042A6A7248000F0157 /* NetworkResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA39A032A6A7248000F0157 /* NetworkResource.swift */; }; + 6FA39A052A6A7248000F0157 /* NetworkResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA39A032A6A7248000F0157 /* NetworkResource.swift */; }; 6FE454F62A5BFB93006549B1 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE454EA2A5BFABA006549B1 /* Adapter.swift */; }; 6FE454F72A5BFB93006549B1 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE454EA2A5BFABA006549B1 /* Adapter.swift */; }; 6FE455092A5D110D006549B1 /* CallbackHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455082A5D110D006549B1 /* CallbackHandler.swift */; }; @@ -97,6 +99,7 @@ 05CF1D03290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FirezoneNetworkExtensionmacOS.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 6F68E44B2A588522003C7D08 /* AllConfigs.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = AllConfigs.xcconfig; path = xcconfig/AllConfigs.xcconfig; sourceTree = ""; }; + 6FA39A032A6A7248000F0157 /* NetworkResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResource.swift; sourceTree = ""; }; 6FE454EA2A5BFABA006549B1 /* Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = ""; }; 6FE455082A5D110D006549B1 /* CallbackHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallbackHandler.swift; path = Connlib/CallbackHandler.swift; sourceTree = ""; }; 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Connlib/Generated/SwiftBridgeCore.swift; sourceTree = ""; }; @@ -156,6 +159,7 @@ 05CF1CDE290B1A9000CF4755 /* FirezoneNetworkExtension_macOS.entitlements */, 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */, 6FE454EA2A5BFABA006549B1 /* Adapter.swift */, + 6FA39A032A6A7248000F0157 /* NetworkResource.swift */, 6FE455082A5D110D006549B1 /* CallbackHandler.swift */, 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */, 6FE4550E2A5D112C006549B1 /* connlib-apple.swift */, @@ -450,6 +454,7 @@ 6FE4550F2A5D112C006549B1 /* connlib-apple.swift in Sources */, 05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */, 6FE454F62A5BFB93006549B1 /* Adapter.swift in Sources */, + 6FA39A042A6A7248000F0157 /* NetworkResource.swift in Sources */, 6FE4550C2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -462,6 +467,7 @@ 6FE455102A5D112C006549B1 /* connlib-apple.swift in Sources */, 05CF1D16290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */, 6FE454F72A5BFB93006549B1 /* Adapter.swift in Sources */, + 6FA39A052A6A7248000F0157 /* NetworkResource.swift in Sources */, 6FE4550D2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json index 4e7323fa2..375f89ffb 100644 --- a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,67 +1,67 @@ { "images" : [ { - "filename" : "logo-1024.png", + "filename" : "appicon-ios-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { - "filename" : "logo-16.png", + "filename" : "appicon-mac-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { - "filename" : "logo-32.png", + "filename" : "appicon-mac-16-2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { - "filename" : "logo-32 1.png", + "filename" : "appicon-mac-32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { - "filename" : "logo-64.png", + "filename" : "appicon-mac-32-2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { - "filename" : "logo-128.png", + "filename" : "appicon-mac-128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { - "filename" : "logo-256.png", + "filename" : "appicon-mac-128-2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { - "filename" : "logo-256 1.png", + "filename" : "appicon-mac-256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { - "filename" : "logo-512.png", + "filename" : "appicon-mac-256-2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { - "filename" : "logo-512 1.png", + "filename" : "appicon-mac-512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { - "filename" : "logo-1024 1.png", + "filename" : "appicon-mac-512-2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-1024 1.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-ios-1024.png similarity index 100% rename from swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-1024 1.png rename to swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-ios-1024.png diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128-2x.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128-2x.png new file mode 100644 index 000000000..b3beb6a32 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128-2x.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128.png new file mode 100644 index 000000000..866c74472 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-128.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16-2x.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16-2x.png new file mode 100644 index 000000000..95680eee5 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16-2x.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16.png new file mode 100644 index 000000000..4dadc7ec9 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-16.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256-2x.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256-2x.png new file mode 100644 index 000000000..e06250499 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256-2x.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256.png new file mode 100644 index 000000000..157cc8ec4 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-256.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32-2x.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32-2x.png new file mode 100644 index 000000000..4fc6da72e Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32-2x.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32.png new file mode 100644 index 000000000..70b0ce692 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-32.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512-2x.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512-2x.png new file mode 100644 index 000000000..e69907646 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512-2x.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512.png new file mode 100644 index 000000000..3cef30a09 Binary files /dev/null and b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/appicon-mac-512.png differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-1024.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-1024.png deleted file mode 100644 index 692b2a845..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-1024.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-128.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-128.png deleted file mode 100644 index 0b9819139..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-128.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-16.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-16.png deleted file mode 100644 index 711e8b464..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-16.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256 1.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256 1.png deleted file mode 100644 index 9acc8eb69..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256 1.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256.png deleted file mode 100644 index 9acc8eb69..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-256.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32 1.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32 1.png deleted file mode 100644 index 51da6224b..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32 1.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32.png deleted file mode 100644 index 51da6224b..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-32.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512 1.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512 1.png deleted file mode 100644 index 836392b17..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512 1.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512.png deleted file mode 100644 index 836392b17..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-512.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-64.png b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-64.png deleted file mode 100644 index fabe502e2..000000000 Binary files a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/logo-64.png and /dev/null differ diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/Contents.json b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/Contents.json new file mode 100644 index 000000000..ffbc8757f --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "logo-main-connected-light.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-main-connected-dark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-dark.svg b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-dark.svg new file mode 100644 index 000000000..74a3c8e21 --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-light.svg b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-light.svg new file mode 100644 index 000000000..e1667b168 --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconConnected.imageset/logo-main-connected-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/Contents.json b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/Contents.json new file mode 100644 index 000000000..3d715462c --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "logo-main-disconnected-light.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-main-disconnected-dark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-dark.svg b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-dark.svg new file mode 100644 index 000000000..32d077f7c --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-light.svg b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-light.svg new file mode 100644 index 000000000..51a59d2c7 --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/MenuBarIconDisconnected.imageset/logo-main-disconnected-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/swift/apple/Firezone/Firezone.entitlements b/swift/apple/Firezone/Firezone.entitlements index 441279cd4..606375b71 100644 --- a/swift/apple/Firezone/Firezone.entitlements +++ b/swift/apple/Firezone/Firezone.entitlements @@ -2,14 +2,14 @@ + com.apple.developer.networking.multipath + com.apple.developer.networking.networkextension packet-tunnel-provider com.apple.security.app-sandbox - com.apple.security.files.user-selected.read-only - com.apple.security.network.client diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift index 862a4098b..2fa4f8d49 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift @@ -23,7 +23,7 @@ public final class SettingsViewModel: ObservableObject { } } - func saveButtonTapped() { + func save() { settingsClient.saveSettings(settings) onSettingsSaved() } @@ -31,6 +31,7 @@ public final class SettingsViewModel: ObservableObject { public struct SettingsView: View { @ObservedObject var model: SettingsViewModel + @Environment(\.dismiss) var dismiss public init(model: SettingsViewModel) { self.model = model @@ -76,12 +77,19 @@ public struct SettingsView: View { .navigationTitle("Settings") .toolbar { ToolbarItem(placement: .primaryAction) { - Button("Save") { - model.saveButtonTapped() + #if os(macOS) + Button("Done") { + self.doneButtonTapped() } + #endif } } } + + func doneButtonTapped() { + model.save() + dismiss() + } } struct FormTextField: View { @@ -104,10 +112,15 @@ struct FormTextField: View { .keyboardType(.URL) } #else - TextField(title, text: text, prompt: Text(placeholder)) - .autocorrectionDisabled() - .multilineTextAlignment(.trailing) - .foregroundColor(.secondary) + HStack(spacing: 30) { + Spacer() + TextField(title, text: text, prompt: Text(placeholder)) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundColor(.secondary) + .frame(maxWidth: 360) + Spacer() + } #endif } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/DisplayableResources.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/DisplayableResources.swift new file mode 100644 index 000000000..8af425fc3 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/DisplayableResources.swift @@ -0,0 +1,65 @@ +// +// DisplayableResources.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +// This models resources that are displayed in the UI + +import Foundation + +public class DisplayableResources { + public typealias Resource = (name: String, location: String) + public private(set) var version: UInt64 + public private(set) var versionString: String + public private(set) var orderedResources: [Resource] + + public init(version: UInt64, resources: [Resource]) { + self.version = version + self.versionString = "\(version)" + self.orderedResources = resources.sorted { $0.name < $1.name } + } + + public convenience init() { + self.init(version: 0, resources: []) + } + + public func update(resources: [Resource]) { + self.version = self.version &+ 1 // Overflow is ok + self.versionString = "\(version)" + self.orderedResources = resources.sorted { $0.name < $1.name } + } +} + +extension DisplayableResources { + public func toData() -> Data? { + ( + "\(versionString)," + + (orderedResources.flatMap { [$0.name, $0.location] }) + .map { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) }.compactMap { $0 } + .joined(separator: ",") + ).data(using: .utf8) + } + + public convenience init?(from data: Data) { + guard let components = String(data: data, encoding: .utf8)?.split(separator: ",") else { + return nil + } + guard let versionString = components.first, let version = UInt64(versionString) else { + return nil + } + var resources: [Resource] = [] + for index in stride(from: 2, to: components.count, by: 2) { + guard let name = components[index - 1].removingPercentEncoding, + let location = components[index].removingPercentEncoding else { + continue + } + resources.append((name: name, location: location)) + } + self.init(version: version, resources: resources) + } + + public func versionStringToData() -> Data { + versionString.data(using: .utf8)! + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift index 7b89a742a..0a7a717e3 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift @@ -11,7 +11,6 @@ import OSLog // TODO: Can this file be removed since we're managing the tunnel in connlib? -@MainActor final class TunnelStore: ObservableObject { private static let logger = Logger.make(for: TunnelStore.self) @@ -27,6 +26,12 @@ final class TunnelStore: ObservableObject { didSet { TunnelStore.logger.info("isEnabled changed: \(self.isEnabled.description)") } } + @Published private(set) var resources = DisplayableResources() + + private var resourcesTimer: Timer? { + didSet(oldValue) { oldValue?.invalidate() } + } + private var tunnelObservingTasks: [Task] = [] init(tunnel: NETunnelProviderManager) { @@ -71,6 +76,37 @@ final class TunnelStore: ObservableObject { session.stopTunnel() } + func beginUpdatingResources() { + self.updateResources() + let timer = Timer(timeInterval: 1 /*second*/, repeats: true) { [weak self] _ in + guard let self = self else { return } + guard self.status == .connected else { return } + self.updateResources() + } + RunLoop.main.add(timer, forMode: .common) + self.resourcesTimer = timer + } + + func endUpdatingResources() { + self.resourcesTimer = nil + } + + private func updateResources() { + let session = tunnel.connection as! NETunnelProviderSession + let resourcesQuery = resources.versionStringToData() + do { + try session.sendProviderMessage(resourcesQuery) { [weak self] reply in + if let reply = reply { // If reply is nil, then the resources have not changed + if let updatedResources = DisplayableResources(from: reply) { + self?.resources = updatedResources + } + } + } + } catch { + TunnelStore.logger.error("Error: sendProviderMessage: \(error)") + } + } + private static func makeManager() -> NETunnelProviderManager { logger.trace("\(#function)") diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index 7ab895bab..a98569fa1 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -13,7 +13,7 @@ import SwiftUI @MainActor - public final class MenuBar { +public final class MenuBar: NSObject { let logger = Logger.make(for: MenuBar.self) @Dependency(\.mainQueue) private var mainQueue @@ -25,6 +25,12 @@ private var cancellables: Set = [] private var statusItem: NSStatusItem + private var orderedResources: [DisplayableResources.Resource] = [] + private var isMenuVisible = false { + didSet { handleMenuVisibilityOrStatusChanged() } + } + private lazy var disconnectedIcon = NSImage(named: "MenuBarIconDisconnected") + private lazy var connectedIcon = NSImage(named: "MenuBarIconConnected") let settingsViewModel: SettingsViewModel @@ -37,16 +43,13 @@ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - if let button = statusItem.button { - button.image = NSImage( - // TODO: Replace with AppIcon when it exists - systemSymbolName: "circle", - accessibilityDescription: "Firezone icon" - ) - } - + super.init() createMenu() + if let button = statusItem.button { + button.image = disconnectedIcon + } + Task { let tunnel = try await TunnelStore.loadOrCreate() self.appStore = AppStore(tunnelStore: TunnelStore(tunnel: tunnel)) @@ -70,9 +73,23 @@ .sink { [weak self] status in if status == .connected { self?.connectionMenuItem.title = "Disconnect" + self?.statusItem.button?.image = self?.connectedIcon } else { self?.connectionMenuItem.title = "Connect" + self?.statusItem.button?.image = self?.disconnectedIcon } + self?.handleMenuVisibilityOrStatusChanged() + if status != .connected { + self?.setOrderedResources([]) + } + } + .store(in: &cancellables) + + appStore?.tunnel.$resources + .receive(on: mainQueue) + .sink { [weak self] resources in + guard let self = self else { return } + self.setOrderedResources(resources.orderedResources) } .store(in: &cancellables) } @@ -83,6 +100,7 @@ menu, title: "Connect", action: #selector(connectButtonTapped), + isHidden: true, target: self ) @@ -99,35 +117,62 @@ isHidden: true, target: self ) + private lazy var resourcesTitleMenuItem = createMenuItem( + menu, + title: "No Resources", + action: nil, + isHidden: false, + target: self + ) + private lazy var resourcesSeparatorMenuItem = NSMenuItem.separator() + private lazy var aboutMenuItem = createMenuItem( + menu, + title: "About", + action: #selector(aboutButtonTapped), + target: self + ) private lazy var settingsMenuItem = createMenuItem( menu, title: "Settings", action: #selector(settingsButtonTapped), target: self ) - private lazy var quitMenuItem = createMenuItem( - menu, - title: "Quit", - action: #selector(NSApplication.terminate(_:)), - key: "q", - target: nil - ) + private lazy var quitMenuItem: NSMenuItem = { + let menuItem = createMenuItem( + menu, + title: "Quit", + action: #selector(NSApplication.terminate(_:)), + key: "q", + target: nil + ) + if let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String { + menuItem.title = "Quit \(appName)" + } + return menuItem + }() private func createMenu() { menu.addItem(connectionMenuItem) menu.addItem(loginMenuItem) menu.addItem(logoutMenuItem) menu.addItem(NSMenuItem.separator()) + + menu.addItem(resourcesTitleMenuItem) + menu.addItem(resourcesSeparatorMenuItem) + + menu.addItem(aboutMenuItem) menu.addItem(settingsMenuItem) menu.addItem(quitMenuItem) + menu.delegate = self + statusItem.menu = menu } private func createMenuItem( _: NSMenu, title: String, - action: Selector, + action: Selector?, isHidden: Bool = false, key: String = "", target: AnyObject? @@ -136,6 +181,7 @@ item.isHidden = isHidden item.target = target + item.isEnabled = (action != nil) return item } @@ -148,7 +194,6 @@ } loginMenuItem.target = nil logoutMenuItem.isHidden = false - connectionMenuItem.isHidden = false } private func showLoggedOut() { @@ -156,7 +201,6 @@ loginMenuItem.target = self logoutMenuItem.isHidden = true - connectionMenuItem.isHidden = true } @objc private func connectButtonTapped() { @@ -195,8 +239,81 @@ openSettingsWindow() } + @objc private func aboutButtonTapped() { + NSApp.activate(ignoringOtherApps: true) + NSApp.orderFrontStandardAboutPanel(self) + } + private func openSettingsWindow() { NSWorkspace.shared.open(URL(string: "firezone://settings")!) } + + private func handleMenuVisibilityOrStatusChanged() { + guard let appStore = appStore else { return } + let status = appStore.tunnel.status + if isMenuVisible && status == .connected { + appStore.tunnel.beginUpdatingResources() + } else { + appStore.tunnel.endUpdatingResources() + } + resourcesTitleMenuItem.isHidden = (status != .connected) + resourcesSeparatorMenuItem.isHidden = (status != .connected) + } + + private func setOrderedResources(_ newOrderedResources: [DisplayableResources.Resource]) { + let diff = newOrderedResources.difference( + from: self.orderedResources, + by: { $0.name == $1.name && $0.location == $1.location } + ) + let baseIndex = menu.index(of: resourcesTitleMenuItem) + 1 + for change in diff { + switch change { + case .insert(offset: let offset, element: let element, associatedWith: _): + let menuItem = createResourceMenuItem(title: element.name, submenuTitle: element.location) + menu.insertItem(menuItem, at: baseIndex + offset) + orderedResources.insert(element, at: offset) + case .remove(offset: let offset, element: _, associatedWith: _): + menu.removeItem(at: baseIndex + offset) + orderedResources.remove(at: offset) + } + } + resourcesTitleMenuItem.title = orderedResources.isEmpty ? "No Resources" : "Resources" + } + + private func createResourceMenuItem(title: String, submenuTitle: String) -> NSMenuItem { + let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") + + let subMenu = NSMenu() + let subMenuItem = NSMenuItem(title: submenuTitle, action: #selector(resourceValueTapped(_:)), keyEquivalent: "") + subMenuItem.isEnabled = true + subMenuItem.target = self + subMenu.addItem(subMenuItem) + + item.isHidden = false + item.submenu = subMenu + + return item + } + + @objc private func resourceValueTapped(_ sender: AnyObject?) { + if let value = (sender as? NSMenuItem)?.title { + copyToClipboard(value) + } + } + + private func copyToClipboard(_ string: String) { + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.writeObjects([string as NSString]) + } + } + + extension MenuBar: NSMenuDelegate { + public func menuNeedsUpdate(_ menu: NSMenu) { + isMenuVisible = true + } + public func menuDidClose(_ menu: NSMenu) { + isMenuVisible = false + } } #endif diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index 6adb88d6d..6c0ee8ca4 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -5,6 +5,7 @@ // import Foundation import NetworkExtension +import FirezoneKit import os.log public enum AdapterError: Error { @@ -31,11 +32,7 @@ private enum State { public class Adapter { private let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") - // Maintain a handle to the currently instantiated tunnel adapter 🤮 - public static var currentAdapter: Adapter? - - // Maintain a reference to the initialized callback handler - public static var callbackHandler: CallbackHandler? + private var callbackHandler: CallbackHandler // Latest applied NETunnelProviderNetworkSettings public var lastNetworkSettings: NEPacketTunnelNetworkSettings? @@ -52,19 +49,16 @@ public class Adapter { /// Adapter state. private var state: State = .stopped + /// Keep track of resources + private var displayableResources = DisplayableResources() + public init(with packetTunnelProvider: NEPacketTunnelProvider) { self.packetTunnelProvider = packetTunnelProvider - - // There must be a better way than making this a static class var... - Self.currentAdapter = self - Self.callbackHandler = CallbackHandler() - Self.callbackHandler?.delegate = self + self.callbackHandler = CallbackHandler() + self.callbackHandler.delegate = self } deinit { - // Remove static var reference - Self.currentAdapter = nil - // Cancel network monitor networkMonitor?.cancel() @@ -94,7 +88,7 @@ public class Adapter { do { try self.setNetworkSettings(self.generateNetworkSettings(ipv4Routes: [], ipv6Routes: [])) self.state = .started( - try WrappedSession.connect("http://localhost:4568", "test-token", Self.callbackHandler!) + try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler) ) self.networkMonitor = networkMonitor completionHandler(nil) @@ -129,6 +123,17 @@ public class Adapter { } } + public func getDisplayableResourcesIfVersionDifferentFrom( + referenceVersionString: String, completionHandler: @escaping (DisplayableResources?) -> Void) { + workQueue.async { + if referenceVersionString == self.displayableResources.versionString { + completionHandler(nil) + } else { + completionHandler(self.displayableResources) + } + } + } + public func generateNetworkSettings( addresses4: [String] = ["100.100.111.2"], addresses6: [String] = ["fd00:0222:2011:1111::2"], ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] @@ -254,14 +259,16 @@ public class Adapter { private func didReceivePathUpdate(path: Network.NWPath) { #if os(macOS) if case .started(let wrappedSession) = self.state { - wrappedSession.bumpSockets() + self.logger.log(level: .debug, "Suppressing call to bumpSockets()") + // wrappedSession.bumpSockets() } #elseif os(iOS) switch self.state { case .started(let wrappedSession): if path.status == .satisfied { - wrappedSession.disableSomeRoamingForBrokenMobileSemantics() - wrappedSession.bumpSockets() + self.logger.log(level: .debug, "Suppressing calls to disableSomeRoamingForBrokenMobileSemantics() and bumpSockets()") + // wrappedSession.disableSomeRoamingForBrokenMobileSemantics() + // wrappedSession.bumpSockets() } else { //self.logger.log(.debug, "Connectivity offline, pausing backend.") self.state = .temporaryShutdown @@ -277,7 +284,7 @@ public class Adapter { try self.setNetworkSettings(self.lastNetworkSettings!) self.state = .started( - try WrappedSession.connect("http://localhost:4568", "test-token", Self.callbackHandler!) + try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler) ) } catch { self.logger.log(level: .debug, "Failed to restart backend: \(error.localizedDescription)") @@ -294,72 +301,41 @@ public class Adapter { } extension Adapter: CallbackHandlerDelegate { - public func onConnect(tunnelAddressIPv4: String, tunnelAddressIPv6: String) { - let addresses4 = [tunnelAddressIPv4] - let addresses6 = [tunnelAddressIPv6] - let ipv4Routes = - Adapter.currentAdapter?.lastNetworkSettings?.ipv4Settings?.includedRoutes ?? [] - let ipv6Routes = - Adapter.currentAdapter?.lastNetworkSettings?.ipv6Settings?.includedRoutes ?? [] - - _ = setTunnelSettingsKeepingSomeExisting( - addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes - ) - } - - public func onUpdateResources(resourceList: String) { - let addresses4 = - self.lastNetworkSettings?.ipv4Settings?.addresses ?? ["100.100.111.2"] - let addresses6 = - self.lastNetworkSettings?.ipv6Settings?.addresses ?? [ - "fd00:0222:2021:1111::2" - ] - - // TODO: Use actual passed in resources to achieve split tunnel - let ipv4Routes = [NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0")] - let ipv6Routes = [ - NEIPv6Route(destinationAddress: "fd00:0222:2021:1111::0", networkPrefixLength: 64) - ] - - _ = setTunnelSettingsKeepingSomeExisting( - addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes - ) - } - - public func onDisconnect() { + public func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String) { // Unimplemented } - public func onError(error: Error, isRecoverable: Bool) { - let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") - logger.log(level: .error, "Internal connlib error: \(String(describing: error), privacy: .public)") + public func onTunnelReady() { + // Unimplemented } - private func setTunnelSettingsKeepingSomeExisting( - addresses4: [String], addresses6: [String], ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] - ) -> Bool { - let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") + public func onAddRoute(_: String) { + // Unimplemented + } - do { - /* If the tunnel interface addresses are being updated, it's impossible for the tunnel to - stay up due to the way WireGuard works. Still, we try not to change the tunnel's routes - here Just In Case™. - */ - try self.setNetworkSettings( - self.generateNetworkSettings( - addresses4: addresses4, - addresses6: addresses6, - ipv4Routes: ipv4Routes, - ipv6Routes: ipv6Routes - ) - ) + public func onRemoveRoute(_: String) { + // Unimplemented + } - return true - } catch let error { - logger.log(level: .debug, "Error setting adapter settings: \(String(describing: error))") - - return false + public func onUpdateResources(resourceList: String) { + workQueue.async { + let jsonString = "[\(resourceList)]" + guard let jsonData = jsonString.data(using: .utf8) else { + return + } + guard let networkResources = try? JSONDecoder().decode([NetworkResource].self, from: jsonData) else { + return + } + self.displayableResources.update(resources: networkResources.map { $0.displayableResource }) } } + public func onDisconnect(error: Error) { + // Unimplemented + } + + public func onError(error: Error) { + let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") + logger.log(level: .error, "Internal connlib error: \(String(describing: error), privacy: .public)") + } } diff --git a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements index b63ed3cab..ffab33e01 100644 --- a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements +++ b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements @@ -6,7 +6,5 @@ packet-tunnel-provider - com.apple.security.application-groups - diff --git a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements index 9712ea7ce..4e920afc0 100644 --- a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements +++ b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements @@ -8,11 +8,7 @@ com.apple.security.app-sandbox - com.apple.security.application-groups - com.apple.security.network.client - com.apple.security.network.server - diff --git a/swift/apple/FirezoneNetworkExtension/NetworkResource.swift b/swift/apple/FirezoneNetworkExtension/NetworkResource.swift new file mode 100644 index 000000000..eafe5028e --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/NetworkResource.swift @@ -0,0 +1,79 @@ +// +// NetworkResource.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +public struct NetworkResource: Decodable { + enum ResourceLocation { + case dns(domain: String, ipv4: String, ipv6: String) + case cidr(cidrAddress: String) + + func toString() -> String { + switch self { + case .dns(let domain, ipv4: _, ipv6: _): return domain + case .cidr(let cidrAddress): return cidrAddress + } + } + } + + let name: String + let resourceLocation: ResourceLocation + + var displayableResource: (name: String, location: String) { + (name: name, location: resourceLocation.toString()) + } +} + +// A DNS resource example: +// { +// "type": "dns", +// "address": "app.posthog.com", +// "name": "PostHog", +// "ipv4": "100.64.0.1", +// "ipv6": "fd00:2021:11111::1" +// } +// +// A CIDR resource example: +// { +// "type": "cidr", +// "address": "10.0.0.0/24", +// "name": "AWS SJC VPC1", +// } + +extension NetworkResource { + enum ResourceKeys: String, CodingKey { + case type + case address + case name + case ipv4 + case ipv6 + } + + enum DecodeError: Error { + case invalidType(String) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceKeys.self) + let name = try container.decode(String.self, forKey: .name) + let type = try container.decode(String.self, forKey: .type) + let resourceLocation: ResourceLocation = try { + switch type { + case "dns": + let domain = try container.decode(String.self, forKey: .address) + let ipv4 = try container.decode(String.self, forKey: .ipv4) + let ipv6 = try container.decode(String.self, forKey: .ipv6) + return .dns(domain: domain, ipv4: ipv4, ipv6: ipv6) + case "cidr": + let address = try container.decode(String.self, forKey: .address) + return .cidr(cidrAddress: address) + default: + throw DecodeError.invalidType(type) + } + }() + self.init(name: name, resourceLocation: resourceLocation) + } +} diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index 8e7946315..2cef17484 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -69,4 +69,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { exit(0) #endif } + + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + let query = String(data: messageData, encoding: .utf8) ?? "" + adapter.getDisplayableResourcesIfVersionDifferentFrom(referenceVersionString: query) { displayableResources in + completionHandler?(displayableResources?.toData()) + } + } }