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 <jamilbk@users.noreply.github.com>
Co-authored-by: Gabi <gabrielalejandro7@gmail.com>
This commit is contained in:
Jamil
2023-10-30 23:46:53 -07:00
committed by GitHub
parent 01f7839d0f
commit 2bca378f17
50 changed files with 569 additions and 316 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,7 @@
android:exported="true"
android:launchMode="singleTop">
<!-- Staging -->
<intent-filter
android:label="@string/app_name"
android:autoVerify="true">
@@ -66,6 +67,24 @@
<data android:host="app.firez.one" />
<data android:pathPrefix="/handle_client_auth_callback" />
</intent-filter>
<!-- Prod -->
<intent-filter
android:label="@string/app_name"
android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- These must match the resulting URL from the Portal exactly.
Don't use variables here otherwise this can break when testing in the emulator.
For this to work in the emulator, you must use a host/IP that *both* the emulator and the
host can access. E.g. 10.0.2.2 will not work. -->
<data android:scheme="https" />
<data android:host="app.firezone.dev" />
<data android:pathPrefix="/handle_client_auth_callback" />
</intent-filter>
</activity>
<activity

View File

@@ -3,6 +3,7 @@ package dev.firezone.android.core
import android.content.SharedPreferences
import dev.firezone.android.BuildConfig
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response
@@ -14,16 +15,15 @@ internal class BaseUrlInterceptor(
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val accountId = sharedPreferences.getString(ACCOUNT_ID_KEY, "") ?: ""
val newUrl =
originalRequest.url.newBuilder()
.scheme(BuildConfig.AUTH_SCHEME)
.host(BuildConfig.AUTH_HOST)
.port(BuildConfig.AUTH_PORT)
.addPathSegment(accountId)
.build()
val newUrl = "${BuildConfig.AUTH_BASE_URL}/$accountId"?.toHttpUrlOrNull()
if (newUrl == null) {
throw IllegalStateException("Invalid AUTH_BASE_URL. Check Settings?")
}
val newRequest =
originalRequest.newBuilder()
.url(newUrl)
.url(newUrl!!)
.build()
return chain.proceed(newRequest)
}

View File

@@ -9,7 +9,12 @@ internal interface PreferenceRepository {
fun getConfig(): Flow<Config>
fun saveAccountId(value: String): Flow<Unit>
fun saveSettings(
accountId: String,
authBaseUrl: String,
apiUrl: String,
logFilter: String,
): Flow<Unit>
fun saveToken(value: String): Flow<Unit>

View File

@@ -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<Unit> =
override fun saveSettings(
accountId: String,
authBaseUrl: String,
apiUrl: String,
logFilter: String,
): Flow<Unit> =
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<Boolean> =
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"
}

View File

@@ -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?,
)

View File

@@ -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<Unit> = repository.saveAccountId(accountId)
operator fun invoke(
accountId: String,
authBaseUrl: String,
apiUrl: String,
logFilter: String,
): Flow<Unit> = repository.saveSettings(accountId, authBaseUrl, apiUrl, logFilter)
}

View File

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

View File

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

View File

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

View File

@@ -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<ViewState>()
val stateLiveData: LiveData<ViewState> = stateMutableLiveData
@@ -25,13 +27,21 @@ internal class SettingsViewModel
private val actionMutableLiveData = MutableLiveData<ViewAction>()
val actionLiveData: LiveData<ViewAction> = 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,
)
}

View File

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

View File

@@ -3,7 +3,7 @@ package dev.firezone.android.tunnel
object TunnelSession {
external fun connect(
controlPlaneUrl: String,
apiUrl: String,
token: String,
deviceId: String,
logDir: String,

View File

@@ -10,10 +10,10 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AUTH Complete"
app:layout_constraintTop_toTopOf="parent"
android:text="Signed in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -36,7 +36,7 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvSignStatus"
android:id="@+id/tvLaunchingAuthFlow"
style="@style/AppTheme.Base.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -34,7 +34,7 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvSignStatus"
android:id="@+id/tvResourcesList"
style="@style/AppTheme.Base.H5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -43,10 +43,10 @@
app:layout_constraintTop_toBottomOf="@+id/llContainer" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/resourcesList"
android:id="@+id/rvResourcesList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/tvSignStatus"
app:layout_constraintTop_toBottomOf="@id/tvResourcesList"
app:layout_constraintBottom_toTopOf="@id/btSignOut" />
<com.google.android.material.button.MaterialButton

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
@@ -31,36 +32,157 @@
android:text="@string/app_short_name" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ilUrlInput"
android:layout_width="0dp"
<TextView
android:id="@+id/tvSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:errorEnabled="true"
android:hint="@string/enter_team_id"
android:text="@string/settings_title"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.225" />
<TextView
android:id="@+id/tvRequiredSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/required_settings_title"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.335" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ilAccountIdInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.42">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAccountIdInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/account_id"
android:importantForAutofill="no"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvAdvancedSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/advanced_settings_title"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.54" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ilAuthBaseUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.66">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAuthBaseUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/auth_base_url"
android:importantForAutofill="no"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ilApiUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.78">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etInput"
android:id="@+id/etApiUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/account_id"
android:hint="@string/api_url"
android:importantForAutofill="no"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btLogin"
android:layout_width="0dp"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ilLogFilterInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.90">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etLogFilterInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/log_filter"
android:importantForAutofill="no"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btSaveSettings"
android:layout_width="135dp"
android:layout_height="50dp"
android:layout_marginBottom="8dp"
android:enabled="false"
android:text="@string/save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btCancel"
android:layout_width="135dp"
android:layout_height="50dp"
android:layout_marginBottom="8dp"
android:enabled="true"
android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -34,7 +34,7 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvSignStatus"
android:id="@+id/tvSessionStatus"
style="@style/AppTheme.Base.HeaderText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -47,20 +47,24 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btSignIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_width="135dp"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:text="@string/sign_in"
app:layout_constraintBottom_toTopOf="@+id/btSettings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btSettings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_width="135dp"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:text="@string/settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -2,9 +2,16 @@
<string name="app_short_name">firezone</string>
<!-- Settings -->
<string name="account_id">account-id</string>
<string name="settings_title">Settings</string>
<string name="required_settings_title">Required</string>
<string name="advanced_settings_title">Advanced</string>
<string name="account_id">Account ID</string>
<string name="auth_base_url">Auth Base URL</string>
<string name="api_url">API URL</string>
<string name="log_filter">Log Filter</string>
<string name="save">Save</string>
<!-- Sign In -->
<string name="sign_in">Sign In</string>
<string name="settings">Settings</string>
@@ -15,18 +22,14 @@
<string name="sign_out">Sign Out</string>
<!-- Auth -->
<string name="launching_auth_flow">Launching Auth Flow…</string>
<string name="launching_auth_flow">Launching Chrome to sign in...</string>
<!-- Error Dialog -->
<string name="error_dialog_title">Error</string>
<string name="error_dialog_message">Ops, something went wrong. Please try again later.</string>
<string name="error_dialog_message">Oops! Something went wrong. Contact your admin if this issue persists.</string>
<string name="error_dialog_button_text">Ok</string>
<string name="enable_vpn_permission">Enable VPN Permission</string>
<string name="vpn_permission_description">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.</string>
<string name="request_permission">Request Permission</string>
<string name="enter_team_id">Enter team id</string>
<string name="sign_in_debug_user">Sign In (Debug User)</string>
<string name="signing_in_requires_chrome_browser">Signing in requires chrome browser</string>
<!-- Error Dialog -->
<string name="signing_in_requires_chrome_browser">Signing in requires Chrome browser</string>
</resources>

View File

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

View File

@@ -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<Session<CallbackHandler>, 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,

View File

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

View File

@@ -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<Url>,
api_url: impl TryInto<Url>,
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<StopRuntime>,
portal_url: Url,
api_url: Url,
token: SecretString,
device_id: String,
callbacks: CallbackErrorFacade<CB>,
) {
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
);

View File

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

View File

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

View File

@@ -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=<gateway_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 <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

View File

@@ -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?);

View File

@@ -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=<service_account_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 <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:

View File

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

View File

@@ -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=<relay_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 <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

View File

@@ -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://<health_check_addr>/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<SecretString>,
#[arg(env = "FIREZONE_TOKEN")]
token: Option<SecretString>,
/// A seed to use for all randomness operations.
///
/// Only available in debug builds.
#[arg(long, env)]
#[arg(long, env, hide = true)]
rng_seed: Option<u64>,
/// 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<SocketAddr>,
/// 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<String>,
}
@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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