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;