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