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