Skip to content

Commit 2303dd4

Browse files
committed
Pool upstream HTTP/2 connections and stop pinning preferred IP into resolve cache
Each browser request used to trigger a fresh TCP+TLS+ECH handshake to Cloudflare, which on real pages multiplies into many seconds of overhead. Keep H2 senders in an in-memory pool keyed by (host, addr) and reuse them via H2 multiplexing; fall back to a fresh handshake when the pooled connection is unhealthy. Also fix prioritize_preferred_upstream baking the preferred address into the resolved cache entry: once an IP succeeded once, the cached addrs vector kept it at position 0 forever, overriding managed_prefer_ipv6 changes after the first fallback. Cache the DoH-ordered list and only prioritize at use time.
1 parent 78aaa81 commit 2303dd4

1 file changed

Lines changed: 86 additions & 29 deletions

File tree

src/proxy.rs

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ struct AppState {
4848
resolve_cache: RwLock<HashMap<ResolveCacheKey, CachedResolvedUpstream>>,
4949
doh_cache: RwLock<HashMap<DohCacheKey, CachedDohAnswers>>,
5050
preferred_upstream_addr: RwLock<HashMap<ResolveCacheKey, SocketAddr>>,
51+
upstream_h2_pool: RwLock<HashMap<H2PoolKey, client_http2::SendRequest<Full<Bytes>>>>,
52+
}
53+
54+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
55+
struct H2PoolKey {
56+
host: String,
57+
addr: SocketAddr,
5158
}
5259

5360
#[derive(Clone, Debug)]
@@ -143,6 +150,7 @@ pub async fn run_proxy(
143150
resolve_cache: RwLock::new(HashMap::new()),
144151
doh_cache: RwLock::new(HashMap::new()),
145152
preferred_upstream_addr: RwLock::new(HashMap::new()),
153+
upstream_h2_pool: RwLock::new(HashMap::new()),
146154
});
147155
let http_state = state.clone();
148156
let https_state = state.clone();
@@ -482,30 +490,99 @@ async fn send_once(
482490
body: Bytes,
483491
path_and_query: &str,
484492
) -> Result<UpstreamResponse> {
485-
let request = build_upstream_request(request_host, method, headers, body, path_and_query)?;
486-
487493
if upstream_scheme.eq_ignore_ascii_case("http") {
494+
let request =
495+
build_upstream_request(request_host, method, headers, body, path_and_query)?;
488496
let stream = connect_tcp(addr).await?;
489497
return Ok(UpstreamResponse {
490498
response: send_over_io(TokioIo::new(stream), request).await?,
491499
negotiated_protocol: "http/1.1",
492500
});
493501
}
494502

503+
let pool_key = H2PoolKey {
504+
host: request_host.to_ascii_lowercase(),
505+
addr,
506+
};
507+
508+
if let Some(mut sender) = get_pooled_h2_sender(state, &pool_key).await {
509+
if sender.ready().await.is_ok() {
510+
let request = build_upstream_request(
511+
request_host,
512+
method.clone(),
513+
headers.clone(),
514+
body.clone(),
515+
path_and_query,
516+
)?;
517+
match sender.send_request(request).await {
518+
Ok(response) => {
519+
return Ok(UpstreamResponse {
520+
response,
521+
negotiated_protocol: "h2",
522+
});
523+
}
524+
Err(_) => {
525+
remove_h2_sender(state, &pool_key).await;
526+
}
527+
}
528+
} else {
529+
remove_h2_sender(state, &pool_key).await;
530+
}
531+
}
532+
495533
let tls_stream = connect_tls(state, request_host, outer_sni, ech_config, addr).await?;
496534
let negotiated_h2 = tls_stream.get_ref().1.alpn_protocol() == Some(b"h2");
535+
let request = build_upstream_request(request_host, method, headers, body, path_and_query)?;
536+
497537
if negotiated_h2 {
538+
let mut builder = client_http2::Builder::new(TokioExecutor::new());
539+
builder.adaptive_window(true);
540+
let (mut sender, connection) = builder
541+
.handshake(TokioIo::new(tls_stream))
542+
.await
543+
.context("failed to initialize upstream HTTP/2 client")?;
544+
tokio::spawn(async move {
545+
let _ = connection.await;
546+
});
547+
let response = sender
548+
.send_request(request)
549+
.await
550+
.context("failed to contact upstream")?;
551+
store_h2_sender(state, pool_key, sender).await;
498552
return Ok(UpstreamResponse {
499-
response: send_over_io_http2(TokioIo::new(tls_stream), request).await?,
553+
response,
500554
negotiated_protocol: "h2",
501555
});
502556
}
557+
503558
Ok(UpstreamResponse {
504559
response: send_over_io(TokioIo::new(tls_stream), request).await?,
505560
negotiated_protocol: "http/1.1",
506561
})
507562
}
508563

564+
async fn get_pooled_h2_sender(
565+
state: &AppState,
566+
key: &H2PoolKey,
567+
) -> Option<client_http2::SendRequest<Full<Bytes>>> {
568+
let pool = state.upstream_h2_pool.read().await;
569+
pool.get(key).cloned()
570+
}
571+
572+
async fn store_h2_sender(
573+
state: &AppState,
574+
key: H2PoolKey,
575+
sender: client_http2::SendRequest<Full<Bytes>>,
576+
) {
577+
let mut pool = state.upstream_h2_pool.write().await;
578+
pool.insert(key, sender);
579+
}
580+
581+
async fn remove_h2_sender(state: &AppState, key: &H2PoolKey) {
582+
let mut pool = state.upstream_h2_pool.write().await;
583+
pool.remove(key);
584+
}
585+
509586
fn build_upstream_request(
510587
request_host: &str,
511588
method: Method,
@@ -545,28 +622,6 @@ where
545622
.context("failed to contact upstream")
546623
}
547624

548-
async fn send_over_io_http2<T>(
549-
io: TokioIo<T>,
550-
request: Request<Full<Bytes>>,
551-
) -> Result<Response<Incoming>>
552-
where
553-
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
554-
{
555-
let mut builder = client_http2::Builder::new(TokioExecutor::new());
556-
builder.adaptive_window(true);
557-
let (mut sender, connection) = builder
558-
.handshake(io)
559-
.await
560-
.context("failed to initialize upstream HTTP/2 client")?;
561-
tokio::spawn(async move {
562-
let _ = connection.await;
563-
});
564-
sender
565-
.send_request(request)
566-
.await
567-
.context("failed to contact upstream")
568-
}
569-
570625
async fn connect_tls(
571626
state: &AppState,
572627
request_host: &str,
@@ -744,7 +799,12 @@ async fn resolve_upstream(state: &AppState, host: &str, port: u16) -> Result<Res
744799
.into_iter()
745800
.map(|ip| SocketAddr::new(ip, port))
746801
.collect::<Vec<_>>();
747-
let mut upstream = ResolvedUpstream { addrs, ech_config };
802+
let upstream = ResolvedUpstream { addrs, ech_config };
803+
let resolve_ttl = min_duration_options(binding_ttl, min_duration_options(addr_ttl, extra_ttl))
804+
.unwrap_or(FALLBACK_RESOLVE_CACHE_TTL);
805+
write_cached_upstream(state, cache_key.clone(), upstream.clone(), resolve_ttl).await;
806+
807+
let mut upstream = upstream;
748808
prioritize_preferred_upstream(state, &cache_key, &mut upstream.addrs).await;
749809
log_upstream_debug(
750810
state,
@@ -765,9 +825,6 @@ async fn resolve_upstream(state: &AppState, host: &str, port: u16) -> Result<Res
765825
.unwrap_or("-")
766826
),
767827
);
768-
let resolve_ttl = min_duration_options(binding_ttl, min_duration_options(addr_ttl, extra_ttl))
769-
.unwrap_or(FALLBACK_RESOLVE_CACHE_TTL);
770-
write_cached_upstream(state, cache_key, upstream.clone(), resolve_ttl).await;
771828
Ok(upstream)
772829
}
773830

0 commit comments

Comments
 (0)