use std::net::Ipv4Addr;
#[derive(Debug, Clone)]
pub enum NetworkMode {
None,
Host,
Bridge(BridgeConfig),
}
#[derive(Debug, Clone)]
pub struct BridgeConfig {
pub bridge_name: String,
pub subnet: String,
pub container_ip: Option<String>,
pub dns: Vec<String>,
pub port_forwards: Vec<PortForward>,
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
bridge_name: "nucleus0".to_string(),
subnet: "10.0.42.0/24".to_string(),
container_ip: None,
dns: Vec::new(),
port_forwards: Vec::new(),
}
}
}
impl BridgeConfig {
pub fn with_public_dns(mut self) -> Self {
self.dns = vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()];
self
}
pub fn with_dns(mut self, servers: Vec<String>) -> Self {
self.dns = servers;
self
}
pub fn validate(&self) -> crate::error::Result<()> {
if self.bridge_name.is_empty() || self.bridge_name.len() > 15 {
return Err(crate::error::NucleusError::NetworkError(format!(
"Bridge name must be 1-15 characters, got '{}'",
self.bridge_name
)));
}
if !self
.bridge_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(crate::error::NucleusError::NetworkError(format!(
"Bridge name contains invalid characters (allowed: a-zA-Z0-9_-): '{}'",
self.bridge_name
)));
}
validate_ipv4_cidr(&self.subnet).map_err(crate::error::NucleusError::NetworkError)?;
if let Some(ref ip) = self.container_ip {
validate_ipv4_addr(ip).map_err(crate::error::NucleusError::NetworkError)?;
}
for dns in &self.dns {
validate_ipv4_addr(dns).map_err(crate::error::NucleusError::NetworkError)?;
}
Ok(())
}
}
fn validate_ipv4_addr(s: &str) -> Result<(), String> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 4 {
return Err(format!("Invalid IPv4 address: '{}'", s));
}
for part in &parts {
if part.is_empty() {
return Err(format!("Invalid IPv4 address: '{}'", s));
}
if part.len() > 1 && part.starts_with('0') {
return Err(format!(
"Invalid IPv4 address: '{}' — octet '{}' has leading zero",
s, part
));
}
match part.parse::<u8>() {
Ok(_) => {}
Err(_) => return Err(format!("Invalid IPv4 address: '{}'", s)),
}
}
Ok(())
}
fn validate_ipv4_cidr(s: &str) -> Result<(), String> {
let (addr, prefix) = s
.split_once('/')
.ok_or_else(|| format!("Invalid CIDR (missing /prefix): '{}'", s))?;
validate_ipv4_addr(addr)?;
let prefix: u8 = prefix
.parse()
.map_err(|_| format!("Invalid CIDR prefix: '{}'", s))?;
if prefix > 32 {
return Err(format!("CIDR prefix must be 0-32, got {}", prefix));
}
Ok(())
}
pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
validate_ipv4_cidr(s)
}
#[derive(Debug, Clone)]
pub struct EgressPolicy {
pub allowed_cidrs: Vec<String>,
pub allowed_tcp_ports: Vec<u16>,
pub allowed_udp_ports: Vec<u16>,
pub log_denied: bool,
pub allow_dns: bool,
}
impl Default for EgressPolicy {
fn default() -> Self {
Self {
allowed_cidrs: Vec::new(),
allowed_tcp_ports: Vec::new(),
allowed_udp_ports: Vec::new(),
log_denied: true,
allow_dns: true,
}
}
}
impl EgressPolicy {
pub fn deny_all() -> Self {
Self::default()
}
pub fn with_allowed_cidrs(mut self, cidrs: Vec<String>) -> Self {
self.allowed_cidrs = cidrs;
self
}
pub fn with_allowed_tcp_ports(mut self, ports: Vec<u16>) -> Self {
self.allowed_tcp_ports = ports;
self
}
pub fn with_allowed_udp_ports(mut self, ports: Vec<u16>) -> Self {
self.allowed_udp_ports = ports;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Tcp,
Udp,
}
impl Protocol {
pub fn as_str(self) -> &'static str {
match self {
Self::Tcp => "tcp",
Self::Udp => "udp",
}
}
}
impl std::fmt::Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct PortForward {
pub host_ip: Option<Ipv4Addr>,
pub host_port: u16,
pub container_port: u16,
pub protocol: Protocol,
}
impl PortForward {
pub fn parse(spec: &str) -> crate::error::Result<Self> {
let (ports, protocol) = if let Some((p, proto)) = spec.rsplit_once('/') {
let protocol = match proto {
"tcp" => Protocol::Tcp,
"udp" => Protocol::Udp,
_ => {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid protocol '{}', must be tcp or udp",
proto
)))
}
};
(p, protocol)
} else {
(spec, Protocol::Tcp)
};
let parts: Vec<&str> = ports.split(':').collect();
let (host_ip, host_port, container_port) = match parts.as_slice() {
[host_port, container_port] => (None, *host_port, *container_port),
[host_ip, host_port, container_port] => {
validate_ipv4_addr(host_ip).map_err(crate::error::NucleusError::ConfigError)?;
let host_ip = host_ip.parse::<Ipv4Addr>().map_err(|_| {
crate::error::NucleusError::ConfigError(format!(
"Invalid host IP address: {}",
host_ip
))
})?;
(Some(host_ip), *host_port, *container_port)
}
_ => {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid port forward format '{}', expected HOST:CONTAINER or HOST_IP:HOST:CONTAINER",
spec
)))
}
};
let host_port: u16 = host_port.parse().map_err(|_| {
crate::error::NucleusError::ConfigError(format!("Invalid host port: {}", host_port))
})?;
let container_port: u16 = container_port.parse().map_err(|_| {
crate::error::NucleusError::ConfigError(format!(
"Invalid container port: {}",
container_port
))
})?;
Ok(Self {
host_ip,
host_port,
container_port,
protocol,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_forward_parse() {
let pf = PortForward::parse("8080:80").unwrap();
assert_eq!(pf.host_ip, None);
assert_eq!(pf.host_port, 8080);
assert_eq!(pf.container_port, 80);
assert_eq!(pf.protocol, Protocol::Tcp);
let pf = PortForward::parse("5353:53/udp").unwrap();
assert_eq!(pf.host_ip, None);
assert_eq!(pf.host_port, 5353);
assert_eq!(pf.container_port, 53);
assert_eq!(pf.protocol, Protocol::Udp);
let pf = PortForward::parse("127.0.0.1:8080:80").unwrap();
assert_eq!(pf.host_ip, Some(Ipv4Addr::new(127, 0, 0, 1)));
assert_eq!(pf.host_port, 8080);
assert_eq!(pf.container_port, 80);
assert_eq!(pf.protocol, Protocol::Tcp);
let pf = PortForward::parse("10.0.0.5:5353:53/udp").unwrap();
assert_eq!(pf.host_ip, Some(Ipv4Addr::new(10, 0, 0, 5)));
assert_eq!(pf.host_port, 5353);
assert_eq!(pf.container_port, 53);
assert_eq!(pf.protocol, Protocol::Udp);
}
#[test]
fn test_port_forward_parse_invalid() {
assert!(PortForward::parse("8080").is_err());
assert!(PortForward::parse("abc:80").is_err());
assert!(PortForward::parse("8080:abc").is_err());
assert!(PortForward::parse("127.0.0.1:abc:80").is_err());
assert!(PortForward::parse("999.0.0.1:8080:80").is_err());
}
#[test]
fn test_validate_ipv4_addr_rejects_leading_zeros() {
assert!(validate_ipv4_addr("10.0.42.1").is_ok());
assert!(validate_ipv4_addr("0.0.0.0").is_ok());
assert!(
validate_ipv4_addr("010.0.0.1").is_err(),
"leading zero in first octet must be rejected"
);
assert!(
validate_ipv4_addr("10.01.0.1").is_err(),
"leading zero in second octet must be rejected"
);
assert!(
validate_ipv4_addr("10.0.01.1").is_err(),
"leading zero in third octet must be rejected"
);
assert!(
validate_ipv4_addr("10.0.0.01").is_err(),
"leading zero in fourth octet must be rejected"
);
}
}