refactor(rust): move device_id to bin-shared (#9040)

Both `device_id` and `device_info` are used by the headless-client and
the GUI client / IPC service. They should therefore be defined in the
`bin-shared` crate.
This commit is contained in:
Thomas Eizinger
2025-05-06 14:52:37 +10:00
committed by GitHub
parent 5ac5fc45e4
commit f2b1fbe718
10 changed files with 56 additions and 48 deletions

View File

@@ -9,7 +9,7 @@ license = { workspace = true }
[dependencies]
anyhow = { workspace = true }
atomicwrites = { workspace = true } # Needed to safely backup `/etc/resolv.conf` and write the device ID on behalf of `gui-client`
atomicwrites = { workspace = true } # Needed to safely backup `/etc/resolv.conf`
backoff = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "string"] }
connlib-client-shared = { workspace = true }
@@ -31,7 +31,6 @@ secrecy = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_variant = { workspace = true }
smbios-lib = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
# This actually relies on many other features in Tokio, so this will probably

View File

@@ -1,161 +0,0 @@
use anyhow::{Context as _, Result};
use atomicwrites::{AtomicFile, OverwriteBehavior};
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
pub struct DeviceId {
pub id: String,
}
/// Returns the path of the randomly-generated Firezone device ID
///
/// e.g. `C:\ProgramData\dev.firezone.client/firezone-id.json` or
/// `/var/lib/dev.firezone.client/config/firezone-id.json`.
pub(crate) fn path() -> Result<PathBuf> {
let path = firezone_bin_shared::known_dirs::ipc_service_config()
.context("Failed to compute path for firezone-id file")?
.join("firezone-id.json");
Ok(path)
}
fn device_serial() -> Option<String> {
const DEFAULT_SERIAL: &str = "123456789";
let data = smbioslib::table_load_from_device().ok()?;
let serial = data.find_map(|sys_info: smbioslib::SMBiosSystemInformation| {
sys_info.serial_number().to_utf8_lossy()
})?;
if serial == DEFAULT_SERIAL {
return None;
}
Some(serial)
}
fn device_uuid() -> Option<String> {
let data = smbioslib::table_load_from_device().ok()?;
let uuid = data.find_map(|sys_info: smbioslib::SMBiosSystemInformation| sys_info.uuid());
uuid?.to_string().into()
}
pub fn device_info() -> phoenix_channel::DeviceInfo {
phoenix_channel::DeviceInfo {
device_serial: device_serial(),
device_uuid: device_uuid(),
..Default::default()
}
}
/// Returns the device ID without generating it
pub fn get() -> Result<DeviceId> {
let path = path()?;
let content = fs::read_to_string(&path).context("Failed to read file")?;
let device_id_json = serde_json::from_str::<DeviceIdJson>(&content)
.context("Failed to deserialize content as JSON")?;
Ok(DeviceId {
id: device_id_json.device_id(),
})
}
/// Returns the device ID, generating it and saving it to disk if needed.
///
/// Per <https://github.com/firezone/firezone/issues/2697> and <https://github.com/firezone/firezone/issues/2711>,
/// clients must generate their own random IDs and persist them to disk, to handle situations like VMs where a hardware ID is not unique or not available.
///
/// Returns: The UUID as a String, suitable for sending verbatim to `connlib_client_shared::Session::connect`.
///
/// Errors: If the disk is unwritable when initially generating the ID, or unwritable when re-generating an invalid ID.
pub fn get_or_create() -> Result<DeviceId> {
let path = path()?;
let dir = path
.parent()
.context("Device ID path should always have a parent")?;
// Make sure the dir exists, and fix its permissions so the GUI can write the
// log filter file
fs::create_dir_all(dir).context("Failed to create dir for firezone-id")?;
set_dir_permissions(dir).with_context(|| {
format!(
"Couldn't set permissions on IPC service config dir `{}`",
dir.display()
)
})?;
// Try to read it from the disk
if let Some(j) = fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str::<DeviceIdJson>(&s).ok())
{
let id = j.device_id();
tracing::debug!(?id, "Loaded device ID from disk");
// Correct permissions for #6989
set_id_permissions(&path).context("Couldn't set permissions on Firezone ID file")?;
return Ok(DeviceId { id });
}
// Couldn't read, it's missing or invalid, generate a new one and save it.
let id = uuid::Uuid::new_v4();
let j = DeviceIdJson { id };
let content =
serde_json::to_string(&j).context("Impossible: Failed to serialize firezone-id")?;
let file = AtomicFile::new(&path, OverwriteBehavior::DisallowOverwrite);
file.write(|f| f.write_all(content.as_bytes()))
.context("Failed to write firezone-id file")?;
let id = j.device_id();
tracing::debug!(?id, "Saved device ID to disk");
set_id_permissions(&path).context("Couldn't set permissions on Firezone ID file")?;
Ok(DeviceId { id })
}
#[cfg(target_os = "linux")]
fn set_dir_permissions(dir: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
// user read/write, group read/write, others nothing
// directories need `+x` to work of course
let perms = fs::Permissions::from_mode(0o770);
std::fs::set_permissions(dir, perms)?;
Ok(())
}
/// Does nothing on non-Linux systems
#[cfg(not(target_os = "linux"))]
#[expect(clippy::unnecessary_wraps)]
fn set_dir_permissions(_: &Path) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
fn set_id_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
// user read/write, group read, others nothing
let perms = fs::Permissions::from_mode(0o640);
std::fs::set_permissions(path, perms)?;
Ok(())
}
/// Does nothing on non-Linux systems
#[cfg(not(target_os = "linux"))]
#[expect(clippy::unnecessary_wraps)]
fn set_id_permissions(_: &Path) -> Result<()> {
Ok(())
}
#[derive(serde::Deserialize, serde::Serialize)]
struct DeviceIdJson {
id: uuid::Uuid,
}
impl DeviceIdJson {
fn device_id(&self) -> String {
self.id.to_string()
}
}

View File

@@ -1,10 +1,11 @@
use crate::{CallbackHandler, CliCommon, ConnlibMsg, device_id};
use crate::{CallbackHandler, CliCommon, ConnlibMsg};
use anyhow::{Context as _, Result, bail};
use atomicwrites::{AtomicFile, OverwriteBehavior};
use clap::Parser;
use connlib_model::ResourceView;
use firezone_bin_shared::{
DnsControlMethod, DnsController, TOKEN_ENV_KEY, TunDeviceManager, known_dirs,
DnsControlMethod, DnsController, TOKEN_ENV_KEY, TunDeviceManager, device_id, device_info,
known_dirs,
platform::{tcp_socket_factory, udp_socket_factory},
signals,
};
@@ -15,7 +16,7 @@ use futures::{
future::poll_fn,
task::{Context, Poll},
};
use phoenix_channel::LoginUrl;
use phoenix_channel::{DeviceInfo, LoginUrl};
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::{
@@ -566,7 +567,11 @@ impl<'a> Handler<'a> {
&token,
device_id.id,
None,
device_id::device_info(),
DeviceInfo {
device_serial: device_info::serial(),
device_uuid: device_info::uuid(),
..Default::default()
},
)
.context("Failed to create `LoginUrl`")?;

View File

@@ -24,8 +24,6 @@ use tokio::sync::mpsc;
use tracing_subscriber::{EnvFilter, Layer as _, Registry, fmt, layer::SubscriberExt as _};
mod clear_logs;
/// Generate a persistent device ID, stores it to disk, and reads it back.
pub mod device_id;
mod ipc_service;
pub use clear_logs::clear_logs;

View File

@@ -7,19 +7,20 @@ use backoff::ExponentialBackoffBuilder;
use clap::Parser;
use connlib_client_shared::Session;
use firezone_bin_shared::{
DnsController, TOKEN_ENV_KEY, TunDeviceManager, new_dns_notifier, new_network_notifier,
DnsController, TOKEN_ENV_KEY, TunDeviceManager, device_id, device_info, new_dns_notifier,
new_network_notifier,
platform::{tcp_socket_factory, udp_socket_factory},
signals,
};
use firezone_headless_client::{CallbackHandler, CliCommon, ConnlibMsg, device_id};
use firezone_headless_client::{CallbackHandler, CliCommon, ConnlibMsg};
use firezone_logging::telemetry_span;
use firezone_telemetry::Telemetry;
use firezone_telemetry::otel;
use futures::StreamExt as _;
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use phoenix_channel::LoginUrl;
use phoenix_channel::PhoenixChannel;
use phoenix_channel::get_user_agent;
use phoenix_channel::{DeviceInfo, LoginUrl};
use secrecy::{Secret, SecretString};
use std::{
path::{Path, PathBuf},
@@ -194,7 +195,11 @@ fn main() -> Result<()> {
&token,
firezone_id.clone(),
cli.firezone_name,
device_id::device_info(),
DeviceInfo {
device_serial: device_info::serial(),
device_uuid: device_info::uuid(),
..Default::default()
},
)?;
if cli.check {