apple: Advanced settings and settings ux changes (#2540)

This is a follow-up to #2477 -- I requested the apple changes to be not
included in that PR.

This PR has:
  - A new tabbed UI for settings
- Advanced settings for the dataplane configuration mentioned in #2477
This commit is contained in:
Roopesh Chander
2023-11-02 20:04:24 +05:30
committed by GitHub
parent 8763be7e5c
commit a9f262c3ee
6 changed files with 612 additions and 205 deletions

View File

@@ -18,47 +18,85 @@ enum SettingsViewError: Error {
public final class SettingsViewModel: ObservableObject {
@Dependency(\.authStore) private var authStore
@Published var settings: Settings
@Published var accountSettings: AccountSettings
@Published var advancedSettings: AdvancedSettings
public var onSettingsSaved: () -> Void = unimplemented()
private var cancellables = Set<AnyCancellable>()
public init() {
settings = Settings()
load()
accountSettings = AccountSettings()
advancedSettings = AdvancedSettings.defaultValue
loadAccountSettings()
loadAdvancedSettings()
}
func load() {
func loadAccountSettings() {
Task {
authStore.tunnelStore.$tunnelAuthStatus
.filter { $0.isInitialized }
.receive(on: RunLoop.main)
.sink { [weak self] tunnelAuthStatus in
guard let self = self else { return }
self.settings = Settings(accountId: tunnelAuthStatus.accountId() ?? "")
self.accountSettings = AccountSettings(accountId: tunnelAuthStatus.accountId() ?? "")
}
.store(in: &cancellables)
}
}
func save() {
func saveAccountSettings() {
Task {
let accountId = await authStore.loginStatus.accountId
if accountId == settings.accountId {
if accountId == accountSettings.accountId {
// Not changed
await MainActor.run {
accountSettings.isSavedToDisk = true
}
return
}
let tunnelAuthStatus: TunnelAuthStatus = await {
if settings.accountId.isEmpty {
return .accountNotSetup
} else {
return await authStore.tunnelAuthStatusForAccount(accountId: settings.accountId)
}
}()
try await authStore.tunnelStore.setAuthStatus(tunnelAuthStatus)
onSettingsSaved()
try await updateTunnelAuthStatus(accountId: accountSettings.accountId)
await MainActor.run {
accountSettings.isSavedToDisk = true
}
}
}
func loadAdvancedSettings() {
advancedSettings = authStore.tunnelStore.advancedSettings() ?? AdvancedSettings.defaultValue
}
func saveAdvancedSettings() {
Task {
guard let authBaseURL = URL(string: advancedSettings.authBaseURLString) else {
fatalError("Saved authBaseURL is invalid")
}
try await authStore.tunnelStore.saveAdvancedSettings(advancedSettings)
await MainActor.run {
advancedSettings.isSavedToDisk = true
}
var isChanged = false
await authStore.setAuthBaseURL(authBaseURL, isChanged: &isChanged)
if isChanged {
try await updateTunnelAuthStatus(
accountId: authStore.tunnelStore.tunnelAuthStatus.accountId() ?? "")
}
}
}
// updateTunnelAuthStatus:
// When the authBaseURL or the accountId changes, we should update the signed-in-ness.
// This is done by searching the keychain for an entry with the authBaseURL+accountId
// combination. If an entry was found, we consider that entry to mean we're logged in.
func updateTunnelAuthStatus(accountId: String) async throws {
let tunnelAuthStatus: TunnelAuthStatus = await {
if accountId.isEmpty {
return .accountNotSetup
} else {
return await authStore.tunnelAuthStatusForAccount(accountId: accountId)
}
}()
try await authStore.tunnelStore.saveAuthStatus(tunnelAuthStatus)
}
}
public struct SettingsView: View {
@@ -67,7 +105,6 @@ public struct SettingsView: View {
@ObservedObject var model: SettingsViewModel
@Environment(\.dismiss) var dismiss
let teamIdAllowedCharacterSet: CharacterSet
@State private var isExportingLogs = false
#if os(iOS)
@@ -75,127 +112,337 @@ public struct SettingsView: View {
@State private var isPresentingExportLogShareSheet = false
#endif
struct PlaceholderText {
static let accountId = "account-id"
static let authBaseURL = "Admin portal base URL"
static let apiURL = "Control plane WebSocket URL"
static let logFilter = "RUST_LOG-style filter string"
}
struct FootnoteText {
static let forAccount = "Your account ID is provided by your admin"
static let forAdvanced = try! AttributedString(
markdown: """
**WARNING:** These settings are intended for internal debug purposes **only**. \
Changing these is not supported and will disrupt access to your Firezone resources.
""")
}
public init(model: SettingsViewModel) {
self.model = model
self.teamIdAllowedCharacterSet = {
var pathAllowed = CharacterSet.urlPathAllowed
pathAllowed.remove("/")
return pathAllowed
}()
}
public var body: some View {
#if os(iOS)
ios
NavigationView {
TabView {
accountTab
.tabItem {
Image(systemName: "person.3.fill")
Text("Account")
}
.badge(model.accountSettings.isValid ? nil : "!")
advancedTab
.tabItem {
Image(systemName: "slider.horizontal.3")
Text("Advanced")
}
.badge(model.advancedSettings.isValid ? nil : "!")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
self.saveSettings()
}
.disabled(
(model.accountSettings.isSavedToDisk && model.advancedSettings.isSavedToDisk)
|| !model.accountSettings.isValid
|| !model.advancedSettings.isValid
)
}
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
self.loadSettings()
}
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
}
#elseif os(macOS)
mac
VStack {
TabView {
accountTab
.tabItem {
Text("Account")
}
advancedTab
.tabItem {
Text("Advanced")
}
}
.padding(20)
}
.onDisappear(perform: { self.loadSettings() })
#else
#error("Unsupported platform")
#endif
}
#if os(iOS)
private var ios: some View {
NavigationView {
VStack(spacing: 10) {
form
ExportLogsButton(isProcessing: $isExportingLogs) {
self.isExportingLogs = true
Task {
self.logTempZipFileURL = try await createLogZipBundle()
self.isPresentingExportLogShareSheet = true
private var accountTab: some View {
#if os(macOS)
VStack {
Spacer()
Form {
Section(
content: {
HStack(spacing: 15) {
Spacer()
Text("Account ID:")
TextField(
"",
text: Binding(
get: { model.accountSettings.accountId },
set: { model.accountSettings.accountId = $0 }
),
prompt: Text(PlaceholderText.accountId)
)
.frame(maxWidth: 240)
.onSubmit {
self.model.saveAccountSettings()
}
Spacer()
}
},
footer: {
Text(FootnoteText.forAccount)
.foregroundStyle(.secondary)
}
)
Button(
"Apply",
action: {
self.model.saveAccountSettings()
}
)
.disabled(
model.accountSettings.isSavedToDisk
|| !model.accountSettings.isValid
)
.padding(.top, 5)
}
Spacer()
}
#elseif os(iOS)
VStack {
Form {
Section(
content: {
HStack(spacing: 15) {
Text("Account ID")
.foregroundStyle(.secondary)
TextField(
PlaceholderText.accountId,
text: Binding(
get: { model.accountSettings.accountId },
set: { model.accountSettings.accountId = $0 }
)
)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
}
},
header: { Text("Account") },
footer: { Text(FootnoteText.forAccount) }
)
}
}
#else
#error("Unsupported platform")
#endif
}
private var advancedTab: some View {
#if os(macOS)
VStack {
Spacer()
HStack {
Spacer()
Form {
TextField(
"Auth Base URL:",
text: Binding(
get: { model.advancedSettings.authBaseURLString },
set: { model.advancedSettings.authBaseURLString = $0 }
),
prompt: Text(PlaceholderText.authBaseURL)
)
TextField(
"API URL:",
text: Binding(
get: { model.advancedSettings.apiURLString },
set: { model.advancedSettings.apiURLString = $0 }
),
prompt: Text(PlaceholderText.apiURL)
)
TextField(
"Log Filter:",
text: Binding(
get: { model.advancedSettings.connlibLogFilterString },
set: { model.advancedSettings.connlibLogFilterString = $0 }
),
prompt: Text(PlaceholderText.logFilter)
)
Text(FootnoteText.forAdvanced)
.foregroundStyle(.secondary)
HStack(spacing: 30) {
Button(
"Apply",
action: {
self.model.saveAdvancedSettings()
}
)
.disabled(model.advancedSettings.isSavedToDisk || !model.advancedSettings.isValid)
Button(
"Reset to Defaults",
action: {
self.restoreAdvancedSettingsToDefaults()
}
)
.disabled(model.advancedSettings == AdvancedSettings.defaultValue)
}
.padding(.top, 5)
}
.sheet(isPresented: $isPresentingExportLogShareSheet) {
if let logfileURL = self.logTempZipFileURL {
ShareSheetView(
localFileURL: logfileURL,
completionHandler: {
self.isPresentingExportLogShareSheet = false
self.isExportingLogs = false
self.logTempZipFileURL = nil
})
}
.padding(10)
Spacer()
}
Spacer()
HStack {
Spacer()
ExportLogsButton(isProcessing: $isExportingLogs) {
self.exportLogsWithSavePanelOnMac()
}
Spacer()
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
self.saveButtonTapped()
}
.disabled(!isTeamIdValid(model.settings.accountId))
}
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
self.cancelButtonTapped()
Spacer()
}
#elseif os(iOS)
VStack {
Form {
Section(
content: {
HStack(spacing: 15) {
Text("Auth Base URL")
.foregroundStyle(.secondary)
TextField(
PlaceholderText.authBaseURL,
text: Binding(
get: { model.advancedSettings.authBaseURLString },
set: { model.advancedSettings.authBaseURLString = $0 }
)
)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
}
HStack(spacing: 15) {
Text("API URL")
.foregroundStyle(.secondary)
TextField(
PlaceholderText.apiURL,
text: Binding(
get: { model.advancedSettings.apiURLString },
set: { model.advancedSettings.apiURLString = $0 }
)
)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
}
HStack(spacing: 15) {
Text("Log Filter")
.foregroundStyle(.secondary)
TextField(
PlaceholderText.logFilter,
text: Binding(
get: { model.advancedSettings.connlibLogFilterString },
set: { model.advancedSettings.connlibLogFilterString = $0 }
)
)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
}
HStack {
Spacer()
Button(
"Reset to Defaults",
action: {
self.restoreAdvancedSettingsToDefaults()
}
)
.disabled(model.advancedSettings == AdvancedSettings.defaultValue)
Spacer()
}
},
header: { Text("Advanced Settings") },
footer: { Text(FootnoteText.forAdvanced) }
)
Section(header: Text("Logs")) {
HStack {
Spacer()
ExportLogsButton(isProcessing: $isExportingLogs) {
self.isExportingLogs = true
Task {
self.logTempZipFileURL = try await createLogZipBundle()
self.isPresentingExportLogShareSheet = true
}
}.sheet(isPresented: $isPresentingExportLogShareSheet) {
if let logfileURL = self.logTempZipFileURL {
ShareSheetView(
localFileURL: logfileURL,
completionHandler: {
self.isPresentingExportLogShareSheet = false
self.isExportingLogs = false
self.logTempZipFileURL = nil
})
}
}
Spacer()
}
}
}
}
}
#endif
#if os(macOS)
private var mac: some View {
VStack(spacing: 50) {
form
HStack(spacing: 30) {
Button(
"Cancel",
action: {
self.cancelButtonTapped()
})
Button(
"Save",
action: {
self.saveButtonTapped()
}
)
.disabled(!isTeamIdValid(model.settings.accountId))
}
ExportLogsButton(isProcessing: $isExportingLogs) {
self.exportLogsWithSavePanelOnMac()
}
}
}
#endif
private var form: some View {
Form {
Section {
FormTextField(
title: "Account ID:",
baseURLString: AppInfoPlistConstants.authBaseURL.absoluteString,
placeholder: "account-id",
text: Binding(
get: { model.settings.accountId },
set: { model.settings.accountId = $0 }
)
)
}
}
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .primaryAction) {
}
}
#endif
}
private func isTeamIdValid(_ teamId: String) -> Bool {
!teamId.isEmpty && teamId.unicodeScalars.allSatisfy { teamIdAllowedCharacterSet.contains($0) }
}
func saveButtonTapped() {
model.save()
func saveSettings() {
model.saveAccountSettings()
model.saveAdvancedSettings()
dismiss()
}
func cancelButtonTapped() {
model.load()
func loadSettings() {
model.loadAccountSettings()
model.loadAdvancedSettings()
dismiss()
}
func restoreAdvancedSettingsToDefaults() {
let defaultValue = AdvancedSettings.defaultValue
model.advancedSettings.authBaseURLString = defaultValue.authBaseURLString
model.advancedSettings.apiURLString = defaultValue.apiURLString
model.advancedSettings.connlibLogFilterString = defaultValue.connlibLogFilterString
model.saveAdvancedSettings()
}
#if os(macOS)
func exportLogsWithSavePanelOnMac() {
self.isExportingLogs = true
@@ -302,15 +549,16 @@ struct FormTextField: View {
var body: some View {
#if os(iOS)
HStack(spacing: 15) {
Text(title)
VStack(spacing: 10) {
Spacer()
HStack(spacing: 5) {
Text(title)
Spacer()
TextField(baseURLString, text: text, prompt: Text(placeholder))
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
}
Spacer()
TextField(baseURLString, text: text, prompt: Text(placeholder))
.autocorrectionDisabled()
.multilineTextAlignment(.leading)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
.textInputAutocapitalization(.never)
}
#else
HStack(spacing: 30) {

View File

@@ -6,6 +6,62 @@
import Foundation
struct Settings: Codable, Hashable {
var accountId: String = ""
struct AccountSettings {
var accountId: String = "" {
didSet { if oldValue != accountId { isSavedToDisk = false } }
}
var isSavedToDisk = true
var isValid: Bool {
!accountId.isEmpty
&& accountId.unicodeScalars.allSatisfy { Self.teamIdAllowedCharacterSet.contains($0) }
}
static let teamIdAllowedCharacterSet: CharacterSet = {
var pathAllowed = CharacterSet.urlPathAllowed
pathAllowed.remove("/")
return pathAllowed
}()
}
struct AdvancedSettings: Equatable {
var authBaseURLString: String {
didSet { if oldValue != authBaseURLString { isSavedToDisk = false } }
}
var apiURLString: String {
didSet { if oldValue != apiURLString { isSavedToDisk = false } }
}
var connlibLogFilterString: String {
didSet { if oldValue != connlibLogFilterString { isSavedToDisk = false } }
}
var isSavedToDisk = true
var isValid: Bool {
URL(string: authBaseURLString) != nil
&& URL(string: apiURLString) != nil
&& !connlibLogFilterString.isEmpty
}
static let defaultValue: AdvancedSettings = {
#if DEBUG
AdvancedSettings(
authBaseURLString: "https://app.firez.one/",
apiURLString: "wss://api.firez.one/",
connlibLogFilterString:
"connlib_client_apple=debug,firezone_tunnel=trace,connlib_shared=debug,connlib_client_shared=debug,warn"
)
#else
AdvancedSettings(
authBaseURLString: "https://app.firezone.dev/",
apiURLString: "wss://api.firezone.dev/",
connlibLogFilterString:
"connlib_client_apple=info,firezone_tunnel=info,connlib_shared=info,connlib_client_shared=info,warn"
)
#endif
}()
// Note: To see what the connlibLogFilterString values mean, see:
// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
}

View File

@@ -45,7 +45,7 @@ final class AuthStore: ObservableObject {
let tunnelStore: TunnelStore
public let authBaseURL: URL
public var authBaseURL: URL
private var cancellables = Set<AnyCancellable>()
@Published private(set) var loginStatus: LoginStatus
@@ -55,6 +55,10 @@ final class AuthStore: ObservableObject {
self.authBaseURL = AppInfoPlistConstants.authBaseURL
self.loginStatus = .uninitialized
Task {
self.loginStatus = await self.getLoginStatus(from: tunnelStore.tunnelAuthStatus)
}
tunnelStore.$tunnelAuthStatus
.sink { [weak self] tunnelAuthStatus in
guard let self = self else { return }
@@ -71,15 +75,11 @@ final class AuthStore: ObservableObject {
return .uninitialized
case .accountNotSetup:
return .signedOut(accountId: nil)
case .signedOut(let tunnelAuthBaseURL, let tunnelAccountId):
if self.authBaseURL == tunnelAuthBaseURL {
return .signedOut(accountId: tunnelAccountId)
} else {
return .signedOut(accountId: nil)
}
case .signedOut(_, let tunnelAccountId):
return .signedOut(accountId: tunnelAccountId)
case .signedIn(let tunnelAuthBaseURL, let tunnelAccountId, let tokenReference):
guard self.authBaseURL == tunnelAuthBaseURL else {
return .signedOut(accountId: nil)
return .signedOut(accountId: tunnelAccountId)
}
let tunnelPortalURLString = self.authURL(accountId: tunnelAccountId).absoluteString
guard let tokenAttributes = await keychain.loadAttributes(tokenReference),
@@ -100,7 +100,7 @@ final class AuthStore: ObservableObject {
authURLString: portalURL.absoluteString, actorName: authResponse.actorName ?? "")
let tokenRef = try await keychain.store(authResponse.token, attributes)
try await tunnelStore.setAuthStatus(
try await tunnelStore.saveAuthStatus(
.signedIn(authBaseURL: self.authBaseURL, accountId: accountId, tokenReference: tokenRef))
}
@@ -143,4 +143,9 @@ final class AuthStore: ObservableObject {
func authURL(accountId: String) -> URL {
self.authBaseURL.appendingPathComponent(accountId)
}
func setAuthBaseURL(_ authBaseURL: URL, isChanged: inout Bool) {
isChanged = (self.authBaseURL == authBaseURL)
self.authBaseURL = authBaseURL
}
}

View File

@@ -13,17 +13,19 @@ enum TunnelStoreError: Error {
case tunnelCouldNotBeStarted
}
public struct TunnelProviderKeys {
static let keyAuthBaseURLString = "authBaseURLString"
static let keyAccountId = "accountId"
public static let keyConnlibLogFilter = "connlibLogFilter"
}
final class TunnelStore: ObservableObject {
private static let logger = Logger.make(for: TunnelStore.self)
static let shared = TunnelStore()
static let keyAuthBaseURLString = "authBaseURLString"
static let keyAccountId = "accountId"
@Published private var tunnel: NETunnelProviderManager?
@Published private(set) var tunnelAuthStatus: TunnelAuthStatus = TunnelAuthStatus(
protocolConfiguration: nil)
@Published private(set) var tunnelAuthStatus: TunnelAuthStatus = .tunnelUninitialized
@Published private(set) var status: NEVPNStatus {
didSet { TunnelStore.logger.info("status changed: \(self.status.description)") }
@@ -41,7 +43,7 @@ final class TunnelStore: ObservableObject {
init() {
self.tunnel = nil
self.tunnelAuthStatus = TunnelAuthStatus(protocolConfiguration: nil)
self.tunnelAuthStatus = .tunnelUninitialized
self.status = .invalid
Task {
@@ -56,12 +58,11 @@ final class TunnelStore: ObservableObject {
if let tunnel = managers.first {
Self.logger.log("\(#function): Tunnel already exists")
self.tunnel = tunnel
self.tunnelAuthStatus = TunnelAuthStatus(
protocolConfiguration: tunnel.protocolConfiguration as? NETunnelProviderProtocol)
self.tunnelAuthStatus = tunnel.authStatus()
} else {
let tunnel = NETunnelProviderManager()
tunnel.localizedDescription = "Firezone"
tunnel.protocolConfiguration = TunnelAuthStatus.accountNotSetup.toProtocolConfiguration()
tunnel.protocolConfiguration = basicProviderProtocol()
try await tunnel.saveToPreferences()
Self.logger.log("\(#function): Tunnel created")
self.tunnel = tunnel
@@ -74,7 +75,7 @@ final class TunnelStore: ObservableObject {
}
}
func setAuthStatus(_ tunnelAuthStatus: TunnelAuthStatus) async throws {
func saveAuthStatus(_ tunnelAuthStatus: TunnelAuthStatus) async throws {
guard let tunnel = tunnel else {
fatalError("Tunnel not initialized yet")
}
@@ -84,11 +85,46 @@ final class TunnelStore: ObservableObject {
if wasConnected {
stop()
}
tunnel.protocolConfiguration = tunnelAuthStatus.toProtocolConfiguration()
try await tunnel.saveToPreferences()
try await tunnel.saveAuthStatus(tunnelAuthStatus)
self.tunnelAuthStatus = tunnelAuthStatus
}
func saveAdvancedSettings(_ advancedSettings: AdvancedSettings) async throws {
guard let tunnel = tunnel else {
fatalError("Tunnel not initialized yet")
}
let wasConnected =
(tunnel.connection.status == .connected || tunnel.connection.status == .connecting)
if wasConnected {
stop()
}
try await tunnel.saveAdvancedSettings(advancedSettings)
}
func advancedSettings() -> AdvancedSettings? {
guard let tunnel = tunnel else {
return nil
}
return tunnel.advancedSettings()
}
func basicProviderProtocol() -> NETunnelProviderProtocol {
let protocolConfiguration = NETunnelProviderProtocol()
protocolConfiguration.providerBundleIdentifier = Bundle.main.bundleIdentifier.map {
"\($0).network-extension"
}
protocolConfiguration.serverAddress = AdvancedSettings.defaultValue.apiURLString
protocolConfiguration.providerConfiguration = [
TunnelProviderKeys.keyConnlibLogFilter:
AdvancedSettings.defaultValue.connlibLogFilterString
]
return protocolConfiguration
}
func start() async throws {
guard let tunnel = tunnel else {
Self.logger.log("\(#function): TunnelStore is not initialized")
@@ -101,6 +137,10 @@ final class TunnelStore: ObservableObject {
return
}
if tunnel.advancedSettings().connlibLogFilterString.isEmpty {
tunnel.setConnlibLogFilter(AdvancedSettings.defaultValue.connlibLogFilterString)
}
tunnel.isEnabled = true
try await tunnel.saveToPreferences()
try await tunnel.loadFromPreferences()
@@ -134,7 +174,7 @@ final class TunnelStore: ObservableObject {
session.stopTunnel()
if case .signedIn(let authBaseURL, let accountId, let tokenReference) = self.tunnelAuthStatus {
try await setAuthStatus(.signedOut(authBaseURL: authBaseURL, accountId: accountId))
try await saveAuthStatus(.signedOut(authBaseURL: authBaseURL, accountId: accountId))
return tokenReference
}
@@ -259,57 +299,6 @@ enum TunnelAuthStatus {
}
}
init(protocolConfiguration: NETunnelProviderProtocol?) {
if let protocolConfiguration = protocolConfiguration {
let providerConfig = protocolConfiguration.providerConfiguration
let authBaseURL: URL? = {
guard let urlString = providerConfig?[TunnelStore.keyAuthBaseURLString] as? String else {
return nil
}
return URL(string: urlString)
}()
let accountId = providerConfig?[TunnelStore.keyAccountId] as? String
let tokenRef = protocolConfiguration.passwordReference
if let authBaseURL = authBaseURL, let accountId = accountId {
if let tokenRef = tokenRef {
self = .signedIn(authBaseURL: authBaseURL, accountId: accountId, tokenReference: tokenRef)
} else {
self = .signedOut(authBaseURL: authBaseURL, accountId: accountId)
}
} else {
self = .accountNotSetup
}
} else {
self = .tunnelUninitialized
}
}
func toProtocolConfiguration() -> NETunnelProviderProtocol {
let protocolConfiguration = NETunnelProviderProtocol()
protocolConfiguration.providerBundleIdentifier = Bundle.main.bundleIdentifier.map {
"\($0).network-extension"
}
protocolConfiguration.serverAddress = AppInfoPlistConstants.controlPlaneURL.absoluteString
switch self {
case .tunnelUninitialized, .accountNotSetup:
break
case .signedOut(let authBaseURL, let accountId):
protocolConfiguration.providerConfiguration = [
TunnelStore.keyAuthBaseURLString: authBaseURL.absoluteString,
TunnelStore.keyAccountId: accountId,
]
case .signedIn(let authBaseURL, let accountId, let tokenReference):
protocolConfiguration.providerConfiguration = [
TunnelStore.keyAuthBaseURLString: authBaseURL.absoluteString,
TunnelStore.keyAccountId: accountId,
]
protocolConfiguration.passwordReference = tokenReference
}
return protocolConfiguration
}
func accountId() -> String? {
switch self {
case .tunnelUninitialized, .accountNotSetup:
@@ -338,3 +327,107 @@ extension NEVPNStatus: CustomStringConvertible {
}
}
}
extension NETunnelProviderManager {
func authStatus() -> TunnelAuthStatus {
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfig = protocolConfiguration.providerConfiguration
{
let authBaseURL: URL? = {
guard let urlString = providerConfig[TunnelProviderKeys.keyAuthBaseURLString] as? String
else {
return nil
}
return URL(string: urlString)
}()
let accountId = providerConfig[TunnelProviderKeys.keyAccountId] as? String
let tokenRef = protocolConfiguration.passwordReference
if let authBaseURL = authBaseURL, let accountId = accountId {
if let tokenRef = tokenRef {
return .signedIn(authBaseURL: authBaseURL, accountId: accountId, tokenReference: tokenRef)
} else {
return .signedOut(authBaseURL: authBaseURL, accountId: accountId)
}
} else {
return .accountNotSetup
}
}
return .accountNotSetup
}
func saveAuthStatus(_ authStatus: TunnelAuthStatus) async throws {
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
{
var providerConfig = providerConfiguration
switch authStatus {
case .tunnelUninitialized, .accountNotSetup:
break
case .signedOut(let authBaseURL, let accountId):
providerConfig[TunnelProviderKeys.keyAuthBaseURLString] = authBaseURL.absoluteString
providerConfig[TunnelProviderKeys.keyAccountId] = accountId
case .signedIn(let authBaseURL, let accountId, let tokenReference):
providerConfig[TunnelProviderKeys.keyAuthBaseURLString] = authBaseURL.absoluteString
providerConfig[TunnelProviderKeys.keyAccountId] = accountId
protocolConfiguration.passwordReference = tokenReference
}
protocolConfiguration.providerConfiguration = providerConfig
try await saveToPreferences()
}
}
func advancedSettings() -> AdvancedSettings {
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfig = protocolConfiguration.providerConfiguration
{
let authBaseURLString =
(providerConfig[TunnelProviderKeys.keyAuthBaseURLString] as? String)
?? AdvancedSettings.defaultValue.authBaseURLString
let logFilter =
(providerConfig[TunnelProviderKeys.keyConnlibLogFilter] as? String)
?? AdvancedSettings.defaultValue.connlibLogFilterString
let apiURLString =
protocolConfiguration.serverAddress
?? AdvancedSettings.defaultValue.apiURLString
return AdvancedSettings(
authBaseURLString: authBaseURLString,
apiURLString: apiURLString,
connlibLogFilterString: logFilter
)
}
return AdvancedSettings.defaultValue
}
func setConnlibLogFilter(_ logFiler: String) {
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
{
var providerConfig = providerConfiguration
providerConfig[TunnelProviderKeys.keyConnlibLogFilter] = logFiler
protocolConfiguration.providerConfiguration = providerConfig
}
}
func saveAdvancedSettings(_ advancedSettings: AdvancedSettings) async throws {
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
{
var providerConfig = providerConfiguration
providerConfig[TunnelProviderKeys.keyAuthBaseURLString] =
advancedSettings.authBaseURLString
providerConfig[TunnelProviderKeys.keyConnlibLogFilter] =
advancedSettings.connlibLogFilterString
protocolConfiguration.providerConfiguration = providerConfig
protocolConfiguration.serverAddress = advancedSettings.apiURLString
try await saveToPreferences()
}
}
}

View File

@@ -89,25 +89,19 @@ public class Adapter {
private var controlPlaneURLString: String
private var token: String
// Docs on filter strings: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
#if DEBUG
private let logFilterString =
"connlib_client_apple=debug,firezone_tunnel=trace,connlib_shared=debug,connlib_client_shared=debug,warn"
#else
private let logFilterString =
"connlib_client_apple=info,firezone_tunnel=info,connlib_shared=info,connlib_client_shared=info,warn"
#endif
private let logFilter: String
private let connlibLogFolderPath: String
public init(
controlPlaneURLString: String, token: String, packetTunnelProvider: NEPacketTunnelProvider
controlPlaneURLString: String, token: String,
logFilter: String, packetTunnelProvider: NEPacketTunnelProvider
) {
self.controlPlaneURLString = controlPlaneURLString
self.token = token
self.packetTunnelProvider = packetTunnelProvider
self.callbackHandler = CallbackHandler()
self.state = .stoppedTunnel
self.logFilter = logFilter
self.connlibLogFolderPath = SharedAccess.connlibLogFolderURL?.path ?? ""
}
@@ -147,7 +141,7 @@ public class Adapter {
self.state = .startingTunnel(
session: try WrappedSession.connect(
self.controlPlaneURLString, self.token, self.getDeviceId(), self.connlibLogFolderPath,
self.logFilterString, self.callbackHandler),
self.logFilter, self.callbackHandler),
onStarted: completionHandler
)
} catch let error {
@@ -284,7 +278,7 @@ extension Adapter {
self.state = .startingTunnel(
session: try WrappedSession.connect(
controlPlaneURLString, token, self.getDeviceId(), self.connlibLogFolderPath,
logFilterString,
self.logFilter,
self.callbackHandler),
onStarted: { error in
if let error = error {

View File

@@ -40,6 +40,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
let providerConfig = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration
guard let connlibLogFilter = providerConfig?[TunnelProviderKeys.keyConnlibLogFilter] as? String
else {
Self.logger.error("connlibLogFilter is missing")
completionHandler(
PacketTunnelProviderError.savedProtocolConfigurationIsInvalid("connlibLogFilter"))
return
}
Task {
let keychain = Keychain()
guard let token = await keychain.load(persistentRef: tokenRef) else {
@@ -48,7 +58,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
let adapter = Adapter(
controlPlaneURLString: controlPlaneURLString, token: token, packetTunnelProvider: self)
controlPlaneURLString: controlPlaneURLString, token: token, logFilter: connlibLogFilter,
packetTunnelProvider: self)
self.adapter = adapter
do {
try adapter.start { error in