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