squib_api/schemas/
mmds.rs1use std::net::{IpAddr, Ipv4Addr};
13
14use serde::{Deserialize, Serialize};
15
16use super::common::{IfaceId, MAX_NICS};
17
18pub const MAX_TOKEN_TTL_SECONDS: u64 = 21_600;
20
21#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
23pub enum MmdsVersion {
24 V1,
26 #[default]
28 V2,
29}
30
31#[derive(Debug, Clone, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct RawMmdsConfig {
35 #[serde(default)]
37 pub version: MmdsVersion,
38 pub network_interfaces: Vec<String>,
40 #[serde(default)]
42 pub ipv4_address: Option<Ipv4Addr>,
43 #[serde(default)]
45 pub imds_compat: bool,
46 #[serde(default)]
48 pub token_ttl_seconds: Option<u64>,
49}
50
51#[derive(Debug, Clone, Serialize)]
53#[non_exhaustive]
54pub struct MmdsConfig {
55 pub version: MmdsVersion,
57 pub network_interfaces: Vec<IfaceId>,
59 pub ipv4_address: Ipv4Addr,
61 pub imds_compat: bool,
63 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#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct MmdsContents(pub serde_json::Value);
120
121impl MmdsContents {
122 pub fn new(tree: serde_json::Value) -> Self {
124 Self(tree)
125 }
126}
127
128#[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}