Skip to main content

sozu_lib/protocol/proxy_protocol/
relay.rs

1//! PROXY-v2 relay state.
2//!
3//! Reads an inbound PROXY-v2 header (`parse_v2_header`) and forwards the
4//! captured bytes verbatim onto a freshly opened backend `TcpStream` before
5//! the rest of the byte stream begins. Used when Sōzu sits between two
6//! PROXY-aware peers and must preserve the original client identity.
7
8use std::{
9    cell::RefCell,
10    io::{ErrorKind, Write},
11    rc::Rc,
12};
13
14use mio::{Token, net::TcpStream};
15use nom::{Err, Offset};
16use rusty_ulid::Ulid;
17use sozu_command::logging::ansi_palette;
18
19use crate::metrics::names;
20use crate::{
21    Protocol, Readiness, SessionMetrics, SessionResult,
22    pool::Checkout,
23    protocol::{
24        pipe::{Pipe, WebSocketContext},
25        proxy_protocol::{header::ProxyAddr, parser::parse_v2_header},
26    },
27    socket::{SocketHandler, SocketResult},
28    sozu_command::ready::Ready,
29    tcp::TcpListener,
30};
31
32/// Module-level prefix used on every log line emitted from this module when
33/// no per-session state is in scope. Produces a bold bright-white
34/// `PROXY-RELAY` label (uniform across every protocol) when the logger is in
35/// colored mode.
36#[allow(unused_macros)]
37macro_rules! log_module_context {
38    () => {{
39        let (open, reset, _, _, _) = ansi_palette();
40        format!("{open}PROXY-RELAY{reset}\t >>>", open = open, reset = reset)
41    }};
42}
43
44/// Per-session prefix for log lines emitted with a [`RelayProxyProtocol`] in
45/// scope. Renders the canonical
46/// `\tPROXY-RELAY\tSession(...)\t >>>` envelope. The relay state has no
47/// `request_id`-keyed [`LogContext`] (the caller-side ulid is not yet bound
48/// to a request); the bracket carries the front/back tokens instead.
49macro_rules! log_context {
50    ($self:expr) => {{
51        let (open, reset, grey, gray, white) = ansi_palette();
52        format!(
53            "{open}PROXY-RELAY{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend}{reset}, {gray}backend{reset}={white}{backend}{reset}, {gray}front_readiness{reset}={white}{front_readiness}{reset}, {gray}back_readiness{reset}={white}{back_readiness}{reset})\t >>>",
54            open = open,
55            reset = reset,
56            grey = grey,
57            gray = gray,
58            white = white,
59            frontend = $self.frontend_token.0,
60            backend = $self.backend_token.map(|t| t.0.to_string()).unwrap_or_else(|| "<none>".to_string()),
61            front_readiness = $self.frontend_readiness,
62            back_readiness = $self.backend_readiness,
63        )
64    }};
65}
66
67pub struct RelayProxyProtocol<Front: SocketHandler> {
68    cursor_header: usize,
69    pub backend_readiness: Readiness,
70    pub backend_token: Option<Token>,
71    pub backend: Option<TcpStream>,
72    pub frontend_buffer: Checkout,
73    pub frontend_readiness: Readiness,
74    pub frontend_token: Token,
75    pub frontend: Front,
76    pub header_size: Option<usize>,
77    pub request_id: Ulid,
78    /// Parsed PROXY-v2 address pair captured from the inbound header.
79    /// `None` until the parser succeeds, and `Some(ProxyAddr::AfUnspec)` for a
80    /// header that declared AF_UNSPEC or carried `Command::Local` — whose
81    /// address block `parse_v2_header` discards per the HAProxy PROXY protocol
82    /// specification §2.2, whatever that block actually held on the wire. The
83    /// pipe phase uses `ProxyAddr::source()` here to attribute the real client
84    /// instead of the upstream PROXY-emitter's `peer_addr`.
85    pub addresses: Option<ProxyAddr>,
86}
87
88impl<Front: SocketHandler> RelayProxyProtocol<Front> {
89    /// Instantiate a new RelayProxyProtocol SessionState with:
90    /// - frontend_interest: READABLE | HUP | ERROR
91    /// - frontend_event: EMPTY
92    pub fn new(
93        frontend: Front,
94        frontend_token: Token,
95        request_id: Ulid,
96        backend: Option<TcpStream>,
97        front_buf: Checkout,
98    ) -> Self {
99        RelayProxyProtocol {
100            backend_readiness: Readiness {
101                interest: Ready::HUP | Ready::ERROR,
102                event: Ready::EMPTY,
103            },
104            backend_token: None,
105            backend,
106            cursor_header: 0,
107            frontend_buffer: front_buf,
108            frontend_readiness: Readiness {
109                interest: Ready::READABLE | Ready::HUP | Ready::ERROR,
110                event: Ready::EMPTY,
111            },
112            frontend_token,
113            frontend,
114            header_size: None,
115            request_id,
116            addresses: None,
117        }
118    }
119
120    pub fn readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
121        let space_before = self.frontend_buffer.available_space();
122        let (sz, res) = self.frontend.socket_read(self.frontend_buffer.space());
123        debug!("{} read {} bytes and res={:?}", log_context!(self), sz, res);
124        // The socket can only write into the free space it was handed.
125        debug_assert!(
126            sz <= space_before,
127            "socket_read cannot return more bytes than the available space"
128        );
129
130        if sz > 0 {
131            let data_before = self.frontend_buffer.available_data();
132            self.frontend_buffer.fill(sz);
133            // `fill` admits the freshly read bytes into the readable window:
134            // available data grows by exactly `sz` (the read fit, asserted
135            // above, so `fill` does not clamp).
136            debug_assert_eq!(
137                self.frontend_buffer.available_data(),
138                data_before + sz,
139                "fill must expose exactly the bytes just read"
140            );
141
142            count!(names::backend::BYTES_IN, sz as i64);
143            metrics.bin += sz;
144
145            if res == SocketResult::Error {
146                error!(
147                    "{} front socket error, closing the connection",
148                    log_context!(self)
149                );
150                incr!(names::proxy_protocol::ERRORS);
151                self.frontend_readiness.reset();
152                self.backend_readiness.reset();
153                return SessionResult::Close;
154            }
155
156            if res == SocketResult::WouldBlock {
157                self.frontend_readiness.event.remove(Ready::READABLE);
158            }
159
160            let data_len = self.frontend_buffer.available_data();
161            let read_sz = match parse_v2_header(self.frontend_buffer.data()) {
162                Ok((rest, header)) => {
163                    self.frontend_readiness.interest.remove(Ready::READABLE);
164                    self.backend_readiness.interest.insert(Ready::WRITABLE);
165                    // Capture the parsed addresses so the pipe phase can
166                    // attribute traffic to the real client (see `into_pipe`).
167                    // The header BYTES are a separate matter: they stay in
168                    // `frontend_buffer` untouched, and `back_writable` relays
169                    // that buffered prefix verbatim. Nothing here
170                    // re-serializes `addresses`, so consuming the header at
171                    // this point would destroy the only copy of it.
172                    self.addresses = Some(header.addr);
173                    let consumed = self.frontend_buffer.data().offset(rest);
174                    // The header length is the prefix the parser consumed; it
175                    // is a real (non-empty) prefix of the buffered data and can
176                    // never exceed what was buffered.
177                    debug_assert!(
178                        consumed <= data_len,
179                        "parsed header length cannot exceed the buffered bytes"
180                    );
181                    debug_assert!(consumed > 0, "a recognized v2 header is non-empty");
182                    consumed
183                }
184                Err(Err::Incomplete(_)) => return SessionResult::Continue,
185                Err(e) => {
186                    error!(
187                        "{} error parsing the proxy protocol header (error={:?}), closing the connection",
188                        log_context!(self),
189                        e
190                    );
191                    return SessionResult::Close;
192                }
193            };
194
195            self.header_size = Some(read_sz);
196            // Deliberately no `consume` here. The first `read_sz` buffered
197            // bytes ARE the header `back_writable` has to relay, and anything
198            // the client pipelined behind them belongs to the pipe phase:
199            // `into_pipe` hands this very `Checkout` to `Pipe::new` and then
200            // calls `restore_readiness_events`, which re-runs
201            // `arm_inherited_buffer_writes` AFTER restoring this state's event
202            // words -- that re-run, not the one inside `Pipe::new`, is what
203            // actually leaves the backend WRITABLE readiness armed for a
204            // non-empty buffer. The buffer is drained from the front, one
205            // forwarded byte at a time, by `back_writable`.
206            return SessionResult::Continue;
207        }
208
209        // Reached only when the read returned nothing. `tcp_socket_read`
210        // answers `(0, SocketResult::Continue)` the moment the slice it is
211        // handed is empty (`socket.rs`), which is what `space()` yields once
212        // the buffer is full -- a client that declares a large `len` and then
213        // stalls gets there. Leaving READABLE set in both readiness words
214        // makes `tcp.rs::ready_inner` call this again at once with no space to
215        // read into, burning `MAX_LOOP_ITERATIONS` before closing the session.
216        // Drop the event and wait for a real edge, exactly as
217        // `expect.rs::readable` does on a non-positive read.
218        debug_assert_eq!(
219            self.frontend_buffer.available_space(),
220            space_before,
221            "a zero-length read must leave the buffer untouched"
222        );
223        self.frontend_readiness.event.remove(Ready::READABLE);
224        SessionResult::Continue
225    }
226
227    // The header is send immediately at once upon the connection is establish
228    // and prepended before any data.
229    pub fn back_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
230        debug!("{} writing proxy protocol header", log_context!(self));
231
232        if let Some(ref mut socket) = self.backend
233            && let Some(header_size) = self.header_size
234        {
235            // Termination: every iteration either returns, breaks, or advances
236            // `cursor_header` by at least one byte towards `header_size`, so
237            // the loop runs at most `header_size` times. `MAX_LOOP_ITERATIONS`
238            // bounds the OUTER dispatch loop in `tcp.rs::ready_inner` and would
239            // not bound this one, so the variant has to hold here.
240            loop {
241                // `readable` consumes nothing, and this loop only drains the
242                // buffer from the front, so the unsent header tail is always
243                // the first `header_size - cursor_header` buffered bytes.
244                // Everything behind it is client payload pipelined into the
245                // same read: it belongs to the pipe phase and is deliberately
246                // NOT written here.
247                debug_assert!(
248                    self.cursor_header <= header_size,
249                    "the forwarding cursor must stay within the parsed header"
250                );
251                let remaining = header_size.saturating_sub(self.cursor_header);
252                let buffered = self.frontend_buffer.data();
253                // Clamped rather than asserted outright. `remaining <=
254                // buffered.len()` does hold for every state `readable` can
255                // produce, but `a_zero_length_write_yields_instead_of_spinning`
256                // deliberately violates it -- it drains the buffer behind this
257                // function's back to prove the `Ok(0)` arm below still yields --
258                // so a bare `debug_assert!` on it would fire in that test
259                // instead of letting it exercise the guard. Assert the
260                // invariant for the states that must satisfy it, and let the
261                // clamp carry the degenerate one.
262                debug_assert!(
263                    buffered.is_empty() || remaining <= buffered.len(),
264                    "a partially consumed header must still be buffered in full"
265                );
266                let offered = remaining.min(buffered.len());
267
268                match socket.write(&buffered[..offered]) {
269                    Ok(0) => {
270                        // A zero-length write moves no byte: `cursor_header`
271                        // would not advance, the `cursor_header >= header_size`
272                        // exit would never be reached, and the only other exit
273                        // is the `Err` arm below -- so looping here spins the
274                        // worker at 100% CPU with its event loop starved, and
275                        // every co-resident session with it. Answer it the way
276                        // `send.rs::back_writable` answers `WouldBlock` and
277                        // `expect.rs::readable` answers a zero-length read:
278                        // drop the WRITABLE readiness and wait for the next
279                        // epoll edge.
280                        debug!(
281                            "{} the backend accepted no byte, waiting for the next writable event",
282                            log_context!(self)
283                        );
284                        self.backend_readiness.event.remove(Ready::WRITABLE);
285                        break;
286                    }
287                    Ok(sz) => {
288                        // A socket write reports at most the bytes it was
289                        // offered, so the forwarded count never exceeds the
290                        // unsent header tail.
291                        debug_assert!(
292                            sz <= offered,
293                            "socket.write cannot send more than the bytes it was offered"
294                        );
295                        let cursor_before = self.cursor_header;
296                        self.cursor_header += sz;
297                        // The forwarding cursor is strictly monotonic and
298                        // tracks exactly the bytes emitted this write.
299                        debug_assert_eq!(
300                            self.cursor_header,
301                            cursor_before + sz,
302                            "header cursor advances by exactly the bytes written"
303                        );
304
305                        count!(names::backend::BACK_BYTES_OUT, sz as i64);
306                        metrics.backend_bout += sz;
307                        self.frontend_buffer.consume(sz);
308
309                        if self.cursor_header >= header_size {
310                            info!("{} proxy protocol sent, upgrading", log_context!(self));
311                            return SessionResult::Upgrade;
312                        }
313                    }
314                    Err(e) => match e.kind() {
315                        ErrorKind::WouldBlock => {
316                            // The backend's send queue is full. A v2 header is
317                            // `16 + len` with `len: u16`, so it can be far
318                            // larger than that queue: this is ordinary
319                            // backpressure, not a failure. Park the session the
320                            // way `send.rs::back_writable` does -- clear the
321                            // stale WRITABLE event, KEEP the interest -- so the
322                            // next epoll edge resumes the half-written header.
323                            // Resetting both readiness words, which this arm
324                            // did for every error kind, means no later edge is
325                            // ever acted on: the session then dies at the
326                            // frontend timeout with a truncated PROXY header
327                            // already delivered to the backend.
328                            debug!(
329                                "{} backend send queue full, waiting for the next writable event",
330                                log_context!(self)
331                            );
332                            self.backend_readiness.event.remove(Ready::WRITABLE);
333                            break;
334                        }
335                        ErrorKind::Interrupted => {
336                            // A signal landed mid-write. Nothing about the
337                            // socket changed, so edge-triggered epoll owes no
338                            // new edge and dropping the readiness here would
339                            // strand the session. Leave both words untouched
340                            // and break: `tcp.rs::ready_inner` still sees
341                            // WRITABLE in `interest & event` and re-enters
342                            // immediately, so the retry is bounded by
343                            // `MAX_LOOP_ITERATIONS` rather than looping
344                            // unbounded in here. `udp.rs` retries the same way.
345                            debug!("{} write interrupted, retrying", log_context!(self));
346                            break;
347                        }
348                        _ => {
349                            incr!(names::proxy_protocol::ERRORS);
350                            self.frontend_readiness.reset();
351                            self.backend_readiness.reset();
352                            debug!("{} write error: {}", log_context!(self), e);
353                            break;
354                        }
355                    },
356                }
357            }
358        }
359        SessionResult::Continue
360    }
361
362    pub fn front_socket(&self) -> &TcpStream {
363        self.frontend.socket_ref()
364    }
365
366    pub fn front_socket_mut(&mut self) -> &mut TcpStream {
367        self.frontend.socket_mut()
368    }
369
370    pub fn back_socket(&self) -> Option<&TcpStream> {
371        self.backend.as_ref()
372    }
373
374    pub fn back_socket_mut(&mut self) -> Option<&mut TcpStream> {
375        self.backend.as_mut()
376    }
377
378    pub fn set_back_socket(&mut self, socket: TcpStream) {
379        self.backend = Some(socket);
380    }
381
382    pub fn back_token(&self) -> Option<Token> {
383        self.backend_token
384    }
385
386    pub fn set_back_token(&mut self, token: Token) {
387        self.backend_token = Some(token);
388    }
389
390    pub fn into_pipe(
391        mut self,
392        back_buf: Checkout,
393        listener: Rc<RefCell<TcpListener>>,
394    ) -> Pipe<Front, TcpListener> {
395        let backend_socket = self.backend.take().unwrap();
396        // Same rationale as `ExpectProxyProtocol::into_pipe`: prefer the
397        // PROXY-v2 source over the TCP `peer_addr`. In Relay mode the
398        // upstream emitter is also the TCP peer, so without this fix
399        // the pipe phase records the LB / edge proxy instead of the
400        // real client. Falls back whenever `self.addresses` is `AfUnspec` —
401        // a header that declared AF_UNSPEC, or one that carried
402        // `Command::Local`, whose address block the parser discards per the
403        // HAProxy PROXY protocol specification §2.2 even when the wire block
404        // was populated.
405        let addr = self
406            .addresses
407            .as_ref()
408            .and_then(|pa| pa.source())
409            .or_else(|| self.front_socket().peer_addr().ok());
410
411        let mut pipe = Pipe::new(
412            back_buf,
413            None,
414            Some(backend_socket),
415            None,
416            None,
417            None,
418            None,
419            self.frontend_buffer,
420            self.frontend_token,
421            self.frontend,
422            listener,
423            Protocol::TCP,
424            self.request_id,
425            self.request_id,
426            addr,
427            WebSocketContext::Tcp,
428        );
429
430        // Not a bare `pipe.*_readiness.event = ..` pair. `Pipe::new` has just
431        // armed the backend WRITABLE readiness for the inherited
432        // `frontend_buffer` (`arm_inherited_buffer_writes`), and assigning the
433        // event words over it clobbers that arm. `restore_readiness_events`
434        // sets both words and THEN re-runs the arm, so the payload this state
435        // deliberately leaves buffered is flushed by construction instead of
436        // by the accident that `back_writable` happens to be entered with
437        // WRITABLE already set. `tcp.rs`'s `build_pipe_from_preread` and
438        // `upgrade_send` say the same, for the same reason
439        // (sozu-proxy/sozu#1279's close-before-flush truncation).
440        pipe.restore_readiness_events(self.frontend_readiness.event, self.backend_readiness.event);
441
442        if let Some(back_token) = self.backend_token {
443            pipe.set_back_token(back_token);
444        }
445
446        pipe
447    }
448}
449
450#[cfg(test)]
451mod relay_test {
452    use std::{
453        io::{Read, Write},
454        net::{
455            IpAddr, Ipv4Addr, SocketAddr, TcpListener as StdTcpListener, TcpStream as StdTcpStream,
456        },
457        os::unix::io::FromRawFd,
458        sync::{
459            Arc, Barrier,
460            atomic::{AtomicBool, Ordering},
461            mpsc,
462        },
463        thread::{self, JoinHandle},
464        time::{Duration, Instant},
465    };
466
467    use mio::net::TcpListener;
468    use rusty_ulid::Ulid;
469    use socket2::SockRef;
470
471    use super::*;
472    use crate::{
473        pool::Pool,
474        protocol::proxy_protocol::header::{Command, HeaderV2},
475    };
476
477    /// Address pair the upfront middleware encapsulates. Under
478    /// `Command::Local` these are the *forged* values a crafted peer would
479    /// send: the wire format lets a `LOCAL` header carry a fully populated
480    /// address block.
481    fn header_src() -> SocketAddr {
482        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(125, 25, 10, 1)), 8080)
483    }
484
485    fn header_dst() -> SocketAddr {
486        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 4, 5, 8)), 4200)
487    }
488
489    /// Drives `RelayProxyProtocol::readable` against a real loopback
490    /// connection carrying one PROXY-v2 header, and returns the source address
491    /// `into_pipe` would attribute to the client, i.e.
492    /// `self.addresses.as_ref().and_then(|pa| pa.source())` -- `None` meaning
493    /// `into_pipe` falls back to the front socket's `peer_addr`.
494    fn attributed_source_for(command: Command) -> Option<SocketAddr> {
495        setup_test_logger!();
496        let listener = TcpListener::bind("127.0.0.1:0".parse().expect("parse address error"))
497            .expect("could not bind the relay listener");
498        let relay_addr = listener
499            .local_addr()
500            .expect("the relay listener must expose its address");
501        let barrier = Arc::new(Barrier::new(2));
502
503        let upfront = start_upfront_middleware(relay_addr, barrier.clone(), command);
504
505        barrier.wait();
506        let session_stream = loop {
507            if let Ok((stream, _addr)) = listener.accept() {
508                break stream;
509            }
510        };
511
512        let mut pool = Pool::with_capacity(1, 2, 16_384);
513        let front_buf = pool.checkout().expect("the pool must hand out a buffer");
514        let mut relay =
515            RelayProxyProtocol::new(session_stream, Token(0), Ulid::generate(), None, front_buf);
516
517        let mut session_metrics = SessionMetrics::new(None);
518        // The front socket is non-blocking, so `readable` legitimately returns
519        // without progress until the peer's bytes land. The bound is wall-clock
520        // rather than an iteration count: a spin count turns a scheduling stall
521        // under load into a spurious failure, whereas this only fires if the
522        // header genuinely never arrives. It mirrors the 10s frontend timeout
523        // the expect state uses.
524        let deadline = Instant::now() + Duration::from_secs(10);
525        while relay.header_size.is_none() {
526            assert_eq!(
527                relay.readable(&mut session_metrics),
528                SessionResult::Continue,
529                "the relay must keep reading until a complete header is parsed"
530            );
531            assert!(
532                Instant::now() < deadline,
533                "the relay never parsed a complete PROXY-v2 header within 10s"
534            );
535        }
536
537        upfront.join().expect("should join");
538
539        relay.addresses.as_ref().and_then(ProxyAddr::source)
540    }
541
542    #[test]
543    fn a_proxy_command_header_attributes_the_encapsulated_source() {
544        assert_eq!(
545            attributed_source_for(Command::Proxy),
546            Some(header_src()),
547            "a PROXY header still carries the real client through to the pipe phase"
548        );
549    }
550
551    /// The PROXY protocol `LOCAL` command (ver/cmd `0x20`) describes a
552    /// connection the upstream proxy originated itself -- typically a health
553    /// check -- and the HAProxy PROXY protocol specification §2.2 requires the
554    /// receiver to discard its address block. Nothing on the wire pairs
555    /// `LOCAL` with `AF_UNSPEC`; only HAProxy's own emitter happens to. The
556    /// header written below is exactly what `HeaderV2::new(Command::Local, ..)`
557    /// emits: ver/cmd `0x20`, family `0x11`, and a populated 12-byte `AF_INET`
558    /// block.
559    ///
560    /// `into_pipe` prefers `self.addresses.source()` over the socket's
561    /// `peer_addr`, so before the fix that forged pair became the client
562    /// address in the access logs, in `X-Real-IP` and in the
563    /// `max_connections_per_ip` counters. The header bytes themselves are
564    /// relayed verbatim to the backend either way -- `back_writable` forwards
565    /// the buffered prefix, it never re-serializes `addresses`. That claim was
566    /// written here before anything tested it, and was in fact false until
567    /// `readable` stopped consuming the header; it is now pinned by
568    /// `back_writable_forwards_the_header_verbatim`.
569    ///
570    /// To SEE THIS RED: in `parse_v2_header`
571    /// (`lib/src/protocol/proxy_protocol/parser.rs`), replace the
572    /// `Command::Local => ProxyAddr::AfUnspec` arm of the `addr` binding with
573    /// `parsed_addr` -- this then yields `Some(125.25.10.1:8080)`.
574    #[test]
575    fn a_local_command_header_attributes_no_source_address() {
576        assert_eq!(
577            attributed_source_for(Command::Local),
578            None,
579            "a LOCAL header must attribute no source, so into_pipe falls back to peer_addr"
580        );
581    }
582
583    // Connect to the relay and send one proxy protocol header.
584    fn start_upfront_middleware(
585        relay_addr: SocketAddr,
586        barrier: Arc<Barrier>,
587        command: Command,
588    ) -> JoinHandle<()> {
589        thread::spawn(move || {
590            let proxy_protocol = HeaderV2::new(command, header_src(), header_dst()).into_bytes();
591
592            barrier.wait();
593            match StdTcpStream::connect(relay_addr) {
594                Ok(mut stream) => {
595                    stream.write_all(&proxy_protocol).unwrap();
596                }
597                Err(e) => panic!("could not connect to the relay: {e}"),
598            };
599        })
600    }
601
602    /// Exactly what a PROXY-aware peer puts on the wire ahead of the payload:
603    /// the 28-byte PROXY-v2 `PROXY` header over IPv4.
604    fn wire_header() -> Vec<u8> {
605        HeaderV2::new(Command::Proxy, header_src(), header_dst()).into_bytes()
606    }
607
608    /// How long each stage BEFORE `back_writable` -- the header-parse loop and
609    /// the pipelined top-up -- may take. Deliberately well below the
610    /// `recv_timeout` every caller passes (5s), so a stage that stalls reports
611    /// ITSELF instead of letting the outer timeout expire and blame
612    /// `back_writable`. A parse or read defect must never be able to describe
613    /// itself as the remote-triggerable worker wedge; this PR's own history is
614    /// one such misattribution and the harness must not be able to produce
615    /// another.
616    const STAGE_DEADLINE: Duration = Duration::from_secs(3);
617
618    /// Everything one relay pass hands back across the worker-thread boundary.
619    /// `Checkout` and the mio sockets stay inside the worker; only plain data
620    /// crosses.
621    #[derive(Debug)]
622    struct RelayPass {
623        /// What `back_writable` returned.
624        result: SessionResult,
625        /// The bytes the backend actually received.
626        forwarded: Vec<u8>,
627        /// What `frontend_buffer` still holds once `back_writable` returned,
628        /// i.e. exactly what `into_pipe` hands `Pipe::new`.
629        pending_for_pipe: Vec<u8>,
630        /// Whether the backend WRITABLE *event* survived the pass.
631        backend_writable_event: bool,
632    }
633
634    /// Drives one complete relay pass on a worker thread: a real loopback
635    /// client writes `client_bytes` at a real frontend socket, `readable`
636    /// parses the header, and `back_writable` forwards it onto a real
637    /// connected backend.
638    ///
639    /// Returns `None` when the pass did not come back within `timeout` -- which
640    /// is exactly what a spinning `back_writable` looks like from the outside.
641    /// A wedged worker thread cannot be cancelled, so it keeps burning its core
642    /// until the test binary exits. That is the point: in production that
643    /// thread is the whole sozu worker, and every co-resident session starves
644    /// with it.
645    ///
646    /// `starve_frontend_buffer` drains the buffer after the header is parsed,
647    /// reproducing -- without depending on `readable` -- the state a
648    /// header-consuming read path leaves behind.
649    fn run_relay_pass(
650        client_bytes: Vec<u8>,
651        starve_frontend_buffer: bool,
652        timeout: Duration,
653    ) -> Option<RelayPass> {
654        setup_test_logger!();
655        let (sender, receiver) = mpsc::channel();
656
657        thread::spawn(move || {
658            let expected_buffered = client_bytes.len();
659
660            // Frontend: a real accepted loopback connection already carrying
661            // the client's bytes.
662            let front_listener =
663                TcpListener::bind("127.0.0.1:0".parse().expect("parse frontend address"))
664                    .expect("could not bind the relay listener");
665            let front_addr = front_listener
666                .local_addr()
667                .expect("the relay listener must expose its address");
668            let mut client =
669                StdTcpStream::connect(front_addr).expect("the client could not reach the relay");
670            client
671                .write_all(&client_bytes)
672                .expect("the client could not write its bytes");
673            let frontend = loop {
674                if let Ok((stream, _addr)) = front_listener.accept() {
675                    break stream;
676                }
677            };
678
679            // Backend: a real connected TCP pair. The relay owns the writing
680            // end; `backend_peer` is what a PROXY-aware backend sees.
681            let backend_listener =
682                StdTcpListener::bind("127.0.0.1:0").expect("could not bind the backend listener");
683            let backend_addr = backend_listener
684                .local_addr()
685                .expect("the backend listener must expose its address");
686            let relay_side =
687                StdTcpStream::connect(backend_addr).expect("the relay could not reach the backend");
688            let (mut backend_peer, _addr) = backend_listener
689                .accept()
690                .expect("the backend must accept the relay connection");
691            relay_side
692                .set_nonblocking(true)
693                .expect("the relay's backend socket must be non-blocking");
694
695            let mut pool = Pool::with_capacity(1, 2, 16_384);
696            let front_buf = pool.checkout().expect("the pool must hand out a buffer");
697            let mut relay = RelayProxyProtocol::new(
698                frontend,
699                Token(0),
700                Ulid::generate(),
701                Some(TcpStream::from_std(relay_side)),
702                front_buf,
703            );
704            relay.set_back_token(Token(1));
705            // What the event loop hands `back_writable`: a connected backend
706            // whose WRITABLE edge has been observed.
707            relay.backend_readiness.interest.insert(Ready::WRITABLE);
708            relay.backend_readiness.event.insert(Ready::WRITABLE);
709
710            let mut metrics = SessionMetrics::new(None);
711            // The front socket is non-blocking, so `readable` legitimately
712            // makes no progress until the peer's bytes land. Failures here are
713            // SENT rather than asserted: a panic in this thread only drops the
714            // channel, and the caller would then see the same `None` a wedged
715            // `back_writable` produces and report the wedge.
716            let deadline = Instant::now() + STAGE_DEADLINE;
717            while relay.header_size.is_none() {
718                let result = relay.readable(&mut metrics);
719                if result != SessionResult::Continue {
720                    let _ = sender.send(Err(format!(
721                        "readable returned {result:?} before a complete header was parsed"
722                    )));
723                    return;
724                }
725                if Instant::now() >= deadline {
726                    let _ = sender.send(Err(format!(
727                        "the relay never parsed a complete PROXY-v2 header within {STAGE_DEADLINE:?}"
728                    )));
729                    return;
730                }
731            }
732            // The client wrote header and payload in a single `write_all`
733            // before the relay ever accepted, so loopback delivers both in one
734            // read -- but TCP promises no such thing, and a split read would
735            // silently downgrade the pipelined case into the header-only one.
736            // Bounded top-up; a no-op on loopback.
737            //
738            // The gate is `metrics.bin` -- what `readable` has pulled off the
739            // SOCKET -- and deliberately not `frontend_buffer.available_data()`,
740            // which is what survives in the buffer. A mutation to the
741            // consumption logic (the very thing these tests exist to catch)
742            // drives `available_data()` to zero, so gating on it would stall
743            // this loop for its full deadline and redden every test in here
744            // WITHOUT EVER REACHING `back_writable` -- a green-looking red that
745            // proves nothing. `metrics.bin` only ever counts bytes that arrived.
746            while metrics.bin < expected_buffered && Instant::now() < deadline {
747                let result = relay.readable(&mut metrics);
748                if result != SessionResult::Continue {
749                    let _ = sender.send(Err(format!(
750                        "readable returned {result:?} while the client's bytes were still in flight"
751                    )));
752                    return;
753                }
754            }
755
756            if starve_frontend_buffer {
757                let buffered = relay.frontend_buffer.available_data();
758                relay.frontend_buffer.consume(buffered);
759            }
760
761            let result = relay.back_writable(&mut metrics);
762
763            // Read back whatever reached the backend. A short read timeout is
764            // enough: `back_writable` has already returned, so every byte it
765            // was ever going to emit is in flight.
766            backend_peer
767                .set_read_timeout(Some(Duration::from_millis(300)))
768                .expect("the backend peer must accept a read timeout");
769            let mut forwarded = Vec::new();
770            let mut chunk = [0u8; 256];
771            loop {
772                match backend_peer.read(&mut chunk) {
773                    Ok(0) => break,
774                    Ok(sz) => forwarded.extend_from_slice(&chunk[..sz]),
775                    Err(_) => break,
776                }
777            }
778
779            let _ = sender.send(Ok(RelayPass {
780                result,
781                forwarded,
782                pending_for_pipe: relay.frontend_buffer.data().to_vec(),
783                backend_writable_event: relay.backend_readiness.event.is_writable(),
784            }));
785        });
786
787        match receiver.recv_timeout(timeout) {
788            Ok(Ok(pass)) => Some(pass),
789            // A stage BEFORE `back_writable` failed. Surface THAT, so a parse
790            // or read defect names itself instead of arriving as a `None` the
791            // caller can only read as the wedge.
792            Ok(Err(stage)) => panic!("{stage}"),
793            // Nothing came back in time: the pass is stuck inside
794            // `back_writable`, which is the wedge the callers assert on.
795            Err(_) => None,
796        }
797    }
798
799    /// The wedge. `readable` used to `consume(sz)` everything it had just read
800    /// -- the header included -- leaving `back_writable` looping on
801    /// `socket.write(&[])`, which a connected non-blocking socket answers
802    /// `Ok(0)` forever: `cursor_header += 0` never reaches `header_size`, the
803    /// only other exit is the `Err` arm, and the worker spins at 100% CPU with
804    /// its event loop starved. `MAX_LOOP_ITERATIONS` bounds the OUTER dispatch
805    /// loop in `tcp.rs::ready_inner`, never this inner one, and a single
806    /// unauthenticated TCP connection to a `RelayHeader` cluster reaches it.
807    ///
808    /// To SEE THIS RED: BOTH halves of the fix have to go, because each one
809    /// alone now stops the wedge -- that redundancy is the point. In `readable`
810    /// re-add `self.frontend_buffer.consume(sz);` immediately after
811    /// `self.header_size = Some(read_sz);`, AND delete the `Ok(0)` arm from
812    /// `back_writable`'s `match socket.write(..)`. That is the original defect
813    /// exactly, and `back_writable` then never returns, failing this test on
814    /// its 5s deadline. Re-adding the `consume` on its own leaves this test
815    /// GREEN: the empty-buffer write lands in the `Ok(0)` arm, which yields --
816    /// see `a_zero_length_write_yields_instead_of_spinning`.
817    #[test]
818    fn back_writable_returns_instead_of_spinning() {
819        assert!(
820            run_relay_pass(wire_header(), false, Duration::from_secs(5)).is_some(),
821            "back_writable did not return within 5s: this is the remote-triggerable worker wedge"
822        );
823    }
824
825    /// The correctness half. A `RelayHeader` cluster exists precisely so the
826    /// backend receives the client's OWN header, byte for byte --
827    /// `back_writable` forwards the buffered prefix, it never re-serializes
828    /// `addresses`.
829    ///
830    /// Nothing covered this before: the other `relay_test` cases stop at
831    /// `readable`, and e2e's `try_tcp_sni_relay_proxy_forwards_header_verbatim`
832    /// drives the SNI-preread path, which reaches `Pipe` through
833    /// `tcp.rs::build_pipe_from_preread` without ever building a
834    /// `RelayProxyProtocol`.
835    ///
836    /// To SEE THIS RED: in `readable`, replace `self.header_size = Some(read_sz);`
837    /// with `self.header_size = Some(read_sz - 4);`. The backend then receives a
838    /// truncated 24-byte header and the comparison below fails.
839    #[test]
840    fn back_writable_forwards_the_header_verbatim() {
841        let header = wire_header();
842        let pass = run_relay_pass(header.clone(), false, Duration::from_secs(5))
843            .expect("back_writable must return within 5s");
844
845        assert_eq!(
846            pass.result,
847            SessionResult::Upgrade,
848            "a fully forwarded header must upgrade the session to the pipe phase"
849        );
850        assert_eq!(
851            pass.forwarded, header,
852            "the backend must receive the client's PROXY-v2 header byte for byte"
853        );
854        assert!(
855            pass.pending_for_pipe.is_empty(),
856            "a header-only client leaves the pipe phase nothing to flush"
857        );
858    }
859
860    /// A client is free to pipeline payload into the same read as its header.
861    /// Those bytes belong to the PIPE phase, not to the relay: `into_pipe`
862    /// hands `frontend_buffer` straight to `Pipe::new`, whose
863    /// `arm_inherited_buffer_writes` arms the backend WRITABLE readiness
864    /// exactly when that buffer is non-empty, and `Pipe::backend_writable`
865    /// drains it (covered in `protocol/pipe.rs` by
866    /// `backend_writable_drains_inherited_frontend_buffer_before_splice_engages`).
867    /// So the relay forwards the header and stops there; dropping the tail
868    /// would be silent data loss.
869    ///
870    /// To SEE THIS RED: in `back_writable`, widen the write back to the whole
871    /// buffer -- `let offered = buffered.len();` instead of
872    /// `remaining.min(buffered.len())`. The relay then forwards the payload
873    /// too and hands the pipe an empty buffer, so the "forwards the header and
874    /// nothing more" assertion below fails on the full 75-byte read.
875    #[test]
876    fn back_writable_leaves_pipelined_payload_for_the_pipe_phase() {
877        let header = wire_header();
878        let payload = b"GET / HTTP/1.1\r\nHost: pipelined.example.com\r\n\r\n".to_vec();
879        let mut client_bytes = header.clone();
880        client_bytes.extend_from_slice(&payload);
881
882        let pass = run_relay_pass(client_bytes, false, Duration::from_secs(5))
883            .expect("back_writable must return within 5s");
884
885        assert_eq!(
886            pass.result,
887            SessionResult::Upgrade,
888            "a fully forwarded header must upgrade the session to the pipe phase"
889        );
890        assert_eq!(
891            pass.forwarded, header,
892            "the relay forwards the header and nothing more"
893        );
894        assert_eq!(
895            pass.pending_for_pipe, payload,
896            "pipelined payload must survive in frontend_buffer for the pipe phase"
897        );
898    }
899
900    /// The zero-write guard itself, independent of `readable`: whatever leaves
901    /// the frontend buffer empty while a `header_size` is pending, a
902    /// zero-length write must cost one syscall and then yield.
903    /// `send.rs::back_writable` answers the same "the backend took nothing"
904    /// condition by dropping the WRITABLE readiness and returning, and
905    /// `expect.rs::readable` does the same on a zero-length read.
906    ///
907    /// To SEE THIS RED: delete the `Ok(0)` arm from `back_writable`'s
908    /// `match socket.write(..)`, folding it back into `Ok(sz)`.
909    /// `cursor_header += 0` then never reaches `header_size` and this test
910    /// fails on its 5s deadline.
911    #[test]
912    fn a_zero_length_write_yields_instead_of_spinning() {
913        let pass = run_relay_pass(wire_header(), true, Duration::from_secs(5))
914            .expect("back_writable must return within 5s on a zero-length write");
915
916        assert_eq!(
917            pass.result,
918            SessionResult::Continue,
919            "a zero-length write makes no progress, so the session continues instead of upgrading"
920        );
921        assert!(
922            pass.forwarded.is_empty(),
923            "an empty buffer has nothing to forward"
924        );
925        assert!(
926            !pass.backend_writable_event,
927            "a zero-length write must drop the backend WRITABLE event so the next epoll edge drives the retry"
928        );
929    }
930
931    /// A real connected loopback TCP pair, both ends non-blocking. When
932    /// `socket_buffer` is `Some`, the kernel send/receive queues are shrunk so
933    /// a bounded write can actually exhaust them.
934    fn connected_pair(socket_buffer: Option<usize>) -> (StdTcpStream, StdTcpStream) {
935        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind the test listener");
936        if let Some(size) = socket_buffer {
937            // Set before the handshake so the accepted socket inherits it.
938            SockRef::from(&listener)
939                .set_recv_buffer_size(size)
940                .expect("shrink the listener receive queue");
941        }
942        let address = listener
943            .local_addr()
944            .expect("the test listener must expose its address");
945        let local = StdTcpStream::connect(address).expect("connect the test pair");
946        let (peer, _addr) = listener.accept().expect("accept the test pair");
947        if let Some(size) = socket_buffer {
948            SockRef::from(&local)
949                .set_send_buffer_size(size)
950                .expect("shrink the local send queue");
951            SockRef::from(&peer)
952                .set_recv_buffer_size(size)
953                .expect("shrink the peer receive queue");
954        }
955        local
956            .set_nonblocking(true)
957            .expect("the local end must be non-blocking");
958        peer.set_nonblocking(true)
959            .expect("the peer end must be non-blocking");
960        (local, peer)
961    }
962
963    /// `back_writable`'s `Err` arm only went live with the forwarding fix
964    /// above: before it, `readable` emptied the buffer, so the loop called
965    /// `write(&[])` and got `Ok(0)` forever without ever reaching an error.
966    /// Now that the relay really writes, a backpressured backend is reachable
967    /// — a v2 header is `16 + len` with `len: u16`, so it can be far larger
968    /// than a socket send queue (the 232-byte cap belongs to `expect.rs`'s
969    /// fixed array, not here, and this state reads into a pool `Checkout`).
970    /// That must park the session on the next epoll edge, not tear it down:
971    /// clearing WRITABLE from `interest` as well would mean no later edge is
972    /// ever acted on and the half-written PROXY header never completes, so the
973    /// session dies at the frontend timeout with a truncated header already on
974    /// the backend.
975    ///
976    /// To SEE THIS RED: delete the `ErrorKind::WouldBlock` arm from
977    /// `back_writable`'s `Err(e) => match e.kind()`, leaving the catch-all that
978    /// calls `frontend_readiness.reset()` / `backend_readiness.reset()`.
979    #[test]
980    fn back_writable_parks_the_session_on_a_backpressured_backend() {
981        setup_test_logger!();
982        let (frontend, _front_peer) = connected_pair(None);
983        // 4 KiB queues each way, and the peer never reads, so the 256 KiB
984        // header below cannot drain and the write must hit WouldBlock.
985        let (relay_side, _backend_peer) = connected_pair(Some(4096));
986
987        let header_size = 256 * 1024;
988        let mut pool = Pool::with_capacity(1, 1, header_size);
989        let mut front_buf = pool.checkout().expect("the pool must hand out a buffer");
990        front_buf.space().fill(b'x');
991        front_buf.fill(header_size);
992
993        let mut relay = RelayProxyProtocol::new(
994            TcpStream::from_std(frontend),
995            Token(0),
996            Ulid::generate(),
997            Some(TcpStream::from_std(relay_side)),
998            front_buf,
999        );
1000        relay.header_size = Some(header_size);
1001        relay.backend_readiness.interest.insert(Ready::WRITABLE);
1002        relay.backend_readiness.event.insert(Ready::WRITABLE);
1003
1004        let mut metrics = SessionMetrics::new(None);
1005        assert_eq!(
1006            relay.back_writable(&mut metrics),
1007            SessionResult::Continue,
1008            "a backpressured backend parks the session, it does not close it"
1009        );
1010        assert!(
1011            !relay.backend_readiness.event.is_writable(),
1012            "WouldBlock must clear the stale WRITABLE event so the loop yields"
1013        );
1014        assert!(
1015            relay.backend_readiness.interest.is_writable(),
1016            "WouldBlock must KEEP the WRITABLE interest, or no later epoll edge is acted on and the half-written header never completes"
1017        );
1018        assert!(
1019            relay.frontend_readiness.interest.is_hup(),
1020            "a backpressured backend must not tear down the frontend readiness"
1021        );
1022        assert!(
1023            metrics.backend_bout > 0 && metrics.backend_bout < header_size,
1024            "the write must have made partial progress before blocking: {} of {header_size} bytes",
1025            metrics.backend_bout
1026        );
1027    }
1028
1029    /// The same missing-guard defect class one function up, in `readable`.
1030    /// `tcp_socket_read` returns `(0, SocketResult::Continue)` the moment the
1031    /// slice it is handed is empty (`lib/src/socket.rs`), which is exactly what
1032    /// `frontend_buffer.space()` yields once the buffer is full. A client that
1033    /// declares a large `len` and then stops fills the buffer, the streaming
1034    /// parser answers `Incomplete`, and `readable` returns `Continue` with
1035    /// READABLE still set in both readiness words — so `ready_inner` calls it
1036    /// again immediately, with no space to read into. `MAX_LOOP_ITERATIONS`
1037    /// bounds that OUTER loop, so this is CPU amplification and a spurious
1038    /// close rather than a wedge, but it is the same guard that was missing
1039    /// below. `ExpectProxyProtocol::readable`
1040    /// (`lib/src/protocol/proxy_protocol/expect.rs`) drops READABLE on any
1041    /// non-positive read; this now matches it.
1042    ///
1043    /// To SEE THIS RED: delete the `else` branch `readable` takes when
1044    /// `sz == 0`.
1045    #[test]
1046    fn readable_yields_on_a_zero_length_read() {
1047        setup_test_logger!();
1048        let (frontend, mut client) = connected_pair(None);
1049
1050        // A well-formed v2 prefix that declares 65535 address bytes and then
1051        // stops: the parser can never complete it, and the filler fills the
1052        // session buffer exactly.
1053        let capacity = 64;
1054        let mut stalled = Vec::with_capacity(capacity);
1055        stalled.extend_from_slice(&[
1056            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
1057        ]);
1058        stalled.push(0x21); // version 2, command PROXY
1059        stalled.push(0x11); // AF_INET over STREAM
1060        stalled.extend_from_slice(&u16::MAX.to_be_bytes());
1061        stalled.resize(capacity, b'x');
1062        client
1063            .write_all(&stalled)
1064            .expect("the client must be able to write its stalled header");
1065
1066        let mut pool = Pool::with_capacity(1, 1, capacity);
1067        let front_buf = pool.checkout().expect("the pool must hand out a buffer");
1068        let mut relay = RelayProxyProtocol::new(
1069            TcpStream::from_std(frontend),
1070            Token(0),
1071            Ulid::generate(),
1072            None,
1073            front_buf,
1074        );
1075        // What the event loop hands `readable`.
1076        relay.frontend_readiness.event.insert(Ready::READABLE);
1077
1078        let mut metrics = SessionMetrics::new(None);
1079        let deadline = Instant::now() + Duration::from_secs(10);
1080        while relay.frontend_buffer.available_space() > 0 && Instant::now() < deadline {
1081            assert_eq!(
1082                relay.readable(&mut metrics),
1083                SessionResult::Continue,
1084                "an incomplete header keeps the session reading"
1085            );
1086        }
1087        assert_eq!(
1088            relay.frontend_buffer.available_data(),
1089            capacity,
1090            "the stalled header must fill the whole session buffer"
1091        );
1092        assert!(
1093            relay.header_size.is_none(),
1094            "the declared 65535-byte address block never arrives, so no header is ever parsed"
1095        );
1096
1097        // No space left, so this read is handed an empty slice and returns
1098        // zero bytes without touching the socket.
1099        assert_eq!(
1100            relay.readable(&mut metrics),
1101            SessionResult::Continue,
1102            "a zero-length read keeps the session alive"
1103        );
1104        assert!(
1105            !relay.frontend_readiness.event.is_readable(),
1106            "a zero-length read must clear the READABLE event; leaving it set makes ready_inner call readable again at once, up to MAX_LOOP_ITERATIONS"
1107        );
1108    }
1109
1110    /// `back_writable`'s `Interrupted` arm, exercised for real.
1111    ///
1112    /// The arm is DEFENSIVE: it is unreachable in production, because the
1113    /// backend socket is built by `mio::net::TcpStream::connect` in
1114    /// `Backend::try_connect` (`lib/src/backends.rs`) and is therefore always
1115    /// non-blocking, and a non-blocking `send` answers EAGAIN, never EINTR.
1116    /// Reaching it at all needs a blocking socket, so this test builds one.
1117    /// The seam is that `mio::net::TcpStream::from_std` only wraps a
1118    /// descriptor and accepts any socket fd: a `socketpair(AF_UNIX,
1119    /// SOCK_STREAM)` -- deliberately not a pipe, whose fd answers `send(2)`
1120    /// with `ENOTSOCK` (os error 88) -- is filled until it refuses another
1121    /// byte, so the next blocking send PARKS with nothing written. That is
1122    /// what makes the outcome deterministic: with zero free space a partial
1123    /// `Ok(n)` is impossible and EINTR is the only way out. SIGUSR1 is
1124    /// installed WITHOUT `SA_RESTART`, so the parked send returns
1125    /// `Interrupted` instead of being resumed by the kernel.
1126    ///
1127    /// The contract under test: a signal is not a socket state change, so
1128    /// edge-triggered epoll owes no new edge and BOTH readiness words must
1129    /// survive untouched. `tcp.rs::ready_inner` then re-enters `back_writable`
1130    /// on its own `MAX_LOOP_ITERATIONS`-bounded loop, and that re-entry is the
1131    /// retry -- the shape `udp.rs` uses for EINTR, without an unbounded
1132    /// `continue` in here.
1133    ///
1134    /// To SEE THIS RED: add `self.backend_readiness.event.remove(Ready::WRITABLE);`
1135    /// to the `ErrorKind::Interrupted` arm, i.e. give it the `WouldBlock`
1136    /// treatment.
1137    #[test]
1138    fn back_writable_keeps_its_readiness_when_a_signal_interrupts_the_write() {
1139        setup_test_logger!();
1140        extern "C" fn noop_handler(_signal: libc::c_int) {}
1141
1142        let (frontend, _front_peer) = connected_pair(None);
1143
1144        let mut fds = [0 as libc::c_int; 2];
1145        assert_eq!(
1146            unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) },
1147            0,
1148            "the socketpair must be created"
1149        );
1150        let (write_fd, read_fd) = (fds[0], fds[1]);
1151        for (fd, option) in [(write_fd, libc::SO_SNDBUF), (read_fd, libc::SO_RCVBUF)] {
1152            let size: libc::c_int = 4096;
1153            assert_eq!(
1154                unsafe {
1155                    libc::setsockopt(
1156                        fd,
1157                        libc::SOL_SOCKET,
1158                        option,
1159                        std::ptr::addr_of!(size).cast(),
1160                        std::mem::size_of::<libc::c_int>() as libc::socklen_t,
1161                    )
1162                },
1163                0,
1164                "the socketpair buffers must be shrinkable"
1165            );
1166        }
1167
1168        // Fill it until it refuses another byte. `MSG_DONTWAIT` keeps the
1169        // descriptor itself blocking, which is what the write under test needs.
1170        let chunk = [b'f'; 4096];
1171        let mut filled = 0usize;
1172        loop {
1173            let sent = unsafe {
1174                libc::send(
1175                    write_fd,
1176                    chunk.as_ptr().cast(),
1177                    chunk.len(),
1178                    libc::MSG_DONTWAIT,
1179                )
1180            };
1181            if sent < 0 {
1182                break;
1183            }
1184            filled += sent as usize;
1185            assert!(filled < 8 << 20, "the socketpair never stopped accepting");
1186        }
1187        assert!(filled > 0, "the socketpair must have accepted some bytes");
1188
1189        let header_size = 64;
1190        let mut pool = Pool::with_capacity(1, 1, header_size);
1191        let mut front_buf = pool.checkout().expect("the pool must hand out a buffer");
1192        front_buf.space().fill(b'h');
1193        front_buf.fill(header_size);
1194
1195        // SAFETY: `write_fd` is a live socket descriptor this test owns and
1196        // never touches again. Ownership passes to the stream, whose `Drop`
1197        // closes it. `from_std` only wraps the descriptor, so the socket stays
1198        // blocking -- exactly what makes EINTR reachable here.
1199        let backend = unsafe { StdTcpStream::from_raw_fd(write_fd) };
1200        let mut relay = RelayProxyProtocol::new(
1201            TcpStream::from_std(frontend),
1202            Token(0),
1203            Ulid::generate(),
1204            Some(TcpStream::from_std(backend)),
1205            front_buf,
1206        );
1207        relay.header_size = Some(header_size);
1208        relay.backend_readiness.interest.insert(Ready::WRITABLE);
1209        relay.backend_readiness.event.insert(Ready::WRITABLE);
1210
1211        let mut handler: libc::sigaction = unsafe { std::mem::zeroed() };
1212        handler.sa_sigaction = noop_handler as *const () as usize;
1213        handler.sa_flags = 0; // deliberately NOT SA_RESTART
1214        unsafe { libc::sigemptyset(&mut handler.sa_mask) };
1215        let mut previous: libc::sigaction = unsafe { std::mem::zeroed() };
1216        assert_eq!(
1217            unsafe { libc::sigaction(libc::SIGUSR1, &handler, &mut previous) },
1218            0,
1219            "the SIGUSR1 handler must install"
1220        );
1221
1222        // If the signal is somehow missed, drain the peer so the parked send
1223        // completes: the test then FAILS on its assertions instead of hanging
1224        // the suite.
1225        let finished = Arc::new(AtomicBool::new(false));
1226        let watchdog = {
1227            let finished = Arc::clone(&finished);
1228            thread::spawn(move || {
1229                let deadline = Instant::now() + Duration::from_secs(5);
1230                while !finished.load(Ordering::SeqCst) {
1231                    if Instant::now() >= deadline {
1232                        let mut sink = [0u8; 4096];
1233                        loop {
1234                            let got = unsafe {
1235                                libc::recv(
1236                                    read_fd,
1237                                    sink.as_mut_ptr().cast(),
1238                                    sink.len(),
1239                                    libc::MSG_DONTWAIT,
1240                                )
1241                            };
1242                            if got <= 0 {
1243                                break;
1244                            }
1245                        }
1246                        return;
1247                    }
1248                    thread::sleep(Duration::from_millis(25));
1249                }
1250            })
1251        };
1252
1253        let target = unsafe { libc::pthread_self() };
1254        let signaller = thread::spawn(move || {
1255            thread::sleep(Duration::from_millis(250));
1256            unsafe { libc::pthread_kill(target, libc::SIGUSR1) };
1257        });
1258
1259        let mut metrics = SessionMetrics::new(None);
1260        let result = relay.back_writable(&mut metrics);
1261
1262        finished.store(true, Ordering::SeqCst);
1263        signaller
1264            .join()
1265            .expect("the signalling thread must not panic");
1266        watchdog.join().expect("the watchdog thread must not panic");
1267        // Restore only AFTER joining the signaller: restoring first lets a late
1268        // SIGUSR1 reach the default disposition, which kills the test process
1269        // with signal 10.
1270        unsafe { libc::sigaction(libc::SIGUSR1, &previous, std::ptr::null_mut()) };
1271        unsafe { libc::close(read_fd) };
1272
1273        assert_eq!(
1274            result,
1275            SessionResult::Continue,
1276            "an interrupted write parks the session, it does not close it"
1277        );
1278        assert_eq!(
1279            metrics.backend_bout, 0,
1280            "the send parked on a full socket, so no byte can have been written"
1281        );
1282        assert!(
1283            relay.backend_readiness.event.is_writable(),
1284            "Interrupted must LEAVE the WRITABLE event set: the socket did not change state, so edge-triggered epoll owes no new edge and clearing it strands the session"
1285        );
1286        assert!(
1287            relay.backend_readiness.interest.is_writable(),
1288            "Interrupted must leave the WRITABLE interest set so ready_inner re-enters back_writable"
1289        );
1290    }
1291}