@@ -1818,7 +1818,7 @@ async fn dispatch_tunnel(
18181818 host,
18191819 port
18201820 ) ;
1821- run_mitm_then_relay ( sock, & host, port, mitm, & fronter) . await ;
1821+ run_mitm_then_relay ( sock, & host, port, mitm, & fronter, & rewrite_ctx . tls_connector , rewrite_ctx . upstream_socks5 . as_deref ( ) ) . await ;
18221822 return Ok ( ( ) ) ;
18231823 }
18241824
@@ -1832,7 +1832,7 @@ async fn dispatch_tunnel(
18321832 port,
18331833 scheme
18341834 ) ;
1835- relay_http_stream_raw ( sock, & host, port, scheme, & fronter) . await ;
1835+ relay_http_stream_raw ( sock, & host, port, scheme, & fronter, & rewrite_ctx . tls_connector , rewrite_ctx . upstream_socks5 . as_deref ( ) ) . await ;
18361836 return Ok ( ( ) ) ;
18371837 }
18381838
@@ -2115,6 +2115,8 @@ async fn run_mitm_then_relay(
21152115 port : u16 ,
21162116 mitm : Arc < Mutex < MitmCertManager > > ,
21172117 fronter : & DomainFronter ,
2118+ tls_connector : & TlsConnector ,
2119+ upstream_socks5 : Option < & str > ,
21182120) {
21192121 // Peek the TLS ClientHello BEFORE minting the MITM cert. When the client
21202122 // resolves the hostname itself (DoH in Chrome/Firefox) and hands us a raw
@@ -2176,7 +2178,7 @@ async fn run_mitm_then_relay(
21762178 // latter would produce an IP-in-Host request that Cloudflare/etc. reject
21772179 // outright.
21782180 loop {
2179- match handle_mitm_request ( & mut tls, & effective_host, port, fronter, "https" ) . await {
2181+ match handle_mitm_request ( & mut tls, & effective_host, port, fronter, "https" , tls_connector , upstream_socks5 ) . await {
21802182 Ok ( true ) => continue ,
21812183 Ok ( false ) => break ,
21822184 Err ( e) => {
@@ -2203,9 +2205,11 @@ async fn relay_http_stream_raw(
22032205 port : u16 ,
22042206 scheme : & str ,
22052207 fronter : & DomainFronter ,
2208+ tls_connector : & TlsConnector ,
2209+ upstream_socks5 : Option < & str > ,
22062210) {
22072211 loop {
2208- match handle_mitm_request ( & mut sock, host, port, fronter, scheme) . await {
2212+ match handle_mitm_request ( & mut sock, host, port, fronter, scheme, tls_connector , upstream_socks5 ) . await {
22092213 Ok ( true ) => continue ,
22102214 Ok ( false ) => break ,
22112215 Err ( e) => {
@@ -2377,12 +2381,139 @@ fn parse_host_port(target: &str) -> (String, u16) {
23772381 }
23782382}
23792383
2384+ /// Serialise a parsed request back to wire bytes so it can be forwarded to
2385+ /// the real upstream server during WebSocket passthrough. Forwards all headers
2386+ /// except hop-by-hop proxy headers (`Proxy-Connection`, `Proxy-Authorization`).
2387+ fn rebuild_request_bytes ( method : & str , path : & str , version : & str , headers : & [ ( String , String ) ] ) -> Vec < u8 > {
2388+ let mut out = Vec :: with_capacity ( 512 ) ;
2389+ out. extend_from_slice ( method. as_bytes ( ) ) ;
2390+ out. push ( b' ' ) ;
2391+ out. extend_from_slice ( path. as_bytes ( ) ) ;
2392+ out. push ( b' ' ) ;
2393+ out. extend_from_slice ( version. as_bytes ( ) ) ;
2394+ out. extend_from_slice ( b"\r \n " ) ;
2395+ for ( k, v) in headers {
2396+ let kl = k. to_ascii_lowercase ( ) ;
2397+ if kl == "proxy-connection" || kl == "proxy-authorization" {
2398+ continue ;
2399+ }
2400+ out. extend_from_slice ( k. as_bytes ( ) ) ;
2401+ out. extend_from_slice ( b": " ) ;
2402+ out. extend_from_slice ( v. as_bytes ( ) ) ;
2403+ out. extend_from_slice ( b"\r \n " ) ;
2404+ }
2405+ out. extend_from_slice ( b"\r \n " ) ;
2406+ out
2407+ }
2408+
2409+ /// After a WebSocket upgrade is detected inside the MITM TLS session, this
2410+ /// helper connects directly to the real `host:port` (optionally via SOCKS5),
2411+ /// performs a TLS handshake, forwards the upgrade request, relays the 101
2412+ /// response back to the client, then splices both directions until one side
2413+ /// closes. Apps Script cannot hold persistent WebSocket connections, so this
2414+ /// bypasses the relay entirely.
2415+ async fn ws_tls_passthrough < S > (
2416+ client : & mut S ,
2417+ host : & str ,
2418+ port : u16 ,
2419+ upgrade_request : & [ u8 ] ,
2420+ tls_connector : & TlsConnector ,
2421+ upstream_socks5 : Option < & str > ,
2422+ ) -> std:: io:: Result < ( ) >
2423+ where
2424+ S : tokio:: io:: AsyncRead + tokio:: io:: AsyncWrite + Unpin ,
2425+ {
2426+ let connect_timeout = std:: time:: Duration :: from_secs ( 15 ) ;
2427+
2428+ let tcp = if let Some ( proxy) = upstream_socks5 {
2429+ match socks5_connect_via ( proxy, host, port) . await {
2430+ Ok ( s) => s,
2431+ Err ( e) => {
2432+ tracing:: warn!( "ws passthrough: socks5 {} -> {}:{} failed: {}" , proxy, host, port, e) ;
2433+ client. write_all ( b"HTTP/1.1 502 Bad Gateway\r \n Content-Length: 0\r \n Connection: close\r \n \r \n " ) . await ?;
2434+ return Ok ( ( ) ) ;
2435+ }
2436+ }
2437+ } else {
2438+ match tokio:: time:: timeout ( connect_timeout, TcpStream :: connect ( ( host, port) ) ) . await {
2439+ Ok ( Ok ( s) ) => s,
2440+ Ok ( Err ( e) ) => {
2441+ tracing:: warn!( "ws passthrough: direct connect to {}:{} failed: {}" , host, port, e) ;
2442+ client. write_all ( b"HTTP/1.1 502 Bad Gateway\r \n Content-Length: 0\r \n Connection: close\r \n \r \n " ) . await ?;
2443+ return Ok ( ( ) ) ;
2444+ }
2445+ Err ( _) => {
2446+ tracing:: warn!( "ws passthrough: connect to {}:{} timed out" , host, port) ;
2447+ client. write_all ( b"HTTP/1.1 502 Bad Gateway\r \n Content-Length: 0\r \n Connection: close\r \n \r \n " ) . await ?;
2448+ return Ok ( ( ) ) ;
2449+ }
2450+ }
2451+ } ;
2452+
2453+ let server_name = match ServerName :: try_from ( host. to_string ( ) ) {
2454+ Ok ( sn) => sn,
2455+ Err ( _) => {
2456+ tracing:: warn!( "ws passthrough: invalid server name {}" , host) ;
2457+ client. write_all ( b"HTTP/1.1 502 Bad Gateway\r \n Content-Length: 0\r \n Connection: close\r \n \r \n " ) . await ?;
2458+ return Ok ( ( ) ) ;
2459+ }
2460+ } ;
2461+
2462+ let mut server = match tls_connector. connect ( server_name, tcp) . await {
2463+ Ok ( s) => s,
2464+ Err ( e) => {
2465+ tracing:: warn!( "ws passthrough: TLS to {}:{} failed: {}" , host, port, e) ;
2466+ client. write_all ( b"HTTP/1.1 502 Bad Gateway\r \n Content-Length: 0\r \n Connection: close\r \n \r \n " ) . await ?;
2467+ return Ok ( ( ) ) ;
2468+ }
2469+ } ;
2470+
2471+ // Forward the upgrade request to the real server.
2472+ server. write_all ( upgrade_request) . await ?;
2473+ server. flush ( ) . await ?;
2474+
2475+ // Read the server's response headers (up to \r\n\r\n) and forward to client.
2476+ let mut resp_buf = Vec :: with_capacity ( 512 ) ;
2477+ let mut tmp = [ 0u8 ; 1 ] ;
2478+ loop {
2479+ server. read_exact ( & mut tmp) . await ?;
2480+ resp_buf. push ( tmp[ 0 ] ) ;
2481+ if resp_buf. ends_with ( b"\r \n \r \n " ) {
2482+ break ;
2483+ }
2484+ if resp_buf. len ( ) > 8192 {
2485+ tracing:: warn!( "ws passthrough: server response headers too large from {}:{}" , host, port) ;
2486+ return Ok ( ( ) ) ;
2487+ }
2488+ }
2489+
2490+ // Check the server actually agreed to the upgrade.
2491+ let resp_str = String :: from_utf8_lossy ( & resp_buf) ;
2492+ let status_line = resp_str. lines ( ) . next ( ) . unwrap_or ( "" ) ;
2493+ if !status_line. contains ( "101" ) {
2494+ tracing:: warn!( "ws passthrough: {}:{} refused upgrade ({})" , host, port, status_line. trim( ) ) ;
2495+ client. write_all ( & resp_buf) . await ?;
2496+ client. flush ( ) . await ?;
2497+ return Ok ( ( ) ) ;
2498+ }
2499+
2500+ client. write_all ( & resp_buf) . await ?;
2501+ client. flush ( ) . await ?;
2502+
2503+ // Both sides agreed: splice raw bytes bidirectionally.
2504+ tracing:: info!( "ws passthrough: splicing {}:{}" , host, port) ;
2505+ let _ = tokio:: io:: copy_bidirectional ( client, & mut server) . await ;
2506+ Ok ( ( ) )
2507+ }
2508+
23802509async fn handle_mitm_request < S > (
23812510 stream : & mut S ,
23822511 host : & str ,
23832512 port : u16 ,
23842513 fronter : & DomainFronter ,
23852514 scheme : & str ,
2515+ tls_connector : & TlsConnector ,
2516+ upstream_socks5 : Option < & str > ,
23862517) -> std:: io:: Result < bool >
23872518where
23882519 S : tokio:: io:: AsyncRead + tokio:: io:: AsyncWrite + Unpin ,
@@ -2415,11 +2546,24 @@ where
24152546 }
24162547 } ;
24172548
2418- let ( method, path, _version , headers) = match parse_request_head ( & head) {
2549+ let ( method, path, version , headers) = match parse_request_head ( & head) {
24192550 Some ( v) => v,
24202551 None => return Ok ( false ) ,
24212552 } ;
24222553
2554+ // WebSocket upgrade: Apps Script cannot relay persistent connections.
2555+ // Detect before read_body (upgrade requests have no body) and splice
2556+ // directly to the real server instead.
2557+ let is_ws_upgrade =
2558+ header_value ( & headers, "connection" ) . map ( |v| v. to_ascii_lowercase ( ) . contains ( "upgrade" ) ) . unwrap_or ( false )
2559+ && header_value ( & headers, "upgrade" ) . map ( |v| v. eq_ignore_ascii_case ( "websocket" ) ) . unwrap_or ( false ) ;
2560+ if is_ws_upgrade {
2561+ tracing:: info!( "WebSocket upgrade for {}:{} — bypassing Apps Script relay" , host, port) ;
2562+ let raw_request = rebuild_request_bytes ( & method, & path, & version, & headers) ;
2563+ ws_tls_passthrough ( stream, host, port, & raw_request, tls_connector, upstream_socks5) . await ?;
2564+ return Ok ( false ) ;
2565+ }
2566+
24232567 let body = read_body ( stream, & leftover, & headers) . await ?;
24242568
24252569 // ── Per-host URL fix-ups ──────────────────────────────────────────
0 commit comments