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}