@@ -327,6 +327,28 @@ impl ProxyServer {
327327 } ) ;
328328 }
329329
330+ // Apps Script container keepalive. `warm()` above keeps the TCP
331+ // pool warm at startup, but the V8 container behind UrlFetchApp
332+ // goes cold after ~5min idle and costs 1-3s to wake. A periodic
333+ // HEAD ping prevents the cold-start lag on the first request
334+ // after a quiet pause (most visible as YouTube player stalls).
335+ // Skipped in google_only mode for the same reason as warm —
336+ // there's no fronter to ping.
337+ //
338+ // The handle is captured (not fire-and-forget) so the shutdown
339+ // arm of the select! below can abort it. Without that, hitting
340+ // Stop in the UI would leave the keepalive holding an
341+ // Arc<DomainFronter> on stale config and pinging Apps Script
342+ // every 240s — same class of bug that issue #99 hit for the
343+ // accept loops.
344+ let keepalive_task = if let Some ( keepalive_fronter) = self . fronter . clone ( ) {
345+ tokio:: spawn ( async move {
346+ keepalive_fronter. run_h1_keepalive ( ) . await ;
347+ } )
348+ } else {
349+ tokio:: spawn ( async move { std:: future:: pending :: < ( ) > ( ) . await } )
350+ } ;
351+
330352 let stats_task = if let Some ( stats_fronter) = self . fronter . clone ( ) {
331353 tokio:: spawn ( async move {
332354 let mut ticker = tokio:: time:: interval ( std:: time:: Duration :: from_secs ( 60 ) ) ;
@@ -434,6 +456,7 @@ impl ProxyServer {
434456 _ = & mut shutdown_rx => {
435457 tracing:: info!( "Shutdown signal received, stopping listeners" ) ;
436458 stats_task. abort( ) ;
459+ keepalive_task. abort( ) ;
437460 http_task. abort( ) ;
438461 socks_task. abort( ) ;
439462 }
@@ -507,8 +530,26 @@ async fn handle_http_client(
507530 tunnel_mux : Option < Arc < TunnelMux > > ,
508531) -> std:: io:: Result < ( ) > {
509532 let ( head, leftover) = match read_http_head ( & mut sock) . await ? {
510- Some ( v) => v,
511- None => return Ok ( ( ) ) ,
533+ HeadReadResult :: Got { head, leftover } => ( head, leftover) ,
534+ HeadReadResult :: Closed => return Ok ( ( ) ) ,
535+ HeadReadResult :: Oversized => {
536+ // Reply with 431 instead of just dropping the socket so the
537+ // browser shows a real error rather than retrying the same
538+ // oversized request in a loop.
539+ tracing:: warn!(
540+ "request head exceeds {} bytes — refusing with 431" ,
541+ MAX_HEADER_BYTES
542+ ) ;
543+ let _ = sock
544+ . write_all (
545+ b"HTTP/1.1 431 Request Header Fields Too Large\r \n \
546+ Connection: close\r \n \
547+ Content-Length: 0\r \n \r \n ",
548+ )
549+ . await ;
550+ let _ = sock. flush ( ) . await ;
551+ return Ok ( ( ) ) ;
552+ }
512553 } ;
513554
514555 let ( method, target, _version, _headers) = parse_request_head ( & head)
@@ -1608,14 +1649,35 @@ fn looks_like_http(first_bytes: &[u8]) -> bool {
16081649/// Read an HTTP head (request line + headers) up to the first \r\n\r\n.
16091650/// Returns (head_bytes, leftover_after_head). The leftover may contain part
16101651/// of the request body already received.
1611- async fn read_http_head ( sock : & mut TcpStream ) -> std:: io:: Result < Option < ( Vec < u8 > , Vec < u8 > ) > > {
1652+ /// Maximum size of an HTTP request head (request line + all headers).
1653+ ///
1654+ /// Set to match upstream Python's `MAX_HEADER_BYTES` (64 KB,
1655+ /// masterking32/MasterHttpRelayVPN constants.py). Real browsers
1656+ /// virtually never exceed ~16 KB; anything past 64 KB is either a
1657+ /// buggy client or a deliberate slowloris-style header bomb.
1658+ /// Previously 1 MB, which let a misbehaving client allocate a lot
1659+ /// of memory before failing.
1660+ const MAX_HEADER_BYTES : usize = 64 * 1024 ;
1661+
1662+ /// Result of `read_http_head` / `read_http_head_io`.
1663+ /// `Oversized` is distinct from other I/O errors so the caller can
1664+ /// reply with `431 Request Header Fields Too Large` instead of just
1665+ /// dropping the connection (which a browser would silently retry,
1666+ /// reproducing the same problem).
1667+ enum HeadReadResult {
1668+ Got { head : Vec < u8 > , leftover : Vec < u8 > } ,
1669+ Closed ,
1670+ Oversized ,
1671+ }
1672+
1673+ async fn read_http_head ( sock : & mut TcpStream ) -> std:: io:: Result < HeadReadResult > {
16121674 let mut buf = Vec :: with_capacity ( 4096 ) ;
16131675 let mut tmp = [ 0u8 ; 4096 ] ;
16141676 loop {
16151677 let n = sock. read ( & mut tmp) . await ?;
16161678 if n == 0 {
16171679 return if buf. is_empty ( ) {
1618- Ok ( None )
1680+ Ok ( HeadReadResult :: Closed )
16191681 } else {
16201682 Err ( std:: io:: Error :: new (
16211683 std:: io:: ErrorKind :: UnexpectedEof ,
@@ -1627,13 +1689,10 @@ async fn read_http_head(sock: &mut TcpStream) -> std::io::Result<Option<(Vec<u8>
16271689 if let Some ( pos) = find_headers_end ( & buf) {
16281690 let head = buf[ ..pos] . to_vec ( ) ;
16291691 let leftover = buf[ pos..] . to_vec ( ) ;
1630- return Ok ( Some ( ( head, leftover) ) ) ;
1692+ return Ok ( HeadReadResult :: Got { head, leftover } ) ;
16311693 }
1632- if buf. len ( ) > 1024 * 1024 {
1633- return Err ( std:: io:: Error :: new (
1634- std:: io:: ErrorKind :: InvalidData ,
1635- "headers too large" ,
1636- ) ) ;
1694+ if buf. len ( ) > MAX_HEADER_BYTES {
1695+ return Ok ( HeadReadResult :: Oversized ) ;
16371696 }
16381697 }
16391698}
@@ -1942,8 +2001,31 @@ where
19422001 S : tokio:: io:: AsyncRead + tokio:: io:: AsyncWrite + Unpin ,
19432002{
19442003 let ( head, leftover) = match read_http_head_io ( stream) . await ? {
1945- Some ( v) => v,
1946- None => return Ok ( false ) ,
2004+ HeadReadResult :: Got { head, leftover } => ( head, leftover) ,
2005+ HeadReadResult :: Closed => return Ok ( false ) ,
2006+ HeadReadResult :: Oversized => {
2007+ // Inside MITM: same reasoning as the plaintext path. Return
2008+ // 431 over the decrypted stream so the browser surfaces a
2009+ // real error to the user instead of looping a connection
2010+ // reset, which was the symptom upstream caught (Apps Script
2011+ // ate malformed JSON when truncated header blocks were
2012+ // forwarded blindly).
2013+ tracing:: warn!(
2014+ "MITM header block exceeds {} bytes — closing ({}:{})" ,
2015+ MAX_HEADER_BYTES ,
2016+ host,
2017+ port
2018+ ) ;
2019+ let _ = stream
2020+ . write_all (
2021+ b"HTTP/1.1 431 Request Header Fields Too Large\r \n \
2022+ Connection: close\r \n \
2023+ Content-Length: 0\r \n \r \n ",
2024+ )
2025+ . await ;
2026+ let _ = stream. flush ( ) . await ;
2027+ return Ok ( false ) ;
2028+ }
19472029 } ;
19482030
19492031 let ( method, path, _version, headers) = match parse_request_head ( & head) {
@@ -2064,7 +2146,7 @@ where
20642146 Ok ( !connection_close)
20652147}
20662148
2067- async fn read_http_head_io < S > ( stream : & mut S ) -> std:: io:: Result < Option < ( Vec < u8 > , Vec < u8 > ) > >
2149+ async fn read_http_head_io < S > ( stream : & mut S ) -> std:: io:: Result < HeadReadResult >
20682150where
20692151 S : tokio:: io:: AsyncRead + Unpin ,
20702152{
@@ -2074,7 +2156,7 @@ where
20742156 let n = stream. read ( & mut tmp) . await ?;
20752157 if n == 0 {
20762158 return if buf. is_empty ( ) {
2077- Ok ( None )
2159+ Ok ( HeadReadResult :: Closed )
20782160 } else {
20792161 Err ( std:: io:: Error :: new (
20802162 std:: io:: ErrorKind :: UnexpectedEof ,
@@ -2086,13 +2168,10 @@ where
20862168 if let Some ( pos) = find_headers_end ( & buf) {
20872169 let head = buf[ ..pos] . to_vec ( ) ;
20882170 let leftover = buf[ pos..] . to_vec ( ) ;
2089- return Ok ( Some ( ( head, leftover) ) ) ;
2171+ return Ok ( HeadReadResult :: Got { head, leftover } ) ;
20902172 }
2091- if buf. len ( ) > 1024 * 1024 {
2092- return Err ( std:: io:: Error :: new (
2093- std:: io:: ErrorKind :: InvalidData ,
2094- "headers too large" ,
2095- ) ) ;
2173+ if buf. len ( ) > MAX_HEADER_BYTES {
2174+ return Ok ( HeadReadResult :: Oversized ) ;
20962175 }
20972176 }
20982177}
0 commit comments