feat: add DNS-over-TLS (DoT) listener #25
72
src/dot.rs
72
src/dot.rs
@@ -275,8 +275,9 @@ mod tests {
|
|||||||
use crate::question::QueryType;
|
use crate::question::QueryType;
|
||||||
use crate::record::DnsRecord;
|
use crate::record::DnsRecord;
|
||||||
|
|
||||||
/// Generate a self-signed cert + key in memory, return (ServerConfig, ClientConfig).
|
/// Generate a self-signed DoT server config and return its leaf cert DER
|
||||||
fn test_tls_configs() -> (Arc<ServerConfig>, Arc<rustls::ClientConfig>) {
|
/// so callers can build matching client configs with arbitrary ALPN.
|
||||||
|
fn test_tls_configs() -> (Arc<ServerConfig>, CertificateDer<'static>) {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
// Mirror production self_signed_tls SAN shape: *.numa wildcard plus
|
// Mirror production self_signed_tls SAN shape: *.numa wildcard plus
|
||||||
@@ -301,22 +302,31 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
server_config.alpn_protocols = dot_alpn();
|
server_config.alpn_protocols = dot_alpn();
|
||||||
|
|
||||||
let mut root_store = rustls::RootCertStore::empty();
|
(Arc::new(server_config), cert_der)
|
||||||
root_store.add(cert_der).unwrap();
|
|
||||||
let mut client_config = rustls::ClientConfig::builder()
|
|
||||||
.with_root_certificates(root_store)
|
|
||||||
.with_no_client_auth();
|
|
||||||
client_config.alpn_protocols = dot_alpn();
|
|
||||||
|
|
||||||
(Arc::new(server_config), Arc::new(client_config))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spin up a DoT listener with a test TLS config. Returns (addr, client_config).
|
/// Build a TLS client config that trusts `cert_der` and advertises the
|
||||||
|
/// given ALPN protocols. Used by tests to vary ALPN per test case.
|
||||||
|
fn dot_client(
|
||||||
|
cert_der: &CertificateDer<'static>,
|
||||||
|
alpn: Vec<Vec<u8>>,
|
||||||
|
) -> Arc<rustls::ClientConfig> {
|
||||||
|
let mut root_store = rustls::RootCertStore::empty();
|
||||||
|
root_store.add(cert_der.clone()).unwrap();
|
||||||
|
let mut config = rustls::ClientConfig::builder()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_no_client_auth();
|
||||||
|
config.alpn_protocols = alpn;
|
||||||
|
Arc::new(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spin up a DoT listener with a test TLS config. Returns the bind addr
|
||||||
|
/// and the leaf cert DER so callers can build clients with arbitrary ALPN.
|
||||||
/// The upstream is pointed at a bound-but-unresponsive UDP socket we own, so
|
/// The upstream is pointed at a bound-but-unresponsive UDP socket we own, so
|
||||||
/// any query that escapes to the upstream path times out deterministically
|
/// any query that escapes to the upstream path times out deterministically
|
||||||
/// (SERVFAIL) regardless of what the host has running on port 53.
|
/// (SERVFAIL) regardless of what the host has running on port 53.
|
||||||
async fn spawn_dot_server() -> (SocketAddr, Arc<rustls::ClientConfig>) {
|
async fn spawn_dot_server() -> (SocketAddr, CertificateDer<'static>) {
|
||||||
let (server_tls, client_tls) = test_tls_configs();
|
let (server_tls, cert_der) = test_tls_configs();
|
||||||
|
|
||||||
let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
// Bind an unresponsive upstream and leak it so it lives for the test duration.
|
// Bind an unresponsive upstream and leak it so it lives for the test duration.
|
||||||
@@ -375,7 +385,7 @@ mod tests {
|
|||||||
|
|
||||||
tokio::spawn(accept_loop(listener, acceptor, ctx));
|
tokio::spawn(accept_loop(listener, acceptor, ctx));
|
||||||
|
|
||||||
(addr, client_tls)
|
(addr, cert_der)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a TLS connection to the DoT server and return the stream.
|
/// Open a TLS connection to the DoT server and return the stream.
|
||||||
@@ -419,7 +429,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dot_resolves_local_zone() {
|
async fn dot_resolves_local_zone() {
|
||||||
let (addr, client_config) = spawn_dot_server().await;
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, dot_alpn());
|
||||||
let mut stream = dot_connect(addr, &client_config).await;
|
let mut stream = dot_connect(addr, &client_config).await;
|
||||||
|
|
||||||
let query = DnsPacket::query(0x1234, "dot-test.example", QueryType::A);
|
let query = DnsPacket::query(0x1234, "dot-test.example", QueryType::A);
|
||||||
@@ -441,7 +452,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dot_multiple_queries_on_persistent_connection() {
|
async fn dot_multiple_queries_on_persistent_connection() {
|
||||||
let (addr, client_config) = spawn_dot_server().await;
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, dot_alpn());
|
||||||
let mut stream = dot_connect(addr, &client_config).await;
|
let mut stream = dot_connect(addr, &client_config).await;
|
||||||
|
|
||||||
for i in 0..3u16 {
|
for i in 0..3u16 {
|
||||||
@@ -455,7 +467,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dot_nxdomain_for_unknown() {
|
async fn dot_nxdomain_for_unknown() {
|
||||||
let (addr, client_config) = spawn_dot_server().await;
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, dot_alpn());
|
||||||
let mut stream = dot_connect(addr, &client_config).await;
|
let mut stream = dot_connect(addr, &client_config).await;
|
||||||
|
|
||||||
let query = DnsPacket::query(0xBEEF, "nonexistent.test", QueryType::A);
|
let query = DnsPacket::query(0xBEEF, "nonexistent.test", QueryType::A);
|
||||||
@@ -472,15 +485,36 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dot_negotiates_alpn() {
|
async fn dot_negotiates_alpn() {
|
||||||
let (addr, client_config) = spawn_dot_server().await;
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, dot_alpn());
|
||||||
let stream = dot_connect(addr, &client_config).await;
|
let stream = dot_connect(addr, &client_config).await;
|
||||||
let (_io, conn) = stream.get_ref();
|
let (_io, conn) = stream.get_ref();
|
||||||
assert_eq!(conn.alpn_protocol(), Some(&b"dot"[..]));
|
assert_eq!(conn.alpn_protocol(), Some(&b"dot"[..]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dot_rejects_non_dot_alpn() {
|
||||||
|
// Cross-protocol confusion defense: a client that only offers "h2"
|
||||||
|
// (e.g. an HTTP/2 client mistakenly hitting :853) must not complete
|
||||||
|
// a TLS handshake with the DoT server. Verifies the rustls server
|
||||||
|
// sends `no_application_protocol` rather than silently negotiating.
|
||||||
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, vec![b"h2".to_vec()]);
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(client_config);
|
||||||
|
let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
|
||||||
|
let result = connector
|
||||||
|
.connect(ServerName::try_from("numa.numa").unwrap(), tcp)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"DoT server must reject ALPN that doesn't include \"dot\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dot_concurrent_connections() {
|
async fn dot_concurrent_connections() {
|
||||||
let (addr, client_config) = spawn_dot_server().await;
|
let (addr, cert_der) = spawn_dot_server().await;
|
||||||
|
let client_config = dot_client(&cert_der, dot_alpn());
|
||||||
|
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
for i in 0..5u16 {
|
for i in 0..5u16 {
|
||||||
|
|||||||
Reference in New Issue
Block a user