Skip to main content

git_policy_cli/push/
tcp_sockets.rs

1//! What:
2//!  The TCP sockets of this machine that are connected, with the bytes each has sent and had acknowledged and the bytes it has
3//!  received, and the sockets a process holds.
4//! Why:
5//!  `rchar` and `wchar` of `/proc/<pid>/io` count `read` and `write`, not `send` and `recv`, so a transfer done through `libcurl`
6//!  (`git-remote-http`) moves no byte the push meter can see, and `/proc/net/tcp` has queue lengths and no running count. The
7//!  kernel's own socket diagnostics (`NETLINK_SOCK_DIAG`, `man 7 sock_diag`, the interface `ss` reads) return, for every
8//!  socket, its state, its inode and a `struct tcp_info` with `tcpi_bytes_acked` and `tcpi_bytes_received`; a socket a process
9//!  holds is the `socket:[<inode>]` link of its `/proc/<pid>/fd` (`man 5 proc_pid_fd`).
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const sockets = establishedSockets(); const mine = socketInodesOf(pid); sockets.filter((socket) => mine.includes(socket.inode));
14//! ```
15
16/// The state number of a connected socket, `TCP_ESTABLISHED` in `include/net/tcp_states.h`.
17pub const ESTABLISHED: u8 = 1;
18
19/// One connected TCP socket as the kernel reports it.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub struct TcpSocket {
22    /// The socket's inode, the number in `socket:[<inode>]`.
23    pub inode: u64,
24    /// The state number (`ESTABLISHED`).
25    pub state: u8,
26    /// Bytes sent that the peer has acknowledged (`tcpi_bytes_acked`).
27    pub bytes_acked: u64,
28    /// Bytes received (`tcpi_bytes_received`).
29    pub bytes_received: u64,
30}
31
32/// The netlink protocol number of the socket diagnostics, `NETLINK_SOCK_DIAG` (`man 7 sock_diag`, `include/uapi/linux/netlink.h`).
33#[cfg(target_os = "linux")]
34const NETLINK_SOCK_DIAG: libc::c_int = 4;
35
36/// The message type that asks for the sockets of one address family, `SOCK_DIAG_BY_FAMILY` (`include/uapi/linux/sock_diag.h`).
37#[cfg(target_os = "linux")]
38const SOCK_DIAG_BY_FAMILY: u16 = 20;
39
40/// The request flags: a request (`NLM_F_REQUEST`, 0x01) for the whole list (`NLM_F_DUMP`, 0x300), `include/uapi/linux/netlink.h`.
41#[cfg(target_os = "linux")]
42const REQUEST_AND_DUMP: u16 = 0x01 | 0x300;
43
44/// The message types that end a reply: `NLMSG_ERROR` and `NLMSG_DONE` (`include/uapi/linux/netlink.h`).
45#[cfg(target_os = "linux")]
46const NLMSG_ERROR: u16 = 2;
47/// The message type that ends a dump.
48#[cfg(target_os = "linux")]
49const NLMSG_DONE: u16 = 3;
50
51/// The attribute that carries a `struct tcp_info`: `INET_DIAG_INFO` (`include/uapi/linux/inet_diag.h`, the third of its enum).
52#[cfg(target_os = "linux")]
53const INET_DIAG_INFO: u16 = 2;
54
55/// The cookie value that means "no particular socket" (`INET_DIAG_NOCOOKIE`, `include/uapi/linux/inet_diag.h`).
56#[cfg(target_os = "linux")]
57const INET_DIAG_NOCOOKIE: u32 = u32::MAX;
58
59/// The length of a `struct nlmsghdr`: length (4), type (2), flags (2), sequence (4), port (4).
60#[cfg(target_os = "linux")]
61const NETLINK_HEADER: usize = 16;
62
63/// The length of a `struct inet_diag_req_v2`: family, protocol, extension and pad (1 each), states (4), and a
64///  `struct inet_diag_sockid` of ports (2 + 2), source and destination (16 + 16), interface (4) and cookie (8).
65#[cfg(target_os = "linux")]
66const REQUEST_BODY: usize = 56;
67
68/// The length of a `struct inet_diag_msg`, the fixed part of each reply (`man 7 sock_diag`, "IPv4 and IPv6 sockets").
69#[cfg(target_os = "linux")]
70const MESSAGE_BODY: usize = 72;
71
72/// Where the state byte sits in an `inet_diag_msg` (`idiag_family` is the first byte).
73#[cfg(target_os = "linux")]
74const STATE_OFFSET: usize = 1;
75
76/// Where `idiag_inode` sits in an `inet_diag_msg`: after the four bytes of family, state, timer and retransmits, the 48 of
77///  the socket id, and `idiag_expires`, `idiag_rqueue`, `idiag_wqueue` and `idiag_uid` of 4 each.
78#[cfg(target_os = "linux")]
79const INODE_OFFSET: usize = 68;
80
81/// The length of an attribute header (`struct rtattr`): length (2) and type (2).
82#[cfg(target_os = "linux")]
83const ATTRIBUTE_HEADER: usize = 4;
84
85/// Where `tcpi_bytes_acked` sits in a `struct tcp_info` (`include/uapi/linux/tcp.h`): eight one-byte fields, then
86///  `tcpi_rto` to `tcpi_rcv_mss` (4 x 4), `tcpi_unacked` to `tcpi_fackets` (5 x 4), the four `tcpi_last_*` times (4 x 4),
87///  `tcpi_pmtu` to `tcpi_reordering` (8 x 4), `tcpi_rcv_rtt`, `tcpi_rcv_space` and `tcpi_total_retrans` (3 x 4), and the two
88///  eight-byte rates, which puts the two eight-byte counters at 120 and 128. A control checks both against bytes it counted.
89#[cfg(target_os = "linux")]
90const BYTES_ACKED_OFFSET: usize = 120;
91
92/// Where `tcpi_bytes_received` sits in a `struct tcp_info`: right after `tcpi_bytes_acked`.
93#[cfg(target_os = "linux")]
94const BYTES_RECEIVED_OFFSET: usize = BYTES_ACKED_OFFSET + 8;
95
96/// How much of a `struct tcp_info` a reply must hold for both counters to be in it (an older kernel's is shorter).
97#[cfg(target_os = "linux")]
98const BYTES_END: usize = BYTES_RECEIVED_OFFSET + 8;
99
100/// How long the kernel may take to answer one read of the diagnostics before the answer is given up (`SO_RCVTIMEO`).
101#[cfg(target_os = "linux")]
102const REPLY_WAIT: libc::timeval = libc::timeval { tv_sec: 2, tv_usec: 0 };
103
104/// How many bytes one read of the reply takes: a reply is cut into messages of this size or smaller.
105#[cfg(target_os = "linux")]
106const READ_BUFFER: usize = 64 * 1024;
107
108/// The address families asked, IPv4 first.
109#[cfg(target_os = "linux")]
110const FAMILIES: [libc::c_int; 2] = [libc::AF_INET, libc::AF_INET6];
111
112/// Read four bytes at `at` as a native-endian number, or nothing where the buffer ends first.
113#[cfg(target_os = "linux")]
114fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
115    let window: &[u8] = bytes.get(at..at + 4)?;
116    return Some(u32::from_ne_bytes([window[0], window[1], window[2], window[3]]));
117}
118
119/// Read two bytes at `at` as a native-endian number, or nothing where the buffer ends first.
120#[cfg(target_os = "linux")]
121fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
122    let window: &[u8] = bytes.get(at..at + 2)?;
123    return Some(u16::from_ne_bytes([window[0], window[1]]));
124}
125
126/// Read eight bytes at `at` as a native-endian number, or nothing where the buffer ends first.
127#[cfg(target_os = "linux")]
128fn u64_at(bytes: &[u8], at: usize) -> Option<u64> {
129    let window: &[u8] = bytes.get(at..at + 8)?;
130    let mut array: [u8; 8] = [0; 8];
131    array.copy_from_slice(window);
132    return Some(u64::from_ne_bytes(array));
133}
134
135/// What:
136///  The request for every connected TCP socket of one address family, with the `struct tcp_info` of each.
137/// Why:
138///   One dump request returns all of them; the extension byte asks for `INET_DIAG_INFO` (bit `INET_DIAG_INFO - 1`) and the
139///  state mask for the connected state only.
140///
141/// In TS you'd write (pseudocode):
142/// ```ts
143/// Buffer.concat([netlinkHeader({ type: SOCK_DIAG_BY_FAMILY, flags: REQUEST_AND_DUMP, sequence }), diagRequest({ family, protocol: TCP, ext: 1 << (INET_DIAG_INFO - 1), states: 1 << ESTABLISHED })])
144/// ```
145#[cfg(target_os = "linux")]
146fn dump_request(family: libc::c_int, sequence: u32) -> Vec<u8> {
147    let mut request: Vec<u8> = Vec::with_capacity(NETLINK_HEADER + REQUEST_BODY);
148    request.extend_from_slice(&u32::try_from(NETLINK_HEADER + REQUEST_BODY).unwrap_or(0).to_ne_bytes());
149    request.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
150    request.extend_from_slice(&REQUEST_AND_DUMP.to_ne_bytes());
151    request.extend_from_slice(&sequence.to_ne_bytes());
152    // The port number of the sender: zero lets the kernel answer to the socket this was sent from.
153    request.extend_from_slice(&0u32.to_ne_bytes());
154    // `sdiag_family`, `sdiag_protocol`, `idiag_ext` and the pad byte.
155    request.push(u8::try_from(family).unwrap_or(0));
156    request.push(u8::try_from(libc::IPPROTO_TCP).unwrap_or(0));
157    request.push(1 << (INET_DIAG_INFO - 1));
158    request.push(0);
159    request.extend_from_slice(&(1u32 << ESTABLISHED).to_ne_bytes());
160    // The socket id is ignored in a dump: ports, addresses and interface zero, the cookie says "none".
161    request.extend_from_slice(&[0; 2 + 2 + 16 + 16 + 4]);
162    request.extend_from_slice(&INET_DIAG_NOCOOKIE.to_ne_bytes());
163    request.extend_from_slice(&INET_DIAG_NOCOOKIE.to_ne_bytes());
164    return request;
165}
166
167/// What:
168///  The sockets in one block of reply, and whether the block ended the dump (or failed it).
169/// Why:
170///   A reply is a run of netlink messages, each a header, an `inet_diag_msg` and attributes; the dump ends with `NLMSG_DONE`.
171///
172/// In TS you'd write (pseudocode):
173/// ```ts
174/// const { sockets, ended } = readReply(bytes);
175/// ```
176#[cfg(target_os = "linux")]
177fn read_reply(block: &[u8], into: &mut Vec<TcpSocket>) -> Result<bool, ()> {
178    let mut at: usize = 0;
179    while at + NETLINK_HEADER <= block.len() {
180        let length: usize = u32_at(block, at).ok_or(())? as usize;
181        let kind: u16 = u16_at(block, at + 4).ok_or(())?;
182        if length < NETLINK_HEADER || at + length > block.len() {
183            return Err(());
184        }
185        if kind == NLMSG_ERROR {
186            return Err(());
187        }
188        if kind == NLMSG_DONE {
189            return Ok(true);
190        }
191        if kind == SOCK_DIAG_BY_FAMILY {
192            let message: &[u8] = &block[at + NETLINK_HEADER..at + length];
193            if let Some(socket) = read_message(message) {
194                into.push(socket);
195            }
196        }
197        // Each message starts on a four-byte boundary (`NLMSG_ALIGN`).
198        at += (length + 3) & !3;
199    }
200    return Ok(false);
201}
202
203/// One `inet_diag_msg` and its attributes: the socket's state and inode, and the byte counters of its `struct tcp_info`.
204#[cfg(target_os = "linux")]
205fn read_message(message: &[u8]) -> Option<TcpSocket> {
206    let state: u8 = *message.get(STATE_OFFSET)?;
207    let inode: u64 = u64::from(u32_at(message, INODE_OFFSET)?);
208    let mut counters: (u64, u64) = (0, 0);
209    let mut at: usize = MESSAGE_BODY;
210    while at + ATTRIBUTE_HEADER <= message.len() {
211        let length: usize = u16_at(message, at)? as usize;
212        let kind: u16 = u16_at(message, at + 2)?;
213        if length < ATTRIBUTE_HEADER || at + length > message.len() {
214            break;
215        }
216        if kind == INET_DIAG_INFO {
217            let info: &[u8] = &message[at + ATTRIBUTE_HEADER..at + length];
218            if info.len() >= BYTES_END {
219                counters = (u64_at(info, BYTES_ACKED_OFFSET)?, u64_at(info, BYTES_RECEIVED_OFFSET)?);
220            }
221        }
222        // Each attribute starts on a four-byte boundary (`RTA_ALIGN`).
223        at += (length + 3) & !3;
224    }
225    return Some(TcpSocket { inode, state, bytes_acked: counters.0, bytes_received: counters.1 });
226}
227
228/// Ask for the sockets of one family on an open diagnostics socket and read the whole dump.
229#[cfg(target_os = "linux")]
230fn dump_family(descriptor: libc::c_int, family: libc::c_int, sequence: u32) -> Option<Vec<TcpSocket>> {
231    let request: Vec<u8> = dump_request(family, sequence);
232    // SAFETY: `send` reads `request.len()` bytes from the buffer, which lives through the call, on a descriptor this module owns.
233    let sent: isize = unsafe { libc::send(descriptor, request.as_ptr().cast(), request.len(), 0) };
234    if sent < 0 {
235        return None;
236    }
237    let mut sockets: Vec<TcpSocket> = Vec::new();
238    let mut buffer: Vec<u8> = vec![0; READ_BUFFER];
239    loop {
240        // SAFETY: `recv` writes at most `buffer.len()` bytes into the buffer, which lives through the call.
241        let received: isize = unsafe { libc::recv(descriptor, buffer.as_mut_ptr().cast(), buffer.len(), 0) };
242        if received <= 0 {
243            return None;
244        }
245        let block: &[u8] = &buffer[..usize::try_from(received).ok()?];
246        if read_reply(block, &mut sockets).ok()? {
247            return Some(sockets);
248        }
249    }
250}
251
252/// Every connected TCP socket of the network namespace of this process, or nothing where the kernel cannot say.
253#[cfg(target_os = "linux")]
254pub fn established_sockets() -> Option<Vec<TcpSocket>> {
255    // SAFETY: `socket` takes plain integers and returns a descriptor or -1; the descriptor is closed below on every path.
256    let descriptor: libc::c_int = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_RAW | libc::SOCK_CLOEXEC, NETLINK_SOCK_DIAG) };
257    if descriptor < 0 {
258        return None;
259    }
260    // A copy of the constant on the stack, because the call needs an address to read from.
261    let patience: libc::timeval = REPLY_WAIT;
262    // SAFETY: `setsockopt` reads `size_of::<timeval>()` bytes from the value, which lives through the call.
263    unsafe {
264        libc::setsockopt(descriptor, libc::SOL_SOCKET, libc::SO_RCVTIMEO, (&raw const patience).cast(), std::mem::size_of::<libc::timeval>() as libc::socklen_t);
265    }
266    let mut all: Option<Vec<TcpSocket>> = None;
267    for (sequence, family) in (1u32..).zip(FAMILIES) {
268        // A family the kernel does not offer (no IPv6) makes no list; the others still answer.
269        if let Some(found) = dump_family(descriptor, family, sequence) {
270            all.get_or_insert_with(Vec::new).extend(found);
271        }
272    }
273    // SAFETY: `close` ends the descriptor opened above, once.
274    unsafe {
275        libc::close(descriptor);
276    }
277    return all;
278}
279
280/// Only Linux reports sockets this way; elsewhere nothing is measured.
281// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
282#[cfg(not(target_os = "linux"))]
283pub fn established_sockets() -> Option<Vec<TcpSocket>> {
284    return None;
285}
286
287/// The inodes of the sockets a process holds open, or nothing where its descriptors cannot be read.
288#[cfg(target_os = "linux")]
289pub fn socket_inodes_of(pid: i32) -> Vec<u64> {
290    let mut inodes: Vec<u64> = Vec::new();
291    let Ok(descriptors) = std::fs::read_dir(format!("/proc/{pid}/fd")) else {
292        return inodes;
293    };
294    for entry in descriptors.flatten() {
295        let Ok(link) = std::fs::read_link(entry.path()) else { continue };
296        let text: String = link.to_string_lossy().into_owned();
297        // A socket's link reads `socket:[<inode>]` (`man 5 proc_pid_fd`).
298        if let Some(number) = text.strip_prefix("socket:[").and_then(|rest| { return rest.strip_suffix(']'); })
299            && let Ok(inode) = number.parse::<u64>()
300        {
301            inodes.push(inode);
302        }
303    }
304    return inodes;
305}
306
307/// Only Linux lists the sockets of a process; elsewhere none is found.
308// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
309#[cfg(not(target_os = "linux"))]
310pub fn socket_inodes_of(_pid: i32) -> Vec<u64> {
311    return Vec::new();
312}
313
314/// Controls for what the kernel reports.
315#[cfg(test)]
316#[path = "tcp_sockets_tests.rs"]
317mod tests;