From efdad2e7219107be28247b5429643daacc7fc6df Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Tue, 7 Apr 2026 23:22:04 +0300 Subject: [PATCH] fix: DoT cert needs explicit {tld}.{tld} SAN, not just *.{tld} wildcard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit self_signed_tls was passing an empty service_names list, so the generated cert only had the *.numa wildcard SAN. Strict TLS clients (browsers, possibly some iOS versions) reject wildcards under single-label TLDs — see the existing comment in tls.rs explaining why the proxy lists each service explicitly. setup-phone's mobileconfig sends ServerName "numa.numa" as SNI, so the DoT cert must have an explicit numa.numa SAN. Pass proxy_tld itself as a service name, mirroring how main.rs already registers "numa" as a service for the proxy's TLS cert. Test fixture updated to mirror the production SAN shape (*.numa + numa.numa) and switched the client to SNI "numa.numa", so the existing DoT test suite implicitly exercises the SNI path used by setup-phone clients. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dot.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/dot.rs b/src/dot.rs index b7e7875..487c25f 100644 --- a/src/dot.rs +++ b/src/dot.rs @@ -47,8 +47,15 @@ fn load_tls_config(cert_path: &Path, key_path: &Path) -> crate::Result Option> { - match crate::tls::build_tls_config(&ctx.proxy_tld, &[], dot_alpn()) { + let service_names = [ctx.proxy_tld.clone()]; + match crate::tls::build_tls_config(&ctx.proxy_tld, &service_names, dot_alpn()) { Ok(cfg) => Some(cfg), Err(e) => { warn!( @@ -272,12 +279,17 @@ mod tests { fn test_tls_configs() -> (Arc, Arc) { let _ = rustls::crypto::ring::default_provider().install_default(); + // Mirror production self_signed_tls SAN shape: *.numa wildcard plus + // explicit numa.numa apex (the ServerName setup-phone uses as SNI). let key_pair = KeyPair::generate().unwrap(); let mut params = CertificateParams::default(); params .distinguished_name - .push(DnType::CommonName, "localhost"); - params.subject_alt_names = vec![rcgen::SanType::DnsName("localhost".try_into().unwrap())]; + .push(DnType::CommonName, "Numa .numa services"); + params.subject_alt_names = vec![ + rcgen::SanType::DnsName("*.numa".try_into().unwrap()), + rcgen::SanType::DnsName("numa.numa".try_into().unwrap()), + ]; let cert = params.self_signed(&key_pair).unwrap(); let cert_der = CertificateDer::from(cert.der().to_vec()); @@ -367,6 +379,7 @@ mod tests { } /// Open a TLS connection to the DoT server and return the stream. + /// Uses SNI "numa.numa" to mirror what setup-phone's mobileconfig sends. async fn dot_connect( addr: SocketAddr, client_config: &Arc, @@ -374,7 +387,7 @@ mod tests { let connector = tokio_rustls::TlsConnector::from(Arc::clone(client_config)); let tcp = tokio::net::TcpStream::connect(addr).await.unwrap(); connector - .connect(ServerName::try_from("localhost").unwrap(), tcp) + .connect(ServerName::try_from("numa.numa").unwrap(), tcp) .await .unwrap() }