diff --git a/.codespellrc b/.codespellrc index a2c339d12..6377506dd 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] skip = ./rust/target,Cargo.lock,./www/docs/reference/api/*.mdx,./erl_crash.dump,./apps/*/erl_crash.dump,./cover,./vendor,*.json,yarn.lock,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./www/build,./_build -ignore-words-list = crate,keypair,keypairs,iif,statics,wee,anull,commitish +ignore-words-list = crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 5f548db5e..701d1e8e6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -3,6 +3,7 @@ on: pull_request: paths: - "swift/**" + - "rust/connlib/**" - ".github/workflows/swift.yml" merge_group: types: [checks_requested] @@ -26,8 +27,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # TODO: Add a basic CI for the Apple client - # See rust.yml how we build, package and release connlib as an example build: runs-on: macos-latest permissions: @@ -39,4 +38,45 @@ jobs: - draft-release steps: - uses: actions/checkout@v3 - # TODO: Build Apple client from the CLI + - run: rustup show + - uses: Swatinem/rust-cache@v2 + with: + workspaces: ./rust + prefix-key: rust-${{ matrix.runs-on }} + save-if: ${{ github.ref == 'refs/heads/cloud' }} + - name: Update toolchain + run: rustup show + - name: Setup lipo + run: cargo install cargo-lipo + - uses: actions/cache@v3 + with: + path: apple/.build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + - name: Build Connlib.xcframework + env: + CONFIGURATION: Release + PROJECT_DIR: . + working-directory: ./rust/connlib/clients/apple + run: | + # build-xcframework.sh calls build-rust.sh indirectly via `xcodebuild`, but it pollutes the environment + # to the point that it causes the `ring` build to fail for the aarch64-apple-darwin target. So, explicitly + # build first. See https://github.com/briansmith/ring/issues/1332 + PLATFORM_NAME=macosx ./build-rust.sh + PLATFORM_NAME=iphoneos ./build-rust.sh + ./build-xcframework-dev.sh + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build app for macOS + working-directory: ./swift/apple + run: | + cp Firezone/Developer.xcconfig.ci-macOS Firezone/Developer.xcconfig + xcodebuild build -scheme Firezone -sdk macosx -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO + - name: Build app for iOS + working-directory: ./swift/apple + run: | + cp Firezone/Developer.xcconfig.ci-iOS Firezone/Developer.xcconfig + xcodebuild build -scheme Firezone -sdk iphoneos -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO diff --git a/rust/connlib/clients/apple/build-rust.sh b/rust/connlib/clients/apple/build-rust.sh index 44c5edf8c..9487e514e 100755 --- a/rust/connlib/clients/apple/build-rust.sh +++ b/rust/connlib/clients/apple/build-rust.sh @@ -6,13 +6,6 @@ set -ex -if [[ -z "$PROJECT_DIR" ]]; then - echo "Must provide PROJECT_DIR environment variable set to the Xcode project directory." 1>&2 - exit 1 -fi - -cd $PROJECT_DIR - # Default PLATFORM_NAME to macosx if not set. : "${PLATFORM_NAME:=macosx}" diff --git a/swift/apple/.gitignore b/swift/apple/.gitignore new file mode 100644 index 000000000..ac8336430 --- /dev/null +++ b/swift/apple/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.build/ +build/ +DerivedData/ +xcuserdata/ +**/*.xcuserstate + +Firezone/Developer.xcconfig diff --git a/swift/apple/.swiftformat b/swift/apple/.swiftformat new file mode 100644 index 000000000..ce03f6850 --- /dev/null +++ b/swift/apple/.swiftformat @@ -0,0 +1,13 @@ +--swiftversion 5.7 +--binarygrouping none +--decimalgrouping none +--hexgrouping none +--indent 2 +--octalgrouping none +--semicolons never +--wraparguments before-first +--wrapcollections before-first +--wrapparameters before-first +--extensionacl on-declarations +--maxwidth 100 +--header \n {file}\n (c) {created.year} Firezone, Inc.\n LICENSE: Apache-2.0\n diff --git a/swift/apple/Firezone.xcodeproj/project.pbxproj b/swift/apple/Firezone.xcodeproj/project.pbxproj new file mode 100644 index 000000000..18be8b2c8 --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/project.pbxproj @@ -0,0 +1,797 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 05CF1CF1290B1CEE00CF4755 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; }; + 05CF1CF9290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 05CF1CF0290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 05CF1D04290B1DCD00CF4755 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; platformFilters = (macos, ); }; + 05CF1D0C290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 05CF1D03290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 05CF1D16290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; }; + 05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; }; + 05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; }; + 794C38152970A2660029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38142970A2660029F38F /* FirezoneKit */; }; + 794C38172970A26A0029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38162970A26A0029F38F /* FirezoneKit */; }; + 79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 79756C6529704A7A0018E2D5 /* FirezoneKit */; }; + 8DCC021D28D512AC007E12D2 /* FirezoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */; }; + 8DCC022628D512AE007E12D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022528D512AE007E12D2 /* Assets.xcassets */; }; + 8DCC022A28D512AE007E12D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 05CF1CF7290B1CEE00CF4755 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8DCC021128D512AC007E12D2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05CF1CEF290B1CEE00CF4755; + remoteInfo = FirezoneNetworkExtensioniOS; + }; + 05CF1D0A290B1DCD00CF4755 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8DCC021128D512AC007E12D2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05CF1D02290B1DCD00CF4755; + remoteInfo = FirezoneNetworkExtensionmacOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0556189428EF883500DF9E3C /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 05CF1CF9290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex in Embed Foundation Extensions */, + 05CF1D0C290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 05CF1C8A290B11A500CF4755 /* Embed System Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed System Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; + 05B6467B292C36140014A4D4 /* AuthenticationServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceStub.swift; sourceTree = ""; }; + 05CF1C39290995DA00CF4755 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05CF1CDE290B1A9000CF4755 /* FirezoneNetworkExtension_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirezoneNetworkExtension_macOS.entitlements; sourceTree = ""; }; + 05CF1CF0290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FirezoneNetworkExtensioniOS.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirezoneNetworkExtension_iOS.entitlements; sourceTree = ""; }; + 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; }; + 79BA2AF229A00F8800A2E6DC /* Developer.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Developer.xcconfig; sourceTree = ""; }; + 79C88B24296F494500261800 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + 8DCC021928D512AC007E12D2 /* Firezone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firezone.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirezoneApp.swift; sourceTree = ""; }; + 8DCC022528D512AE007E12D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8DCC022728D512AE007E12D2 /* Firezone.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Firezone.entitlements; sourceTree = ""; }; + 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8DD2C4C3297B37BA00F984BF /* FirezoneKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FirezoneKit; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05CF1CED290B1CEE00CF4755 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 794C38152970A2660029F38F /* FirezoneKit in Frameworks */, + 05CF1CF1290B1CEE00CF4755 /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CF1D00290B1DCD00CF4755 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 794C38172970A26A0029F38F /* FirezoneKit in Frameworks */, + 05CF1D04290B1DCD00CF4755 /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DCC021628D512AC007E12D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */, + 79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0580451B29385C150080D1F0 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 05B6467B292C36140014A4D4 /* AuthenticationServiceStub.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + 05833DF928F73B070008FAB0 /* FirezoneNetworkExtension */ = { + isa = PBXGroup; + children = ( + 05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension_iOS.entitlements */, + 05CF1CDE290B1A9000CF4755 /* FirezoneNetworkExtension_macOS.entitlements */, + 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */, + ); + path = FirezoneNetworkExtension; + sourceTree = ""; + }; + 05E1505F28FF398000170F82 /* Application */ = { + isa = PBXGroup; + children = ( + 8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */, + ); + path = Application; + sourceTree = ""; + }; + 8D3F90C328D64FAD00980124 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8DCC021028D512AC007E12D2 = { + isa = PBXGroup; + children = ( + 8DD2C4C2297B37BA00F984BF /* Packages */, + 8DCC021B28D512AC007E12D2 /* Firezone */, + 05833DF928F73B070008FAB0 /* FirezoneNetworkExtension */, + 8DCC021A28D512AC007E12D2 /* Products */, + 8D3F90C328D64FAD00980124 /* Frameworks */, + 0580451B29385C150080D1F0 /* Recovered References */, + ); + sourceTree = ""; + }; + 8DCC021A28D512AC007E12D2 /* Products */ = { + isa = PBXGroup; + children = ( + 8DCC021928D512AC007E12D2 /* Firezone.app */, + 05CF1CF0290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex */, + 05CF1D03290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex */, + ); + name = Products; + sourceTree = ""; + }; + 8DCC021B28D512AC007E12D2 /* Firezone */ = { + isa = PBXGroup; + children = ( + 05E1505F28FF398000170F82 /* Application */, + 8DCC022528D512AE007E12D2 /* Assets.xcassets */, + 79C88B24296F494500261800 /* Config.xcconfig */, + 79BA2AF229A00F8800A2E6DC /* Developer.xcconfig */, + 8DCC022728D512AE007E12D2 /* Firezone.entitlements */, + 05CF1C39290995DA00CF4755 /* Info.plist */, + 8DCC022828D512AE007E12D2 /* Preview Content */, + ); + path = Firezone; + sourceTree = ""; + }; + 8DCC022828D512AE007E12D2 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8DD2C4C2297B37BA00F984BF /* Packages */ = { + isa = PBXGroup; + children = ( + 8DD2C4C3297B37BA00F984BF /* FirezoneKit */, + ); + name = Packages; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05CF1CEF290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05CF1CFA290B1CEE00CF4755 /* Build configuration list for PBXNativeTarget "FirezoneNetworkExtensioniOS" */; + buildPhases = ( + 05CF1CEC290B1CEE00CF4755 /* Sources */, + 05CF1CED290B1CEE00CF4755 /* Frameworks */, + 05CF1CEE290B1CEE00CF4755 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FirezoneNetworkExtensioniOS; + packageProductDependencies = ( + 794C38142970A2660029F38F /* FirezoneKit */, + ); + productName = FirezoneNetworkExtensioniOS; + productReference = 05CF1CF0290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 05CF1D02290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05CF1D0D290B1DCD00CF4755 /* Build configuration list for PBXNativeTarget "FirezoneNetworkExtensionmacOS" */; + buildPhases = ( + 05CF1CFF290B1DCD00CF4755 /* Sources */, + 05CF1D00290B1DCD00CF4755 /* Frameworks */, + 05CF1D01290B1DCD00CF4755 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FirezoneNetworkExtensionmacOS; + packageProductDependencies = ( + 794C38162970A26A0029F38F /* FirezoneKit */, + ); + productName = FirezoneNetworkExtensionmacOS; + productReference = 05CF1D03290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 8DCC021828D512AC007E12D2 /* Firezone */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8DCC024328D512AE007E12D2 /* Build configuration list for PBXNativeTarget "Firezone" */; + buildPhases = ( + 8DCC021528D512AC007E12D2 /* Sources */, + 8DCC021628D512AC007E12D2 /* Frameworks */, + 8DCC021728D512AC007E12D2 /* Resources */, + 0556189428EF883500DF9E3C /* Embed Foundation Extensions */, + 05CF1C8A290B11A500CF4755 /* Embed System Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 05CF1CF8290B1CEE00CF4755 /* PBXTargetDependency */, + 05CF1D0B290B1DCD00CF4755 /* PBXTargetDependency */, + ); + name = Firezone; + packageProductDependencies = ( + 79756C6529704A7A0018E2D5 /* FirezoneKit */, + ); + productName = Firezone; + productReference = 8DCC021928D512AC007E12D2 /* Firezone.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8DCC021128D512AC007E12D2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1420; + TargetAttributes = { + 05CF1CEF290B1CEE00CF4755 = { + CreatedOnToolsVersion = 14.0.1; + }; + 05CF1D02290B1DCD00CF4755 = { + CreatedOnToolsVersion = 14.0.1; + }; + 8DCC021828D512AC007E12D2 = { + CreatedOnToolsVersion = 14.0; + LastSwiftMigration = 1400; + }; + }; + }; + buildConfigurationList = 8DCC021428D512AC007E12D2 /* Build configuration list for PBXProject "Firezone" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8DCC021028D512AC007E12D2; + packageReferences = ( + ); + productRefGroup = 8DCC021A28D512AC007E12D2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DCC021828D512AC007E12D2 /* Firezone */, + 05CF1CEF290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS */, + 05CF1D02290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05CF1CEE290B1CEE00CF4755 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CF1D01290B1DCD00CF4755 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DCC021728D512AC007E12D2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DCC022A28D512AE007E12D2 /* Preview Assets.xcassets in Resources */, + 8DCC022628D512AE007E12D2 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05CF1CEC290B1CEE00CF4755 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CF1CFF290B1DCD00CF4755 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CF1D16290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DCC021528D512AC007E12D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DCC021D28D512AC007E12D2 /* FirezoneApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 05CF1CF8290B1CEE00CF4755 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 05CF1CEF290B1CEE00CF4755 /* FirezoneNetworkExtensioniOS */; + targetProxy = 05CF1CF7290B1CEE00CF4755 /* PBXContainerItemProxy */; + }; + 05CF1D0B290B1DCD00CF4755 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilters = ( + macos, + ); + target = 05CF1D02290B1DCD00CF4755 /* FirezoneNetworkExtensionmacOS */; + targetProxy = 05CF1D0A290B1DCD00CF4755 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 05CF1CFB290B1CEE00CF4755 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FirezoneNetworkExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MARKETING_VERSION = 1.0; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = ""; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Debug; + }; + 05CF1CFC290B1CEE00CF4755 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FirezoneNetworkExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = ""; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Release; + }; + 05CF1D0E290B1DCD00CF4755 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FirezoneNetworkExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = ""; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Debug; + }; + 05CF1D0F290B1DCD00CF4755 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FirezoneNetworkExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = ""; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Release; + }; + 8DCC024128D512AE007E12D2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79C88B24296F494500261800 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8DCC024228D512AE007E12D2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79C88B24296F494500261800 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8DCC024428D512AE007E12D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Firezone/Firezone.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEFINES_MODULE = NO; + DEVELOPMENT_ASSET_PATHS = "\"Firezone/Preview Content\""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Firezone/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Firezone; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_LSUIElement = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = ""; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Debug; + }; + 8DCC024528D512AE007E12D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Firezone/Firezone.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEFINES_MODULE = NO; + DEVELOPMENT_ASSET_PATHS = "\"Firezone/Preview Content\""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Firezone/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Firezone; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_LSUIElement = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = NO; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = ""; + WATCHOS_DEPLOYMENT_TARGET = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05CF1CFA290B1CEE00CF4755 /* Build configuration list for PBXNativeTarget "FirezoneNetworkExtensioniOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05CF1CFB290B1CEE00CF4755 /* Debug */, + 05CF1CFC290B1CEE00CF4755 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05CF1D0D290B1DCD00CF4755 /* Build configuration list for PBXNativeTarget "FirezoneNetworkExtensionmacOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05CF1D0E290B1DCD00CF4755 /* Debug */, + 05CF1D0F290B1DCD00CF4755 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8DCC021428D512AC007E12D2 /* Build configuration list for PBXProject "Firezone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DCC024128D512AE007E12D2 /* Debug */, + 8DCC024228D512AE007E12D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8DCC024328D512AE007E12D2 /* Build configuration list for PBXNativeTarget "Firezone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DCC024428D512AE007E12D2 /* Debug */, + 8DCC024528D512AE007E12D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 794C38142970A2660029F38F /* FirezoneKit */ = { + isa = XCSwiftPackageProductDependency; + productName = FirezoneKit; + }; + 794C38162970A26A0029F38F /* FirezoneKit */ = { + isa = XCSwiftPackageProductDependency; + productName = FirezoneKit; + }; + 79756C6529704A7A0018E2D5 /* FirezoneKit */ = { + isa = XCSwiftPackageProductDependency; + productName = FirezoneKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 8DCC021128D512AC007E12D2 /* Project object */; +} diff --git a/swift/apple/Firezone.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/swift/apple/Firezone.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..0ffb69d6e --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,77 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "0625932976b3ae23949f6b816d13bd97f3b40b7c", + "version" : "0.10.0" + } + }, + { + "identity" : "jwtdecode.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/auth0/JWTDecode.swift", + "state" : { + "revision" : "7918a343e674c7707e0be120bb4e21d679be014c", + "version" : "3.0.1" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "f9acfa1a45f4483fe0f2c434a74e6f68f865d12d", + "version" : "0.3.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "84b30e1af72e0ffe6dfbfe39d53b8173caacf224", + "version" : "0.10.2" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "ad0a6a0dd4d4741263e798f4f5029589c9b5da73", + "version" : "0.4.2" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6", + "version" : "0.7.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", + "version" : "0.8.5" + } + } + ], + "version" : 2 +} diff --git a/swift/apple/Firezone.xcodeproj/xcshareddata/xcschemes/Firezone.xcscheme b/swift/apple/Firezone.xcodeproj/xcshareddata/xcschemes/Firezone.xcscheme new file mode 100644 index 000000000..9297a2f9b --- /dev/null +++ b/swift/apple/Firezone.xcodeproj/xcshareddata/xcschemes/Firezone.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift/apple/Firezone/Application/FirezoneApp.swift b/swift/apple/Firezone/Application/FirezoneApp.swift new file mode 100644 index 000000000..8e8a9fd54 --- /dev/null +++ b/swift/apple/Firezone/Application/FirezoneApp.swift @@ -0,0 +1,50 @@ +// +// FirezoneApp.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import FirezoneKit +import SwiftUI + +@main +struct FirezoneApp: App { + #if os(macOS) + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif + + #if os(iOS) + @StateObject var model = AppViewModel() + #endif + + var body: some Scene { + #if os(iOS) + WindowGroup { + AppView(model: model) + } + #else + WindowGroup("Settings") { + SettingsView(model: appDelegate.settingsViewModel) + } + .handlesExternalEvents(matching: ["settings"]) + #endif + } +} + +#if os(macOS) + @MainActor + final class AppDelegate: NSObject, NSApplicationDelegate { + let settingsViewModel = SettingsViewModel() + private var menuBar: MenuBar! + + func applicationDidFinishLaunching(_: Notification) { + menuBar = MenuBar(settingsViewModel: settingsViewModel) + + // SwiftUI will show the first window group, so close it on launch + let window = NSApp.windows[0] + window.close() + } + + func applicationWillTerminate(_: Notification) {} + } +#endif diff --git a/swift/apple/Firezone/Assets.xcassets/AccentColor.colorset/Contents.json b/swift/apple/Firezone/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..532cd729c --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/swift/apple/Firezone/Assets.xcassets/Contents.json b/swift/apple/Firezone/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/swift/apple/Firezone/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/swift/apple/Firezone/Config.xcconfig b/swift/apple/Firezone/Config.xcconfig new file mode 100644 index 000000000..f2ad4da32 --- /dev/null +++ b/swift/apple/Firezone/Config.xcconfig @@ -0,0 +1 @@ +#include "Developer.xcconfig" diff --git a/swift/apple/Firezone/Developer.xcconfig.ci-iOS b/swift/apple/Firezone/Developer.xcconfig.ci-iOS new file mode 100644 index 000000000..01a3f3b00 --- /dev/null +++ b/swift/apple/Firezone/Developer.xcconfig.ci-iOS @@ -0,0 +1,2 @@ +DEVELOPMENT_TEAM = 0000000000 +PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.ios diff --git a/swift/apple/Firezone/Developer.xcconfig.ci-macOS b/swift/apple/Firezone/Developer.xcconfig.ci-macOS new file mode 100644 index 000000000..f1fad4428 --- /dev/null +++ b/swift/apple/Firezone/Developer.xcconfig.ci-macOS @@ -0,0 +1,2 @@ +DEVELOPMENT_TEAM = 0000000000 +PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.macos diff --git a/swift/apple/Firezone/Developer.xcconfig.template b/swift/apple/Firezone/Developer.xcconfig.template new file mode 100644 index 000000000..e1fd00e2c --- /dev/null +++ b/swift/apple/Firezone/Developer.xcconfig.template @@ -0,0 +1,7 @@ +// You Apple developer account's Team ID +DEVELOPMENT_TEAM = + +// The bundle identifier of the apps. +// Should be an app id created at developer.apple.com +// with Network Extensions capability. +PRODUCT_BUNDLE_IDENTIFIER = diff --git a/swift/apple/Firezone/Firezone.entitlements b/swift/apple/Firezone/Firezone.entitlements new file mode 100644 index 000000000..441279cd4 --- /dev/null +++ b/swift/apple/Firezone/Firezone.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/swift/apple/Firezone/Firezone.xcdatamodeld/.xccurrentversion b/swift/apple/Firezone/Firezone.xcdatamodeld/.xccurrentversion new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/swift/apple/Firezone/Firezone.xcdatamodeld/.xccurrentversion @@ -0,0 +1,5 @@ + + + + + diff --git a/swift/apple/Firezone/Firezone.xcdatamodeld/Firezone.xcdatamodel/contents b/swift/apple/Firezone/Firezone.xcdatamodeld/Firezone.xcdatamodel/contents new file mode 100644 index 000000000..523d1805a --- /dev/null +++ b/swift/apple/Firezone/Firezone.xcdatamodeld/Firezone.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/swift/apple/Firezone/Info.plist b/swift/apple/Firezone/Info.plist new file mode 100644 index 000000000..3a8d4e241 --- /dev/null +++ b/swift/apple/Firezone/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + firezone + CFBundleURLSchemes + + firezone + + + + + diff --git a/swift/apple/Firezone/Preview Content/Preview Assets.xcassets/Contents.json b/swift/apple/Firezone/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/swift/apple/Firezone/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/swift/apple/FirezoneKit/.gitignore b/swift/apple/FirezoneKit/.gitignore new file mode 100644 index 000000000..3b2981208 --- /dev/null +++ b/swift/apple/FirezoneKit/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/swift/apple/FirezoneKit/Package.resolved b/swift/apple/FirezoneKit/Package.resolved new file mode 100644 index 000000000..1387b21f2 --- /dev/null +++ b/swift/apple/FirezoneKit/Package.resolved @@ -0,0 +1,86 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", + "version" : "0.9.1" + } + }, + { + "identity" : "jwtdecode.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/auth0/JWTDecode.swift", + "state" : { + "revision" : "7918a343e674c7707e0be120bb4e21d679be014c", + "version" : "3.0.1" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "870133b7b2387df136ad301ec67b2e864b51dda1", + "version" : "0.14.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "de8ba65649e7ee317b9daf27dd5eebf34bd4be57", + "version" : "0.9.1" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "6bb1034e8a1bfbf46dfb766b6c09b7b17e1cba10", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", + "version" : "1.5.2" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6", + "version" : "0.7.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", + "version" : "0.8.4" + } + } + ], + "version" : 2 +} diff --git a/swift/apple/FirezoneKit/Package.swift b/swift/apple/FirezoneKit/Package.swift new file mode 100644 index 000000000..07120381d --- /dev/null +++ b/swift/apple/FirezoneKit/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "FirezoneKit", + platforms: [.iOS(.v15), .macOS(.v12)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to + // other packages. + .library(name: "FirezoneKit", targets: ["FirezoneKit", "Connlib"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.1.3"), + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.5.0"), + .package(url: "https://github.com/auth0/JWTDecode.swift", from: "3.0.0"), + ], + targets: [ + .binaryTarget( + name: "Connlib", + path: "../../../rust/connlib/clients/apple/Connlib.xcframework" + ), + .target( + name: "FirezoneKit", + dependencies: [ + .product(name: "SwiftUINavigation", package: "swiftui-navigation"), + .product(name: "_SwiftUINavigationState", package: "swiftui-navigation"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "JWTDecode", package: "JWTDecode.swift"), + ] + ), + .testTarget( + name: "FirezoneKitTests", + dependencies: ["FirezoneKit"] + ), + ] +) diff --git a/swift/apple/FirezoneKit/README.md b/swift/apple/FirezoneKit/README.md new file mode 100644 index 000000000..0069d70d7 --- /dev/null +++ b/swift/apple/FirezoneKit/README.md @@ -0,0 +1,3 @@ +# FirezoneKit + +A description of this package. diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/AuthClient/AuthClient.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/AuthClient/AuthClient.swift new file mode 100644 index 000000000..de5ef3f3e --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/AuthClient/AuthClient.swift @@ -0,0 +1,105 @@ +// +// AuthClient.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import AuthenticationServices +import Dependencies +import Foundation +import JWTDecode + +enum AuthClientError: Error { + case invalidCallbackURL(URL?) + case jwtDecoderFailure(Error) + case sessionFailure(Error) +} + +struct AuthClient: Sendable { + var signIn: @Sendable (URL) async throws -> Token +} + +extension AuthClient: DependencyKey { + static var liveValue: AuthClient { + let session = WebAuthenticationSession() + return AuthClient( + signIn: { host in + try await session.signIn(host) + } + ) + } +} + +extension DependencyValues { + var auth: AuthClient { + get { self[AuthClient.self] } + set { self[AuthClient.self] = newValue } + } +} + +private final class WebAuthenticationSession: NSObject, + ASWebAuthenticationPresentationContextProviding +{ + @MainActor + func signIn(_ host: URL) async throws -> Token { + try await withCheckedThrowingContinuation { continuation in + let callbackURLScheme = "firezone-fd0020211111" + let session = ASWebAuthenticationSession( + url: host.appendingPathComponent("auth") + .appendingQueryItem(URLQueryItem(name: "dest", value: "\(callbackURLScheme)://auth")), + callbackURLScheme: callbackURLScheme + ) { callbackURL, error in + if let error { + continuation.resume(throwing: AuthClientError.sessionFailure(error)) + return + } + + guard let callbackURL else { + continuation.resume(throwing: AuthClientError.invalidCallbackURL(callbackURL)) + return + } + + guard + let jwt = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)? + .queryItems? + .first(where: { $0.name == "client_auth_token" })? + .value + else { + continuation.resume(throwing: AuthClientError.invalidCallbackURL(callbackURL)) + return + } + + do { + let token = try Token(portalURL: host, tokenString: jwt) + continuation.resume(returning: token) + } catch { + continuation.resume(throwing: AuthClientError.jwtDecoderFailure(error)) + } + } + + session.presentationContextProvider = self + session.prefersEphemeralWebBrowserSession = true + + session.start() + } + } + + func presentationAnchor(for _: ASWebAuthenticationSession) -> ASPresentationAnchor { + ASPresentationAnchor() + } +} + +extension URL { + func appendingQueryItem(_ queryItem: URLQueryItem) -> URL { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + if components.queryItems == nil { + components.queryItems = [] + } + + components.queryItems!.append(queryItem) + return components.url ?? self + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AppView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AppView.swift new file mode 100644 index 000000000..c9d408f69 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AppView.swift @@ -0,0 +1,52 @@ +// +// AppView.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import _SwiftUINavigationState +import Combine +import Dependencies +import SwiftUI +import SwiftUINavigation + +@MainActor +public final class AppViewModel: ObservableObject { + @Published var welcomeViewModel: WelcomeViewModel? + + public init() { + Task { + let tunnel = try await TunnelStore.loadOrCreate() + self.welcomeViewModel = WelcomeViewModel( + appStore: AppStore( + tunnelStore: TunnelStore( + tunnel: tunnel + ) + ) + ) + } + } +} + +public struct AppView: View { + @ObservedObject var model: AppViewModel + + public init(model: AppViewModel) { + self.model = model + } + + @ViewBuilder + public var body: some View { + if let model = model.welcomeViewModel { + WelcomeView(model: model) + } else { + ProgressView() + } + } +} + +struct AppView_Previews: PreviewProvider { + static var previews: some View { + AppView(model: AppViewModel()) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AuthView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AuthView.swift new file mode 100644 index 000000000..d46564c31 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/AuthView.swift @@ -0,0 +1,58 @@ +// +// AuthView.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import AuthenticationServices +import Combine +import Dependencies +import JWTDecode +import SwiftUI +import XCTestDynamicOverlay + +@MainActor +final class AuthViewModel: ObservableObject { + @Dependency(\.settingsClient) private var settingsClient + @Dependency(\.authStore) private var authStore + + var settingsUndefined: () -> Void = unimplemented("\(AuthViewModel.self).settingsUndefined") + + private var cancellables = Set() + + func logInButtonTapped() async { + guard let portalURL = settingsClient.fetchSettings()?.portalURL else { + settingsUndefined() + return + } + + do { + try await authStore.signIn(portalURL: portalURL) + } catch { + dump(error) + } + } +} + +struct AuthView: View { + @ObservedObject var model: AuthViewModel + + var body: some View { + VStack { + Text("Welcome to Firezone").font(.largeTitle) + + Button("Log in") { + Task { + await model.logInButtonTapped() + } + } + .buttonStyle(.borderedProminent) + } + } +} + +struct AuthView_Previews: PreviewProvider { + static var previews: some View { + AuthView(model: AuthViewModel()) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/MainView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/MainView.swift new file mode 100644 index 000000000..1a72d3e77 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/MainView.swift @@ -0,0 +1,93 @@ +// +// MainView.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Combine +import Dependencies +import NetworkExtension +import OSLog +import SwiftUI + +@MainActor +final class MainViewModel: ObservableObject { + private let logger = Logger.make(for: MainViewModel.self) + private var cancellables: Set = [] + + private let appStore: AppStore + + var token: Token? { + appStore.auth.token + } + + var status: NEVPNStatus { + appStore.tunnel.status + } + + init(appStore: AppStore) { + self.appStore = appStore + + appStore.objectWillChange + .sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + } + + func signOutButtonTapped() { + appStore.auth.signOut() + } + + func startTunnel() async { + do { + if let token = token { + try await appStore.tunnel.start(token: token) + } + } catch { + logger.error("Error starting tunnel: \(String(describing: error))") + } + } + + func stopTunnel() { + appStore.tunnel.stop() + } +} + +struct MainView: View { + @ObservedObject var model: MainViewModel + + var body: some View { + VStack(spacing: 56) { + VStack { + Text("Authenticated").font(.title) + Text(model.token?.user ?? "").foregroundColor(.secondary) + } + + Button("Sign out") { + model.signOutButtonTapped() + } + } + .toolbar { + ToolbarItem(placement: .principal) { + ConnectionSwitch( + status: model.status, + connect: { await model.startTunnel() }, + disconnect: { model.stopTunnel() } + ) + } + } + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView( + model: MainViewModel( + appStore: AppStore( + tunnelStore: TunnelStore( + tunnel: NETunnelProviderManager() + ) + ) + ) + ) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift new file mode 100644 index 000000000..862a4098b --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/SettingsView.swift @@ -0,0 +1,119 @@ +// +// SettingsView.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Dependencies +import SwiftUI +import XCTestDynamicOverlay + +public final class SettingsViewModel: ObservableObject { + @Dependency(\.settingsClient) private var settingsClient + + @Published var settings: Settings + + public var onSettingsSaved: () -> Void = unimplemented() + + public init() { + settings = Settings(portalURL: nil) + + if let storedSettings = settingsClient.fetchSettings() { + settings = storedSettings + } + } + + func saveButtonTapped() { + settingsClient.saveSettings(settings) + onSettingsSaved() + } +} + +public struct SettingsView: View { + @ObservedObject var model: SettingsViewModel + + public init(model: SettingsViewModel) { + self.model = model + } + + public var body: some View { + #if os(iOS) + ios + #elseif os(macOS) + mac + #else + #error("Unsupported platform") + #endif + } + + #if os(iOS) + private var ios: some View { + NavigationView { + form + } + } + #endif + + #if os(macOS) + private var mac: some View { + form + } + #endif + + private var form: some View { + Form { + Section { + FormTextField( + title: "Portal URL", + placeholder: "http://localhost:4567", + text: Binding( + get: { model.settings.portalURL?.absoluteString ?? "" }, + set: { model.settings.portalURL = URL(string: $0) } + ) + ) + } + } + .navigationTitle("Settings") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Save") { + model.saveButtonTapped() + } + } + } + } +} + +struct FormTextField: View { + let title: String + let placeholder: String + let text: Binding + + var body: some View { + #if os(iOS) + HStack { + Text(title) + Spacer() + TextField(placeholder, text: text) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + .textInputAutocapitalization(.never) + .textContentType(.URL) + .keyboardType(.URL) + } + #else + TextField(title, text: text, prompt: Text(placeholder)) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundColor(.secondary) + #endif + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView(model: SettingsViewModel()) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/WelcomeView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/WelcomeView.swift new file mode 100644 index 000000000..3ebce07fc --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Features/WelcomeView.swift @@ -0,0 +1,157 @@ +// +// WelcomeView.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import _SwiftUINavigationState +import Combine +import Dependencies +import SwiftUI +import SwiftUINavigation + +@MainActor +final class WelcomeViewModel: ObservableObject { + @Dependency(\.settingsClient) private var settingsClient + @Dependency(\.mainQueue) private var mainQueue + + private var cancellables = Set() + + enum Destination { + case settings(SettingsViewModel) + case undefinedSettingsAlert(AlertState) + } + + enum UndefinedSettingsAlertAction { + case confirmDefineSettingsButtonTapped + } + + enum State { + case unauthenticated(AuthViewModel) + case authenticated(MainViewModel) + } + + @Published var destination: Destination? { + didSet { + bindDestination() + } + } + + @Published var state: State? { + didSet { + bindState() + } + } + + private let appStore: AppStore + + init(appStore: AppStore) { + self.appStore = appStore + + appStore.objectWillChange + .receive(on: mainQueue) + .sink { [weak self] in self?.objectWillChange.send() } + .store(in: &cancellables) + + defer { bindDestination() } + + if settingsClient.fetchSettings()?.portalURL == nil { + destination = .undefinedSettingsAlert(.undefinedSettings) + } + + appStore.auth.$token + .receive(on: mainQueue) + .sink(receiveValue: { [weak self] token in + guard let self else { + return + } + + if token != nil { + self.state = .authenticated(MainViewModel(appStore: self.appStore)) + } else { + self.state = .unauthenticated(AuthViewModel()) + } + }) + .store(in: &cancellables) + } + + func settingsButtonTapped() { + destination = .settings(SettingsViewModel()) + } + + func handleUndefinedSettingsAlertAction(_ action: UndefinedSettingsAlertAction) { + switch action { + case .confirmDefineSettingsButtonTapped: + destination = .settings(SettingsViewModel()) + } + } + + private func bindDestination() { + switch destination { + case let .settings(model): + model.onSettingsSaved = { [weak self] in + self?.destination = nil + self?.state = .unauthenticated(AuthViewModel()) + } + + case .undefinedSettingsAlert, .none: + break + } + } + + private func bindState() { + switch state { + case let .unauthenticated(model): + model.settingsUndefined = { [weak self] in + self?.destination = .undefinedSettingsAlert(.undefinedSettings) + } + + case .authenticated, .none: + break + } + } +} + +struct WelcomeView: View { + @ObservedObject var model: WelcomeViewModel + + var body: some View { + NavigationView { + Group { + switch model.state { + case let .unauthenticated(model): + AuthView(model: model) + case let .authenticated(model): + MainView(model: model) + case .none: + ProgressView() + } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + model.settingsButtonTapped() + } label: { + Label("Settings", systemImage: "gear") + } + } + } + } + .sheet(unwrapping: $model.destination, case: /WelcomeViewModel.Destination.settings) { $model in + SettingsView(model: model) + } + .alert( + unwrapping: $model.destination, + case: /WelcomeViewModel.Destination.undefinedSettingsAlert, + action: model.handleUndefinedSettingsAlertAction + ) + } +} + +struct WelcomeView_Previews: PreviewProvider { + static var previews: some View { + WelcomeView( + model: WelcomeViewModel(appStore: AppStore(tunnelStore: TunnelStore(tunnel: .init()))) + ) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Logger.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Logger.swift new file mode 100644 index 000000000..3891ddadb --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Logger.swift @@ -0,0 +1,19 @@ +// +// Logger.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Dependencies +import Foundation +import OSLog + +extension Logger { + static func make(for type: (some Any).Type) -> Logger { + make(category: String(describing: type)) + } + + static func make(category: String) -> Logger { + Logger(subsystem: Bundle.main.bundleIdentifier ?? "dev.firezone.firezone", category: category) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Optional+Unwrap.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Optional+Unwrap.swift new file mode 100644 index 000000000..93d06657b --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Optional+Unwrap.swift @@ -0,0 +1,19 @@ +// +// Optional+Unwrap.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +extension Optional { + /// Unwraps `self` or throws error if `none`. + /// - Parameter error: Error to thrown in case of nil value. + /// - Returns: The wrapped optional value. + func unwrap(throwing error: @autoclosure () -> Error) throws -> Wrapped { + guard let self else { + throw error() + } + return self + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain+Token.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain+Token.swift new file mode 100644 index 000000000..e1d36ca13 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain+Token.swift @@ -0,0 +1,28 @@ +// +// Keychain+Token.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import JWTDecode + +extension KeychainStorage { + static let tokenKey = "token" + + func tokenString() async throws -> String? { + let jwt = try await load(KeychainStorage.tokenKey).flatMap { data in + String(data: data, encoding: .utf8) + } + + guard let jwt else { return nil } + return jwt + } + + func save(tokenString: String) async throws { + try await store(KeychainStorage.tokenKey, tokenString.data(using: .utf8)!) + } + + func deleteTokenString() async throws { + try await delete(KeychainStorage.tokenKey) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift new file mode 100644 index 000000000..9f722caa5 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift @@ -0,0 +1,98 @@ +// +// Keychain.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +enum KeychainError: Error { + case securityError(Status) +} + +actor Keychain { + private static let account = "Firezone" + + func store(key: String, data: Data) throws { + let query = ([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: getServiceIdentifier(key), + kSecAttrAccount: Keychain.account, + kSecValueData: data, + ] as [CFString: Any]) as CFDictionary + + let status = SecItemAdd(query, nil) + + if status == Status.duplicateItem { + try update(key: key, data: data) + } else if status != Status.success { + throw securityError(status) + } + } + + func update(key: String, data: Data) throws { + let query = ([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: getServiceIdentifier(key), + kSecAttrAccount: Keychain.account, + ] as [CFString: Any]) as CFDictionary + + let updatedData = [kSecValueData: data] as CFDictionary + + let status = SecItemUpdate(query, updatedData) + + if status != Status.success { + throw securityError(status) + } + } + + func load(key: String) throws -> Data? { + let query = ([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: getServiceIdentifier(key), + kSecAttrAccount: Keychain.account, + kSecReturnData: kCFBooleanTrue!, + kSecMatchLimit: kSecMatchLimitOne, + ] as [CFString: Any]) as CFDictionary + + var data: AnyObject? + + let status = SecItemCopyMatching(query, &data) + + if status == Status.success { + return data as? Data + } else if status == Status.itemNotFound { + return nil + } else { + throw securityError(status) + } + } + + func delete(key: String) throws { + let query = ([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: getServiceIdentifier(key), + kSecAttrAccount: Keychain.account, + ] as [CFString: Any]) as CFDictionary + + let status = SecItemDelete(query) + + if status != Status.success { + throw securityError(status) + } + } + + private func getServiceIdentifier(_ key: String) -> String { + var bundleIdentifier = Bundle.main.bundleIdentifier ?? "dev.firezone.firezone" + + if bundleIdentifier.hasSuffix(".network-extension") { + bundleIdentifier.removeLast(".network-extension".count) + } + + return bundleIdentifier + "." + key + } + + private func securityError(_ status: OSStatus) -> Error { + KeychainError.securityError(Status(rawValue: status)!) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStorage.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStorage.swift new file mode 100644 index 000000000..a379af9a2 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStorage.swift @@ -0,0 +1,50 @@ +// +// KeychainStorage.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Dependencies +import Foundation + +struct KeychainStorage: Sendable { + var store: @Sendable (String, Data) async throws -> Void + var load: @Sendable (String) async throws -> Data? + var delete: @Sendable (String) async throws -> Void +} + +extension KeychainStorage: DependencyKey { + static var liveValue: KeychainStorage { + let keychain = Keychain() + + return KeychainStorage( + store: { try await keychain.store(key: $0, data: $1) }, + load: { try await keychain.load(key: $0) }, + delete: { try await keychain.delete(key: $0) } + ) + } + + static var testValue: KeychainStorage { + let storage = LockIsolated([String: Data]()) + return KeychainStorage( + store: { key, data in + storage.withValue { + $0[key] = data + } + }, + load: { storage.value[$0] }, + delete: { key in + storage.withValue { + $0[key] = nil + } + } + ) + } +} + +extension DependencyValues { + var keychain: KeychainStorage { + get { self[KeychainStorage.self] } + set { self[KeychainStorage.self] = newValue } + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Status.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Status.swift new file mode 100644 index 000000000..f407705c7 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Status.swift @@ -0,0 +1,431 @@ +// +// Status.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +// swiftlint:disable type_body_length +// swiftlint:disable file_length +// swiftlint:disable identifier_name + +// Copyright (c) 2014 kishikawa katsumi +// The MIT License (MIT) +// https://github.com/kishikawakatsumi/KeychainAccess + +import Foundation + +public enum Status: OSStatus, Error { + case success = 0 + case unimplemented = -4 + case diskFull = -34 + case io = -36 + case opWr = -49 + case param = -50 + case wrPerm = -61 + case allocate = -108 + case userCanceled = -128 + case badReq = -909 + case internalComponent = -2070 + case notAvailable = -25291 + case readOnly = -25292 + case authFailed = -25293 + case noSuchKeychain = -25294 + case invalidKeychain = -25295 + case duplicateKeychain = -25296 + case duplicateCallback = -25297 + case invalidCallback = -25298 + case duplicateItem = -25299 + case itemNotFound = -25300 + case bufferTooSmall = -25301 + case dataTooLarge = -25302 + case noSuchAttr = -25303 + case invalidItemRef = -25304 + case invalidSearchRef = -25305 + case noSuchClass = -25306 + case noDefaultKeychain = -25307 + case interactionNotAllowed = -25308 + case readOnlyAttr = -25309 + case wrongSecVersion = -25310 + case keySizeNotAllowed = -25311 + case noStorageModule = -25312 + case noCertificateModule = -25313 + case noPolicyModule = -25314 + case interactionRequired = -25315 + case dataNotAvailable = -25316 + case dataNotModifiable = -25317 + case createChainFailed = -25318 + case invalidPrefsDomain = -25319 + case inDarkWake = -25320 + case aclNotSimple = -25240 + case policyNotFound = -25241 + case invalidTrustSetting = -25242 + case noAccessForItem = -25243 + case invalidOwnerEdit = -25244 + case trustNotAvailable = -25245 + case unsupportedFormat = -25256 + case unknownFormat = -25257 + case keyIsSensitive = -25258 + case multiplePrivKeys = -25259 + case passphraseRequired = -25260 + case invalidPasswordRef = -25261 + case invalidTrustSettings = -25262 + case noTrustSettings = -25263 + case pkcs12VerifyFailure = -25264 + case invalidCertificate = -26265 + case notSigner = -26267 + case policyDenied = -26270 + case invalidKey = -26274 + case decode = -26275 + case `internal` = -26276 + case unsupportedAlgorithm = -26268 + case unsupportedOperation = -26271 + case unsupportedPadding = -26273 + case itemInvalidKey = -34000 + case itemInvalidKeyType = -34001 + case itemInvalidValue = -34002 + case itemClassMissing = -34003 + case itemMatchUnsupported = -34004 + case useItemListUnsupported = -34005 + case useKeychainUnsupported = -34006 + case useKeychainListUnsupported = -34007 + case returnDataUnsupported = -34008 + case returnAttributesUnsupported = -34009 + case returnRefUnsupported = -34010 + case returnPersitentRefUnsupported = -34011 + case valueRefUnsupported = -34012 + case valuePersistentRefUnsupported = -34013 + case returnMissingPointer = -34014 + case matchLimitUnsupported = -34015 + case itemIllegalQuery = -34016 + case waitForCallback = -34017 + case missingEntitlement = -34018 + case upgradePending = -34019 + case mpSignatureInvalid = -25327 + case otrTooOld = -25328 + case otrIDTooNew = -25329 + case serviceNotAvailable = -67585 + case insufficientClientID = -67586 + case deviceReset = -67587 + case deviceFailed = -67588 + case appleAddAppACLSubject = -67589 + case applePublicKeyIncomplete = -67590 + case appleSignatureMismatch = -67591 + case appleInvalidKeyStartDate = -67592 + case appleInvalidKeyEndDate = -67593 + case conversionError = -67594 + case appleSSLv2Rollback = -67595 + case quotaExceeded = -67596 + case fileTooBig = -67597 + case invalidDatabaseBlob = -67598 + case invalidKeyBlob = -67599 + case incompatibleDatabaseBlob = -67600 + case incompatibleKeyBlob = -67601 + case hostNameMismatch = -67602 + case unknownCriticalExtensionFlag = -67603 + case noBasicConstraints = -67604 + case noBasicConstraintsCA = -67605 + case invalidAuthorityKeyID = -67606 + case invalidSubjectKeyID = -67607 + case invalidKeyUsageForPolicy = -67608 + case invalidExtendedKeyUsage = -67609 + case invalidIDLinkage = -67610 + case pathLengthConstraintExceeded = -67611 + case invalidRoot = -67612 + case crlExpired = -67613 + case crlNotValidYet = -67614 + case crlNotFound = -67615 + case crlServerDown = -67616 + case crlBadURI = -67617 + case unknownCertExtension = -67618 + case unknownCRLExtension = -67619 + case crlNotTrusted = -67620 + case crlPolicyFailed = -67621 + case idpFailure = -67622 + case smimeEmailAddressesNotFound = -67623 + case smimeBadExtendedKeyUsage = -67624 + case smimeBadKeyUsage = -67625 + case smimeKeyUsageNotCritical = -67626 + case smimeNoEmailAddress = -67627 + case smimeSubjAltNameNotCritical = -67628 + case sslBadExtendedKeyUsage = -67629 + case ocspBadResponse = -67630 + case ocspBadRequest = -67631 + case ocspUnavailable = -67632 + case ocspStatusUnrecognized = -67633 + case endOfData = -67634 + case incompleteCertRevocationCheck = -67635 + case networkFailure = -67636 + case ocspNotTrustedToAnchor = -67637 + case recordModified = -67638 + case ocspSignatureError = -67639 + case ocspNoSigner = -67640 + case ocspResponderMalformedReq = -67641 + case ocspResponderInternalError = -67642 + case ocspResponderTryLater = -67643 + case ocspResponderSignatureRequired = -67644 + case ocspResponderUnauthorized = -67645 + case ocspResponseNonceMismatch = -67646 + case codeSigningBadCertChainLength = -67647 + case codeSigningNoBasicConstraints = -67648 + case codeSigningBadPathLengthConstraint = -67649 + case codeSigningNoExtendedKeyUsage = -67650 + case codeSigningDevelopment = -67651 + case resourceSignBadCertChainLength = -67652 + case resourceSignBadExtKeyUsage = -67653 + case trustSettingDeny = -67654 + case invalidSubjectName = -67655 + case unknownQualifiedCertStatement = -67656 + case mobileMeRequestQueued = -67657 + case mobileMeRequestRedirected = -67658 + case mobileMeServerError = -67659 + case mobileMeServerNotAvailable = -67660 + case mobileMeServerAlreadyExists = -67661 + case mobileMeServerServiceErr = -67662 + case mobileMeRequestAlreadyPending = -67663 + case mobileMeNoRequestPending = -67664 + case mobileMeCSRVerifyFailure = -67665 + case mobileMeFailedConsistencyCheck = -67666 + case notInitialized = -67667 + case invalidHandleUsage = -67668 + case pvcReferentNotFound = -67669 + case functionIntegrityFail = -67670 + case internalError = -67671 + case memoryError = -67672 + case invalidData = -67673 + case mdsError = -67674 + case invalidPointer = -67675 + case selfCheckFailed = -67676 + case functionFailed = -67677 + case moduleManifestVerifyFailed = -67678 + case invalidGUID = -67679 + case invalidHandle = -67680 + case invalidDBList = -67681 + case invalidPassthroughID = -67682 + case invalidNetworkAddress = -67683 + case crlAlreadySigned = -67684 + case invalidNumberOfFields = -67685 + case verificationFailure = -67686 + case unknownTag = -67687 + case invalidSignature = -67688 + case invalidName = -67689 + case invalidCertificateRef = -67690 + case invalidCertificateGroup = -67691 + case tagNotFound = -67692 + case invalidQuery = -67693 + case invalidValue = -67694 + case callbackFailed = -67695 + case aclDeleteFailed = -67696 + case aclReplaceFailed = -67697 + case aclAddFailed = -67698 + case aclChangeFailed = -67699 + case invalidAccessCredentials = -67700 + case invalidRecord = -67701 + case invalidACL = -67702 + case invalidSampleValue = -67703 + case incompatibleVersion = -67704 + case privilegeNotGranted = -67705 + case invalidScope = -67706 + case pvcAlreadyConfigured = -67707 + case invalidPVC = -67708 + case emmLoadFailed = -67709 + case emmUnloadFailed = -67710 + case addinLoadFailed = -67711 + case invalidKeyRef = -67712 + case invalidKeyHierarchy = -67713 + case addinUnloadFailed = -67714 + case libraryReferenceNotFound = -67715 + case invalidAddinFunctionTable = -67716 + case invalidServiceMask = -67717 + case moduleNotLoaded = -67718 + case invalidSubServiceID = -67719 + case attributeNotInContext = -67720 + case moduleManagerInitializeFailed = -67721 + case moduleManagerNotFound = -67722 + case eventNotificationCallbackNotFound = -67723 + case inputLengthError = -67724 + case outputLengthError = -67725 + case privilegeNotSupported = -67726 + case deviceError = -67727 + case attachHandleBusy = -67728 + case notLoggedIn = -67729 + case algorithmMismatch = -67730 + case keyUsageIncorrect = -67731 + case keyBlobTypeIncorrect = -67732 + case keyHeaderInconsistent = -67733 + case unsupportedKeyFormat = -67734 + case unsupportedKeySize = -67735 + case invalidKeyUsageMask = -67736 + case unsupportedKeyUsageMask = -67737 + case invalidKeyAttributeMask = -67738 + case unsupportedKeyAttributeMask = -67739 + case invalidKeyLabel = -67740 + case unsupportedKeyLabel = -67741 + case invalidKeyFormat = -67742 + case unsupportedVectorOfBuffers = -67743 + case invalidInputVector = -67744 + case invalidOutputVector = -67745 + case invalidContext = -67746 + case invalidAlgorithm = -67747 + case invalidAttributeKey = -67748 + case missingAttributeKey = -67749 + case invalidAttributeInitVector = -67750 + case missingAttributeInitVector = -67751 + case invalidAttributeSalt = -67752 + case missingAttributeSalt = -67753 + case invalidAttributePadding = -67754 + case missingAttributePadding = -67755 + case invalidAttributeRandom = -67756 + case missingAttributeRandom = -67757 + case invalidAttributeSeed = -67758 + case missingAttributeSeed = -67759 + case invalidAttributePassphrase = -67760 + case missingAttributePassphrase = -67761 + case invalidAttributeKeyLength = -67762 + case missingAttributeKeyLength = -67763 + case invalidAttributeBlockSize = -67764 + case missingAttributeBlockSize = -67765 + case invalidAttributeOutputSize = -67766 + case missingAttributeOutputSize = -67767 + case invalidAttributeRounds = -67768 + case missingAttributeRounds = -67769 + case invalidAlgorithmParms = -67770 + case missingAlgorithmParms = -67771 + case invalidAttributeLabel = -67772 + case missingAttributeLabel = -67773 + case invalidAttributeKeyType = -67774 + case missingAttributeKeyType = -67775 + case invalidAttributeMode = -67776 + case missingAttributeMode = -67777 + case invalidAttributeEffectiveBits = -67778 + case missingAttributeEffectiveBits = -67779 + case invalidAttributeStartDate = -67780 + case missingAttributeStartDate = -67781 + case invalidAttributeEndDate = -67782 + case missingAttributeEndDate = -67783 + case invalidAttributeVersion = -67784 + case missingAttributeVersion = -67785 + case invalidAttributePrime = -67786 + case missingAttributePrime = -67787 + case invalidAttributeBase = -67788 + case missingAttributeBase = -67789 + case invalidAttributeSubprime = -67790 + case missingAttributeSubprime = -67791 + case invalidAttributeIterationCount = -67792 + case missingAttributeIterationCount = -67793 + case invalidAttributeDLDBHandle = -67794 + case missingAttributeDLDBHandle = -67795 + case invalidAttributeAccessCredentials = -67796 + case missingAttributeAccessCredentials = -67797 + case invalidAttributePublicKeyFormat = -67798 + case missingAttributePublicKeyFormat = -67799 + case invalidAttributePrivateKeyFormat = -67800 + case missingAttributePrivateKeyFormat = -67801 + case invalidAttributeSymmetricKeyFormat = -67802 + case missingAttributeSymmetricKeyFormat = -67803 + case invalidAttributeWrappedKeyFormat = -67804 + case missingAttributeWrappedKeyFormat = -67805 + case stagedOperationInProgress = -67806 + case stagedOperationNotStarted = -67807 + case verifyFailed = -67808 + case querySizeUnknown = -67809 + case blockSizeMismatch = -67810 + case publicKeyInconsistent = -67811 + case deviceVerifyFailed = -67812 + case invalidLoginName = -67813 + case alreadyLoggedIn = -67814 + case invalidDigestAlgorithm = -67815 + case invalidCRLGroup = -67816 + case certificateCannotOperate = -67817 + case certificateExpired = -67818 + case certificateNotValidYet = -67819 + case certificateRevoked = -67820 + case certificateSuspended = -67821 + case insufficientCredentials = -67822 + case invalidAction = -67823 + case invalidAuthority = -67824 + case verifyActionFailed = -67825 + case invalidCertAuthority = -67826 + case invaldCRLAuthority = -67827 + case invalidCRLEncoding = -67828 + case invalidCRLType = -67829 + case invalidCRL = -67830 + case invalidFormType = -67831 + case invalidID = -67832 + case invalidIdentifier = -67833 + case invalidIndex = -67834 + case invalidPolicyIdentifiers = -67835 + case invalidTimeString = -67836 + case invalidReason = -67837 + case invalidRequestInputs = -67838 + case invalidResponseVector = -67839 + case invalidStopOnPolicy = -67840 + case invalidTuple = -67841 + case multipleValuesUnsupported = -67842 + case notTrusted = -67843 + case noDefaultAuthority = -67844 + case rejectedForm = -67845 + case requestLost = -67846 + case requestRejected = -67847 + case unsupportedAddressType = -67848 + case unsupportedService = -67849 + case invalidTupleGroup = -67850 + case invalidBaseACLs = -67851 + case invalidTupleCredendtials = -67852 + case invalidEncoding = -67853 + case invalidValidityPeriod = -67854 + case invalidRequestor = -67855 + case requestDescriptor = -67856 + case invalidBundleInfo = -67857 + case invalidCRLIndex = -67858 + case noFieldValues = -67859 + case unsupportedFieldFormat = -67860 + case unsupportedIndexInfo = -67861 + case unsupportedLocality = -67862 + case unsupportedNumAttributes = -67863 + case unsupportedNumIndexes = -67864 + case unsupportedNumRecordTypes = -67865 + case fieldSpecifiedMultiple = -67866 + case incompatibleFieldFormat = -67867 + case invalidParsingModule = -67868 + case databaseLocked = -67869 + case datastoreIsOpen = -67870 + case missingValue = -67871 + case unsupportedQueryLimits = -67872 + case unsupportedNumSelectionPreds = -67873 + case unsupportedOperator = -67874 + case invalidDBLocation = -67875 + case invalidAccessRequest = -67876 + case invalidIndexInfo = -67877 + case invalidNewOwner = -67878 + case invalidModifyMode = -67879 + case missingRequiredExtension = -67880 + case extendedKeyUsageNotCritical = -67881 + case timestampMissing = -67882 + case timestampInvalid = -67883 + case timestampNotTrusted = -67884 + case timestampServiceNotAvailable = -67885 + case timestampBadAlg = -67886 + case timestampBadRequest = -67887 + case timestampBadDataFormat = -67888 + case timestampTimeNotAvailable = -67889 + case timestampUnacceptedPolicy = -67890 + case timestampUnacceptedExtension = -67891 + case timestampAddInfoNotAvailable = -67892 + case timestampSystemFailure = -67893 + case signingTimeMissing = -67894 + case timestampRejection = -67895 + case timestampWaiting = -67896 + case timestampRevocationWarning = -67897 + case timestampRevocationNotification = -67898 + case unexpectedError = -99999 +} + +extension Status { + static func == (lhs: OSStatus, rhs: Status) -> Bool { + lhs == rhs.rawValue + } + + static func != (lhs: OSStatus, rhs: Status) -> Bool { + lhs != rhs.rawValue + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneError.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneError.swift new file mode 100644 index 000000000..ee2efca91 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneError.swift @@ -0,0 +1,11 @@ +// +// FirezoneError.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +enum FirezoneError: Error { + case missingPortalURL +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift new file mode 100644 index 000000000..13e7727fe --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift @@ -0,0 +1,59 @@ +// +// Token.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation +import JWTDecode + +struct Token { + // The user associated with this token parsed from the jwt sub claim. + var user: String? { jwt["sub"].string } + + // The VPN session duration parsed from the jwt exp claim. + var expiresAt: Date? { jwt["exp"].date } + + // A convenience property to check if the token has expired. + var expired: Bool { jwt.expired } + + // The base64 encoded jwt string. + var string: String { jwt.string } + + // The portal URL + let portalURL: URL + + // The decoded jwt. + private let jwt: JWT + + init(portalURL: URL, tokenString: String) throws { + self.portalURL = portalURL + self.jwt = try decode(jwt: tokenString) + } +} + +extension Token: Hashable { + static func == (lhs: Token, rhs: Token) -> Bool { + lhs.string == rhs.string + } + + func hash(into hasher: inout Hasher) { + string.hash(into: &hasher) + } +} + +#if DEBUG + extension Token { + static let expired = + try! Token( + portalURL: URL(string: "http://localhost:4568")!, + tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGZpcmV6b25lLmRldiIsImV4cCI6MTY2NzE0MDU1NX0.YLLQyfX6AlHQb90AMRrBTbvBuRxBzVYe0YwohfcD0r7KdBmR5Y-AcP0eYVC2DK-MSSJVjzs2j7SMPCvwJRc1z0LX_U4PkHjL5HPUIb3_rE1MIP8Hn8Ng5mk6SaTj6EJm3qTmm44bPiy21kntcqp-b9CSFqwc1IQHVHXnbcqcv4sVit2sTXJSNvNRRtO8ZTsC007T9skYBGVfCI-kSFyxQe9CoPQxYzFF8KKtCqmmT-t5g0et78IcwToOYeCxc0zOe14OQFadDZabmvJ_xfvC4iRKPfbOyQfNQqIQ_xh3iaGry2iSD4yMALKvgA7Ij4Ixz0GEnyqEfvOeCRA1UoFcLg" + ) + + static let valid = + try! Token( + portalURL: URL(string: "http://localhost:4568")!, + tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGZpcmV6b25lLmRldiIsImV4cCI6MTY2OTk0MDU1NX0.cLkLdIUBF5FH9e32yBPcfOXun11iYhtbxsVdqlZt3U5J-CYfBNikg5jbG6N7h2BYCBjScnmpu7G249la-lahO7IZWM3qil4NNyhKMZxzA_3cgC3362MBX-7xmlpxjR61b_yE1wOfGjjm_xOKIUTfkkfyTGxvQkXecdbOgpZ7WV4PkG7QD-JHgaQvIQCMbQuC5-d225z6rC43itiRxkq5mRet1d6N5VPxq1tdOD4N7mMs9I1NjJpFGcmJw8r8tlik8r7oxlJpmZN6aw4wcHV3gMd_Dmm-ZYW8M6aVvOpNIabi4hvB0Snqx-w4SqHy1stjfm-vHiGfusucX4G4igk4Qw" + ) + } +#endif diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/Settings.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/Settings.swift new file mode 100644 index 000000000..6ee6fdec7 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/Settings.swift @@ -0,0 +1,11 @@ +// +// Settings.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +struct Settings: Codable, Hashable { + var portalURL: URL? +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/SettingsClient.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/SettingsClient.swift new file mode 100644 index 000000000..2e6841fd7 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/SettingsClient/SettingsClient.swift @@ -0,0 +1,44 @@ +// +// SettingsClient.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Dependencies +import Foundation + +struct SettingsClient { + var fetchSettings: () -> Settings? + var saveSettings: (Settings?) -> Void +} + +extension SettingsClient: DependencyKey { + static let liveValue = SettingsClient( + fetchSettings: { + guard let data = UserDefaults.standard.data(forKey: "settings") else { + return nil + } + + return try? JSONDecoder().decode(Settings.self, from: data) + }, + saveSettings: { settings in + let data = try? JSONEncoder().encode(settings) + UserDefaults.standard.set(data, forKey: "settings") + } + ) + + static var testValue: SettingsClient { + let settings = LockIsolated(Settings?.none) + return SettingsClient( + fetchSettings: { settings.value }, + saveSettings: { settings.setValue($0) } + ) + } +} + +extension DependencyValues { + var settingsClient: SettingsClient { + get { self[SettingsClient.self] } + set { self[SettingsClient.self] = newValue } + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AppStore.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AppStore.swift new file mode 100644 index 000000000..e8af9e7ac --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AppStore.swift @@ -0,0 +1,60 @@ +// +// AppStore.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Combine +import Dependencies +import OSLog + +@MainActor +final class AppStore: ObservableObject { + private let logger = Logger.make(for: AppStore.self) + + @Dependency(\.authStore) var auth + @Dependency(\.mainQueue) var mainQueue + + let tunnel: TunnelStore + private var cancellables: Set = [] + + init(tunnelStore: TunnelStore) { + tunnel = tunnelStore + + Publishers.Merge( + auth.objectWillChange, + tunnel.objectWillChange + ) + .receive(on: mainQueue) + .sink { [weak self] in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + auth.$token + .receive(on: mainQueue) + .sink { [weak self] token in + Task { [weak self] in + await self?.handleTokenChanged(token) + } + } + .store(in: &cancellables) + } + + private func handleTokenChanged(_ token: Token?) async { + if let token = token { + do { + try await tunnel.start(token: token) + } catch { + logger.error("Error starting tunnel: \(String(describing: error))") + } + } else { + tunnel.stop() + } + } + + private func signOutAndStopTunnel() { + tunnel.stop() + auth.signOut() + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AuthStore.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AuthStore.swift new file mode 100644 index 000000000..df3997c2d --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/AuthStore.swift @@ -0,0 +1,92 @@ +// +// AuthStore.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Combine +import Dependencies +import Foundation +import OSLog + +extension AuthStore: DependencyKey { + static var liveValue: AuthStore = .shared +} + +extension DependencyValues { + var authStore: AuthStore { + get { self[AuthStore.self] } + set { self[AuthStore.self] = newValue } + } +} + +@MainActor +final class AuthStore: ObservableObject { + private let logger = Logger.make(for: AuthStore.self) + + static let shared = AuthStore() + + @Dependency(\.keychain) private var keychain + @Dependency(\.auth) private var auth + @Dependency(\.settingsClient) private var settingsClient + + private var cancellables = Set() + + @Published private(set) var token: Token? + + private init() { + Task { + self.token = await { + guard let portalURL = settingsClient.fetchSettings()?.portalURL else { + logger.debug("No portal URL found in settings") + return nil + } + guard let tokenString = try? await keychain.tokenString() else { + logger.debug("Token string not found in keychain") + return nil + } + guard let token = try? Token(portalURL: portalURL, tokenString: tokenString) else { + logger.debug("Token string recovered from keychain is invalid") + return nil + } + logger.debug("Token recovered from keychain.") + return token + }() + } + + $token.dropFirst() + .sink { [weak self] token in + Task { [weak self] in + if let token { + try? await self?.keychain.save(tokenString: token.string) + self?.logger.debug("token saved on keychain.") + } else { + try? await self?.keychain.deleteTokenString() + self?.logger.debug("token deleted from keychain.") + } + } + } + .store(in: &cancellables) + } + + func signIn(portalURL: URL) async throws { + logger.trace("\(#function)") + + let token = try await auth.signIn(portalURL) + self.token = token + } + + func signIn() async throws { + logger.trace("\(#function)") + + let portalURL = try settingsClient.fetchSettings().flatMap(\.portalURL) + .unwrap(throwing: FirezoneError.missingPortalURL) + try await signIn(portalURL: portalURL) + } + + func signOut() { + logger.trace("\(#function)") + + token = nil + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift new file mode 100644 index 000000000..7b89a742a --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/TunnelStore.swift @@ -0,0 +1,148 @@ +// +// TunnelStore.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Combine +import Foundation +import NetworkExtension +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) + + var tunnel: NETunnelProviderManager { + didSet { setupTunnelObservers() } + } + + @Published private(set) var status: NEVPNStatus = .invalid { + didSet { TunnelStore.logger.info("status changed: \(self.status.description)") } + } + + @Published private(set) var isEnabled = false { + didSet { TunnelStore.logger.info("isEnabled changed: \(self.isEnabled.description)") } + } + + private var tunnelObservingTasks: [Task] = [] + + init(tunnel: NETunnelProviderManager) { + self.tunnel = tunnel + tunnel.isEnabled = true + setupTunnelObservers() + } + + static func loadOrCreate() async throws -> NETunnelProviderManager { + logger.trace("\(#function)") + + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + + if let tunnel = managers.first { + return tunnel + } + + let tunnel = makeManager() + try await tunnel.saveToPreferences() + try await tunnel.loadFromPreferences() + + return tunnel + } + + func start(token: Token) async throws { + TunnelStore.logger.trace("\(#function)") + + // make sure we have latest preferences before starting + try await tunnel.loadFromPreferences() + + tunnel.protocolConfiguration = Self.makeProtocolConfiguration(token: token) + tunnel.isEnabled = true + try await tunnel.saveToPreferences() + + let session = tunnel.connection as! NETunnelProviderSession + try session.startTunnel() + } + + func stop() { + TunnelStore.logger.trace("\(#function)") + let session = tunnel.connection as! NETunnelProviderSession + session.stopTunnel() + } + + private static func makeManager() -> NETunnelProviderManager { + logger.trace("\(#function)") + + let manager = NETunnelProviderManager() + manager.localizedDescription = "Firezone" + + let proto = makeProtocolConfiguration() + manager.protocolConfiguration = proto + manager.isEnabled = true + + return manager + } + + private static func makeProtocolConfiguration(token: Token? = nil) -> NETunnelProviderProtocol { + let proto = NETunnelProviderProtocol() + + proto.providerBundleIdentifier = Bundle.main.bundleIdentifier.map { + "\($0).network-extension" + } + if let token = token { + proto.providerConfiguration = [ + "portalURL": token.portalURL.absoluteString, + "token": token.string + ] + } + proto.serverAddress = "Firezone addresses" + return proto + } + + private func setupTunnelObservers() { + TunnelStore.logger.trace("\(#function)") + + tunnelObservingTasks.forEach { $0.cancel() } + tunnelObservingTasks.removeAll() + + tunnelObservingTasks.append( + Task { + for await notification in NotificationCenter.default.notifications( + named: .NEVPNStatusDidChange, + object: nil + ) { + guard let session = notification.object as? NETunnelProviderSession, + let tunnelProvider = session.manager as? NETunnelProviderManager + else { + return + } + self.status = tunnelProvider.connection.status + } + } + ) + } + + func removeProfile() async throws { + TunnelStore.logger.trace("\(#function)") + + try await tunnel.removeFromPreferences() + } +} + +// MARK: - Extensions + +/// Make NEVPNStatus convertible to a string +extension NEVPNStatus: CustomStringConvertible { + public var description: String { + switch self { + case .disconnected: return "Disconnected" + case .invalid: return "Invalid" + case .connected: return "Connected" + case .connecting: return "Connecting" + case .disconnecting: return "Disconnecting" + case .reasserting: return "Reconnecting" + @unknown default: return "Unknown" + } + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/Alerts.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/Alerts.swift new file mode 100644 index 000000000..5d2573d25 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/Alerts.swift @@ -0,0 +1,18 @@ +// +// Alerts.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import _SwiftUINavigationState + +extension AlertState where Action == WelcomeViewModel.UndefinedSettingsAlertAction { + static let undefinedSettings = AlertState( + title: TextState("No settings found."), + message: TextState("To sign in, you first need to configure portal settings."), + dismissButton: .default( + TextState("Define settings"), + action: .send(.confirmDefineSettingsButtonTapped) + ) + ) +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ConnectionSwitch.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ConnectionSwitch.swift new file mode 100644 index 000000000..d56090038 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ConnectionSwitch.swift @@ -0,0 +1,55 @@ +// +// ConnectionSwitch.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import NetworkExtension +import SwiftUI + +struct ConnectionSwitch: View { + let status: NEVPNStatus + var connect: () async -> Void + var disconnect: () async -> Void + + @State private var isInFlight = false + + var body: some View { + HStack { + ZStack { + Toggle( + "", isOn: .init( + get: { status == .connected }, + set: { isOn in + Task { + isInFlight = true + defer { isInFlight = false } + + if isOn { + await connect() + } else { + await disconnect() + } + } + } + ) + ) + .labelsHidden() + .toggleStyle(.switch) + .opacity(isInFlight ? 0 : 1) + + if isInFlight { + ProgressView() + } + } + + Text(status.description).frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +struct ConnectionSwitch_Previews: PreviewProvider { + static var previews: some View { + ConnectionSwitch(status: .connected, connect: {}, disconnect: {}) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift new file mode 100644 index 000000000..7ab895bab --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -0,0 +1,202 @@ +// +// MenuBar.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +// swiftlint:disable function_parameter_count + +#if os(macOS) + import Combine + import Dependencies + import OSLog + import SwiftUI + + @MainActor + public final class MenuBar { + let logger = Logger.make(for: MenuBar.self) + @Dependency(\.mainQueue) private var mainQueue + + private var appStore: AppStore? { + didSet { + setupObservers() + } + } + + private var cancellables: Set = [] + private var statusItem: NSStatusItem + + let settingsViewModel: SettingsViewModel + + public init(settingsViewModel: SettingsViewModel) { + self.settingsViewModel = settingsViewModel + + settingsViewModel.onSettingsSaved = { + // TODO: close settings window and sign in + } + + 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" + ) + } + + createMenu() + + Task { + let tunnel = try await TunnelStore.loadOrCreate() + self.appStore = AppStore(tunnelStore: TunnelStore(tunnel: tunnel)) + } + } + + private func setupObservers() { + appStore?.auth.$token + .receive(on: mainQueue) + .sink { [weak self] token in + if let token { + self?.showLoggedIn(token.user) + } else { + self?.showLoggedOut() + } + } + .store(in: &cancellables) + + appStore?.tunnel.$status + .receive(on: mainQueue) + .sink { [weak self] status in + if status == .connected { + self?.connectionMenuItem.title = "Disconnect" + } else { + self?.connectionMenuItem.title = "Connect" + } + } + .store(in: &cancellables) + } + + private lazy var menu = NSMenu() + + private lazy var connectionMenuItem = createMenuItem( + menu, + title: "Connect", + action: #selector(connectButtonTapped), + target: self + ) + + private lazy var loginMenuItem = createMenuItem( + menu, + title: "Login", + action: #selector(loginButtonTapped), + target: self + ) + private lazy var logoutMenuItem = createMenuItem( + menu, + title: "Logout", + action: #selector(logoutButtonTapped), + isHidden: true, + 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 func createMenu() { + menu.addItem(connectionMenuItem) + menu.addItem(loginMenuItem) + menu.addItem(logoutMenuItem) + menu.addItem(NSMenuItem.separator()) + menu.addItem(settingsMenuItem) + menu.addItem(quitMenuItem) + + statusItem.menu = menu + } + + private func createMenuItem( + _: NSMenu, + title: String, + action: Selector, + isHidden: Bool = false, + key: String = "", + target: AnyObject? + ) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: key) + + item.isHidden = isHidden + item.target = target + + return item + } + + private func showLoggedIn(_ user: String?) { + if let user { + loginMenuItem.title = "Logged in as \(user)" + } else { + loginMenuItem.title = "Logged in" + } + loginMenuItem.target = nil + logoutMenuItem.isHidden = false + connectionMenuItem.isHidden = false + } + + private func showLoggedOut() { + loginMenuItem.title = "Login" + loginMenuItem.target = self + + logoutMenuItem.isHidden = true + connectionMenuItem.isHidden = true + } + + @objc private func connectButtonTapped() { + if appStore?.tunnel.status == .connected { + appStore?.tunnel.stop() + } else { + Task { + if let token = appStore?.auth.token { + do { + try await appStore?.tunnel.start(token: token) + } catch { + logger.error("error connecting to tunnel: \(String(describing: error))") + } + } + } + } + } + + @objc private func loginButtonTapped() { + Task { + do { + try await appStore?.auth.signIn() + } catch FirezoneError.missingPortalURL { + openSettingsWindow() + } catch { + logger.error("Error signing in: \(String(describing: error))") + } + } + } + + @objc private func logoutButtonTapped() { + appStore?.auth.signOut() + } + + @objc private func settingsButtonTapped() { + openSettingsWindow() + } + + private func openSettingsWindow() { + NSWorkspace.shared.open(URL(string: "firezone://settings")!) + } + } +#endif diff --git a/swift/apple/FirezoneKit/Tests/FirezoneKitTests/AppStoreTests.swift b/swift/apple/FirezoneKit/Tests/FirezoneKitTests/AppStoreTests.swift new file mode 100644 index 000000000..0ef606a0a --- /dev/null +++ b/swift/apple/FirezoneKit/Tests/FirezoneKitTests/AppStoreTests.swift @@ -0,0 +1,11 @@ +// +// AppStoreTests.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import XCTest + +@testable import FirezoneKit + +final class AppStoreTests: XCTestCase {} diff --git a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements new file mode 100644 index 000000000..b63ed3cab --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.application-groups + + + diff --git a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements new file mode 100644 index 000000000..9712ea7ce --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/swift/apple/FirezoneNetworkExtension/Info.plist b/swift/apple/FirezoneNetworkExtension/Info.plist new file mode 100644 index 000000000..3059459e1 --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift new file mode 100644 index 000000000..8cc47b168 --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -0,0 +1,73 @@ +// +// PacketTunnelProvider.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import connlib +import Dependencies +import NetworkExtension +import os + +enum PacketTunnelProviderError: String, Error { + case savedProtocolConfigurationIsInvalid + case couldNotSetNetworkSettings +} + +class PacketTunnelProvider: NEPacketTunnelProvider { + static let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") + + private lazy var adapter = Adapter(with: self) + + override func startTunnel( + options _: [String: NSObject]? = nil, + completionHandler: @escaping (Error?) -> Void + ) { + guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol else { + completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) + return + } + + let providerConfiguration = tunnelProviderProtocol.providerConfiguration + guard let portalURL = providerConfiguration?["portalURL"] as? String else { + completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) + return + } + guard let token = providerConfiguration?["token"] as? String else { + completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) + return + } + + Self.logger.log("portalURL = \(portalURL, privacy: .public)") + Self.logger.log("token = \(token, privacy: .public)") + + do { + // Once connlib is updated to take in portalURL and token, this call + // should become adapter.start(portalURL: portalURL, token: token) + try adapter.start { error in + if let error { + Self.logger.error("Error in adapter.start: \(error)") + } + completionHandler(error) + } + } catch { + completionHandler(error) + } + } + + override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) { + adapter.stop { error in + if let error { + Self.logger.error("Error in adapter.stop: \(error)") + } + completionHandler() + } + + #if os(macOS) + // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107). + // Remove it when they finally fix this upstream and the fix has been rolled out to + // sufficient quantities of users. + exit(0) + #endif + } +} diff --git a/swift/apple/LICENSE b/swift/apple/LICENSE new file mode 100644 index 000000000..e0d17e843 --- /dev/null +++ b/swift/apple/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Firezone, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/swift/apple/Makefile b/swift/apple/Makefile new file mode 100644 index 000000000..54d584766 --- /dev/null +++ b/swift/apple/Makefile @@ -0,0 +1,17 @@ +# Creates a macOS debug build + +PLATFORM=macOS +ARCH=$(shell uname -m) + +build-macos: + echo "Building debug build for ${PLATFORM}, ${ARCH}" + cd ../../rust/connlib/clients/apple && rm -rf ./Connlib.xcframework && ./build-rust.sh && ./build-xcframework-dev.sh + @xcodebuild build -scheme Firezone -sdk macosx -destination 'platform=${PLATFORM},arch=${ARCH}' + +clean: + @xcodebuild clean -scheme Firezone -sdk macosx -destination 'platform=${PLATFORM},arch=${ARCH}' + cd ../../rust/connlib/clients/apple && rm -rf ./Connlib.xcframework + +.PHONY: format +format: + @swiftformat . diff --git a/swift/apple/PortalMock/.tool-versions b/swift/apple/PortalMock/.tool-versions new file mode 100644 index 000000000..059ca477b --- /dev/null +++ b/swift/apple/PortalMock/.tool-versions @@ -0,0 +1 @@ +ruby 3.1.0 diff --git a/swift/apple/PortalMock/Gemfile b/swift/apple/PortalMock/Gemfile new file mode 100644 index 000000000..dc86e9cd9 --- /dev/null +++ b/swift/apple/PortalMock/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem 'sinatra' +gem 'thin' diff --git a/swift/apple/PortalMock/Gemfile.lock b/swift/apple/PortalMock/Gemfile.lock new file mode 100644 index 000000000..5ef51e3f6 --- /dev/null +++ b/swift/apple/PortalMock/Gemfile.lock @@ -0,0 +1,32 @@ +GEM + remote: https://rubygems.org/ + specs: + daemons (1.4.1) + eventmachine (1.2.7) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + rack (2.2.4) + rack-protection (3.0.4) + rack + ruby2_keywords (0.0.5) + sinatra (3.0.4) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.4) + tilt (~> 2.0) + thin (1.8.1) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) + tilt (2.0.11) + +PLATFORMS + arm64-darwin-21 + arm64-darwin-22 + +DEPENDENCIES + sinatra + thin + +BUNDLED WITH + 2.3.3 diff --git a/swift/apple/PortalMock/Info.plist b/swift/apple/PortalMock/Info.plist new file mode 100644 index 000000000..3059459e1 --- /dev/null +++ b/swift/apple/PortalMock/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/swift/apple/PortalMock/data/client_auth_token b/swift/apple/PortalMock/data/client_auth_token new file mode 100644 index 000000000..597b16ec2 --- /dev/null +++ b/swift/apple/PortalMock/data/client_auth_token @@ -0,0 +1 @@ +eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMN3k3RUM1T3VSZUNNNnIzX2l0MXNJbjNqeTdiZ2JPSVB3Z0xoejV0SGsifQ.eyJpc3MiOiJodHRwczovL2ZpcmV6b25lLmxvY2FsIiwic3ViIjoidGVzdEBmaXJlem9uZS5kZXYiLCJjbGllbnRfaWQiOiJmaXJlem9uZSIsImV4cCI6MTY3MjgzNzU0NCwiaWF0IjoxNjY4MTMzOTQ0fQ.NvvGWvrMvshKp5MYycDWXa8gQ41Ptrr_nIKzfPWzci8fxwmQYJ5hL1vQpdmECtR5NeGv7qTavi6yq19Kqmwrn27numDXaET2b2xypGbFOm1TJmcbZ4Rxy_-FfAeer-7YNhW_p83a0N7UoPORpxVs8hp76sKe_klfmoM830frrLzeqz0VYxBZXhPiTAlqiG39cY74yk-drxLY4xeRBAXh_TdewrkRkPpTpsrXFz60fF5P8AaRnUKlDSRq89ZIC-zo2ysJsXIZLrJpfcNgkscohZZfXfCLIFaiGvZseW0XHWfq-V5HOXVf09-57GHdmCr-AAJ7sqpnPrSBvg7EDBvylg diff --git a/swift/apple/PortalMock/server.rb b/swift/apple/PortalMock/server.rb new file mode 100755 index 000000000..275585fd5 --- /dev/null +++ b/swift/apple/PortalMock/server.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +require 'sinatra' +require 'erb' + +set :bind, '0.0.0.0' +set :port, 4568 + +get '/auth' do + dest = params['dest'] + ERB.new("

Auth page

Proceed").result(binding) +end + +get '/redirect' do + dest = params['dest'] + client_auth_token = File.read('./data/client_auth_token').strip + redirect "#{dest}?client_auth_token=#{client_auth_token}" +end diff --git a/swift/README.md b/swift/apple/README.md similarity index 64% rename from swift/README.md rename to swift/apple/README.md index 40a397727..795a45af0 100644 --- a/swift/README.md +++ b/swift/apple/README.md @@ -8,7 +8,14 @@ Clone this repo: ```bash git clone https://github.com/firezone/firezone -cd swift +``` + +Build Connlib: +```bash +cd rust/connlib/clients/apple +PLATFORM_NAME=macosx ./build-rust.sh # For macOS +PLATFORM_NAME=iphoneos ./build-rust.sh # For iOS +./build-xcframework-dev.sh ``` Rename and populate developer team ID file: @@ -23,3 +30,5 @@ Open project in Xcode: ```bash open Firezone.xcodeproj ``` + +Build the Firezone target