Skip to main content

memf_linux/
netfilter.rs

1//! Linux netfilter (iptables) rule extraction from kernel memory.
2//!
3//! Reads the kernel's iptables rule structures from the `xt_table` chain.
4//! The kernel organizes rules into tables (filter, nat, mangle) and chains
5//! (INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING).
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, NetfilterRuleInfo, Result};
11
12/// Known iptables table names and their kernel symbols.
13const TABLE_NAMES: &[(&str, &str)] = &[
14    ("filter", "iptable_filter_net_ops"),
15    ("nat", "iptable_nat_net_ops"),
16    ("mangle", "iptable_mangle_net_ops"),
17];
18
19/// Walk kernel iptables tables and extract rules.
20///
21/// Attempts to find the `init_net` namespace, then reads each registered
22/// iptables table (filter, nat, mangle) and parses rule entries.
23pub fn walk_netfilter_rules<P: PhysicalMemoryProvider>(
24    reader: &ObjectReader<P>,
25) -> Result<Vec<NetfilterRuleInfo>> {
26    // Look for init_net symbol
27    let init_net_addr =
28        reader
29            .symbols()
30            .symbol_address("init_net")
31            .ok_or_else(|| Error::MissingKernelSymbol {
32                name: "init_net".into(),
33            })?;
34
35    let mut rules = Vec::new();
36
37    // Try to read xt_table for each known table
38    for &(table_name, _symbol) in TABLE_NAMES {
39        // Find the table's xt_table by walking net->xt.tables[AF_INET]
40        // AF_INET = 2, so offset into xt.tables array
41        if let Ok(table_rules) = read_xt_table(reader, init_net_addr, table_name) {
42            rules.extend(table_rules);
43        }
44    }
45
46    Ok(rules)
47}
48
49fn read_xt_table<P: PhysicalMemoryProvider>(
50    reader: &ObjectReader<P>,
51    init_net_addr: u64,
52    table_name: &str,
53) -> Result<Vec<NetfilterRuleInfo>> {
54    // Read net.xt offset
55    let xt_offset =
56        reader
57            .symbols()
58            .field_offset("net", "xt")
59            .ok_or_else(|| Error::MissingField {
60                struct_name: "net".into(),
61                field_name: "xt".into(),
62            })?;
63    let xt_addr = init_net_addr + xt_offset;
64
65    // xt contains tables_lock and tables array.
66    // netns_xt.tables is an array of list_head, indexed by protocol family.
67    // AF_INET = 2
68    let tables_offset = reader
69        .symbols()
70        .field_offset("netns_xt", "tables")
71        .ok_or_else(|| Error::MissingField {
72            struct_name: "netns_xt".into(),
73            field_name: "tables".into(),
74        })?;
75
76    let list_head_size = reader.symbols().struct_size("list_head").unwrap_or(16);
77    let af_inet_list = xt_addr + tables_offset + 2 * list_head_size; // AF_INET = 2
78
79    // Walk the list to find xt_table with matching name
80    let table_addrs = reader.walk_list(af_inet_list, "xt_table", "list")?;
81
82    for &table_addr in &table_addrs {
83        let name = reader.read_field_string(table_addr, "xt_table", "name", 32)?;
84        if name == table_name {
85            return parse_table_rules(reader, table_addr, table_name);
86        }
87    }
88
89    Ok(Vec::new())
90}
91
92fn parse_table_rules<P: PhysicalMemoryProvider>(
93    reader: &ObjectReader<P>,
94    table_addr: u64,
95    table_name: &str,
96) -> Result<Vec<NetfilterRuleInfo>> {
97    // Read xt_table.private → pointer to xt_table_info.
98    let private_ptr = reader.read_pointer(table_addr, "xt_table", "private")?;
99    if private_ptr == 0 {
100        return Ok(Vec::new());
101    }
102
103    // Read xt_table_info.entries → virtual address of the flat ipt_entry region.
104    let entries_vaddr = reader.read_pointer(private_ptr, "xt_table_info", "entries")?;
105    // Read xt_table_info.size → byte length of the region.
106    let size: u64 = reader.read_field::<u64>(private_ptr, "xt_table_info", "size")?;
107
108    parse_ipt_entries(reader, entries_vaddr, size, table_name)
109}
110
111/// Parse a flat region of `ipt_entry` structures from raw memory.
112///
113/// `data_vaddr` is the virtual address of the first entry; `data_len` is the
114/// byte length of the region.  Entries are walked via `next_offset` until it
115/// is 0 or the end of the region is reached.
116///
117/// `ipt_entry` field offsets (kernel ABI, x86-64):
118///   0x00: src_ip (u32)
119///   0x04: dst_ip (u32)
120///   0x10: protocol (u16)
121///   0x58: target_offset (u16) — offset within entry to `ipt_entry_target`
122///   0x5A: next_offset (u16) — stride to next entry; 0 = end of table
123///
124/// `ipt_entry_target` at `entry_base + target_offset`:
125///   +0: name (29 bytes, null-terminated ASCII)
126pub fn parse_ipt_entries<P: PhysicalMemoryProvider>(
127    reader: &ObjectReader<P>,
128    data_vaddr: u64,
129    data_len: u64,
130    table_name: &str,
131) -> Result<Vec<NetfilterRuleInfo>> {
132    const MAX_RULES: usize = 10_000;
133    let data_len = data_len as usize;
134    // Read the entire data region at once.
135    let data = reader.read_bytes(data_vaddr, data_len).unwrap_or_default();
136    if data.is_empty() {
137        return Ok(Vec::new());
138    }
139
140    let mut rules = Vec::new();
141    let mut offset = 0usize;
142
143    for _ in 0..MAX_RULES {
144        // Need at least 0x5C bytes to read target_offset + next_offset.
145        if offset + 0x5C > data.len() {
146            break;
147        }
148
149        let src_ip = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap_or([0; 4]));
150        let dst_ip = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap_or([0; 4]));
151        let proto = u16::from_le_bytes(
152            data[offset + 0x10..offset + 0x12]
153                .try_into()
154                .unwrap_or([0; 2]),
155        );
156        let target_off = u16::from_le_bytes(
157            data[offset + 0x58..offset + 0x5A]
158                .try_into()
159                .unwrap_or([0; 2]),
160        ) as usize;
161        let next_off = u16::from_le_bytes(
162            data[offset + 0x5A..offset + 0x5C]
163                .try_into()
164                .unwrap_or([0; 2]),
165        ) as usize;
166
167        // Parse ipt_entry_target.name (29 bytes at entry_base + target_offset).
168        let target_name = if target_off > 0 && offset + target_off + 29 <= data.len() {
169            let name_bytes = &data[offset + target_off..offset + target_off + 29];
170            let end = name_bytes.iter().position(|&b| b == 0).unwrap_or(29);
171            String::from_utf8_lossy(&name_bytes[..end]).into_owned()
172        } else {
173            String::new()
174        };
175
176        let source = if src_ip != 0 {
177            Some(format_ipv4(src_ip))
178        } else {
179            None
180        };
181        let destination = if dst_ip != 0 {
182            Some(format_ipv4(dst_ip))
183        } else {
184            None
185        };
186
187        rules.push(NetfilterRuleInfo {
188            table: table_name.to_string(),
189            chain: String::new(), // chain resolution requires hook_entry offsets
190            target: target_name,
191            protocol: protocol_name(proto),
192            source,
193            destination,
194        });
195
196        if next_off == 0 {
197            break;
198        }
199        offset += next_off;
200        if offset >= data.len() {
201            break;
202        }
203    }
204
205    Ok(rules)
206}
207
208/// Format a raw u32 IPv4 address (little-endian stored) as a dotted string.
209fn format_ipv4(ip: u32) -> String {
210    let b = ip.to_le_bytes();
211    format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3])
212}
213
214/// Parse a protocol number to name.
215pub fn protocol_name(proto: u16) -> String {
216    match proto {
217        0 => "all".to_string(),
218        6 => "tcp".to_string(),
219        17 => "udp".to_string(),
220        1 => "icmp".to_string(),
221        _ => format!("proto:{proto}"),
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
229    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
230    use memf_symbols::isf::IsfResolver;
231    use memf_symbols::test_builders::IsfBuilder;
232
233    #[test]
234    fn protocol_name_known() {
235        assert_eq!(protocol_name(0), "all");
236        assert_eq!(protocol_name(6), "tcp");
237        assert_eq!(protocol_name(17), "udp");
238        assert_eq!(protocol_name(1), "icmp");
239    }
240
241    #[test]
242    fn protocol_name_unknown() {
243        assert_eq!(protocol_name(132), "proto:132");
244        assert_eq!(protocol_name(255), "proto:255");
245    }
246
247    // ---------------------------------------------------------------------------
248    // ipt_entry parsing tests
249    // ---------------------------------------------------------------------------
250
251    /// Build a minimal reader that maps a fake table data region and exposes
252    /// `parse_ipt_entries` directly.
253    fn make_ipt_entry_data(src_ip: u32, dst_ip: u32, proto: u16, target_name: &str) -> Vec<u8> {
254        // ipt_entry layout (offsets per kernel ABI):
255        //   0x00: src_ip (u32)
256        //   0x04: dst_ip (u32)
257        //   0x10: protocol (u16)
258        //   0x58: target_offset (u16) — relative offset to ipt_entry_target within entry
259        //   0x5A: next_offset (u16) — stride to next entry (0 = end)
260        //
261        // ipt_entry_target (at base + target_offset):
262        //   +0: name (29 bytes, null-terminated)
263        //
264        // We place one entry at the start.  target_offset = 0x60 (96 bytes into entry).
265        // next_offset = 0 means no more entries.
266        let mut data = vec![0u8; 256];
267
268        // src_ip at 0x00
269        data[0x00..0x04].copy_from_slice(&src_ip.to_le_bytes());
270        // dst_ip at 0x04
271        data[0x04..0x08].copy_from_slice(&dst_ip.to_le_bytes());
272        // protocol at 0x10
273        data[0x10..0x12].copy_from_slice(&proto.to_le_bytes());
274        // target_offset at 0x58: 0x60 (96 bytes in)
275        let target_off: u16 = 0x60;
276        data[0x58..0x5A].copy_from_slice(&target_off.to_le_bytes());
277        // next_offset at 0x5A: 0 = end
278        data[0x5A..0x5C].copy_from_slice(&0u16.to_le_bytes());
279        // target name at base + target_offset
280        let name_bytes = target_name.as_bytes();
281        let len = name_bytes.len().min(28);
282        data[0x60..0x60 + len].copy_from_slice(&name_bytes[..len]);
283
284        data
285    }
286
287    fn make_ipt_reader(
288        entry_data: &[u8],
289        entry_vaddr: u64,
290        entry_paddr: u64,
291    ) -> ObjectReader<SyntheticPhysMem> {
292        let isf = IsfBuilder::new().build_json();
293        let resolver = IsfResolver::from_value(&isf).unwrap();
294        let (cr3, mut mem) = PageTableBuilder::new()
295            .map_4k(entry_vaddr, entry_paddr, flags::PRESENT | flags::WRITABLE)
296            .build();
297        mem.write_bytes(entry_paddr, entry_data);
298        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
299        ObjectReader::new(vas, Box::new(resolver))
300    }
301
302    #[test]
303    fn parse_ipt_entries_src_ip_and_target() {
304        // src = 192.168.1.1 = 0xC0A80101 (LE), dst = 0, proto = tcp (6), target = "ACCEPT"
305        let src_ip: u32 = 0xC0A8_0101_u32.to_le();
306        let dst_ip: u32 = 0;
307        let data = make_ipt_entry_data(src_ip, dst_ip, 6, "ACCEPT");
308
309        let entry_vaddr: u64 = 0xFFFF_8000_0010_0000;
310        let entry_paddr: u64 = 0x0080_0000;
311        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
312
313        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
314        assert_eq!(rules.len(), 1, "should parse exactly one ipt_entry");
315        let rule = &rules[0];
316        assert_eq!(rule.target, "ACCEPT");
317        assert_eq!(rule.protocol, "tcp");
318        assert!(rule.source.is_some());
319    }
320
321    #[test]
322    fn parse_ipt_entries_drop_rule() {
323        // src = 0 (any), dst = 10.0.0.1, proto = all (0), target = "DROP"
324        let data = make_ipt_entry_data(0, 0x0A00_0001_u32.to_le(), 0, "DROP");
325
326        let entry_vaddr: u64 = 0xFFFF_8000_0020_0000;
327        let entry_paddr: u64 = 0x0090_0000;
328        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
329
330        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "nat").unwrap();
331        assert_eq!(rules.len(), 1);
332        let rule = &rules[0];
333        assert_eq!(rule.target, "DROP");
334        assert_eq!(rule.protocol, "all");
335    }
336
337    #[test]
338    fn parse_ipt_entries_empty_data_returns_empty() {
339        // data_len = 0 → read_bytes will return empty, parse returns Ok([])
340        let entry_vaddr: u64 = 0xFFFF_8000_0030_0000;
341        let isf = IsfBuilder::new().build_json();
342        let resolver = IsfResolver::from_value(&isf).unwrap();
343        let (cr3, mem) = PageTableBuilder::new().build();
344        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
345        let reader = ObjectReader::new(vas, Box::new(resolver));
346
347        let rules = parse_ipt_entries(&reader, entry_vaddr, 0, "filter").unwrap();
348        assert!(rules.is_empty(), "zero data_len should produce no rules");
349    }
350
351    #[test]
352    fn parse_ipt_entries_icmp_protocol() {
353        // icmp protocol (1)
354        let data = make_ipt_entry_data(0, 0, 1, "ACCEPT");
355        let entry_vaddr: u64 = 0xFFFF_8000_0040_0000;
356        let entry_paddr: u64 = 0x00B0_0000;
357        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
358
359        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
360        assert_eq!(rules.len(), 1);
361        assert_eq!(rules[0].protocol, "icmp");
362    }
363
364    #[test]
365    fn parse_ipt_entries_udp_protocol() {
366        // udp protocol (17)
367        let data = make_ipt_entry_data(0, 0, 17, "ACCEPT");
368        let entry_vaddr: u64 = 0xFFFF_8000_0050_0000;
369        let entry_paddr: u64 = 0x00C0_0000;
370        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
371
372        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "mangle").unwrap();
373        assert_eq!(rules.len(), 1);
374        assert_eq!(rules[0].protocol, "udp");
375    }
376
377    #[test]
378    fn parse_ipt_entries_unknown_protocol() {
379        // Unknown protocol number 47 (GRE)
380        let data = make_ipt_entry_data(0, 0, 47, "ACCEPT");
381        let entry_vaddr: u64 = 0xFFFF_8000_0060_0000;
382        let entry_paddr: u64 = 0x00D0_0000;
383        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
384
385        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
386        assert_eq!(rules.len(), 1);
387        assert_eq!(rules[0].protocol, "proto:47");
388    }
389
390    #[test]
391    fn walk_netfilter_rules_missing_init_net_returns_error() {
392        // init_net symbol absent → Error returned
393        let isf = IsfBuilder::new().build_json();
394        let resolver = IsfResolver::from_value(&isf).unwrap();
395        let (cr3, mem) = PageTableBuilder::new().build();
396        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
397        let reader = ObjectReader::new(vas, Box::new(resolver));
398
399        let result = walk_netfilter_rules(&reader);
400        assert!(
401            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_net"),
402            "expected MissingKernelSymbol {{name: \"init_net\"}}, got {result:?}"
403        );
404    }
405
406    #[test]
407    fn walk_netfilter_rules_init_net_present_no_xt_field_returns_empty() {
408        // init_net present but net.xt field missing → read_xt_table returns Err
409        // → errors are swallowed in the for loop → Ok(vec![])
410        let init_net_vaddr: u64 = 0xFFFF_8800_0080_0000;
411        let init_net_paddr: u64 = 0x00D0_0000;
412
413        let page = [0u8; 4096];
414
415        let isf = IsfBuilder::new()
416            .add_symbol("init_net", init_net_vaddr)
417            // "net" struct exists but has no "xt" field → field_offset returns None
418            .add_struct("net", 256)
419            .build_json();
420        let resolver = IsfResolver::from_value(&isf).unwrap();
421
422        let (cr3, mem) = PageTableBuilder::new()
423            .map_4k(init_net_vaddr, init_net_paddr, flags::WRITABLE)
424            .write_phys(init_net_paddr, &page)
425            .build();
426        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
427        let reader = ObjectReader::new(vas, Box::new(resolver));
428
429        let result = walk_netfilter_rules(&reader).unwrap_or_default();
430        assert!(
431            result.is_empty(),
432            "missing net.xt field → all tables fail → empty result"
433        );
434    }
435
436    #[test]
437    fn walk_netfilter_rules_init_net_present_no_netns_xt_tables_returns_empty() {
438        // init_net + net.xt present, but netns_xt.tables missing → read_xt_table fails
439        let init_net_vaddr: u64 = 0xFFFF_8800_0090_0000;
440        let init_net_paddr: u64 = 0x00E0_0000;
441
442        let page = [0u8; 4096];
443
444        let isf = IsfBuilder::new()
445            .add_symbol("init_net", init_net_vaddr)
446            .add_struct("net", 256)
447            .add_field("net", "xt", 0x00u64, "netns_xt")
448            // netns_xt struct exists but no "tables" field
449            .add_struct("netns_xt", 256)
450            .build_json();
451        let resolver = IsfResolver::from_value(&isf).unwrap();
452
453        let (cr3, mem) = PageTableBuilder::new()
454            .map_4k(init_net_vaddr, init_net_paddr, flags::WRITABLE)
455            .write_phys(init_net_paddr, &page)
456            .build();
457        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
458        let reader = ObjectReader::new(vas, Box::new(resolver));
459
460        let result = walk_netfilter_rules(&reader).unwrap_or_default();
461        assert!(
462            result.is_empty(),
463            "missing netns_xt.tables → all tables fail → empty result"
464        );
465    }
466
467    #[test]
468    fn walk_netfilter_rules_init_net_with_empty_xt_list() {
469        // init_net + net.xt + netns_xt.tables present; walk_list on the list_head
470        // returns empty because the list_head is self-pointing (no entries)
471        let init_net_vaddr: u64 = 0xFFFF_8800_00A0_0000;
472        let init_net_paddr: u64 = 0x00B0_0000;
473
474        // We put list_head for AF_INET (index 2) at offset = xt_offset + tables_offset + 2*16
475        // xt_offset = 0, tables_offset = 0, list_head_size=16 → AF_INET list at byte 32
476        // list_head is self-pointing → empty list
477        let af_inet_list_offset: usize = 32; // 0 + 0 + 2*16
478        let af_inet_list_vaddr = init_net_vaddr + af_inet_list_offset as u64;
479
480        let mut page = [0u8; 4096];
481        // Self-pointing list_head at offset 32
482        page[af_inet_list_offset..af_inet_list_offset + 8]
483            .copy_from_slice(&af_inet_list_vaddr.to_le_bytes());
484        page[af_inet_list_offset + 8..af_inet_list_offset + 16]
485            .copy_from_slice(&af_inet_list_vaddr.to_le_bytes());
486
487        let isf = IsfBuilder::new()
488            .add_symbol("init_net", init_net_vaddr)
489            .add_struct("net", 256)
490            .add_field("net", "xt", 0x00u64, "netns_xt")
491            .add_struct("netns_xt", 256)
492            .add_field("netns_xt", "tables", 0x00u64, "pointer")
493            .add_struct("list_head", 16)
494            .add_field("list_head", "next", 0x00u64, "pointer")
495            .add_field("list_head", "prev", 0x08u64, "pointer")
496            .add_struct("xt_table", 128)
497            .add_field("xt_table", "list", 0x00u64, "list_head")
498            .add_field("xt_table", "name", 0x10u64, "char")
499            .build_json();
500        let resolver = IsfResolver::from_value(&isf).unwrap();
501
502        let (cr3, mem) = PageTableBuilder::new()
503            .map_4k(init_net_vaddr, init_net_paddr, flags::WRITABLE)
504            .write_phys(init_net_paddr, &page)
505            .build();
506        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
507        let reader = ObjectReader::new(vas, Box::new(resolver));
508
509        let result = walk_netfilter_rules(&reader).unwrap_or_default();
510        assert!(
511            result.is_empty(),
512            "empty xt_table list should produce no rules"
513        );
514    }
515
516    #[test]
517    fn format_ipv4_correct() {
518        // 192.168.1.1 stored as little-endian u32: 0xC0A80101
519        // bytes: [0x01, 0x01, 0xA8, 0xC0] → "1.1.168.192"
520        // Actually: to_le_bytes of 0xC0A80101 = [0x01, 0x01, 0xA8, 0xC0]
521        // format_ipv4 formats b[0].b[1].b[2].b[3]
522        // We test through parse_ipt_entries with a known src_ip
523        let src_ip: u32 = u32::from_le_bytes([1, 2, 3, 4]); // stored as-is
524        let data = make_ipt_entry_data(src_ip, 0, 6, "ACCEPT");
525        let entry_vaddr: u64 = 0xFFFF_8000_0070_0000;
526        let entry_paddr: u64 = 0x00E0_0000;
527        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
528
529        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
530        assert_eq!(rules.len(), 1);
531        // source should be Some with dotted notation
532        let src = rules[0].source.as_deref().unwrap_or("");
533        assert!(
534            src.contains('.'),
535            "source IP should be dotted notation: {src}"
536        );
537    }
538
539    // --- parse_ipt_entries: two chained entries (non-zero next_offset) ---
540    // Exercises lines 182-186: when next_off != 0, offset advances to next entry.
541    #[test]
542    fn parse_ipt_entries_two_chained_entries() {
543        // Two ipt_entry records back to back. First has next_off = 128 (size of one record).
544        // Second has next_off = 0 (terminates).
545        let entry_size = 128usize;
546        let mut data = vec![0u8; entry_size * 2];
547
548        // Entry 1: src=1.2.3.4 (stored as bytes [1,2,3,4] → u32), proto=6, target="ACCEPT", next=128
549        let src1 = u32::from_le_bytes([1, 2, 3, 4]);
550        data[0x00..0x04].copy_from_slice(&src1.to_le_bytes());
551        data[0x10..0x12].copy_from_slice(&6u16.to_le_bytes()); // tcp
552                                                               // target_offset at 0x58: 0x60 → but 0x60 > entry_size(128=0x80), still within 2*128=256
553        let target_off1: u16 = 0x60;
554        data[0x58..0x5A].copy_from_slice(&target_off1.to_le_bytes());
555        // next_offset at 0x5A = 128
556        data[0x5A..0x5C].copy_from_slice(&(entry_size as u16).to_le_bytes());
557        // target name at 0x60: "ACCEPT"
558        data[0x60..0x66].copy_from_slice(b"ACCEPT");
559
560        // Entry 2 at offset 128: dst=5.6.7.8, proto=17, target="DROP", next=0
561        let dst2 = u32::from_le_bytes([5, 6, 7, 8]);
562        data[entry_size + 0x04..entry_size + 0x08].copy_from_slice(&dst2.to_le_bytes());
563        data[entry_size + 0x10..entry_size + 0x12].copy_from_slice(&17u16.to_le_bytes()); // udp
564                                                                                          // target_offset for entry 2: since entry 2 starts at 128, target at 128+0x60=0xE0
565                                                                                          // but we need it relative to entry 2's base: 0x60 places it at offset 96 within entry
566                                                                                          // data[entry_size+0x60..] is within our 256-byte buffer
567        let target_off2: u16 = 0x60;
568        data[entry_size + 0x58..entry_size + 0x5A].copy_from_slice(&target_off2.to_le_bytes());
569        data[entry_size + 0x5A..entry_size + 0x5C].copy_from_slice(&0u16.to_le_bytes()); // next=0
570        data[entry_size + 0x60..entry_size + 0x64].copy_from_slice(b"DROP");
571
572        let entry_vaddr: u64 = 0xFFFF_8000_0080_0000;
573        let entry_paddr: u64 = 0x00F0_0000;
574        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
575
576        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
577        assert_eq!(
578            rules.len(),
579            2,
580            "two chained entries should produce two rules"
581        );
582        assert_eq!(rules[0].target, "ACCEPT");
583        assert_eq!(rules[0].protocol, "tcp");
584        assert!(rules[0].source.is_some(), "entry 1 has src_ip");
585        assert_eq!(rules[1].target, "DROP");
586        assert_eq!(rules[1].protocol, "udp");
587        assert!(rules[1].destination.is_some(), "entry 2 has dst_ip");
588    }
589
590    // --- parse_ipt_entries: target_offset == 0 → target_name is empty string ---
591    // Exercises line 153: target_off == 0 → target_name = ""
592    #[test]
593    fn parse_ipt_entries_zero_target_offset_empty_target_name() {
594        let data = vec![0u8; 256];
595        // target_offset at 0x58 = 0 → condition `target_off > 0` is false → empty name
596        // next_offset at 0x5A = 0 → terminates
597        let entry_vaddr: u64 = 0xFFFF_8000_0090_0000;
598        let entry_paddr: u64 = 0x00F1_0000;
599        let reader = make_ipt_reader(&data, entry_vaddr, entry_paddr);
600
601        let rules = parse_ipt_entries(&reader, entry_vaddr, data.len() as u64, "filter").unwrap();
602        assert_eq!(rules.len(), 1);
603        assert!(
604            rules[0].target.is_empty(),
605            "zero target_offset must produce empty target name"
606        );
607    }
608
609    // --- walk_netfilter_rules: xt_table list has an entry whose name matches ---
610    // Exercises lines 72-76: the loop inside read_xt_table finds a matching table name
611    // → parse_table_rules is called → returns empty (stub) → rules stays empty.
612    #[test]
613    fn walk_netfilter_rules_matching_table_name_calls_parse() {
614        // We need:
615        //   init_net symbol, net.xt at offset 0, netns_xt.tables at offset 0,
616        //   list_head for AF_INET (index 2 × 16 = offset 32) that points to an
617        //   xt_table entry whose "name" field contains "filter".
618        //
619        // init_net_vaddr layout:
620        //   [0..16]  = net.xt (embedded netns_xt, tables at offset 0 within it)
621        //   [32..40] = AF_INET list_head.next → xt_table_vaddr (pointing at the table)
622        //   [40..48] = AF_INET list_head.prev → af_inet_list_vaddr (self)
623        //
624        // xt_table_vaddr layout (same page, offset 0x100):
625        //   list.next @ offset 0 → af_inet_list_vaddr  (back to head → list terminates after one entry)
626        //   list.prev @ offset 8 → af_inet_list_vaddr
627        //   name      @ offset 0x10 → "filter\0"
628        //   (no private/entries fields needed — parse_table_rules stub returns empty)
629
630        let init_net_vaddr: u64 = 0xFFFF_8800_00A1_0000;
631        let init_net_paddr: u64 = 0x00A1_0000;
632
633        let af_inet_offset: u64 = 32; // 2 * list_head_size(16)
634        let af_inet_list_vaddr = init_net_vaddr + af_inet_offset;
635        let xt_table_off: u64 = 0x100;
636        let xt_table_vaddr = init_net_vaddr + xt_table_off;
637
638        let mut page = [0u8; 4096];
639
640        // AF_INET list_head at [32..48]: next=xt_table_vaddr, prev=af_inet_list_vaddr
641        page[32..40].copy_from_slice(&xt_table_vaddr.to_le_bytes()); // list_head.next → xt_table
642        page[40..48].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); // list_head.prev
643
644        // xt_table at [0x100..]:
645        //   list.next @ 0x100 = af_inet_list_vaddr (back to head, terminates walk_list)
646        //   list.prev @ 0x108 = af_inet_list_vaddr
647        //   name @ 0x110 = "filter\0"
648        page[0x100..0x108].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); // list.next
649        page[0x108..0x110].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); // list.prev
650        let name_bytes = b"filter\0";
651        page[0x110..0x110 + name_bytes.len()].copy_from_slice(name_bytes);
652
653        let isf = IsfBuilder::new()
654            .add_symbol("init_net", init_net_vaddr)
655            .add_struct("net", 256)
656            .add_field("net", "xt", 0x00u64, "netns_xt")
657            .add_struct("netns_xt", 256)
658            .add_field("netns_xt", "tables", 0x00u64, "pointer")
659            .add_struct("list_head", 16)
660            .add_field("list_head", "next", 0x00u64, "pointer")
661            .add_field("list_head", "prev", 0x08u64, "pointer")
662            .add_struct("xt_table", 128)
663            .add_field("xt_table", "list", 0x00u64, "list_head")
664            .add_field("xt_table", "name", 0x10u64, "char")
665            .build_json();
666        let resolver = IsfResolver::from_value(&isf).unwrap();
667
668        let (cr3, mem) = PageTableBuilder::new()
669            .map_4k(init_net_vaddr, init_net_paddr, flags::WRITABLE)
670            .write_phys(init_net_paddr, &page)
671            .build();
672        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
673        let reader = ObjectReader::new(vas, Box::new(resolver));
674
675        // parse_table_rules is a stub that returns Ok([]) → total result is empty
676        let result = walk_netfilter_rules(&reader).unwrap_or_default();
677        assert!(
678            result.is_empty(),
679            "matching table name calls parse_table_rules (stub) → still empty"
680        );
681    }
682
683    // ---------------------------------------------------------------------------
684    // parse_table_rules tests
685    // ---------------------------------------------------------------------------
686
687    /// Build a reader wired up with `xt_table` and `xt_table_info` ISF types plus
688    /// the real entry data so that `parse_table_rules` can follow the full chain:
689    ///   xt_table.private → xt_table_info → (entries vaddr, size) → ipt_entry region
690    // Test builder wiring the xt_table → xt_table_info → ipt_entry chain; arity matches the layout.
691    #[allow(clippy::too_many_arguments)]
692    fn make_parse_table_rules_reader(
693        private_ptr: u64, // value stored in xt_table.private (0 = null test)
694        table_vaddr: u64,
695        table_paddr: u64,
696        table_info_vaddr: u64,
697        table_info_paddr: u64,
698        entries_vaddr: u64,
699        entries_paddr: u64,
700        entry_data: &[u8],
701    ) -> ObjectReader<SyntheticPhysMem> {
702        // ISF: add xt_table.private (pointer), xt_table_info.entries (pointer),
703        //      xt_table_info.size (unsigned long)
704        let isf = IsfBuilder::new()
705            // xt_table: only need the fields parse_table_rules reads
706            .add_struct("xt_table", 256)
707            .add_field("xt_table", "private", 0x00u64, "pointer")
708            // xt_table_info: entries at offset 0, size at offset 8
709            .add_struct("xt_table_info", 256)
710            .add_field("xt_table_info", "entries", 0x00u64, "pointer")
711            .add_field("xt_table_info", "size", 0x08u64, "unsigned long")
712            .build_json();
713        let resolver = IsfResolver::from_value(&isf).unwrap();
714
715        let entry_size = entry_data.len() as u64;
716
717        // xt_table page: private pointer at offset 0
718        let mut table_page = [0u8; 4096];
719        table_page[0x00..0x08].copy_from_slice(&private_ptr.to_le_bytes());
720
721        // xt_table_info page: entries pointer at offset 0, size at offset 8
722        let mut info_page = [0u8; 4096];
723        info_page[0x00..0x08].copy_from_slice(&entries_vaddr.to_le_bytes());
724        info_page[0x08..0x10].copy_from_slice(&entry_size.to_le_bytes());
725
726        let mut builder = PageTableBuilder::new()
727            .map_4k(table_vaddr, table_paddr, flags::PRESENT | flags::WRITABLE)
728            .write_phys(table_paddr, &table_page);
729
730        if private_ptr != 0 {
731            builder = builder
732                .map_4k(
733                    table_info_vaddr,
734                    table_info_paddr,
735                    flags::PRESENT | flags::WRITABLE,
736                )
737                .write_phys(table_info_paddr, &info_page)
738                .map_4k(
739                    entries_vaddr,
740                    entries_paddr,
741                    flags::PRESENT | flags::WRITABLE,
742                )
743                .write_phys(entries_paddr, entry_data);
744        }
745
746        let (cr3, mem) = builder.build();
747        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
748        ObjectReader::new(vas, Box::new(resolver))
749    }
750
751    /// parse_table_rules should follow xt_table.private → xt_table_info → ipt_entry region
752    /// and return at least one NetfilterRuleInfo when given a valid ipt_entry.
753    #[test]
754    fn parse_table_rules_returns_rules_from_xt_table() {
755        let entry_data = make_ipt_entry_data(0, 0, 6, "ACCEPT");
756
757        let table_vaddr: u64 = 0xFFFF_8000_0100_0000;
758        let table_paddr: u64 = 0x0010_0000;
759        let table_info_vaddr: u64 = 0xFFFF_8000_0101_0000;
760        let table_info_paddr: u64 = 0x0011_0000;
761        let entries_vaddr: u64 = 0xFFFF_8000_0102_0000;
762        let entries_paddr: u64 = 0x0012_0000;
763
764        let reader = make_parse_table_rules_reader(
765            table_info_vaddr, // private → points to xt_table_info
766            table_vaddr,
767            table_paddr,
768            table_info_vaddr,
769            table_info_paddr,
770            entries_vaddr,
771            entries_paddr,
772            &entry_data,
773        );
774
775        let rules = parse_table_rules(&reader, table_vaddr, "filter").unwrap();
776        assert!(
777            !rules.is_empty(),
778            "parse_table_rules should return at least one rule from the ipt_entry region"
779        );
780        assert_eq!(rules[0].target, "ACCEPT");
781        assert_eq!(rules[0].protocol, "tcp");
782    }
783
784    /// When xt_table.private is 0 (null), parse_table_rules must return an empty Vec.
785    #[test]
786    fn parse_table_rules_empty_when_private_null() {
787        let table_vaddr: u64 = 0xFFFF_8000_0110_0000;
788        let table_paddr: u64 = 0x0013_0000;
789
790        let reader = make_parse_table_rules_reader(
791            0, // private = null
792            table_vaddr,
793            table_paddr,
794            0, // unused
795            0,
796            0,
797            0,
798            &[],
799        );
800
801        let rules = parse_table_rules(&reader, table_vaddr, "filter").unwrap();
802        assert!(
803            rules.is_empty(),
804            "null xt_table.private must produce an empty rule list"
805        );
806    }
807
808    // --- NetfilterRuleInfo: Clone + Debug coverage ---
809    #[test]
810    fn netfilter_rule_info_clone_debug() {
811        use crate::NetfilterRuleInfo;
812        let rule = NetfilterRuleInfo {
813            table: "filter".to_string(),
814            chain: "INPUT".to_string(),
815            target: "DROP".to_string(),
816            protocol: "tcp".to_string(),
817            source: Some("1.2.3.4".to_string()),
818            destination: None,
819        };
820        let cloned = rule.clone();
821        assert_eq!(cloned.table, "filter");
822        assert_eq!(cloned.target, "DROP");
823        let dbg = format!("{cloned:?}");
824        assert!(dbg.contains("DROP"));
825        assert!(dbg.contains("1.2.3.4"));
826    }
827}