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