diff --git a/.github/workflows/_rust.yml b/.github/workflows/_rust.yml index 1d6ecb7a9..eddc6c527 100644 --- a/.github/workflows/_rust.yml +++ b/.github/workflows/_rust.yml @@ -125,7 +125,7 @@ jobs: matrix: # TODO: Add Windows as part of issue #3782 runs-on: [ubuntu-20.04, ubuntu-22.04] - test: [linux-group] + test: [linux-group, token-path] runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v4 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6da129b84..ec0f2d006 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1952,6 +1952,7 @@ dependencies = [ "dirs", "firezone-cli-utils", "futures", + "git-version", "humantime", "nix 0.28.0", "resolv-conf", diff --git a/rust/headless-client/Cargo.toml b/rust/headless-client/Cargo.toml index c0d6fdbdf..8140b4a10 100644 --- a/rust/headless-client/Cargo.toml +++ b/rust/headless-client/Cargo.toml @@ -10,11 +10,13 @@ authors = ["Firezone, Inc."] [dependencies] anyhow = { version = "1.0" } clap = { version = "4.5", features = ["derive", "env"] } +git-version = "0.3.9" humantime = "2.1" serde = { version = "1.0.197", features = ["derive"] } # This actually relies on many other features in Tokio, so this will probably # fail to build outside the workspace. tokio = { version = "1.36.0", features = ["macros", "signal"] } +tracing = { workspace = true } url = { version = "2.3.1", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] @@ -29,7 +31,6 @@ sd-notify = "0.4.1" # This is a pure Rust re-implementation, so it isn't vulnera serde_json = "1.0.115" secrecy = { workspace = true } tokio-util = { version = "0.7.10", features = ["codec"] } -tracing = { workspace = true } [lints] workspace = true diff --git a/rust/headless-client/src/linux.rs b/rust/headless-client/src/imp_linux.rs similarity index 96% rename from rust/headless-client/src/linux.rs rename to rust/headless-client/src/imp_linux.rs index ca0eb4f1a..21a97b11c 100644 --- a/rust/headless-client/src/linux.rs +++ b/rust/headless-client/src/imp_linux.rs @@ -1,3 +1,5 @@ +//! Implementation, Linux-specific + use super::{Cli, Cmd}; use anyhow::{bail, Context, Result}; use clap::Parser; @@ -29,11 +31,19 @@ const ROOT_USER: u32 = 0; /// on some systems, `/run` should be the newer version. const SOCK_PATH: &str = "/run/firezone-client.sock"; +pub fn default_token_path() -> PathBuf { + PathBuf::from("/etc") + .join(connlib_shared::BUNDLE_ID) + .join("token") +} + pub async fn run() -> Result<()> { let cli = Cli::parse(); let (layer, _handle) = cli.log_dir.as_deref().map(file_logger::layer).unzip(); setup_global_subscriber(layer); + tracing::info!(git_version = crate::GIT_VERSION); + match cli.command() { Cmd::Auto => { if let Some(token) = token(&cli)? { @@ -44,7 +54,12 @@ pub async fn run() -> Result<()> { } Cmd::IpcService => run_ipc_service(cli).await, Cmd::Standalone => { - let token = token(&cli)?.context("Need a token to run as standalone Client")?; + let token = token(&cli)?.with_context(|| { + format!( + "Can't find the Firezone token in $FIREZONE_TOKEN or in `{}`", + cli.token_path + ) + })?; run_standalone(cli, &token).await } Cmd::StubIpcClient => run_debug_ipc_client(cli).await, @@ -55,9 +70,7 @@ pub async fn run() -> Result<()> { /// /// Sync because we do blocking file I/O fn token(cli: &Cli) -> Result> { - let path = PathBuf::from("/etc") - .join(connlib_shared::BUNDLE_ID) - .join("token.txt"); + let path = PathBuf::from(&cli.token_path); if let Some(token) = &cli.token { // Token was provided in CLI args or env var @@ -124,6 +137,11 @@ async fn run_standalone(cli: Cli, token: &SecretString) -> Result<()> { let (private_key, public_key) = keypair(); let login = LoginUrl::client(cli.api_url, token, firezone_id, None, public_key.to_bytes())?; + if cli.check { + tracing::info!("Check passed"); + return Ok(()); + } + let session = Session::connect( login, Sockets::new(), diff --git a/rust/headless-client/src/lib.rs b/rust/headless-client/src/lib.rs index 9ec2e5c27..c38243a93 100644 --- a/rust/headless-client/src/lib.rs +++ b/rust/headless-client/src/lib.rs @@ -10,25 +10,43 @@ use std::path::PathBuf; -#[cfg(target_os = "linux")] -mod linux; +pub use imp::{default_token_path, run}; #[cfg(target_os = "linux")] -pub use linux::run; +mod imp_linux; +#[cfg(target_os = "linux")] +use imp_linux as imp; #[cfg(target_os = "windows")] -mod windows { +mod imp_windows { use clap::Parser; + pub fn default_token_path() -> std::path::PathBuf { + todo!() + } + pub async fn run() -> anyhow::Result<()> { let cli = super::Cli::parse(); let _cmd = cli.command(); + tracing::info!(git_version = crate::GIT_VERSION); Ok(()) } } - #[cfg(target_os = "windows")] -pub use windows::run; +use imp_windows as imp; + +/// Output of `git describe` at compile time +/// e.g. `1.0.0-pre.4-20-ged5437c88-modified` where: +/// +/// * `1.0.0-pre.4` is the most recent ancestor tag +/// * `20` is the number of commits since then +/// * `g` doesn't mean anything +/// * `ed5437c88` is the Git commit hash +/// * `-modified` is present if the working dir has any changes from that commit number +pub const GIT_VERSION: &str = git_version::git_version!( + args = ["--always", "--dirty=-modified", "--tags"], + fallback = "unknown" +); #[derive(clap::Parser)] #[command(author, version, about, long_about = None)] @@ -45,6 +63,13 @@ struct Cli { )] pub api_url: url::Url, + /// Check the configuration and return 0 before connecting to the API + /// + /// Returns 1 if the configuration is wrong. Mostly non-destructive but may + /// write a device ID to disk if one is not found. + #[arg(long)] + check: bool, + /// Token generated by the portal to authorize websocket connection. // TODO: It isn't good for security to pass the token as a CLI arg. @@ -53,6 +78,10 @@ struct Cli { #[arg(env = "FIREZONE_TOKEN", hide = true, long)] pub token: Option, + /// A filesystem path where the token can be found + #[arg(default_value_t = default_token_path().display().to_string(), env = "FIREZONE_TOKEN_PATH", long)] + token_path: String, + /// Identifier used by the portal to identify and display the device. // AKA `device_id` in the Windows and Linux GUI clients @@ -79,7 +108,7 @@ impl Cli { #[derive(clap::Subcommand, Clone, Copy)] enum Cmd { - /// If there is a token on disk, run in standalone mode. Otherwise, run as an IPC daemon. This will be removed in a future version. + /// If there is a token on disk, run in standalone mode. Otherwise, run as an IPC service. This will be removed in a future version. #[command(hide = true)] Auto, /// Listen for IPC connections and act as a privileged tunnel process for a GUI client diff --git a/scripts/tests/lib.sh b/scripts/tests/lib.sh index 73eff3dd1..568089a88 100755 --- a/scripts/tests/lib.sh +++ b/scripts/tests/lib.sh @@ -65,10 +65,14 @@ function assert_process_state { function create_token_file { CONFIG_DIR=/etc/dev.firezone.client - TOKEN_PATH="$CONFIG_DIR/token.txt" + TOKEN_PATH="$CONFIG_DIR/token" sudo mkdir "$CONFIG_DIR" sudo touch "$TOKEN_PATH" sudo chmod 600 "$TOKEN_PATH" echo "n.SFMyNTY.g2gDaANtAAAAJGM4OWJjYzhjLTkzOTItNGRhZS1hNDBkLTg4OGFlZjZkMjhlMG0AAAAkN2RhN2QxY2QtMTExYy00NGE3LWI1YWMtNDAyN2I5ZDIzMGU1bQAAACtBaUl5XzZwQmstV0xlUkFQenprQ0ZYTnFJWktXQnMyRGR3XzJ2Z0lRdkZnbgYAGUmu74wBYgABUYA.UN3vSLLcAMkHeEh5VHumPOutkuue8JA6wlxM9JxJEPE" | sudo tee "$TOKEN_PATH" > /dev/null + + # Also put it in `token.txt` for backwards compat, until pull #4666 merges and is + # cut into a release. + sudo cp "$TOKEN_PATH" "$TOKEN_PATH.txt" } diff --git a/scripts/tests/linux-group.sh b/scripts/tests/linux-group.sh index bb9ca3cf6..3fb9afa51 100755 --- a/scripts/tests/linux-group.sh +++ b/scripts/tests/linux-group.sh @@ -10,12 +10,6 @@ FZ_GROUP="firezone" SERVICE_NAME=firezone-client export RUST_LOG=info -function print_debug_info { - systemctl status "$SERVICE_NAME" -} - -trap print_debug_info EXIT - # Copy the Linux Client out of the build dir ls . ./rust ./rust/target ./rust/target/debug sudo cp "rust/target/debug/firezone-headless-client" "/usr/bin/$BINARY_NAME" @@ -35,5 +29,8 @@ sudo su --login "$USER" --command RUST_LOG="$RUST_LOG" "$BINARY_NAME" stub-ipc-c echo "# Expect Firezone to reject our command if we run without 'su --login'" "$BINARY_NAME" stub-ipc-client && exit 1 +# Stop the service in case other tests run on the same VM +sudo systemctl stop "$SERVICE_NAME" + # Explicitly exiting is needed when we're intentionally having commands fail exit 0 diff --git a/scripts/tests/systemd/dns-systemd-resolved.sh b/scripts/tests/systemd/dns-systemd-resolved.sh index f9594c44a..f247537d5 100755 --- a/scripts/tests/systemd/dns-systemd-resolved.sh +++ b/scripts/tests/systemd/dns-systemd-resolved.sh @@ -30,7 +30,7 @@ curl --interface "$FZ_IFACE" $HTTPBIN/get && exit 1 # Start Firezone resolvectl dns tun-firezone && exit 1 stat /usr/bin/firezone-linux-client -sudo systemctl start "$SERVICE_NAME" +sudo systemctl start "$SERVICE_NAME" || systemctl status "$SERVICE_NAME" resolvectl dns tun-firezone resolvectl query "$HTTPBIN" diff --git a/scripts/tests/token-path.sh b/scripts/tests/token-path.sh new file mode 100755 index 000000000..bc21a4a23 --- /dev/null +++ b/scripts/tests/token-path.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +source "./scripts/tests/lib.sh" + +BINARY_NAME=firezone-linux-client +TOKEN_PATH="token" + +sudo cp "rust/target/debug/firezone-headless-client" "/usr/bin/$BINARY_NAME" + +# Check should fail because there's no token yet +sudo "$BINARY_NAME" standalone --check && exit 1 + +touch "$TOKEN_PATH" +chmod 600 "$TOKEN_PATH" +sudo chown root:root "$TOKEN_PATH" +echo "n.SFMyNTY.g2gDaANtAAAAJGM4OWJjYzhjLTkzOTItNGRhZS1hNDBkLTg4OGFlZjZkMjhlMG0AAAAkN2RhN2QxY2QtMTExYy00NGE3LWI1YWMtNDAyN2I5ZDIzMGU1bQAAACtBaUl5XzZwQmstV0xlUkFQenprQ0ZYTnFJWktXQnMyRGR3XzJ2Z0lRdkZnbgYAGUmu74wBYgABUYA.UN3vSLLcAMkHeEh5VHumPOutkuue8JA6wlxM9JxJEPE" | sudo tee "$TOKEN_PATH" > /dev/null + +# Check should fail because the token is not in the default path +sudo "$BINARY_NAME" --check standalone && exit 1 + +# Check should pass if we tell it where to look +sudo "$BINARY_NAME" --check --token-path "$TOKEN_PATH" standalone + +# Move the token to the default path +sudo mkdir /etc/dev.firezone.client +sudo mv "$TOKEN_PATH" /etc/dev.firezone.client/token + +# Check should now pass with the default path +sudo "$BINARY_NAME" --check standalone + +# Redundant, but helps if the last command has an `&& exit 1` +exit 0 diff --git a/website/src/app/kb/user-guides/linux-client/readme.mdx b/website/src/app/kb/user-guides/linux-client/readme.mdx index b9dd71b7f..ef8d33868 100644 --- a/website/src/app/kb/user-guides/linux-client/readme.mdx +++ b/website/src/app/kb/user-guides/linux-client/readme.mdx @@ -66,6 +66,8 @@ Commands: Options: --token Token generated by the portal to authorize websocket connection [env: FIREZONE_TOKEN=] + --token-path + A filesystem path where the token can be found [env: FIREZONE_TOKEN_PATH=] [default: /etc/dev.firezone.client/token] -i, --firezone-id Identifier used by the portal to identify and display the device [env: FIREZONE_ID=] -l, --log-dir