1use std::net::Ipv4Addr;
2
3#[derive(Debug, Clone)]
5pub enum NetworkMode {
6 None,
8 Host,
10 Bridge(BridgeConfig),
12}
13
14#[derive(Debug, Clone)]
16pub struct BridgeConfig {
17 pub bridge_name: String,
19 pub subnet: String,
21 pub container_ip: Option<String>,
23 pub dns: Vec<String>,
25 pub port_forwards: Vec<PortForward>,
27}
28
29impl Default for BridgeConfig {
30 fn default() -> Self {
31 Self {
32 bridge_name: "nucleus0".to_string(),
33 subnet: "10.0.42.0/24".to_string(),
34 container_ip: None,
35 dns: Vec::new(),
38 port_forwards: Vec::new(),
39 }
40 }
41}
42
43impl BridgeConfig {
44 pub fn with_public_dns(mut self) -> Self {
47 self.dns = vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()];
48 self
49 }
50
51 pub fn with_dns(mut self, servers: Vec<String>) -> Self {
52 self.dns = servers;
53 self
54 }
55
56 pub fn validate(&self) -> crate::error::Result<()> {
58 if self.bridge_name.is_empty() || self.bridge_name.len() > 15 {
60 return Err(crate::error::NucleusError::NetworkError(format!(
61 "Bridge name must be 1-15 characters, got '{}'",
62 self.bridge_name
63 )));
64 }
65 if !self
66 .bridge_name
67 .chars()
68 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
69 {
70 return Err(crate::error::NucleusError::NetworkError(format!(
71 "Bridge name contains invalid characters (allowed: a-zA-Z0-9_-): '{}'",
72 self.bridge_name
73 )));
74 }
75
76 validate_ipv4_cidr(&self.subnet)
78 .map_err(|e| crate::error::NucleusError::NetworkError(e))?;
79
80 if let Some(ref ip) = self.container_ip {
82 validate_ipv4_addr(ip).map_err(|e| crate::error::NucleusError::NetworkError(e))?;
83 }
84
85 for dns in &self.dns {
87 validate_ipv4_addr(dns).map_err(|e| crate::error::NucleusError::NetworkError(e))?;
88 }
89
90 Ok(())
91 }
92}
93
94fn validate_ipv4_addr(s: &str) -> Result<(), String> {
96 let parts: Vec<&str> = s.split('.').collect();
97 if parts.len() != 4 {
98 return Err(format!("Invalid IPv4 address: '{}'", s));
99 }
100 for part in &parts {
101 if part.is_empty() {
102 return Err(format!("Invalid IPv4 address: '{}'", s));
103 }
104 if part.len() > 1 && part.starts_with('0') {
105 return Err(format!(
106 "Invalid IPv4 address: '{}' — octet '{}' has leading zero",
107 s, part
108 ));
109 }
110 match part.parse::<u8>() {
111 Ok(_) => {}
112 Err(_) => return Err(format!("Invalid IPv4 address: '{}'", s)),
113 }
114 }
115 Ok(())
116}
117
118fn validate_ipv4_cidr(s: &str) -> Result<(), String> {
120 let (addr, prefix) = s
121 .split_once('/')
122 .ok_or_else(|| format!("Invalid CIDR (missing /prefix): '{}'", s))?;
123 validate_ipv4_addr(addr)?;
124 let prefix: u8 = prefix
125 .parse()
126 .map_err(|_| format!("Invalid CIDR prefix: '{}'", s))?;
127 if prefix > 32 {
128 return Err(format!("CIDR prefix must be 0-32, got {}", prefix));
129 }
130 Ok(())
131}
132
133pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
135 validate_ipv4_cidr(s)
136}
137
138#[derive(Debug, Clone)]
144pub struct EgressPolicy {
145 pub allowed_cidrs: Vec<String>,
147 pub allowed_tcp_ports: Vec<u16>,
149 pub allowed_udp_ports: Vec<u16>,
151 pub log_denied: bool,
153 pub allow_dns: bool,
157}
158
159impl Default for EgressPolicy {
160 fn default() -> Self {
161 Self {
162 allowed_cidrs: Vec::new(),
163 allowed_tcp_ports: Vec::new(),
164 allowed_udp_ports: Vec::new(),
165 log_denied: true,
166 allow_dns: true,
167 }
168 }
169}
170
171impl EgressPolicy {
172 pub fn deny_all() -> Self {
176 Self::default()
177 }
178
179 pub fn with_allowed_cidrs(mut self, cidrs: Vec<String>) -> Self {
181 self.allowed_cidrs = cidrs;
182 self
183 }
184
185 pub fn with_allowed_tcp_ports(mut self, ports: Vec<u16>) -> Self {
186 self.allowed_tcp_ports = ports;
187 self
188 }
189
190 pub fn with_allowed_udp_ports(mut self, ports: Vec<u16>) -> Self {
191 self.allowed_udp_ports = ports;
192 self
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum Protocol {
199 Tcp,
200 Udp,
201}
202
203impl Protocol {
204 pub fn as_str(self) -> &'static str {
205 match self {
206 Self::Tcp => "tcp",
207 Self::Udp => "udp",
208 }
209 }
210}
211
212impl std::fmt::Display for Protocol {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 f.write_str(self.as_str())
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct PortForward {
221 pub host_ip: Option<Ipv4Addr>,
223 pub host_port: u16,
225 pub container_port: u16,
227 pub protocol: Protocol,
229}
230
231impl PortForward {
232 pub fn parse(spec: &str) -> crate::error::Result<Self> {
238 let (ports, protocol) = if let Some((p, proto)) = spec.rsplit_once('/') {
239 let protocol = match proto {
240 "tcp" => Protocol::Tcp,
241 "udp" => Protocol::Udp,
242 _ => {
243 return Err(crate::error::NucleusError::ConfigError(format!(
244 "Invalid protocol '{}', must be tcp or udp",
245 proto
246 )))
247 }
248 };
249 (p, protocol)
250 } else {
251 (spec, Protocol::Tcp)
252 };
253
254 let parts: Vec<&str> = ports.split(':').collect();
255 let (host_ip, host_port, container_port) = match parts.as_slice() {
256 [host_port, container_port] => (None, *host_port, *container_port),
257 [host_ip, host_port, container_port] => {
258 validate_ipv4_addr(host_ip).map_err(crate::error::NucleusError::ConfigError)?;
259 let host_ip = host_ip.parse::<Ipv4Addr>().map_err(|_| {
260 crate::error::NucleusError::ConfigError(format!(
261 "Invalid host IP address: {}",
262 host_ip
263 ))
264 })?;
265 (Some(host_ip), *host_port, *container_port)
266 }
267 _ => {
268 return Err(crate::error::NucleusError::ConfigError(format!(
269 "Invalid port forward format '{}', expected HOST:CONTAINER or HOST_IP:HOST:CONTAINER",
270 spec
271 )))
272 }
273 };
274
275 let host_port: u16 = host_port.parse().map_err(|_| {
276 crate::error::NucleusError::ConfigError(format!("Invalid host port: {}", host_port))
277 })?;
278 let container_port: u16 = container_port.parse().map_err(|_| {
279 crate::error::NucleusError::ConfigError(format!(
280 "Invalid container port: {}",
281 container_port
282 ))
283 })?;
284
285 Ok(Self {
286 host_ip,
287 host_port,
288 container_port,
289 protocol,
290 })
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_port_forward_parse() {
300 let pf = PortForward::parse("8080:80").unwrap();
301 assert_eq!(pf.host_ip, None);
302 assert_eq!(pf.host_port, 8080);
303 assert_eq!(pf.container_port, 80);
304 assert_eq!(pf.protocol, Protocol::Tcp);
305
306 let pf = PortForward::parse("5353:53/udp").unwrap();
307 assert_eq!(pf.host_ip, None);
308 assert_eq!(pf.host_port, 5353);
309 assert_eq!(pf.container_port, 53);
310 assert_eq!(pf.protocol, Protocol::Udp);
311
312 let pf = PortForward::parse("127.0.0.1:8080:80").unwrap();
313 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(127, 0, 0, 1)));
314 assert_eq!(pf.host_port, 8080);
315 assert_eq!(pf.container_port, 80);
316 assert_eq!(pf.protocol, Protocol::Tcp);
317
318 let pf = PortForward::parse("10.0.0.5:5353:53/udp").unwrap();
319 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(10, 0, 0, 5)));
320 assert_eq!(pf.host_port, 5353);
321 assert_eq!(pf.container_port, 53);
322 assert_eq!(pf.protocol, Protocol::Udp);
323 }
324
325 #[test]
326 fn test_port_forward_parse_invalid() {
327 assert!(PortForward::parse("8080").is_err());
328 assert!(PortForward::parse("abc:80").is_err());
329 assert!(PortForward::parse("8080:abc").is_err());
330 assert!(PortForward::parse("127.0.0.1:abc:80").is_err());
331 assert!(PortForward::parse("999.0.0.1:8080:80").is_err());
332 }
333
334 #[test]
335 fn test_validate_ipv4_addr_rejects_leading_zeros() {
336 assert!(validate_ipv4_addr("10.0.42.1").is_ok());
337 assert!(validate_ipv4_addr("0.0.0.0").is_ok());
338 assert!(
339 validate_ipv4_addr("010.0.0.1").is_err(),
340 "leading zero in first octet must be rejected"
341 );
342 assert!(
343 validate_ipv4_addr("10.01.0.1").is_err(),
344 "leading zero in second octet must be rejected"
345 );
346 assert!(
347 validate_ipv4_addr("10.0.01.1").is_err(),
348 "leading zero in third octet must be rejected"
349 );
350 assert!(
351 validate_ipv4_addr("10.0.0.01").is_err(),
352 "leading zero in fourth octet must be rejected"
353 );
354 }
355}