Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/agentd/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ fn main() {
// Capture CLOCK_BOOTTIME immediately — this represents kernel boot duration.
let boot_time_ns = microsandbox_agentd::clock::boottime_ns();

// Read all MSB_* environment variables once at startup.
let config = microsandbox_agentd::AgentdConfig::from_env();

// Phase 1: Synchronous init (mount filesystems, prepare runtime directories).
let init_start = microsandbox_agentd::clock::boottime_ns();
if let Err(e) = microsandbox_agentd::init::init() {
if let Err(e) = microsandbox_agentd::init::init(&config) {
eprintln!("agentd: init failed: {e}");
std::process::exit(1);
}
Expand All @@ -33,7 +36,7 @@ fn main() {
.expect("agentd: failed to build tokio runtime");

rt.block_on(async {
match microsandbox_agentd::agent::run(boot_time_ns, init_time_ns).await {
match microsandbox_agentd::agent::run(boot_time_ns, init_time_ns, &config).await {
Ok(()) => {}
Err(microsandbox_agentd::AgentdError::Shutdown) => {}
Err(e) => {
Expand Down
7 changes: 5 additions & 2 deletions crates/agentd/lib/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use microsandbox_protocol::{
};

use crate::{
config::AgentdConfig,
error::{AgentdError, AgentdResult},
fs::FsWriteSession,
heartbeat::{heartbeat_dir_exists, write_heartbeat},
Expand Down Expand Up @@ -55,7 +56,7 @@ const MAX_INPUT_BUF_SIZE: usize = MAX_FRAME_SIZE as usize + 4;
///
/// - `boot_time_ns`: `CLOCK_BOOTTIME` at `main()` start (kernel boot duration).
/// - `init_time_ns`: nanoseconds spent in `init::init()`.
pub async fn run(boot_time_ns: u64, init_time_ns: u64) -> AgentdResult<()> {
pub async fn run(boot_time_ns: u64, init_time_ns: u64, config: &AgentdConfig) -> AgentdResult<()> {
// Discover serial port.
let port_path = find_serial_port(AGENT_PORT_NAME)?;

Expand Down Expand Up @@ -140,6 +141,7 @@ pub async fn run(boot_time_ns: u64, init_time_ns: u64) -> AgentdResult<()> {
&mut write_sessions,
&session_tx,
&mut serial_out_buf,
config,
).await?;
}

Expand Down Expand Up @@ -214,14 +216,15 @@ async fn handle_message(
write_sessions: &mut HashMap<u32, FsWriteSession>,
session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
out_buf: &mut Vec<u8>,
config: &AgentdConfig,
) -> AgentdResult<()> {
match msg.t {
MessageType::ExecRequest => {
let mut req: ExecRequest = msg
.payload()
.map_err(|e| AgentdError::ExecSession(format!("decode exec request: {e}")))?;
prepend_scripts_to_path(&mut req);
match ExecSession::spawn(msg.id, &req, session_tx.clone()) {
match ExecSession::spawn(msg.id, &req, session_tx.clone(), config.user.as_deref()) {
Ok(session) => {
let reply = Message::with_payload(
MessageType::ExecStarted,
Expand Down
76 changes: 76 additions & 0 deletions crates/agentd/lib/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Agentd configuration, read once from environment variables at startup.

use microsandbox_protocol::{
ENV_BLOCK_ROOT, ENV_HOSTNAME, ENV_MOUNTS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6, ENV_TMPFS,
ENV_USER,
};

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// Configuration for agentd, read once from environment variables at startup.
///
/// All `MSB_*` environment variables are captured into this struct during
/// construction via [`AgentdConfig::from_env`]. Downstream functions receive
/// the config by reference, avoiding repeated (and thread-unsafe) env var reads.
#[derive(Debug, Clone, Default)]
pub struct AgentdConfig {
/// `MSB_BLOCK_ROOT` — block device for rootfs switch.
pub block_root: Option<String>,

/// `MSB_MOUNTS` — virtiofs volume mount specs.
pub mounts: Option<String>,

/// `MSB_TMPFS` — tmpfs mount specs.
pub tmpfs: Option<String>,

/// `MSB_HOSTNAME` — guest hostname.
pub hostname: Option<String>,

/// `MSB_NET` — network interface config.
pub net: Option<String>,

/// `MSB_NET_IPV4` — IPv4 config.
pub net_ipv4: Option<String>,

/// `MSB_NET_IPV6` — IPv6 config.
pub net_ipv6: Option<String>,

/// `MSB_USER` — default guest user for exec sessions.
pub user: Option<String>,
}

//--------------------------------------------------------------------------------------------------
// Implementations
//--------------------------------------------------------------------------------------------------

impl AgentdConfig {
/// Reads all `MSB_*` environment variables once and returns the config.
///
/// Empty or whitespace-only values are treated as absent (`None`).
pub fn from_env() -> Self {
Self {
block_root: read_env(ENV_BLOCK_ROOT),
mounts: read_env(ENV_MOUNTS),
tmpfs: read_env(ENV_TMPFS),
hostname: read_env(ENV_HOSTNAME),
net: read_env(ENV_NET),
net_ipv4: read_env(ENV_NET_IPV4),
net_ipv6: read_env(ENV_NET_IPV6),
user: read_env(ENV_USER),
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one more nit 😅.

It would be nice if all the env parsing operations are moved into this file as well.

So we parse all the env vars once in from_env and fields in AgentdConfig can be the actual types. Something like

pub(crate) struct AgentdConfig<'a> { 
    block_root: Option<BlockRootSpec<'a>>, 
    mounts: Option<VolumeMountSpec<'a>>, 
    ... 
}`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved all env parsing into config.rs. Spec types own String instead of borrowing — keeps the API simple with no lifetimes and a single-step from_env(). a907292

}

//--------------------------------------------------------------------------------------------------
// Helper Functions
//--------------------------------------------------------------------------------------------------

/// Reads a single environment variable, returning `None` for missing or empty values.
fn read_env(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
52 changes: 29 additions & 23 deletions crates/agentd/lib/init.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! PID 1 init: mount filesystems, apply tmpfs mounts, prepare runtime directories.

use crate::config::AgentdConfig;
use crate::error::{AgentdError, AgentdResult};

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -39,15 +40,19 @@ struct VolumeMountSpec<'a> {
/// Mounts essential filesystems, applies volume and tmpfs mounts from
/// `MSB_MOUNTS` / `MSB_TMPFS` env vars, configures networking from
/// `MSB_NET*` env vars, and prepares runtime directories.
pub fn init() -> AgentdResult<()> {
pub fn init(config: &AgentdConfig) -> AgentdResult<()> {
linux::mount_filesystems()?;
linux::mount_runtime()?;
linux::mount_block_root()?;
linux::apply_volume_mounts()?;
crate::network::apply_hostname()?;
linux::apply_tmpfs_mounts()?;
linux::mount_block_root(config.block_root.as_deref())?;
linux::apply_volume_mounts(config.mounts.as_deref())?;
crate::network::apply_hostname(config.hostname.as_deref())?;
linux::apply_tmpfs_mounts(config.tmpfs.as_deref())?;
linux::ensure_standard_tmp_permissions()?;
crate::network::apply_network_config()?;
crate::network::apply_network_config(
config.net.as_deref(),
config.net_ipv4.as_deref(),
config.net_ipv6.as_deref(),
)?;
crate::tls::install_ca_cert()?;
linux::ensure_scripts_path_in_profile()?;
linux::create_run_dir()?;
Expand Down Expand Up @@ -292,17 +297,18 @@ mod linux {
Ok(())
}

/// Mounts a block device as the new root filesystem, if `MSB_BLOCK_ROOT` is set.
/// Mounts a block device as the new root filesystem.
///
/// Steps: mount block device at `/newroot`, bind-mount `/.msb` into it,
/// pivot via `MS_MOVE` + `chroot`, then re-mount essential filesystems.
pub fn mount_block_root() -> AgentdResult<()> {
let val = match std::env::var(microsandbox_protocol::ENV_BLOCK_ROOT) {
Ok(v) if !v.is_empty() => v,
/// No-op if `block_root` is `None`. Steps: mount block device at `/newroot`,
/// bind-mount `/.msb` into it, pivot via `MS_MOVE` + `chroot`, then re-mount
/// essential filesystems.
pub fn mount_block_root(block_root: Option<&str>) -> AgentdResult<()> {
let val = match block_root {
Some(v) if !v.is_empty() => v,
_ => return Ok(()),
};

let spec = super::parse_block_root(&val)?;
let spec = super::parse_block_root(val)?;

// Create the temporary mount point.
mkdir_ignore_exists("/newroot")?;
Expand Down Expand Up @@ -390,17 +396,17 @@ mod linux {
)))
}

/// Reads `MSB_MOUNTS` env var and mounts each virtiofs volume.
/// Mounts each virtiofs volume from the mount spec.
///
/// For each entry, creates the guest mount point directory and mounts the
/// virtiofs share using the tag provided by the host. If the entry
/// specifies `:ro`, the mount is made read-only via `MS_RDONLY`.
///
/// Missing env var is not an error (no volume mounts requested).
/// No-op if `mounts` is `None` (no volume mounts requested).
/// Parse failures and mount failures are hard errors.
pub fn apply_volume_mounts() -> AgentdResult<()> {
let val = match std::env::var(microsandbox_protocol::ENV_MOUNTS) {
Ok(v) if !v.is_empty() => v,
pub fn apply_volume_mounts(mounts: Option<&str>) -> AgentdResult<()> {
let val = match mounts {
Some(v) if !v.is_empty() => v,
_ => return Ok(()),
};

Expand Down Expand Up @@ -439,13 +445,13 @@ mod linux {
Ok(())
}

/// Reads `MSB_TMPFS` env var and mounts each tmpfs entry.
/// Mounts each tmpfs entry from the spec.
///
/// Missing env var is not an error (no tmpfs mounts requested).
/// No-op if `tmpfs` is `None` (no tmpfs mounts requested).
/// Parse failures and mount failures are hard errors.
pub fn apply_tmpfs_mounts() -> AgentdResult<()> {
let val = match std::env::var(microsandbox_protocol::ENV_TMPFS) {
Ok(v) if !v.is_empty() => v,
pub fn apply_tmpfs_mounts(tmpfs: Option<&str>) -> AgentdResult<()> {
let val = match tmpfs {
Some(v) if !v.is_empty() => v,
_ => return Ok(()),
};

Expand Down
2 changes: 2 additions & 0 deletions crates/agentd/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#![cfg(target_os = "linux")]
#![warn(missing_docs)]

mod config;
mod error;

//--------------------------------------------------------------------------------------------------
Expand All @@ -22,4 +23,5 @@ pub mod serial;
pub mod session;
pub mod tls;

pub use config::AgentdConfig;
pub use error::*;
39 changes: 19 additions & 20 deletions crates/agentd/lib/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,53 +41,52 @@ struct NetIpv6Spec {
// Functions
//--------------------------------------------------------------------------------------------------

/// Sets the guest hostname from `MSB_HOSTNAME`.
/// Sets the guest hostname.
///
/// Calls `sethostname()`, writes `/etc/hostname`, and provisions
/// `/etc/hosts` with localhost aliases and the hostname entry.
pub fn apply_hostname() -> AgentdResult<()> {
let hostname = match std::env::var(microsandbox_protocol::ENV_HOSTNAME) {
Ok(v) if !v.is_empty() => Some(v),
_ => None,
};
pub fn apply_hostname(hostname: Option<&str>) -> AgentdResult<()> {
linux::write_hosts_file(hostname)?;

linux::write_hosts_file(hostname.as_deref())?;

if let Some(ref name) = hostname {
if let Some(name) = hostname {
linux::set_hostname(name)?;
}

Ok(())
}

/// Applies network configuration from `MSB_NET*` environment variables.
/// Applies network configuration.
///
/// Always provisions loopback, even when no external network interface is
/// requested. Missing `MSB_NET` is not an error (no networking requested).
/// requested. Missing `net` is not an error (no networking requested).
/// Parse failures and configuration failures are hard errors.
pub fn apply_network_config() -> AgentdResult<()> {
pub fn apply_network_config(
net: Option<&str>,
net_ipv4: Option<&str>,
net_ipv6: Option<&str>,
) -> AgentdResult<()> {
linux::configure_loopback()?;

let val = match std::env::var(microsandbox_protocol::ENV_NET) {
Ok(v) if !v.is_empty() => v,
let val = match net {
Some(v) if !v.is_empty() => v,
_ => return Ok(()),
};

let net = parse_net(&val)?;
let parsed_net = parse_net(val)?;

// Parse optional IPv4 config.
let ipv4 = match std::env::var(microsandbox_protocol::ENV_NET_IPV4) {
Ok(v) if !v.is_empty() => Some(parse_net_ipv4(&v)?),
let ipv4 = match net_ipv4 {
Some(v) if !v.is_empty() => Some(parse_net_ipv4(v)?),
_ => None,
};

// Parse optional IPv6 config.
let ipv6 = match std::env::var(microsandbox_protocol::ENV_NET_IPV6) {
Ok(v) if !v.is_empty() => Some(parse_net_ipv6(&v)?),
let ipv6 = match net_ipv6 {
Some(v) if !v.is_empty() => Some(parse_net_ipv6(v)?),
_ => None,
};

linux::configure_interface(&net, ipv4.as_ref(), ipv6.as_ref())
linux::configure_interface(&parsed_net, ipv4.as_ref(), ipv6.as_ref())
}

fn hosts_file_contents(hostname: Option<&str>) -> String {
Expand Down
Loading
Loading