Skip to main content

postrust_proxy/vendored/
forwarder.rs

1//! Vendored forwarder client from rpxy-lib: forwarder/client.rs
2//!
3//! This module handles forwarding requests to upstream backends.
4
5use crate::config::{Backend, UpstreamHttpVersion};
6use crate::vendored::conn::InFlight;
7use crate::vendored::hyper_ext::{empty_body, IncomingBodyExt, ProxyBody};
8use crate::vendored::types::ProxyError;
9use base64::Engine;
10use hyper::body::Incoming;
11use hyper::header::{CONNECTION, HOST, UPGRADE};
12use hyper::{Method, Request, Response, StatusCode, Version};
13use hyper_util::client::legacy::Client;
14use hyper_util::rt::TokioExecutor as HyperTokioExecutor;
15use hyper_util::rt::TokioIo;
16use rand::RngCore;
17use std::time::Duration;
18use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
19use tokio::net::TcpStream;
20use tracing::debug;
21
22/// HTTP connector type alias.
23type HttpConnector = hyper_util::client::legacy::connect::HttpConnector;
24
25/// Forwarder client for sending requests to upstream backends.
26pub struct ForwarderClient {
27    /// HTTP/1.1 client, used for every backend that has not opted into h2c.
28    http_client: Client<HttpConnector, ProxyBody>,
29    /// Prior-knowledge h2c client, for backends declared as `http_version = "h2c"`.
30    ///
31    /// A separate client because the choice is per-connection and h2c has no
32    /// ALPN to negotiate it -- the pool has to be told up front which protocol
33    /// its connections speak.
34    http2_client: Client<HttpConnector, ProxyBody>,
35    /// Request timeout
36    timeout: Duration,
37}
38
39impl ForwarderClient {
40    /// Create a new forwarder client.
41    pub fn new(timeout: Duration) -> Self {
42        let mut http_connector = HttpConnector::new();
43        // Without this, Nagle batches small writes and, paired with delayed ACK
44        // on the far side, adds latency to every small forwarded request.
45        http_connector.set_nodelay(true);
46        let http_client = Client::builder(HyperTokioExecutor::new())
47            .pool_idle_timeout(Duration::from_secs(30))
48            .pool_max_idle_per_host(32)
49            .build(http_connector);
50
51        let mut http2_connector = HttpConnector::new();
52        http2_connector.set_nodelay(true);
53        let http2_client = Client::builder(HyperTokioExecutor::new())
54            .pool_idle_timeout(Duration::from_secs(30))
55            .http2_only(true)
56            .build(http2_connector);
57
58        Self {
59            http_client,
60            http2_client,
61            timeout,
62        }
63    }
64
65    /// Forward a request to a backend.
66    pub async fn forward(
67        &self,
68        backend: &Backend,
69        mut request: Request<ProxyBody>,
70    ) -> Result<Response<Incoming>, ProxyError> {
71        // Build the upstream URL
72        let uri = request.uri();
73        let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
74
75        let upstream_uri = format!("{}://{}{}", backend.scheme, backend.address, path_and_query);
76
77        // Update request URI
78        *request.uri_mut() = upstream_uri
79            .parse()
80            .map_err(|e| ProxyError::Request(format!("Invalid upstream URI: {}", e)))?;
81
82        // HTTP/2 is a per-hop protocol, so the inbound version never carries onto
83        // the upstream connection: it is whatever this backend was declared to
84        // speak. Getting this wrong is not subtle -- the client rejects the
85        // request as UserUnsupportedVersion before a byte goes out.
86        let (client, version) = match backend.http_version {
87            UpstreamHttpVersion::H2c => (&self.http2_client, Version::HTTP_2),
88            UpstreamHttpVersion::Http11 => (&self.http_client, Version::HTTP_11),
89        };
90        *request.version_mut() = version;
91
92        // Send request with timeout
93        let response = tokio::time::timeout(self.timeout, client.request(request))
94            .await
95            .map_err(|_| ProxyError::Timeout)?
96            .map_err(|e| ProxyError::Connection(e.to_string()))?;
97
98        Ok(response)
99    }
100
101    /// Forward a protocol-upgrade request (WebSocket) and tunnel the result.
102    ///
103    /// This cannot use the pooled client: once the upstream answers 101 the
104    /// connection stops being request/response and becomes an opaque byte
105    /// stream, which a connection pool has no way to hand back. So dial the
106    /// backend directly, keep the lower-level connection, and splice the two
107    /// upgraded halves together.
108    ///
109    /// HTTP/1.1 only; [`Self::forward_h2_websocket`] is the HTTP/2 spelling.
110    ///
111    /// Which protocols may be upgraded to is the caller's decision, made
112    /// before this is called: this tunnels whatever `Upgrade` the request
113    /// carries. The tunnel is closed after `tunnel_idle` with no bytes in
114    /// either direction.
115    pub async fn forward_upgrade(
116        &self,
117        backend: &Backend,
118        mut request: Request<ProxyBody>,
119        tunnel_idle: Duration,
120    ) -> Result<Response<ProxyBody>, ProxyError> {
121        // Claim the client half of the upgrade before the request is consumed.
122        // This also takes the OnUpgrade extension out, so it is not forwarded.
123        let client_upgrade = hyper::upgrade::on(&mut request);
124        // The listener's marker that a request is in flight. The tunnel keeps
125        // it, so the connection is governed by `tunnel_idle` rather than by
126        // the shorter connection idle timeout.
127        let in_flight = request.extensions_mut().remove::<InFlight>();
128
129        // The low-level h1 sender wants origin-form; Host carries the authority.
130        let path_and_query = request
131            .uri()
132            .path_and_query()
133            .map(|pq| pq.as_str().to_string())
134            .unwrap_or_else(|| "/".to_string());
135        *request.uri_mut() = path_and_query
136            .parse()
137            .map_err(|e| ProxyError::Request(format!("Invalid upstream URI: {}", e)))?;
138        *request.version_mut() = Version::HTTP_11;
139
140        let stream = tokio::time::timeout(self.timeout, TcpStream::connect(&backend.address))
141            .await
142            .map_err(|_| ProxyError::Timeout)?
143            .map_err(|e| ProxyError::Connection(e.to_string()))?;
144
145        // A tunnel relays whatever chunking the client used. With Nagle on, small
146        // writes coalesce and the upstream sees frames batched together that the
147        // client sent separately -- which changes what the upstream does, not just
148        // when it does it. Autobahn 3.4 is exactly that: the client sends a text
149        // frame and then a bad frame in one-byte chops, and a batched upstream
150        // fails the connection without ever echoing the first.
151        if let Err(e) = stream.set_nodelay(true) {
152            debug!("Could not set TCP_NODELAY on the upstream socket: {}", e);
153        }
154
155        let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream))
156            .await
157            .map_err(|e| ProxyError::Connection(e.to_string()))?;
158
159        // with_upgrades keeps the connection task alive past the 101 so the
160        // upgraded stream is actually reachable.
161        tokio::spawn(async move {
162            if let Err(e) = conn.with_upgrades().await {
163                debug!("Upstream upgrade connection error: {}", e);
164            }
165        });
166
167        let mut response = tokio::time::timeout(self.timeout, sender.send_request(request))
168            .await
169            .map_err(|_| ProxyError::Timeout)?
170            .map_err(|e| ProxyError::Connection(e.to_string()))?;
171
172        // The upstream declined to upgrade: pass its answer straight through.
173        if response.status() != StatusCode::SWITCHING_PROTOCOLS {
174            let (parts, body) = response.into_parts();
175            return Ok(Response::from_parts(parts, body.boxed_body()));
176        }
177
178        let upstream_upgrade = hyper::upgrade::on(&mut response);
179
180        // Both halves only resolve after their 101 has gone out, so the splice
181        // has to outlive this call.
182        tokio::spawn(async move {
183            let _in_flight = in_flight;
184            match tokio::try_join!(client_upgrade, upstream_upgrade) {
185                Ok((client_io, upstream_io)) => {
186                    Self::splice(
187                        TokioIo::new(client_io),
188                        TokioIo::new(upstream_io),
189                        tunnel_idle,
190                    )
191                    .await
192                }
193                Err(e) => debug!("Upgrade handshake failed: {}", e),
194            }
195        });
196
197        // Hand the 101 and its headers back; the body is the tunnel, not content.
198        let (parts, _) = response.into_parts();
199        Ok(Response::from_parts(parts, empty_body()))
200    }
201
202    /// Forward an HTTP/2 extended CONNECT (RFC 8441) to an HTTP/1.1 WebSocket
203    /// origin, translating between the two handshakes.
204    ///
205    /// The two are not the same conversation, so this is a rewrite rather than
206    /// a relay:
207    ///
208    /// - HTTP/2 carries the intent in `:method = CONNECT` and `:protocol`;
209    ///   HTTP/1.1 carries it in `Upgrade` plus a `Connection` token.
210    /// - HTTP/2 has no `Sec-WebSocket-Key`. The nonce exists to stop an HTTP/1.1
211    ///   cache from mistaking the handshake for an ordinary response, which does
212    ///   not apply to a multiplexed stream. The h1 origin still requires one, so
213    ///   the proxy generates it here.
214    /// - Success is `101` on the wire to the origin but `200` back to the
215    ///   client; a 101 has no meaning on an HTTP/2 stream.
216    ///
217    /// The origin's `Sec-WebSocket-Accept` is not verified against the generated
218    /// key. Status 101 plus a websocket `Upgrade` is taken as sufficient, which
219    /// avoids a SHA-1 dependency for a check that guards against a
220    /// misconfigured origin rather than an attacker.
221    ///
222    /// As with [`Self::forward_upgrade`], whether `protocol` is allowed is for
223    /// the caller to decide first.
224    pub async fn forward_h2_websocket(
225        &self,
226        backend: &Backend,
227        mut request: Request<ProxyBody>,
228        protocol: &str,
229        tunnel_idle: Duration,
230    ) -> Result<Response<ProxyBody>, ProxyError> {
231        let client_upgrade = hyper::upgrade::on(&mut request);
232        let in_flight = request.extensions_mut().remove::<InFlight>();
233
234        let path_and_query = request
235            .uri()
236            .path_and_query()
237            .map(|pq| pq.as_str().to_string())
238            .unwrap_or_else(|| "/".to_string());
239
240        // A fresh nonce for the HTTP/1.1 leg, which the HTTP/2 client never sent.
241        let mut nonce = [0u8; 16];
242        rand::rng().fill_bytes(&mut nonce);
243        let websocket_key = base64::engine::general_purpose::STANDARD.encode(nonce);
244
245        let mut builder = Request::builder()
246            .method(Method::GET)
247            .uri(&path_and_query)
248            .version(Version::HTTP_11)
249            .header(HOST, &backend.address)
250            .header(UPGRADE, protocol)
251            .header(CONNECTION, "upgrade")
252            .header("sec-websocket-version", "13")
253            .header("sec-websocket-key", &websocket_key);
254
255        // Carry the client's own headers over. The handshake fields above are
256        // ours to set, so skip any the client happened to send.
257        const REWRITTEN: [&str; 5] = [
258            "host",
259            "upgrade",
260            "connection",
261            "sec-websocket-key",
262            "sec-websocket-version",
263        ];
264        for (name, value) in request.headers() {
265            if !REWRITTEN.contains(&name.as_str()) {
266                builder = builder.header(name, value);
267            }
268        }
269
270        let upstream_request = builder
271            .body(empty_body())
272            .map_err(|e| ProxyError::Request(format!("could not build upgrade request: {}", e)))?;
273
274        let stream = tokio::time::timeout(self.timeout, TcpStream::connect(&backend.address))
275            .await
276            .map_err(|_| ProxyError::Timeout)?
277            .map_err(|e| ProxyError::Connection(e.to_string()))?;
278        if let Err(e) = stream.set_nodelay(true) {
279            debug!("Could not set TCP_NODELAY on the upstream socket: {}", e);
280        }
281
282        let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream))
283            .await
284            .map_err(|e| ProxyError::Connection(e.to_string()))?;
285        tokio::spawn(async move {
286            if let Err(e) = conn.with_upgrades().await {
287                debug!("Upstream upgrade connection error: {}", e);
288            }
289        });
290
291        let mut response =
292            tokio::time::timeout(self.timeout, sender.send_request(upstream_request))
293                .await
294                .map_err(|_| ProxyError::Timeout)?
295                .map_err(|e| ProxyError::Connection(e.to_string()))?;
296
297        if response.status() != StatusCode::SWITCHING_PROTOCOLS {
298            // The origin declined. Pass its answer back on the HTTP/2 stream,
299            // minus the 101 semantics that do not translate.
300            let status = response.status();
301            let mut out = Response::builder().status(status);
302            for (name, value) in response.headers() {
303                if name != UPGRADE && name != CONNECTION {
304                    out = out.header(name, value);
305                }
306            }
307            return out
308                .body(empty_body())
309                .map_err(|e| ProxyError::Response(e.to_string()));
310        }
311
312        let upstream_upgrade = hyper::upgrade::on(&mut response);
313
314        tokio::spawn(async move {
315            let _in_flight = in_flight;
316            match tokio::try_join!(client_upgrade, upstream_upgrade) {
317                Ok((client_io, upstream_io)) => {
318                    Self::splice(
319                        TokioIo::new(client_io),
320                        TokioIo::new(upstream_io),
321                        tunnel_idle,
322                    )
323                    .await
324                }
325                Err(e) => debug!("RFC 8441 upgrade handshake failed: {}", e),
326            }
327        });
328
329        // 200, not 101: on an HTTP/2 stream a 2xx is what completes an extended
330        // CONNECT. Carry across only what the client still needs to know.
331        let mut out = Response::builder().status(StatusCode::OK);
332        for name in ["sec-websocket-protocol", "sec-websocket-extensions"] {
333            if let Some(value) = response.headers().get(name) {
334                out = out.header(name, value);
335            }
336        }
337        out.body(empty_body())
338            .map_err(|e| ProxyError::Response(e.to_string()))
339    }
340
341    /// Splice two upgraded streams, draining both directions independently.
342    ///
343    /// Deliberately not `copy_bidirectional`, which returns on the first error
344    /// in either direction and drops the other mid-flight. That loses data in a
345    /// real and reachable case: the upstream fails the connection and closes
346    /// while the client is still writing, the client-to-upstream copy takes an
347    /// EPIPE, and whatever the upstream had already sent -- its last frames and
348    /// its close -- is discarded instead of reaching the client. Autobahn case
349    /// 3.4 caught exactly that, intermittently, because it is a race.
350    ///
351    /// Each direction here runs to its own end and half-closes the peer's write
352    /// side so the other side sees a clean EOF. `join!` rather than `try_join!`
353    /// so that one direction failing cannot cancel the other.
354    ///
355    /// The whole tunnel is dropped once `idle` passes with no bytes in either
356    /// direction. Without that, a tunnel whose peers simply stop talking --
357    /// or one half-closed and left that way -- holds two sockets and a
358    /// connection slot forever.
359    async fn splice<A, B>(client_io: A, upstream_io: B, idle: Duration)
360    where
361        A: AsyncRead + AsyncWrite + Unpin,
362        B: AsyncRead + AsyncWrite + Unpin,
363    {
364        let (mut client_read, mut client_write) = tokio::io::split(client_io);
365        let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream_io);
366        let last_activity = parking_lot::Mutex::new(tokio::time::Instant::now());
367
368        let to_upstream = async {
369            let result = relay(&mut client_read, &mut upstream_write, &last_activity).await;
370            // Half-close: tell the upstream the client is done, without tearing
371            // down the half still carrying its reply.
372            let _ = upstream_write.shutdown().await;
373            result
374        };
375
376        let to_client = async {
377            let result = relay(&mut upstream_read, &mut client_write, &last_activity).await;
378            let _ = client_write.shutdown().await;
379            result
380        };
381
382        let watchdog = async {
383            loop {
384                let deadline = *last_activity.lock() + idle;
385                if tokio::time::Instant::now() >= deadline {
386                    return;
387                }
388                tokio::time::sleep_until(deadline).await;
389            }
390        };
391
392        tokio::select! {
393            (up, down) = async { tokio::join!(to_upstream, to_client) } => match (up, down) {
394                (Ok(up), Ok(down)) => {
395                    debug!("Tunnel closed: {} bytes up, {} bytes down", up, down)
396                }
397                (up, down) => {
398                    // One side erroring is normal for an abrupt close; log both
399                    // outcomes rather than only the first failure.
400                    debug!("Tunnel closed with errors: up={:?} down={:?}", up, down)
401                }
402            },
403            _ = watchdog => debug!("Tunnel closed after {:?} idle", idle),
404        }
405    }
406}
407
408/// Copy one direction of a tunnel, noting each chunk in `last_activity`.
409///
410/// One read, one write, one flush: a chunk goes out as it came in. Coalescing
411/// chunks changes what a WebSocket peer sees, not just when -- see
412/// [`ForwarderClient::splice`].
413async fn relay<R, W>(
414    reader: &mut R,
415    writer: &mut W,
416    last_activity: &parking_lot::Mutex<tokio::time::Instant>,
417) -> std::io::Result<u64>
418where
419    R: AsyncRead + Unpin,
420    W: AsyncWrite + Unpin,
421{
422    let mut buf = vec![0u8; 16 * 1024];
423    let mut total = 0u64;
424    loop {
425        let n = reader.read(&mut buf).await?;
426        if n == 0 {
427            return Ok(total);
428        }
429        writer.write_all(&buf[..n]).await?;
430        writer.flush().await?;
431        total += n as u64;
432        *last_activity.lock() = tokio::time::Instant::now();
433    }
434}
435
436impl Default for ForwarderClient {
437    fn default() -> Self {
438        Self::new(Duration::from_secs(30))
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[tokio::test(start_paused = true)]
447    async fn a_tunnel_that_goes_quiet_is_closed() {
448        let (mut client, client_side) = tokio::io::duplex(1024);
449        let (upstream_side, mut upstream) = tokio::io::duplex(1024);
450        let splice = tokio::spawn(ForwarderClient::splice(
451            client_side,
452            upstream_side,
453            Duration::from_secs(10),
454        ));
455
456        // Traffic keeps it open past the idle timeout...
457        for _ in 0..3 {
458            tokio::time::sleep(Duration::from_secs(8)).await;
459            client.write_all(b"ping").await.unwrap();
460            let mut buf = [0u8; 4];
461            upstream.read_exact(&mut buf).await.unwrap();
462            assert_eq!(&buf, b"ping");
463        }
464        assert!(!splice.is_finished());
465
466        // ...and silence closes it, with both peers seeing the end.
467        tokio::time::sleep(Duration::from_secs(11)).await;
468        splice.await.unwrap();
469        let mut buf = [0u8; 1];
470        assert_eq!(client.read(&mut buf).await.unwrap(), 0);
471        assert_eq!(upstream.read(&mut buf).await.unwrap(), 0);
472    }
473
474    #[tokio::test(start_paused = true)]
475    async fn a_tunnel_relays_both_ways_and_half_closes() {
476        let (mut client, client_side) = tokio::io::duplex(1024);
477        let (upstream_side, mut upstream) = tokio::io::duplex(1024);
478        let splice = tokio::spawn(ForwarderClient::splice(
479            client_side,
480            upstream_side,
481            Duration::from_secs(10),
482        ));
483
484        upstream.write_all(b"hello").await.unwrap();
485        let mut buf = [0u8; 5];
486        client.read_exact(&mut buf).await.unwrap();
487        assert_eq!(&buf, b"hello");
488
489        client.shutdown().await.unwrap();
490        let mut rest = Vec::new();
491        upstream.read_to_end(&mut rest).await.unwrap();
492        assert!(rest.is_empty());
493        drop(upstream);
494        splice.await.unwrap();
495    }
496}