295 lines
15 KiB
Rust
295 lines
15 KiB
Rust
async fn serve_inner(
|
|
listener: TcpListener,
|
|
router: Router,
|
|
shutdown: CancellationToken,
|
|
permits: Arc<Semaphore>,
|
|
grace: Duration,
|
|
metrics: Option<Arc<Metrics>>,
|
|
tls: Option<TlsMode>,
|
|
h2c: bool,
|
|
frontend_state: Option<Arc<App>>,
|
|
http2_settings: Http2Connections,
|
|
) -> Result<()> {
|
|
let mut tasks = JoinSet::new();
|
|
loop {
|
|
tokio::select! {
|
|
biased;
|
|
_ = shutdown.cancelled() => break,
|
|
_ = tasks.join_next(), if !tasks.is_empty() => {},
|
|
accepted = listener.accept() => {
|
|
let (stream, peer) = accepted.context("listener accept failed")?;
|
|
let local = stream.local_addr().context("accepted local address unavailable")?;
|
|
let Ok(permit) = permits.clone().try_acquire_owned() else {
|
|
if let Some(m) = &metrics { m.rejected_connections.fetch_add(1, Ordering::Relaxed); }
|
|
let log_state = match &tls {
|
|
Some(TlsMode::Frontend(state, _)) => Some(state),
|
|
_ => frontend_state.as_ref(),
|
|
};
|
|
log_protocol_error(
|
|
log_state,
|
|
"connection_limit_rejected",
|
|
"connection concurrency limit reached",
|
|
peer, local,
|
|
if tls.is_some() { "tls" } else { "tcp" },
|
|
"", 0, None,
|
|
);
|
|
drop(stream); continue;
|
|
};
|
|
let _ = stream.set_nodelay(true);
|
|
if let Some(m) = &metrics {
|
|
m.connections.fetch_add(1, Ordering::Relaxed); m.active_connections.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
let lease = Arc::new(ConnectionLease { _permit: permit, metrics: metrics.clone() });
|
|
let router = router.clone(); let stop = shutdown.clone(); let tls = tls.clone(); let metrics = metrics.clone();
|
|
let frontend_state = frontend_state.clone();
|
|
tasks.spawn(async move {
|
|
match tls {
|
|
Some(TlsMode::Frontend(state, http2)) => {
|
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
|
let acceptor = tokio_rustls::LazyConfigAcceptor::new(
|
|
rustls::server::Acceptor::default(),
|
|
stream,
|
|
);
|
|
let started = tokio::select! {
|
|
_ = stop.cancelled() => return,
|
|
started = tokio::time::timeout_at(deadline, acceptor) => started,
|
|
};
|
|
let start = match started {
|
|
Ok(Ok(start)) => start,
|
|
Ok(Err(error)) => {
|
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
|
log_protocol_error(
|
|
Some(&state), "tls_client_hello_error", error, peer, local,
|
|
"tls", "", 0, None,
|
|
);
|
|
return;
|
|
}
|
|
Err(_) => {
|
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
|
log_protocol_error(
|
|
Some(&state), "tls_client_hello_timeout",
|
|
"TLS ClientHello timeout", peer, local, "tls", "", 0, None,
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
let host = start
|
|
.client_hello()
|
|
.server_name()
|
|
.and_then(|name| crate::config::request_host(name).ok());
|
|
let config = {
|
|
let snapshot = state.config.load();
|
|
let frontend = host
|
|
.as_deref()
|
|
.and_then(|host| snapshot.frontend(host, local, true));
|
|
let local_host = host
|
|
.as_deref()
|
|
.and_then(|host| snapshot.not_found_host_on(host, local, true));
|
|
if let Some(frontend) = frontend {
|
|
if http2 && frontend.http2_enabled {
|
|
frontend.tls.clone()
|
|
} else {
|
|
frontend.tls_http1.clone()
|
|
}
|
|
} else if http2 && local_host.is_none_or(|item| item.http2_enabled) {
|
|
snapshot.tls.clone()
|
|
} else {
|
|
snapshot.tls_http1.clone()
|
|
}
|
|
};
|
|
let Some(config) = config else {
|
|
log_protocol_error(
|
|
Some(&state), "tls_configuration_unavailable",
|
|
"no TLS configuration is available for this connection",
|
|
peer, local, "tls", "", 0, host.clone(),
|
|
);
|
|
return;
|
|
};
|
|
let accepted = tokio::select! {
|
|
_ = stop.cancelled() => return,
|
|
accepted = tokio::time::timeout_at(deadline, start.into_stream(config)) => accepted,
|
|
};
|
|
match accepted {
|
|
Ok(Ok(stream)) => {
|
|
let h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2".as_slice());
|
|
state.metrics.tls_connections.fetch_add(1, Ordering::Relaxed);
|
|
if h2 { state.metrics.http2_connections.fetch_add(1, Ordering::Relaxed); }
|
|
let extended_connect = h2 && state.config.load().websocket_rfc8441_on(local, true);
|
|
connection(
|
|
stream,
|
|
router,
|
|
peer,
|
|
local,
|
|
Transport { tls: true },
|
|
host.clone(),
|
|
h2,
|
|
extended_connect,
|
|
false,
|
|
Some(state.clone()),
|
|
lease,
|
|
stop,
|
|
grace,
|
|
http2_settings,
|
|
Default::default(),
|
|
)
|
|
.await;
|
|
},
|
|
Ok(Err(error)) => {
|
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
|
log_protocol_error(
|
|
Some(&state), "tls_handshake_error", error, peer, local,
|
|
"tls", "", 0, host.clone(),
|
|
);
|
|
},
|
|
Err(_) => {
|
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
|
log_protocol_error(
|
|
Some(&state), "tls_handshake_timeout", "TLS handshake timeout",
|
|
peer, local, "tls", "", 0, host.clone(),
|
|
);
|
|
},
|
|
}
|
|
}
|
|
Some(TlsMode::Fixed(config, plaintext_router)) => {
|
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
|
if let Some(plain) = plaintext_router {
|
|
let mut first = [0u8; 1];
|
|
let peeked = tokio::select! {
|
|
_ = stop.cancelled() => return,
|
|
peeked = tokio::time::timeout_at(deadline, stream.peek(&mut first)) => peeked,
|
|
};
|
|
match peeked {
|
|
Ok(Ok(n)) if n > 0 && first[0] != 0x16 => {
|
|
connection(
|
|
stream,
|
|
plain,
|
|
peer,
|
|
local,
|
|
Transport { tls: false },
|
|
None,
|
|
false,
|
|
false,
|
|
false,
|
|
None,
|
|
lease,
|
|
stop,
|
|
grace,
|
|
Http2Connections::default(),
|
|
Default::default(),
|
|
)
|
|
.await;
|
|
return;
|
|
}
|
|
Ok(Ok(n)) if n > 0 => {}
|
|
_ => return,
|
|
}
|
|
}
|
|
let acceptor = tokio_rustls::TlsAcceptor::from(config);
|
|
let accepted = tokio::select! {
|
|
_ = stop.cancelled() => return,
|
|
accepted = tokio::time::timeout_at(deadline, acceptor.accept(stream)) => accepted,
|
|
};
|
|
if let Ok(Ok(stream)) = accepted {
|
|
let h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2".as_slice());
|
|
connection(
|
|
stream,
|
|
router,
|
|
peer,
|
|
local,
|
|
Transport { tls: true },
|
|
None,
|
|
h2,
|
|
false,
|
|
false,
|
|
None,
|
|
lease,
|
|
stop,
|
|
grace,
|
|
Http2Connections::default(),
|
|
Default::default(),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
None => {
|
|
let prior_h2 = if h2c {
|
|
let detected = tokio::time::timeout(Duration::from_secs(10), async {
|
|
let mut preface = [0u8; HTTP2_PRIOR_KNOWLEDGE_PREFACE.len()];
|
|
loop {
|
|
let n = stream.peek(&mut preface).await?;
|
|
if n == 0 {
|
|
return Ok::<bool, std::io::Error>(false);
|
|
}
|
|
let prefix = &preface[..n.min(HTTP2_PRIOR_KNOWLEDGE_PREFACE.len())];
|
|
if !h2_prior_knowledge_prefix(prefix) {
|
|
return Ok(false);
|
|
}
|
|
if n >= HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() {
|
|
return Ok(prefix == HTTP2_PRIOR_KNOWLEDGE_PREFACE);
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}).await;
|
|
matches!(detected, Ok(Ok(true)))
|
|
} else {
|
|
false
|
|
};
|
|
if prior_h2 {
|
|
if let Some(m) = &metrics { m.http2_connections.fetch_add(1, Ordering::Relaxed); }
|
|
}
|
|
let extended_connect = prior_h2
|
|
&& frontend_state
|
|
.as_ref()
|
|
.is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false));
|
|
#[cfg(target_os = "linux")]
|
|
if !prior_h2 && frontend_state.as_ref().is_some_and(|state| {
|
|
state.config.load().web_php_services.values()
|
|
.any(|service| service.sendfile == crate::web_server::SendfileMode::Auto)
|
|
}) {
|
|
let (io, slots) = sendfile::SendfileIo::new(stream);
|
|
connection(
|
|
io, router, peer, local, Transport { tls: false }, None,
|
|
false, false, h2c, frontend_state, lease, stop, grace,
|
|
http2_settings, Some(slots),
|
|
).await;
|
|
return;
|
|
}
|
|
connection(
|
|
stream,
|
|
router,
|
|
peer,
|
|
local,
|
|
Transport { tls: false },
|
|
None,
|
|
prior_h2,
|
|
extended_connect,
|
|
h2c && !prior_h2,
|
|
frontend_state,
|
|
lease,
|
|
stop,
|
|
grace,
|
|
http2_settings,
|
|
Default::default(),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
}
|
|
}
|
|
let _ = tokio::time::timeout(grace + Duration::from_secs(1), async {
|
|
while tasks.join_next().await.is_some() {}
|
|
})
|
|
.await;
|
|
tasks.abort_all();
|
|
while tasks.join_next().await.is_some() {}
|
|
Ok(())
|
|
}
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum H2ShutdownCause {
|
|
Idle,
|
|
Lifetime,
|
|
Process,
|
|
}
|