feat(apple): Add Sentry reporting to Swift codebase (#7620)

Adds Sentry SDK to the Swift codebase. Currently error captures are
implemented and happen during `Log.error` calls.

Fixes #7560
This commit is contained in:
Jamil
2025-01-03 16:59:59 -08:00
committed by GitHub
parent c423c34aa7
commit 525c1b502b
23 changed files with 443 additions and 143 deletions

View File

@@ -10,41 +10,27 @@ concurrency:
cancel-in-progress: false
jobs:
create_gateway_sentry_release:
if: ${{ startsWith(github.event.release.name, 'gateway') }}
create_sentry_release:
name: create_${{ matrix.component }}_sentry_release
runs-on: ubuntu-22.04
strategy:
matrix:
include:
- component: gateway
projects: gateway
- component: gui-client
projects: gui-client-gui gui-client-ipc-service
- component: headless-client
projects: headless-client
- component: macos-client
projects: apple-client
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/create-sentry-release
if: ${{ startsWith(github.event.release.name, matrix.component) }}
with:
component: gateway
projects: gateway
sentry_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
create_gui-client_sentry_release:
if: ${{ startsWith(github.event.release.name, 'gui-client') }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/create-sentry-release
with:
component: gui-client
projects: gui-client-gui gui-client-ipc-service
sentry_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
create_headless-client_sentry_release:
if: ${{ startsWith(github.event.release.name, 'headless-client') }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/create-sentry-release
with:
component: headless-client
projects: headless-client
component: ${{ matrix.component }}
projects: ${{ matrix.projects }}
sentry_token: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@@ -17,6 +17,9 @@
6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; };
794C38152970A2660029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38142970A2660029F38F /* FirezoneKit */; };
79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 79756C6529704A7A0018E2D5 /* FirezoneKit */; };
8D4087D52D24653B005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087D42D24653B005B2BAF /* Sentry */; };
8D4087D92D246541005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087D82D246541005B2BAF /* Sentry */; };
8D4087DD2D246651005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087DC2D246651005B2BAF /* Sentry */; };
8D41B9A52D15DD6800D16065 /* TunnelLogArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */; };
8D41B9A62D15DD6800D16065 /* TunnelLogArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */; };
8D5047F52CE6AA1E009802E9 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D5047F32CE6AA1E009802E9 /* libresolv.9.tbd */; };
@@ -129,6 +132,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8D4087D52D24653B005B2BAF /* Sentry in Frameworks */,
8DC08BD42B297B8200675F46 /* libresolv.tbd in Frameworks */,
8DC08BCB2B296C4500675F46 /* libresolv.9.tbd in Frameworks */,
794C38152970A2660029F38F /* FirezoneKit in Frameworks */,
@@ -145,6 +149,7 @@
8D5047F82CE6AA22009802E9 /* FirezoneKit in Frameworks */,
8DC9FB852CF5A738001BCE6A /* NetworkExtension.framework in Frameworks */,
8D5047FA2CE6AA2E009802E9 /* SystemConfiguration.framework in Frameworks */,
8D4087D92D246541005B2BAF /* Sentry in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -154,6 +159,7 @@
files = (
05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */,
79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */,
8D4087DD2D246651005B2BAF /* Sentry in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -276,6 +282,7 @@
name = FirezoneNetworkExtensioniOS;
packageProductDependencies = (
794C38142970A2660029F38F /* FirezoneKit */,
8D4087D42D24653B005B2BAF /* Sentry */,
);
productName = FirezoneNetworkExtensioniOS;
productReference = 05CF1CF0290B1CEE00CF4755 /* dev.firezone.firezone.network-extension.appex */;
@@ -298,6 +305,7 @@
name = FirezoneNetworkExtensionmacOS;
packageProductDependencies = (
8D5047F72CE6AA22009802E9 /* FirezoneKit */,
8D4087D82D246541005B2BAF /* Sentry */,
);
productName = FirezoneNetworkExtensionStandalonemacOS;
productReference = 8D5047E32CE6A8F4009802E9 /* dev.firezone.firezone.network-extension.systemextension */;
@@ -323,6 +331,7 @@
name = Firezone;
packageProductDependencies = (
79756C6529704A7A0018E2D5 /* FirezoneKit */,
8D4087DC2D246651005B2BAF /* Sentry */,
);
productName = Firezone;
productReference = 8DCC021928D512AC007E12D2 /* Firezone.app */;
@@ -361,6 +370,7 @@
);
mainGroup = 8DCC021028D512AC007E12D2;
packageReferences = (
8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
);
productRefGroup = 8DCC021A28D512AC007E12D2 /* Products */;
projectDirPath = "";
@@ -986,6 +996,17 @@
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/getsentry/sentry-cocoa";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 8.42.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
794C38142970A2660029F38F /* FirezoneKit */ = {
isa = XCSwiftPackageProductDependency;
@@ -995,6 +1016,21 @@
isa = XCSwiftPackageProductDependency;
productName = FirezoneKit;
};
8D4087D42D24653B005B2BAF /* Sentry */ = {
isa = XCSwiftPackageProductDependency;
package = 8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
productName = Sentry;
};
8D4087D82D246541005B2BAF /* Sentry */ = {
isa = XCSwiftPackageProductDependency;
package = 8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
productName = Sentry;
};
8D4087DC2D246651005B2BAF /* Sentry */ = {
isa = XCSwiftPackageProductDependency;
package = 8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
productName = Sentry;
};
8D5047F72CE6AA22009802E9 /* FirezoneKit */ = {
isa = XCSwiftPackageProductDependency;
productName = FirezoneKit;

View File

@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "c1404acfa3a71e42aa789c6d5564da59d4377569",
"version" : "8.42.1"
}
}
],
"version" : 2
}

View File

@@ -18,6 +18,9 @@ struct FirezoneApp: App {
@StateObject var store: Store
init() {
// Initialize Telemetry as early as possible
Telemetry.start()
let favorites = Favorites()
let store = Store()
_favorites = StateObject(wrappedValue: favorites)
@@ -80,7 +83,10 @@ struct FirezoneApp: App {
// Can be removed once all clients >= 1.4.0
try await FirezoneId.migrate()
try await FirezoneId.createIfMissing()
let id = try await FirezoneId.createIfMissing()
// Hydrate telemetry userId with our firezone id
Telemetry.setFirezoneId(id.uuid.uuidString)
}
if let store = store {

View File

@@ -50,9 +50,10 @@ public final class Log {
logWriter?.write(severity: .warning, message: message)
}
public static func error(_ message: String) {
self.logger.error("\(message, privacy: .public)")
logWriter?.write(severity: .error, message: message)
public static func error(_ err: Error) {
self.logger.error("\(err.localizedDescription, privacy: .public)")
logWriter?.write(severity: .error, message: err.localizedDescription)
Telemetry.capture(err)
}
// Returns the size in bytes of the provided directory, calculated by summing

View File

@@ -7,6 +7,17 @@
import Foundation
public struct SharedAccess {
public enum Error: Swift.Error {
case unableToWriteToFile(URL, Swift.Error)
var localizedDescription: String {
switch self {
case .unableToWriteToFile(let url, let error):
return "Unable to write to \(url): \(error)"
}
}
}
public static var baseFolderURL: URL {
guard
let url = FileManager.default.containerURL(

View File

@@ -0,0 +1,98 @@
//
// Telemetry.swift
// (c) 2024 Firezone, Inc.
// LICENSE: Apache-2.0
//
import Sentry
public enum Telemetry {
// We can only create a new User object after Sentry is started; not retrieve
// the existing one. So we need to collect these fields from various codepaths
// during initialization / sign in so we can build a new User object any time
// one of these is updated.
private static var userId: String?
private static var accountSlug: String?
public static func start() {
SentrySDK.start { options in
options.dsn = "https://66c71f83675f01abfffa8eb977bcbbf7@o4507971108339712.ingest.us.sentry.io/4508175177023488"
options.environment = "entrypoint" // will be reconfigured in TunnelManager
options.releaseName = releaseName()
#if DEBUG
// https://docs.sentry.io/platforms/apple/guides/ios/configuration/options/#debug
options.debug = true
#endif
}
}
public static func setEnvironmentOrClose(_ apiURLString: String) {
var environment: String?
if apiURLString.starts(with: "wss://api.firezone.dev") {
environment = "production"
} else if apiURLString.starts(with: "wss://api.firez.one") {
environment = "staging"
}
guard let environment
else {
// Disable Sentry in unknown environments
SentrySDK.close()
return
}
SentrySDK.configureScope { configuration in
configuration.setEnvironment(environment)
}
}
public static func capture(_ err: Error) {
SentrySDK.capture(error: err)
}
public static func setFirezoneId(_ id: String?) {
self.userId = id
updateUser()
}
public static func setAccountSlug(_ slug: String?) {
self.accountSlug = slug
updateUser()
}
private static func updateUser() {
guard let userId,
let accountSlug
else {
return
}
SentrySDK.configureScope { configuration in
// Matches the format we use in rust/telemetry/lib.rs
let user = User(userId: userId)
user.data = ["account_slug": accountSlug]
configuration.setUser(user)
}
}
private static func releaseName() -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"]
as? String ?? "unknown"
#if os(iOS)
return "ios-appstore-\(version)"
#else
// Apps from the app store have a receipt file
if let receiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: receiptURL.path) {
return "macos-appstore-\(version)"
}
return "macos-client-\(version)"
#endif
}
}

View File

@@ -13,13 +13,29 @@ import NetworkExtension
enum TunnelManagerError: Error {
case cannotSaveIfMissing
case cannotLoad
case decodeIPCDataFailed
case invalidStatusChange
var localizedDescription: String {
switch self {
case .cannotSaveIfMissing:
return "Manager doesn't seem initialized. Can't save settings."
case .decodeIPCDataFailed:
return "Decoding IPC data failed."
case .invalidStatusChange:
return "NEVPNStatusDidChange notification doesn't seem to be valid."
case .cannotLoad:
return "Could not load VPN configurations!"
}
}
}
public enum TunnelManagerKeys {
static let actorName = "actorName"
static let authBaseURL = "authBaseURL"
static let apiURL = "apiURL"
public static let accountSlug = "accountSlug"
public static let logFilter = "logFilter"
public static let internetResourceEnabled = "internetResourceEnabled"
}
@@ -152,7 +168,7 @@ public class TunnelManager {
// Since our bundle ID can change (by us), find the one that's current and ignore the others.
guard let managers = try? await NETunnelProviderManager.loadAllFromPreferences()
else {
Log.error("\(#function): Could not load VPN configurations!")
Log.error(TunnelManagerError.cannotLoad)
return
}
@@ -172,6 +188,12 @@ public class TunnelManager {
}
let status = manager.connection.status
// Configure our Telemetry environment
Telemetry.setEnvironmentOrClose(settings.apiURL)
Telemetry.setAccountSlug(
providerConfiguration[TunnelManagerKeys.accountSlug]
)
// Share what we found with our caller
callback(status, settings, actorName)
@@ -194,16 +216,16 @@ public class TunnelManager {
}
}
func saveActorName(_ actorName: String?) async throws {
func saveAuthResponse(_ authResponse: AuthResponse) async throws {
guard let manager = manager,
let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
var providerConfiguration = protocolConfiguration.providerConfiguration
else {
Log.error("Manager doesn't seem initialized. Can't save settings.")
throw TunnelManagerError.cannotSaveIfMissing
}
providerConfiguration[TunnelManagerKeys.actorName] = actorName
providerConfiguration[TunnelManagerKeys.actorName] = authResponse.actorName
providerConfiguration[TunnelManagerKeys.accountSlug] = authResponse.accountSlug
protocolConfiguration.providerConfiguration = providerConfiguration
manager.protocolConfiguration = protocolConfiguration
@@ -220,7 +242,6 @@ public class TunnelManager {
let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
Log.error("Manager doesn't seem initialized. Can't save settings.")
throw TunnelManagerError.cannotSaveIfMissing
}
@@ -238,6 +259,9 @@ public class TunnelManager {
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
// Reconfigure our Telemetry environment in case it changed
Telemetry.setEnvironmentOrClose(settings.apiURL)
}
func start(token: String? = nil) {
@@ -250,7 +274,7 @@ public class TunnelManager {
do {
try session()?.startTunnel(options: options)
} catch {
Log.error("Error starting tunnel: \(error)")
Log.error(error)
}
}
@@ -261,7 +285,7 @@ public class TunnelManager {
self.session()?.stopTunnel()
}
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
}
} else {
session()?.stopTunnel()
@@ -294,7 +318,7 @@ public class TunnelManager {
callback(self.resourcesListCache)
}
} catch {
Log.error("Error: sendProviderMessage: \(error)")
Log.error(error)
}
}
@@ -350,7 +374,6 @@ public class TunnelManager {
) { data in
guard let data = data
else {
Log.error("Error: \(#function): No data received")
errorHandler(TunnelManagerError.decodeIPCDataFailed)
return
@@ -360,7 +383,6 @@ public class TunnelManager {
LogChunk.self, from: data
)
else {
Log.error("Error: \(#function): Invalid data received")
errorHandler(TunnelManagerError.decodeIPCDataFailed)
return
@@ -374,7 +396,7 @@ public class TunnelManager {
}
}
} catch {
Log.error("Error: \(#function): \(error)")
Log.error(error)
}
}
@@ -434,7 +456,7 @@ public class TunnelManager {
) {
guard let session = notification.object as? NETunnelProviderSession
else {
Log.error("\(#function): NEVPNStatusDidChange notification doesn't seem to be valid")
Log.error(TunnelManagerError.invalidStatusChange)
return
}

View File

@@ -36,25 +36,23 @@ struct AuthClient {
func response(url: URL?) throws -> AuthResponse {
guard let url = url,
let returnedState = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first(where: { $0.name == "state" })?.value,
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
let returnedState = urlComponents.sanitizedQueryParam("state"),
areStringsEqualConstantTime(state, returnedState),
let fragment = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == "fragment" })?
.value,
let actorName = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == "actor_name" })?
.value?
.removingPercentEncoding?
.replacingOccurrences(of: "+", with: " ")
let fragment = urlComponents.sanitizedQueryParam("fragment"),
let actorName = urlComponents.sanitizedQueryParam("actor_name"),
let accountSlug = urlComponents.sanitizedQueryParam("account_slug")
else {
throw AuthClientError.invalidCallbackURL
}
let token = nonce + fragment
return AuthResponse(token: token, actorName: actorName)
return AuthResponse(
actorName: actorName,
accountSlug: accountSlug,
token: token
)
}
private static func createRandomHexString(byteCount: Int) throws -> String {
@@ -101,3 +99,11 @@ extension URL {
return components.url ?? self
}
}
extension URLComponents {
func sanitizedQueryParam(_ queryParam: String) -> String? {
let value = self.queryItems?.first(where: { $0.name == queryParam })?.value
return value?.removingPercentEncoding?.replacingOccurrences(of: "+", with: " ")
}
}

View File

@@ -10,11 +10,9 @@ struct AuthResponse {
// The user associated with this authResponse.
let actorName: String
// The account slug of the account the user signed in to.
let accountSlug: String
// The opaque auth token
let token: String
init(token: String, actorName: String) {
self.actorName = actorName
self.token = token
}
}

View File

@@ -15,7 +15,7 @@ public struct FirezoneId {
kSecAttrDescription: "Firezone device id",
]
private var uuid: UUID
public var uuid: UUID
public init(_ uuid: UUID? = nil) {
self.uuid = uuid ?? UUID()
@@ -91,12 +91,17 @@ public struct FirezoneId {
try await firezoneId.save()
}
public static func createIfMissing() async throws {
guard try await load() == nil
else { return } // New firezone-id already saved in Keychain
public static func createIfMissing() async throws -> FirezoneId {
guard let id = try await load()
else {
let id = FirezoneId(UUID())
try await id.save()
let firezoneId = FirezoneId(UUID())
try await firezoneId.save()
return id
}
// New firezone-id already saved in Keychain
return id
}
}

View File

@@ -97,9 +97,9 @@ public class SessionNotification: NSObject {
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
Log.error("\(#function): Error requesting notification: \(error)")
Log.error(error)
} else {
Log.error("\(#function): Successfully requested notification")
Log.debug("\(#function): Successfully requested notification")
}
}
}

View File

@@ -32,7 +32,13 @@ struct WebAuthSession {
return
}
Task { try await store.signIn(authResponse: authResponse) }
Task {
do {
try await store.signIn(authResponse: authResponse)
} catch {
Log.error(error)
}
}
}
// Apple weirdness, doesn't seem to be actually used in macOS

View File

@@ -126,7 +126,7 @@ public final class Store: ObservableObject {
DispatchQueue.main.async { self.actorName = authResponse.actorName }
try await TunnelManager.shared.saveSettings(settings)
try await TunnelManager.shared.saveActorName(authResponse.actorName)
try await TunnelManager.shared.saveAuthResponse(authResponse)
// Bring the tunnel up and send it a token to start
TunnelManager.shared.start(token: authResponse.token)
@@ -163,7 +163,7 @@ public final class Store: ObservableObject {
try await TunnelManager.shared.saveSettings(newSettings)
DispatchQueue.main.async { self.settings = newSettings }
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
}
}
}

View File

@@ -21,7 +21,7 @@ final class GrantVPNViewModel: ObservableObject {
do {
try await store.createVPNProfile()
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
}
}
}

View File

@@ -293,7 +293,7 @@ public final class MenuBar: NSObject, ObservableObject {
do {
try await model.store.createVPNProfile()
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
}
}
}

View File

@@ -10,6 +10,13 @@ import SwiftUI
enum SettingsViewError: Error {
case logFolderIsUnavailable
var localizedDescription: String {
switch self {
case .logFolderIsUnavailable:
return "Log folder is unavailable."
}
}
}
@MainActor
@@ -47,7 +54,7 @@ public final class SettingsViewModel: ObservableObject {
do {
try await store.save(settings)
} catch {
Log.error("Error saving settings to tunnel store: \(error)")
Log.error(error)
}
}
}
@@ -67,8 +74,6 @@ public final class SettingsViewModel: ObservableObject {
Log.log("\(#function)")
guard let logFilesFolderURL = SharedAccess.logFolderURL else {
Log.error("\(#function): Log folder is unavailable")
return "Unknown"
}
@@ -90,7 +95,7 @@ public final class SettingsViewModel: ObservableObject {
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
return "Unknown"
}
@@ -111,6 +116,17 @@ public final class SettingsViewModel: ObservableObject {
}
extension FileManager {
enum FileManagerError: Error {
case invalidURL(URL, Error)
var localizedDescription: String {
switch self {
case .invalidURL(let url, let error):
return "Unable to get resource value for '\(url)': \(error)"
}
}
}
func forEachFileUnder(
_ dirURL: URL,
including resourceKeys: Set<URLResourceKey>,
@@ -135,7 +151,7 @@ extension FileManager {
let resourceValues = try url.resourceValues(forKeys: resourceKeys)
handler(url, resourceValues)
} catch {
Log.error("Unable to get resource value for '\(url)': \(error)")
Log.error(FileManagerError.invalidURL(url, error))
}
}
}
@@ -598,7 +614,7 @@ public struct SettingsView: View {
window.contentViewController?.presentingViewController?.dismiss(self)
}
} catch {
Log.error("\(#function): \(error)")
Log.error(error)
let alert = NSAlert()
alert.messageText = "Error exporting logs: \(error.localizedDescription)"

View File

@@ -11,6 +11,17 @@ import UserNotifications
import Cocoa
class UpdateChecker {
enum UpdateError: Error {
case invalidVersion(String)
var localizedDescription: String {
switch self {
case .invalidVersion(let version):
return "Invalid version: \(version)"
}
}
}
private var timer: Timer?
private let notificationAdapter: NotificationAdapter = NotificationAdapter()
private let versionCheckUrl: URL = URL(string: "https://www.firezone.dev/api/releases")!
@@ -36,12 +47,13 @@ class UpdateChecker {
guard let self = self else { return }
if let error = error {
Log.error("Error fetching version manifest: \(error)")
Log.error(error)
return
}
guard let versionInfo = VersionInfo.from(data: data) else {
Log.error("No data or failed to decode data")
let attemptedVersion = String(data: data ?? Data(), encoding: .utf8) ?? ""
Log.error(UpdateError.invalidVersion(attemptedVersion))
return
}
@@ -89,9 +101,9 @@ private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.sound, .badge, .alert]) { _, error in
if let error = error {
Log.error("Failed to request authorization for notifications: \(error)")
}
if let error = error {
Log.error(error)
}
}
}
@@ -114,7 +126,7 @@ private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
Log.error("\(#function): Error requesting notification: \(error)")
Log.error(error)
}
}

View File

@@ -8,16 +8,25 @@ import Foundation
import NetworkExtension
import OSLog
public enum AdapterError: Error {
enum AdapterError: Error {
/// Failure to perform an operation in such state.
case invalidState
case invalidState(AdapterState)
/// connlib failed to start
case connlibConnectError(String)
var localizedDescription: String {
switch self {
case .invalidState(let state):
return "Adapter is in an invalid state: \(state)"
case .connlibConnectError(let error):
return "connlib failed to start: \(error)"
}
}
}
/// Enum representing internal state of the adapter
private enum AdapterState: CustomStringConvertible {
enum AdapterState: CustomStringConvertible {
case tunnelStarted(session: WrappedSession)
case tunnelStopped
@@ -118,15 +127,11 @@ class Adapter {
public func start() async throws {
Log.log("Adapter.start")
guard case .tunnelStopped = self.state else {
throw AdapterError.invalidState
throw AdapterError.invalidState(self.state)
}
callbackHandler.delegate = self
if connlibLogFolderPath.isEmpty {
Log.error("Cannot get shared log folder for connlib")
}
Log.log("Adapter.start: Starting connlib")
do {
let jsonEncoder = JSONEncoder()
@@ -377,8 +382,7 @@ extension Adapter: CallbackHandlerDelegate {
networkSettings.dnsAddresses = dnsAddresses
networkSettings.apply()
case .tunnelStopped:
Log.error(
"\(#function): Unexpected state: \(self.state)")
Log.error(AdapterError.invalidState(self.state))
}
}
}

View File

@@ -53,9 +53,7 @@ class NetworkSettings {
packetTunnelProvider?.setTunnelNetworkSettings(tunnelNetworkSettings) { error in
if let error = error {
Log.error(
"\(#function): Error occurred while applying network settings! Error: \(error.localizedDescription)"
)
Log.error(error)
}
completionHandler?()

View File

@@ -24,6 +24,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var logExportState: LogExportState = .idle
override init() {
// Initialize Telemetry as early as possible
Telemetry.start()
super.init()
}
override func startTunnel(
options: [String: NSObject]?,
completionHandler: @escaping (Error?) -> Void
@@ -38,7 +45,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// The tunnel can come up without the app having been launched first, so
// initialize the id here too.
try await FirezoneId.createIfMissing()
let id = try await FirezoneId.createIfMissing()
// Hydrate the telemetry userId with our firezone id
Telemetry.setFirezoneId(id.uuid.uuidString)
let passedToken = options?["token"] as? String
let keychainToken = try await Token.load()
@@ -62,6 +72,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
// Reconfigure our Telemetry environment now that we know the API URL
Telemetry.setEnvironmentOrClose(apiURL)
guard
let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?
.providerConfiguration as? [String: String],
@@ -73,6 +86,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
// Hydrate telemetry account slug
Telemetry.setAccountSlug(
providerConfiguration[TunnelManagerKeys.accountSlug]
)
let internetResourceEnabled: Bool = if let internetResourceEnabledJSON = providerConfiguration[TunnelManagerKeys.internetResourceEnabled]?.data(using: .utf8) {
(try? JSONDecoder().decode(Bool.self, from: internetResourceEnabledJSON )) ?? false
} else {
@@ -90,7 +108,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// `connected`.
completionHandler(nil)
} catch {
Log.error("\(#function): Error! \(error)")
Log.error(error)
completionHandler(error)
}
}
@@ -116,7 +134,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
to: SharedAccess.providerStopReasonURL, atomically: true, encoding: .utf8)
} catch {
Log.error(
"\(#function): Couldn't write provider stop reason to file. Notification won't work.")
SharedAccess.Error.unableToWriteToFile(
SharedAccess.providerStopReasonURL,
error
)
)
}
#if os(iOS)
// iOS notifications should be shown from the tunnel process
@@ -155,32 +177,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
getLogFolderSize(completionHandler)
case .exportLogs:
Task {
guard let completionHandler
else {
Log.error(
"\(#function): Need a completion handler to export logs."
)
return
}
exportLogs(completionHandler)
exportLogs(completionHandler!)
}
case .consumeStopReason:
Task {
guard let completionHandler
else {
Log.error(
"\(#function): Need a completion handler to consumeStopReason."
)
return
}
consumeStopReason(completionHandler)
consumeStopReason(completionHandler!)
}
}
}
func clearLogs(_ completionHandler: ((Data?) -> Void)? = nil) {
@@ -188,7 +191,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do {
try Log.clear(in: SharedAccess.logFolderURL)
} catch {
Log.error("Error clearing logs: \(error)")
Log.error(error)
}
completionHandler?(nil)
@@ -217,7 +220,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let chunk = try tunnelLogArchive.readChunk()
completionHandler(chunk)
} catch {
Log.error("\(#function): error reading chunk: \(error)")
Log.error(error)
completionHandler(nil)
}
@@ -232,7 +235,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard let logFolderURL = SharedAccess.logFolderURL,
let logFolderPath = FilePath(logFolderURL)
else {
Log.error("\(#function): log folder not available")
completionHandler(nil)
return
@@ -243,7 +245,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do {
try tunnelLogArchive.archive()
} catch {
Log.error("\(#function): error archiving logs: \(error)")
Log.error(error)
completionHandler(nil)
return
@@ -255,20 +257,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
func consumeStopReason(_ completionHandler: (Data?) -> Void) {
do {
let data = try Data(
contentsOf: SharedAccess.providerStopReasonURL
)
try? FileManager.default
.removeItem(at: SharedAccess.providerStopReasonURL)
completionHandler(data)
} catch {
Log.error("\(#function): error reading stop reason: \(error)")
guard let data = try? Data(contentsOf: SharedAccess.providerStopReasonURL)
else {
completionHandler(nil)
return
}
try? FileManager.default
.removeItem(at: SharedAccess.providerStopReasonURL)
completionHandler(data)
}
}

View File

@@ -10,6 +10,19 @@ import Foundation
import SystemConfiguration
class SystemConfigurationResolvers {
enum SystemConfigurationError: Error {
case failedToCreateDynamicStore
case unableToRetrieveNetworkServices
var localizedDescription: String {
switch self {
case .failedToCreateDynamicStore:
return "Failed to create dynamic store"
case .unableToRetrieveNetworkServices:
return "Unable to retrieve network services"
}
}
}
private var dynamicStore: SCDynamicStore?
// Arbitrary name for the connection to the store
@@ -18,7 +31,7 @@ class SystemConfigurationResolvers {
init() {
guard let dynamicStore = SCDynamicStoreCreate(nil, storeName, nil, nil)
else {
Log.error("\(#function): Failed to create dynamic store")
Log.error(SystemConfigurationError.failedToCreateDynamicStore)
self.dynamicStore = nil
return
}
@@ -47,7 +60,7 @@ class SystemConfigurationResolvers {
let interfaceSearchKey = "Setup:/Network/Service/.*/Interface" as CFString
guard let services = SCDynamicStoreCopyKeyList(dynamicStore, interfaceSearchKey) as? [String]
else {
Log.error("\(#function): Unable to retrieve network services")
Log.error(SystemConfigurationError.unableToRetrieveNetworkServices)
return []
}

View File

@@ -2,8 +2,50 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- BEGIN Sentry -->
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false />
<key>NSPrivacyCollectedDataTypeTracking</key>
<false />
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypePerformanceData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false />
<key>NSPrivacyCollectedDataTypeTracking</key>
<false />
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false />
<key>NSPrivacyCollectedDataTypeTracking</key>
<false />
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<!-- END Sentry -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<!-- BEGIN various connlib syscalls -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
@@ -12,6 +54,33 @@
<string>3B52.1</string>
</array>
</dict>
<!-- END various connlib syscalls -->
<!-- BEGIN Sentry -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<!-- END Sentry -->
</array>
</dict>
</plist>