From 2bca378f17f46e309f075062b0043019296ca265 Mon Sep 17 00:00:00 2001 From: Jamil Date: Mon, 30 Oct 2023 23:46:53 -0700 Subject: [PATCH] Allow data plane configuration at runtime (#2477) ## Changelog - Updates connlib parameter API_URL (formerly known under different names as `CONTROL_PLANE_URL`, `PORTAL_URL`, `PORTAL_WS_URL`, and friends) to be configured as an "advanced" or "hidden" feature at runtime so that we can test production builds on both staging and production. - Makes `AUTH_BASE_URL` configurable at runtime too - Moves `CONNLIB_LOG_FILTER_STRING` to be configured like this as well and simplifies its naming - Fixes a timing attack bug on Android when comparing the `csrf` token - Adds proper account ID validation to Android to prevent invalid URL parameter strings from being saved and used - Cleans up a number of UI / view issues on Android regarding typos, consistency, etc - Hides vars from from the `relay` CLI we may not want to expose just yet - `get_device_id()` is flawed for connlib components -- SMBios is rarely available. Data plane components now require a `FIREZONE_ID` now instead to use for upserting. Fixes #2482 Fixes #2471 --------- Signed-off-by: Jamil Co-authored-by: Gabi --- .github/workflows/_swift.yml | 4 +- .github/workflows/ci.yml | 7 +- docker-compose.yml | 14 +- .../apps/web/lib/web/live/relay_groups/new.ex | 2 +- .../test/web/live/relay_groups/new_test.exs | 4 +- kotlin/android/README.md | 52 +++--- kotlin/android/app/build.gradle.kts | 34 ++-- .../android/app/src/main/AndroidManifest.xml | 19 +++ .../android/core/BaseUrlInterceptor.kt | 16 +- .../android/core/data/PreferenceRepository.kt | 7 +- .../core/data/PreferenceRepositoryImpl.kt | 24 ++- .../android/core/data/model/Config.kt | 4 +- ...untIdUseCase.kt => SaveSettingsUseCase.kt} | 9 +- .../android/features/auth/ui/AuthViewModel.kt | 9 +- .../features/session/ui/SessionActivity.kt | 6 +- .../features/settings/ui/SettingsFragment.kt | 69 +++++--- .../features/settings/ui/SettingsViewModel.kt | 88 +++++++++-- .../firezone/android/tunnel/TunnelService.kt | 5 +- .../firezone/android/tunnel/TunnelSession.kt | 2 +- .../res/layout/activity_app_link_handler.xml | 6 +- .../app/src/main/res/layout/activity_auth.xml | 2 +- .../src/main/res/layout/activity_session.xml | 6 +- .../src/main/res/layout/fragment_settings.xml | 148 ++++++++++++++++-- .../src/main/res/layout/fragment_sign_in.xml | 16 +- .../app/src/main/res/values/strings.xml | 19 ++- kotlin/android/local.properties.template | 9 -- rust/connlib/clients/android/src/lib.rs | 18 +-- rust/connlib/clients/apple/src/lib.rs | 6 +- rust/connlib/clients/shared/src/lib.rs | 10 +- rust/connlib/shared/src/lib.rs | 48 ++---- rust/firezone-cli-utils/src/lib.rs | 13 +- rust/gateway/README.md | 22 ++- rust/gateway/src/main.rs | 8 +- rust/linux-client/README.md | 31 +++- rust/linux-client/src/main.rs | 14 +- rust/relay/README.md | 27 ++-- rust/relay/src/main.rs | 32 ++-- .../xcconfig/{dev.xcconfig => debug.xcconfig} | 0 .../{prod.xcconfig => release.xcconfig} | 0 .../apple/Firezone/xcconfig/staging.xcconfig | 17 -- swift/apple/README.md | 2 +- terraform/environments/production/gateways.tf | 14 +- terraform/environments/production/main.tf | 6 +- .../environments/production/variables.tf | 4 +- terraform/environments/staging/main.tf | 6 +- terraform/environments/staging/variables.tf | 2 +- .../gateway-google-cloud-compute/main.tf | 8 +- .../gateway-google-cloud-compute/variables.tf | 4 +- terraform/modules/relay-app/main.tf | 8 +- terraform/modules/relay-app/variables.tf | 4 +- 50 files changed, 569 insertions(+), 316 deletions(-) rename kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/{SaveAccountIdUseCase.kt => SaveSettingsUseCase.kt} (54%) delete mode 100644 kotlin/android/local.properties.template rename swift/apple/Firezone/xcconfig/{dev.xcconfig => debug.xcconfig} (100%) rename swift/apple/Firezone/xcconfig/{prod.xcconfig => release.xcconfig} (100%) delete mode 100644 swift/apple/Firezone/xcconfig/staging.xcconfig diff --git a/.github/workflows/_swift.yml b/.github/workflows/_swift.yml index 6603319f6..eb692beec 100644 --- a/.github/workflows/_swift.yml +++ b/.github/workflows/_swift.yml @@ -117,14 +117,12 @@ jobs: # Needed because `productbuild` doesn't support picking this up automatically like Xcode does INSTALLER_CODE_SIGN_IDENTITY: "3rd Party Mac Developer Installer: Firezone, Inc. (47R2M6779T)" REQUESTED_XCODE_VERSION: ${{ matrix.xcode }} - # TODO: Change this to "prod" when app is released - XCCONFIG_ENV: staging run: | # Set Xcode version to use if provided [[ ! -z "$REQUESTED_XCODE_VERSION" ]] && sudo xcode-select -s /Applications/Xcode_$REQUESTED_XCODE_VERSION.app # Copy xcconfig - cp Firezone/xcconfig/${{ env.XCCONFIG_ENV }}.xcconfig Firezone/xcconfig/config.xcconfig + cp Firezone/xcconfig/release.xcconfig Firezone/xcconfig/config.xcconfig # App Store Connect requires a new build version on each upload and it must be an integer. # See https://developer.apple.com/documentation/xcode/build-settings-reference#Current-Project-Version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d55fee6a..719223d6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,12 @@ jobs: fail-fast: false matrix: include: - # TODO: Add more NAT type tests here + # TODO + # - Run control plane components as services + # - Test clients + # - Test with different NAT types + # - Test IPv6 + # - Test end-to-end critical paths - test_name: Relayed flow setup: | # Disallow traffic between gateway and client container diff --git a/docker-compose.yml b/docker-compose.yml index 80d0b816c..c06ef8830 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,9 +113,10 @@ services: client: environment: - PORTAL_URL: "ws://api:8081/" - PORTAL_TOKEN: "SFMyNTY.g2gDaAN3CGlkZW50aXR5bQAAACQ3ZGE3ZDFjZC0xMTFjLTQ0YTctYjVhYy00MDI3YjlkMjMwZTVtAAAAIBn8Xu1jtFlxZxp4ZvAz0f0QEN2PZThA-7awHMPxn_tHbgYAbLRvQokBYgHhM38.pM-prhb7uvvCVKf51-tAUMEtMzLPZk1n3nLsY44dGFA" + FIREZONE_TOKEN: "SFMyNTY.g2gDaAN3CGlkZW50aXR5bQAAACQ3ZGE3ZDFjZC0xMTFjLTQ0YTctYjVhYy00MDI3YjlkMjMwZTVtAAAAIBn8Xu1jtFlxZxp4ZvAz0f0QEN2PZThA-7awHMPxn_tHbgYAbLRvQokBYgHhM38.pM-prhb7uvvCVKf51-tAUMEtMzLPZk1n3nLsY44dGFA" RUST_LOG: firezone_linux_client=trace,connlib_client_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn + FIREZONE_API_URL: ws://api:8081 + FIREZONE_ID: D0455FDE-8F65-4960-A778-B934E4E85A5F build: target: debug context: rust @@ -149,10 +150,11 @@ services: healthcheck: test: ["CMD-SHELL", "ip link | grep tun-firezone"] environment: - PORTAL_URL: "ws://api:8081/" - PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAEC0b0KJAWIAAVGA.9Oirn9t8rvQpfOhW7hwGBFVzeMm9di0xYGTlwf9cFFk" + FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAEC0b0KJAWIAAVGA.9Oirn9t8rvQpfOhW7hwGBFVzeMm9di0xYGTlwf9cFFk" RUST_LOG: firezone_gateway=trace,connlib_gateway_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn FIREZONE_ENABLE_MASQUERADE: 1 + FIREZONE_API_URL: ws://api:8081 + FIREZONE_ID: 4694E56C-7643-4A15-9DF3-638E5B05F570 build: target: debug context: rust @@ -201,10 +203,10 @@ services: PUBLIC_IP6_ADDR: fcff:3990:3990::101 LOWEST_PORT: 55555 HIGHEST_PORT: 55666 - PORTAL_URL: "ws://api:8081/" - PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAFSzb0KJAWIAAVGA.waeGE26tbgkgIcMrWyck0ysv9SHIoHr0zqoM3wao84M" + FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAFSzb0KJAWIAAVGA.waeGE26tbgkgIcMrWyck0ysv9SHIoHr0zqoM3wao84M" RUST_LOG: "debug" RUST_BACKTRACE: 1 + FIREZONE_API_URL: ws://api:8081 build: target: debug context: rust diff --git a/elixir/apps/web/lib/web/live/relay_groups/new.ex b/elixir/apps/web/lib/web/live/relay_groups/new.ex index 92ecaab24..1f4029e30 100644 --- a/elixir/apps/web/lib/web/live/relay_groups/new.ex +++ b/elixir/apps/web/lib/web/live/relay_groups/new.ex @@ -98,7 +98,7 @@ defmodule Web.RelayGroups.New do --name=firezone-relay-0 \\ --restart=always \\ -v /dev/net/tun:/dev/net/tun \\ - -e PORTAL_TOKEN=#{secret} \\ + -e FIREZONE_TOKEN=#{secret} \\ us-east1-docker.pkg.dev/firezone/firezone/relay:stable """ end diff --git a/elixir/apps/web/test/web/live/relay_groups/new_test.exs b/elixir/apps/web/test/web/live/relay_groups/new_test.exs index 0ac3721b0..5d37bd97e 100644 --- a/elixir/apps/web/test/web/live/relay_groups/new_test.exs +++ b/elixir/apps/web/test/web/live/relay_groups/new_test.exs @@ -119,11 +119,11 @@ defmodule Web.Live.RelayGroups.NewTest do |> render_submit() assert html =~ "Select deployment method" - assert html =~ "PORTAL_TOKEN=" + assert html =~ "FIREZONE_TOKEN=" assert html =~ "docker run" assert html =~ "Waiting for relay connection..." - token = Regex.run(~r/PORTAL_TOKEN=([^ ]+)/, html) |> List.last() + token = Regex.run(~r/FIREZONE_TOKEN=([^ ]+)/, html) |> List.last() assert {:ok, _token} = Domain.Relays.authorize_relay(token) group = Repo.get_by(Domain.Relays.Group, name: attrs.name) |> Repo.preload(:tokens) diff --git a/kotlin/android/README.md b/kotlin/android/README.md index c46720073..17ce35f16 100644 --- a/kotlin/android/README.md +++ b/kotlin/android/README.md @@ -1,24 +1,38 @@ # Firezone Android client -## Prerequisites for developing locally +This README contains instructions for building and testing the Android client +locally. -1. Install a recent `ruby` for your platform. Ruby is used for the mock auth - server. -1. Install needed gems and start mock auth server: +## Dev Setup +1. [Install Rust](https://www.rust-lang.org/tools/install) +1. [Install Android Studio](https://developer.android.com/studio) +1. Install your JDK 17 of choice. We recommend just + [updating your CLI](https://stackoverflow.com/questions/43211282/using-jdk-that-is-bundled-inside-android-studio-as-java-home-on-mac) + environment to use the JDK bundled in Android Studio to ensure you're using + the same JDK on the CLI as Android Studio. +1. Perform a test build: `./gradlew assembleDebug` +1. Add your debug signing key's SHA256 fingerprint to the portal's + [`assetlinks.json`](../../elixir/apps/web/priv/static/.well-known/assetlinks.json) + file. This is required for the App Links to successfully intercept the Auth + redirect. + ``` + ./gradlew signingReport + ``` + +# Release Setup + +We release from GitHub CI, so this shouldn't be necessary. But if you're looking +to test the `release` variant locally: + +1. Download the keystore from 1Pass and save to `app/.signing/keystore.jks` dir. +1. Download firebase credentials from 1Pass and save to + `app/.signing/firebase.json` +1. Now you can execute the `*Release` tasks with: + +```shell +export KEYSTORE_PATH="$(pwd)/app/.signing/keystore.jks" +export FIREBASE_CREDENTIALS_PATH="$(pwd)/app/.signing/firebase.json" +HISTCONTROL=ignorespace # prevents saving the next line in shell history + KEYSTORE_PASSWORD='keystore_password' KEYSTORE_KEY_PASSWORD='keystore_key_password' ./gradlew assembleRelease ``` -cd server -bundle install -ruby server.rb -``` - -1. Add the following to a `./local.properties` file: - -```gradle -sdk.dir=/path/to/your/ANDROID_HOME -``` - -Replace `/path/to/your/ANDROID_HOME` with the path to your locally installed -Android SDK. On macOS this is `/Users/jamil/Library./Android/sdk` - -1. Perform a test build: `./gradlew build` diff --git a/kotlin/android/app/build.gradle.kts b/kotlin/android/app/build.gradle.kts index d9bc98f1e..ebe28a1ba 100644 --- a/kotlin/android/app/build.gradle.kts +++ b/kotlin/android/app/build.gradle.kts @@ -65,20 +65,15 @@ android { // Debug Config getByName("debug") { isDebuggable = true + resValue("string", "app_name", "\"Firezone (Dev)\"") - buildConfigField("String", "AUTH_HOST", "\"app.firez.one\"") - buildConfigField("String", "AUTH_SCHEME", "\"https\"") - buildConfigField("Integer", "AUTH_PORT", "443") - buildConfigField("String", "CONTROL_PLANE_URL", "\"wss://api.firez.one/\"") - - // Docs on filter strings: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html + buildConfigField("String", "AUTH_BASE_URL", "\"https://app.firez.one\"") + buildConfigField("String", "API_URL", "\"wss://api.firez.one\"") buildConfigField( "String", - "CONNLIB_LOG_FILTER_STRING", + "LOG_FILTER", "\"connlib_client_android=debug,firezone_tunnel=trace,connlib_shared=debug,connlib_client_shared=debug,warn\"", ) - - resValue("string", "app_name", "\"Firezone (Dev)\"") } // Release Config @@ -103,25 +98,20 @@ android { ) isDebuggable = false - buildConfigField("String", "AUTH_HOST", "\"app.firezone.dev\"") - buildConfigField("String", "AUTH_SCHEME", "\"https\"") - buildConfigField("Integer", "AUTH_PORT", "443") - buildConfigField("String", "CONTROL_PLANE_URL", "\"wss://api.firezone.dev/\"") - - // Docs on filter strings: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html - buildConfigField( - "String", - "CONNLIB_LOG_FILTER_STRING", - "\"connlib_client_android=info,firezone_tunnel=info,connlib_shared=info,connlib_client_shared=info,warn\"", - ) - resValue("string", "app_name", "\"Firezone\"") + buildConfigField("String", "AUTH_BASE_URL", "\"https://app.firezone.dev\"") + buildConfigField("String", "API_URL", "\"wss://api.firezone.dev\"") + buildConfigField( + "String", + "LOG_FILTER", + "\"connlib_client_android=info,firezone_tunnel=info,connlib_shared=info,connlib_client_shared=info,warn\"", + ) firebaseAppDistribution { serviceCredentialsFile = System.getenv("FIREBASE_CREDENTIALS_PATH") artifactType = "AAB" releaseNotes = "https://github.com/firezone/firezone/releases" - groups = "firezone-engineering" + groups = "firezone-engineering, firezone-go-to-market" artifactPath = "app/build/outputs/bundle/release/app-release.aab" } } diff --git a/kotlin/android/app/src/main/AndroidManifest.xml b/kotlin/android/app/src/main/AndroidManifest.xml index e687b2497..0bd8bb418 100644 --- a/kotlin/android/app/src/main/AndroidManifest.xml +++ b/kotlin/android/app/src/main/AndroidManifest.xml @@ -50,6 +50,7 @@ android:exported="true" android:launchMode="singleTop"> + @@ -66,6 +67,24 @@ + + + + + + + + + + + + + - fun saveAccountId(value: String): Flow + fun saveSettings( + accountId: String, + authBaseUrl: String, + apiUrl: String, + logFilter: String, + ): Flow fun saveToken(value: String): Flow diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt index 248cb0286..e574d12ec 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt @@ -2,11 +2,13 @@ package dev.firezone.android.core.data import android.content.SharedPreferences +import dev.firezone.android.BuildConfig import dev.firezone.android.core.data.model.Config import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import java.security.MessageDigest import javax.inject.Inject internal class PreferenceRepositoryImpl @@ -18,6 +20,9 @@ internal class PreferenceRepositoryImpl override fun getConfigSync(): Config = Config( accountId = sharedPreferences.getString(ACCOUNT_ID_KEY, null), + authBaseUrl = sharedPreferences.getString(AUTH_BASE_URL_KEY, null) ?: BuildConfig.AUTH_BASE_URL, + apiUrl = sharedPreferences.getString(API_URL_KEY, null) ?: BuildConfig.API_URL, + logFilter = sharedPreferences.getString(LOG_FILTER_KEY, null) ?: BuildConfig.LOG_FILTER, token = sharedPreferences.getString(TOKEN_KEY, null), ) @@ -26,12 +31,20 @@ internal class PreferenceRepositoryImpl emit(getConfigSync()) }.flowOn(coroutineDispatcher) - override fun saveAccountId(value: String): Flow = + override fun saveSettings( + accountId: String, + authBaseUrl: String, + apiUrl: String, + logFilter: String, + ): Flow = flow { emit( sharedPreferences .edit() - .putString(ACCOUNT_ID_KEY, value) + .putString(ACCOUNT_ID_KEY, accountId) + .putString(AUTH_BASE_URL_KEY, authBaseUrl) + .putString(API_URL_KEY, apiUrl) + .putString(LOG_FILTER_KEY, logFilter) .apply(), ) }.flowOn(coroutineDispatcher) @@ -48,8 +61,8 @@ internal class PreferenceRepositoryImpl override fun validateCsrfToken(value: String): Flow = flow { - val token = sharedPreferences.getString(CSRF_KEY, "") - emit(token == value) + val token = sharedPreferences.getString(CSRF_KEY, "").orEmpty() + emit(MessageDigest.isEqual(token.toByteArray(), value.toByteArray())) }.flowOn(coroutineDispatcher) override fun clearToken() { @@ -66,6 +79,9 @@ internal class PreferenceRepositoryImpl companion object { private const val ACCOUNT_ID_KEY = "accountId" + private const val AUTH_BASE_URL_KEY = "authBaseUrl" + private const val API_URL_KEY = "apiUrl" + private const val LOG_FILTER_KEY = "logFilter" private const val TOKEN_KEY = "token" private const val CSRF_KEY = "csrf" } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt index 395aa8d44..3ab327e16 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt @@ -3,6 +3,8 @@ package dev.firezone.android.core.data.model internal data class Config( val accountId: String?, - val isConnected: Boolean = false, + val authBaseUrl: String, + val apiUrl: String, + val logFilter: String, val token: String?, ) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveSettingsUseCase.kt similarity index 54% rename from kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveSettingsUseCase.kt index c55c9aeb1..a8d13929d 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveSettingsUseCase.kt @@ -5,10 +5,15 @@ import dev.firezone.android.core.data.PreferenceRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject -internal class SaveAccountIdUseCase +internal class SaveSettingsUseCase @Inject constructor( private val repository: PreferenceRepository, ) { - operator fun invoke(accountId: String): Flow = repository.saveAccountId(accountId) + operator fun invoke( + accountId: String, + authBaseUrl: String, + apiUrl: String, + logFilter: String, + ): Flow = repository.saveSettings(accountId, authBaseUrl, apiUrl, logFilter) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt index 0c03a1f69..6664bfcc9 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.firezone.android.BuildConfig import dev.firezone.android.core.domain.auth.GetCsrfTokenUseCase import dev.firezone.android.core.domain.preference.GetConfigUseCase import kotlinx.coroutines.flow.firstOrNull @@ -43,7 +42,9 @@ internal class AuthViewModel } else { authFlowLaunched = true ViewAction.LaunchAuthFlow( - url = "$AUTH_URL${config.accountId}?client_csrf_token=$csrfToken&client_platform=android", + url = + "${config.authBaseUrl}/${config.accountId}" + + "?client_csrf_token=$csrfToken&client_platform=android", ) }, ) @@ -52,10 +53,6 @@ internal class AuthViewModel actionMutableLiveData.postValue(ViewAction.ShowError) } - companion object { - val AUTH_URL = "${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/" - } - internal sealed class ViewAction { data class LaunchAuthFlow(val url: String) : ViewAction() diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt index 88b65a313..86d39112e 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt @@ -49,9 +49,9 @@ internal class SessionActivity : AppCompatActivity() { this@SessionActivity, layoutManager.orientation, ) - binding.resourcesList.addItemDecoration(dividerItemDecoration) - binding.resourcesList.adapter = resourcesAdapter - binding.resourcesList.layoutManager = layoutManager + binding.rvResourcesList.addItemDecoration(dividerItemDecoration) + binding.rvResourcesList.adapter = resourcesAdapter + binding.rvResourcesList.layoutManager = layoutManager } private fun setupObservers() { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt index f5cd4ce33..ea4db661d 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt @@ -2,6 +2,7 @@ package dev.firezone.android.features.settings.ui import android.os.Bundle +import android.util.Log import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.widget.doOnTextChanged @@ -27,15 +28,16 @@ internal class SettingsFragment : Fragment(R.layout.fragment_settings) { setupViews() setupStateObservers() setupActionObservers() - setupButtonListener() + setupButtonListeners() - viewModel.getAccountId() + viewModel.populateFieldsFromConfig() } private fun setupStateObservers() { viewModel.stateLiveData.observe(viewLifecycleOwner) { state -> with(binding) { - btLogin.isEnabled = state.isButtonEnabled + Log.d("SettingsFragment", "state: $state") + btSaveSettings.isEnabled = state.isSaveButtonEnabled } } } @@ -47,10 +49,19 @@ internal class SettingsFragment : Fragment(R.layout.fragment_settings) { findNavController().navigate( R.id.signInFragment, ) - is SettingsViewModel.ViewAction.FillAccountId -> { - binding.etInput.apply { - setText(action.value) - isCursorVisible = false + is SettingsViewModel.ViewAction.FillSettings -> { + Log.d("SettingsFragment", "action: $action") + binding.etAccountIdInput.apply { + setText(action.accountId) + } + binding.etAuthBaseUrlInput.apply { + setText(action.authBaseUrl) + } + binding.etApiUrlInput.apply { + setText(action.apiUrl) + } + binding.etLogFilterInput.apply { + setText(action.logFilter) } } } @@ -58,24 +69,46 @@ internal class SettingsFragment : Fragment(R.layout.fragment_settings) { } private fun setupViews() { - binding.ilUrlInput.apply { - prefixText = SettingsViewModel.AUTH_URL - } - - binding.etInput.apply { + binding.etAccountIdInput.apply { imeOptions = EditorInfo.IME_ACTION_DONE setOnClickListener { isCursorVisible = true } - doOnTextChanged { input, _, _, _ -> - viewModel.onValidateInput(input.toString()) + doOnTextChanged { accountId, _, _, _ -> + viewModel.onValidateAccountId(accountId.toString()) } - requestFocus() } - binding.btLogin.setOnClickListener { + binding.etAuthBaseUrlInput.apply { + imeOptions = EditorInfo.IME_ACTION_DONE + setOnClickListener { isCursorVisible = true } + doOnTextChanged { authBaseUrl, _, _, _ -> + viewModel.onValidateAuthBaseUrl(authBaseUrl.toString()) + } + } + + binding.etApiUrlInput.apply { + imeOptions = EditorInfo.IME_ACTION_DONE + setOnClickListener { isCursorVisible = true } + doOnTextChanged { apiUrl, _, _, _ -> + viewModel.onValidateApiUrl(apiUrl.toString()) + } + } + + binding.etLogFilterInput.apply { + imeOptions = EditorInfo.IME_ACTION_DONE + setOnClickListener { isCursorVisible = true } + doOnTextChanged { logFilter, _, _, _ -> + viewModel.onValidateLogFilter(logFilter.toString()) + } + } + } + + private fun setupButtonListeners() { + binding.btSaveSettings.setOnClickListener { viewModel.onSaveSettingsCompleted() } - } - private fun setupButtonListener() { + binding.btCancel.setOnClickListener { + viewModel.onCancel() + } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt index ef002ffbf..70a834918 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt @@ -1,15 +1,17 @@ /* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */ package dev.firezone.android.features.settings.ui +import android.webkit.URLUtil import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.firezone.android.BuildConfig import dev.firezone.android.core.domain.preference.GetConfigUseCase -import dev.firezone.android.core.domain.preference.SaveAccountIdUseCase +import dev.firezone.android.core.domain.preference.SaveSettingsUseCase import kotlinx.coroutines.launch +import java.net.URI +import java.net.URISyntaxException import javax.inject.Inject @HiltViewModel @@ -17,7 +19,7 @@ internal class SettingsViewModel @Inject constructor( private val getConfigUseCase: GetConfigUseCase, - private val saveAccountIdUseCase: SaveAccountIdUseCase, + private val saveSettingsUseCase: SaveSettingsUseCase, ) : ViewModel() { private val stateMutableLiveData = MutableLiveData() val stateLiveData: LiveData = stateMutableLiveData @@ -25,13 +27,21 @@ internal class SettingsViewModel private val actionMutableLiveData = MutableLiveData() val actionLiveData: LiveData = actionMutableLiveData - private var input = "" + private var accountId = "" + private var authBaseUrl = "" + private var apiUrl = "" + private var logFilter = "" - fun getAccountId() { + fun populateFieldsFromConfig() { viewModelScope.launch { getConfigUseCase().collect { actionMutableLiveData.postValue( - ViewAction.FillAccountId(it.accountId.orEmpty()), + ViewAction.FillSettings( + it.accountId.orEmpty(), + it.authBaseUrl.orEmpty(), + it.apiUrl.orEmpty(), + it.logFilter.orEmpty(), + ), ) } } @@ -39,32 +49,82 @@ internal class SettingsViewModel fun onSaveSettingsCompleted() { viewModelScope.launch { - saveAccountIdUseCase(input).collect { + saveSettingsUseCase(accountId, authBaseUrl, apiUrl, logFilter).collect { actionMutableLiveData.postValue(ViewAction.NavigateToSignIn) } } } - fun onValidateInput(input: String) { - this.input = input + fun onCancel() { + actionMutableLiveData.postValue(ViewAction.NavigateToSignIn) + } + + fun onValidateAccountId(accountId: String) { + this.accountId = accountId stateMutableLiveData.postValue( ViewState().copy( - isButtonEnabled = input.isEmpty().not(), + isSaveButtonEnabled = areFieldsValid(), ), ) } - companion object { - val AUTH_URL = "${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/" + fun onValidateAuthBaseUrl(authBaseUrl: String) { + this.authBaseUrl = authBaseUrl + stateMutableLiveData.postValue( + ViewState().copy( + isSaveButtonEnabled = areFieldsValid(), + ), + ) + } + + fun onValidateApiUrl(apiUrl: String) { + this.apiUrl = apiUrl + stateMutableLiveData.postValue( + ViewState().copy( + isSaveButtonEnabled = areFieldsValid(), + ), + ) + } + + fun onValidateLogFilter(logFilter: String) { + this.logFilter = logFilter + stateMutableLiveData.postValue( + ViewState().copy( + isSaveButtonEnabled = areFieldsValid(), + ), + ) } internal sealed class ViewAction { object NavigateToSignIn : ViewAction() - data class FillAccountId(val value: String) : ViewAction() + data class FillSettings( + val accountId: String, + val authBaseUrl: String, + val apiUrl: String, + val logFilter: String, + ) : ViewAction() + } + + private fun areFieldsValid(): Boolean { + // This comes from the backend account slug validator at elixir/apps/domain/lib/domain/accounts/account/changeset.ex + val accountIdRegex = Regex("^[a-z0-9_]{3,100}\$") + return accountIdRegex.matches(accountId) && + URLUtil.isValidUrl(authBaseUrl) && + isUriValid(apiUrl) && + logFilter.isNotBlank() + } + + private fun isUriValid(uri: String): Boolean { + return try { + URI(uri) + true + } catch (e: URISyntaxException) { + false + } } internal data class ViewState( - val isButtonEnabled: Boolean = false, + val isSaveButtonEnabled: Boolean = false, ) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index 3cd6df592..c3fe92ac1 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -16,7 +16,6 @@ import com.google.firebase.installations.FirebaseInstallations import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import dagger.hilt.android.AndroidEntryPoint -import dev.firezone.android.BuildConfig import dev.firezone.android.R import dev.firezone.android.core.data.PreferenceRepository import dev.firezone.android.core.domain.preference.GetConfigUseCase @@ -187,11 +186,11 @@ class TunnelService : VpnService() { sessionPtr = TunnelSession.connect( - controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL, + apiUrl = config.apiUrl, token = config.token, deviceId = deviceId(), logDir = getLogDir(), - logFilter = BuildConfig.CONNLIB_LOG_FILTER_STRING, + logFilter = config.logFilter, callback = callback, ) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt index e0ae48bca..3aa20008d 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt @@ -3,7 +3,7 @@ package dev.firezone.android.tunnel object TunnelSession { external fun connect( - controlPlaneUrl: String, + apiUrl: String, token: String, deviceId: String, logDir: String, diff --git a/kotlin/android/app/src/main/res/layout/activity_app_link_handler.xml b/kotlin/android/app/src/main/res/layout/activity_app_link_handler.xml index a761bd95b..1a7a60f98 100644 --- a/kotlin/android/app/src/main/res/layout/activity_app_link_handler.xml +++ b/kotlin/android/app/src/main/res/layout/activity_app_link_handler.xml @@ -10,10 +10,10 @@ + app:layout_constraintTop_toTopOf="parent" /> diff --git a/kotlin/android/app/src/main/res/layout/activity_auth.xml b/kotlin/android/app/src/main/res/layout/activity_auth.xml index 168ca4f99..09f7a24d4 100644 --- a/kotlin/android/app/src/main/res/layout/activity_auth.xml +++ b/kotlin/android/app/src/main/res/layout/activity_auth.xml @@ -36,7 +36,7 @@ @@ -31,36 +32,157 @@ android:text="@string/app_short_name" /> - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.225" /> + + + + + + + + + + + + + + + + - - + + + + + + + + + + diff --git a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml index dae93dd55..b43ef1095 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml @@ -34,7 +34,7 @@ diff --git a/kotlin/android/app/src/main/res/values/strings.xml b/kotlin/android/app/src/main/res/values/strings.xml index c4cbf1187..a97712246 100644 --- a/kotlin/android/app/src/main/res/values/strings.xml +++ b/kotlin/android/app/src/main/res/values/strings.xml @@ -2,9 +2,16 @@ firezone - account-id + Settings + Required + Advanced + Account ID + Auth Base URL + API URL + Log Filter Save + Sign In Settings @@ -15,18 +22,14 @@ Sign Out - Launching Auth Flow… + Launching Chrome to sign in... Error - Ops, something went wrong. Please try again later. + Oops! Something went wrong. Contact your admin if this issue persists. Ok Enable VPN Permission Firezone requires the VPN permission in order to route packets from your device to protected resources in a secure manner. All communication is end-to-end encrypted; we can never decrypt or otherwise monitor your communication. Please grant the VPN permission by tapping the button below. Request Permission - Enter team id - Sign In (Debug User) - Signing in requires chrome browser - - + Signing in requires Chrome browser diff --git a/kotlin/android/local.properties.template b/kotlin/android/local.properties.template deleted file mode 100644 index b6cb17675..000000000 --- a/kotlin/android/local.properties.template +++ /dev/null @@ -1,9 +0,0 @@ -# Copy this file to local.properties and define the variables below. -# -# Location of the SDK. This is only used by Gradle. For customization when using a Version Control System, please read the -# header note. -# e.g. /Users/jamil/Library/Android/sdk -sdk.dir= - -# Auth token from portal to use for debugUser in order to bypass the auth flow and send to connlib. -token= diff --git a/rust/connlib/clients/android/src/lib.rs b/rust/connlib/clients/android/src/lib.rs index 23a1447ba..a5a72fc39 100644 --- a/rust/connlib/clients/android/src/lib.rs +++ b/rust/connlib/clients/android/src/lib.rs @@ -394,15 +394,15 @@ macro_rules! string_from_jstring { fn connect( env: &mut JNIEnv, - portal_url: JString, - portal_token: JString, + api_url: JString, + token: JString, device_id: JString, log_dir: JString, log_filter: JString, callback_handler: GlobalRef, ) -> Result, ConnectError> { - let portal_url = string_from_jstring!(env, portal_url); - let secret = SecretString::from(string_from_jstring!(env, portal_token)); + let api_url = string_from_jstring!(env, api_url); + let secret = SecretString::from(string_from_jstring!(env, token)); let device_id = string_from_jstring!(env, device_id); let log_dir = string_from_jstring!(env, log_dir); let log_filter = string_from_jstring!(env, log_filter); @@ -415,7 +415,7 @@ fn connect( handle, }; - let session = Session::connect(portal_url.as_str(), secret, device_id, callback_handler)?; + let session = Session::connect(api_url.as_str(), secret, device_id, callback_handler)?; Ok(session) } @@ -428,8 +428,8 @@ fn connect( pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_connect( mut env: JNIEnv, _class: JClass, - portal_url: JString, - portal_token: JString, + api_url: JString, + token: JString, device_id: JString, log_dir: JString, log_filter: JString, @@ -442,8 +442,8 @@ pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_con let connect = catch_and_throw(&mut env, |env| { connect( env, - portal_url, - portal_token, + api_url, + token, device_id, log_dir, log_filter, diff --git a/rust/connlib/clients/apple/src/lib.rs b/rust/connlib/clients/apple/src/lib.rs index 50d0e0fdb..f3415cb60 100644 --- a/rust/connlib/clients/apple/src/lib.rs +++ b/rust/connlib/clients/apple/src/lib.rs @@ -20,7 +20,7 @@ mod ffi { #[swift_bridge(associated_to = WrappedSession)] fn connect( - portal_url: String, + api_url: String, token: String, device_id: String, log_dir: String, @@ -162,7 +162,7 @@ fn init_logging(log_dir: PathBuf, log_filter: String) -> file_logger::Handle { impl WrappedSession { fn connect( - portal_url: String, + api_url: String, token: String, device_id: String, log_dir: String, @@ -172,7 +172,7 @@ impl WrappedSession { let secret = SecretString::from(token); let session = Session::connect( - portal_url.as_str(), + api_url.as_str(), secret, device_id, CallbackHandler { diff --git a/rust/connlib/clients/shared/src/lib.rs b/rust/connlib/clients/shared/src/lib.rs index 52c082222..000f41596 100644 --- a/rust/connlib/clients/shared/src/lib.rs +++ b/rust/connlib/clients/shared/src/lib.rs @@ -1,5 +1,5 @@ //! Main connlib library for clients. -pub use connlib_shared::{get_device_id, messages::ResourceDescription}; +pub use connlib_shared::messages::ResourceDescription; pub use connlib_shared::{Callbacks, Error}; pub use tracing_appender::non_blocking::WorkerGuard; @@ -60,7 +60,7 @@ where /// On a fatal error you should call `[Session::disconnect]` and start a new one. // TODO: token should be something like SecretString but we need to think about FFI compatibility pub fn connect( - portal_url: impl TryInto, + api_url: impl TryInto, token: SecretString, device_id: String, callbacks: CB, @@ -105,7 +105,7 @@ where Self::connect_inner( &runtime, tx, - portal_url.try_into().map_err(|_| Error::UriError)?, + api_url.try_into().map_err(|_| Error::UriError)?, token, device_id, this.callbacks.clone(), @@ -121,14 +121,14 @@ where fn connect_inner( runtime: &Runtime, runtime_stopper: tokio::sync::mpsc::Sender, - portal_url: Url, + api_url: Url, token: SecretString, device_id: String, callbacks: CallbackErrorFacade, ) { runtime.spawn(async move { let (connect_url, private_key) = fatal_error!( - login_url(Mode::Client, portal_url, token, device_id), + login_url(Mode::Client, api_url, token, device_id), runtime_stopper, &callbacks ); diff --git a/rust/connlib/shared/src/lib.rs b/rust/connlib/shared/src/lib.rs index 18feb5d35..06e9733d5 100644 --- a/rust/connlib/shared/src/lib.rs +++ b/rust/connlib/shared/src/lib.rs @@ -31,8 +31,8 @@ const LIB_NAME: &str = "connlib"; /// Creates a new login URL to use with the portal. pub fn login_url( mode: Mode, - portal_url: Url, - portal_token: SecretString, + api_url: Url, + token: SecretString, device_id: String, ) -> Result<(Url, StaticSecret)> { let private_key = StaticSecret::random_from_rng(rand::rngs::OsRng); @@ -44,8 +44,8 @@ pub fn login_url( let external_id = sha256(device_id); let url = get_websocket_path( - portal_url, - portal_token, + api_url, + token, match mode { Mode::Client => "client", Mode::Gateway => "gateway", @@ -73,36 +73,6 @@ pub fn get_user_agent() -> String { format!("{os_type}/{os_version} {lib_name}/{lib_version}") } -/// Returns the SMBios Serial of the device or a random UUIDv4 if the SMBios is not available. -#[cfg(not(any(target_os = "ios", target_os = "android")))] -pub fn get_device_id() -> String { - match smbioslib::table_load_from_device() { - Ok(data) => { - if let Some(uuid) = - data.find_map(|sys_info: smbioslib::SMBiosSystemInformation| sys_info.uuid()) - { - tracing::debug!("get_device_id() found SMBios Serial: {}", uuid); - return uuid.to_string(); - } - } - Err(e) => { - tracing::warn!("get_device_id() couldn't load SMBios. Error: {}", e); - } - } - - tracing::warn!("get_device_id() couldn't find a SMBios Serial. Using random UUIDv4 instead."); - uuid::Uuid::new_v4().to_string() -} - -#[cfg(any(target_os = "ios", target_os = "android"))] -pub fn get_device_id() -> String { - tracing::warn!( - "get_device_id() is not implemented for this platform. Using random UUIDv4 instead." - ); - - uuid::Uuid::new_v4().to_string() -} - fn set_ws_scheme(url: &mut Url) -> Result<()> { let scheme = match url.scheme() { "http" | "ws" => "ws", @@ -128,24 +98,24 @@ fn sha256(input: String) -> String { } fn get_websocket_path( - mut url: Url, + mut api_url: Url, secret: SecretString, mode: &str, public_key: &Key, external_id: &str, name_suffix: &str, ) -> Result { - set_ws_scheme(&mut url)?; + set_ws_scheme(&mut api_url)?; { - let mut paths = url.path_segments_mut().map_err(|_| Error::UriError)?; + let mut paths = api_url.path_segments_mut().map_err(|_| Error::UriError)?; paths.pop_if_empty(); paths.push(mode); paths.push("websocket"); } { - let mut query_pairs = url.query_pairs_mut(); + let mut query_pairs = api_url.query_pairs_mut(); query_pairs.clear(); query_pairs.append_pair("token", secret.expose_secret()); query_pairs.append_pair("public_key", &public_key.to_string()); @@ -153,5 +123,5 @@ fn get_websocket_path( query_pairs.append_pair("name_suffix", name_suffix); } - Ok(url) + Ok(api_url) } diff --git a/rust/firezone-cli-utils/src/lib.rs b/rust/firezone-cli-utils/src/lib.rs index 2670010d4..001ad7f9e 100644 --- a/rust/firezone-cli-utils/src/lib.rs +++ b/rust/firezone-cli-utils/src/lib.rs @@ -24,15 +24,18 @@ where /// Arguments common to all Firezone CLI components. #[derive(Args, Clone)] pub struct CommonArgs { - /// Firezone admin portal websocket URL #[arg( short = 'u', long, - env = "PORTAL_URL", + hide = true, + env = "FIREZONE_API_URL", default_value = "wss://api.firezone.dev" )] - pub portal_url: Url, + pub api_url: Url, + /// Identifier generated by the portal to identify and display the device. + #[arg(short = 'i', long, env = "FIREZONE_ID")] + pub firezone_id: String, /// Token generated by the portal to authorize websocket connection. - #[arg(short = 't', long, env = "PORTAL_TOKEN")] - pub portal_token: String, + #[arg(env = "FIREZONE_TOKEN")] + pub token: String, } diff --git a/rust/gateway/README.md b/rust/gateway/README.md index 7604501e8..2c05b98e1 100644 --- a/rust/gateway/README.md +++ b/rust/gateway/README.md @@ -10,17 +10,25 @@ You should then find a binary in `target/release/firezone-gateway`. ## Running -To run the gateway: +The Firezone Gateway supports Linux only. To run the Gateway binary on your +Linux host: + +1. Generate a new Gateway token from the "Gateways" section of the admin portal + and save it in your secrets manager. +1. Ensure the `FIREZONE_TOKEN=` environment variable is set + securely in your Gateway's shell environment. The Gateway requires this + variable at startup. +1. Set `FIREZONE_ID` to a unique string to identify this gateway in the portal, + e.g. `export FIREZONE_ID=$(uuidgen)`. The Gateway requires this variable at + startup. +1. Now, you can start the Gateway with: ``` -firezone-gateway --portal_token +firezone-gateway ``` -where `portal_token` is the token shown when creating a gateway group in the -Firezone admin portal. - -If you're running as an unprivileged user, you'll need the `CAP_NET_ADMIN` -capability to open `/dev/net/tun`. You can add this to the gateway binary with: +If you're running as a non-root user, you'll need the `CAP_NET_ADMIN` capability +to open `/dev/net/tun`. You can add this to the gateway binary with: ``` sudo setcap 'cap_net_admin+eip' /path/to/firezone-gateway diff --git a/rust/gateway/src/main.rs b/rust/gateway/src/main.rs index 9bbda7e34..13271984d 100644 --- a/rust/gateway/src/main.rs +++ b/rust/gateway/src/main.rs @@ -3,7 +3,7 @@ use crate::messages::InitGateway; use anyhow::{Context, Result}; use backoff::ExponentialBackoffBuilder; use clap::Parser; -use connlib_shared::{get_device_id, get_user_agent, login_url, Callbacks, Mode}; +use connlib_shared::{get_user_agent, login_url, Callbacks, Mode}; use firezone_cli_utils::{setup_global_subscriber, CommonArgs}; use firezone_tunnel::{GatewayState, Tunnel}; use futures::{future, TryFutureExt}; @@ -27,9 +27,9 @@ async fn main() -> Result<()> { let (connect_url, private_key) = login_url( Mode::Gateway, - cli.common.portal_url, - SecretString::new(cli.common.portal_token), - get_device_id(), + cli.common.api_url, + SecretString::new(cli.common.token), + cli.common.firezone_id, )?; let tunnel = Arc::new(Tunnel::new(private_key, CallbackHandler).await?); diff --git a/rust/linux-client/README.md b/rust/linux-client/README.md index 5bf474f22..462161953 100644 --- a/rust/linux-client/README.md +++ b/rust/linux-client/README.md @@ -4,22 +4,39 @@ This crate houses the Firezone linux client. ## Building -You can build the linux client using: -`cargo build --release --bin firezone-linux-client` +Assuming you have Rust installed, you can build the Linux client from a Linux +host with: + +``` +cargo build --release --bin firezone-linux-client +``` You should then find a binary in `target/release/firezone-linux-client`. ## Running -To run the linux client: +To run the Linux client: + +1. Generate a new Service account token from the "Actors -> Service Accounts" + section of the admin portal and save it in your secrets manager. The Firezone + Linux client requires a service account at this time. +1. Ensure the `FIREZONE_TOKEN=` environment variable is + set securely in your client's shell environment. The client requires this + variable at startup. +1. Set `FIREZONE_ID` to a unique string to identify this client in the portal, + e.g. `export FIREZONE_ID=$(uuidgen)`. The client requires this variable at + startup. +1. Set `LOG_DIR` to a suitable directory for writing logs + ``` + export LOG_DIR=/tmp/firezone-logs + mkdir $LOG_DIR + ``` +1. Now, you can start the client with: ``` -firezone-linux-client --portal_token +./firezone-linux-client ``` -where `portal_token` is the token shown when creating a client group in the -Firezone admin portal. - If you're running as an unprivileged user, you'll need the `CAP_NET_ADMIN` capability to open `/dev/net/tun`. You can add this to the client binary with: diff --git a/rust/linux-client/src/main.rs b/rust/linux-client/src/main.rs index 645cdb4cd..d1495cf20 100644 --- a/rust/linux-client/src/main.rs +++ b/rust/linux-client/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Parser; -use connlib_client_shared::{file_logger, get_device_id, Callbacks, Error, Session}; +use connlib_client_shared::{file_logger, Callbacks, Error, Session}; use firezone_cli_utils::{block_on_ctrl_c, setup_global_subscriber, CommonArgs}; use secrecy::SecretString; use std::path::PathBuf; @@ -11,12 +11,10 @@ fn main() -> Result<()> { let (layer, handle) = cli.log_dir.as_deref().map(file_logger::layer).unzip(); setup_global_subscriber(layer); - let device_id = get_device_id(); - let mut session = Session::connect( - cli.common.portal_url, - SecretString::from(cli.common.portal_token), - device_id, + cli.common.api_url, + SecretString::from(cli.common.token), + cli.common.firezone_id, CallbackHandler { handle }, ) .unwrap(); @@ -55,7 +53,7 @@ struct Cli { #[command(flatten)] common: CommonArgs, - /// File logging directory. - #[arg(short, long, env = "FZ_LOG_DIR")] + /// File logging directory. Should be a path that's writeable by the current user. + #[arg(short, long, env = "LOG_DIR")] log_dir: Option, } diff --git a/rust/relay/README.md b/rust/relay/README.md index 3430f53e4..5435cd2f0 100644 --- a/rust/relay/README.md +++ b/rust/relay/README.md @@ -22,19 +22,25 @@ You should then find a binary in `target/release/firezone-relay`. ## Running -To run the relay: +The Firezone Relay supports Linux only. To run the Relay binary on your Linux +host: + +1. Generate a new Relay token from the "Relays" section of the admin portal and + save it in your secrets manager. +1. Ensure the `FIREZONE_TOKEN=` environment variable is set + securely in your Relay's shell environment. The Relay expects this variable + at startup. +1. Now, you can start the Firezone Relay with: ``` -firezone-relay --portal_token +firezone-relay ``` -where `portal_token` is the token shown when creating a Relay in the Firezone -admin portal. +To view more advanced configuration options pass the `--help` flag: -For an up-to-date documentation on the available configurations options and a -detailed help text, run `cargo run --bin relay -- --help`. All command-line -options can be overridden using environment variables. Those variables are -listed in the `--help` output at the bottom of each command. +``` +firezone-relay --help +``` ### Ports @@ -44,9 +50,8 @@ not configurable. Additionally, the relay needs to have access to the port range ### Portal Connection -When given a `portal_token`, the relay will connect to the Firezone portal -(default `wss://api.firezone.dev`) and wait for an `init` message before -commencing relay operations. +When given a `token`, the relay will connect to the Firezone portal and wait for +an `init` message before commencing relay operations. ## Design diff --git a/rust/relay/src/main.rs b/rust/relay/src/main.rs index e6bb1921e..a1f4604b0 100644 --- a/rust/relay/src/main.rs +++ b/rust/relay/src/main.rs @@ -36,44 +36,48 @@ struct Args { /// The address of the local interface where we should serve our health-check endpoint. /// /// The actual health-check endpoint will be at `http:///healthz`. - #[arg(long, env, default_value = "0.0.0.0:8080")] + #[arg(long, env, hide = true, default_value = "0.0.0.0:8080")] health_check_addr: SocketAddr, // See https://www.rfc-editor.org/rfc/rfc8656.html#name-allocations /// The lowest port used for TURN allocations. - #[arg(long, env, default_value = "49152")] + #[arg(long, env, hide = true, default_value = "49152")] lowest_port: u16, /// The highest port used for TURN allocations. - #[arg(long, env, default_value = "65535")] + #[arg(long, env, hide = true, default_value = "65535")] highest_port: u16, - /// Firezone admin portal websocket URL - #[arg(long, env, default_value = "wss://api.firezone.dev")] - portal_url: Url, + #[arg( + long, + env = "FIREZONE_API_URL", + hide = true, + default_value = "wss://api.firezone.dev" + )] + api_url: Url, /// Token generated by the portal to authorize websocket connection. /// /// If omitted, we won't connect to the portal on startup. - #[arg(long, env)] - portal_token: Option, + #[arg(env = "FIREZONE_TOKEN")] + token: Option, /// A seed to use for all randomness operations. /// /// Only available in debug builds. - #[arg(long, env)] + #[arg(long, env, hide = true)] rng_seed: Option, /// How to format the logs. - #[arg(long, env, default_value = "human")] + #[arg(long, env, default_value = "human", hide = true)] log_format: LogFormat, /// Which OTLP collector we should connect to. /// /// If set, we will report traces and metrics to this collector via gRPC. - #[arg(long, env)] + #[arg(long, env, hide = true)] otlp_grpc_endpoint: Option, /// The Google Project ID to embed in spans. /// /// Set this if you are running on Google Cloud but using the OTLP trace collector. /// OTLP is vendor-agnostic but for spans to be correctly recognised by Google Cloud, they need the project ID to be set. - #[arg(long, env)] + #[arg(long, env, hide = true)] google_cloud_project_id: Option, } @@ -106,8 +110,8 @@ async fn main() -> Result<()> { args.highest_port, ); - let channel = if let Some(token) = args.portal_token.as_ref() { - let base_url = args.portal_url.clone(); + let channel = if let Some(token) = args.token.as_ref() { + let base_url = args.api_url.clone(); let stamp_secret = server.auth_secret(); let span = tracing::error_span!("connect_to_portal", config_url = %base_url); diff --git a/swift/apple/Firezone/xcconfig/dev.xcconfig b/swift/apple/Firezone/xcconfig/debug.xcconfig similarity index 100% rename from swift/apple/Firezone/xcconfig/dev.xcconfig rename to swift/apple/Firezone/xcconfig/debug.xcconfig diff --git a/swift/apple/Firezone/xcconfig/prod.xcconfig b/swift/apple/Firezone/xcconfig/release.xcconfig similarity index 100% rename from swift/apple/Firezone/xcconfig/prod.xcconfig rename to swift/apple/Firezone/xcconfig/release.xcconfig diff --git a/swift/apple/Firezone/xcconfig/staging.xcconfig b/swift/apple/Firezone/xcconfig/staging.xcconfig deleted file mode 100644 index 3a2aaa7e8..000000000 --- a/swift/apple/Firezone/xcconfig/staging.xcconfig +++ /dev/null @@ -1,17 +0,0 @@ -// Apple Developer account-specific configuration -DEVELOPMENT_TEAM = 47R2M6779T -PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.firezone -APP_GROUP_ID[sdk=macosx*] = 47R2M6779T.group.dev.firezone.firezone -APP_GROUP_ID[sdk=iphoneos*] = group.dev.firezone.firezone -CODE_SIGN_STYLE = Manual -CODE_SIGN_IDENTITY = Apple Distribution: Firezone, Inc. (47R2M6779T) -IOS_APP_PROVISIONING_PROFILE_IDENTIFIER = b32a5853-699d-4f19-85d3-5b13b1ac5dbb -MACOS_APP_PROVISIONING_PROFILE_IDENTIFIER = 70055d90-0252-4ee5-a60c-4d6f3840ee62 -IOS_NE_PROVISIONING_PROFILE_IDENTIFIER = 23402aaa-f72c-4947-a795-23a9cf495968 -MACOS_NE_PROVISIONING_PROFILE_IDENTIFIER = 1e965794-0b2c-46d3-a955-d96aacf25546 - -// Portal Auth -AUTH_URL_SCHEME = https -AUTH_URL_HOST = app.firez.one -CONTROL_PLANE_URL_SCHEME = wss -CONTROL_PLANE_URL_HOST = api.firez.one diff --git a/swift/apple/README.md b/swift/apple/README.md index e998bb25b..ff9ccdf76 100644 --- a/swift/apple/README.md +++ b/swift/apple/README.md @@ -30,7 +30,7 @@ CI/CD pipeline. 1. Copy an appropriate xcconfig and edit as necessary: ```bash - cp Firezone/xcconfig/dev.xcconfig Firezone/xcconfig/config.xcconfig + cp Firezone/xcconfig/debug.xcconfig Firezone/xcconfig/config.xcconfig vim Firezone/xcconfig/config.xcconfig ``` diff --git a/terraform/environments/production/gateways.tf b/terraform/environments/production/gateways.tf index 92c35d393..9ae917e18 100644 --- a/terraform/environments/production/gateways.tf +++ b/terraform/environments/production/gateways.tf @@ -32,7 +32,7 @@ resource "google_compute_subnetwork" "gateways" { } module "gateways" { - count = var.gateway_portal_token != null ? 1 : 0 + count = var.gateway_token != null ? 1 : 0 source = "../../modules/gateway-google-cloud-compute" project_id = module.google-cloud-project.project.project_id @@ -74,14 +74,14 @@ module "gateways" { } } - portal_websocket_url = "wss://api.${local.tld}" - portal_token = var.gateway_portal_token + api_url = "wss://api.${local.tld}" + token = var.gateway_token } # Allow inbound traffic # resource "google_compute_firewall" "ingress-ipv4" { -# count = var.gateway_portal_token != null ? 1 : 0 +# count = var.gateway_token != null ? 1 : 0 # project = module.google-cloud-project.project.project_id @@ -98,7 +98,7 @@ module "gateways" { # } # resource "google_compute_firewall" "ingress-ipv6" { -# count = var.gateway_portal_token != null ? 1 : 0 +# count = var.gateway_token != null ? 1 : 0 # project = module.google-cloud-project.project.project_id @@ -116,7 +116,7 @@ module "gateways" { # Allow outbound traffic resource "google_compute_firewall" "egress-ipv4" { - count = var.gateway_portal_token != null ? 1 : 0 + count = var.gateway_token != null ? 1 : 0 project = module.google-cloud-project.project.project_id @@ -133,7 +133,7 @@ resource "google_compute_firewall" "egress-ipv4" { } resource "google_compute_firewall" "egress-ipv6" { - count = var.gateway_portal_token != null ? 1 : 0 + count = var.gateway_token != null ? 1 : 0 project = module.google-cloud-project.project.project_id diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf index 2d3a1e1ee..19e8cc1df 100644 --- a/terraform/environments/production/main.tf +++ b/terraform/environments/production/main.tf @@ -619,7 +619,7 @@ resource "google_project_iam_member" "application" { # Deploy relays module "relays" { - count = var.relay_portal_token != null ? 1 : 0 + count = var.relay_token != null ? 1 : 0 source = "../../modules/relay-app" project_id = module.google-cloud-project.project.project_id @@ -708,8 +708,8 @@ module "relays" { } } - portal_websocket_url = "wss://api.${local.tld}" - portal_token = var.relay_portal_token + api_url = "wss://api.${local.tld}" + token = var.relay_token depends_on = [ module.api diff --git a/terraform/environments/production/variables.tf b/terraform/environments/production/variables.tf index 22f329b35..c15f17827 100644 --- a/terraform/environments/production/variables.tf +++ b/terraform/environments/production/variables.tf @@ -3,12 +3,12 @@ variable "image_tag" { description = "Image tag for all services. Notice: we assume all services are deployed with the same version" } -variable "relay_portal_token" { +variable "relay_token" { type = string default = null } -variable "gateway_portal_token" { +variable "gateway_token" { type = string default = null } diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf index 0770eda97..5963b8cd0 100644 --- a/terraform/environments/staging/main.tf +++ b/terraform/environments/staging/main.tf @@ -627,7 +627,7 @@ resource "google_project_iam_member" "application" { # Deploy relays module "relays" { - count = var.relay_portal_token != null ? 1 : 0 + count = var.relay_token != null ? 1 : 0 source = "../../modules/relay-app" project_id = module.google-cloud-project.project.project_id @@ -716,8 +716,8 @@ module "relays" { } } - portal_websocket_url = "wss://api.${local.tld}" - portal_token = var.relay_portal_token + api_url = "wss://api.${local.tld}" + token = var.relay_token } # Enable SSH on staging diff --git a/terraform/environments/staging/variables.tf b/terraform/environments/staging/variables.tf index 17f9ad46b..a095a0da7 100644 --- a/terraform/environments/staging/variables.tf +++ b/terraform/environments/staging/variables.tf @@ -3,7 +3,7 @@ variable "image_tag" { description = "Image tag for all services. Notice: we assume all services are deployed with the same version" } -variable "relay_portal_token" { +variable "relay_token" { type = string default = null } diff --git a/terraform/modules/gateway-google-cloud-compute/main.tf b/terraform/modules/gateway-google-cloud-compute/main.tf index bdd5ed2ce..e51e9f394 100644 --- a/terraform/modules/gateway-google-cloud-compute/main.tf +++ b/terraform/modules/gateway-google-cloud-compute/main.tf @@ -40,12 +40,12 @@ locals { value = "127.0.0.1:4317" }, { - name = "PORTAL_TOKEN" - value = var.portal_token + name = "FIREZONE_TOKEN" + value = var.token }, { - name = "PORTAL_URL" - value = var.portal_websocket_url + name = "FIREZONE_API_URL" + value = var.api_url } ], var.application_environment_variables) } diff --git a/terraform/modules/gateway-google-cloud-compute/variables.tf b/terraform/modules/gateway-google-cloud-compute/variables.tf index c189ab0dc..0f0575e1b 100644 --- a/terraform/modules/gateway-google-cloud-compute/variables.tf +++ b/terraform/modules/gateway-google-cloud-compute/variables.tf @@ -148,12 +148,12 @@ variable "application_environment_variables" { ## Firezone ################################################################################ -variable "portal_token" { +variable "token" { type = string description = "Portal token to use for authentication." } -variable "portal_websocket_url" { +variable "api_url" { type = string default = "wss://api.firezone.dev" description = "URL of the control plane endpoint." diff --git a/terraform/modules/relay-app/main.tf b/terraform/modules/relay-app/main.tf index 4af3fdbca..c4a59106d 100644 --- a/terraform/modules/relay-app/main.tf +++ b/terraform/modules/relay-app/main.tf @@ -38,12 +38,12 @@ locals { value = "127.0.0.1:4317" }, { - name = "PORTAL_TOKEN" - value = var.portal_token + name = "FIREZONE_TOKEN" + value = var.token }, { - name = "PORTAL_URL" - value = var.portal_websocket_url + name = "FIREZONE_API_URL" + value = var.api_url } ], var.application_environment_variables) } diff --git a/terraform/modules/relay-app/variables.tf b/terraform/modules/relay-app/variables.tf index f95a4721b..33521d762 100644 --- a/terraform/modules/relay-app/variables.tf +++ b/terraform/modules/relay-app/variables.tf @@ -133,12 +133,12 @@ variable "application_environment_variables" { ## Firezone ################################################################################ -variable "portal_token" { +variable "token" { type = string description = "Portal token to use for authentication." } -variable "portal_websocket_url" { +variable "api_url" { type = string default = "wss://api.firezone.dev" description = "URL of the control plane endpoint."