Skip to main content

nucleus/network/
config.rs

1use std::net::Ipv4Addr;
2
3/// Network mode for container
4#[derive(Debug, Clone)]
5pub enum NetworkMode {
6    /// No networking (default, fully isolated)
7    None,
8    /// Share host network namespace
9    Host,
10    /// Bridge network with NAT
11    Bridge(BridgeConfig),
12}
13
14/// NAT backend for native bridge-style networking.
15///
16/// `Auto` preserves the historical behavior for privileged callers while
17/// enabling a userspace NAT path for rootless/native containers.
18#[derive(
19    Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
20)]
21#[serde(rename_all = "lowercase")]
22pub enum NatBackend {
23    /// Select kernel bridge + iptables when privileged, otherwise userspace NAT.
24    #[value(name = "auto")]
25    Auto,
26    /// Require the kernel bridge/veth/iptables backend.
27    #[value(name = "kernel")]
28    Kernel,
29    /// Require the userspace NAT backend.
30    #[value(name = "userspace")]
31    Userspace,
32}
33
34impl NatBackend {
35    pub fn as_str(self) -> &'static str {
36        match self {
37            Self::Auto => "auto",
38            Self::Kernel => "kernel",
39            Self::Userspace => "userspace",
40        }
41    }
42}
43
44/// Configuration for bridge networking
45#[derive(Debug, Clone)]
46pub struct BridgeConfig {
47    /// Bridge interface name
48    pub bridge_name: String,
49    /// Subnet (e.g., "10.0.42.0/24")
50    pub subnet: String,
51    /// Container IP address (auto-assigned from subnet)
52    pub container_ip: Option<String>,
53    /// DNS servers
54    pub dns: Vec<String>,
55    /// Port forwarding rules
56    pub port_forwards: Vec<PortForward>,
57    /// NAT backend selection for the native runtime.
58    pub nat_backend: NatBackend,
59}
60
61impl Default for BridgeConfig {
62    fn default() -> Self {
63        Self {
64            bridge_name: "nucleus0".to_string(),
65            subnet: "10.0.42.0/24".to_string(),
66            container_ip: None,
67            // Empty by default – production services must configure DNS explicitly.
68            // Agent mode callers can use BridgeConfig::with_public_dns() for convenience.
69            dns: Vec::new(),
70            port_forwards: Vec::new(),
71            nat_backend: NatBackend::Auto,
72        }
73    }
74}
75
76impl BridgeConfig {
77    /// Convenience: populate with public Google DNS resolvers.
78    /// Suitable for agent/sandbox workloads, NOT for production services.
79    pub fn with_public_dns(mut self) -> Self {
80        self.dns = vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()];
81        self
82    }
83
84    pub fn with_dns(mut self, servers: Vec<String>) -> Self {
85        self.dns = servers;
86        self
87    }
88
89    pub fn with_nat_backend(mut self, backend: NatBackend) -> Self {
90        self.nat_backend = backend;
91        self
92    }
93
94    pub fn selected_nat_backend(&self, host_is_root: bool, rootless: bool) -> NatBackend {
95        match self.nat_backend {
96            NatBackend::Auto if host_is_root && !rootless => NatBackend::Kernel,
97            NatBackend::Auto => NatBackend::Userspace,
98            explicit => explicit,
99        }
100    }
101
102    /// Validate all fields to prevent argument injection into ip/iptables commands.
103    pub fn validate(&self) -> crate::error::Result<()> {
104        // Bridge name: alphanumeric, dash, underscore; max 15 chars (Linux IFNAMSIZ)
105        if self.bridge_name.is_empty() || self.bridge_name.len() > 15 {
106            return Err(crate::error::NucleusError::NetworkError(format!(
107                "Bridge name must be 1-15 characters, got '{}'",
108                self.bridge_name
109            )));
110        }
111        if !self
112            .bridge_name
113            .chars()
114            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
115        {
116            return Err(crate::error::NucleusError::NetworkError(format!(
117                "Bridge name contains invalid characters (allowed: a-zA-Z0-9_-): '{}'",
118                self.bridge_name
119            )));
120        }
121
122        // Subnet: must be valid IPv4 CIDR
123        validate_ipv4_cidr(&self.subnet).map_err(crate::error::NucleusError::NetworkError)?;
124
125        // Container IP (if specified)
126        if let Some(ref ip) = self.container_ip {
127            validate_ipv4_addr(ip).map_err(crate::error::NucleusError::NetworkError)?;
128        }
129
130        // DNS servers
131        for dns in &self.dns {
132            validate_ipv4_addr(dns).map_err(crate::error::NucleusError::NetworkError)?;
133        }
134
135        Ok(())
136    }
137}
138
139/// Validate that a string is a valid IPv4 address (no leading dashes, proper octets).
140fn validate_ipv4_addr(s: &str) -> Result<(), String> {
141    let parts: Vec<&str> = s.split('.').collect();
142    if parts.len() != 4 {
143        return Err(format!("Invalid IPv4 address: '{}'", s));
144    }
145    for part in &parts {
146        if part.is_empty() {
147            return Err(format!("Invalid IPv4 address: '{}'", s));
148        }
149        if part.len() > 1 && part.starts_with('0') {
150            return Err(format!(
151                "Invalid IPv4 address: '{}' – octet '{}' has leading zero",
152                s, part
153            ));
154        }
155        match part.parse::<u8>() {
156            Ok(_) => {}
157            Err(_) => return Err(format!("Invalid IPv4 address: '{}'", s)),
158        }
159    }
160    Ok(())
161}
162
163/// Validate that a string is a valid IPv4 CIDR (e.g., "10.0.42.0/24").
164fn validate_ipv4_cidr(s: &str) -> Result<(), String> {
165    let (addr, prefix) = s
166        .split_once('/')
167        .ok_or_else(|| format!("Invalid CIDR (missing /prefix): '{}'", s))?;
168    validate_ipv4_addr(addr)?;
169    let prefix: u8 = prefix
170        .parse()
171        .map_err(|_| format!("Invalid CIDR prefix: '{}'", s))?;
172    if prefix > 32 {
173        return Err(format!("CIDR prefix must be 0-32, got {}", prefix));
174    }
175    Ok(())
176}
177
178/// Validate that a string is a valid IPv4 CIDR for egress rules.
179pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
180    validate_ipv4_cidr(s)
181}
182
183/// Egress policy for audited outbound network access.
184///
185/// When set, iptables OUTPUT chain rules restrict which destinations the
186/// container process can connect to. Use [`EgressPolicy::deny_all`] when no
187/// outbound connections, including DNS, should be permitted.
188#[derive(Debug, Clone)]
189pub struct EgressPolicy {
190    /// Allowed destination CIDRs (e.g., "10.0.0.0/8", "192.168.1.0/24").
191    pub allowed_cidrs: Vec<String>,
192    /// Allowed destination TCP ports. Empty means all ports on allowed CIDRs.
193    pub allowed_tcp_ports: Vec<u16>,
194    /// Allowed destination UDP ports.
195    pub allowed_udp_ports: Vec<u16>,
196    /// Whether to log denied egress attempts (rate-limited).
197    pub log_denied: bool,
198    /// Whether to add implicit DNS (port 53 UDP/TCP) allow rules for configured
199    /// resolvers. Defaults to `true` for explicit allowlist usability.
200    pub allow_dns: bool,
201}
202
203impl Default for EgressPolicy {
204    fn default() -> Self {
205        Self {
206            allowed_cidrs: Vec::new(),
207            allowed_tcp_ports: Vec::new(),
208            allowed_udp_ports: Vec::new(),
209            log_denied: true,
210            allow_dns: true,
211        }
212    }
213}
214
215impl EgressPolicy {
216    /// Create a strict deny-all egress policy, including DNS.
217    pub fn deny_all() -> Self {
218        Self {
219            allow_dns: false,
220            ..Self::default()
221        }
222    }
223
224    /// Allow egress to the given CIDRs on any port.
225    pub fn with_allowed_cidrs(mut self, cidrs: Vec<String>) -> Self {
226        self.allowed_cidrs = cidrs;
227        self
228    }
229
230    pub fn with_allowed_tcp_ports(mut self, ports: Vec<u16>) -> Self {
231        self.allowed_tcp_ports = ports;
232        self
233    }
234
235    pub fn with_allowed_udp_ports(mut self, ports: Vec<u16>) -> Self {
236        self.allowed_udp_ports = ports;
237        self
238    }
239}
240
241/// Network protocol for port forwarding rules.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum Protocol {
244    Tcp,
245    Udp,
246}
247
248impl Protocol {
249    pub fn as_str(self) -> &'static str {
250        match self {
251            Self::Tcp => "tcp",
252            Self::Udp => "udp",
253        }
254    }
255}
256
257impl std::fmt::Display for Protocol {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.write_str(self.as_str())
260    }
261}
262
263/// Port forwarding rule
264#[derive(Debug, Clone)]
265pub struct PortForward {
266    /// Optional host bind IP address. When omitted, match all local addresses.
267    pub host_ip: Option<Ipv4Addr>,
268    /// Host port
269    pub host_port: u16,
270    /// Container port
271    pub container_port: u16,
272    /// Protocol (tcp/udp)
273    pub protocol: Protocol,
274}
275
276impl PortForward {
277    /// Parse a port forward spec like:
278    /// - "8080:80"
279    /// - "8080:80/udp"
280    /// - "127.0.0.1:8080:80"
281    /// - "127.0.0.1:8080:80/udp"
282    pub fn parse(spec: &str) -> crate::error::Result<Self> {
283        let (ports, protocol) = if let Some((p, proto)) = spec.rsplit_once('/') {
284            let protocol = match proto {
285                "tcp" => Protocol::Tcp,
286                "udp" => Protocol::Udp,
287                _ => {
288                    return Err(crate::error::NucleusError::ConfigError(format!(
289                        "Invalid protocol '{}', must be tcp or udp",
290                        proto
291                    )))
292                }
293            };
294            (p, protocol)
295        } else {
296            (spec, Protocol::Tcp)
297        };
298
299        let parts: Vec<&str> = ports.split(':').collect();
300        let (host_ip, host_port, container_port) = match parts.as_slice() {
301            [host_port, container_port] => (None, *host_port, *container_port),
302            [host_ip, host_port, container_port] => {
303                validate_ipv4_addr(host_ip).map_err(crate::error::NucleusError::ConfigError)?;
304                let host_ip = host_ip.parse::<Ipv4Addr>().map_err(|_| {
305                    crate::error::NucleusError::ConfigError(format!(
306                        "Invalid host IP address: {}",
307                        host_ip
308                    ))
309                })?;
310                (Some(host_ip), *host_port, *container_port)
311            }
312            _ => {
313                return Err(crate::error::NucleusError::ConfigError(format!(
314                    "Invalid port forward format '{}', expected HOST:CONTAINER or HOST_IP:HOST:CONTAINER",
315                    spec
316                )))
317            }
318        };
319
320        let host_port: u16 = host_port.parse().map_err(|_| {
321            crate::error::NucleusError::ConfigError(format!("Invalid host port: {}", host_port))
322        })?;
323        let container_port: u16 = container_port.parse().map_err(|_| {
324            crate::error::NucleusError::ConfigError(format!(
325                "Invalid container port: {}",
326                container_port
327            ))
328        })?;
329
330        Ok(Self {
331            host_ip,
332            host_port,
333            container_port,
334            protocol,
335        })
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_port_forward_parse() {
345        let pf = PortForward::parse("8080:80").unwrap();
346        assert_eq!(pf.host_ip, None);
347        assert_eq!(pf.host_port, 8080);
348        assert_eq!(pf.container_port, 80);
349        assert_eq!(pf.protocol, Protocol::Tcp);
350
351        let pf = PortForward::parse("5353:53/udp").unwrap();
352        assert_eq!(pf.host_ip, None);
353        assert_eq!(pf.host_port, 5353);
354        assert_eq!(pf.container_port, 53);
355        assert_eq!(pf.protocol, Protocol::Udp);
356
357        let pf = PortForward::parse("127.0.0.1:8080:80").unwrap();
358        assert_eq!(pf.host_ip, Some(Ipv4Addr::new(127, 0, 0, 1)));
359        assert_eq!(pf.host_port, 8080);
360        assert_eq!(pf.container_port, 80);
361        assert_eq!(pf.protocol, Protocol::Tcp);
362
363        let pf = PortForward::parse("10.0.0.5:5353:53/udp").unwrap();
364        assert_eq!(pf.host_ip, Some(Ipv4Addr::new(10, 0, 0, 5)));
365        assert_eq!(pf.host_port, 5353);
366        assert_eq!(pf.container_port, 53);
367        assert_eq!(pf.protocol, Protocol::Udp);
368    }
369
370    #[test]
371    fn test_port_forward_parse_invalid() {
372        assert!(PortForward::parse("8080").is_err());
373        assert!(PortForward::parse("abc:80").is_err());
374        assert!(PortForward::parse("8080:abc").is_err());
375        assert!(PortForward::parse("127.0.0.1:abc:80").is_err());
376        assert!(PortForward::parse("999.0.0.1:8080:80").is_err());
377    }
378
379    #[test]
380    fn test_validate_ipv4_addr_rejects_leading_zeros() {
381        assert!(validate_ipv4_addr("10.0.42.1").is_ok());
382        assert!(validate_ipv4_addr("0.0.0.0").is_ok());
383        assert!(
384            validate_ipv4_addr("010.0.0.1").is_err(),
385            "leading zero in first octet must be rejected"
386        );
387        assert!(
388            validate_ipv4_addr("10.01.0.1").is_err(),
389            "leading zero in second octet must be rejected"
390        );
391        assert!(
392            validate_ipv4_addr("10.0.01.1").is_err(),
393            "leading zero in third octet must be rejected"
394        );
395        assert!(
396            validate_ipv4_addr("10.0.0.01").is_err(),
397            "leading zero in fourth octet must be rejected"
398        );
399    }
400}