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