feat: DoT write timeout and ALPN "dot" advertisement
Two DoS/interop hardening items:
1. Bound write_framed by WRITE_TIMEOUT (10s) so a slow-reader
attacker can't indefinitely hold a worker task and its connection
permit. Symmetric to the existing handshake timeout.
2. Advertise ALPN "dot" per RFC 7858 §3.2. Required by some strict
DoT clients (newer Apple stacks, some Android versions). rustls
ServerConfig exposes alpn_protocols as a pub field so we set it
after with_single_cert:
- load_tls_config (user-provided cert/key): set directly
- self_signed_tls (new, replaces fallback_tls): builds a fresh
DoT-specific TLS config via build_tls_config with the ALPN list
build_tls_config now takes an `alpn: Vec<Vec<u8>>` parameter so
DoT and the proxy can pass different ALPN lists while sharing the
same CA. Proxy callers pass Vec::new() (unchanged behavior).
Dropped the ctx.tls_config reuse branch: we can't mutate a shared
Arc<ServerConfig> to add DoT-specific ALPN, and reusing the proxy
config was already quietly broken re: SAN (proxy cert covers
*.{tld}, not the DoT server's bind hostname/IP).
Added dot_negotiates_alpn test that asserts conn.alpn_protocol()
returns Some(b"dot") after handshake. 126/126 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
src/tls.rs
13
src/tls.rs
@@ -24,7 +24,7 @@ pub fn regenerate_tls(ctx: &ServerCtx) {
|
||||
names.extend(ctx.lan_peers.lock().unwrap().names());
|
||||
let names: Vec<String> = names.into_iter().collect();
|
||||
|
||||
match build_tls_config(&ctx.proxy_tld, &names) {
|
||||
match build_tls_config(&ctx.proxy_tld, &names, Vec::new()) {
|
||||
Ok(new_config) => {
|
||||
tls.store(new_config);
|
||||
info!("TLS cert regenerated for {} services", names.len());
|
||||
@@ -36,7 +36,13 @@ pub fn regenerate_tls(ctx: &ServerCtx) {
|
||||
/// Build a TLS config with a cert covering all provided service names.
|
||||
/// Wildcards under single-label TLDs (*.numa) are rejected by browsers,
|
||||
/// so we list each service explicitly as a SAN.
|
||||
pub fn build_tls_config(tld: &str, service_names: &[String]) -> crate::Result<Arc<ServerConfig>> {
|
||||
/// `alpn` is advertised in the TLS ServerHello — pass empty for the proxy
|
||||
/// (which accepts any ALPN), or `[b"dot"]` for DoT (RFC 7858 §3.2).
|
||||
pub fn build_tls_config(
|
||||
tld: &str,
|
||||
service_names: &[String],
|
||||
alpn: Vec<Vec<u8>>,
|
||||
) -> crate::Result<Arc<ServerConfig>> {
|
||||
let dir = crate::data_dir();
|
||||
let (ca_cert, ca_key) = ensure_ca(&dir)?;
|
||||
let (cert_chain, key) = generate_service_cert(&ca_cert, &ca_key, tld, service_names)?;
|
||||
@@ -44,9 +50,10 @@ pub fn build_tls_config(tld: &str, service_names: &[String]) -> crate::Result<Ar
|
||||
// Ensure a crypto provider is installed (rustls needs one)
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let config = ServerConfig::builder()
|
||||
let mut config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, key)?;
|
||||
config.alpn_protocols = alpn;
|
||||
|
||||
info!(
|
||||
"TLS configured for {} .{} domains",
|
||||
|
||||
Reference in New Issue
Block a user