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}