Skip to main content

wireshift_fallback/
net_ops.rs

1//! Network operation implementations for the fallback backend.
2
3use std::io::{Read, Write};
4use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use wireshift_core::buffer::{Buffer, Submitted};
10use wireshift_core::op::CompletionPayload;
11use wireshift_core::{Error, Result};
12
13macro_rules! retry_eintr {
14    ($expr:expr) => {
15        loop {
16            match $expr {
17                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
18                result => break result,
19            }
20        }
21    };
22}
23
24pub(crate) fn execute_connect(
25    addr: SocketAddr,
26    timeout: Option<Duration>,
27    canceled: Arc<AtomicBool>,
28) -> Result<CompletionPayload> {
29    let stream = TcpStream::connect_timeout(&addr, timeout.unwrap_or(Duration::from_secs(5)))
30        .map_err(|error| {
31            Error::io(
32                "connect failed",
33                error,
34                "ensure the remote host is reachable",
35            )
36        })?;
37    if canceled.load(Ordering::Relaxed) {
38        let _ = stream.shutdown(Shutdown::Both);
39        return Err(Error::canceled(
40            "connect canceled after socket creation",
41            "avoid canceling the request after the socket connects",
42        ));
43    }
44    Ok(CompletionPayload::Stream(stream))
45}
46
47pub(crate) fn execute_accept(
48    listener: TcpListener,
49    canceled: Arc<AtomicBool>,
50    timeout: Option<Duration>,
51) -> Result<CompletionPayload> {
52    listener.set_nonblocking(true).map_err(|error| {
53        Error::io(
54            "accept setup failed",
55            error,
56            "ensure the listener remains valid",
57        )
58    })?;
59    let deadline = Instant::now() + timeout.unwrap_or(Duration::from_secs(5));
60    loop {
61        if canceled.load(Ordering::Relaxed) {
62            return Err(Error::canceled(
63                "accept canceled before a peer connected",
64                "avoid canceling the request while a connection is expected",
65            ));
66        }
67        match retry_eintr!(listener.accept()) {
68            Ok((stream, _)) => return Ok(CompletionPayload::Stream(stream)),
69            Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
70                let remaining = deadline.saturating_duration_since(Instant::now());
71                if remaining.is_zero() {
72                    return Err(Error::timeout(
73                        "accept timed out waiting for an inbound peer",
74                        "connect a client before waiting on the accept request",
75                    ));
76                }
77                // Event-driven wait on listener readiness instead of a fixed
78                // 10ms sleep. The old sleep added up to a full 10ms of latency
79                // to every connection that arrived mid-quantum (Law 7); poll(2)
80                // returns the instant the listening socket has a pending
81                // connection, so accept is serviced in sub-millisecond time.
82                // The timeout is capped at a short quantum so cancellation and
83                // the overall deadline are still re-checked promptly, and it
84                // never exceeds the time left until `deadline`.
85                wait_for_listener_readiness(&listener, remaining);
86            }
87            Err(error) => {
88                return Err(Error::io(
89                    "accept failed",
90                    error,
91                    "ensure the listener is bound and still accepting connections",
92                ));
93            }
94        }
95    }
96}
97
98/// Block until the listening socket has a pending connection or the (capped)
99/// timeout elapses, using `poll(2)` for event-driven readiness.
100///
101/// The poll timeout is capped at a short quantum so the accept loop still
102/// re-checks cancellation and the overall deadline promptly, and never exceeds
103/// the caller's remaining budget. A pending connection makes the listener
104/// readable immediately, so `poll` returns without waiting the full quantum -
105/// eliminating the fixed-sleep connection latency (Law 7). Spurious wakeups or
106/// `poll` errors (e.g. `EINTR`) simply return; the caller re-attempts `accept`,
107/// which surfaces any genuine listener error loudly.
108fn wait_for_listener_readiness(listener: &TcpListener, remaining: Duration) {
109    use std::os::unix::io::AsRawFd;
110
111    // Cap at 10ms to preserve the previous cancellation-check cadence, but never
112    // wait longer than the time left before the deadline.
113    let wait = remaining.min(Duration::from_millis(10));
114    let wait_ms = i32::try_from(wait.as_millis()).unwrap_or(i32::MAX);
115
116    let mut pfd = libc::pollfd {
117        fd: listener.as_raw_fd(),
118        events: libc::POLLIN,
119        revents: 0,
120    };
121    // SAFETY: `pfd` is a single, live, correctly-initialized pollfd; `poll`
122    // reads `nfds` entries and writes `revents`. The listener outlives the call.
123    unsafe {
124        libc::poll(std::ptr::addr_of_mut!(pfd), 1, wait_ms);
125    }
126}
127
128/// Apply an optional read timeout to `stream`, failing CLOSED on error.
129///
130/// The previous `let _ = stream.set_read_timeout(..)` discarded a configuration
131/// failure and then performed a read with NO timeout - which can hang the
132/// worker thread indefinitely (Law 10). Propagating the error surfaces the
133/// misconfiguration loudly instead of silently dropping the timeout.
134fn apply_read_timeout(stream: &TcpStream, timeout: Option<Duration>) -> Result<()> {
135    if let Some(t) = timeout {
136        stream.set_read_timeout(Some(t)).map_err(|error| {
137            Error::io(
138                "recv set_read_timeout failed",
139                error,
140                "ensure the socket is valid and the read timeout is a non-zero duration",
141            )
142        })?;
143    }
144    Ok(())
145}
146
147pub(crate) fn execute_send(
148    mut stream: TcpStream,
149    buffer: Buffer<Submitted>,
150) -> Result<CompletionPayload> {
151    let bytes = retry_eintr!(stream.write(buffer.backend_ref())).map_err(|error| {
152        Error::io(
153            "send failed",
154            error,
155            "ensure the TCP stream remains writable while sending data",
156        )
157    })?;
158    Ok(CompletionPayload::Bytes(bytes))
159}
160
161pub(crate) fn execute_recv(
162    mut stream: TcpStream,
163    mut buffer: Buffer<Submitted>,
164    canceled: Arc<AtomicBool>,
165    timeout: Option<Duration>,
166) -> Result<CompletionPayload> {
167    apply_read_timeout(&stream, timeout)?;
168    if canceled.load(Ordering::Relaxed) {
169        return Err(Error::canceled(
170            "recv canceled before data transfer started",
171            "avoid canceling the request before the backend starts receiving data",
172        ));
173    }
174    let bytes = retry_eintr!(stream.read(buffer.backend_mut())).map_err(|error| {
175        Error::io(
176            "recv failed",
177            error,
178            "ensure the TCP stream remains readable while receiving data",
179        )
180    })?;
181    let completed = buffer.into_completed(bytes)?;
182    Ok(CompletionPayload::Recv {
183        stream,
184        buffer: completed,
185        bytes,
186    })
187}
188
189#[cfg(test)]
190mod net_ops_tests {
191    use super::{apply_read_timeout, execute_accept};
192    use std::net::{TcpListener, TcpStream};
193    use std::sync::atomic::AtomicBool;
194    use std::sync::Arc;
195    use std::time::{Duration, Instant};
196    use wireshift_core::op::CompletionPayload;
197
198    #[test]
199    fn apply_read_timeout_surfaces_invalid_zero_duration() {
200        // A zero-duration read timeout is invalid at the OS level; the previous
201        // `let _ = set_read_timeout(..)` swallowed that error and left the read
202        // with no timeout (a potential hang). The fix must surface it (Law 10).
203        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
204        let addr = listener.local_addr().unwrap();
205        let client = TcpStream::connect(addr).unwrap();
206
207        let result = apply_read_timeout(&client, Some(Duration::from_secs(0)));
208        let err = result.expect_err("zero-duration read timeout must be rejected loudly");
209        assert!(
210            err.to_string().to_lowercase().contains("timeout"),
211            "error must identify the read-timeout configuration failure, got: {err}"
212        );
213    }
214
215    #[test]
216    fn apply_read_timeout_accepts_valid_duration_and_none() {
217        // Guard against over-failing: a valid timeout and `None` both succeed.
218        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
219        let addr = listener.local_addr().unwrap();
220        let client = TcpStream::connect(addr).unwrap();
221
222        apply_read_timeout(&client, Some(Duration::from_millis(50)))
223            .expect("a valid read timeout must be accepted");
224        apply_read_timeout(&client, None).expect("no timeout must be a no-op");
225    }
226
227    #[test]
228    fn accept_wakes_on_connection_without_fixed_sleep_latency() {
229        // Start accept BEFORE any peer connects so it parks in the readiness
230        // wait (WouldBlock -> poll). A peer then connects after a delay; the
231        // event-driven poll must return the instant the listener is readable,
232        // so the connect->accept-return latency is far below the old fixed 10ms
233        // sleep quantum. Asserting < 5ms discriminates the fixed-sleep bug while
234        // tolerating normal scheduler jitter.
235        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
236        let addr = listener.local_addr().unwrap();
237        let canceled = Arc::new(AtomicBool::new(false));
238
239        let connector = std::thread::spawn(move || {
240            std::thread::sleep(Duration::from_millis(25));
241            let connect_at = Instant::now();
242            let stream = TcpStream::connect(addr).unwrap();
243            (connect_at, stream) // hold the client open past accept
244        });
245
246        let payload = execute_accept(listener, canceled, Some(Duration::from_secs(2)))
247            .expect("accept must succeed once a peer connects");
248        let accept_done = Instant::now();
249        let (connect_at, _client) = connector.join().unwrap();
250
251        assert!(
252            matches!(payload, CompletionPayload::Stream(_)),
253            "accept must yield the accepted stream"
254        );
255        let latency = accept_done.saturating_duration_since(connect_at);
256        assert!(
257            latency < Duration::from_millis(5),
258            "accept must wake on listener readiness (sub-quantum), not after a fixed 10ms sleep; \
259             connect->accept latency was {latency:?}"
260        );
261    }
262}