Skip to main content

memf_linux/
network.rs

1//! Linux network connection walker.
2//!
3//! Enumerates TCP connections by scanning the kernel's `tcp_hashinfo.ehash`
4//! hash table. Each bucket contains a `hlist_nulls` chain of `sock` structs.
5
6use memf_core::object_reader::ObjectReader;
7use memf_format::PhysicalMemoryProvider;
8
9use crate::{ConnectionInfo, ConnectionState, Error, Protocol, Result};
10
11/// Walk Linux TCP connections via `tcp_hashinfo.ehash`.
12pub fn walk_connections<P: PhysicalMemoryProvider>(
13    reader: &ObjectReader<P>,
14) -> Result<Vec<ConnectionInfo>> {
15    let tcp_hashinfo_addr = reader
16        .symbols()
17        .symbol_address("tcp_hashinfo")
18        .ok_or_else(|| Error::MissingKernelSymbol {
19            name: "tcp_hashinfo".into(),
20        })?;
21
22    let ehash_ptr: u64 = reader.read_field(tcp_hashinfo_addr, "inet_hashinfo", "ehash")?;
23    let ehash_mask: u32 = reader.read_field(tcp_hashinfo_addr, "inet_hashinfo", "ehash_mask")?;
24
25    if ehash_ptr == 0 {
26        return Ok(Vec::new());
27    }
28
29    let mut connections = Vec::new();
30    let bucket_count = u64::from(ehash_mask) + 1;
31
32    for i in 0..bucket_count {
33        let bucket_size = reader
34            .symbols()
35            .struct_size("inet_ehash_bucket")
36            .unwrap_or(8);
37        let bucket_addr = ehash_ptr + i * bucket_size;
38
39        let chain_first: u64 = match reader.read_field(bucket_addr, "inet_ehash_bucket", "chain") {
40            Ok(v) => v,
41            Err(_) => continue,
42        };
43
44        // hlist_nulls terminates with low bit set
45        if chain_first == 0 || chain_first & 1 != 0 {
46            continue;
47        }
48
49        let mut sk_addr = chain_first;
50        let mut chain_len = 0;
51        while sk_addr != 0 && sk_addr & 1 == 0 && chain_len < 1000 {
52            if let Ok(conn) = read_inet_sock(reader, sk_addr) {
53                connections.push(conn);
54            }
55
56            sk_addr = match reader.read_pointer(sk_addr, "sock_common", "skc_nulls_node") {
57                Ok(v) => v,
58                Err(_) => break,
59            };
60            chain_len += 1;
61        }
62    }
63
64    Ok(connections)
65}
66
67fn read_inet_sock<P: PhysicalMemoryProvider>(
68    reader: &ObjectReader<P>,
69    sk_addr: u64,
70) -> Result<ConnectionInfo> {
71    let sk_common_off = reader
72        .symbols()
73        .field_offset("sock", "__sk_common")
74        .unwrap_or(0);
75    let common_addr = sk_addr + sk_common_off;
76
77    let daddr: u32 = reader.read_field(common_addr, "sock_common", "skc_daddr")?;
78    let saddr: u32 = reader.read_field(common_addr, "sock_common", "skc_rcv_saddr")?;
79    let dport: u16 = reader.read_field(common_addr, "sock_common", "skc_dport")?;
80    let sport: u16 = reader.read_field(common_addr, "sock_common", "skc_num")?;
81    let state: u8 = reader.read_field(common_addr, "sock_common", "skc_state")?;
82
83    // dport is in network byte order (big-endian)
84    let dport = u16::from_be(dport);
85
86    Ok(ConnectionInfo {
87        protocol: Protocol::Tcp,
88        local_addr: ipv4_to_string(saddr),
89        local_port: sport,
90        remote_addr: ipv4_to_string(daddr),
91        remote_port: dport,
92        state: ConnectionState::from_raw(state),
93        pid: None,
94    })
95}
96
97fn ipv4_to_string(addr: u32) -> String {
98    let bytes = addr.to_le_bytes();
99    format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
100}
101
102/// Walk Linux TCP IPv6 connections via `tcp6_hashinfo.ehash`.
103///
104/// Returns `Ok(Vec::new())` if `tcp6_hashinfo` symbol is absent.
105pub fn walk_connections6<P: PhysicalMemoryProvider>(
106    reader: &ObjectReader<P>,
107) -> Result<Vec<ConnectionInfo>> {
108    let tcp6_hashinfo_addr = match reader.symbols().symbol_address("tcp6_hashinfo") {
109        Some(a) => a,
110        None => return Ok(Vec::new()),
111    };
112
113    let ehash_ptr: u64 = reader.read_field(tcp6_hashinfo_addr, "inet_hashinfo", "ehash")?;
114    let ehash_mask: u32 = reader.read_field(tcp6_hashinfo_addr, "inet_hashinfo", "ehash_mask")?;
115
116    if ehash_ptr == 0 {
117        return Ok(Vec::new());
118    }
119
120    let mut connections = Vec::new();
121    let bucket_count = u64::from(ehash_mask) + 1;
122
123    for i in 0..bucket_count {
124        let bucket_size = reader
125            .symbols()
126            .struct_size("inet_ehash_bucket")
127            .unwrap_or(8);
128        let bucket_addr = ehash_ptr + i * bucket_size;
129
130        let chain_first: u64 = match reader.read_field(bucket_addr, "inet_ehash_bucket", "chain") {
131            Ok(v) => v,
132            Err(_) => continue,
133        };
134
135        if chain_first == 0 || chain_first & 1 != 0 {
136            continue;
137        }
138
139        let mut sk_addr = chain_first;
140        let mut chain_len = 0;
141        while sk_addr != 0 && sk_addr & 1 == 0 && chain_len < 1000 {
142            if let Ok(conn) = read_inet6_sock(reader, sk_addr) {
143                connections.push(conn);
144            }
145            sk_addr = match reader.read_pointer(sk_addr, "sock_common", "skc_nulls_node") {
146                Ok(v) => v,
147                Err(_) => break,
148            };
149            chain_len += 1;
150        }
151    }
152
153    Ok(connections)
154}
155
156fn read_inet6_sock<P: PhysicalMemoryProvider>(
157    reader: &ObjectReader<P>,
158    sk_addr: u64,
159) -> Result<ConnectionInfo> {
160    let sk_common_off = reader
161        .symbols()
162        .field_offset("sock", "__sk_common")
163        .unwrap_or(0);
164    let common_addr = sk_addr + sk_common_off;
165
166    // Read 16-byte IPv6 addresses using read_bytes on the computed field offsets.
167    let daddr_off = reader
168        .symbols()
169        .field_offset("sock_common", "skc_v6_daddr")
170        .unwrap_or(8);
171    let saddr_off = reader
172        .symbols()
173        .field_offset("sock_common", "skc_v6_rcv_saddr")
174        .unwrap_or(24);
175
176    let daddr_bytes = reader.read_bytes(common_addr + daddr_off, 16)?;
177    let saddr_bytes = reader.read_bytes(common_addr + saddr_off, 16)?;
178
179    let mut daddr = [0u8; 16];
180    let mut saddr = [0u8; 16];
181    daddr.copy_from_slice(&daddr_bytes);
182    saddr.copy_from_slice(&saddr_bytes);
183
184    let dport: u16 = reader.read_field(common_addr, "sock_common", "skc_dport")?;
185    let sport: u16 = reader.read_field(common_addr, "sock_common", "skc_num")?;
186    let state: u8 = reader.read_field(common_addr, "sock_common", "skc_state")?;
187
188    Ok(ConnectionInfo {
189        protocol: Protocol::Tcp6,
190        local_addr: ipv6_to_string(&saddr),
191        local_port: sport,
192        remote_addr: ipv6_to_string(&daddr),
193        remote_port: u16::from_be(dport),
194        state: ConnectionState::from_raw(state),
195        pid: None,
196    })
197}
198
199pub(crate) fn ipv6_to_string(addr: &[u8; 16]) -> String {
200    use std::net::Ipv6Addr;
201    let mut groups = [0u16; 8];
202    for (i, chunk) in addr.chunks_exact(2).enumerate() {
203        groups[i] = u16::from_be_bytes([chunk[0], chunk[1]]);
204    }
205    Ipv6Addr::from(groups).to_string()
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
212    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
213    use memf_symbols::isf::IsfResolver;
214    use memf_symbols::test_builders::IsfBuilder;
215
216    #[test]
217    fn walk_ipv6_no_symbol_returns_empty() {
218        let isf = IsfBuilder::new().build_json();
219        let resolver = IsfResolver::from_value(&isf).unwrap();
220        let (cr3, mem) = PageTableBuilder::new().build();
221        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
222        let reader = ObjectReader::new(vas, Box::new(resolver));
223        let result = walk_connections6(&reader).unwrap();
224        assert!(result.is_empty());
225    }
226
227    #[test]
228    fn ipv6_loopback_formats_correctly() {
229        let addr = [0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
230        assert_eq!(ipv6_to_string(&addr), "::1");
231    }
232
233    #[test]
234    fn ipv6_full_address_formats_correctly() {
235        let addr = [0x20u8, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
236        assert_eq!(ipv6_to_string(&addr), "2001:db8::1");
237    }
238
239    fn make_net_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
240        let isf = IsfBuilder::new()
241            .add_struct("inet_hashinfo", 64)
242            .add_field("inet_hashinfo", "ehash", 0, "pointer")
243            .add_field("inet_hashinfo", "ehash_mask", 8, "unsigned int")
244            .add_struct("inet_ehash_bucket", 8)
245            .add_field("inet_ehash_bucket", "chain", 0, "pointer")
246            .add_struct("sock_common", 64)
247            .add_field("sock_common", "skc_nulls_node", 0, "pointer")
248            .add_field("sock_common", "skc_daddr", 8, "unsigned int")
249            .add_field("sock_common", "skc_rcv_saddr", 12, "unsigned int")
250            .add_field("sock_common", "skc_dport", 16, "unsigned short")
251            .add_field("sock_common", "skc_num", 18, "unsigned short")
252            .add_field("sock_common", "skc_state", 20, "unsigned char")
253            .add_struct("sock", 256)
254            .add_field("sock", "__sk_common", 0, "sock_common")
255            .add_symbol("tcp_hashinfo", vaddr)
256            .build_json();
257
258        let resolver = IsfResolver::from_value(&isf).unwrap();
259        let (cr3, mem) = PageTableBuilder::new()
260            .map_4k(vaddr, paddr, flags::WRITABLE)
261            .write_phys(paddr, data)
262            .build();
263        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
264        ObjectReader::new(vas, Box::new(resolver))
265    }
266
267    #[test]
268    fn walk_single_connection() {
269        let vaddr: u64 = 0xFFFF_8000_0010_0000;
270        let paddr: u64 = 0x0080_0000;
271        let mut data = vec![0u8; 4096];
272
273        let ehash_addr = vaddr + 0x100;
274        data[0..8].copy_from_slice(&ehash_addr.to_le_bytes());
275        data[8..12].copy_from_slice(&0u32.to_le_bytes());
276
277        let sock_addr = vaddr + 0x200;
278        data[0x100..0x108].copy_from_slice(&sock_addr.to_le_bytes());
279
280        // sock_common at vaddr + 0x200
281        data[0x200..0x208].copy_from_slice(&1u64.to_le_bytes()); // null terminator
282        let daddr: u32 = u32::from_le_bytes([192, 168, 1, 100]);
283        data[0x208..0x20C].copy_from_slice(&daddr.to_le_bytes());
284        let saddr: u32 = u32::from_le_bytes([10, 0, 0, 1]);
285        data[0x20C..0x210].copy_from_slice(&saddr.to_le_bytes());
286        data[0x210..0x212].copy_from_slice(&443u16.to_be_bytes());
287        data[0x212..0x214].copy_from_slice(&54321u16.to_le_bytes());
288        data[0x214] = 1; // ESTABLISHED
289
290        let reader = make_net_reader(&data, vaddr, paddr);
291        let conns = walk_connections(&reader).unwrap();
292
293        assert_eq!(conns.len(), 1);
294        assert_eq!(conns[0].protocol, Protocol::Tcp);
295        assert_eq!(conns[0].local_addr, "10.0.0.1");
296        assert_eq!(conns[0].local_port, 54321);
297        assert_eq!(conns[0].remote_addr, "192.168.1.100");
298        assert_eq!(conns[0].remote_port, 443);
299        assert_eq!(conns[0].state, ConnectionState::Established);
300    }
301
302    #[test]
303    fn empty_hash_table() {
304        let vaddr: u64 = 0xFFFF_8000_0010_0000;
305        let paddr: u64 = 0x0080_0000;
306        let mut data = vec![0u8; 4096];
307        data[0..8].copy_from_slice(&0u64.to_le_bytes());
308        data[8..12].copy_from_slice(&0u32.to_le_bytes());
309
310        let reader = make_net_reader(&data, vaddr, paddr);
311        let conns = walk_connections(&reader).unwrap();
312        assert!(conns.is_empty());
313    }
314
315    #[test]
316    fn ipv4_formatting() {
317        assert_eq!(
318            ipv4_to_string(u32::from_le_bytes([127, 0, 0, 1])),
319            "127.0.0.1"
320        );
321        assert_eq!(
322            ipv4_to_string(u32::from_le_bytes([192, 168, 1, 1])),
323            "192.168.1.1"
324        );
325    }
326
327    #[test]
328    fn missing_tcp_hashinfo_returns_missing_kernel_symbol() {
329        let isf = IsfBuilder::new().build_json();
330        let resolver = IsfResolver::from_value(&isf).unwrap();
331        let (cr3, mem) = PageTableBuilder::new().build();
332        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
333        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
334        let result = walk_connections(&reader);
335        assert!(
336            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "tcp_hashinfo"),
337            "expected MissingKernelSymbol {{name: \"tcp_hashinfo\"}}, got {result:?}"
338        );
339    }
340}