1use std::net::Ipv4Addr;
2
3#[derive(Debug, Clone)]
5pub enum NetworkMode {
6 None,
8 Host,
10 Bridge(BridgeConfig),
12}
13
14#[derive(
19 Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
20)]
21#[serde(rename_all = "lowercase")]
22pub enum NatBackend {
23 #[value(name = "auto")]
25 Auto,
26 #[value(name = "kernel")]
28 Kernel,
29 #[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#[derive(Debug, Clone)]
46pub struct BridgeConfig {
47 pub bridge_name: String,
49 pub subnet: String,
51 pub container_ip: Option<String>,
53 pub dns: Vec<String>,
55 pub port_forwards: Vec<PortForward>,
57 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 dns: Vec::new(),
70 port_forwards: Vec::new(),
71 nat_backend: NatBackend::Auto,
72 }
73 }
74}
75
76impl BridgeConfig {
77 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 pub fn validate(&self) -> crate::error::Result<()> {
104 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 validate_ipv4_cidr(&self.subnet).map_err(crate::error::NucleusError::NetworkError)?;
124
125 if let Some(ref ip) = self.container_ip {
127 validate_ipv4_addr(ip).map_err(crate::error::NucleusError::NetworkError)?;
128 }
129
130 for dns in &self.dns {
132 validate_ipv4_addr(dns).map_err(crate::error::NucleusError::NetworkError)?;
133 }
134
135 Ok(())
136 }
137}
138
139fn 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
163fn 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
178pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
180 validate_ipv4_cidr(s)
181}
182
183#[derive(Debug, Clone)]
189pub struct EgressPolicy {
190 pub allowed_cidrs: Vec<String>,
192 pub allowed_tcp_ports: Vec<u16>,
194 pub allowed_udp_ports: Vec<u16>,
196 pub log_denied: bool,
198 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 pub fn deny_all() -> Self {
218 Self {
219 allow_dns: false,
220 ..Self::default()
221 }
222 }
223
224 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#[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#[derive(Debug, Clone)]
265pub struct PortForward {
266 pub host_ip: Option<Ipv4Addr>,
268 pub host_port: u16,
270 pub container_port: u16,
272 pub protocol: Protocol,
274}
275
276impl PortForward {
277 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}