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

veridian_kernel/virt/containers/
mod.rs

1//! Enhanced container runtime with OCI specification support, cgroup
2//! controllers, overlay filesystem, veth networking, and seccomp BPF filtering.
3//!
4//! This module implements 7 container enhancement sprints:
5//! 1. OCI Runtime Specification (config.json parsing, lifecycle, hooks,
6//!    pivot_root)
7//! 2. Container Image Format (layers, overlay composition, manifest, SHA-256
8//!    IDs)
9//! 3. Cgroup Memory Controller (limits, usage tracking, OOM, hierarchical
10//!    accounting)
11//! 4. Cgroup CPU Controller (shares, quota/period, throttling, burst,
12//!    hierarchy)
13//! 5. Overlay Filesystem (lower/upper layers, copy-up, whiteout, directory
14//!    merge)
15//! 6. Veth Networking (virtual pairs, bridge, NAT masquerade, ARP proxy, MTU)
16//! 7. Seccomp BPF (filter instructions, syscall filtering, arg inspection,
17//!    inheritance)
18
19mod cgroups;
20mod image;
21mod networking;
22mod oci;
23mod overlay;
24mod seccomp;
25
26// Re-export everything so external consumers see no change.
27pub use self::{cgroups::*, image::*, networking::*, oci::*, overlay::*, seccomp::*};
28
29// ---------------------------------------------------------------------------
30// Shared helpers used across submodules
31// ---------------------------------------------------------------------------
32
33/// Parse a u32 from a decimal string.
34pub(crate) fn parse_u32(s: &str) -> Option<u32> {
35    let mut result: u32 = 0;
36    for b in s.bytes() {
37        if b.is_ascii_digit() {
38            result = result.checked_mul(10)?;
39            result = result.checked_add((b - b'0') as u32)?;
40        } else {
41            return None;
42        }
43    }
44    Some(result)
45}
46
47/// Parse a u64 from a decimal string.
48pub(crate) fn parse_u64(s: &str) -> Option<u64> {
49    let mut result: u64 = 0;
50    for b in s.bytes() {
51        if b.is_ascii_digit() {
52            result = result.checked_mul(10)?;
53            result = result.checked_add((b - b'0') as u64)?;
54        } else {
55            return None;
56        }
57    }
58    Some(result)
59}
60
61/// Minimal SHA-256 implementation (same algorithm as crypto::hash::sha256
62/// but self-contained to avoid circular dependencies).
63pub(crate) fn simple_sha256(data: &[u8]) -> [u8; 32] {
64    const K: [u32; 64] = [
65        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
66        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
67        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
68        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
69        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
70        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
71        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
72        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
73        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
74        0xc67178f2,
75    ];
76
77    let mut h: [u32; 8] = [
78        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
79        0x5be0cd19,
80    ];
81
82    let original_len_bits = (data.len() as u64).saturating_mul(8);
83
84    // Pad message: append 0x80, zeros, then 64-bit big-endian length
85    let padded_len = (data.len() + 9).div_ceil(64) * 64;
86    // Use a stack buffer for small inputs, otherwise heap
87    let mut padded = [0u8; 128]; // enough for up to 119 bytes of input
88    let use_stack = padded_len <= 128;
89
90    #[cfg(feature = "alloc")]
91    let mut heap_padded: alloc::vec::Vec<u8>;
92    #[cfg(not(feature = "alloc"))]
93    let heap_padded: [u8; 0] = [];
94
95    let buf: &mut [u8] = if use_stack {
96        padded[..data.len()].copy_from_slice(data);
97        padded[data.len()] = 0x80;
98        let len_offset = padded_len - 8;
99        padded[len_offset..len_offset + 8].copy_from_slice(&original_len_bits.to_be_bytes());
100        &mut padded[..padded_len]
101    } else {
102        #[cfg(feature = "alloc")]
103        {
104            heap_padded = alloc::vec![0u8; padded_len];
105            heap_padded[..data.len()].copy_from_slice(data);
106            heap_padded[data.len()] = 0x80;
107            let len_offset = padded_len - 8;
108            heap_padded[len_offset..len_offset + 8]
109                .copy_from_slice(&original_len_bits.to_be_bytes());
110            &mut heap_padded[..]
111        }
112        #[cfg(not(feature = "alloc"))]
113        {
114            // Without alloc, we cannot handle inputs > 119 bytes.
115            // Return zeros as a safe fallback.
116            return [0u8; 32];
117        }
118    };
119
120    // Process 64-byte blocks
121    let mut w = [0u32; 64];
122    let mut block_offset = 0;
123    while block_offset < buf.len() {
124        let block = &buf[block_offset..block_offset + 64];
125        for i in 0..16 {
126            w[i] = u32::from_be_bytes([
127                block[i * 4],
128                block[i * 4 + 1],
129                block[i * 4 + 2],
130                block[i * 4 + 3],
131            ]);
132        }
133        for i in 16..64 {
134            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
135            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
136            w[i] = w[i - 16]
137                .wrapping_add(s0)
138                .wrapping_add(w[i - 7])
139                .wrapping_add(s1);
140        }
141
142        let mut a = h[0];
143        let mut b = h[1];
144        let mut c = h[2];
145        let mut d = h[3];
146        let mut e = h[4];
147        let mut f = h[5];
148        let mut g = h[6];
149        let mut hh = h[7];
150
151        for i in 0..64 {
152            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
153            let ch = (e & f) ^ ((!e) & g);
154            let temp1 = hh
155                .wrapping_add(s1)
156                .wrapping_add(ch)
157                .wrapping_add(K[i])
158                .wrapping_add(w[i]);
159            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
160            let maj = (a & b) ^ (a & c) ^ (b & c);
161            let temp2 = s0.wrapping_add(maj);
162
163            hh = g;
164            g = f;
165            f = e;
166            e = d.wrapping_add(temp1);
167            d = c;
168            c = b;
169            b = a;
170            a = temp1.wrapping_add(temp2);
171        }
172
173        h[0] = h[0].wrapping_add(a);
174        h[1] = h[1].wrapping_add(b);
175        h[2] = h[2].wrapping_add(c);
176        h[3] = h[3].wrapping_add(d);
177        h[4] = h[4].wrapping_add(e);
178        h[5] = h[5].wrapping_add(f);
179        h[6] = h[6].wrapping_add(g);
180        h[7] = h[7].wrapping_add(hh);
181
182        block_offset += 64;
183    }
184
185    let mut out = [0u8; 32];
186    for (i, &val) in h.iter().enumerate() {
187        out[i * 4..i * 4 + 4].copy_from_slice(&val.to_be_bytes());
188    }
189    out
190}
191
192// ---------------------------------------------------------------------------
193// Tests
194// ---------------------------------------------------------------------------
195
196#[cfg(test)]
197mod tests {
198    #[allow(unused_imports)]
199    use alloc::vec;
200
201    use super::*;
202
203    // --- OCI Runtime Spec tests ---
204
205    #[test]
206    fn test_oci_lifecycle_state_display() {
207        assert_eq!(
208            alloc::format!("{}", OciLifecycleState::Creating),
209            "creating"
210        );
211        assert_eq!(alloc::format!("{}", OciLifecycleState::Created), "created");
212        assert_eq!(alloc::format!("{}", OciLifecycleState::Running), "running");
213        assert_eq!(alloc::format!("{}", OciLifecycleState::Stopped), "stopped");
214    }
215
216    #[test]
217    fn test_oci_namespace_kind_parse() {
218        assert_eq!(
219            OciNamespaceKind::from_str_kind("pid"),
220            Some(OciNamespaceKind::Pid)
221        );
222        assert_eq!(
223            OciNamespaceKind::from_str_kind("network"),
224            Some(OciNamespaceKind::Network)
225        );
226        assert_eq!(OciNamespaceKind::from_str_kind("invalid"), None);
227    }
228
229    #[test]
230    fn test_oci_config_parse_basic() {
231        let input = concat!(
232            "oci_version=1.0.2\n",
233            "root_path=/rootfs\n",
234            "root_readonly=true\n",
235            "hostname=mycontainer\n",
236            "process_cwd=/app\n",
237            "process_uid=1000\n",
238            "process_gid=1000\n",
239            "process_terminal=true\n",
240            "process_arg=/bin/sh\n",
241            "process_arg=-c\n",
242            "process_env=PATH=/usr/bin\n",
243            "namespace=pid\n",
244            "namespace=network:/proc/123/ns/net\n",
245            "cgroups_path=/sys/fs/cgroup/mycontainer\n",
246            "memory_limit=67108864\n",
247            "cpu_shares=512\n",
248            "cpu_quota=50000\n",
249            "cpu_period=100000\n",
250            "hook_prestart=/usr/bin/hook:5\n",
251            "mount=/proc:proc:proc:nosuid,noexec\n",
252        );
253        let config = OciConfig::parse(input).unwrap();
254        assert_eq!(config.oci_version, "1.0.2");
255        assert_eq!(config.root.path, "/rootfs");
256        assert!(config.root.readonly);
257        assert_eq!(config.hostname, "mycontainer");
258        assert_eq!(config.process.cwd, "/app");
259        assert_eq!(config.process.uid, 1000);
260        assert_eq!(config.process.gid, 1000);
261        assert!(config.process.terminal);
262        assert_eq!(config.process.args.len(), 2);
263        assert_eq!(config.process.args[0], "/bin/sh");
264        assert_eq!(config.process.env.len(), 1);
265        assert_eq!(config.linux.namespaces.len(), 2);
266        assert_eq!(config.linux.namespaces[0].kind, OciNamespaceKind::Pid);
267        assert!(config.linux.namespaces[0].path.is_none());
268        assert_eq!(config.linux.namespaces[1].kind, OciNamespaceKind::Network);
269        assert!(config.linux.namespaces[1].path.is_some());
270        assert_eq!(config.linux.memory_limit, 67108864);
271        assert_eq!(config.linux.cpu_shares, 512);
272        assert_eq!(config.linux.cpu_quota, 50000);
273        assert_eq!(config.hooks.prestart.len(), 1);
274        assert_eq!(config.hooks.prestart[0].timeout_secs, 5);
275        assert_eq!(config.mounts.len(), 1);
276        assert_eq!(config.mounts[0].options.len(), 2);
277    }
278
279    #[test]
280    fn test_oci_config_validate_empty_args() {
281        let input = "root_path=/rootfs\n";
282        let config = OciConfig::parse(input).unwrap();
283        assert!(config.validate().is_err());
284    }
285
286    #[test]
287    fn test_oci_container_lifecycle() {
288        let input = "root_path=/rootfs\nprocess_arg=/bin/sh\nprocess_cwd=/\n";
289        let config = OciConfig::parse(input).unwrap();
290        let mut container = OciContainer::new("test1", "/bundles/test1", config).unwrap();
291        assert_eq!(container.state, OciLifecycleState::Creating);
292
293        container.mark_created().unwrap();
294        assert_eq!(container.state, OciLifecycleState::Created);
295
296        container.start(42).unwrap();
297        assert_eq!(container.state, OciLifecycleState::Running);
298        assert_eq!(container.pid, 42);
299
300        container.stop().unwrap();
301        assert_eq!(container.state, OciLifecycleState::Stopped);
302    }
303
304    #[test]
305    fn test_oci_container_invalid_transition() {
306        let input = "root_path=/rootfs\nprocess_arg=/bin/sh\nprocess_cwd=/\n";
307        let config = OciConfig::parse(input).unwrap();
308        let mut container = OciContainer::new("test1", "/bundles/test1", config).unwrap();
309        // Cannot start from Creating (must be Created first)
310        assert!(container.start(1).is_err());
311    }
312
313    #[test]
314    fn test_oci_container_pivot_root() {
315        let input = "root_path=/rootfs\nprocess_arg=/bin/sh\nprocess_cwd=/\n";
316        let config = OciConfig::parse(input).unwrap();
317        let container = OciContainer::new("test1", "/bundles/test1", config).unwrap();
318        let (old, new) = container.pivot_root().unwrap();
319        assert_eq!(old, "/.pivot_root");
320        assert_eq!(new, "/rootfs");
321    }
322
323    // --- Container Image Format tests ---
324
325    #[test]
326    fn test_layer_digest_compute() {
327        let digest = LayerDigest::compute(b"hello");
328        // SHA-256("hello") =
329        // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
330        assert_eq!(digest.bytes[0], 0x2c);
331        assert_eq!(digest.bytes[1], 0xf2);
332    }
333
334    #[test]
335    fn test_layer_digest_hex() {
336        let digest = LayerDigest::compute(b"");
337        let hex = digest.to_hex();
338        assert_eq!(hex.len(), 64);
339        // SHA-256("") =
340        // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
341        assert!(hex.starts_with("e3b0c442"));
342    }
343
344    #[test]
345    fn test_is_gzip() {
346        assert!(is_gzip(&[0x1f, 0x8b, 0x08]));
347        assert!(!is_gzip(&[0x00, 0x00]));
348        assert!(!is_gzip(&[0x1f]));
349    }
350
351    #[test]
352    fn test_tar_size_parse() {
353        let mut header = [0u8; 512];
354        // Octal "0000644" at offset 124
355        header[124] = b'0';
356        header[125] = b'0';
357        header[126] = b'0';
358        header[127] = b'0';
359        header[128] = b'6';
360        header[129] = b'4';
361        header[130] = b'4';
362        assert_eq!(parse_tar_size(&header), 0o644);
363    }
364
365    #[test]
366    fn test_container_image_compose() {
367        let config = b"config data";
368        let layer1 = b"layer 1 data";
369        let layer2 = b"layer 2 data";
370        let image = ContainerImage::compose("test:latest", config, &[layer1, layer2]);
371        assert_eq!(image.name, "test:latest");
372        assert_eq!(image.layers.len(), 2);
373        assert_eq!(image.manifest.layer_digests.len(), 2);
374        assert_eq!(image.manifest.schema_version, 2);
375        assert_eq!(image.image_id, image.manifest.config_digest);
376    }
377
378    #[test]
379    fn test_layer_cache_operations() {
380        let mut cache = LayerCache::new(2);
381        let digest = LayerDigest::compute(b"test");
382        let hex = digest.to_hex();
383        let layer = CachedLayer {
384            digest: digest.clone(),
385            extracted_path: String::from("/layers/test"),
386            size_bytes: 1024,
387            reference_count: 1,
388        };
389        assert!(cache.insert(layer));
390        assert_eq!(cache.entry_count(), 1);
391        assert!(cache.get(&hex).is_some());
392        assert!(cache.add_ref(&hex));
393        assert_eq!(cache.get(&hex).unwrap().reference_count, 2);
394        assert!(cache.release(&hex));
395        assert_eq!(cache.get(&hex).unwrap().reference_count, 1);
396        assert!(cache.release(&hex)); // drops to 0, removed
397        assert_eq!(cache.entry_count(), 0);
398    }
399
400    #[test]
401    fn test_layer_cache_full() {
402        let mut cache = LayerCache::new(1);
403        let l1 = CachedLayer {
404            digest: LayerDigest::compute(b"a"),
405            extracted_path: String::from("/a"),
406            size_bytes: 100,
407            reference_count: 1,
408        };
409        let l2 = CachedLayer {
410            digest: LayerDigest::compute(b"b"),
411            extracted_path: String::from("/b"),
412            size_bytes: 200,
413            reference_count: 1,
414        };
415        assert!(cache.insert(l1));
416        assert!(cache.is_full());
417        assert!(!cache.insert(l2));
418    }
419
420    // --- Cgroup Memory Controller tests ---
421
422    #[test]
423    fn test_cgroup_memory_basic() {
424        let mut mem = CgroupMemoryController::new(1);
425        mem.set_hard_limit(4096).unwrap();
426        assert!(mem.charge(2048).is_ok());
427        assert_eq!(mem.usage_current, 2048);
428        assert_eq!(mem.usage_peak, 2048);
429        mem.uncharge(1024);
430        assert_eq!(mem.usage_current, 1024);
431        assert_eq!(mem.usage_peak, 2048); // peak unchanged
432    }
433
434    #[test]
435    fn test_cgroup_memory_oom() {
436        let mut mem = CgroupMemoryController::new(1);
437        mem.set_hard_limit(1024).unwrap();
438        mem.charge(512).unwrap();
439        let result = mem.charge(1024);
440        assert!(result.is_err());
441        assert_eq!(mem.oom.oom_kill_count, 1);
442        assert!(mem.oom.under_oom);
443    }
444
445    #[test]
446    fn test_cgroup_memory_soft_limit() {
447        let mut mem = CgroupMemoryController::new(1);
448        mem.set_soft_limit(512);
449        mem.charge(256).unwrap();
450        assert!(!mem.soft_limit_exceeded());
451        // Set hard limit high enough to not OOM
452        mem.set_hard_limit(4096).unwrap();
453        mem.charge(512).unwrap();
454        assert!(mem.soft_limit_exceeded());
455    }
456
457    #[test]
458    fn test_cgroup_memory_reclaim_from_cache() {
459        let mut mem = CgroupMemoryController::new(1);
460        mem.set_hard_limit(2048).unwrap();
461        mem.charge(1024).unwrap();
462        mem.add_cache(512);
463        // Now usage_current = 1024 + 512 = 1536, cache = 512
464        // Charge 1024 more would exceed 2048, but cache can be reclaimed
465        assert!(mem.charge(1024).is_ok());
466    }
467
468    #[test]
469    fn test_memory_stat_total() {
470        let stat = MemoryStat {
471            rss: 1000,
472            cache: 500,
473            mapped_file: 200,
474            anon: 300,
475            swap: 0,
476        };
477        assert_eq!(stat.total(), 1500);
478    }
479
480    // --- Cgroup CPU Controller tests ---
481
482    #[test]
483    fn test_cgroup_cpu_shares() {
484        let mut cpu = CgroupCpuController::new(1);
485        assert_eq!(cpu.shares, 1024);
486        cpu.set_shares(2048).unwrap();
487        assert_eq!(cpu.shares, 2048);
488        assert!(cpu.set_shares(1).is_err()); // below minimum
489        assert!(cpu.set_shares(300000).is_err()); // above maximum
490    }
491
492    #[test]
493    fn test_cgroup_cpu_bandwidth() {
494        let mut cpu = CgroupCpuController::new(1);
495        cpu.set_bandwidth(50000, 100000).unwrap();
496        assert_eq!(cpu.quota_us, 50000);
497        assert_eq!(cpu.period_us, 100000);
498        // 50% CPU
499        assert_eq!(cpu.effective_cpu_percent_x100(), 5000);
500    }
501
502    #[test]
503    fn test_cgroup_cpu_throttle() {
504        let mut cpu = CgroupCpuController::new(1);
505        cpu.set_bandwidth(10000, 100000).unwrap(); // 10% CPU
506                                                   // 10000us = 10_000_000ns quota
507        assert!(!cpu.consume_runtime(5_000_000)); // 5ms, not throttled
508        assert!(cpu.consume_runtime(6_000_000)); // 6ms more, total 11ms > 10ms quota
509        assert!(cpu.throttled);
510        assert_eq!(cpu.stats.nr_throttled, 1);
511    }
512
513    #[test]
514    fn test_cgroup_cpu_period_reset() {
515        let mut cpu = CgroupCpuController::new(1);
516        cpu.set_bandwidth(10000, 100000).unwrap();
517        cpu.consume_runtime(10_000_000);
518        cpu.new_period();
519        assert!(!cpu.throttled);
520        assert_eq!(cpu.stats.nr_periods, 1);
521    }
522
523    #[test]
524    fn test_cgroup_cpu_burst() {
525        let mut cpu = CgroupCpuController::new(1);
526        cpu.set_bandwidth(10000, 100000).unwrap(); // 10ms quota
527        cpu.set_burst(5000); // 5ms burst allowed
528                             // Use only 5ms of 10ms quota -> 5ms unused
529        cpu.consume_runtime(5_000_000);
530        cpu.new_period(); // 5ms saved as burst budget
531        assert!(cpu.burst_budget_ns > 0);
532        // Now can use up to 15ms (10ms quota + 5ms burst)
533        assert!(!cpu.consume_runtime(14_000_000));
534    }
535
536    #[test]
537    fn test_cgroup_cpu_proportional() {
538        let cpu = CgroupCpuController::new(1);
539        // Default shares=1024, total=2048 -> 50% of period
540        let ns = cpu.proportional_runtime_ns(2048);
541        // period=100000us=100_000_000ns, 1024/2048 = 50% = 50_000_000ns
542        assert_eq!(ns, 50_000_000);
543    }
544
545    // --- Overlay Filesystem tests ---
546
547    #[test]
548    fn test_overlay_basic_lookup() {
549        let mut lower = OverlayLayer::new(true);
550        // Add entry directly (bypass readonly check for setup)
551        lower.entries.insert(
552            String::from("etc/passwd"),
553            OverlayEntry {
554                path: String::from("etc/passwd"),
555                kind: OverlayEntryKind::File,
556                content: b"root:x:0:0".to_vec(),
557                mode: 0o644,
558            },
559        );
560
561        let mut fs = OverlayFs::new("/tmp/work");
562        fs.add_lower_layer(lower);
563        let entry = fs.lookup("etc/passwd");
564        assert!(entry.is_some());
565        assert_eq!(entry.unwrap().content, b"root:x:0:0");
566    }
567
568    #[test]
569    fn test_overlay_upper_takes_precedence() {
570        let mut lower = OverlayLayer::new(true);
571        lower.entries.insert(
572            String::from("etc/hostname"),
573            OverlayEntry {
574                path: String::from("etc/hostname"),
575                kind: OverlayEntryKind::File,
576                content: b"oldhost".to_vec(),
577                mode: 0o644,
578            },
579        );
580        let mut fs = OverlayFs::new("/tmp/work");
581        fs.add_lower_layer(lower);
582        fs.write_file("etc/hostname", b"newhost".to_vec(), 0o644)
583            .unwrap();
584        let entry = fs.lookup("etc/hostname").unwrap();
585        assert_eq!(entry.content, b"newhost");
586    }
587
588    #[test]
589    fn test_overlay_whiteout() {
590        let mut lower = OverlayLayer::new(true);
591        lower.entries.insert(
592            String::from("etc/shadow"),
593            OverlayEntry {
594                path: String::from("etc/shadow"),
595                kind: OverlayEntryKind::File,
596                content: b"secret".to_vec(),
597                mode: 0o600,
598            },
599        );
600        let mut fs = OverlayFs::new("/tmp/work");
601        fs.add_lower_layer(lower);
602        assert!(fs.lookup("etc/shadow").is_some());
603        fs.delete_file("etc/shadow").unwrap();
604        assert!(fs.lookup("etc/shadow").is_none());
605    }
606
607    #[test]
608    fn test_overlay_opaque_dir() {
609        let mut lower = OverlayLayer::new(true);
610        lower.entries.insert(
611            String::from("etc/conf.d/old.conf"),
612            OverlayEntry {
613                path: String::from("etc/conf.d/old.conf"),
614                kind: OverlayEntryKind::File,
615                content: b"old".to_vec(),
616                mode: 0o644,
617            },
618        );
619        let mut fs = OverlayFs::new("/tmp/work");
620        fs.add_lower_layer(lower);
621        fs.make_opaque_dir("etc/conf.d").unwrap();
622        // The lower layer file should not be visible via listing
623        let listing = fs.list_dir("etc/conf.d");
624        assert!(listing.is_empty());
625    }
626
627    #[test]
628    fn test_overlay_readonly_layer() {
629        let mut lower = OverlayLayer::new(true);
630        let result = lower.add_entry(OverlayEntry {
631            path: String::from("test"),
632            kind: OverlayEntryKind::File,
633            content: Vec::new(),
634            mode: 0o644,
635        });
636        assert!(result.is_err());
637    }
638
639    // --- Veth Networking tests ---
640
641    #[test]
642    fn test_veth_pair_creation() {
643        let pair = create_veth_pair("veth0", "eth0", 42);
644        assert_eq!(pair.host.name, "veth0");
645        assert_eq!(pair.container.name, "eth0");
646        assert_eq!(pair.host.peer_name, "eth0");
647        assert_eq!(pair.container.peer_name, "veth0");
648        assert_eq!(pair.host.namespace_id, 0);
649        assert_eq!(pair.container.namespace_id, 42);
650        assert_eq!(pair.host.mtu, 1500);
651        // MACs differ
652        assert_ne!(pair.host.mac, pair.container.mac);
653    }
654
655    #[test]
656    fn test_veth_mac_generation() {
657        let mac1 = generate_veth_mac(1);
658        let mac2 = generate_veth_mac(2);
659        assert_eq!(mac1[0], 0x02); // locally administered
660        assert_ne!(mac1, mac2);
661    }
662
663    #[test]
664    fn test_nat_table() {
665        let mut nat = NatTable::new(0xC0A80001); // 192.168.0.1
666        nat.enable_masquerade();
667        assert!(nat.masquerade_enabled);
668
669        let mapping = NatPortMapping {
670            external_port: 8080,
671            internal_port: 80,
672            protocol: 6,              // TCP
673            container_ip: 0x0A000002, // 10.0.0.2
674        };
675        nat.add_port_mapping(mapping).unwrap();
676        assert_eq!(nat.port_mappings.len(), 1);
677
678        // Duplicate should fail
679        let dup = NatPortMapping {
680            external_port: 8080,
681            internal_port: 8080,
682            protocol: 6,
683            container_ip: 0x0A000003,
684        };
685        assert!(nat.add_port_mapping(dup).is_err());
686
687        // Lookup
688        let found = nat.lookup_inbound(8080, 6).unwrap();
689        assert_eq!(found.internal_port, 80);
690        assert_eq!(found.container_ip, 0x0A000002);
691
692        // SNAT rewrite
693        let rewritten = nat.snat_rewrite(0x0A000002);
694        assert_eq!(rewritten, Some(0xC0A80001));
695
696        // Remove
697        assert!(nat.remove_port_mapping(8080, 6));
698        assert!(nat.lookup_inbound(8080, 6).is_none());
699    }
700
701    #[test]
702    fn test_veth_bridge() {
703        let mut bridge = VethBridge::new("br0", 0x0A000001, 0xFFFFFF00);
704        bridge.attach("veth0");
705        bridge.attach("veth1");
706        assert_eq!(bridge.attached_count(), 2);
707        assert!(bridge.in_subnet(0x0A0000FE)); // 10.0.0.254
708        assert!(!bridge.in_subnet(0x0B000001)); // 11.0.0.1
709
710        bridge.add_arp_proxy(ArpProxyEntry {
711            ip: 0x0A000002,
712            mac: [0x02, 0x42, 0x00, 0x00, 0x00, 0x01],
713        });
714        assert!(bridge.arp_lookup(0x0A000002).is_some());
715        assert!(bridge.arp_lookup(0x0A000003).is_none());
716
717        bridge.detach("veth0");
718        assert_eq!(bridge.attached_count(), 1);
719    }
720
721    // --- Seccomp BPF tests ---
722
723    #[test]
724    fn test_seccomp_data_as_bytes() {
725        let data = SeccompData::new(1, audit_arch::X86_64, [0; 6]);
726        let bytes = data.as_bytes();
727        assert_eq!(bytes.len(), 64);
728        // nr=1 at offset 0
729        assert_eq!(
730            u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
731            1
732        );
733        // arch at offset 4
734        assert_eq!(
735            u32::from_ne_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
736            audit_arch::X86_64
737        );
738    }
739
740    #[test]
741    fn test_seccomp_filter_validate() {
742        let mut filter = SeccompFilter::new();
743        assert!(filter.validate().is_err()); // empty
744
745        filter.push(BpfInstruction::ret(SeccompAction::Allow as u32));
746        assert!(filter.validate().is_ok());
747    }
748
749    #[test]
750    fn test_seccomp_filter_deny_syscalls() {
751        let filter = SeccompFilter::deny_syscalls(
752            audit_arch::X86_64,
753            &[59], // deny execve
754            1,     // EPERM
755        );
756        assert!(filter.validate().is_ok());
757
758        // Test: execve should be denied
759        let data_execve = SeccompData::new(59, audit_arch::X86_64, [0; 6]);
760        let action = filter.evaluate(&data_execve);
761        assert_eq!(action, SeccompAction::errno(1));
762
763        // Test: read should be allowed
764        let data_read = SeccompData::new(0, audit_arch::X86_64, [0; 6]);
765        let action = filter.evaluate(&data_read);
766        assert_eq!(action, SeccompAction::Allow as u32);
767    }
768
769    #[test]
770    fn test_seccomp_filter_allow_syscalls() {
771        let filter = SeccompFilter::allow_syscalls(
772            audit_arch::X86_64,
773            &[0, 1, 60], // read, write, exit
774        );
775        assert!(filter.validate().is_ok());
776
777        let data_read = SeccompData::new(0, audit_arch::X86_64, [0; 6]);
778        assert_eq!(filter.evaluate(&data_read), SeccompAction::Allow as u32);
779
780        let data_execve = SeccompData::new(59, audit_arch::X86_64, [0; 6]);
781        assert_eq!(
782            filter.evaluate(&data_execve),
783            SeccompAction::KillProcess as u32
784        );
785    }
786
787    #[test]
788    fn test_seccomp_wrong_arch_killed() {
789        let filter = SeccompFilter::deny_syscalls(audit_arch::X86_64, &[59], 1);
790        let data = SeccompData::new(59, audit_arch::AARCH64, [0; 6]);
791        assert_eq!(filter.evaluate(&data), SeccompAction::KillProcess as u32);
792    }
793
794    #[test]
795    fn test_seccomp_state_disabled() {
796        let state = SeccompState::new();
797        let data = SeccompData::new(59, audit_arch::X86_64, [0; 6]);
798        assert_eq!(state.evaluate(&data), SeccompAction::Allow as u32);
799    }
800
801    #[test]
802    fn test_seccomp_state_strict() {
803        let mut state = SeccompState::new();
804        state.mode = SeccompMode::Strict;
805        // read(0) allowed
806        let data = SeccompData::new(0, audit_arch::X86_64, [0; 6]);
807        assert_eq!(state.evaluate(&data), SeccompAction::Allow as u32);
808        // execve(59) killed
809        let data2 = SeccompData::new(59, audit_arch::X86_64, [0; 6]);
810        assert_eq!(state.evaluate(&data2), SeccompAction::KillThread as u32);
811    }
812
813    #[test]
814    fn test_seccomp_state_filter_install() {
815        let mut state = SeccompState::new();
816        let filter = SeccompFilter::deny_syscalls(audit_arch::X86_64, &[59], 1);
817        state.install_filter(filter).unwrap();
818        assert_eq!(state.mode, SeccompMode::Filter);
819        assert_eq!(state.filter_count(), 1);
820    }
821
822    #[test]
823    fn test_seccomp_fork_inherit() {
824        let mut state = SeccompState::new();
825        let mut f1 = SeccompFilter::deny_syscalls(audit_arch::X86_64, &[59], 1);
826        f1.inherit_on_fork = true;
827        let mut f2 = SeccompFilter::deny_syscalls(audit_arch::X86_64, &[60], 1);
828        f2.inherit_on_fork = false;
829        state.install_filter(f1).unwrap();
830        state.install_filter(f2).unwrap();
831        let child = state.fork_inherit();
832        assert_eq!(child.filter_count(), 1); // only inherited one
833    }
834
835    #[test]
836    fn test_bpf_instruction_constructors() {
837        let load = BpfInstruction::load_word(4);
838        assert_eq!(load.code, BpfOpcode::LdAbsW as u16);
839        assert_eq!(load.k, 4);
840
841        let jeq = BpfInstruction::jump_eq(42, 1, 0);
842        assert_eq!(jeq.code, BpfOpcode::JmpJeqK as u16);
843        assert_eq!(jeq.k, 42);
844        assert_eq!(jeq.jt, 1);
845        assert_eq!(jeq.jf, 0);
846
847        let ret = BpfInstruction::ret(SeccompAction::Allow as u32);
848        assert_eq!(ret.code, BpfOpcode::Ret as u16);
849    }
850
851    #[test]
852    fn test_seccomp_errno_action() {
853        let action = SeccompAction::errno(13); // EACCES
854        assert_eq!(action, 0x0005_000D);
855    }
856
857    // --- Helper tests ---
858
859    #[test]
860    fn test_parse_u32() {
861        assert_eq!(parse_u32("12345"), Some(12345));
862        assert_eq!(parse_u32("0"), Some(0));
863        assert_eq!(parse_u32("abc"), None);
864        assert_eq!(parse_u32(""), Some(0));
865    }
866
867    #[test]
868    fn test_parse_u64() {
869        assert_eq!(parse_u64("123456789"), Some(123456789));
870        assert_eq!(parse_u64("0"), Some(0));
871    }
872
873    #[test]
874    fn test_sha256_empty() {
875        let hash = simple_sha256(b"");
876        // SHA-256("") =
877        // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
878        assert_eq!(hash[0], 0xe3);
879        assert_eq!(hash[1], 0xb0);
880        assert_eq!(hash[2], 0xc4);
881        assert_eq!(hash[3], 0x42);
882    }
883
884    #[test]
885    fn test_sha256_hello() {
886        let hash = simple_sha256(b"hello");
887        // SHA-256("hello") =
888        // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
889        assert_eq!(hash[0], 0x2c);
890        assert_eq!(hash[1], 0xf2);
891        assert_eq!(hash[2], 0x4d);
892        assert_eq!(hash[3], 0xba);
893    }
894}