⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/services/cni/
plugin.rs

1//! CNI Plugin Framework
2//!
3//! Provides a plugin-based container networking interface with bridge
4//! configuration, veth pair creation, and NAT setup.
5
6#![allow(dead_code)]
7
8use alloc::{collections::BTreeMap, string::String, vec::Vec};
9
10// ---------------------------------------------------------------------------
11// CNI Config
12// ---------------------------------------------------------------------------
13
14/// CNI plugin configuration.
15#[derive(Debug, Clone)]
16pub struct CniConfig {
17    /// CNI specification version.
18    pub cni_version: String,
19    /// Network name.
20    pub name: String,
21    /// Plugin type name.
22    pub type_name: String,
23    /// Bridge device name (for bridge plugin).
24    pub bridge: String,
25    /// Subnet in CIDR notation (e.g., "10.244.0.0/24").
26    pub subnet: String,
27    /// Gateway address (e.g., "10.244.0.1").
28    pub gateway: String,
29    /// Additional plugin-specific options.
30    pub options: BTreeMap<String, String>,
31}
32
33impl Default for CniConfig {
34    fn default() -> Self {
35        CniConfig {
36            cni_version: String::from("1.0.0"),
37            name: String::from("veridian-net"),
38            type_name: String::from("bridge"),
39            bridge: String::from("cni0"),
40            subnet: String::from("10.244.0.0/24"),
41            gateway: String::from("10.244.0.1"),
42            options: BTreeMap::new(),
43        }
44    }
45}
46
47impl CniConfig {
48    /// Parse a CNI config from a simple key=value format.
49    ///
50    /// Each line is `key=value`. Recognized keys:
51    /// cniVersion, name, type, bridge, subnet, gateway.
52    pub fn from_key_value(input: &str) -> Self {
53        let mut config = CniConfig::default();
54        for line in input.lines() {
55            let line = line.trim();
56            if line.is_empty() || line.starts_with('#') {
57                continue;
58            }
59            if let Some((key, value)) = line.split_once('=') {
60                let key = key.trim();
61                let value = value.trim();
62                match key {
63                    "cniVersion" => config.cni_version = String::from(value),
64                    "name" => config.name = String::from(value),
65                    "type" => config.type_name = String::from(value),
66                    "bridge" => config.bridge = String::from(value),
67                    "subnet" => config.subnet = String::from(value),
68                    "gateway" => config.gateway = String::from(value),
69                    _ => {
70                        config
71                            .options
72                            .insert(String::from(key), String::from(value));
73                    }
74                }
75            }
76        }
77        config
78    }
79}
80
81// ---------------------------------------------------------------------------
82// CNI Result
83// ---------------------------------------------------------------------------
84
85/// A network interface in the CNI result.
86#[derive(Debug, Clone)]
87pub struct CniInterface {
88    /// Interface name (e.g., "eth0").
89    pub name: String,
90    /// MAC address (e.g., "02:42:ac:11:00:02").
91    pub mac: String,
92    /// Whether this is inside the container sandbox.
93    pub sandbox: bool,
94}
95
96/// An IP address assignment in the CNI result.
97#[derive(Debug, Clone)]
98pub struct CniIpConfig {
99    /// IP address with prefix (e.g., "10.244.0.5/24").
100    pub address: String,
101    /// Gateway address.
102    pub gateway: String,
103    /// Interface index this IP is assigned to.
104    pub interface_idx: usize,
105}
106
107/// A route in the CNI result.
108#[derive(Debug, Clone)]
109pub struct CniRoute {
110    /// Destination CIDR (e.g., "0.0.0.0/0" for default).
111    pub dst: String,
112    /// Gateway address.
113    pub gw: String,
114}
115
116/// DNS configuration in the CNI result.
117#[derive(Debug, Clone, Default)]
118pub struct CniDns {
119    /// DNS nameservers.
120    pub nameservers: Vec<String>,
121    /// Search domains.
122    pub search: Vec<String>,
123}
124
125/// Result returned from a CNI plugin operation.
126#[derive(Debug, Clone)]
127pub struct CniResult {
128    /// CNI version.
129    pub cni_version: String,
130    /// Created interfaces.
131    pub interfaces: Vec<CniInterface>,
132    /// Assigned IP addresses.
133    pub ips: Vec<CniIpConfig>,
134    /// Routes added.
135    pub routes: Vec<CniRoute>,
136    /// DNS configuration.
137    pub dns: CniDns,
138}
139
140impl Default for CniResult {
141    fn default() -> Self {
142        CniResult {
143            cni_version: String::from("1.0.0"),
144            interfaces: Vec::new(),
145            ips: Vec::new(),
146            routes: Vec::new(),
147            dns: CniDns::default(),
148        }
149    }
150}
151
152// ---------------------------------------------------------------------------
153// CNI Plugin Trait
154// ---------------------------------------------------------------------------
155
156/// CNI plugin error.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum CniError {
159    /// Configuration error.
160    InvalidConfig(String),
161    /// Network setup failed.
162    SetupFailed(String),
163    /// Network teardown failed.
164    TeardownFailed(String),
165    /// Plugin not found.
166    PluginNotFound(String),
167    /// Address exhaustion.
168    NoAddressAvailable,
169}
170
171/// Trait for CNI plugin implementations.
172pub trait CniPlugin {
173    /// Add a container to the network.
174    fn add(&mut self, container_id: &str, config: &CniConfig) -> Result<CniResult, CniError>;
175
176    /// Remove a container from the network.
177    fn del(&mut self, container_id: &str, config: &CniConfig) -> Result<(), CniError>;
178
179    /// Check that a container's networking is correct.
180    fn check(&self, container_id: &str, config: &CniConfig) -> Result<(), CniError>;
181
182    /// Return supported CNI versions.
183    fn version(&self) -> Vec<String>;
184}
185
186// ---------------------------------------------------------------------------
187// Bridge Plugin
188// ---------------------------------------------------------------------------
189
190/// Veth pair representing a container-to-bridge link.
191#[derive(Debug, Clone)]
192pub struct VethPair {
193    /// Host-side veth name.
194    pub host_name: String,
195    /// Container-side veth name.
196    pub container_name: String,
197    /// Container ID.
198    pub container_id: String,
199    /// Assigned IP address.
200    pub ip_address: String,
201    /// MAC address.
202    pub mac_address: String,
203}
204
205/// Bridge plugin: creates veth pairs and attaches them to a bridge device.
206#[derive(Debug)]
207pub struct BridgePlugin {
208    /// Bridge device name.
209    bridge_name: String,
210    /// Active veth pairs keyed by container ID.
211    veth_pairs: BTreeMap<String, VethPair>,
212    /// Next IP host part to assign.
213    next_host_part: u32,
214    /// Next veth index for naming.
215    next_veth_idx: u32,
216}
217
218impl Default for BridgePlugin {
219    fn default() -> Self {
220        Self::new(String::from("cni0"))
221    }
222}
223
224impl BridgePlugin {
225    /// Create a new bridge plugin.
226    pub fn new(bridge_name: String) -> Self {
227        BridgePlugin {
228            bridge_name,
229            veth_pairs: BTreeMap::new(),
230            next_host_part: 2, // .1 is gateway
231            next_veth_idx: 0,
232        }
233    }
234
235    /// Create a veth pair for a container.
236    fn setup_veth(&mut self, container_id: &str) -> VethPair {
237        let idx = self.next_veth_idx;
238        self.next_veth_idx += 1;
239
240        let host_name = alloc::format!("veth{:04x}", idx);
241        let container_name = String::from("eth0");
242
243        // Generate deterministic MAC
244        let mac_address =
245            alloc::format!("02:42:ac:11:{:02x}:{:02x}", (idx >> 8) & 0xFF, idx & 0xFF);
246
247        VethPair {
248            host_name,
249            container_name,
250            container_id: String::from(container_id),
251            ip_address: String::new(), // filled in by add()
252            mac_address,
253        }
254    }
255
256    /// Attach a veth to the bridge (conceptual).
257    fn attach_to_bridge(&self, _veth: &VethPair) -> Result<(), CniError> {
258        // In a real implementation this would call netlink
259        Ok(())
260    }
261
262    /// Configure NAT for outbound traffic (conceptual).
263    fn configure_nat(&self, _subnet: &str) -> Result<(), CniError> {
264        // In a real implementation this would set up iptables/nftables rules
265        Ok(())
266    }
267
268    /// Allocate the next IP address from the subnet.
269    fn allocate_ip(&mut self, config: &CniConfig) -> Result<String, CniError> {
270        // Parse subnet base (simplified: assume /24 with "x.y.z.0/24" format)
271        let subnet = &config.subnet;
272        let slash_pos = subnet.find('/').unwrap_or(subnet.len());
273        let base = &subnet[..slash_pos];
274
275        // Find last dot to replace host part
276        if let Some(last_dot) = base.rfind('.') {
277            let prefix = &base[..last_dot];
278            let host_part = self.next_host_part;
279            if host_part > 254 {
280                return Err(CniError::NoAddressAvailable);
281            }
282            self.next_host_part += 1;
283            Ok(alloc::format!("{}.{}/24", prefix, host_part))
284        } else {
285            Err(CniError::InvalidConfig(String::from(
286                "invalid subnet format",
287            )))
288        }
289    }
290
291    /// Get the number of active veth pairs.
292    pub fn active_count(&self) -> usize {
293        self.veth_pairs.len()
294    }
295}
296
297impl CniPlugin for BridgePlugin {
298    fn add(&mut self, container_id: &str, config: &CniConfig) -> Result<CniResult, CniError> {
299        // Check for duplicate
300        if self.veth_pairs.contains_key(container_id) {
301            return Err(CniError::SetupFailed(String::from(
302                "container already attached",
303            )));
304        }
305
306        let mut veth = self.setup_veth(container_id);
307        let ip_address = self.allocate_ip(config)?;
308        veth.ip_address = ip_address.clone();
309
310        self.attach_to_bridge(&veth)?;
311        self.configure_nat(&config.subnet)?;
312
313        let result = CniResult {
314            cni_version: config.cni_version.clone(),
315            interfaces: alloc::vec![
316                CniInterface {
317                    name: veth.host_name.clone(),
318                    mac: String::new(),
319                    sandbox: false,
320                },
321                CniInterface {
322                    name: veth.container_name.clone(),
323                    mac: veth.mac_address.clone(),
324                    sandbox: true,
325                },
326            ],
327            ips: alloc::vec![CniIpConfig {
328                address: ip_address,
329                gateway: config.gateway.clone(),
330                interface_idx: 1,
331            }],
332            routes: alloc::vec![CniRoute {
333                dst: String::from("0.0.0.0/0"),
334                gw: config.gateway.clone(),
335            }],
336            dns: CniDns::default(),
337        };
338
339        self.veth_pairs.insert(String::from(container_id), veth);
340        Ok(result)
341    }
342
343    fn del(&mut self, container_id: &str, _config: &CniConfig) -> Result<(), CniError> {
344        if self.veth_pairs.remove(container_id).is_none() {
345            return Err(CniError::TeardownFailed(String::from(
346                "container not found",
347            )));
348        }
349        Ok(())
350    }
351
352    fn check(&self, container_id: &str, _config: &CniConfig) -> Result<(), CniError> {
353        if self.veth_pairs.contains_key(container_id) {
354            Ok(())
355        } else {
356            Err(CniError::SetupFailed(String::from(
357                "container not attached",
358            )))
359        }
360    }
361
362    fn version(&self) -> Vec<String> {
363        alloc::vec![
364            String::from("0.3.0"),
365            String::from("0.3.1"),
366            String::from("0.4.0"),
367            String::from("1.0.0"),
368        ]
369    }
370}
371
372// ---------------------------------------------------------------------------
373// Tests
374// ---------------------------------------------------------------------------
375
376#[cfg(test)]
377mod tests {
378    #[allow(unused_imports)]
379    use alloc::string::ToString;
380
381    use super::*;
382
383    fn default_config() -> CniConfig {
384        CniConfig::default()
385    }
386
387    #[test]
388    fn test_cni_config_default() {
389        let config = CniConfig::default();
390        assert_eq!(config.type_name, "bridge");
391        assert_eq!(config.subnet, "10.244.0.0/24");
392    }
393
394    #[test]
395    fn test_cni_config_parse() {
396        let input = "name=my-net\ntype=bridge\nsubnet=10.0.0.0/16\ngateway=10.0.0.1\n";
397        let config = CniConfig::from_key_value(input);
398        assert_eq!(config.name, "my-net");
399        assert_eq!(config.subnet, "10.0.0.0/16");
400        assert_eq!(config.gateway, "10.0.0.1");
401    }
402
403    #[test]
404    fn test_bridge_add() {
405        let mut plugin = BridgePlugin::default();
406        let config = default_config();
407        let result = plugin.add("container1", &config).unwrap();
408        assert_eq!(result.interfaces.len(), 2);
409        assert_eq!(result.ips.len(), 1);
410        assert!(result.ips[0].address.contains("10.244.0."));
411        assert_eq!(result.routes.len(), 1);
412    }
413
414    #[test]
415    fn test_bridge_add_duplicate() {
416        let mut plugin = BridgePlugin::default();
417        let config = default_config();
418        plugin.add("c1", &config).unwrap();
419        let result = plugin.add("c1", &config);
420        assert!(result.is_err());
421    }
422
423    #[test]
424    fn test_bridge_del() {
425        let mut plugin = BridgePlugin::default();
426        let config = default_config();
427        plugin.add("c1", &config).unwrap();
428        plugin.del("c1", &config).unwrap();
429        assert_eq!(plugin.active_count(), 0);
430    }
431
432    #[test]
433    fn test_bridge_del_not_found() {
434        let mut plugin = BridgePlugin::default();
435        let config = default_config();
436        assert!(plugin.del("nonexistent", &config).is_err());
437    }
438
439    #[test]
440    fn test_bridge_check() {
441        let mut plugin = BridgePlugin::default();
442        let config = default_config();
443        plugin.add("c1", &config).unwrap();
444        assert!(plugin.check("c1", &config).is_ok());
445        assert!(plugin.check("c2", &config).is_err());
446    }
447
448    #[test]
449    fn test_bridge_version() {
450        let plugin = BridgePlugin::default();
451        let versions = plugin.version();
452        assert!(versions.contains(&String::from("1.0.0")));
453    }
454
455    #[test]
456    fn test_multiple_containers_get_different_ips() {
457        let mut plugin = BridgePlugin::default();
458        let config = default_config();
459        let r1 = plugin.add("c1", &config).unwrap();
460        let r2 = plugin.add("c2", &config).unwrap();
461        assert_ne!(r1.ips[0].address, r2.ips[0].address);
462    }
463
464    #[test]
465    fn test_config_parse_with_comments() {
466        let input = "# comment\nname=test\n\ntype=bridge\n";
467        let config = CniConfig::from_key_value(input);
468        assert_eq!(config.name, "test");
469        assert_eq!(config.type_name, "bridge");
470    }
471}