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).map_err(crate::error::NucleusError::NetworkError)?;
78
79 if let Some(ref ip) = self.container_ip {
81 validate_ipv4_addr(ip).map_err(crate::error::NucleusError::NetworkError)?;
82 }
83
84 for dns in &self.dns {
86 validate_ipv4_addr(dns).map_err(crate::error::NucleusError::NetworkError)?;
87 }
88
89 Ok(())
90 }
91}
92
93fn validate_ipv4_addr(s: &str) -> Result<(), String> {
95 let parts: Vec<&str> = s.split('.').collect();
96 if parts.len() != 4 {
97 return Err(format!("Invalid IPv4 address: '{}'", s));
98 }
99 for part in &parts {
100 if part.is_empty() {
101 return Err(format!("Invalid IPv4 address: '{}'", s));
102 }
103 if part.len() > 1 && part.starts_with('0') {
104 return Err(format!(
105 "Invalid IPv4 address: '{}' — octet '{}' has leading zero",
106 s, part
107 ));
108 }
109 match part.parse::<u8>() {
110 Ok(_) => {}
111 Err(_) => return Err(format!("Invalid IPv4 address: '{}'", s)),
112 }
113 }
114 Ok(())
115}
116
117fn validate_ipv4_cidr(s: &str) -> Result<(), String> {
119 let (addr, prefix) = s
120 .split_once('/')
121 .ok_or_else(|| format!("Invalid CIDR (missing /prefix): '{}'", s))?;
122 validate_ipv4_addr(addr)?;
123 let prefix: u8 = prefix
124 .parse()
125 .map_err(|_| format!("Invalid CIDR prefix: '{}'", s))?;
126 if prefix > 32 {
127 return Err(format!("CIDR prefix must be 0-32, got {}", prefix));
128 }
129 Ok(())
130}
131
132pub fn validate_egress_cidr(s: &str) -> Result<(), String> {
134 validate_ipv4_cidr(s)
135}
136
137#[derive(Debug, Clone)]
143pub struct EgressPolicy {
144 pub allowed_cidrs: Vec<String>,
146 pub allowed_tcp_ports: Vec<u16>,
148 pub allowed_udp_ports: Vec<u16>,
150 pub log_denied: bool,
152 pub allow_dns: bool,
156}
157
158impl Default for EgressPolicy {
159 fn default() -> Self {
160 Self {
161 allowed_cidrs: Vec::new(),
162 allowed_tcp_ports: Vec::new(),
163 allowed_udp_ports: Vec::new(),
164 log_denied: true,
165 allow_dns: true,
166 }
167 }
168}
169
170impl EgressPolicy {
171 pub fn deny_all() -> Self {
175 Self::default()
176 }
177
178 pub fn with_allowed_cidrs(mut self, cidrs: Vec<String>) -> Self {
180 self.allowed_cidrs = cidrs;
181 self
182 }
183
184 pub fn with_allowed_tcp_ports(mut self, ports: Vec<u16>) -> Self {
185 self.allowed_tcp_ports = ports;
186 self
187 }
188
189 pub fn with_allowed_udp_ports(mut self, ports: Vec<u16>) -> Self {
190 self.allowed_udp_ports = ports;
191 self
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum Protocol {
198 Tcp,
199 Udp,
200}
201
202impl Protocol {
203 pub fn as_str(self) -> &'static str {
204 match self {
205 Self::Tcp => "tcp",
206 Self::Udp => "udp",
207 }
208 }
209}
210
211impl std::fmt::Display for Protocol {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.write_str(self.as_str())
214 }
215}
216
217#[derive(Debug, Clone)]
219pub struct PortForward {
220 pub host_ip: Option<Ipv4Addr>,
222 pub host_port: u16,
224 pub container_port: u16,
226 pub protocol: Protocol,
228}
229
230impl PortForward {
231 pub fn parse(spec: &str) -> crate::error::Result<Self> {
237 let (ports, protocol) = if let Some((p, proto)) = spec.rsplit_once('/') {
238 let protocol = match proto {
239 "tcp" => Protocol::Tcp,
240 "udp" => Protocol::Udp,
241 _ => {
242 return Err(crate::error::NucleusError::ConfigError(format!(
243 "Invalid protocol '{}', must be tcp or udp",
244 proto
245 )))
246 }
247 };
248 (p, protocol)
249 } else {
250 (spec, Protocol::Tcp)
251 };
252
253 let parts: Vec<&str> = ports.split(':').collect();
254 let (host_ip, host_port, container_port) = match parts.as_slice() {
255 [host_port, container_port] => (None, *host_port, *container_port),
256 [host_ip, host_port, container_port] => {
257 validate_ipv4_addr(host_ip).map_err(crate::error::NucleusError::ConfigError)?;
258 let host_ip = host_ip.parse::<Ipv4Addr>().map_err(|_| {
259 crate::error::NucleusError::ConfigError(format!(
260 "Invalid host IP address: {}",
261 host_ip
262 ))
263 })?;
264 (Some(host_ip), *host_port, *container_port)
265 }
266 _ => {
267 return Err(crate::error::NucleusError::ConfigError(format!(
268 "Invalid port forward format '{}', expected HOST:CONTAINER or HOST_IP:HOST:CONTAINER",
269 spec
270 )))
271 }
272 };
273
274 let host_port: u16 = host_port.parse().map_err(|_| {
275 crate::error::NucleusError::ConfigError(format!("Invalid host port: {}", host_port))
276 })?;
277 let container_port: u16 = container_port.parse().map_err(|_| {
278 crate::error::NucleusError::ConfigError(format!(
279 "Invalid container port: {}",
280 container_port
281 ))
282 })?;
283
284 Ok(Self {
285 host_ip,
286 host_port,
287 container_port,
288 protocol,
289 })
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_port_forward_parse() {
299 let pf = PortForward::parse("8080:80").unwrap();
300 assert_eq!(pf.host_ip, None);
301 assert_eq!(pf.host_port, 8080);
302 assert_eq!(pf.container_port, 80);
303 assert_eq!(pf.protocol, Protocol::Tcp);
304
305 let pf = PortForward::parse("5353:53/udp").unwrap();
306 assert_eq!(pf.host_ip, None);
307 assert_eq!(pf.host_port, 5353);
308 assert_eq!(pf.container_port, 53);
309 assert_eq!(pf.protocol, Protocol::Udp);
310
311 let pf = PortForward::parse("127.0.0.1:8080:80").unwrap();
312 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(127, 0, 0, 1)));
313 assert_eq!(pf.host_port, 8080);
314 assert_eq!(pf.container_port, 80);
315 assert_eq!(pf.protocol, Protocol::Tcp);
316
317 let pf = PortForward::parse("10.0.0.5:5353:53/udp").unwrap();
318 assert_eq!(pf.host_ip, Some(Ipv4Addr::new(10, 0, 0, 5)));
319 assert_eq!(pf.host_port, 5353);
320 assert_eq!(pf.container_port, 53);
321 assert_eq!(pf.protocol, Protocol::Udp);
322 }
323
324 #[test]
325 fn test_port_forward_parse_invalid() {
326 assert!(PortForward::parse("8080").is_err());
327 assert!(PortForward::parse("abc:80").is_err());
328 assert!(PortForward::parse("8080:abc").is_err());
329 assert!(PortForward::parse("127.0.0.1:abc:80").is_err());
330 assert!(PortForward::parse("999.0.0.1:8080:80").is_err());
331 }
332
333 #[test]
334 fn test_validate_ipv4_addr_rejects_leading_zeros() {
335 assert!(validate_ipv4_addr("10.0.42.1").is_ok());
336 assert!(validate_ipv4_addr("0.0.0.0").is_ok());
337 assert!(
338 validate_ipv4_addr("010.0.0.1").is_err(),
339 "leading zero in first octet must be rejected"
340 );
341 assert!(
342 validate_ipv4_addr("10.01.0.1").is_err(),
343 "leading zero in second octet must be rejected"
344 );
345 assert!(
346 validate_ipv4_addr("10.0.01.1").is_err(),
347 "leading zero in third octet must be rejected"
348 );
349 assert!(
350 validate_ipv4_addr("10.0.0.01").is_err(),
351 "leading zero in fourth octet must be rejected"
352 );
353 }
354}