1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, NetfilterRuleInfo, Result};
11
12const TABLE_NAMES: &[(&str, &str)] = &[
14 ("filter", "iptable_filter_net_ops"),
15 ("nat", "iptable_nat_net_ops"),
16 ("mangle", "iptable_mangle_net_ops"),
17];
18
19pub fn walk_netfilter_rules<P: PhysicalMemoryProvider>(
24 reader: &ObjectReader<P>,
25) -> Result<Vec<NetfilterRuleInfo>> {
26 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 for &(table_name, _symbol) in TABLE_NAMES {
39 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 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 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; 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 let private_ptr = reader.read_pointer(table_addr, "xt_table", "private")?;
99 if private_ptr == 0 {
100 return Ok(Vec::new());
101 }
102
103 let entries_vaddr = reader.read_pointer(private_ptr, "xt_table_info", "entries")?;
105 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
111pub 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 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 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 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(), 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
208fn format_ipv4(ip: u32) -> String {
210 let b = ip.to_le_bytes();
211 format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3])
212}
213
214pub 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 fn make_ipt_entry_data(src_ip: u32, dst_ip: u32, proto: u16, target_name: &str) -> Vec<u8> {
254 let mut data = vec![0u8; 256];
267
268 data[0x00..0x04].copy_from_slice(&src_ip.to_le_bytes());
270 data[0x04..0x08].copy_from_slice(&dst_ip.to_le_bytes());
272 data[0x10..0x12].copy_from_slice(&proto.to_le_bytes());
274 let target_off: u16 = 0x60;
276 data[0x58..0x5A].copy_from_slice(&target_off.to_le_bytes());
277 data[0x5A..0x5C].copy_from_slice(&0u16.to_le_bytes());
279 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 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 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 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 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 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 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 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 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 .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 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 .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 let init_net_vaddr: u64 = 0xFFFF_8800_00A0_0000;
472 let init_net_paddr: u64 = 0x00B0_0000;
473
474 let af_inet_list_offset: usize = 32; let af_inet_list_vaddr = init_net_vaddr + af_inet_list_offset as u64;
479
480 let mut page = [0u8; 4096];
481 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 let src_ip: u32 = u32::from_le_bytes([1, 2, 3, 4]); 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 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 #[test]
542 fn parse_ipt_entries_two_chained_entries() {
543 let entry_size = 128usize;
546 let mut data = vec![0u8; entry_size * 2];
547
548 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()); let target_off1: u16 = 0x60;
554 data[0x58..0x5A].copy_from_slice(&target_off1.to_le_bytes());
555 data[0x5A..0x5C].copy_from_slice(&(entry_size as u16).to_le_bytes());
557 data[0x60..0x66].copy_from_slice(b"ACCEPT");
559
560 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()); 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()); 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 #[test]
593 fn parse_ipt_entries_zero_target_offset_empty_target_name() {
594 let data = vec![0u8; 256];
595 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 #[test]
613 fn walk_netfilter_rules_matching_table_name_calls_parse() {
614 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; 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 page[32..40].copy_from_slice(&xt_table_vaddr.to_le_bytes()); page[40..48].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); page[0x100..0x108].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); page[0x108..0x110].copy_from_slice(&af_inet_list_vaddr.to_le_bytes()); 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 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 #[allow(clippy::too_many_arguments)]
692 fn make_parse_table_rules_reader(
693 private_ptr: u64, 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 let isf = IsfBuilder::new()
705 .add_struct("xt_table", 256)
707 .add_field("xt_table", "private", 0x00u64, "pointer")
708 .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 let mut table_page = [0u8; 4096];
719 table_page[0x00..0x08].copy_from_slice(&private_ptr.to_le_bytes());
720
721 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 #[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, 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 #[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, table_vaddr,
793 table_paddr,
794 0, 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 #[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}