Skip to main content

squib_api/schemas/
mmds.rs

1//! `/mmds` and `/mmds/config` bodies.
2//!
3//! Per [21-api-compat-matrix.md `/mmds/config`
4//! PUT](../../../specs/21-api-compat-matrix.md#mmdsconfig-put):
5//!
6//! - `version` — `V1 | V2`.
7//! - `network_interfaces` — list of `iface_id`s, max 8 (matches `MAX_NICS`).
8//! - `ipv4_address` — link-local in `169.254.0.0/16`; default `169.254.169.254`.
9//! - `imds_compat` — bool default false.
10//! - `token_ttl_seconds` (V2 only) — `1..=21600`.
11
12use std::net::{IpAddr, Ipv4Addr};
13
14use serde::{Deserialize, Serialize};
15
16use super::common::{IfaceId, MAX_NICS};
17
18/// Maximum value for `token_ttl_seconds`, matching upstream's `MAX_TOKEN_TTL_SECONDS`.
19pub const MAX_TOKEN_TTL_SECONDS: u64 = 21_600;
20
21/// MMDS protocol version.
22#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
23pub enum MmdsVersion {
24    /// `IMDSv1` — no token negotiation.
25    V1,
26    /// `IMDSv2` — token-based, default for new instances.
27    #[default]
28    V2,
29}
30
31/// Raw `/mmds/config` PUT body off the wire.
32#[derive(Debug, Clone, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct RawMmdsConfig {
35    /// MMDS protocol version.
36    #[serde(default)]
37    pub version: MmdsVersion,
38    /// IDs of network interfaces the dumbo intercept binds to.
39    pub network_interfaces: Vec<String>,
40    /// Optional IMDS link-local address; default `169.254.169.254`.
41    #[serde(default)]
42    pub ipv4_address: Option<Ipv4Addr>,
43    /// `imds_compat` overrides the `Accept` header to plain text.
44    #[serde(default)]
45    pub imds_compat: bool,
46    /// V2 token TTL in seconds (1..=21600); ignored for V1.
47    #[serde(default)]
48    pub token_ttl_seconds: Option<u64>,
49}
50
51/// Validated `/mmds/config` PUT body.
52#[derive(Debug, Clone, Serialize)]
53#[non_exhaustive]
54pub struct MmdsConfig {
55    /// MMDS protocol version.
56    pub version: MmdsVersion,
57    /// Validated `iface_id`s.
58    pub network_interfaces: Vec<IfaceId>,
59    /// Resolved IMDS IPv4 (always link-local).
60    pub ipv4_address: Ipv4Addr,
61    /// IMDS-compat flag.
62    pub imds_compat: bool,
63    /// Validated V2 token TTL (`1..=21600`).
64    pub token_ttl_seconds: u64,
65}
66
67fn is_link_local(addr: Ipv4Addr) -> bool {
68    let octets = addr.octets();
69    octets[0] == 169 && octets[1] == 254
70}
71
72impl TryFrom<RawMmdsConfig> for MmdsConfig {
73    type Error = String;
74
75    fn try_from(raw: RawMmdsConfig) -> Result<Self, Self::Error> {
76        if raw.network_interfaces.is_empty() {
77            return Err("Invalid mmds-config: network_interfaces must not be empty".into());
78        }
79        if raw.network_interfaces.len() > MAX_NICS {
80            return Err(format!(
81                "Invalid mmds-config: network_interfaces exceeds {MAX_NICS} entries"
82            ));
83        }
84        let mut ifaces = Vec::with_capacity(raw.network_interfaces.len());
85        for id in raw.network_interfaces {
86            ifaces.push(IfaceId::new(id)?);
87        }
88        let ipv4_address = raw
89            .ipv4_address
90            .unwrap_or_else(|| Ipv4Addr::new(169, 254, 169, 254));
91        if !is_link_local(ipv4_address) {
92            return Err(format!(
93                "Invalid ipv4_address: {ipv4_address} is not in 169.254.0.0/16"
94            ));
95        }
96        let token_ttl_seconds = raw.token_ttl_seconds.unwrap_or(MAX_TOKEN_TTL_SECONDS);
97        if token_ttl_seconds == 0 || token_ttl_seconds > MAX_TOKEN_TTL_SECONDS {
98            return Err(format!(
99                "Invalid token_ttl_seconds: must be 1..={MAX_TOKEN_TTL_SECONDS}"
100            ));
101        }
102        Ok(Self {
103            version: raw.version,
104            network_interfaces: ifaces,
105            ipv4_address,
106            imds_compat: raw.imds_compat,
107            token_ttl_seconds,
108        })
109    }
110}
111
112/// `PUT /mmds` body — arbitrary user JSON tree, capped by `--mmds-size-limit`.
113///
114/// We serialize the tree back through `serde_json::Value` so unknown structure is
115/// preserved (the MMDS data store *is* dynamic; unlike every other endpoint here, it
116/// has no fixed schema). The size cap is enforced by the `RequestBodyLimitLayer`
117/// middleware against `--mmds-size-limit`.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct MmdsContents(pub serde_json::Value);
120
121impl MmdsContents {
122    /// Wrap a serde tree.
123    pub fn new(tree: serde_json::Value) -> Self {
124        Self(tree)
125    }
126}
127
128/// Helper: returns `Some(addr)` if the address falls in the link-local range.
129#[must_use]
130pub fn link_local_or_default(addr: Option<IpAddr>) -> Option<Ipv4Addr> {
131    match addr {
132        Some(IpAddr::V4(v4)) if is_link_local(v4) => Some(v4),
133        _ => None,
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn raw() -> RawMmdsConfig {
142        RawMmdsConfig {
143            version: MmdsVersion::V2,
144            network_interfaces: vec!["eth0".into()],
145            ipv4_address: None,
146            imds_compat: false,
147            token_ttl_seconds: None,
148        }
149    }
150
151    #[test]
152    fn test_should_accept_minimal_mmds_config() {
153        let cfg = MmdsConfig::try_from(raw()).unwrap();
154        assert_eq!(cfg.ipv4_address, Ipv4Addr::new(169, 254, 169, 254));
155        assert_eq!(cfg.token_ttl_seconds, MAX_TOKEN_TTL_SECONDS);
156    }
157
158    #[test]
159    fn test_should_reject_empty_network_interfaces() {
160        let mut r = raw();
161        r.network_interfaces.clear();
162        assert!(MmdsConfig::try_from(r).is_err());
163    }
164
165    #[test]
166    fn test_should_reject_too_many_network_interfaces() {
167        let mut r = raw();
168        r.network_interfaces = (0..=MAX_NICS).map(|i| format!("eth{i}")).collect();
169        assert!(MmdsConfig::try_from(r).is_err());
170    }
171
172    #[test]
173    fn test_should_reject_non_link_local_ipv4() {
174        let mut r = raw();
175        r.ipv4_address = Some(Ipv4Addr::new(10, 0, 0, 1));
176        assert!(MmdsConfig::try_from(r).is_err());
177    }
178
179    #[test]
180    fn test_should_reject_token_ttl_at_zero() {
181        let mut r = raw();
182        r.token_ttl_seconds = Some(0);
183        assert!(MmdsConfig::try_from(r).is_err());
184    }
185
186    #[test]
187    fn test_should_reject_token_ttl_above_cap() {
188        let mut r = raw();
189        r.token_ttl_seconds = Some(MAX_TOKEN_TTL_SECONDS + 1);
190        assert!(MmdsConfig::try_from(r).is_err());
191    }
192}