22//!
33//! - [`handle_connect`]: Upgrades the client connection and tunnels bytes
44//! bidirectionally to the target via [`tokio::io::copy_bidirectional`].
5- //! - [`handle_forward`]: Rewrites the absolute URI to path-only form, strips
6- //! proxy headers, and forwards the request via hyper's HTTP/1.1 client .
5+ //! - [`handle_forward`]: Strips proxy headers and forwards the request via
6+ //! a pooled HTTP client (or manual connection with TCP Fast Open) .
77
88use anyhow:: Context ;
9- use http_body_util:: { BodyExt , Full } ;
9+ use http_body_util:: { Either , Full } ;
1010use hyper:: body:: { Bytes , Incoming } ;
1111use hyper:: { Request , Response , StatusCode } ;
12- use hyper_util:: rt:: TokioIo ;
12+ use hyper_util:: client:: legacy:: Client ;
13+ use hyper_util:: rt:: { TokioExecutor , TokioIo } ;
1314use tracing:: { error, info} ;
1415
1516use crate :: net;
17+ use crate :: ProxyBody ;
18+
19+ /// Buffer size per direction for CONNECT tunnels (128 KiB).
20+ /// 16x the default 8 KiB — matches TLS record size and reduces syscall count.
21+ const TUNNEL_BUF_SIZE : usize = 128 * 1024 ;
22+
23+ /// Global pooled HTTP/1.1 client for forward proxying (non-TFO path).
24+ static POOLED_CLIENT : std:: sync:: OnceLock < Client < hyper_util:: client:: legacy:: connect:: HttpConnector , Incoming > > = std:: sync:: OnceLock :: new ( ) ;
25+
26+ fn get_pooled_client ( ) -> & ' static Client < hyper_util:: client:: legacy:: connect:: HttpConnector , Incoming > {
27+ POOLED_CLIENT . get_or_init ( || {
28+ Client :: builder ( TokioExecutor :: new ( ) )
29+ . pool_idle_timeout ( std:: time:: Duration :: from_secs ( 90 ) )
30+ . pool_max_idle_per_host ( 32 )
31+ . build_http ( )
32+ } )
33+ }
1634
1735/// Handle an HTTP `CONNECT` request by establishing a TCP tunnel.
1836///
@@ -23,7 +41,7 @@ use crate::net;
2341pub async fn handle_connect (
2442 req : Request < Incoming > ,
2543 fast_open : bool ,
26- ) -> anyhow:: Result < Response < Full < Bytes > > > {
44+ ) -> anyhow:: Result < Response < ProxyBody > > {
2745 let authority = req
2846 . uri ( )
2947 . authority ( )
@@ -49,7 +67,7 @@ pub async fn handle_connect(
4967 match net:: connect ( & addr, fast_open) . await {
5068 Ok ( mut target) => {
5169 if let Err ( e) =
52- tokio:: io:: copy_bidirectional ( & mut client, & mut target) . await
70+ tokio:: io:: copy_bidirectional_with_sizes ( & mut client, & mut target, TUNNEL_BUF_SIZE , TUNNEL_BUF_SIZE ) . await
5371 {
5472 error ! ( "tunnel {addr} io error: {e}" ) ;
5573 }
@@ -68,79 +86,87 @@ pub async fn handle_connect(
6886 // Return 200 to signal the client that the tunnel is established.
6987 Ok ( Response :: builder ( )
7088 . status ( StatusCode :: OK )
71- . body ( Full :: new ( Bytes :: new ( ) ) )
89+ . body ( Either :: Left ( Full :: new ( Bytes :: new ( ) ) ) )
7290 . unwrap ( ) )
7391}
7492
7593/// Handle a plain HTTP forward proxy request with an absolute URI.
7694///
77- /// Rewrites the request URI from absolute form (`http://host/path`) to
78- /// path-only (`/path`), removes `Proxy-Authorization` and `Proxy-Connection`
79- /// headers, connects to the upstream server, and relays the response back
80- /// to the client.
95+ /// For the common case (`fast_open = false`), uses a pooled HTTP client that
96+ /// reuses connections across requests. For `fast_open = true`, uses manual
97+ /// connection setup with TCP Fast Open.
98+ ///
99+ /// The response body is streamed directly from the upstream server without
100+ /// buffering the entire body in memory.
81101pub async fn handle_forward (
82102 mut req : Request < Incoming > ,
83103 fast_open : bool ,
84- ) -> anyhow:: Result < Response < Full < Bytes > > > {
104+ ) -> anyhow:: Result < Response < ProxyBody > > {
85105 let uri = req. uri ( ) . clone ( ) ;
86- let host = uri
87- . authority ( )
88- . context ( "missing authority in forward request" ) ?
89- . to_string ( ) ;
90-
91- let port = uri. port_u16 ( ) . unwrap_or ( match uri. scheme_str ( ) {
92- Some ( "https" ) => 443 ,
93- _ => 80 ,
94- } ) ;
95-
96- let addr = if host. contains ( ':' ) {
97- host. clone ( )
98- } else {
99- format ! ( "{host}:{port}" )
100- } ;
101-
102- info ! ( "forward {} {} -> {addr}" , req. method( ) , uri) ;
103106
104- // Rewrite the URI to path-only form for the upstream request.
105- let path_and_query = uri
106- . path_and_query ( )
107- . map ( |pq| pq. to_string ( ) )
108- . unwrap_or_else ( || "/" . to_string ( ) ) ;
109- * req. uri_mut ( ) = path_and_query. parse ( ) ?;
107+ info ! ( "forward {} {}" , req. method( ) , uri) ;
110108
111109 // Strip hop-by-hop / proxy headers.
112110 let headers = req. headers_mut ( ) ;
113111 headers. remove ( "proxy-authorization" ) ;
114112 headers. remove ( "proxy-connection" ) ;
115113
116- // Connect to the upstream server.
117- let stream = net:: connect ( & addr, fast_open)
118- . await
119- . with_context ( || format ! ( "connect to {addr}" ) ) ?;
120- let io = TokioIo :: new ( stream) ;
114+ if fast_open {
115+ // TFO path: manual connection (pooled client doesn't support custom connectors yet)
116+ let host = uri
117+ . authority ( )
118+ . context ( "missing authority in forward request" ) ?
119+ . to_string ( ) ;
121120
122- let ( mut sender, conn) = hyper:: client:: conn:: http1:: handshake ( io)
123- . await
124- . context ( "upstream handshake" ) ?;
125-
126- tokio:: spawn ( async move {
127- if let Err ( e) = conn. await {
128- error ! ( "upstream connection error: {e}" ) ;
129- }
130- } ) ;
121+ let port = uri. port_u16 ( ) . unwrap_or ( match uri. scheme_str ( ) {
122+ Some ( "https" ) => 443 ,
123+ _ => 80 ,
124+ } ) ;
131125
132- let resp = sender
133- . send_request ( req)
134- . await
135- . context ( "upstream send_request" ) ?;
126+ let addr = if host. contains ( ':' ) {
127+ host. clone ( )
128+ } else {
129+ format ! ( "{host}:{port}" )
130+ } ;
131+
132+ // Rewrite the URI to path-only form for the upstream request.
133+ let path_and_query = uri
134+ . path_and_query ( )
135+ . map ( |pq| pq. to_string ( ) )
136+ . unwrap_or_else ( || "/" . to_string ( ) ) ;
137+ * req. uri_mut ( ) = path_and_query. parse ( ) ?;
138+
139+ let stream = net:: connect ( & addr, true )
140+ . await
141+ . with_context ( || format ! ( "connect to {addr}" ) ) ?;
142+ let io = TokioIo :: new ( stream) ;
143+
144+ let ( mut sender, conn) = hyper:: client:: conn:: http1:: handshake ( io)
145+ . await
146+ . context ( "upstream handshake" ) ?;
147+
148+ tokio:: spawn ( async move {
149+ if let Err ( e) = conn. await {
150+ error ! ( "upstream connection error: {e}" ) ;
151+ }
152+ } ) ;
136153
137- // Collect the upstream response body.
138- let ( parts, body) = resp. into_parts ( ) ;
139- let body_bytes = body
140- . collect ( )
141- . await
142- . context ( "read upstream body" ) ?
143- . to_bytes ( ) ;
154+ let resp = sender
155+ . send_request ( req)
156+ . await
157+ . context ( "upstream send_request" ) ?;
144158
145- Ok ( Response :: from_parts ( parts, Full :: new ( body_bytes) ) )
159+ let ( parts, body) = resp. into_parts ( ) ;
160+ Ok ( Response :: from_parts ( parts, Either :: Right ( body) ) )
161+ } else {
162+ // Pooled client path: connection reuse, automatic URI handling
163+ let client = get_pooled_client ( ) ;
164+ let resp = client
165+ . request ( req)
166+ . await
167+ . context ( "pooled client request" ) ?;
168+
169+ let ( parts, body) = resp. into_parts ( ) ;
170+ Ok ( Response :: from_parts ( parts, Either :: Right ( body) ) )
171+ }
146172}
0 commit comments