Skip to main content

memf_linux/
arp.rs

1//! Linux ARP cache extraction from the kernel neighbour table.
2//!
3//! Walks the `arp_tbl` (neigh_table) hash buckets to enumerate all
4//! ARP cache entries. Each `neighbour` struct holds the IP address,
5//! MAC address, NUD state, and associated network device.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{ArpEntryInfo, Error, NeighState, Result};
11
12/// Walk the kernel ARP neighbour table and extract all entries.
13///
14/// Reads the `arp_tbl` symbol (type `neigh_table`), dereferences
15/// the `nht` pointer to get the `neigh_hash_table`, then iterates
16/// hash buckets reading `neighbour` structs linked via `next`.
17pub fn walk_arp_cache<P: PhysicalMemoryProvider>(
18    reader: &ObjectReader<P>,
19) -> Result<Vec<ArpEntryInfo>> {
20    let arp_tbl_addr =
21        reader
22            .symbols()
23            .symbol_address("arp_tbl")
24            .ok_or_else(|| Error::MissingKernelSymbol {
25                name: "arp_tbl".into(),
26            })?;
27
28    // neigh_table.nht → pointer to neigh_hash_table
29    let nht_ptr: u64 = reader.read_field(arp_tbl_addr, "neigh_table", "nht")?;
30    if nht_ptr == 0 {
31        return Ok(Vec::new());
32    }
33
34    // neigh_hash_table.hash_buckets → pointer to array of neighbour*
35    let buckets_ptr: u64 = reader.read_field(nht_ptr, "neigh_hash_table", "hash_buckets")?;
36    // neigh_hash_table.hash_shift → log2(bucket_count)
37    let hash_shift: u32 = reader.read_field(nht_ptr, "neigh_hash_table", "hash_shift")?;
38    let bucket_count: u64 = 1u64 << hash_shift;
39
40    if buckets_ptr == 0 {
41        return Ok(Vec::new());
42    }
43
44    let mut entries = Vec::new();
45
46    for i in 0..bucket_count {
47        // Each bucket is a pointer (8 bytes) to the first neighbour
48        let bucket_addr = buckets_ptr + i * 8;
49        let neigh_ptr: u64 = match reader.read_bytes(bucket_addr, 8) {
50            Ok(bytes) => bytes[..8].try_into().map_or(0, u64::from_le_bytes),
51            Err(_) => continue,
52        };
53
54        let mut current = neigh_ptr;
55        let mut chain_len = 0;
56        while current != 0 && chain_len < 1000 {
57            match read_neighbour(reader, current) {
58                Ok(entry) => entries.push(entry),
59                Err(e @ (Error::MissingField { .. } | Error::MissingKernelSymbol { .. })) => {
60                    return Err(e);
61                }
62                Err(_) => {}
63            }
64
65            // Follow neighbour.next pointer
66            current = match reader.read_field::<u64>(current, "neighbour", "next") {
67                Ok(v) => v,
68                Err(_) => break,
69            };
70            chain_len += 1;
71        }
72    }
73
74    Ok(entries)
75}
76
77fn read_neighbour<P: PhysicalMemoryProvider>(
78    reader: &ObjectReader<P>,
79    neigh_addr: u64,
80) -> Result<ArpEntryInfo> {
81    // Read the 4-byte IPv4 address from primary_key
82    let ip_raw: u32 = reader.read_field(neigh_addr, "neighbour", "primary_key")?;
83    let ip_bytes = ip_raw.to_le_bytes();
84    let ip_addr = format!(
85        "{}.{}.{}.{}",
86        ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
87    );
88
89    // Read the 6-byte MAC address from ha field
90    let ha_offset = reader
91        .symbols()
92        .field_offset("neighbour", "ha")
93        .ok_or_else(|| Error::MissingField {
94            struct_name: "neighbour".into(),
95            field_name: "ha".into(),
96        })?;
97    let mac_bytes = reader.read_bytes(neigh_addr + ha_offset, 6)?;
98    let mac_addr = format!(
99        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
100        mac_bytes[0], mac_bytes[1], mac_bytes[2], mac_bytes[3], mac_bytes[4], mac_bytes[5]
101    );
102
103    // Read NUD state
104    let nud_state: u8 = reader.read_field(neigh_addr, "neighbour", "nud_state")?;
105
106    // Read device name via dev pointer → net_device.name
107    let dev_ptr: u64 = reader.read_field(neigh_addr, "neighbour", "dev")?;
108    let dev_name = if dev_ptr != 0 {
109        reader.read_field_string(dev_ptr, "net_device", "name", 16)?
110    } else {
111        String::from("?")
112    };
113
114    Ok(ArpEntryInfo {
115        ip_addr,
116        mac_addr,
117        dev_name,
118        state: NeighState::from_raw(nud_state),
119    })
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::NeighState;
126    use memf_core::object_reader::ObjectReader;
127    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
128    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
129    use memf_symbols::isf::IsfResolver;
130    use memf_symbols::test_builders::IsfBuilder;
131
132    // Synthetic layout:
133    //   arp_tbl (neigh_table):
134    //     nht @ 0 (pointer to neigh_hash_table)
135    //
136    //   neigh_hash_table:
137    //     hash_buckets @ 0 (pointer to array of neighbour*)
138    //     hash_shift   @ 8 (u32) — log2(bucket_count)
139    //
140    //   neighbour:
141    //     next          @ 0  (pointer — next in hash chain)
142    //     primary_key   @ 8  (4 bytes — IPv4 address)
143    //     ha            @ 12 (6 bytes — MAC address)
144    //     nud_state     @ 18 (u8)
145    //     dev           @ 24 (pointer to net_device)
146    //     total: 64 bytes
147    //
148    //   net_device:
149    //     name @ 0 (char[16])
150
151    const NHT_PTR_OFF: usize = 0;
152    // neigh_hash_table offsets
153    const HASH_BUCKETS_OFF: usize = 0;
154    const HASH_SHIFT_OFF: usize = 8;
155    // neighbour offsets
156    const NEIGH_NEXT_OFF: usize = 0;
157    const NEIGH_KEY_OFF: usize = 8;
158    const NEIGH_HA_OFF: usize = 12;
159    const NEIGH_NUD_OFF: usize = 18;
160    const NEIGH_DEV_OFF: usize = 24;
161
162    fn build_arp_isf() -> serde_json::Value {
163        IsfBuilder::new()
164            .add_struct("neigh_table", 64)
165            .add_field("neigh_table", "nht", 0, "pointer")
166            .add_struct("neigh_hash_table", 16)
167            .add_field("neigh_hash_table", "hash_buckets", 0, "pointer")
168            .add_field("neigh_hash_table", "hash_shift", 8, "unsigned int")
169            .add_struct("neighbour", 64)
170            .add_field("neighbour", "next", 0, "pointer")
171            .add_field("neighbour", "primary_key", 8, "unsigned int")
172            .add_field("neighbour", "ha", 12, "char")
173            .add_field("neighbour", "nud_state", 18, "unsigned char")
174            .add_field("neighbour", "dev", 24, "pointer")
175            .add_struct("net_device", 256)
176            .add_field("net_device", "name", 0, "char")
177            .add_symbol("arp_tbl", 0xFFFF_8000_0010_0000)
178            .build_json()
179    }
180
181    fn make_reader(pages: &[(u64, u64, &[u8])]) -> ObjectReader<SyntheticPhysMem> {
182        let isf = build_arp_isf();
183        let resolver = IsfResolver::from_value(&isf).unwrap();
184
185        let mut builder = PageTableBuilder::new();
186        for &(vaddr, paddr, data) in pages {
187            builder = builder
188                .map_4k(vaddr, paddr, flags::WRITABLE)
189                .write_phys(paddr, data);
190        }
191        let (cr3, mem) = builder.build();
192        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
193        ObjectReader::new(vas, Box::new(resolver))
194    }
195
196    /// Single ARP entry: 192.168.1.1 -> aa:bb:cc:dd:ee:ff on eth0
197    #[test]
198    fn walk_single_arp_entry() {
199        let arp_tbl_vaddr: u64 = 0xFFFF_8000_0010_0000;
200        let arp_tbl_paddr: u64 = 0x0080_0000;
201
202        let nht_vaddr: u64 = 0xFFFF_8000_0020_0000;
203        let nht_paddr: u64 = 0x0090_0000;
204
205        let neigh_vaddr: u64 = 0xFFFF_8000_0030_0000;
206        let neigh_paddr: u64 = 0x00A0_0000;
207
208        let dev_vaddr: u64 = 0xFFFF_8000_0040_0000;
209        let dev_paddr: u64 = 0x00B0_0000;
210
211        // bucket array lives at nht_vaddr + 0x100
212        let bucket_array_vaddr: u64 = nht_vaddr + 0x100;
213
214        // -- arp_tbl page: nht pointer
215        let mut arp_data = vec![0u8; 4096];
216        arp_data[NHT_PTR_OFF..NHT_PTR_OFF + 8].copy_from_slice(&nht_vaddr.to_le_bytes());
217
218        // -- nht page: bucket array pointer + hash_shift
219        let mut nht_data = vec![0u8; 4096];
220        nht_data[HASH_BUCKETS_OFF..HASH_BUCKETS_OFF + 8]
221            .copy_from_slice(&bucket_array_vaddr.to_le_bytes());
222        // hash_shift = 0 means 1 bucket (2^0 = 1)
223        nht_data[HASH_SHIFT_OFF..HASH_SHIFT_OFF + 4].copy_from_slice(&0u32.to_le_bytes());
224        // bucket[0] = pointer to neighbour
225        nht_data[0x100..0x108].copy_from_slice(&neigh_vaddr.to_le_bytes());
226
227        // -- neighbour page
228        let mut neigh_data = vec![0u8; 4096];
229        neigh_data[NEIGH_NEXT_OFF..NEIGH_NEXT_OFF + 8].copy_from_slice(&0u64.to_le_bytes()); // null = end of chain
230        let ip: u32 = u32::from_le_bytes([192, 168, 1, 1]);
231        neigh_data[NEIGH_KEY_OFF..NEIGH_KEY_OFF + 4].copy_from_slice(&ip.to_le_bytes());
232        neigh_data[NEIGH_HA_OFF..NEIGH_HA_OFF + 6]
233            .copy_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
234        neigh_data[NEIGH_NUD_OFF] = 0x02; // REACHABLE
235        neigh_data[NEIGH_DEV_OFF..NEIGH_DEV_OFF + 8].copy_from_slice(&dev_vaddr.to_le_bytes());
236
237        // -- net_device page
238        let mut dev_data = vec![0u8; 4096];
239        dev_data[..4].copy_from_slice(b"eth0");
240
241        let reader = make_reader(&[
242            (arp_tbl_vaddr, arp_tbl_paddr, &arp_data),
243            (nht_vaddr, nht_paddr, &nht_data),
244            (neigh_vaddr, neigh_paddr, &neigh_data),
245            (dev_vaddr, dev_paddr, &dev_data),
246        ]);
247
248        let entries = walk_arp_cache(&reader).unwrap();
249        assert_eq!(entries.len(), 1);
250        assert_eq!(entries[0].ip_addr, "192.168.1.1");
251        assert_eq!(entries[0].mac_addr, "aa:bb:cc:dd:ee:ff");
252        assert_eq!(entries[0].dev_name, "eth0");
253        assert_eq!(entries[0].state, NeighState::Reachable);
254    }
255
256    /// Empty ARP table (hash_shift=0, bucket[0] is null).
257    #[test]
258    fn walk_empty_arp_table() {
259        let arp_tbl_vaddr: u64 = 0xFFFF_8000_0010_0000;
260        let arp_tbl_paddr: u64 = 0x0080_0000;
261        let nht_vaddr: u64 = 0xFFFF_8000_0020_0000;
262        let nht_paddr: u64 = 0x0090_0000;
263        let bucket_array_vaddr: u64 = nht_vaddr + 0x100;
264
265        let mut arp_data = vec![0u8; 4096];
266        arp_data[NHT_PTR_OFF..NHT_PTR_OFF + 8].copy_from_slice(&nht_vaddr.to_le_bytes());
267
268        let mut nht_data = vec![0u8; 4096];
269        nht_data[HASH_BUCKETS_OFF..HASH_BUCKETS_OFF + 8]
270            .copy_from_slice(&bucket_array_vaddr.to_le_bytes());
271        nht_data[HASH_SHIFT_OFF..HASH_SHIFT_OFF + 4].copy_from_slice(&0u32.to_le_bytes());
272        // bucket[0] = 0 (null)
273        nht_data[0x100..0x108].copy_from_slice(&0u64.to_le_bytes());
274
275        let reader = make_reader(&[
276            (arp_tbl_vaddr, arp_tbl_paddr, &arp_data),
277            (nht_vaddr, nht_paddr, &nht_data),
278        ]);
279
280        let entries = walk_arp_cache(&reader).unwrap();
281        assert!(entries.is_empty());
282    }
283
284    /// Two ARP entries chained in same bucket.
285    #[test]
286    fn walk_two_entries_in_chain() {
287        let arp_tbl_vaddr: u64 = 0xFFFF_8000_0010_0000;
288        let arp_tbl_paddr: u64 = 0x0080_0000;
289        let nht_vaddr: u64 = 0xFFFF_8000_0020_0000;
290        let nht_paddr: u64 = 0x0090_0000;
291        let neigh1_vaddr: u64 = 0xFFFF_8000_0030_0000;
292        let neigh1_paddr: u64 = 0x00A0_0000;
293        let neigh2_vaddr: u64 = 0xFFFF_8000_0050_0000;
294        let neigh2_paddr: u64 = 0x00C0_0000;
295        let dev_vaddr: u64 = 0xFFFF_8000_0040_0000;
296        let dev_paddr: u64 = 0x00B0_0000;
297        let bucket_array_vaddr: u64 = nht_vaddr + 0x100;
298
299        let mut arp_data = vec![0u8; 4096];
300        arp_data[NHT_PTR_OFF..NHT_PTR_OFF + 8].copy_from_slice(&nht_vaddr.to_le_bytes());
301
302        let mut nht_data = vec![0u8; 4096];
303        nht_data[HASH_BUCKETS_OFF..HASH_BUCKETS_OFF + 8]
304            .copy_from_slice(&bucket_array_vaddr.to_le_bytes());
305        nht_data[HASH_SHIFT_OFF..HASH_SHIFT_OFF + 4].copy_from_slice(&0u32.to_le_bytes());
306        nht_data[0x100..0x108].copy_from_slice(&neigh1_vaddr.to_le_bytes());
307
308        // neigh1 -> neigh2
309        let mut neigh1_data = vec![0u8; 4096];
310        neigh1_data[NEIGH_NEXT_OFF..NEIGH_NEXT_OFF + 8]
311            .copy_from_slice(&neigh2_vaddr.to_le_bytes());
312        let ip1: u32 = u32::from_le_bytes([10, 0, 0, 1]);
313        neigh1_data[NEIGH_KEY_OFF..NEIGH_KEY_OFF + 4].copy_from_slice(&ip1.to_le_bytes());
314        neigh1_data[NEIGH_HA_OFF..NEIGH_HA_OFF + 6]
315            .copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]);
316        neigh1_data[NEIGH_NUD_OFF] = 0x04; // STALE
317        neigh1_data[NEIGH_DEV_OFF..NEIGH_DEV_OFF + 8].copy_from_slice(&dev_vaddr.to_le_bytes());
318
319        // neigh2 -> null
320        let mut neigh2_data = vec![0u8; 4096];
321        neigh2_data[NEIGH_NEXT_OFF..NEIGH_NEXT_OFF + 8].copy_from_slice(&0u64.to_le_bytes());
322        let ip2: u32 = u32::from_le_bytes([10, 0, 0, 2]);
323        neigh2_data[NEIGH_KEY_OFF..NEIGH_KEY_OFF + 4].copy_from_slice(&ip2.to_le_bytes());
324        neigh2_data[NEIGH_HA_OFF..NEIGH_HA_OFF + 6]
325            .copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00]);
326        neigh2_data[NEIGH_NUD_OFF] = 0x80; // PERMANENT
327        neigh2_data[NEIGH_DEV_OFF..NEIGH_DEV_OFF + 8].copy_from_slice(&dev_vaddr.to_le_bytes());
328
329        let mut dev_data = vec![0u8; 4096];
330        dev_data[..5].copy_from_slice(b"ens33");
331
332        let reader = make_reader(&[
333            (arp_tbl_vaddr, arp_tbl_paddr, &arp_data),
334            (nht_vaddr, nht_paddr, &nht_data),
335            (neigh1_vaddr, neigh1_paddr, &neigh1_data),
336            (neigh2_vaddr, neigh2_paddr, &neigh2_data),
337            (dev_vaddr, dev_paddr, &dev_data),
338        ]);
339
340        let entries = walk_arp_cache(&reader).unwrap();
341        assert_eq!(entries.len(), 2);
342        assert_eq!(entries[0].ip_addr, "10.0.0.1");
343        assert_eq!(entries[0].mac_addr, "11:22:33:44:55:66");
344        assert_eq!(entries[0].state, NeighState::Stale);
345        assert_eq!(entries[1].ip_addr, "10.0.0.2");
346        assert_eq!(entries[1].mac_addr, "aa:bb:cc:dd:ee:00");
347        assert_eq!(entries[1].state, NeighState::Permanent);
348        assert!(entries.iter().all(|e| e.dev_name == "ens33"));
349    }
350
351    #[test]
352    fn missing_arp_tbl_returns_missing_kernel_symbol() {
353        let isf = IsfBuilder::new().build_json();
354        let resolver = IsfResolver::from_value(&isf).unwrap();
355        let (cr3, mem) = PageTableBuilder::new().build();
356        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
357        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
358        let result = walk_arp_cache(&reader);
359        assert!(
360            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "arp_tbl"),
361            "expected MissingKernelSymbol {{name: \"arp_tbl\"}}, got {result:?}"
362        );
363    }
364
365    #[test]
366    fn missing_neighbour_ha_field_returns_missing_field() {
367        // arp_tbl present, chain leads to a neighbour, but neighbour.ha is absent from ISF
368        let arp_tbl_vaddr: u64 = 0xFFFF_8000_0010_0000;
369        let arp_tbl_paddr: u64 = 0x0080_0000;
370        let nht_vaddr: u64 = 0xFFFF_8000_0020_0000;
371        let nht_paddr: u64 = 0x0090_0000;
372        let neigh_vaddr: u64 = 0xFFFF_8000_0030_0000;
373        let neigh_paddr: u64 = 0x00A0_0000;
374        let bucket_array_vaddr: u64 = nht_vaddr + 0x100;
375
376        let isf = IsfBuilder::new()
377            .add_symbol("arp_tbl", arp_tbl_vaddr)
378            .add_struct("neigh_table", 256)
379            .add_field("neigh_table", "nht", 0, "pointer")
380            .add_struct("neighbour", 128)
381            .add_field("neighbour", "next", 0, "pointer")
382            .add_field("neighbour", "primary_key", 8, "unsigned int")
383            // neighbour.ha intentionally omitted
384            .add_struct("neigh_hash_table", 64)
385            .add_field("neigh_hash_table", "hash_buckets", 0, "pointer")
386            .add_field("neigh_hash_table", "hash_shift", 8, "int")
387            .build_json();
388        let resolver = IsfResolver::from_value(&isf).unwrap();
389
390        let mut arp_data = vec![0u8; 4096];
391        arp_data[0..8].copy_from_slice(&nht_vaddr.to_le_bytes());
392
393        let mut nht_data = vec![0u8; 4096];
394        nht_data[0..8].copy_from_slice(&bucket_array_vaddr.to_le_bytes());
395        nht_data[8..12].copy_from_slice(&0u32.to_le_bytes()); // hash_shift=0 → 1 bucket
396        nht_data[0x100..0x108].copy_from_slice(&neigh_vaddr.to_le_bytes());
397
398        let neigh_data = vec![0u8; 4096]; // neighbour.next=0, primary_key=0; ha absent in ISF
399
400        let (cr3, mem) = PageTableBuilder::new()
401            .map_4k(arp_tbl_vaddr, arp_tbl_paddr, flags::WRITABLE)
402            .write_phys(arp_tbl_paddr, &arp_data)
403            .map_4k(nht_vaddr, nht_paddr, flags::WRITABLE)
404            .write_phys(nht_paddr, &nht_data)
405            .map_4k(neigh_vaddr, neigh_paddr, flags::WRITABLE)
406            .write_phys(neigh_paddr, &neigh_data)
407            .build();
408        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
409        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
410        let result = walk_arp_cache(&reader);
411        assert!(
412            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "neighbour" && field_name == "ha"),
413            "expected MissingField neighbour.ha, got {result:?}"
414        );
415    }
416}