Update Apple client with changes from demoable build (#1809)

Brings in the changes from the Demoable build so I can start getting
feedback from users on.

---------

Co-authored-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
Jamil
2023-07-24 12:59:40 -07:00
committed by GitHub
parent 8f9330ac03
commit f968c8cefc
40 changed files with 500 additions and 131 deletions

View File

@@ -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)
}
}

View File

@@ -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 = "<group>"; };
6FA39A032A6A7248000F0157 /* NetworkResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResource.swift; sourceTree = "<group>"; };
6FE454EA2A5BFABA006549B1 /* Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = "<group>"; };
6FE455082A5D110D006549B1 /* CallbackHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallbackHandler.swift; path = Connlib/CallbackHandler.swift; sourceTree = "<group>"; };
6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Connlib/Generated/SwiftBridgeCore.swift; sourceTree = "<group>"; };
@@ -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;

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1324 2C18.5021 5.47053 13.9784 13.1622 15.5748 16.2679C12.2943 11.7793 16.7638 7.92182 14.1324 2Z" fill="#E6E6E6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5555 8.67994C19.4118 10.4496 15.9831 14.5961 17.8084 15.6575C19.6568 16.7325 19.4477 12.265 22 13.4111C19.3425 12.5489 20.4449 17.8678 17.1522 17.16C13.3839 16.3499 18.0702 10.6345 16.5555 8.67994Z" fill="#E6E6E6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 14.7397C5.43996 10.6068 12.8498 17.239 16.3465 17.6534C20.8434 18.1863 19.6455 13.2284 21.8963 13.4131C19.8223 13.6516 21.0672 18.8367 16.4304 18.9969C11.3806 19.1715 6.34256 11.83 0 14.7397Z" fill="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1324 2C18.5021 5.47053 13.9784 13.1622 15.5748 16.2679C12.2943 11.7793 16.7638 7.92182 14.1324 2Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5555 8.67994C19.4118 10.4496 15.9831 14.5961 17.8084 15.6575C19.6568 16.7325 19.4477 12.265 22 13.4111C19.3425 12.5489 20.4449 17.8678 17.1522 17.16C13.3839 16.3499 18.0702 10.6345 16.5555 8.67994Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 14.7397C5.43996 10.6068 12.8498 17.239 16.3465 17.6534C20.8434 18.1863 19.6455 13.2284 21.8963 13.4131C19.8223 13.6516 21.0672 18.8367 16.4304 18.9969C11.3806 19.1715 6.34256 11.83 0 14.7397Z" fill="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@@ -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
}
}

View File

@@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1324 2C18.5021 5.47053 13.9784 13.1622 15.5748 16.2679C12.2943 11.7793 16.7638 7.92182 14.1324 2Z" fill="#5C5C5C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5555 8.67994C19.4118 10.4496 15.9831 14.5961 17.8084 15.6575C19.6568 16.7325 19.4477 12.265 22 13.4111C19.3425 12.5489 20.4449 17.8678 17.1522 17.16C13.3839 16.3499 18.0702 10.6345 16.5555 8.67994Z" fill="#5C5C5C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 14.7397C5.43996 10.6068 12.8498 17.239 16.3465 17.6534C20.8434 18.1863 19.6455 13.2284 21.8963 13.4131C19.8223 13.6516 21.0672 18.8367 16.4304 18.9969C11.3806 19.1715 6.34256 11.83 0 14.7397Z" fill="#5C5C5C"/>
<line x1="3.35355" y1="3.64645" x2="19.3536" y2="19.6464" stroke="#E6E6E6"/>
</svg>

After

Width:  |  Height:  |  Size: 880 B

View File

@@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1324 2C18.5021 5.47053 13.9784 13.1622 15.5748 16.2679C12.2943 11.7793 16.7638 7.92182 14.1324 2Z" fill="#A9A9A9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5555 8.67994C19.4118 10.4496 15.9831 14.5961 17.8084 15.6575C19.6568 16.7325 19.4477 12.265 22 13.4111C19.3425 12.5489 20.4449 17.8678 17.1522 17.16C13.3839 16.3499 18.0702 10.6345 16.5555 8.67994Z" fill="#A9A9A9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 14.7397C5.43996 10.6068 12.8498 17.239 16.3465 17.6534C20.8434 18.1863 19.6455 13.2284 21.8963 13.4131C19.8223 13.6516 21.0672 18.8367 16.4304 18.9969C11.3806 19.1715 6.34256 11.83 0 14.7397Z" fill="#A9A9A9"/>
<line x1="3.33218" y1="3.6263" x2="21.3322" y2="19.6263" stroke="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 879 B

View File

@@ -2,14 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.multipath</key>
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>

View File

@@ -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
}
}

View File

@@ -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)!
}
}

View File

@@ -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<Void, Never>] = []
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)")

View File

@@ -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<AnyCancellable> = []
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

View File

@@ -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)")
}
}

View File

@@ -6,7 +6,5 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array/>
</dict>
</plist>

View File

@@ -8,11 +8,7 @@
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -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())
}
}
}