Skip to main content

epics_libcom_rs/runtime/
socket.rs

1//! Socket construction with pre-bind options: the one place in the workspace
2//! that opens a socket, sets the address-reuse/broadcast options a protocol
3//! needs, and only then binds or connects it.
4//!
5//! # Why this exists, and why here
6//!
7//! `socket2` — the obvious way to set an option on an unbound socket — does not
8//! build for `armv7-rtems-eabihf` or the `*-wrs-vxworks*` triples (20 compile
9//! errors on the former: `ip_mreqn`, `IovLen`, and friends are Linux shapes the
10//! target's `libc` has no equivalent for). So every reactor-free driver that
11//! needed a pre-bind option grew its own raw-`libc` copy of the same twenty
12//! lines. At the time this module was written there were two, byte-for-byte
13//! alike in everything but their error strings:
14//!
15//! * `epics-ca-rs::server::blocking::bind_udp_search_socket`
16//! * `epics-pva-rs::server_native::blocking`'s UDP search responder
17//!
18//! and `asyn-rs`'s IP drivers were about to be the third. `epics-ca-rs` does not
19//! depend on `epics-pva-rs` and must not, and `asyn-rs` depends on neither, so
20//! there is exactly one crate all three can reach: this one. A primitive
21//! promoted into any protocol crate is one the other two structurally cannot
22//! call — the same reasoning that put `runtime::blocking_io` here, for the same
23//! reason.
24//!
25//! The second reason is coverage. `epics-libcom-rs` is the first entry in
26//! `CRATES` in both `scripts/rtems-check.sh` and `scripts/vxworks-check.sh`, so
27//! target-only `unsafe` placed here is compiled for both triples by the gates
28//! that already run. The same code in a crate outside those lists is compiled
29//! by nothing.
30//!
31//! # Why the option must precede the bind
32//!
33//! `SO_REUSEPORT` is what lets several IOCs share one UDP port and have the
34//! kernel fan each datagram out to all of them. The kernel only honours it on
35//! an **unbound** socket, so `UdpSocket::bind()` followed by a `setsockopt` is
36//! not a slower version of this — it is a version that silently does nothing.
37//! That ordering constraint is the whole reason `std`'s constructors are not
38//! enough and a raw `socket()`/`setsockopt()`/`bind()` sequence is.
39//!
40//! # The C authority for each branch
41//!
42//! Option selection follows EPICS base
43//! `libcom/src/osi/os/default/osdSockAddrReuse.cpp`: the datagram-fanout helper
44//! sets `SO_REUSEPORT` (where defined) *and then* `SO_REUSEADDR`; the
45//! time-wait helper sets `SO_REUSEADDR` alone. Both `SO_REUSEPORT` constants
46//! exist on the two embedded targets (`0x0200` on newlib/RTEMS and on VxWorks),
47//! so neither takes the `#ifndef SO_REUSEPORT` fallback the C comment describes
48//! for older systems.
49//!
50//! Connect behaviour follows asyn `drvAsynIPPort.c`, whose three branches this
51//! module reproduces exactly:
52//!
53//! | target | connect | timeout honoured | C authority |
54//! |---|---|---|---|
55//! | hosted | blocking connect, then non-blocking | connect: no, write: yes | `:535-536` (`setNonBlock` under `USE_POLL`), `:633-651` (`poll(POLLOUT)`, the WRITE deadline) |
56//! | VxWorks | non-blocking, then `select()` via `FAKE_POLL` | yes | `:76`, `:139-164`, `:178` |
57//! | RTEMS | **blocking** | **no** | `:71-72` — `__rtems__` takes `USE_SOCKTIMEOUT`, and both `setNonBlock` and the poll block sit inside `#ifdef USE_POLL` |
58//!
59//! The RTEMS row is not an omission. C genuinely has no connect timeout there;
60//! it bounds the *transfer* instead, with `SO_RCVTIMEO`/`SO_SNDTIMEO`
61//! (`:618-623`, `:744-749`). Honouring the deadline there anyway would be a
62//! deviation invented by the port, so [`tcp_connect`] documents the difference
63//! rather than papering over it.
64
65use std::io;
66use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
67use std::time::Duration;
68
69/// Options applied to a fresh socket *before* it is bound or connected.
70///
71/// A struct rather than three positional `bool`s because the call sites set
72/// different subsets and a positional triple reads identically whichever two
73/// are swapped.
74#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
75pub struct SocketOptions {
76    /// `SO_BROADCAST`. UDP only; asyn's `udp*` protocol suffix.
77    pub broadcast: bool,
78    /// `SO_REUSEADDR`.
79    pub reuse_address: bool,
80    /// `SO_REUSEPORT`, where the platform defines it.
81    pub reuse_port: bool,
82}
83
84impl SocketOptions {
85    /// The datagram-fanout pair: `SO_REUSEPORT` **and** `SO_REUSEADDR`, matching
86    /// C `epicsSocketEnableAddressUseForDatagramFanout`, which sets both.
87    pub const FANOUT: Self = Self {
88        broadcast: false,
89        reuse_address: true,
90        reuse_port: true,
91    };
92
93    /// `SO_REUSEADDR` alone, matching C
94    /// `epicsSocketEnableAddressReuseDuringTimeWaitState`. This is what a TCP
95    /// listener gets; the fanout helper is `SOCK_DGRAM`-only.
96    pub const REUSE_ADDRESS: Self = Self {
97        broadcast: false,
98        reuse_address: true,
99        reuse_port: false,
100    };
101}
102
103/// Open a UDP socket, apply `opts`, and bind it to `local`.
104pub fn udp_socket(local: SocketAddr, opts: SocketOptions) -> io::Result<UdpSocket> {
105    sys::udp_socket(local, opts)
106}
107
108/// Open a TCP socket, apply `opts`, bind it to `local`, and start listening.
109pub fn tcp_listener(
110    local: SocketAddr,
111    opts: SocketOptions,
112    backlog: i32,
113) -> io::Result<TcpListener> {
114    sys::tcp_listener(local, opts, backlog)
115}
116
117/// Open a TCP socket, apply `opts`, optionally bind it to `local`, and connect
118/// it to `remote` within `timeout`.
119///
120/// # The timeout is not honoured on RTEMS
121///
122/// See the module header: C `drvAsynIPPort.c` takes `USE_SOCKTIMEOUT` on
123/// `__rtems__`, which compiles out both the pre-connect `setNonBlock` and the
124/// `poll(POLLOUT)` deadline, leaving a plain blocking `connect()`. This
125/// reproduces that. On every other target the deadline is enforced, as it is in
126/// C.
127pub fn tcp_connect(
128    remote: SocketAddr,
129    local: Option<SocketAddr>,
130    opts: SocketOptions,
131    timeout: Duration,
132) -> io::Result<TcpStream> {
133    sys::tcp_connect(remote, local, opts, timeout)
134}
135
136/// Enable `SO_KEEPALIVE` on an already-accepted TCP connection.
137///
138/// The one option here applied *after* the socket exists rather than before it
139/// binds: a server does not construct the sockets it serves, it accepts them.
140/// It still belongs here, because "reach `setsockopt` without `socket2` on the
141/// embedded triples" is the whole reason this module exists, and a keepalive
142/// setter written at a call site would be the third hand-rolled copy of that
143/// twenty lines (`epics-tools-rs::procserv::client::set_keepalive` is the
144/// second).
145///
146/// The error is returned, not swallowed. C treats this option as required —
147/// `create_tcp_client` calls `destroy_client` and refuses the connection when
148/// it fails (`caservertask.c:1457-1463`) — so the caller gets to make C's
149/// decision.
150///
151/// Only the flag is set, which is all C sets. The idle/probe tuning the hosted
152/// CA driver adds through `socket2` (`epics-ca-rs::server::tcp`, 15 s + 5 s) is
153/// deliberately not reproduced: `TCP_KEEPIDLE` is a Linux spelling, BSD-derived
154/// stacks call it `TCP_KEEPALIVE`, and neither has been measured on RTEMS or
155/// VxWorks. A target's default idle is therefore what applies here, as it is in
156/// C.
157pub fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
158    sys::enable_keepalive(sock)
159}
160
161#[cfg(not(epics_embedded_target))]
162mod sys {
163    //! Hosted: `socket2` already owns the pre-bind option surface and its
164    //! `connect_timeout` is the `poll(POLLOUT)` shape C uses under `USE_POLL`.
165
166    use super::SocketOptions;
167    use std::io;
168    use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
169    use std::time::Duration;
170
171    fn new_socket(
172        addr_is_v6: bool,
173        ty: socket2::Type,
174        protocol: socket2::Protocol,
175        opts: SocketOptions,
176    ) -> io::Result<socket2::Socket> {
177        let domain = if addr_is_v6 {
178            socket2::Domain::IPV6
179        } else {
180            socket2::Domain::IPV4
181        };
182        let socket = socket2::Socket::new(domain, ty, Some(protocol))?;
183        if opts.broadcast {
184            socket.set_broadcast(true)?;
185        }
186        // C's fanout helper sets SO_REUSEPORT first, then SO_REUSEADDR; keep
187        // that order so a platform that rejects the second after the first
188        // fails the same way it does in C.
189        if opts.reuse_port {
190            #[cfg(unix)]
191            socket.set_reuse_port(true)?;
192            // Where the platform has no SO_REUSEPORT the request degrades to
193            // SO_REUSEADDR rather than silently doing nothing — C's
194            // `#ifndef SO_REUSEPORT / # define USE_SO_REUSEADDR`
195            // (`drvAsynIPPort.c:90-92`). Windows is the case that reaches
196            // this; both embedded triples define the option and take the arm
197            // above.
198            #[cfg(not(unix))]
199            socket.set_reuse_address(true)?;
200        }
201        if opts.reuse_address {
202            socket.set_reuse_address(true)?;
203        }
204        Ok(socket)
205    }
206
207    pub(super) fn udp_socket(local: SocketAddr, opts: SocketOptions) -> io::Result<UdpSocket> {
208        let socket = new_socket(
209            local.is_ipv6(),
210            socket2::Type::DGRAM,
211            socket2::Protocol::UDP,
212            opts,
213        )?;
214        socket.bind(&local.into())?;
215        Ok(UdpSocket::from(socket))
216    }
217
218    pub(super) fn tcp_listener(
219        local: SocketAddr,
220        opts: SocketOptions,
221        backlog: i32,
222    ) -> io::Result<TcpListener> {
223        let socket = new_socket(
224            local.is_ipv6(),
225            socket2::Type::STREAM,
226            socket2::Protocol::TCP,
227            opts,
228        )?;
229        socket.bind(&local.into())?;
230        socket.listen(backlog)?;
231        Ok(TcpListener::from(socket))
232    }
233
234    pub(super) fn tcp_connect(
235        remote: SocketAddr,
236        local: Option<SocketAddr>,
237        opts: SocketOptions,
238        timeout: Duration,
239    ) -> io::Result<TcpStream> {
240        let socket = new_socket(
241            remote.is_ipv6(),
242            socket2::Type::STREAM,
243            socket2::Protocol::TCP,
244            opts,
245        )?;
246        if let Some(local) = local {
247            socket.bind(&local.into())?;
248        }
249        match socket.connect_timeout(&remote.into(), timeout) {
250            Ok(()) => Ok(TcpStream::from(socket)),
251            // `socket2::connect_timeout` polls for POLLIN|POLLOUT and rejects a
252            // POLLHUP even when SO_ERROR is clear. macOS raises that when the
253            // peer FINs immediately after accepting; Linux does not. The
254            // handshake did complete, and C — which only inspects SO_ERROR
255            // (`drvAsynIPPort.c:544-554`, asyn PR #211 `e1987063`, later than
256            // the `e2a281e2` these citations otherwise resolve against) —
257            // treats the link as connected and
258            // lets the later read surface the EOF. So if the socket is in fact
259            // connected, the connect succeeded whatever POLLHUP was flagged.
260            Err(e) => match socket.peer_addr() {
261                Ok(_) => Ok(TcpStream::from(socket)),
262                Err(_) => Err(e),
263            },
264        }
265    }
266
267    pub(super) fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
268        socket2::SockRef::from(sock).set_keepalive(true)
269    }
270}
271
272#[cfg(epics_embedded_target)]
273mod sys {
274    //! RTEMS / VxWorks: raw `libc`, because `socket2` does not build for either
275    //! triple. Shapes follow `epics-ca-rs::server::blocking`, which is the
276    //! proven-on-target version of this sequence.
277
278    use super::SocketOptions;
279    use std::io;
280    use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
281    use std::os::fd::{FromRawFd, RawFd};
282    use std::time::Duration;
283
284    /// An fd that closes on drop, so every `?` between `socket()` and the
285    /// hand-off to a `std` type releases it. Without this each early return is
286    /// its own descriptor leak, which is the bug the two hand-written copies of
287    /// this sequence each had to avoid by hand.
288    struct OwnedFd(RawFd);
289
290    impl Drop for OwnedFd {
291        fn drop(&mut self) {
292            // SAFETY: `self.0` is a descriptor this type exclusively owns and
293            // has not yet released.
294            unsafe { libc::close(self.0) };
295        }
296    }
297
298    impl OwnedFd {
299        /// Give up ownership without closing, for hand-off to a `std` socket.
300        fn into_raw(self) -> RawFd {
301            let fd = self.0;
302            std::mem::forget(self);
303            fd
304        }
305    }
306
307    fn last_error() -> io::Error {
308        io::Error::last_os_error()
309    }
310
311    fn set_bool_opt(fd: RawFd, level: libc::c_int, opt: libc::c_int) -> io::Result<()> {
312        let one: libc::c_int = 1;
313        // SAFETY: `fd` is a valid open socket; `one` outlives the call and its
314        // size matches the `c_int` the option expects.
315        let rc = unsafe {
316            libc::setsockopt(
317                fd,
318                level,
319                opt,
320                &one as *const libc::c_int as *const libc::c_void,
321                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
322            )
323        };
324        if rc != 0 {
325            return Err(last_error());
326        }
327        Ok(())
328    }
329
330    fn new_socket(
331        ty: libc::c_int,
332        protocol: libc::c_int,
333        opts: SocketOptions,
334    ) -> io::Result<OwnedFd> {
335        // SAFETY: `socket()` returns a fresh owned descriptor or -1.
336        let fd = unsafe { libc::socket(libc::AF_INET, ty, protocol) };
337        if fd < 0 {
338            return Err(last_error());
339        }
340        // Own it before the first fallible call below, so every `?` closes it.
341        let owned = OwnedFd(fd);
342        if opts.broadcast {
343            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_BROADCAST)?;
344        }
345        if opts.reuse_port {
346            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_REUSEPORT)?;
347        }
348        if opts.reuse_address {
349            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR)?;
350        }
351        Ok(owned)
352    }
353
354    /// Marshal an IPv4 `SocketAddr` into a `sockaddr_in`.
355    ///
356    /// IPv4 only: both targets' asyn drivers are IPv4 in practice, and an
357    /// IPv6 address here is refused loudly rather than silently bound to the
358    /// wrong family. `sin_len` (present on VxWorks, absent on Linux) is left at
359    /// the zero the `zeroed()` gives it — the same choice
360    /// `epics-ca-rs::server::blocking::bind_udp_search_socket` makes, and the
361    /// `socklen_t` argument is what both stacks actually read.
362    fn sockaddr_in(addr: SocketAddr) -> io::Result<libc::sockaddr_in> {
363        let v4 = match addr {
364            SocketAddr::V4(v4) => v4,
365            SocketAddr::V6(_) => {
366                return Err(io::Error::new(
367                    io::ErrorKind::Unsupported,
368                    "IPv6 is not supported on this target",
369                ));
370            }
371        };
372        // SAFETY: `sockaddr_in` is a plain-old-data C struct for which all-zero
373        // is a valid initial value.
374        let mut sin: libc::sockaddr_in = unsafe { std::mem::zeroed() };
375        sin.sin_family = libc::AF_INET as libc::sa_family_t;
376        sin.sin_port = v4.port().to_be();
377        sin.sin_addr = libc::in_addr {
378            s_addr: u32::from(*v4.ip()).to_be(),
379        };
380        Ok(sin)
381    }
382
383    fn bind_fd(fd: RawFd, addr: SocketAddr) -> io::Result<()> {
384        let sin = sockaddr_in(addr)?;
385        // SAFETY: `sin` is fully initialised and the length is its exact size.
386        let rc = unsafe {
387            libc::bind(
388                fd,
389                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
390                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
391            )
392        };
393        if rc != 0 {
394            return Err(last_error());
395        }
396        Ok(())
397    }
398
399    pub(super) fn udp_socket(local: SocketAddr, opts: SocketOptions) -> io::Result<UdpSocket> {
400        let owned = new_socket(libc::SOCK_DGRAM, libc::IPPROTO_UDP, opts)?;
401        bind_fd(owned.0, local)?;
402        // SAFETY: a valid, exclusively-owned socket descriptor released by
403        // `into_raw` precisely so `UdpSocket` becomes its sole owner.
404        Ok(unsafe { UdpSocket::from_raw_fd(owned.into_raw()) })
405    }
406
407    pub(super) fn tcp_listener(
408        local: SocketAddr,
409        opts: SocketOptions,
410        backlog: i32,
411    ) -> io::Result<TcpListener> {
412        let owned = new_socket(libc::SOCK_STREAM, libc::IPPROTO_TCP, opts)?;
413        bind_fd(owned.0, local)?;
414        // SAFETY: `owned.0` is a valid bound socket.
415        if unsafe { libc::listen(owned.0, backlog) } != 0 {
416            return Err(last_error());
417        }
418        // SAFETY: as in `udp_socket`.
419        Ok(unsafe { TcpListener::from_raw_fd(owned.into_raw()) })
420    }
421
422    // This module's target set is exactly {rtems, vxworks} — that is what
423    // `epics_embedded_target` means. Both `connect_fd` and `set_nonblocking`
424    // below are written as that closed pair rather than as a
425    // `vxworks`/`everything else` split, so adding a third embedded triple
426    // fails to compile *here*, at the decision, instead of silently taking
427    // whichever arm happened to be the fallback.
428    #[cfg(not(any(target_os = "rtems", target_os = "vxworks")))]
429    compile_error!(
430        "epics_embedded_target gained a triple beyond rtems/vxworks: choose \
431         its connect_fd and set_nonblocking arms explicitly"
432    );
433
434    /// Put `fd` into non-blocking mode.
435    ///
436    /// C `drvAsynIPPort.c::setNonBlock` (`:176-199`) branches exactly here:
437    /// VxWorks uses `ioctl(fd, FIONBIO, &flags)` — note it passes the address
438    /// of the flag, not the flag itself — where a POSIX target uses `fcntl`.
439    /// Only the VxWorks arm exists: RTEMS takes the blocking `connect_fd`
440    /// below and never needs the socket switched, so an `fcntl` arm here
441    /// would be dead on every triple this module compiles for.
442    #[cfg(target_os = "vxworks")]
443    fn set_nonblocking(fd: RawFd, on: bool) -> io::Result<()> {
444        let mut flags: libc::c_int = i32::from(on);
445        // SAFETY: `fd` is a valid socket; FIONBIO reads one `int` through the
446        // pointer, which `flags` provides for the duration of the call.
447        let rc = unsafe { libc::ioctl(fd, libc::FIONBIO, &mut flags as *mut libc::c_int) };
448        if rc < 0 {
449            return Err(last_error());
450        }
451        Ok(())
452    }
453
454    pub(super) fn tcp_connect(
455        remote: SocketAddr,
456        local: Option<SocketAddr>,
457        opts: SocketOptions,
458        timeout: Duration,
459    ) -> io::Result<TcpStream> {
460        let owned = new_socket(libc::SOCK_STREAM, libc::IPPROTO_TCP, opts)?;
461        if let Some(local) = local {
462            bind_fd(owned.0, local)?;
463        }
464        connect_fd(&owned, remote, timeout)?;
465        // SAFETY: as in `udp_socket`.
466        Ok(unsafe { TcpStream::from_raw_fd(owned.into_raw()) })
467    }
468
469    pub(super) fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
470        use std::os::fd::AsRawFd;
471        set_bool_opt(sock.as_raw_fd(), libc::SOL_SOCKET, libc::SO_KEEPALIVE)
472    }
473
474    /// RTEMS: plain blocking connect, no deadline.
475    ///
476    /// C parity, not a shortcut: `__rtems__` selects `USE_SOCKTIMEOUT`
477    /// (`drvAsynIPPort.c:71-72`), which compiles out both the `setNonBlock`
478    /// under `USE_POLL` (`:536` at the `e2a281e2` pin) and the `poll(POLLOUT)`
479    /// connect deadline (`:544` as of asyn PR #211 `e1987063`, which is also
480    /// what moved `setNonBlock` ahead of the connect). The
481    /// transfer bound C keeps there is `SO_RCVTIMEO`/`SO_SNDTIMEO`, applied by
482    /// the read/write path rather than here.
483    #[cfg(target_os = "rtems")]
484    fn connect_fd(owned: &OwnedFd, remote: SocketAddr, _timeout: Duration) -> io::Result<()> {
485        let sin = sockaddr_in(remote)?;
486        // SAFETY: `sin` is fully initialised and the length is its exact size.
487        let rc = unsafe {
488            libc::connect(
489                owned.0,
490                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
491                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
492            )
493        };
494        if rc != 0 {
495            return Err(last_error());
496        }
497        Ok(())
498    }
499
500    /// VxWorks: non-blocking connect bounded by `poll(POLLOUT)`, then
501    /// `SO_ERROR`.
502    ///
503    /// This is C's `USE_POLL` path (`drvAsynIPPort.c:529-556`) as of asyn
504    /// PR #211 `e1987063` — later than the `e2a281e2` pin, whose `connectIt`
505    /// still connects blocking and only goes non-blocking afterwards, at
506    /// `:536`. VxWorks takes it via `FAKE_POLL`. C fakes `poll` with `select()`
507    /// because its VxWorks headers lack `poll`; the Rust `libc` binding for the
508    /// triple exposes `poll` directly, so the fake is unnecessary and the
509    /// observable behaviour is the same.
510    #[cfg(target_os = "vxworks")]
511    fn connect_fd(owned: &OwnedFd, remote: SocketAddr, timeout: Duration) -> io::Result<()> {
512        let sin = sockaddr_in(remote)?;
513        set_nonblocking(owned.0, true)?;
514        // SAFETY: `sin` is fully initialised and the length is its exact size.
515        let rc = unsafe {
516            libc::connect(
517                owned.0,
518                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
519                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
520            )
521        };
522        if rc == 0 {
523            set_nonblocking(owned.0, false)?;
524            return Ok(());
525        }
526        let err = last_error();
527        let in_progress = matches!(
528            err.raw_os_error(),
529            Some(e) if e == libc::EINPROGRESS || e == libc::EWOULDBLOCK
530        );
531        if !in_progress {
532            return Err(err);
533        }
534
535        let ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
536        let mut pfd = libc::pollfd {
537            fd: owned.0,
538            events: libc::POLLOUT,
539            revents: 0,
540        };
541        // SAFETY: a one-element `pollfd` array, matching the count passed.
542        let n = unsafe { libc::poll(&mut pfd as *mut libc::pollfd, 1, ms) };
543        if n < 0 {
544            return Err(last_error());
545        }
546        if n == 0 {
547            return Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out"));
548        }
549
550        // C reads SO_ERROR and treats a non-zero value as the connect failure
551        // (`:545-560`); poll reporting the fd ready says only that the attempt
552        // finished, not that it succeeded.
553        let mut so_error: libc::c_int = 0;
554        let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
555        // SAFETY: `so_error`/`len` are live for the call and sized as SO_ERROR
556        // expects.
557        let rc = unsafe {
558            libc::getsockopt(
559                owned.0,
560                libc::SOL_SOCKET,
561                libc::SO_ERROR,
562                &mut so_error as *mut libc::c_int as *mut libc::c_void,
563                &mut len as *mut libc::socklen_t,
564            )
565        };
566        if rc != 0 {
567            return Err(last_error());
568        }
569        if so_error != 0 {
570            return Err(io::Error::from_raw_os_error(so_error));
571        }
572        set_nonblocking(owned.0, false)?;
573        Ok(())
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use std::net::{Ipv4Addr, SocketAddrV4};
581
582    fn localhost(port: u16) -> SocketAddr {
583        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port))
584    }
585
586    /// Read the flag back off the socket, because "the call returned `Ok`" and
587    /// "the option is on" are different claims — C checks the `setsockopt`
588    /// status and this checks the state that status is supposed to mean.
589    #[cfg(unix)]
590    #[test]
591    fn enable_keepalive_actually_sets_so_keepalive() {
592        use std::os::fd::AsRawFd;
593
594        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 4).unwrap();
595        let port = listener.local_addr().unwrap().port();
596        let client = TcpStream::connect(localhost(port)).unwrap();
597        let (accepted, _) = listener.accept().unwrap();
598
599        let mut before: libc::c_int = 0;
600        let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
601        // SAFETY: `accepted` owns a live socket for the borrow; `before`/`len`
602        // are sized as SO_KEEPALIVE's `c_int` optval expects.
603        let rc = unsafe {
604            libc::getsockopt(
605                accepted.as_raw_fd(),
606                libc::SOL_SOCKET,
607                libc::SO_KEEPALIVE,
608                std::ptr::addr_of_mut!(before).cast(),
609                &mut len,
610            )
611        };
612        assert_eq!(rc, 0, "getsockopt failed: {}", io::Error::last_os_error());
613        assert_eq!(before, 0, "the fixture must start with the option off");
614
615        enable_keepalive(&accepted).expect("SO_KEEPALIVE");
616
617        let mut after: libc::c_int = 0;
618        // SAFETY: as above.
619        let rc = unsafe {
620            libc::getsockopt(
621                accepted.as_raw_fd(),
622                libc::SOL_SOCKET,
623                libc::SO_KEEPALIVE,
624                std::ptr::addr_of_mut!(after).cast(),
625                &mut len,
626            )
627        };
628        assert_eq!(rc, 0, "getsockopt failed: {}", io::Error::last_os_error());
629        assert_ne!(after, 0, "SO_KEEPALIVE not enabled");
630        drop(client);
631    }
632
633    #[test]
634    fn udp_binds_and_reports_its_port() {
635        let sock = udp_socket(localhost(0), SocketOptions::default()).unwrap();
636        assert_ne!(sock.local_addr().unwrap().port(), 0);
637    }
638
639    /// The invariant the whole module exists for: the option is set on an
640    /// unbound socket, so two sockets can share one port. Bind-then-setsockopt
641    /// would leave the second bind failing with EADDRINUSE.
642    #[test]
643    fn fanout_options_let_two_sockets_share_a_port() {
644        let first = udp_socket(localhost(0), SocketOptions::FANOUT).unwrap();
645        let port = first.local_addr().unwrap().port();
646        let second = udp_socket(localhost(port), SocketOptions::FANOUT).unwrap();
647        assert_eq!(second.local_addr().unwrap().port(), port);
648    }
649
650    /// The negative half: without the options the same second bind must fail.
651    /// Read as a pair with the test above, this is what proves the options are
652    /// doing the work rather than the platform being permissive.
653    #[test]
654    fn without_fanout_options_a_shared_port_is_refused() {
655        let first = udp_socket(localhost(0), SocketOptions::default()).unwrap();
656        let port = first.local_addr().unwrap().port();
657        assert!(udp_socket(localhost(port), SocketOptions::default()).is_err());
658    }
659
660    #[test]
661    fn tcp_listener_accepts_a_connect() {
662        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 8).unwrap();
663        let addr = listener.local_addr().unwrap();
664        let joiner = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
665        let client =
666            tcp_connect(addr, None, SocketOptions::default(), Duration::from_secs(5)).unwrap();
667        let accepted = joiner.join().unwrap().unwrap();
668        assert_eq!(accepted.local_addr().unwrap().port(), addr.port());
669        assert_eq!(client.peer_addr().unwrap().port(), addr.port());
670    }
671
672    /// A connect to a port nothing listens on must fail rather than hang.
673    #[test]
674    fn tcp_connect_to_a_closed_port_fails() {
675        // Bind then drop, so the port is real but unowned.
676        let port = {
677            let probe = tcp_listener(localhost(0), SocketOptions::default(), 1).unwrap();
678            probe.local_addr().unwrap().port()
679        };
680        let r = tcp_connect(
681            localhost(port),
682            None,
683            SocketOptions::default(),
684            Duration::from_secs(5),
685        );
686        assert!(r.is_err());
687    }
688
689    #[test]
690    fn tcp_connect_honours_a_local_bind() {
691        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 8).unwrap();
692        let addr = listener.local_addr().unwrap();
693        let joiner = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
694        let client = tcp_connect(
695            addr,
696            Some(localhost(0)),
697            SocketOptions::REUSE_ADDRESS,
698            Duration::from_secs(5),
699        )
700        .unwrap();
701        let accepted = joiner.join().unwrap().unwrap();
702        assert_eq!(
703            accepted.peer_addr().unwrap().port(),
704            client.local_addr().unwrap().port()
705        );
706    }
707}