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}