Skip to main content

fips_core/config/
gateway.rs

1//! Gateway configuration types.
2//!
3//! Configuration for the outbound LAN gateway (`gateway.*`).
4
5use std::collections::HashSet;
6use std::net::SocketAddrV6;
7
8use serde::{Deserialize, Serialize};
9
10/// Default gateway DNS listen address.
11const DEFAULT_DNS_LISTEN: &str = "[::]:53";
12
13/// Default upstream DNS resolver (FIPS daemon).
14///
15/// Must match the daemon's `dns.bind_addr` default (`::1`). Linux
16/// IPv6 sockets bound to explicit `::1` do not accept v4-mapped
17/// traffic — so a v4 upstream like `127.0.0.1:5354` cannot reach a
18/// daemon bound on `[::1]:5354`. Operators who set a non-default
19/// `dns.bind_addr` on the daemon must also set this field
20/// accordingly.
21const DEFAULT_DNS_UPSTREAM: &str = "[::1]:5354";
22
23/// Default DNS TTL in seconds.
24const DEFAULT_DNS_TTL: u32 = 60;
25
26/// Default pool grace period in seconds.
27const DEFAULT_GRACE_PERIOD: u64 = 60;
28
29/// Default conntrack TCP established timeout (5 days).
30const DEFAULT_CT_TCP_ESTABLISHED: u64 = 432_000;
31
32/// Default conntrack UDP timeout (unreplied).
33const DEFAULT_CT_UDP_TIMEOUT: u64 = 30;
34
35/// Default conntrack UDP assured timeout (bidirectional).
36const DEFAULT_CT_UDP_ASSURED: u64 = 180;
37
38/// Default conntrack ICMP timeout.
39const DEFAULT_CT_ICMP_TIMEOUT: u64 = 30;
40
41/// Gateway configuration (`gateway.*`).
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct GatewayConfig {
44    /// Enable the gateway (`gateway.enabled`, default: false).
45    #[serde(default)]
46    pub enabled: bool,
47
48    /// Virtual IP pool CIDR (e.g., `fd01::/112`).
49    pub pool: String,
50
51    /// LAN-facing interface for proxy ARP/NDP.
52    pub lan_interface: String,
53
54    /// Gateway DNS configuration.
55    #[serde(default)]
56    pub dns: GatewayDnsConfig,
57
58    /// Pool grace period in seconds after last session before reclamation.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub pool_grace_period: Option<u64>,
61
62    /// Conntrack timeout overrides.
63    #[serde(default)]
64    pub conntrack: ConntrackConfig,
65
66    /// Inbound mesh port forwarding rules. See TASK-2026-0061.
67    #[serde(default, skip_serializing_if = "Vec::is_empty")]
68    pub port_forwards: Vec<PortForward>,
69}
70
71impl GatewayConfig {
72    /// Get pool grace period (default: 60 seconds).
73    pub fn grace_period(&self) -> u64 {
74        self.pool_grace_period.unwrap_or(DEFAULT_GRACE_PERIOD)
75    }
76
77    /// Validate inbound port-forward rules: non-zero listen ports and
78    /// uniqueness of `(listen_port, proto)` pairs across the list.
79    /// IPv6-only targets are enforced by `SocketAddrV6` at deserialize
80    /// time.
81    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/// Transport protocol for an inbound port forward.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum Proto {
102    Tcp,
103    Udp,
104}
105
106/// An inbound port-forward rule: `fips0:listen_port/proto` → `target`.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct PortForward {
109    /// Port on `fips0` that mesh peers connect to.
110    pub listen_port: u16,
111    /// Transport protocol to match.
112    pub proto: Proto,
113    /// IPv6 LAN destination (`[addr]:port`). IPv4 targets are rejected
114    /// at parse time by `SocketAddrV6`.
115    pub target: SocketAddrV6,
116}
117
118/// Gateway DNS resolver configuration (`gateway.dns.*`).
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct GatewayDnsConfig {
121    /// Listen address and port (default: `0.0.0.0:53`).
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub listen: Option<String>,
124
125    /// Upstream FIPS daemon DNS resolver (default: `[::1]:5354`,
126    /// matching the daemon's `dns.bind_addr` default).
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub upstream: Option<String>,
129
130    /// DNS record TTL in seconds (default: 60).
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub ttl: Option<u32>,
133}
134
135impl GatewayDnsConfig {
136    /// Get the listen address (default: `0.0.0.0:53`).
137    pub fn listen(&self) -> &str {
138        self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN)
139    }
140
141    /// Get the upstream resolver address (default: `[::1]:5354`).
142    pub fn upstream(&self) -> &str {
143        self.upstream.as_deref().unwrap_or(DEFAULT_DNS_UPSTREAM)
144    }
145
146    /// Get the TTL in seconds (default: 60).
147    pub fn ttl(&self) -> u32 {
148        self.ttl.unwrap_or(DEFAULT_DNS_TTL)
149    }
150}
151
152/// Conntrack timeout overrides (`gateway.conntrack.*`).
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154pub struct ConntrackConfig {
155    /// TCP established timeout in seconds.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub tcp_established: Option<u64>,
158
159    /// UDP unreplied timeout in seconds.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub udp_timeout: Option<u64>,
162
163    /// UDP assured (bidirectional) timeout in seconds.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub udp_assured: Option<u64>,
166
167    /// ICMP timeout in seconds.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub icmp_timeout: Option<u64>,
170}
171
172impl ConntrackConfig {
173    /// TCP established timeout (default: 432000s / 5 days).
174    pub fn tcp_established(&self) -> u64 {
175        self.tcp_established.unwrap_or(DEFAULT_CT_TCP_ESTABLISHED)
176    }
177
178    /// UDP unreplied timeout (default: 30s).
179    pub fn udp_timeout(&self) -> u64 {
180        self.udp_timeout.unwrap_or(DEFAULT_CT_UDP_TIMEOUT)
181    }
182
183    /// UDP assured timeout (default: 180s).
184    pub fn udp_assured(&self) -> u64 {
185        self.udp_assured.unwrap_or(DEFAULT_CT_UDP_ASSURED)
186    }
187
188    /// ICMP timeout (default: 30s).
189    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        // Unset fields use defaults
239        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}