Skip to main content

sozu_command_lib/
scm_socket.rs

1//! SCM_RIGHTS socket for FD passing between master and workers.
2//!
3//! Wraps a `SeqPacket` unix socket and uses the `nix` SCM_RIGHTS helpers
4//! to ship listener/accept FDs across the master ↔ worker boundary at
5//! startup and across hot upgrades. The borrowed-FD wrappers
6//! (`set_blocking`) hold an FD without taking ownership; the listener
7//! teardown paths intentionally take ownership through
8//! `TcpListener::from_raw_fd` so the FD is closed by drop.
9
10use std::{
11    io::{IoSlice, IoSliceMut},
12    os::unix::{
13        io::{FromRawFd, IntoRawFd, RawFd},
14        net::UnixStream as StdUnixStream,
15    },
16};
17
18use mio::net::{TcpListener, UdpSocket};
19use nix::{cmsg_space, sys::socket};
20use prost::{DecodeError, Message};
21
22use crate::{
23    listener_key::{ListenerKey, ListenerKeyError},
24    proto::command::ListenersCount,
25};
26
27pub const MAX_FDS_OUT: usize = 200;
28pub const MAX_BYTES_OUT: usize = 4096;
29
30#[derive(thiserror::Error, Debug)]
31pub enum ScmSocketError {
32    #[error("could not set the blocking status of the unix stream to {blocking}: {error}")]
33    SetBlocking {
34        blocking: bool,
35        error: std::io::Error,
36    },
37    #[error("could not send message for SCM socket: {0}")]
38    Send(String),
39    #[error("could not receive message for SCM socket: {0}")]
40    Receive(String),
41    #[error("invalid char set: {0}")]
42    InvalidCharSet(String),
43    #[error("Could not deserialize utf8 string into listeners: {0}")]
44    ListenerParse(String),
45    #[error("Wrong listener key {address}: {error}")]
46    WrongSocketAddress {
47        address: String,
48        error: ListenerKeyError,
49    },
50    #[error("error decoding the protobuf format of the listeners: {0}")]
51    DecodeError(DecodeError),
52    #[error(
53        "listeners count manifest is inconsistent with the SCM payload: \
54         http={http}, tls={tls}, tcp={tcp} (sum={total}), fds_received={fds_received}, max_fds={max_fds}"
55    )]
56    ListenersCountInconsistent {
57        http: usize,
58        tls: usize,
59        tcp: usize,
60        total: usize,
61        fds_received: usize,
62        max_fds: usize,
63    },
64}
65
66/// A unix socket specialized for file descriptor passing
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct ScmSocket {
69    pub fd: RawFd,
70    pub blocking: bool,
71}
72
73impl ScmSocket {
74    /// Create a blocking SCM socket from a raw file descriptor (unsafe)
75    pub fn new(fd: RawFd) -> Result<Self, ScmSocketError> {
76        // SAFETY: `fd` is borrowed for the duration of this block. We wrap it
77        // in a `StdUnixStream` to call `set_nonblocking`, then immediately
78        // release ownership again with `into_raw_fd` so the descriptor is
79        // not closed by `Drop`. The caller retains ownership of `fd`.
80        unsafe {
81            let stream = StdUnixStream::from_raw_fd(fd);
82            stream
83                .set_nonblocking(false)
84                .map_err(|error| ScmSocketError::SetBlocking {
85                    blocking: false,
86                    error,
87                })?;
88            let _dropped_fd = stream.into_raw_fd();
89        }
90
91        Ok(ScmSocket { fd, blocking: true })
92    }
93
94    /// Get the raw file descriptor of the scm channel
95    pub fn raw_fd(&self) -> i32 {
96        self.fd
97    }
98
99    /// Use the standard library (unsafe) to set the socket to blocking / unblocking
100    pub fn set_blocking(&mut self, blocking: bool) -> Result<(), ScmSocketError> {
101        if self.blocking == blocking {
102            return Ok(());
103        }
104        // Past the idempotent early-return the flag must actually be flipping.
105        debug_assert_ne!(
106            self.blocking, blocking,
107            "set_blocking only reaches the syscall when the state actually changes"
108        );
109        let blocking_before = self.blocking;
110        // SAFETY: `self.fd` is borrowed for the duration of this block. We wrap
111        // it in a `StdUnixStream` to call `set_nonblocking`, then immediately
112        // release ownership with `into_raw_fd` so the descriptor is not closed
113        // by `Drop`. `ScmSocket` retains the original ownership.
114        unsafe {
115            let stream = StdUnixStream::from_raw_fd(self.fd);
116            stream
117                .set_nonblocking(!blocking)
118                .map_err(|error| ScmSocketError::SetBlocking { blocking, error })?;
119            let _dropped_fd = stream.into_raw_fd();
120        }
121        self.blocking = blocking;
122        // The flag landed on the requested value and genuinely toggled.
123        debug_assert_eq!(
124            self.blocking, blocking,
125            "blocking flag must reflect the requested state after a successful set"
126        );
127        debug_assert_ne!(
128            self.blocking, blocking_before,
129            "blocking flag must have toggled across a real state change"
130        );
131        Ok(())
132    }
133
134    /// Send listeners (socket addresses and file descriptors) via an scm socket
135    pub fn send_listeners(&self, listeners: &Listeners) -> Result<(), ScmSocketError> {
136        let listeners_count = ListenersCount {
137            http: listeners.http.iter().map(|t| t.0.to_string()).collect(),
138            tls: listeners.tls.iter().map(|t| t.0.to_string()).collect(),
139            tcp: listeners.tcp.iter().map(|t| t.0.to_string()).collect(),
140            udp: listeners.udp.iter().map(|t| t.0.to_string()).collect(),
141        };
142
143        // The manifest is built 1:1 from the listener tables; each address slot
144        // ships exactly one FD, so the per-protocol counts must agree on both
145        // sides of the wire. The receiver reconstructs (address, fd) pairs by
146        // zipping these counts against the FD array — drift here is the exact
147        // bug class `command_channel_security_tests` guards.
148        debug_assert_eq!(
149            listeners_count.http.len(),
150            listeners.http.len(),
151            "http manifest count must match the http listener table"
152        );
153        debug_assert_eq!(
154            listeners_count.tls.len(),
155            listeners.tls.len(),
156            "tls manifest count must match the tls listener table"
157        );
158        debug_assert_eq!(
159            listeners_count.tcp.len(),
160            listeners.tcp.len(),
161            "tcp manifest count must match the tcp listener table"
162        );
163
164        let message = listeners_count.encode_length_delimited_to_vec();
165
166        let mut file_descriptors: Vec<RawFd> = Vec::new();
167
168        file_descriptors.extend(listeners.http.iter().map(|t| t.1));
169        file_descriptors.extend(listeners.tls.iter().map(|t| t.1));
170        file_descriptors.extend(listeners.tcp.iter().map(|t| t.1));
171        file_descriptors.extend(listeners.udp.iter().map(|t| t.1));
172
173        // The FD vector must reconcile with the address totals: one descriptor
174        // per listener, folded http+tls+tcp+udp. If these disagree the receiver
175        // would zip mismatched (address, fd) pairs.
176        let address_total =
177            listeners.http.len() + listeners.tls.len() + listeners.tcp.len() + listeners.udp.len();
178        debug_assert_eq!(
179            file_descriptors.len(),
180            address_total,
181            "the FD count sent must equal the total listener-address count (one FD per address)"
182        );
183
184        self.send_msg_and_fds(&message, &file_descriptors)
185    }
186
187    /// Receive and parse listeners (socket addresses and file descriptors) via an scm socket
188    pub fn receive_listeners(&self) -> Result<Listeners, ScmSocketError> {
189        let mut buf = vec![0; MAX_BYTES_OUT];
190
191        let mut received_fds: [RawFd; MAX_FDS_OUT] = [0; MAX_FDS_OUT];
192
193        let (size, file_descriptor_length) =
194            self.receive_msg_and_fds(&mut buf, &mut received_fds)?;
195
196        debug!("{} received :{:?}", self.fd, (size, file_descriptor_length));
197
198        let listeners_count = ListenersCount::decode_length_delimited(&buf[..size])
199            .map_err(ScmSocketError::DecodeError)?;
200
201        // Validate the manifest before indexing into the fixed-size FD array.
202        // The peer-controlled `listeners_count.{http,tls,tcp,udp}` lists are
203        // matched 1:1 with `received_fds` slots; without these bounds checks
204        // a peer that declared more entries than MAX_FDS_OUT or more entries
205        // than FDs actually arrived would panic the worker on
206        // `received_fds[index..index + len]`. `udp` is folded into the total
207        // and the inconsistency-error `tcp` slot for diagnostics rather than
208        // widening the error variant.
209        let http_len = listeners_count.http.len();
210        let tls_len = listeners_count.tls.len();
211        let tcp_len = listeners_count.tcp.len();
212        let udp_len = listeners_count.udp.len();
213        let total = http_len
214            .checked_add(tls_len)
215            .and_then(|s| s.checked_add(tcp_len))
216            .and_then(|s| s.checked_add(udp_len))
217            .ok_or(ScmSocketError::ListenersCountInconsistent {
218                http: http_len,
219                tls: tls_len,
220                tcp: tcp_len.saturating_add(udp_len),
221                total: usize::MAX,
222                fds_received: file_descriptor_length,
223                max_fds: MAX_FDS_OUT,
224            })?;
225        if total > MAX_FDS_OUT || total > file_descriptor_length {
226            return Err(ScmSocketError::ListenersCountInconsistent {
227                http: http_len,
228                tls: tls_len,
229                tcp: tcp_len.saturating_add(udp_len),
230                total,
231                fds_received: file_descriptor_length,
232                max_fds: MAX_FDS_OUT,
233            });
234        }
235
236        // Past the consistency guard, the folded total reconciles with both the
237        // fixed FD-array bound and the number of FDs that actually arrived.
238        // These are the invariants that keep every `received_fds[index..index+len]`
239        // slice below in bounds — a malformed manifest already returned an error
240        // and never reaches here.
241        debug_assert_eq!(
242            total,
243            http_len + tls_len + tcp_len + udp_len,
244            "folded total must equal the sum of per-protocol counts"
245        );
246        debug_assert!(
247            total <= MAX_FDS_OUT,
248            "total FD slots must fit the fixed-size received_fds array before indexing"
249        );
250        debug_assert!(
251            total <= file_descriptor_length,
252            "manifest total must not exceed the FDs actually received"
253        );
254        debug_assert!(
255            total <= received_fds.len(),
256            "every (address, fd) zip below must stay within the received_fds array"
257        );
258
259        let mut http_addresses = parse_addresses(&listeners_count.http)?;
260        let mut tls_addresses = parse_addresses(&listeners_count.tls)?;
261        let mut tcp_addresses = parse_addresses(&listeners_count.tcp)?;
262        let mut udp_addresses = parse_addresses(&listeners_count.udp)?;
263
264        // Each parsed address list maps 1:1 onto a contiguous FD slice; the
265        // counts must survive `parse_addresses` unchanged.
266        debug_assert_eq!(
267            http_addresses.len(),
268            http_len,
269            "parsed http address count must match the manifest count"
270        );
271        debug_assert_eq!(
272            tls_addresses.len(),
273            tls_len,
274            "parsed tls address count must match the manifest count"
275        );
276        debug_assert_eq!(
277            tcp_addresses.len(),
278            tcp_len,
279            "parsed tcp address count must match the manifest count"
280        );
281
282        let mut index = 0;
283        let len = http_len;
284        // Each FD slice end must stay within the validated total (and thus the
285        // array); pair-assert the slice window before every zip.
286        debug_assert!(
287            index + len <= total,
288            "http FD slice must lie within the reconciled total"
289        );
290        let mut http = Vec::new();
291        http.extend(
292            http_addresses
293                .drain(..)
294                .zip(received_fds[index..index + len].iter().cloned()),
295        );
296        // Each address was wrapped with exactly one FD.
297        debug_assert_eq!(
298            http.len(),
299            http_len,
300            "every http address must be paired with exactly one FD"
301        );
302
303        index += len;
304        let len = tls_len;
305        debug_assert!(
306            index + len <= total,
307            "tls FD slice must lie within the reconciled total"
308        );
309        let mut tls = Vec::new();
310        tls.extend(
311            tls_addresses
312                .drain(..)
313                .zip(received_fds[index..index + len].iter().cloned()),
314        );
315        debug_assert_eq!(
316            tls.len(),
317            tls_len,
318            "every tls address must be paired with exactly one FD"
319        );
320
321        index += len;
322        let len = tcp_len;
323        debug_assert!(
324            index + len <= total,
325            "tcp FD slice must lie within the reconciled total"
326        );
327        let mut tcp = Vec::new();
328        tcp.extend(
329            tcp_addresses
330                .drain(..)
331                .zip(received_fds[index..index + len].iter().cloned()),
332        );
333        debug_assert_eq!(
334            tcp.len(),
335            tcp_len,
336            "every tcp address must be paired with exactly one FD"
337        );
338
339        index += len;
340        let len = udp_len;
341        let mut udp = Vec::new();
342        udp.extend(
343            udp_addresses
344                .drain(..)
345                .zip(received_fds[index..index + len].iter().cloned()),
346        );
347        debug_assert_eq!(
348            udp.len(),
349            udp_len,
350            "every udp address must be paired with exactly one FD"
351        );
352
353        // The reconstructed tables consume every FD slot the manifest declared:
354        // the final cursor lands exactly on the folded total (http+tls+tcp+udp).
355        debug_assert_eq!(
356            index + len,
357            total,
358            "the (address, fd) zips must consume exactly the reconciled total of FD slots"
359        );
360        debug_assert_eq!(
361            http.len() + tls.len() + tcp.len() + udp.len(),
362            total,
363            "reconstructed listener count must equal the reconciled FD total"
364        );
365
366        Ok(Listeners {
367            http,
368            tls,
369            tcp,
370            udp,
371        })
372    }
373
374    /// Sends message and file descriptors separately. The file descriptors are summed up
375    /// in a ControlMessage.
376    fn send_msg_and_fds(&self, message: &[u8], fds: &[RawFd]) -> Result<(), ScmSocketError> {
377        let iov = [IoSlice::new(message)];
378        let flags = if self.blocking {
379            socket::MsgFlags::empty()
380        } else {
381            socket::MsgFlags::MSG_DONTWAIT
382        };
383
384        if fds.is_empty() {
385            debug!("{} send empty", self.fd);
386            socket::sendmsg::<()>(self.fd, &iov, &[], flags, None)
387                .map_err(|error| ScmSocketError::Send(error.to_string()))?;
388            return Ok(());
389        };
390
391        let control_message = [socket::ControlMessage::ScmRights(fds)];
392        debug!("{} send with data", self.fd);
393        socket::sendmsg::<()>(self.fd, &iov, &control_message, flags, None)
394            .map_err(|error| ScmSocketError::Send(error.to_string()))?;
395        Ok(())
396    }
397
398    /// Parse the message and receives file descriptors separately via the ControlMessage
399    fn receive_msg_and_fds(
400        &self,
401        message: &mut [u8],
402        fds: &mut [RawFd],
403    ) -> Result<(usize, usize), ScmSocketError> {
404        // Snapshot the buffer length before `message` is borrowed mutably by
405        // `iov`; the received byte count is asserted against it below.
406        let message_capacity = message.len();
407        let mut cmsg = cmsg_space!([RawFd; MAX_FDS_OUT]);
408        let mut iov = [IoSliceMut::new(message)];
409
410        let flags = if self.blocking {
411            socket::MsgFlags::empty()
412        } else {
413            socket::MsgFlags::MSG_DONTWAIT
414        };
415
416        // Received descriptors are close-on-exec. The main process forks and
417        // execs a new worker while it holds the listeners it just received
418        // from the retiring one; without the flag the new worker inherits a
419        // second, untracked copy of every listener, which keeps the socket
420        // listening after the worker deactivates it. The paths that do hand a
421        // descriptor across `exec` clear the flag explicitly on that
422        // descriptor (`util::disable_close_on_exec` in `bin/`), and none of
423        // them hands over a received listener. `MSG_CMSG_CLOEXEC` sets the
424        // flag atomically with the receipt; where it does not exist, the flag
425        // is set right after, which leaves a window for a concurrent `fork`.
426        #[cfg(any(
427            target_os = "linux",
428            target_os = "android",
429            target_os = "freebsd",
430            target_os = "dragonfly",
431            target_os = "netbsd",
432            target_os = "openbsd"
433        ))]
434        let flags = flags | socket::MsgFlags::MSG_CMSG_CLOEXEC;
435
436        let msg = socket::recvmsg::<()>(self.fd, &mut iov[..], Some(&mut cmsg), flags)
437            .map_err(|error| ScmSocketError::Receive(error.to_string()))?;
438
439        // The destination slice is the receiver's fixed `[RawFd; MAX_FDS_OUT]`
440        // array; the zip below cannot write past it.
441        let fds_capacity = fds.len();
442        debug_assert!(
443            fds_capacity <= MAX_FDS_OUT,
444            "destination FD slice must not exceed the MAX_FDS_OUT cmsg space"
445        );
446        let mut fd_count = 0;
447        let received_fds = msg
448            .cmsgs()
449            .map_err(|error| ScmSocketError::Receive(error.to_string()))?
450            .filter_map(|cmsg| {
451                if let socket::ControlMessageOwned::ScmRights(s) = cmsg {
452                    Some(s)
453                } else {
454                    None
455                }
456            })
457            .flatten();
458        for (fd, place) in received_fds.zip(fds.iter_mut()) {
459            fd_count += 1;
460            *place = fd;
461            #[cfg(not(any(
462                target_os = "linux",
463                target_os = "android",
464                target_os = "freebsd",
465                target_os = "dragonfly",
466                target_os = "netbsd",
467                target_os = "openbsd"
468            )))]
469            // SAFETY: `fd` was just installed in this process by `recvmsg`
470            // and is owned by the caller's table; `F_SETFD` only changes its
471            // descriptor flags.
472            unsafe {
473                libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
474            }
475            // The zip is bounded by `fds.iter_mut()`, so each wrap stays within
476            // the destination array — never write past `fds_capacity`.
477            debug_assert!(
478                fd_count <= fds_capacity,
479                "received FD count must never exceed the destination array capacity"
480            );
481        }
482        // Post-condition: the reported count reconciles with the destination
483        // bound and the byte count is within the message buffer we handed in.
484        debug_assert!(
485            fd_count <= fds_capacity,
486            "final received FD count must fit the destination array"
487        );
488        debug_assert!(
489            msg.bytes <= message_capacity,
490            "received byte count must not exceed the message buffer it was read into"
491        );
492        Ok((msg.bytes, fd_count))
493    }
494}
495
496/// Listener keys (socket address, plus interface when bound) and file
497/// descriptors of listening sockets, needed by a
498/// Proxy to start listening. The transport is fd-type-agnostic: `udp` carries
499/// `UdpSocket` fds, the others carry `TcpListener` fds.
500#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
501pub struct Listeners {
502    pub http: Vec<(ListenerKey, RawFd)>,
503    pub tls: Vec<(ListenerKey, RawFd)>,
504    pub tcp: Vec<(ListenerKey, RawFd)>,
505    #[serde(default)]
506    pub udp: Vec<(ListenerKey, RawFd)>,
507}
508
509impl Listeners {
510    pub fn get_http(&mut self, addr: &ListenerKey) -> Option<RawFd> {
511        let before = self.http.len();
512        let pos = self.http.iter().position(|(front, _)| front == addr);
513        let result = pos.map(|pos| self.http.remove(pos).1);
514        // Exactly the matched entry is removed: the length drops by one iff a
515        // match was found, and the removed address is truly gone.
516        debug_assert_eq!(
517            self.http.len(),
518            before - result.is_some() as usize,
519            "http listener table shrinks by exactly one iff an address matched"
520        );
521        debug_assert!(
522            result.is_none() || !self.http.iter().any(|(front, _)| front == addr),
523            "the matched http address must no longer be present after removal"
524        );
525        result
526    }
527
528    pub fn get_https(&mut self, addr: &ListenerKey) -> Option<RawFd> {
529        let before = self.tls.len();
530        let pos = self.tls.iter().position(|(front, _)| front == addr);
531        let result = pos.map(|pos| self.tls.remove(pos).1);
532        debug_assert_eq!(
533            self.tls.len(),
534            before - result.is_some() as usize,
535            "tls listener table shrinks by exactly one iff an address matched"
536        );
537        debug_assert!(
538            result.is_none() || !self.tls.iter().any(|(front, _)| front == addr),
539            "the matched tls address must no longer be present after removal"
540        );
541        result
542    }
543
544    pub fn get_tcp(&mut self, addr: &ListenerKey) -> Option<RawFd> {
545        let before = self.tcp.len();
546        let pos = self.tcp.iter().position(|(front, _)| front == addr);
547        let result = pos.map(|pos| self.tcp.remove(pos).1);
548        debug_assert_eq!(
549            self.tcp.len(),
550            before - result.is_some() as usize,
551            "tcp listener table shrinks by exactly one iff an address matched"
552        );
553        debug_assert!(
554            result.is_none() || !self.tcp.iter().any(|(front, _)| front == addr),
555            "the matched tcp address must no longer be present after removal"
556        );
557        result
558    }
559
560    pub fn get_udp(&mut self, addr: &ListenerKey) -> Option<RawFd> {
561        let before = self.udp.len();
562        let pos = self.udp.iter().position(|(front, _)| front == addr);
563        let result = pos.map(|pos| self.udp.remove(pos).1);
564        debug_assert_eq!(
565            self.udp.len(),
566            before - result.is_some() as usize,
567            "udp listener table shrinks by exactly one iff an address matched"
568        );
569        debug_assert!(
570            result.is_none() || !self.udp.iter().any(|(front, _)| front == addr),
571            "the matched udp address must no longer be present after removal"
572        );
573        result
574    }
575
576    /// Deactivate all listeners by closing their file descriptors
577    pub fn close(&self) {
578        for (_, fd) in &self.http {
579            // SAFETY: `*fd` is owned by this `ScmListeners` table and is
580            // about to be closed by the binding's `Drop` (intentional
581            // close-by-drop). No other reference to the descriptor survives.
582            unsafe {
583                let _ = TcpListener::from_raw_fd(*fd);
584            }
585        }
586
587        for (_, fd) in &self.tls {
588            // SAFETY: `*fd` is owned by this `ScmListeners` table and is
589            // about to be closed by the binding's `Drop` (intentional
590            // close-by-drop). No other reference to the descriptor survives.
591            unsafe {
592                let _ = TcpListener::from_raw_fd(*fd);
593            }
594        }
595
596        for (_, fd) in &self.tcp {
597            // SAFETY: `*fd` is owned by this `ScmListeners` table and is
598            // about to be closed by the binding's `Drop` (intentional
599            // close-by-drop). No other reference to the descriptor survives.
600            unsafe {
601                let _ = TcpListener::from_raw_fd(*fd);
602            }
603        }
604
605        for (_, fd) in &self.udp {
606            // SAFETY: `*fd` is owned by this `ScmListeners` table and is
607            // about to be closed by the binding's `Drop` (intentional
608            // close-by-drop). No other reference to the descriptor survives.
609            // UDP listeners are `UdpSocket` fds, so take ownership through the
610            // matching wrapper.
611            unsafe {
612                let _ = UdpSocket::from_raw_fd(*fd);
613            }
614        }
615    }
616}
617
618/// Parse the manifest's listener keys: the socket address, followed by
619/// `%<interface>` for a listener bound to a network interface. A manifest from
620/// a Sōzu that predates interfaces carries bare addresses, which parse as keys
621/// without an interface.
622fn parse_addresses(addresses: &[String]) -> Result<Vec<ListenerKey>, ScmSocketError> {
623    let mut parsed_addresses = Vec::new();
624    for address in addresses {
625        parsed_addresses.push(address.parse::<ListenerKey>().map_err(|error| {
626            ScmSocketError::WrongSocketAddress {
627                address: address.to_owned(),
628                error,
629            }
630        })?);
631    }
632    Ok(parsed_addresses)
633}
634
635#[cfg(test)]
636mod tests {
637
638    use std::{net::SocketAddr, os::unix::prelude::AsRawFd, str::FromStr};
639
640    use mio::net::UnixStream as MioUnixStream;
641
642    use super::*;
643
644    #[test]
645    fn create_block_unblock_an_scm_socket() {
646        let (nonblocking_stream, _) =
647            MioUnixStream::pair().expect("Could not create a pair of unix streams");
648        let raw_file_descriptor = nonblocking_stream.into_raw_fd();
649
650        let scm_socket = ScmSocket::new(raw_file_descriptor);
651        assert!(scm_socket.is_ok());
652
653        let mut scm_socket = scm_socket.unwrap();
654
655        assert!(scm_socket.set_blocking(true).is_ok());
656        assert!(scm_socket.set_blocking(false).is_ok());
657    }
658
659    fn socket_addr_from_str(str: &str) -> SocketAddr {
660        SocketAddr::from_str(str)
661            .unwrap_or_else(|_| panic!("failed to create socket address from string slice {str}"))
662    }
663
664    /// A descriptor received over the SCM socket must be close-on-exec.
665    ///
666    /// Without `MSG_CMSG_CLOEXEC` the kernel installs it without
667    /// `FD_CLOEXEC`, so every process the receiver later spawns with `exec`
668    /// inherits the listening socket: the main process forks and execs a new
669    /// worker while it holds the listeners it just received from the retiring
670    /// one, and that worker ends up holding each listener twice, once through
671    /// an fd it never tracks. When the worker later deactivates the listener,
672    /// the untracked copy keeps it listening.
673    #[test]
674    fn received_listener_descriptors_are_close_on_exec() {
675        let (stream_1, stream_2) =
676            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
677        let sending_scm_socket =
678            ScmSocket::new(stream_1.into_raw_fd()).expect("Could not create scm socket");
679        let receiving_scm_socket =
680            ScmSocket::new(stream_2.into_raw_fd()).expect("Could not create scm socket");
681
682        let tcp_listener =
683            std::net::TcpListener::bind("127.0.0.1:0").expect("Could not bind a TCP listener");
684        let udp_socket =
685            std::net::UdpSocket::bind("127.0.0.1:0").expect("Could not bind a UDP socket");
686        let listeners = Listeners {
687            http: vec![],
688            tls: vec![],
689            tcp: vec![(
690                tcp_listener.local_addr().expect("TCP local address").into(),
691                tcp_listener.as_raw_fd(),
692            )],
693            udp: vec![(
694                udp_socket.local_addr().expect("UDP local address").into(),
695                udp_socket.as_raw_fd(),
696            )],
697        };
698        sending_scm_socket
699            .send_listeners(&listeners)
700            .expect("Could not send listeners");
701
702        let received = receiving_scm_socket
703            .receive_listeners()
704            .expect("Could not receive listeners");
705        let received_fds: Vec<RawFd> = received
706            .tcp
707            .iter()
708            .chain(received.udp.iter())
709            .map(|(_, fd)| *fd)
710            .collect();
711        assert_eq!(received_fds.len(), 2, "both descriptors must arrive");
712        for fd in received_fds {
713            // SAFETY: `fd` is a descriptor this test just received and still
714            // owns; `F_GETFD` only reads its descriptor flags.
715            let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
716            assert!(flags >= 0, "F_GETFD failed on received fd {fd}");
717            assert_ne!(
718                flags & libc::FD_CLOEXEC,
719                0,
720                "received fd {fd} must be close-on-exec so no exec'd child inherits it"
721            );
722        }
723        received.close();
724    }
725
726    #[test]
727    fn send_and_receive_empty_listeners() {
728        let (stream_1, stream_2) =
729            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
730
731        let sending_scm_socket =
732            ScmSocket::new(stream_1.into_raw_fd()).expect("Could not create scm socket");
733
734        let receiving_scm_socket =
735            ScmSocket::new(stream_2.as_raw_fd()).expect("Could not create scm socket");
736
737        let listeners = Listeners::default();
738
739        sending_scm_socket
740            .send_listeners(&listeners)
741            .expect("Could not send listeners");
742
743        let received_listeners = receiving_scm_socket
744            .receive_listeners()
745            .expect("Could not receive listeners");
746
747        assert_eq!(listeners, received_listeners);
748    }
749
750    /// The SCM hand-off (worker upgrade, main upgrade) tells listeners apart by
751    /// interface: two sockets on one address, bound to different interfaces,
752    /// cross the wire under their own keys and each is handed back only to the
753    /// listener with that exact (address, interface).
754    #[test]
755    fn send_and_receive_listeners_keyed_by_interface() {
756        let (stream_1, stream_2) =
757            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
758        let sending_scm_socket =
759            ScmSocket::new(stream_1.into_raw_fd()).expect("Could not create scm socket");
760        let receiving_scm_socket =
761            ScmSocket::new(stream_2.into_raw_fd()).expect("Could not create scm socket");
762
763        let (bare, wg0) =
764            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
765        let (lo, udp) = MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
766
767        let address = socket_addr_from_str("0.0.0.0:8443");
768        let bare_key = ListenerKey::from(address);
769        let wg0_key = ListenerKey::new(address, Some("wg0"));
770        let lo_key = ListenerKey::new(address, Some("lo"));
771        let listeners = Listeners {
772            http: vec![
773                (bare_key.clone(), bare.as_raw_fd()),
774                (wg0_key.clone(), wg0.as_raw_fd()),
775                (lo_key.clone(), lo.as_raw_fd()),
776            ],
777            udp: vec![(
778                ListenerKey::new(socket_addr_from_str("[::]:5353"), Some("wg0")),
779                udp.as_raw_fd(),
780            )],
781            ..Default::default()
782        };
783
784        sending_scm_socket
785            .send_listeners(&listeners)
786            .expect("Could not send listeners");
787        let mut received = receiving_scm_socket
788            .receive_listeners()
789            .expect("Could not receive listeners");
790
791        let keys: Vec<&ListenerKey> = received.http.iter().map(|(key, _)| key).collect();
792        assert_eq!(keys, vec![&bare_key, &wg0_key, &lo_key]);
793        assert_eq!(received.udp[0].0, listeners.udp[0].0);
794
795        let wg0_fd = received.get_http(&wg0_key).expect("the wg0 socket");
796        assert_eq!(received.http[1].0, lo_key, "only the wg0 entry is taken");
797        assert!(received.get_http(&wg0_key).is_none());
798        assert!(
799            received
800                .get_http(&ListenerKey::new(address, Some("eth0")))
801                .is_none(),
802            "an unknown interface on a known address matches nothing"
803        );
804        let bare_fd = received.get_http(&bare_key).expect("the bare socket");
805        assert_ne!(wg0_fd, bare_fd);
806    }
807
808    #[test]
809    fn send_and_receive_socket_addresses() {
810        let (stream_1, stream_2) =
811            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
812
813        println!("unix stream pair: {stream_1:?} and {stream_2:?}");
814        let sending_scm_socket =
815            ScmSocket::new(stream_1.into_raw_fd()).expect("Could not create scm socket");
816
817        println!("sending socket: {sending_scm_socket:?}");
818
819        let receiving_scm_socket =
820            ScmSocket::new(stream_2.into_raw_fd()).expect("Could not create scm socket");
821
822        println!("receiving socket: {receiving_scm_socket:?}");
823
824        // We have to provide actual file descriptors, even if they will all be changed in the takeover
825        let (http_socket1, http_socket2) =
826            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
827        let (tcp_socket1, tcp_socket2) =
828            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
829        let (tls_socket1, tls_socket2) =
830            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
831        let (udp_socket1, udp_socket2) =
832            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
833
834        let listeners = Listeners {
835            http: vec![
836                (
837                    socket_addr_from_str("127.0.1.1:8080").into(),
838                    http_socket1.as_raw_fd(),
839                ),
840                (
841                    socket_addr_from_str("127.0.1.2:8080").into(),
842                    http_socket2.as_raw_fd(),
843                ),
844            ],
845            tcp: vec![
846                (
847                    socket_addr_from_str("127.0.2.1:8080").into(),
848                    tcp_socket1.as_raw_fd(),
849                ),
850                (
851                    socket_addr_from_str("127.0.2.2:8080").into(),
852                    tcp_socket2.as_raw_fd(),
853                ),
854            ],
855            tls: vec![
856                (
857                    socket_addr_from_str("127.0.3.1:8443").into(),
858                    tls_socket1.as_raw_fd(),
859                ),
860                (
861                    socket_addr_from_str("127.0.3.2:8443").into(),
862                    tls_socket2.as_raw_fd(),
863                ),
864            ],
865            udp: vec![
866                (
867                    socket_addr_from_str("127.0.4.1:5353").into(),
868                    udp_socket1.as_raw_fd(),
869                ),
870                (
871                    socket_addr_from_str("127.0.4.2:5353").into(),
872                    udp_socket2.as_raw_fd(),
873                ),
874            ],
875        };
876
877        println!("self.fd: {}", sending_scm_socket.fd);
878        println!("listeners to send: {listeners:#?}");
879
880        sending_scm_socket
881            .send_listeners(&listeners)
882            .expect("Could not send listeners");
883
884        let received_listeners = receiving_scm_socket
885            .receive_listeners()
886            .expect("Could not receive listeners");
887
888        assert_eq!(listeners.http[0].0, received_listeners.http[0].0);
889        assert_eq!(listeners.udp.len(), received_listeners.udp.len());
890        assert_eq!(listeners.udp[0].0, received_listeners.udp[0].0);
891        assert_eq!(listeners.udp[1].0, received_listeners.udp[1].0);
892    }
893
894    /// Regression: a malformed `ListenersCount` whose entry counts do not
895    /// match the number of file descriptors received over SCM must be
896    /// rejected with `ListenersCountInconsistent`, never panic the worker
897    /// on `received_fds[index..index + len]`.
898    ///
899    /// Without the bounds check, a peer that declares more addresses than
900    /// `MAX_FDS_OUT` (or more than the FDs that actually arrived) crashes
901    /// the receiving worker on out-of-bounds array indexing.
902    #[test]
903    fn rejects_listeners_count_with_more_entries_than_fds() {
904        let (stream_1, stream_2) =
905            MioUnixStream::pair().expect("Could not create a pair of mio unix streams");
906        let sender = ScmSocket::new(stream_1.into_raw_fd()).expect("Could not create scm socket");
907        let receiver = ScmSocket::new(stream_2.into_raw_fd()).expect("Could not create scm socket");
908
909        // Declare three HTTP entries but ship zero file descriptors.
910        let bogus = ListenersCount {
911            http: vec![
912                "127.0.0.1:80".to_string(),
913                "127.0.0.2:80".to_string(),
914                "127.0.0.3:80".to_string(),
915            ],
916            tls: vec![],
917            tcp: vec![],
918            udp: vec![],
919        };
920        let payload = bogus.encode_length_delimited_to_vec();
921        sender
922            .send_msg_and_fds(&payload, &[])
923            .expect("manual send_msg_and_fds with zero fds must succeed at the syscall layer");
924
925        match receiver.receive_listeners() {
926            Err(ScmSocketError::ListenersCountInconsistent {
927                http,
928                tls,
929                tcp,
930                total,
931                fds_received,
932                max_fds,
933            }) => {
934                assert_eq!(http, 3);
935                assert_eq!(tls, 0);
936                assert_eq!(tcp, 0);
937                assert_eq!(total, 3);
938                assert_eq!(fds_received, 0);
939                assert_eq!(max_fds, MAX_FDS_OUT);
940            }
941            other => panic!(
942                "expected ListenersCountInconsistent, got {other:?}\n\
943                 NOTE: a panic / OOM here means the SCM bounds check was reverted",
944            ),
945        }
946    }
947}