1use std::time::Duration;
5
6const DEFAULT_SAMPLE_INTERVAL: Duration = Duration::from_millis(400);
7
8#[derive(Debug, dial9_trace_format::TraceEvent)]
10#[traceevent(wire_slot)]
11#[cfg_attr(not(feature = "unstable-events"), non_exhaustive)]
12pub struct TcpAcceptQueueEvent {
13 #[traceevent(timestamp)]
15 pub timestamp_ns: u64,
16 pub socket_cookie: u64,
18 pub socket_inode: u64,
20 pub ip_version: u8,
22 pub local_addr: String,
24 pub local_port: u16,
26 #[traceevent(kind = "gauge")]
28 pub pending_connections: u32,
29 pub backlog_limit: u32,
31}
32
33#[derive(Debug, Clone, bon::Builder)]
56pub struct SocketAcceptQueuesConfig {
57 #[builder(default = DEFAULT_SAMPLE_INTERVAL)]
59 sample_interval: Duration,
60}
61
62impl Default for SocketAcceptQueuesConfig {
63 fn default() -> Self {
64 Self::builder().build()
65 }
66}
67
68impl SocketAcceptQueuesConfig {
69 pub fn sample_interval(&self) -> Duration {
71 self.sample_interval
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn default_sample_interval_is_400ms() {
81 assert_eq!(
82 SocketAcceptQueuesConfig::default().sample_interval(),
83 Duration::from_millis(400)
84 );
85 }
86
87 #[test]
88 fn pending_connections_is_a_gauge() {
89 use dial9_trace_format::TraceEvent;
90
91 let entry = TcpAcceptQueueEvent::schema_entry();
92 let kind_annotations = entry
93 .annotations()
94 .iter()
95 .filter(|annotation| annotation.key() == "kind")
96 .map(|annotation| {
97 (
98 entry.fields()[annotation.field_index() as usize].name(),
99 annotation.value(),
100 )
101 })
102 .collect::<Vec<_>>();
103
104 assert_eq!(kind_annotations, vec![("pending_connections", "gauge")]);
105 }
106}
107
108#[cfg(target_os = "linux")]
109mod linux {
110 use super::{SocketAcceptQueuesConfig, TcpAcceptQueueEvent};
111 use dial9_core::clock::clock_monotonic_ns;
112 use dial9_core::rate_limited;
113 use dial9_core::source::{FlushContext, Source};
114 use netlink_packet_core::{
115 NLM_F_DUMP, NLM_F_REQUEST, NetlinkBuffer, NetlinkMessage, NetlinkPayload,
116 };
117 use netlink_packet_sock_diag::inet::{ExtensionFlags, InetRequest, SocketId, StateFlags};
118 use netlink_packet_sock_diag::{AF_INET, AF_INET6, IPPROTO_TCP, SockDiagMessage, TCP_LISTEN};
119 use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_SOCK_DIAG};
120 use std::collections::{HashMap, HashSet};
121 use std::fs;
122 use std::io;
123 use std::net::IpAddr;
124 use std::os::fd::AsRawFd;
125 use std::path::{Path, PathBuf};
126 use std::time::{Duration, Instant};
127
128 const NETLINK_RECEIVE_TIMEOUT: Duration = Duration::from_millis(100);
129
130 #[derive(Debug)]
134 pub struct SocketAcceptQueuesSource {
135 config: SocketAcceptQueuesConfig,
136 last_sample: Option<Instant>,
137 cache: SocketAcceptQueueCache,
138 }
139
140 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141 struct SocketKey {
142 inode: u64,
143 cookie: u64,
144 }
145
146 #[derive(Debug, Default)]
147 struct SocketAcceptQueueCache {
148 owned: HashMap<SocketKey, PathBuf>,
149 foreign: HashSet<SocketKey>,
150 }
151
152 #[derive(Debug, Clone, PartialEq, Eq)]
153 struct TcpAcceptQueueSnapshot {
154 key: SocketKey,
155 ip_version: u8,
156 local_addr: IpAddr,
157 local_port: u16,
158 pending_connections: u32,
159 backlog_limit: u32,
160 }
161
162 impl SocketAcceptQueuesSource {
163 pub fn new(config: SocketAcceptQueuesConfig) -> Self {
164 Self {
165 config,
166 last_sample: None,
167 cache: SocketAcceptQueueCache::default(),
168 }
169 }
170 }
171
172 impl SocketAcceptQueueCache {
173 fn prune(&mut self, active_keys: &HashSet<SocketKey>) {
174 self.owned.retain(|key, _| active_keys.contains(key));
175 self.foreign.retain(|key| active_keys.contains(key));
176 }
177 }
178
179 impl Source for SocketAcceptQueuesSource {
180 fn flush(&mut self, ctx: &FlushContext<'_>) {
181 let now = Instant::now();
182 if let Some(last_sample) = self.last_sample
183 && now.duration_since(last_sample) < self.config.sample_interval
184 {
185 return;
186 }
187 self.last_sample = Some(now);
188
189 match collect_socket_accept_queues(&mut self.cache) {
190 Ok(snapshots) => {
191 let timestamp_ns = clock_monotonic_ns();
192 for snapshot in snapshots {
193 ctx.record_event(&snapshot.into_event(timestamp_ns));
194 }
195 }
196 Err(e) => rate_limited!(Duration::from_secs(60), {
197 tracing::warn!("failed to read socket accept queues via sock_diag: {e}");
198 }),
199 }
200 }
201
202 fn name(&self) -> &'static str {
203 "socket_accept_queues"
204 }
205 }
206
207 impl TcpAcceptQueueSnapshot {
208 fn into_event(self, timestamp_ns: u64) -> TcpAcceptQueueEvent {
209 TcpAcceptQueueEvent {
210 timestamp_ns,
211 socket_cookie: self.key.cookie,
212 socket_inode: self.key.inode,
213 ip_version: self.ip_version,
214 local_addr: self.local_addr.to_string(),
215 local_port: self.local_port,
216 pending_connections: self.pending_connections,
217 backlog_limit: self.backlog_limit,
218 }
219 }
220 }
221
222 fn collect_socket_accept_queues(
223 cache: &mut SocketAcceptQueueCache,
224 ) -> io::Result<Vec<TcpAcceptQueueSnapshot>> {
225 let mut socket = Socket::new(NETLINK_SOCK_DIAG)?;
226 set_socket_receive_timeout(&socket, NETLINK_RECEIVE_TIMEOUT)?;
227 let _local_addr = socket.bind_auto()?;
228 let kernel_addr = SocketAddr::new(0, 0);
229 socket.connect(&kernel_addr)?;
230
231 let mut snapshots = Vec::new();
232 dump_tcp_listeners(&socket, AF_INET, 1, &mut snapshots)?;
233 dump_tcp_listeners(&socket, AF_INET6, 2, &mut snapshots)?;
234 classify_process_listeners(
235 snapshots,
236 cache,
237 read_socket_inode_for_fd_path,
238 scan_process_socket_fds,
239 )
240 }
241
242 fn dump_tcp_listeners(
243 socket: &Socket,
244 family: u8,
245 sequence_number: u32,
246 snapshots: &mut Vec<TcpAcceptQueueSnapshot>,
247 ) -> io::Result<()> {
248 let socket_id = match family {
249 AF_INET => SocketId::new_v4(),
250 AF_INET6 => SocketId::new_v6(),
251 _ => {
252 return Err(io::Error::new(
253 io::ErrorKind::InvalidInput,
254 format!("unsupported inet socket family {family}"),
255 ));
256 }
257 };
258
259 let request = InetRequest {
260 family,
261 protocol: IPPROTO_TCP,
262 extensions: ExtensionFlags::empty(),
263 states: StateFlags::LISTEN,
264 socket_id,
265 };
266 let mut message = NetlinkMessage::from(SockDiagMessage::InetRequest(request));
267 message.header.flags = NLM_F_REQUEST | NLM_F_DUMP;
268 message.header.sequence_number = sequence_number;
269 message.finalize();
270
271 let mut request_bytes = vec![0; message.buffer_len()];
272 message.serialize(&mut request_bytes);
273 let sent = socket.send(&request_bytes, 0)?;
274 if sent != request_bytes.len() {
275 return Err(io::Error::new(
276 io::ErrorKind::WriteZero,
277 format!(
278 "short netlink sock_diag request write: sent {sent} of {} bytes",
279 request_bytes.len()
280 ),
281 ));
282 }
283
284 loop {
285 let (response_bytes, _sender) = socket.recv_from_full()?;
286 if response_bytes.is_empty() {
287 return Err(io::Error::new(
288 io::ErrorKind::UnexpectedEof,
289 "sock_diag netlink socket returned an empty response",
290 ));
291 }
292
293 let done = parse_response_datagram(&response_bytes, sequence_number, snapshots)?;
294 if done {
295 return Ok(());
296 }
297 }
298 }
299
300 fn parse_response_datagram(
301 bytes: &[u8],
302 sequence_number: u32,
303 snapshots: &mut Vec<TcpAcceptQueueSnapshot>,
304 ) -> io::Result<bool> {
305 let mut offset = 0;
306 let mut done = false;
307
308 while offset < bytes.len() {
309 let remaining = &bytes[offset..];
310 let packet = NetlinkBuffer::new_checked(&remaining).map_err(decode_error)?;
311 let packet_len = packet.length() as usize;
312 if packet_len == 0 {
313 return Err(io::Error::new(
314 io::ErrorKind::InvalidData,
315 "sock_diag returned zero-length netlink message",
316 ));
317 }
318 let message = NetlinkMessage::<SockDiagMessage>::deserialize(&remaining[..packet_len])
319 .map_err(decode_error)?;
320
321 if message.header.sequence_number == sequence_number {
322 match message.payload {
323 NetlinkPayload::Done(_) => done = true,
324 NetlinkPayload::Error(error) => {
325 if error.code.is_some() {
326 return Err(error.into());
327 }
328 }
329 NetlinkPayload::InnerMessage(SockDiagMessage::InetResponse(response)) => {
330 if let Some(snapshot) = snapshot_from_response(&response) {
331 snapshots.push(snapshot);
332 }
333 }
334 _ => {}
335 }
336 }
337
338 offset += nlmsg_align(packet_len);
339 }
340
341 Ok(done)
342 }
343
344 fn snapshot_from_response(
345 response: &netlink_packet_sock_diag::inet::InetResponse,
346 ) -> Option<TcpAcceptQueueSnapshot> {
347 let header = &response.header;
348 let socket_inode = u64::from(header.inode);
349 if header.state != TCP_LISTEN {
350 return None;
351 }
352
353 Some(TcpAcceptQueueSnapshot {
354 key: SocketKey {
355 inode: socket_inode,
356 cookie: socket_cookie_from_diag(header.socket_id.cookie),
357 },
358 ip_version: match header.family {
359 AF_INET => 4,
360 AF_INET6 => 6,
361 _ => return None,
362 },
363 local_addr: header.socket_id.source_address,
364 local_port: header.socket_id.source_port,
365 pending_connections: header.recv_queue,
366 backlog_limit: header.send_queue,
367 })
368 }
369
370 fn classify_process_listeners(
371 snapshots: Vec<TcpAcceptQueueSnapshot>,
372 cache: &mut SocketAcceptQueueCache,
373 mut read_fd_inode: impl FnMut(&Path) -> io::Result<Option<u64>>,
374 mut scan_socket_fds: impl FnMut(&HashSet<u64>) -> io::Result<HashMap<u64, PathBuf>>,
375 ) -> io::Result<Vec<TcpAcceptQueueSnapshot>> {
376 let active_keys = snapshots
379 .iter()
380 .map(|snapshot| snapshot.key)
381 .collect::<HashSet<_>>();
382 cache.prune(&active_keys);
383
384 let mut owned_snapshots = Vec::new();
385 let mut unknown_snapshots = Vec::new();
386 let mut unknown_inodes = HashSet::new();
387
388 for snapshot in snapshots {
389 let key = snapshot.key;
390
391 if let Some(fd_path) = cache.owned.get(&key) {
392 if read_fd_inode(fd_path)? == Some(key.inode) {
393 owned_snapshots.push(snapshot);
394 continue;
395 }
396 cache.owned.remove(&key);
397 }
398
399 if cache.foreign.contains(&key) {
400 continue;
401 }
402
403 unknown_inodes.insert(key.inode);
404 unknown_snapshots.push(snapshot);
405 }
406
407 if unknown_snapshots.is_empty() {
408 return Ok(owned_snapshots);
409 }
410
411 let owned_fds_by_inode = scan_socket_fds(&unknown_inodes)?;
412 for snapshot in unknown_snapshots {
413 if let Some(fd_path) = owned_fds_by_inode.get(&snapshot.key.inode) {
414 cache.owned.insert(snapshot.key, fd_path.clone());
415 owned_snapshots.push(snapshot);
416 } else {
417 cache.foreign.insert(snapshot.key);
418 }
419 }
420
421 Ok(owned_snapshots)
422 }
423
424 fn set_socket_receive_timeout(socket: &Socket, timeout: Duration) -> io::Result<()> {
425 let timeout = libc::timeval {
426 tv_sec: timeout.as_secs().try_into().map_err(|_| {
427 io::Error::new(
428 io::ErrorKind::InvalidInput,
429 format!("socket receive timeout {timeout:?} exceeds timeval.tv_sec"),
430 )
431 })?,
432 tv_usec: timeout.subsec_micros().into(),
433 };
434 let optlen: libc::socklen_t = std::mem::size_of_val(&timeout).try_into().map_err(|_| {
435 io::Error::new(
436 io::ErrorKind::InvalidInput,
437 "timeval size exceeds libc::socklen_t",
438 )
439 })?;
440
441 let result = unsafe {
445 libc::setsockopt(
446 socket.as_raw_fd(),
447 libc::SOL_SOCKET,
448 libc::SO_RCVTIMEO,
449 &timeout as *const libc::timeval as *const libc::c_void,
450 optlen,
451 )
452 };
453 if result == -1 {
454 return Err(io::Error::last_os_error());
455 }
456 Ok(())
457 }
458
459 fn scan_process_socket_fds(target_inodes: &HashSet<u64>) -> io::Result<HashMap<u64, PathBuf>> {
460 let mut fds_by_inode = HashMap::new();
461 if target_inodes.is_empty() {
462 return Ok(fds_by_inode);
463 }
464
465 for entry in fs::read_dir("/proc/self/fd")? {
466 let entry = entry?;
467 let path = entry.path();
468 if let Some(inode) = read_socket_inode_for_fd_path(&path)?
469 && target_inodes.contains(&inode)
470 {
471 fds_by_inode.entry(inode).or_insert(path);
472 if fds_by_inode.len() == target_inodes.len() {
473 break;
474 }
475 }
476 }
477 Ok(fds_by_inode)
478 }
479
480 fn read_socket_inode_for_fd_path(path: &Path) -> io::Result<Option<u64>> {
481 match fs::read_link(path) {
482 Ok(target) => parse_socket_inode(&target),
483 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
484 Err(e) => Err(e),
485 }
486 }
487
488 fn parse_socket_inode(path: &Path) -> io::Result<Option<u64>> {
489 let Some(text) = path.to_str() else {
490 return Ok(None);
491 };
492 let Some(inode) = text
493 .strip_prefix("socket:[")
494 .and_then(|value| value.strip_suffix(']'))
495 else {
496 return Ok(None);
497 };
498
499 inode.parse::<u64>().map(Some).map_err(|e| {
500 io::Error::new(
501 io::ErrorKind::InvalidData,
502 format!("invalid /proc/self/fd socket inode {inode:?}: {e}"),
503 )
504 })
505 }
506
507 fn socket_cookie_from_diag(bytes: [u8; 8]) -> u64 {
508 let low = u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64;
509 let high = u32::from_ne_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as u64;
510 (high << 32) | low
511 }
512
513 fn decode_error(error: impl std::fmt::Display) -> io::Error {
514 io::Error::new(io::ErrorKind::InvalidData, error.to_string())
515 }
516
517 fn nlmsg_align(len: usize) -> usize {
518 (len + 3) & !3
519 }
520
521 #[cfg(test)]
522 mod tests {
523 use super::*;
524 use std::cell::Cell;
525 use std::collections::HashMap;
526 use std::path::PathBuf;
527
528 #[test]
529 fn parse_socket_inode_extracts_socket_symlink_inode() {
530 assert_eq!(
531 parse_socket_inode(Path::new("socket:[12345]")).unwrap(),
532 Some(12345)
533 );
534 }
535
536 #[test]
537 fn parse_socket_inode_ignores_non_socket_symlink() {
538 assert_eq!(parse_socket_inode(Path::new("/dev/null")).unwrap(), None);
539 }
540
541 #[test]
542 fn cached_owned_listener_does_not_scan_process_fds_again() {
543 let key = SocketKey {
544 inode: 123,
545 cookie: 456,
546 };
547 let snapshot = snapshot_with_key(key);
548 let fd_path = PathBuf::from("/proc/self/fd/7");
549 let mut cache = SocketAcceptQueueCache::default();
550 cache.owned.insert(key, fd_path.clone());
551 let scans = Cell::new(0);
552
553 let snapshots = classify_process_listeners(
554 vec![snapshot.clone()],
555 &mut cache,
556 |path| {
557 assert_eq!(path, fd_path.as_path());
558 Ok(Some(key.inode))
559 },
560 |_| {
561 scans.set(scans.get() + 1);
562 Ok(HashMap::new())
563 },
564 )
565 .unwrap();
566
567 assert_eq!(snapshots, vec![snapshot]);
568 assert_eq!(scans.get(), 0);
569 }
570
571 #[test]
572 fn unknown_listener_scans_once_and_caches_owned_listener() {
573 let key = SocketKey {
574 inode: 123,
575 cookie: 456,
576 };
577 let snapshot = snapshot_with_key(key);
578 let fd_path = PathBuf::from("/proc/self/fd/7");
579 let mut cache = SocketAcceptQueueCache::default();
580 let scans = Cell::new(0);
581
582 let snapshots = classify_process_listeners(
583 vec![snapshot.clone()],
584 &mut cache,
585 |_| panic!("unknown listeners should not validate cached fd paths"),
586 |target_inodes| {
587 scans.set(scans.get() + 1);
588 assert!(target_inodes.contains(&key.inode));
589 Ok(HashMap::from([(key.inode, fd_path.clone())]))
590 },
591 )
592 .unwrap();
593
594 assert_eq!(snapshots, vec![snapshot]);
595 assert_eq!(cache.owned.get(&key), Some(&fd_path));
596 assert_eq!(scans.get(), 1);
597 }
598
599 #[test]
600 fn cached_foreign_listener_does_not_rescan() {
601 let key = SocketKey {
602 inode: 123,
603 cookie: 456,
604 };
605 let mut cache = SocketAcceptQueueCache::default();
606 cache.foreign.insert(key);
607 let scans = Cell::new(0);
608
609 let snapshots = classify_process_listeners(
610 vec![snapshot_with_key(key)],
611 &mut cache,
612 |_| panic!("foreign listeners should not validate fd paths"),
613 |_| {
614 scans.set(scans.get() + 1);
615 Ok(HashMap::new())
616 },
617 )
618 .unwrap();
619
620 assert!(snapshots.is_empty());
621 assert_eq!(scans.get(), 0);
622 }
623
624 #[test]
625 fn inactive_cache_entries_are_pruned_before_classification() {
626 let old_key = SocketKey {
627 inode: 123,
628 cookie: 456,
629 };
630 let new_key = SocketKey {
631 inode: 123,
632 cookie: 789,
633 };
634 let fd_path = PathBuf::from("/proc/self/fd/7");
635 let mut cache = SocketAcceptQueueCache::default();
636 cache.foreign.insert(old_key);
637
638 let snapshots = classify_process_listeners(
639 vec![snapshot_with_key(new_key)],
640 &mut cache,
641 |_| panic!("new listener should be unknown, not cached"),
642 |_| Ok(HashMap::from([(new_key.inode, fd_path.clone())])),
643 )
644 .unwrap();
645
646 assert_eq!(snapshots, vec![snapshot_with_key(new_key)]);
647 assert!(!cache.foreign.contains(&old_key));
648 assert_eq!(cache.owned.get(&new_key), Some(&fd_path));
649 }
650
651 #[test]
652 fn socket_cookie_from_diag_combines_kernel_cookie_words() {
653 let low = 0x5566_7788_u32.to_ne_bytes();
654 let high = 0x1122_3344_u32.to_ne_bytes();
655
656 assert_eq!(
657 socket_cookie_from_diag([
658 low[0], low[1], low[2], low[3], high[0], high[1], high[2], high[3],
659 ]),
660 0x1122_3344_5566_7788
661 );
662 }
663
664 fn snapshot_with_key(key: SocketKey) -> TcpAcceptQueueSnapshot {
665 TcpAcceptQueueSnapshot {
666 key,
667 ip_version: 4,
668 local_addr: "127.0.0.1".parse().unwrap(),
669 local_port: 8080,
670 pending_connections: 1,
671 backlog_limit: 128,
672 }
673 }
674 }
675}
676
677#[cfg(target_os = "linux")]
678pub use linux::SocketAcceptQueuesSource;