diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 881004390..690f01e25 100755 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1162,6 +1162,7 @@ dependencies = [ "futures-util", "hickory-resolver", "ip_network", + "libc", "log", "os_info", "parking_lot", @@ -1883,6 +1884,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "uuid", "webrtc", ] diff --git a/rust/connlib/clients/shared/src/lib.rs b/rust/connlib/clients/shared/src/lib.rs index cfc7a565a..1f046ca9a 100644 --- a/rust/connlib/clients/shared/src/lib.rs +++ b/rust/connlib/clients/shared/src/lib.rs @@ -130,7 +130,7 @@ where ) { runtime.spawn(async move { let (connect_url, private_key) = fatal_error!( - login_url(Mode::Client, api_url, token, device_id), + login_url(Mode::Client, api_url, token, device_id, None), runtime_stopper, &callbacks ); diff --git a/rust/connlib/shared/Cargo.toml b/rust/connlib/shared/Cargo.toml index eb7bee37d..050dd34d9 100644 --- a/rust/connlib/shared/Cargo.toml +++ b/rust/connlib/shared/Cargo.toml @@ -34,6 +34,7 @@ webrtc = { workspace = true } ring = "0.17" hickory-resolver = { workspace = true } domain = { workspace = true } +libc = "0.2" # Needed for Android logging until tracing is working log = "0.4" diff --git a/rust/connlib/shared/src/lib.rs b/rust/connlib/shared/src/lib.rs index 83641073f..8a74c6bd9 100644 --- a/rust/connlib/shared/src/lib.rs +++ b/rust/connlib/shared/src/lib.rs @@ -16,8 +16,6 @@ pub use error::Result; use boringtun::x25519::{PublicKey, StaticSecret}; use messages::Key; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; use ring::digest::{Context, SHA256}; use secrecy::{ExposeSecret, SecretString}; use std::net::Ipv4Addr; @@ -30,20 +28,30 @@ pub type Dname = domain::base::Dname>; const VERSION: &str = env!("CARGO_PKG_VERSION"); const LIB_NAME: &str = "connlib"; +// From https://man7.org/linux/man-pages/man2/gethostname.2.html +// SUSv2 guarantees that "Host names are limited to 255 bytes". +// POSIX.1 guarantees that "Host names (not including the +// terminating null byte) are limited to HOST_NAME_MAX bytes". On +// Linux, HOST_NAME_MAX is defined with the value 64, which has been +// the limit since Linux 1.0 (earlier kernels imposed a limit of 8 +// bytes) +// +// We are counting the nul-byte +#[cfg(not(target_os = "windows"))] +const HOST_NAME_MAX: usize = 256; + /// Creates a new login URL to use with the portal. pub fn login_url( mode: Mode, api_url: Url, token: SecretString, device_id: String, + firezone_name: Option, ) -> Result<(Url, StaticSecret)> { let private_key = StaticSecret::random_from_rng(rand::rngs::OsRng); - // FIXME: read FIREZONE_NAME from env (eg. for gateways) and use system hostname by default - let name: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect(); + let name = firezone_name + .or(get_host_name()) + .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); let external_id = sha256(device_id); let url = get_websocket_path( @@ -76,6 +84,23 @@ pub fn get_user_agent() -> String { format!("{os_type}/{os_version} {lib_name}/{lib_version}") } +#[cfg(not(target_os = "windows"))] +fn get_host_name() -> Option { + let mut buf = [0; HOST_NAME_MAX]; + // SAFETY: we just allocated a buffer with that size + if unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, HOST_NAME_MAX) } != 0 { + return None; + } + + String::from_utf8(buf.split(|c| *c == 0).next()?.to_vec()).ok() +} + +#[cfg(target_os = "windows")] +fn get_host_name() -> Option { + // FIXME: windows + None +} + fn set_ws_scheme(url: &mut Url) -> Result<()> { let scheme = match url.scheme() { "http" | "ws" => "ws", diff --git a/rust/firezone-cli-utils/src/lib.rs b/rust/firezone-cli-utils/src/lib.rs index 9b4567102..cb6a53cf3 100644 --- a/rust/firezone-cli-utils/src/lib.rs +++ b/rust/firezone-cli-utils/src/lib.rs @@ -34,10 +34,10 @@ pub struct CommonArgs { default_value = "wss://api.firezone.dev" )] 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(env = "FIREZONE_TOKEN")] pub token: String, + /// Friendly name to display in the UI + #[arg(short = 'n', long, env = "FIREZONE_NAME")] + pub firezone_name: Option, } diff --git a/rust/gateway/Cargo.toml b/rust/gateway/Cargo.toml index ff37d1d10..7ddff1452 100644 --- a/rust/gateway/Cargo.toml +++ b/rust/gateway/Cargo.toml @@ -28,6 +28,7 @@ tracing-subscriber = "0.3.17" url = { version = "2.4.1", default-features = false } webrtc = { workspace = true } domain = { workspace = true } +uuid = { version = "1.6.1", features = ["v4"] } [dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/rust/gateway/src/main.rs b/rust/gateway/src/main.rs index aaffe2e43..f94927e28 100644 --- a/rust/gateway/src/main.rs +++ b/rust/gateway/src/main.rs @@ -13,39 +13,74 @@ use messages::{EgressMessages, IngressMessages}; use phoenix_channel::{PhoenixChannel, SecureUrl}; use secrecy::{Secret, SecretString}; use std::convert::Infallible; +use std::path::Path; use std::pin::pin; use std::sync::Arc; +use tokio::io::AsyncWriteExt; use tokio::signal::ctrl_c; use tokio_tungstenite::tungstenite; use tracing_subscriber::layer; use url::Url; +use uuid::Uuid; mod eventloop; mod messages; +const ID_PATH: &str = "/var/lib/firezone/gateway_id"; + #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); setup_global_subscriber(layer::Identity::new()); + let firezone_id = get_firezone_id(cli.firezone_id).await + .context("Couldn't read FIREZONE_ID or write it to disk: Please provide it through the env variable or provide rw access to /var/lib/firezone/")?; let (connect_url, private_key) = login_url( Mode::Gateway, cli.common.api_url, SecretString::new(cli.common.token), - cli.common.firezone_id, + firezone_id, + cli.common.firezone_name, )?; - let task = tokio::spawn(async move { run(connect_url, private_key).await }).map_err(Into::into); + let task = tokio::spawn(run(connect_url, private_key)).err_into(); let ctrl_c = pin!(ctrl_c().map_err(anyhow::Error::new)); - future::try_select(task, ctrl_c) + match future::try_select(task, ctrl_c) .await - .map_err(|e| e.factor_first().0)?; + .map_err(|e| e.factor_first().0)? + { + future::Either::Left((res, _)) => { + res?; + } + future::Either::Right(_) => {} + }; Ok(()) } +async fn get_firezone_id(env_id: Option) -> Result { + if let Some(id) = env_id { + if !id.is_empty() { + return Ok(id); + } + } + + if let Ok(id) = tokio::fs::read_to_string(ID_PATH).await { + if !id.is_empty() { + return Ok(id); + } + } + + let id_path = Path::new(ID_PATH); + tokio::fs::create_dir_all(id_path.parent().unwrap()).await?; + let mut id_file = tokio::fs::File::create(id_path).await?; + let id = Uuid::new_v4().to_string(); + id_file.write_all(id.as_bytes()).await?; + Ok(id) +} + async fn run(connect_url: Url, private_key: StaticSecret) -> Result { let tunnel: Arc> = Arc::new(Tunnel::new(private_key, CallbackHandler).await?); @@ -83,9 +118,10 @@ async fn run(connect_url: Url, private_key: StaticSecret) -> Result .context("Eventloop failed") }); - future::try_select(portal_task, eventloop_task) + let res = future::try_select(portal_task, eventloop_task) .await .map_err(|e| e.factor_first().0)?; + res.factor_first().0?; unreachable!("should never exit without error"); } @@ -183,6 +219,9 @@ impl Callbacks for CallbackHandler { struct Cli { #[command(flatten)] common: CommonArgs, + /// Identifier generated by the portal to identify and display the device. + #[arg(short = 'i', long, env = "FIREZONE_ID")] + pub firezone_id: Option, } /// Checks whether the given [`std::error::Error`] is in-fact an HTTP error with a 4xx status code. diff --git a/rust/linux-client/src/main.rs b/rust/linux-client/src/main.rs index d1495cf20..ed958949c 100644 --- a/rust/linux-client/src/main.rs +++ b/rust/linux-client/src/main.rs @@ -14,7 +14,7 @@ fn main() -> Result<()> { let mut session = Session::connect( cli.common.api_url, SecretString::from(cli.common.token), - cli.common.firezone_id, + cli.firezone_id, CallbackHandler { handle }, ) .unwrap(); @@ -53,6 +53,10 @@ struct Cli { #[command(flatten)] common: CommonArgs, + /// Identifier generated by the portal to identify and display the device. + #[arg(short = 'i', long, env = "FIREZONE_ID")] + pub firezone_id: String, + /// File logging directory. Should be a path that's writeable by the current user. #[arg(short, long, env = "LOG_DIR")] log_dir: Option,