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 | non-blocking, then `poll(POLLOUT)` | yes | `:511`, `:523`, `:544` under `USE_POLL` |
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//! (`:652-664`, `:778-790`). 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_client` calls `destroy_client` and refuses the connection when it
148/// fails (`caservertask.c:1456`) — so the caller gets to make C's decision.
149///
150/// Only the flag is set, which is all C sets. The idle/probe tuning the hosted
151/// CA driver adds through `socket2` (`epics-ca-rs::server::tcp`, 15 s + 5 s) is
152/// deliberately not reproduced: `TCP_KEEPIDLE` is a Linux spelling, BSD-derived
153/// stacks call it `TCP_KEEPALIVE`, and neither has been measured on RTEMS or
154/// VxWorks. A target's default idle is therefore what applies here, as it is in
155/// C.
156pub fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
157    sys::enable_keepalive(sock)
158}
159
160#[cfg(not(epics_embedded_target))]
161mod sys {
162    //! Hosted: `socket2` already owns the pre-bind option surface and its
163    //! `connect_timeout` is the `poll(POLLOUT)` shape C uses under `USE_POLL`.
164
165    use super::SocketOptions;
166    use std::io;
167    use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
168    use std::time::Duration;
169
170    fn new_socket(
171        addr_is_v6: bool,
172        ty: socket2::Type,
173        protocol: socket2::Protocol,
174        opts: SocketOptions,
175    ) -> io::Result<socket2::Socket> {
176        let domain = if addr_is_v6 {
177            socket2::Domain::IPV6
178        } else {
179            socket2::Domain::IPV4
180        };
181        let socket = socket2::Socket::new(domain, ty, Some(protocol))?;
182        if opts.broadcast {
183            socket.set_broadcast(true)?;
184        }
185        // C's fanout helper sets SO_REUSEPORT first, then SO_REUSEADDR; keep
186        // that order so a platform that rejects the second after the first
187        // fails the same way it does in C.
188        if opts.reuse_port {
189            #[cfg(unix)]
190            socket.set_reuse_port(true)?;
191            // Where the platform has no SO_REUSEPORT the request degrades to
192            // SO_REUSEADDR rather than silently doing nothing — C's
193            // `#ifndef SO_REUSEPORT / # define USE_SO_REUSEADDR`
194            // (`drvAsynIPPort.c:88-92`). Windows is the case that reaches
195            // this; both embedded triples define the option and take the arm
196            // above.
197            #[cfg(not(unix))]
198            socket.set_reuse_address(true)?;
199        }
200        if opts.reuse_address {
201            socket.set_reuse_address(true)?;
202        }
203        Ok(socket)
204    }
205
206    pub(super) fn udp_socket(local: SocketAddr, opts: SocketOptions) -> io::Result<UdpSocket> {
207        let socket = new_socket(
208            local.is_ipv6(),
209            socket2::Type::DGRAM,
210            socket2::Protocol::UDP,
211            opts,
212        )?;
213        socket.bind(&local.into())?;
214        Ok(UdpSocket::from(socket))
215    }
216
217    pub(super) fn tcp_listener(
218        local: SocketAddr,
219        opts: SocketOptions,
220        backlog: i32,
221    ) -> io::Result<TcpListener> {
222        let socket = new_socket(
223            local.is_ipv6(),
224            socket2::Type::STREAM,
225            socket2::Protocol::TCP,
226            opts,
227        )?;
228        socket.bind(&local.into())?;
229        socket.listen(backlog)?;
230        Ok(TcpListener::from(socket))
231    }
232
233    pub(super) fn tcp_connect(
234        remote: SocketAddr,
235        local: Option<SocketAddr>,
236        opts: SocketOptions,
237        timeout: Duration,
238    ) -> io::Result<TcpStream> {
239        let socket = new_socket(
240            remote.is_ipv6(),
241            socket2::Type::STREAM,
242            socket2::Protocol::TCP,
243            opts,
244        )?;
245        if let Some(local) = local {
246            socket.bind(&local.into())?;
247        }
248        match socket.connect_timeout(&remote.into(), timeout) {
249            Ok(()) => Ok(TcpStream::from(socket)),
250            // `socket2::connect_timeout` polls for POLLIN|POLLOUT and rejects a
251            // POLLHUP even when SO_ERROR is clear. macOS raises that when the
252            // peer FINs immediately after accepting; Linux does not. The
253            // handshake did complete, and C — which only inspects SO_ERROR
254            // (`drvAsynIPPort.c:545-560`) — treats the link as connected and
255            // lets the later read surface the EOF. So if the socket is in fact
256            // connected, the connect succeeded whatever POLLHUP was flagged.
257            Err(e) => match socket.peer_addr() {
258                Ok(_) => Ok(TcpStream::from(socket)),
259                Err(_) => Err(e),
260            },
261        }
262    }
263
264    pub(super) fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
265        socket2::SockRef::from(sock).set_keepalive(true)
266    }
267}
268
269#[cfg(epics_embedded_target)]
270mod sys {
271    //! RTEMS / VxWorks: raw `libc`, because `socket2` does not build for either
272    //! triple. Shapes follow `epics-ca-rs::server::blocking`, which is the
273    //! proven-on-target version of this sequence.
274
275    use super::SocketOptions;
276    use std::io;
277    use std::net::{SocketAddr, TcpListener, TcpStream, UdpSocket};
278    use std::os::fd::{FromRawFd, RawFd};
279    use std::time::Duration;
280
281    /// An fd that closes on drop, so every `?` between `socket()` and the
282    /// hand-off to a `std` type releases it. Without this each early return is
283    /// its own descriptor leak, which is the bug the two hand-written copies of
284    /// this sequence each had to avoid by hand.
285    struct OwnedFd(RawFd);
286
287    impl Drop for OwnedFd {
288        fn drop(&mut self) {
289            // SAFETY: `self.0` is a descriptor this type exclusively owns and
290            // has not yet released.
291            unsafe { libc::close(self.0) };
292        }
293    }
294
295    impl OwnedFd {
296        /// Give up ownership without closing, for hand-off to a `std` socket.
297        fn into_raw(self) -> RawFd {
298            let fd = self.0;
299            std::mem::forget(self);
300            fd
301        }
302    }
303
304    fn last_error() -> io::Error {
305        io::Error::last_os_error()
306    }
307
308    fn set_bool_opt(fd: RawFd, level: libc::c_int, opt: libc::c_int) -> io::Result<()> {
309        let one: libc::c_int = 1;
310        // SAFETY: `fd` is a valid open socket; `one` outlives the call and its
311        // size matches the `c_int` the option expects.
312        let rc = unsafe {
313            libc::setsockopt(
314                fd,
315                level,
316                opt,
317                &one as *const libc::c_int as *const libc::c_void,
318                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
319            )
320        };
321        if rc != 0 {
322            return Err(last_error());
323        }
324        Ok(())
325    }
326
327    fn new_socket(
328        ty: libc::c_int,
329        protocol: libc::c_int,
330        opts: SocketOptions,
331    ) -> io::Result<OwnedFd> {
332        // SAFETY: `socket()` returns a fresh owned descriptor or -1.
333        let fd = unsafe { libc::socket(libc::AF_INET, ty, protocol) };
334        if fd < 0 {
335            return Err(last_error());
336        }
337        // Own it before the first fallible call below, so every `?` closes it.
338        let owned = OwnedFd(fd);
339        if opts.broadcast {
340            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_BROADCAST)?;
341        }
342        if opts.reuse_port {
343            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_REUSEPORT)?;
344        }
345        if opts.reuse_address {
346            set_bool_opt(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR)?;
347        }
348        Ok(owned)
349    }
350
351    /// Marshal an IPv4 `SocketAddr` into a `sockaddr_in`.
352    ///
353    /// IPv4 only: both targets' asyn drivers are IPv4 in practice, and an
354    /// IPv6 address here is refused loudly rather than silently bound to the
355    /// wrong family. `sin_len` (present on VxWorks, absent on Linux) is left at
356    /// the zero the `zeroed()` gives it — the same choice
357    /// `epics-ca-rs::server::blocking::bind_udp_search_socket` makes, and the
358    /// `socklen_t` argument is what both stacks actually read.
359    fn sockaddr_in(addr: SocketAddr) -> io::Result<libc::sockaddr_in> {
360        let v4 = match addr {
361            SocketAddr::V4(v4) => v4,
362            SocketAddr::V6(_) => {
363                return Err(io::Error::new(
364                    io::ErrorKind::Unsupported,
365                    "IPv6 is not supported on this target",
366                ));
367            }
368        };
369        // SAFETY: `sockaddr_in` is a plain-old-data C struct for which all-zero
370        // is a valid initial value.
371        let mut sin: libc::sockaddr_in = unsafe { std::mem::zeroed() };
372        sin.sin_family = libc::AF_INET as libc::sa_family_t;
373        sin.sin_port = v4.port().to_be();
374        sin.sin_addr = libc::in_addr {
375            s_addr: u32::from(*v4.ip()).to_be(),
376        };
377        Ok(sin)
378    }
379
380    fn bind_fd(fd: RawFd, addr: SocketAddr) -> io::Result<()> {
381        let sin = sockaddr_in(addr)?;
382        // SAFETY: `sin` is fully initialised and the length is its exact size.
383        let rc = unsafe {
384            libc::bind(
385                fd,
386                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
387                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
388            )
389        };
390        if rc != 0 {
391            return Err(last_error());
392        }
393        Ok(())
394    }
395
396    pub(super) fn udp_socket(local: SocketAddr, opts: SocketOptions) -> io::Result<UdpSocket> {
397        let owned = new_socket(libc::SOCK_DGRAM, libc::IPPROTO_UDP, opts)?;
398        bind_fd(owned.0, local)?;
399        // SAFETY: a valid, exclusively-owned socket descriptor released by
400        // `into_raw` precisely so `UdpSocket` becomes its sole owner.
401        Ok(unsafe { UdpSocket::from_raw_fd(owned.into_raw()) })
402    }
403
404    pub(super) fn tcp_listener(
405        local: SocketAddr,
406        opts: SocketOptions,
407        backlog: i32,
408    ) -> io::Result<TcpListener> {
409        let owned = new_socket(libc::SOCK_STREAM, libc::IPPROTO_TCP, opts)?;
410        bind_fd(owned.0, local)?;
411        // SAFETY: `owned.0` is a valid bound socket.
412        if unsafe { libc::listen(owned.0, backlog) } != 0 {
413            return Err(last_error());
414        }
415        // SAFETY: as in `udp_socket`.
416        Ok(unsafe { TcpListener::from_raw_fd(owned.into_raw()) })
417    }
418
419    // This module's target set is exactly {rtems, vxworks} — that is what
420    // `epics_embedded_target` means. Both `connect_fd` and `set_nonblocking`
421    // below are written as that closed pair rather than as a
422    // `vxworks`/`everything else` split, so adding a third embedded triple
423    // fails to compile *here*, at the decision, instead of silently taking
424    // whichever arm happened to be the fallback.
425    #[cfg(not(any(target_os = "rtems", target_os = "vxworks")))]
426    compile_error!(
427        "epics_embedded_target gained a triple beyond rtems/vxworks: choose \
428         its connect_fd and set_nonblocking arms explicitly"
429    );
430
431    /// Put `fd` into non-blocking mode.
432    ///
433    /// C `drvAsynIPPort.c::setNonBlock` (`:176-199`) branches exactly here:
434    /// VxWorks uses `ioctl(fd, FIONBIO, &flags)` — note it passes the address
435    /// of the flag, not the flag itself — where a POSIX target uses `fcntl`.
436    /// Only the VxWorks arm exists: RTEMS takes the blocking `connect_fd`
437    /// below and never needs the socket switched, so an `fcntl` arm here
438    /// would be dead on every triple this module compiles for.
439    #[cfg(target_os = "vxworks")]
440    fn set_nonblocking(fd: RawFd, on: bool) -> io::Result<()> {
441        let mut flags: libc::c_int = i32::from(on);
442        // SAFETY: `fd` is a valid socket; FIONBIO reads one `int` through the
443        // pointer, which `flags` provides for the duration of the call.
444        let rc = unsafe { libc::ioctl(fd, libc::FIONBIO, &mut flags as *mut libc::c_int) };
445        if rc < 0 {
446            return Err(last_error());
447        }
448        Ok(())
449    }
450
451    pub(super) fn tcp_connect(
452        remote: SocketAddr,
453        local: Option<SocketAddr>,
454        opts: SocketOptions,
455        timeout: Duration,
456    ) -> io::Result<TcpStream> {
457        let owned = new_socket(libc::SOCK_STREAM, libc::IPPROTO_TCP, opts)?;
458        if let Some(local) = local {
459            bind_fd(owned.0, local)?;
460        }
461        connect_fd(&owned, remote, timeout)?;
462        // SAFETY: as in `udp_socket`.
463        Ok(unsafe { TcpStream::from_raw_fd(owned.into_raw()) })
464    }
465
466    pub(super) fn enable_keepalive(sock: &TcpStream) -> io::Result<()> {
467        use std::os::fd::AsRawFd;
468        set_bool_opt(sock.as_raw_fd(), libc::SOL_SOCKET, libc::SO_KEEPALIVE)
469    }
470
471    /// RTEMS: plain blocking connect, no deadline.
472    ///
473    /// C parity, not a shortcut: `__rtems__` selects `USE_SOCKTIMEOUT`
474    /// (`drvAsynIPPort.c:71-72`), which compiles out both the pre-connect
475    /// `setNonBlock` (`:511`) and the `poll(POLLOUT)` deadline (`:544`). The
476    /// transfer bound C keeps there is `SO_RCVTIMEO`/`SO_SNDTIMEO`, applied by
477    /// the read/write path rather than here.
478    #[cfg(target_os = "rtems")]
479    fn connect_fd(owned: &OwnedFd, remote: SocketAddr, _timeout: Duration) -> io::Result<()> {
480        let sin = sockaddr_in(remote)?;
481        // SAFETY: `sin` is fully initialised and the length is its exact size.
482        let rc = unsafe {
483            libc::connect(
484                owned.0,
485                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
486                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
487            )
488        };
489        if rc != 0 {
490            return Err(last_error());
491        }
492        Ok(())
493    }
494
495    /// VxWorks: non-blocking connect bounded by `poll(POLLOUT)`, then
496    /// `SO_ERROR`.
497    ///
498    /// This is C's `USE_POLL` path (`drvAsynIPPort.c:511`, `:523`, `:544-560`),
499    /// which VxWorks takes via `FAKE_POLL`. C fakes `poll` with `select()`
500    /// because its VxWorks headers lack `poll`; the Rust `libc` binding for the
501    /// triple exposes `poll` directly, so the fake is unnecessary and the
502    /// observable behaviour is the same.
503    #[cfg(target_os = "vxworks")]
504    fn connect_fd(owned: &OwnedFd, remote: SocketAddr, timeout: Duration) -> io::Result<()> {
505        let sin = sockaddr_in(remote)?;
506        set_nonblocking(owned.0, true)?;
507        // SAFETY: `sin` is fully initialised and the length is its exact size.
508        let rc = unsafe {
509            libc::connect(
510                owned.0,
511                &sin as *const libc::sockaddr_in as *const libc::sockaddr,
512                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
513            )
514        };
515        if rc == 0 {
516            set_nonblocking(owned.0, false)?;
517            return Ok(());
518        }
519        let err = last_error();
520        let in_progress = matches!(
521            err.raw_os_error(),
522            Some(e) if e == libc::EINPROGRESS || e == libc::EWOULDBLOCK
523        );
524        if !in_progress {
525            return Err(err);
526        }
527
528        let ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
529        let mut pfd = libc::pollfd {
530            fd: owned.0,
531            events: libc::POLLOUT,
532            revents: 0,
533        };
534        // SAFETY: a one-element `pollfd` array, matching the count passed.
535        let n = unsafe { libc::poll(&mut pfd as *mut libc::pollfd, 1, ms) };
536        if n < 0 {
537            return Err(last_error());
538        }
539        if n == 0 {
540            return Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out"));
541        }
542
543        // C reads SO_ERROR and treats a non-zero value as the connect failure
544        // (`:545-560`); poll reporting the fd ready says only that the attempt
545        // finished, not that it succeeded.
546        let mut so_error: libc::c_int = 0;
547        let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
548        // SAFETY: `so_error`/`len` are live for the call and sized as SO_ERROR
549        // expects.
550        let rc = unsafe {
551            libc::getsockopt(
552                owned.0,
553                libc::SOL_SOCKET,
554                libc::SO_ERROR,
555                &mut so_error as *mut libc::c_int as *mut libc::c_void,
556                &mut len as *mut libc::socklen_t,
557            )
558        };
559        if rc != 0 {
560            return Err(last_error());
561        }
562        if so_error != 0 {
563            return Err(io::Error::from_raw_os_error(so_error));
564        }
565        set_nonblocking(owned.0, false)?;
566        Ok(())
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use std::net::{Ipv4Addr, SocketAddrV4};
574
575    fn localhost(port: u16) -> SocketAddr {
576        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port))
577    }
578
579    /// Read the flag back off the socket, because "the call returned `Ok`" and
580    /// "the option is on" are different claims — C checks the `setsockopt`
581    /// status and this checks the state that status is supposed to mean.
582    #[cfg(unix)]
583    #[test]
584    fn enable_keepalive_actually_sets_so_keepalive() {
585        use std::os::fd::AsRawFd;
586
587        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 4).unwrap();
588        let port = listener.local_addr().unwrap().port();
589        let client = TcpStream::connect(localhost(port)).unwrap();
590        let (accepted, _) = listener.accept().unwrap();
591
592        let mut before: libc::c_int = 0;
593        let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
594        // SAFETY: `accepted` owns a live socket for the borrow; `before`/`len`
595        // are sized as SO_KEEPALIVE's `c_int` optval expects.
596        let rc = unsafe {
597            libc::getsockopt(
598                accepted.as_raw_fd(),
599                libc::SOL_SOCKET,
600                libc::SO_KEEPALIVE,
601                std::ptr::addr_of_mut!(before).cast(),
602                &mut len,
603            )
604        };
605        assert_eq!(rc, 0, "getsockopt failed: {}", io::Error::last_os_error());
606        assert_eq!(before, 0, "the fixture must start with the option off");
607
608        enable_keepalive(&accepted).expect("SO_KEEPALIVE");
609
610        let mut after: libc::c_int = 0;
611        // SAFETY: as above.
612        let rc = unsafe {
613            libc::getsockopt(
614                accepted.as_raw_fd(),
615                libc::SOL_SOCKET,
616                libc::SO_KEEPALIVE,
617                std::ptr::addr_of_mut!(after).cast(),
618                &mut len,
619            )
620        };
621        assert_eq!(rc, 0, "getsockopt failed: {}", io::Error::last_os_error());
622        assert_ne!(after, 0, "SO_KEEPALIVE not enabled");
623        drop(client);
624    }
625
626    #[test]
627    fn udp_binds_and_reports_its_port() {
628        let sock = udp_socket(localhost(0), SocketOptions::default()).unwrap();
629        assert_ne!(sock.local_addr().unwrap().port(), 0);
630    }
631
632    /// The invariant the whole module exists for: the option is set on an
633    /// unbound socket, so two sockets can share one port. Bind-then-setsockopt
634    /// would leave the second bind failing with EADDRINUSE.
635    #[test]
636    fn fanout_options_let_two_sockets_share_a_port() {
637        let first = udp_socket(localhost(0), SocketOptions::FANOUT).unwrap();
638        let port = first.local_addr().unwrap().port();
639        let second = udp_socket(localhost(port), SocketOptions::FANOUT).unwrap();
640        assert_eq!(second.local_addr().unwrap().port(), port);
641    }
642
643    /// The negative half: without the options the same second bind must fail.
644    /// Read as a pair with the test above, this is what proves the options are
645    /// doing the work rather than the platform being permissive.
646    #[test]
647    fn without_fanout_options_a_shared_port_is_refused() {
648        let first = udp_socket(localhost(0), SocketOptions::default()).unwrap();
649        let port = first.local_addr().unwrap().port();
650        assert!(udp_socket(localhost(port), SocketOptions::default()).is_err());
651    }
652
653    #[test]
654    fn tcp_listener_accepts_a_connect() {
655        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 8).unwrap();
656        let addr = listener.local_addr().unwrap();
657        let joiner = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
658        let client =
659            tcp_connect(addr, None, SocketOptions::default(), Duration::from_secs(5)).unwrap();
660        let accepted = joiner.join().unwrap().unwrap();
661        assert_eq!(accepted.local_addr().unwrap().port(), addr.port());
662        assert_eq!(client.peer_addr().unwrap().port(), addr.port());
663    }
664
665    /// A connect to a port nothing listens on must fail rather than hang.
666    #[test]
667    fn tcp_connect_to_a_closed_port_fails() {
668        // Bind then drop, so the port is real but unowned.
669        let port = {
670            let probe = tcp_listener(localhost(0), SocketOptions::default(), 1).unwrap();
671            probe.local_addr().unwrap().port()
672        };
673        let r = tcp_connect(
674            localhost(port),
675            None,
676            SocketOptions::default(),
677            Duration::from_secs(5),
678        );
679        assert!(r.is_err());
680    }
681
682    #[test]
683    fn tcp_connect_honours_a_local_bind() {
684        let listener = tcp_listener(localhost(0), SocketOptions::REUSE_ADDRESS, 8).unwrap();
685        let addr = listener.local_addr().unwrap();
686        let joiner = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
687        let client = tcp_connect(
688            addr,
689            Some(localhost(0)),
690            SocketOptions::REUSE_ADDRESS,
691            Duration::from_secs(5),
692        )
693        .unwrap();
694        let accepted = joiner.join().unwrap().unwrap();
695        assert_eq!(
696            accepted.peer_addr().unwrap().port(),
697            client.local_addr().unwrap().port()
698        );
699    }
700}