fips_core/config/
gateway.rs1use std::collections::HashSet;
6use std::net::SocketAddrV6;
7
8use serde::{Deserialize, Serialize};
9
10const DEFAULT_DNS_LISTEN: &str = "[::1]:5353";
18
19const DEFAULT_DNS_UPSTREAM: &str = "[::1]:5354";
28
29const DEFAULT_DNS_TTL: u32 = 60;
31
32const DEFAULT_GRACE_PERIOD: u64 = 60;
34
35const DEFAULT_CT_TCP_ESTABLISHED: u64 = 432_000;
37
38const DEFAULT_CT_UDP_TIMEOUT: u64 = 30;
40
41const DEFAULT_CT_UDP_ASSURED: u64 = 180;
43
44const DEFAULT_CT_ICMP_TIMEOUT: u64 = 30;
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct GatewayConfig {
50 #[serde(default)]
52 pub enabled: bool,
53
54 pub pool: String,
56
57 pub lan_interface: String,
59
60 #[serde(default)]
62 pub dns: GatewayDnsConfig,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub pool_grace_period: Option<u64>,
67
68 #[serde(default)]
70 pub conntrack: ConntrackConfig,
71
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub port_forwards: Vec<PortForward>,
75}
76
77impl GatewayConfig {
78 pub fn grace_period(&self) -> u64 {
80 self.pool_grace_period.unwrap_or(DEFAULT_GRACE_PERIOD)
81 }
82
83 pub fn validate_port_forwards(&self) -> Result<(), String> {
88 let mut seen = HashSet::new();
89 for pf in &self.port_forwards {
90 if pf.listen_port == 0 {
91 return Err("port_forward listen_port must be non-zero".to_string());
92 }
93 if !seen.insert((pf.listen_port, pf.proto)) {
94 return Err(format!(
95 "duplicate port_forward ({:?} {}) — each (listen_port, proto) must be unique",
96 pf.proto, pf.listen_port
97 ));
98 }
99 }
100 Ok(())
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum Proto {
108 Tcp,
109 Udp,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct PortForward {
115 pub listen_port: u16,
117 pub proto: Proto,
119 pub target: SocketAddrV6,
122}
123
124#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126pub struct GatewayDnsConfig {
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub listen: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub upstream: Option<String>,
135
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub ttl: Option<u32>,
139}
140
141impl GatewayDnsConfig {
142 pub fn listen(&self) -> &str {
144 self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN)
145 }
146
147 pub fn upstream(&self) -> &str {
149 self.upstream.as_deref().unwrap_or(DEFAULT_DNS_UPSTREAM)
150 }
151
152 pub fn ttl(&self) -> u32 {
154 self.ttl.unwrap_or(DEFAULT_DNS_TTL)
155 }
156}
157
158#[derive(Debug, Clone, Default, Serialize, Deserialize)]
160pub struct ConntrackConfig {
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub tcp_established: Option<u64>,
164
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub udp_timeout: Option<u64>,
168
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub udp_assured: Option<u64>,
172
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub icmp_timeout: Option<u64>,
176}
177
178impl ConntrackConfig {
179 pub fn tcp_established(&self) -> u64 {
181 self.tcp_established.unwrap_or(DEFAULT_CT_TCP_ESTABLISHED)
182 }
183
184 pub fn udp_timeout(&self) -> u64 {
186 self.udp_timeout.unwrap_or(DEFAULT_CT_UDP_TIMEOUT)
187 }
188
189 pub fn udp_assured(&self) -> u64 {
191 self.udp_assured.unwrap_or(DEFAULT_CT_UDP_ASSURED)
192 }
193
194 pub fn icmp_timeout(&self) -> u64 {
196 self.icmp_timeout.unwrap_or(DEFAULT_CT_ICMP_TIMEOUT)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_gateway_config_defaults() {
206 let yaml = r#"
207pool: "fd01::/112"
208lan_interface: "eth0"
209"#;
210 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
211 assert!(!config.enabled);
212 assert_eq!(config.pool, "fd01::/112");
213 assert_eq!(config.lan_interface, "eth0");
214 assert_eq!(config.dns.listen(), "[::1]:5353");
215 assert_eq!(config.dns.upstream(), "[::1]:5354");
216 assert_eq!(config.dns.ttl(), 60);
217 assert_eq!(config.grace_period(), 60);
218 assert_eq!(config.conntrack.tcp_established(), 432_000);
219 assert_eq!(config.conntrack.udp_timeout(), 30);
220 }
221
222 #[test]
223 fn test_gateway_config_custom() {
224 let yaml = r#"
225enabled: true
226pool: "fd01::/112"
227lan_interface: "enp3s0"
228dns:
229 listen: "192.168.1.1:53"
230 upstream: "127.0.0.1:5354"
231 ttl: 120
232pool_grace_period: 30
233conntrack:
234 tcp_established: 3600
235 udp_timeout: 60
236"#;
237 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
238 assert!(config.enabled);
239 assert_eq!(config.dns.listen(), "192.168.1.1:53");
240 assert_eq!(config.dns.ttl(), 120);
241 assert_eq!(config.grace_period(), 30);
242 assert_eq!(config.conntrack.tcp_established(), 3600);
243 assert_eq!(config.conntrack.udp_timeout(), 60);
244 assert_eq!(config.conntrack.udp_assured(), 180);
246 assert_eq!(config.conntrack.icmp_timeout(), 30);
247 }
248
249 #[test]
250 fn test_root_config_with_gateway() {
251 let yaml = r#"
252gateway:
253 enabled: true
254 pool: "fd01::/112"
255 lan_interface: "eth0"
256"#;
257 let config: crate::Config = serde_yaml::from_str(yaml).unwrap();
258 assert!(config.gateway.is_some());
259 let gw = config.gateway.unwrap();
260 assert!(gw.enabled);
261 assert_eq!(gw.pool, "fd01::/112");
262 }
263
264 #[test]
265 fn test_root_config_without_gateway() {
266 let yaml = "node: {}";
267 let config: crate::Config = serde_yaml::from_str(yaml).unwrap();
268 assert!(config.gateway.is_none());
269 }
270
271 #[test]
272 fn test_port_forwards_default_empty() {
273 let yaml = r#"
274pool: "fd01::/112"
275lan_interface: "eth0"
276"#;
277 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
278 assert!(config.port_forwards.is_empty());
279 config.validate_port_forwards().unwrap();
280 }
281
282 #[test]
283 fn test_port_forwards_parse() {
284 let yaml = r#"
285pool: "fd01::/112"
286lan_interface: "eth0"
287port_forwards:
288 - listen_port: 8080
289 proto: tcp
290 target: "[fd12:3456::10]:80"
291 - listen_port: 2222
292 proto: tcp
293 target: "[fd12:3456::20]:22"
294 - listen_port: 5353
295 proto: udp
296 target: "[fd12:3456::10]:53"
297"#;
298 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
299 assert_eq!(config.port_forwards.len(), 3);
300 assert_eq!(config.port_forwards[0].listen_port, 8080);
301 assert_eq!(config.port_forwards[0].proto, Proto::Tcp);
302 assert_eq!(
303 config.port_forwards[0].target,
304 "[fd12:3456::10]:80".parse::<SocketAddrV6>().unwrap()
305 );
306 assert_eq!(config.port_forwards[2].proto, Proto::Udp);
307 config.validate_port_forwards().unwrap();
308 }
309
310 #[test]
311 fn test_port_forwards_reject_ipv4_target() {
312 let yaml = r#"
313pool: "fd01::/112"
314lan_interface: "eth0"
315port_forwards:
316 - listen_port: 8080
317 proto: tcp
318 target: "192.168.1.10:80"
319"#;
320 let result: Result<GatewayConfig, _> = serde_yaml::from_str(yaml);
321 assert!(
322 result.is_err(),
323 "IPv4 target must fail to deserialize as SocketAddrV6"
324 );
325 }
326
327 #[test]
328 fn test_port_forwards_reject_zero_listen_port() {
329 let yaml = r#"
330pool: "fd01::/112"
331lan_interface: "eth0"
332port_forwards:
333 - listen_port: 0
334 proto: tcp
335 target: "[fd12:3456::10]:80"
336"#;
337 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
338 assert!(config.validate_port_forwards().is_err());
339 }
340
341 #[test]
342 fn test_port_forwards_reject_duplicate() {
343 let yaml = r#"
344pool: "fd01::/112"
345lan_interface: "eth0"
346port_forwards:
347 - listen_port: 8080
348 proto: tcp
349 target: "[fd12:3456::10]:80"
350 - listen_port: 8080
351 proto: tcp
352 target: "[fd12:3456::20]:80"
353"#;
354 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
355 let err = config.validate_port_forwards().unwrap_err();
356 assert!(err.contains("duplicate"), "got: {err}");
357 }
358
359 #[test]
360 fn test_port_forwards_same_port_different_proto_ok() {
361 let yaml = r#"
362pool: "fd01::/112"
363lan_interface: "eth0"
364port_forwards:
365 - listen_port: 53
366 proto: tcp
367 target: "[fd12:3456::10]:53"
368 - listen_port: 53
369 proto: udp
370 target: "[fd12:3456::10]:53"
371"#;
372 let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
373 config.validate_port_forwards().unwrap();
374 }
375}