8000 fix(minvmd,minimald): subnet validation, attached_count lifecycle, tokio::fs by gominimal-aw-bot[bot] · Pull Request #537 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 27 additions & 15 deletions crates/minimald/src/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ const TERM_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NetError {
/// The prefix length is outside the range accepted by [`SwitchSubnet::new`].
///
/// Distinct from [`SubnetExhausted`](Self::SubnetExhausted): a prefix that
/// puts the subnet outside `8..=29` is a misconfiguration (the prefix was
/// never valid), not an exhausted valid subnet.
#[error("prefix /{0} is outside the valid range /8..=/29 for a gvproxy switch subnet")]
InvalidPrefix(u8),
/// The configured subnet has no remaining host address to hand out.
#[error("gvproxy subnet {0} is exhausted; no free PTask address remains")]
SubnetExhausted(SwitchSubnet),
Expand Down Expand Up @@ -135,24 +142,23 @@ impl SwitchSubnet {
///
/// # Errors
///
/// Returns [`NetError::SubnetExhausted`] for a prefix outside `8..=29`. A
/// Returns [`NetError::InvalidPrefix`] for a prefix outside `8..=29`. A
/// prefix narrower than /29 has no room for the reserved
/// network/gateway/host-alias/broadcast addresses plus a PTask address. A
/// prefix wider than /8 lets the high octet vary, which
/// [`MacAddr::for_switch_ip`] does not fold into the derived MAC, so two
/// addresses differing only in that octet would collide.
pub fn new(base: Ipv4Addr, prefix: u8) -> Result<Self, NetError> {
let s = Self { base, prefix };
// Reserve four addresses (network, gateway, host-alias, broadcast): a
// /29 (8 addresses) leaves four allocatable hosts, a /30 (4) leaves
// none, so anything narrower than /29 has no room for a PTask. The lower
// bound keeps MacAddr::for_switch_ip collision-free — it derives the MAC
// from the low three octets, so the high octet must be pinned by the
// prefix (/8 or narrower).
if !(8..=29).contains(&prefix) {
return Err(NetError::SubnetExhausted(s));
return Err(NetError::InvalidPrefix(prefix));
}
Ok(s)
Ok(Self { base, prefix })
}

fn mask(self) -> u32 {
Expand Down Expand Up @@ -406,7 +412,7 @@ impl GvproxySwitch {
/// Propagates config-write, spawn, and socket-readiness failures.
pub async fn attach(&mut self) -> Result<AttachResult, NetError> {
let lease = self.allocator.allocate()?;
self.write_config()?;
self.write_config().await?;
self.ensure_running().await?;
self.attached += 1;
tracing::info!(
Expand Down Expand Up @@ -434,14 +440,18 @@ impl GvproxySwitch {
Ok(())
}

fn write_config(&self) -> Result<(), NetError> {
std::fs::create_dir_all(&self.state_dir).map_err(|source| NetError::WriteConfig {
path: self.state_dir.clone(),
source,
})?;
async fn write_config(&self) -> Result<(), NetError> {
tokio::fs::create_dir_all(&self.state_dir)
.await
.map_err(|source| NetError::WriteConfig {
path: self.state_dir.clone(),
source,
})?;
let path = self.config_path();
let body = render_gvproxy_config(self.allocator.subnet(), self.allocator.leases());
std::fs::write(&path, body).map_err(|source| NetError::WriteConfig { path, source })
tokio::fs::write(&path, body)
.await
.map_err(|source| NetError::WriteConfig { path, source })
}

/// Spawns gvproxy if it is not already running and waits for its control
Expand Down Expand Up @@ -479,7 +489,7 @@ impl GvproxySwitch {
// cannot be cleared, fail now rather than let `wait_for_socket` mistake
// the leftover path for a freshly-bound one and report a switch that
// never actually came up.
match std::fs::remove_file(&sock) {
match tokio::fs::remove_file(&sock).await {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(NetError::Io(e)),
Expand Down Expand Up @@ -592,7 +602,7 @@ impl GvproxySwitch {
let _ = child.wait().await;
}
}
let _ = std::fs::remove_file(self.control_socket());
let _ = tokio::fs::remove_file(self.control_socket()).await;
Ok(())
}
}
Expand Down Expand Up @@ -665,9 +675,11 @@ mod tests {

#[test]
fn subnet_rejects_overly_narrow_prefix() {
// /30 is a misconfiguration (no room for a PTask), not an exhausted
// valid subnet — so the distinct InvalidPrefix error is returned.
assert!(matches!(
SwitchSubnet::new(Ipv4Addr::new(10, 0, 0, 0), 30),
Err(NetError::SubnetExhausted(_))
Err(NetError::InvalidPrefix(30))
));
}

Expand All @@ -677,7 +689,7 @@ mod tests {
// does not cover, so the constructor rejects it to keep MACs unique.
assert!(matches!(
SwitchSubnet::new(Ipv4Addr::new(10, 0, 0, 0), 7),
Err(NetError::SubnetExhausted(_))
Err(NetError::InvalidPrefix(7))
));
}

Expand Down
1 change: 1 addition & 0 deletions crates/minvmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dirs.workspace = true
fd-lock.workspace = true
libc.workspace = true
serde = { workspace = true }
thiserror.workspace = true
serde_json.workspace = true
toml.workspace = true
tracing.workspace = true
Expand Down
Loading
Loading
0