1use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4};
2use std::str::FromStr;
3
4#[derive(Debug, Clone)]
6pub enum NetworkMode {
7 None,
9 Host,
11 GVisorHost,
14 Bridge(BridgeConfig),
16}
17
18#[derive(
23 Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
24)]
25#[serde(rename_all = "lowercase")]
26pub enum NatBackend {
27 #[value(name = "auto")]
29 Auto,
30 #[value(name = "kernel")]
32 Kernel,
33 #[value(name = "userspace")]
35 Userspace,
36}
37
38impl NatBackend {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Auto => "auto",
42 Self::Kernel => "kernel",
43 Self::Userspace => "userspace",
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct BridgeConfig {
51 pub bridge_name: String,
53 pub subnet: String,
55 pub container_ip: Option<String>,
57 pub dns: Vec<String>,
59 pub port_forwards: Vec<PortForward>,
61 pub nat_backend: NatBackend,
63}
64
65impl Default for BridgeConfig {
66 fn default() -> Self {
67 Self {
68 bridge_name: "nucleus0".to_string(),
69 subnet: "10.0.42.0/24".to_string(),
70 container_ip: None,
71 dns: Vec::new(),
74 port_forwards: Vec::new(),
75 nat_backend: NatBackend::Auto,
76 }
77 }
78}
79
80impl BridgeConfig {
81 pub fn with_public_dns(mut self) -> Self {
84 self.dns = vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()];
85 self
86 }
87
88 pub fn with_dns(mut self, servers: Vec<String>) -> Self {
89 self.dns = servers;
90 self
91 }
92
93 pub fn with_nat_backend(mut self, backend: NatBackend) -> Self {
94 self.nat_backend = backend;
95 self
96 }
97
98 pub fn selected_nat_backend(&self, host_is_root: bool, rootless: bool) -> NatBackend {
99 match self.nat_backend {
100 NatBackend::Auto if host_is_root && !rootless => NatBackend::Kernel,
101 NatBackend::Auto => NatBackend::Userspace,
102 explicit => explicit,
103 }
104 }
105
106 pub fn contains_ipv4(&self, ip: Ipv4Addr) -> Result<bool, String> {
107 let (network, prefix) = parse_ipv4_cidr(&self.subnet)?;
108 Ok(ipv4_in_cidr(ip, network, prefix))
109 }
110
111 pub fn gateway_ipv4(&self) -> Result<Ipv4Addr, String> {
113 let (network, prefix) = parse_ipv4_cidr(&self.subnet)?;
114 let network_u32 = u32::from(network) & ipv4_mask(prefix);
115 let gateway = if prefix >= 31 {
116 network_u32
117 } else {
118 network_u32
119 .checked_add(1)
120 .ok_or_else(|| format!("CIDR '{}' overflowed", self.subnet))?
121 };
122 Ok(Ipv4Addr::from(gateway))
123 }
124
125 pub fn validate(&self) -> crate::error::Result<()> {
127 if self.bridge_name.is_empty() || self.bridge_name.len() > 15 {
129 return Err(crate::error::NucleusError::NetworkError(format!(
130 "Bridge name must be 1-15 characters, got '{}'",
131 self.bridge_name
132 )));
133 }
134 if !self
135 .bridge_name
136 .chars()
137 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
138 {
139 return Err(crate::error::NucleusError::NetworkError(format!(
140 "Bridge name contains invalid characters (allowed: a-zA-Z0-9_-): '{}'",
141 self.bridge_name
142 )));
143 }
144
145 validate_ipv4_cidr(&self.subnet).map_err(crate::error::NucleusError::NetworkError)?;
147
148 if let Some(ref ip) = self.container_ip {
150 validate_ipv4_addr(ip).map_err(crate::error::NucleusError::NetworkError)?;
151 }
152
153 for dns in &self.dns {
155 validate_ipv4_addr(dns).map_err(crate::error::NucleusError::NetworkError)?;
156 }
157
158 Ok(())
159 }
160}
161
162fn validate_ipv4_addr(s: &str) -> Result<(), String> {
164 let parts: Vec<&str> = s.split('.').collect();
165 if parts.len() != 4 {
166 return Err(format!("Invalid IPv4 address: '{}'", s));
167 }
168 for part in &parts {
169 if part.is_empty() {
170 return Err(format!("Invalid IPv4 address: '{}'", s));
171 }
172 if part.len() > 1 && part.starts_with('0') {
173 return Err(format!(
174 "Invalid IPv4 address: '{}' – octet '{}' has leading zero",
175 s, part
176 ));
177 }
178 match part.parse::<u8>() {
179 Ok(_) => {}
180 Err(_) => return Err(format!("Invalid IPv4 address: '{}'", s)),
181 }
182 }
183 Ok(())
184}
185
186fn parse_ipv4_cidr(s: &str) -> Result<(Ipv4Addr, u8), String> {
187 let (addr, prefix) = s
188 .split_once('/')
189 .ok_or_else(|| format!("Invalid CIDR (missing /prefix): '{}'", s))?;
190 validate_ipv4_addr(addr)?;
191 let addr = addr
192 .parse::<Ipv4Addr>()
193 .map_err(|_| format!("Invalid IPv4 address: '{}'", addr))?;
194 let prefix: u8 = prefix
195 .parse()
196 .map_err(|_| format!("Invalid CIDR prefix: '{}'", s))?;
197 if prefix > 32 {
198 return Err(format!("CIDR prefix must be 0-32, got {}", prefix));
199 }
200 Ok((addr, prefix))
201}
202
203fn ipv4_mask(prefix: u8) -> u32 {
204 if prefix == 0 {
205 0
206 } else {
207 u32::MAX << (32 - prefix)
208 }
209}
210
211fn ipv4_in_cidr(ip: Ipv4Addr, network: Ipv4Addr, prefix: u8) -> bool {
212 let mask = ipv4_mask(prefix);
213 (u32::from(ip) & mask) == (u32::from(network) & mask)
214}
215
216fn validate_ipv4_cidr(s: &str) -> Result<(), String> {
218 parse_ipv4_cidr(s).map(|_| ())
219}
220
221pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
223 validate_ipv4_cidr(s)
224}
225
226pub fn validate_egress_domain(s: &str) -> Result<(), String> {
228 if s.is_empty() {
229 return Err("Egress domain cannot be empty".to_string());
230 }
231 if s.parse::<Ipv4Addr>().is_ok() || s.parse::<Ipv6Addr>().is_ok() {
232 return Err(format!(
233 "Egress domain '{}' is an IP address; use --egress-allow with a CIDR",
234 s
235 ));
236 }
237
238 let domain = s.strip_suffix('.').unwrap_or(s);
239 if domain.is_empty() {
240 return Err("Egress domain cannot be only '.'".to_string());
241 }
242 if domain.len() > 253 {
243 return Err(format!("Egress domain '{}' is longer than 253 bytes", s));
244 }
245 if !domain.contains('.') {
246 return Err(format!(
247 "Egress domain '{}' must be a fully-qualified name with at least one dot",
248 s
249 ));
250 }
251
252 for label in domain.split('.') {
253 if label.is_empty() {
254 return Err(format!("Egress domain '{}' contains an empty label", s));
255 }
256 if label.len() > 63 {
257 return Err(format!(
258 "Egress domain '{}' contains a label longer than 63 bytes",
259 s
260 ));
261 }
262 if label.starts_with('-') || label.ends_with('-') {
263 return Err(format!(
264 "Egress domain '{}' contains a label starting or ending with '-'",
265 s
266 ));
267 }
268 if !label
269 .bytes()
270 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
271 {
272 return Err(format!("Egress domain '{}' contains invalid characters", s));
273 }
274 }
275
276 Ok(())
277}
278
279#[derive(Debug, Clone)]
285pub struct EgressPolicy {
286 pub allowed_cidrs: Vec<String>,
288 pub allowed_domains: Vec<String>,
290 pub allowed_tcp_ports: Vec<u16>,
292 pub allowed_udp_ports: Vec<u16>,
294 pub log_denied: bool,
296 pub allow_dns: bool,
299}
300
301impl Default for EgressPolicy {
302 fn default() -> Self {
303 Self {
304 allowed_cidrs: Vec::new(),
305 allowed_domains: Vec::new(),
306 allowed_tcp_ports: Vec::new(),
307 allowed_udp_ports: Vec::new(),
308 log_denied: true,
309 allow_dns: true,
310 }
311 }
312}
313
314impl EgressPolicy {
315 pub fn deny_all() -> Self {
317 Self {
318 allow_dns: false,
319 ..Self::default()
320 }
321 }
322
323 pub fn with_allowed_cidrs(mut self, cidrs: Vec<String>) -> Self {
325 self.allowed_cidrs = cidrs;
326 self
327 }
328
329 pub fn with_allowed_domains(mut self, domains: Vec<String>) -> Self {
331 self.allowed_domains = domains;
332 self
333 }
334
335 pub fn with_allowed_tcp_ports(mut self, ports: Vec<u16>) -> Self {
336 self.allowed_tcp_ports = ports;
337 self
338 }
339
340 pub fn with_allowed_udp_ports(mut self, ports: Vec<u16>) -> Self {
341 self.allowed_udp_ports = ports;
342 self
343 }
344
345 pub fn credential_broker_only(broker: &CredentialBrokerConfig) -> Self {
348 Self::deny_all()
349 .with_allowed_cidrs(vec![broker.broker_cidr()])
350 .with_allowed_tcp_ports(vec![broker.broker_port])
351 }
352
353 pub fn is_credential_broker_only(&self, broker: &CredentialBrokerConfig) -> bool {
356 self.allowed_cidrs == vec![broker.broker_cidr()]
357 && self.allowed_domains.is_empty()
358 && self.allowed_tcp_ports == vec![broker.broker_port]
359 && self.allowed_udp_ports.is_empty()
360 && !self.allow_dns
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct CredentialBrokerConfig {
372 pub broker_ip: Ipv4Addr,
374 pub broker_port: u16,
376 pub inject_proxy_env: bool,
380}
381
382impl CredentialBrokerConfig {
383 pub fn new(broker_ip: Ipv4Addr, broker_port: u16) -> Self {
385 Self {
386 broker_ip,
387 broker_port,
388 inject_proxy_env: true,
389 }
390 }
391
392 pub fn parse_endpoint(endpoint: &str) -> Result<Self, String> {
394 let socket = SocketAddrV4::from_str(endpoint).map_err(|_| {
395 format!(
396 "Invalid credential broker endpoint '{}', expected IPv4:PORT",
397 endpoint
398 )
399 })?;
400 let config = Self::new(*socket.ip(), socket.port());
401 config.validate()?;
402 Ok(config)
403 }
404
405 pub fn with_proxy_env(mut self, inject_proxy_env: bool) -> Self {
406 self.inject_proxy_env = inject_proxy_env;
407 self
408 }
409
410 pub fn validate(&self) -> Result<(), String> {
411 if self.broker_port == 0 {
412 return Err("Credential broker port must be non-zero".to_string());
413 }
414 if self.broker_ip.is_unspecified() {
415 return Err("Credential broker IP must not be 0.0.0.0".to_string());
416 }
417 if self.broker_ip.is_loopback() {
418 return Err(
419 "Credential broker IP must be reachable on the bridge, not container loopback"
420 .to_string(),
421 );
422 }
423 if self.broker_ip.is_multicast() || self.broker_ip == Ipv4Addr::BROADCAST {
424 return Err("Credential broker IP must be a unicast IPv4 address".to_string());
425 }
426 Ok(())
427 }
428
429 pub fn validate_for_bridge(&self, bridge: &BridgeConfig) -> Result<(), String> {
430 self.validate()?;
431 let gateway = bridge.gateway_ipv4()?;
432 if self.broker_ip != gateway {
433 return Err(format!(
434 "Credential broker IP must be the host-side bridge address {} for subnet {}, got {}",
435 gateway, bridge.subnet, self.broker_ip
436 ));
437 }
438 Ok(())
439 }
440
441 pub fn broker_cidr(&self) -> String {
442 format!("{}/32", self.broker_ip)
443 }
444
445 pub fn proxy_url(&self) -> String {
446 format!("http://{}:{}", self.broker_ip, self.broker_port)
447 }
448
449 pub fn proxy_environment(&self) -> Vec<(String, String)> {
453 if !self.inject_proxy_env {
454 return Vec::new();
455 }
456
457 let url = self.proxy_url();
458 ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"]
459 .into_iter()
460 .map(|key| (key.to_string(), url.clone()))
461 .collect()
462 }
463
464 pub fn egress_policy(&self) -> EgressPolicy {
465 EgressPolicy::credential_broker_only(self)
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum Protocol {
472 Tcp,
473 Udp,
474}
475
476impl Protocol {
477 pub fn as_str(self) -> &'static str {
478 match self {
479 Self::Tcp => "tcp",
480 Self::Udp => "udp",
481 }
482 }
483}
484
485impl std::fmt::Display for Protocol {
486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487 f.write_str(self.as_str())
488 }
489}
490
491#[derive(Debug, Clone)]
493pub struct PortForward {
494 pub host_ip: Option<Ipv4Addr>,
496 pub host_port: u16,
498 pub container_port: u16,
500 pub protocol: Protocol,
502}
503
504impl PortForward {
505 pub fn parse(spec: &str) -> crate::error::Result<Self> {
511 let (ports, protocol) = if let Some((p, proto)) = spec.rsplit_once('/') {
512 let protocol = match proto {
513 "tcp" => Protocol::Tcp,
514 "udp" => Protocol::Udp,
515 _ => {
516 return Err(crate::error::NucleusError::ConfigError(format!(
517 "Invalid protocol '{}', must be tcp or udp",
518 proto
519 )))
520 }
521 };
522 (p, protocol)
523 } else {
524 (spec, Protocol::Tcp)
525 };
526
527 let parts: Vec<&str> = ports.split(':').collect();
528 let (host_ip, host_port, container_port) = match parts.as_slice() {
529 [host_port, container_port] => (None, *host_port, *container_port),
530 [host_ip, host_port, container_port] => {
531 validate_ipv4_addr(host_ip).map_err(crate::error::NucleusError::ConfigError)?;
532 let host_ip = host_ip.parse::<Ipv4Addr>().map_err(|_| {
533 crate::error::NucleusError::ConfigError(format!(
534 "Invalid host IP address: {}",
535 host_ip
536 ))
537 })?;
538 (Some(host_ip), *host_port, *container_port)
539 }
540 _ => {
541 return Err(crate::error::NucleusError::ConfigError(format!(
542 "Invalid port forward format '{}', expected HOST:CONTAINER or HOST_IP:HOST:CONTAINER",
543 spec
544 )))
545 }
546 };
547
548 let host_port: u16 = host_port.parse().map_err(|_| {
549 crate::error::NucleusError::ConfigError(format!("Invalid host port: {}", host_port))
550 })?;
551 let container_port: u16 = container_port.parse().map_err(|_| {
552 crate::error::NucleusError::ConfigError(format!(
553 "Invalid container port: {}",
554 container_port
555 ))
556 })?;
557
558 Ok(Self {
559 host_ip,
560 host_port,
561 container_port,
562 protocol,
563 })
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_port_forward_parse() {
573 let pf = PortForward::parse("8080:80").unwrap();
574 assert_eq!(pf.host_ip, None);
575 assert_eq!(pf.host_port, 8080);
576 assert_eq!(pf.container_port, 80);
577 assert_eq!(pf.protocol, Protocol::Tcp);
578
579 let pf = PortForward::parse("5353:53/udp").unwrap();
580 assert_eq!(pf.host_ip, None);
581 assert_eq!(pf.host_port, 5353);
582 assert_eq!(pf.container_port, 53);
583 assert_eq!(pf.protocol, Protocol::Udp);
584
585 let pf = PortForward::parse("127.0.0.1:8080:80").unwrap();
586 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(127, 0, 0, 1)));
587 assert_eq!(pf.host_port, 8080);
588 assert_eq!(pf.container_port, 80);
589 assert_eq!(pf.protocol, Protocol::Tcp);
590
591 let pf = PortForward::parse("10.0.0.5:5353:53/udp").unwrap();
592 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(10, 0, 0, 5)));
593 assert_eq!(pf.host_port, 5353);
594 assert_eq!(pf.container_port, 53);
595 assert_eq!(pf.protocol, Protocol::Udp);
596 }
597
598 #[test]
599 fn test_port_forward_parse_invalid() {
600 assert!(PortForward::parse("8080").is_err());
601 assert!(PortForward::parse("abc:80").is_err());
602 assert!(PortForward::parse("8080:abc").is_err());
603 assert!(PortForward::parse("127.0.0.1:abc:80").is_err());
604 assert!(PortForward::parse("999.0.0.1:8080:80").is_err());
605 }
606
607 #[test]
608 fn test_validate_ipv4_addr_rejects_leading_zeros() {
609 assert!(validate_ipv4_addr("10.0.42.1").is_ok());
610 assert!(validate_ipv4_addr("0.0.0.0").is_ok());
611 assert!(
612 validate_ipv4_addr("010.0.0.1").is_err(),
613 "leading zero in first octet must be rejected"
614 );
615 assert!(
616 validate_ipv4_addr("10.01.0.1").is_err(),
617 "leading zero in second octet must be rejected"
618 );
619 assert!(
620 validate_ipv4_addr("10.0.01.1").is_err(),
621 "leading zero in third octet must be rejected"
622 );
623 assert!(
624 validate_ipv4_addr("10.0.0.01").is_err(),
625 "leading zero in fourth octet must be rejected"
626 );
627 }
628}