From 7cc9ae66202219b2fe7b13f04b35aad57aa9fe13 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sat, 11 Apr 2026 01:39:15 +0300 Subject: [PATCH 1/5] chore: document multi-forwarder and cache warming in config and README Co-Authored-By: Claude Opus 4.6 --- README.md | 2 ++ numa.toml | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 69ecd80..44b8aa4 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,8 @@ From Machine B: `curl http://api.numa` → proxied to Machine A's port 8000. Ena - [x] DNS-over-TLS listener — encrypted client connections (RFC 7858, ALPN strict) - [x] Recursive resolution + DNSSEC — chain-of-trust, NSEC/NSEC3 - [x] SRTT-based nameserver selection +- [x] Multi-forwarder failover — multiple upstreams with SRTT ranking, fallback pool +- [x] Cache warming — proactive resolution for configured domains - [x] Mobile onboarding — `setup-phone` QR flow, mobile API, mobileconfig profiles - [ ] pkarr integration — self-sovereign DNS via Mainline DHT - [ ] Global `.numa` names — DHT-backed, no registrar diff --git a/numa.toml b/numa.toml index 4389fdb..5ca95f8 100644 --- a/numa.toml +++ b/numa.toml @@ -12,10 +12,11 @@ api_port = 5380 # [upstream] # mode = "forward" # "forward" (default) — relay to upstream # # "recursive" — resolve from root hints (no address needed) +# address = "9.9.9.9" # single upstream (plain UDP) +# address = ["192.168.1.1", "9.9.9.9:5353"] # multiple upstreams — SRTT picks fastest # address = "https://dns.quad9.net/dns-query" # DNS-over-HTTPS (encrypted) -# address = "https://cloudflare-dns.com/dns-query" # Cloudflare DoH -# address = "9.9.9.9" # plain UDP -# port = 53 # only for forward mode, plain UDP +# fallback = ["8.8.8.8", "1.1.1.1"] # tried only when all primaries fail +# port = 53 # default port for addresses without :port # timeout_ms = 3000 # root_hints = [ # only used in recursive mode # "198.41.0.4", # a.root-servers.net (Verisign) @@ -54,6 +55,7 @@ api_port = 5380 max_entries = 10000 min_ttl = 60 max_ttl = 86400 +# warm = ["google.com", "github.com"] # resolve at startup, refresh before TTL expiry [proxy] enabled = true -- 2.34.1 From d725091642a980dff01b9c234e401d774ed5a3e3 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sat, 11 Apr 2026 03:04:31 +0300 Subject: [PATCH 2/5] feat: DNS-over-HTTPS server endpoint (RFC 8484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serve DoH at POST /dns-query on the existing HTTPS proxy (port 443). Automatically enabled when proxy TLS is active — no config needed. Also fix zone map priority so local zones override RFC 6762 .local special-use handling. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ctx.rs | 8 +- src/doh.rs | 189 +++++++++++++++++++++++++++++++++++++++++++ src/health.rs | 4 + src/lib.rs | 1 + src/main.rs | 9 +++ src/proxy.rs | 33 ++++++-- tests/integration.sh | 48 +++++++++++ 7 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 src/doh.rs diff --git a/src/ctx.rs b/src/ctx.rs index b4e0777..3ef6a0a 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -110,6 +110,10 @@ pub async fn resolve_query( 300, )); (resp, QueryPath::Local, DnssecStatus::Indeterminate) + } else if let Some(records) = ctx.zone_map.get(qname.as_str()).and_then(|m| m.get(&qtype)) { + let mut resp = DnsPacket::response_from(&query, ResultCode::NOERROR); + resp.answers = records.clone(); + (resp, QueryPath::Local, DnssecStatus::Indeterminate) } else if is_special_use_domain(&qname) { // RFC 6761/8880: private PTR, DDR, NAT64 — answer locally let resp = special_use_response(&query, &qname, qtype); @@ -158,10 +162,6 @@ pub async fn resolve_query( 60, )); (resp, QueryPath::Blocked, DnssecStatus::Indeterminate) - } else if let Some(records) = ctx.zone_map.get(qname.as_str()).and_then(|m| m.get(&qtype)) { - let mut resp = DnsPacket::response_from(&query, ResultCode::NOERROR); - resp.answers = records.clone(); - (resp, QueryPath::Local, DnssecStatus::Indeterminate) } else { let cached = ctx.cache.read().unwrap().lookup_with_status(&qname, qtype); if let Some((cached, cached_dnssec)) = cached { diff --git a/src/doh.rs b/src/doh.rs new file mode 100644 index 0000000..44e417f --- /dev/null +++ b/src/doh.rs @@ -0,0 +1,189 @@ +use std::net::SocketAddr; + +use axum::body::Bytes; +use axum::extract::{Request, State}; +use axum::response::{IntoResponse, Response}; +use hyper::StatusCode; +use log::warn; + +use crate::buffer::BytePacketBuffer; +use crate::ctx::{resolve_query, ServerCtx}; +use crate::header::ResultCode; +use crate::packet::DnsPacket; + +const MAX_DNS_MSG: usize = 4096; +const DOH_CONTENT_TYPE: &str = "application/dns-message"; + +pub async fn doh_post( + State(state): State, + req: Request, +) -> Response { + let host = super::proxy::extract_host(&req); + if !is_doh_host(host.as_deref(), &state.ctx.proxy_tld) { + return StatusCode::NOT_FOUND.into_response(); + } + + let content_type = req + .headers() + .get(hyper::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + if !content_type.starts_with(DOH_CONTENT_TYPE) { + return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response(); + } + + let body = match axum::body::to_bytes(req.into_body(), MAX_DNS_MSG).await { + Ok(b) => b, + Err(_) => return (StatusCode::PAYLOAD_TOO_LARGE, "body exceeds 4096 bytes").into_response(), + }; + + if body.is_empty() { + return (StatusCode::BAD_REQUEST, "empty body").into_response(); + } + + let src = state + .remote_addr + .unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 0))); + + resolve_doh(&body, src, &state.ctx).await +} + +fn is_doh_host(host: Option<&str>, tld: &str) -> bool { + match host { + Some(h) if h == tld => true, + Some(h) => h.len() == 2 * tld.len() + 1 + && h.starts_with(tld) + && h.as_bytes().get(tld.len()) == Some(&b'.') + && h.ends_with(tld), + None => false, + } +} + +async fn resolve_doh(dns_bytes: &[u8], src: SocketAddr, ctx: &ServerCtx) -> Response { + let mut buffer = BytePacketBuffer::from_bytes(dns_bytes); + let query = match DnsPacket::from_buffer(&mut buffer) { + Ok(q) => q, + Err(e) => { + warn!("DoH: parse error from {}: {}", src, e); + let query_id = u16::from_be_bytes([ + dns_bytes.first().copied().unwrap_or(0), + dns_bytes.get(1).copied().unwrap_or(0), + ]); + let mut resp = DnsPacket::new(); + resp.header.id = query_id; + resp.header.response = true; + resp.header.rescode = ResultCode::FORMERR; + return serialize_response(&resp); + } + }; + + let query_id = query.header.id; + let query_rd = query.header.recursion_desired; + let questions = query.questions.clone(); + + match resolve_query(query, src, ctx).await { + Ok(resp_buffer) => { + let min_ttl = extract_min_ttl(resp_buffer.filled()); + dns_response(resp_buffer.filled(), min_ttl) + } + Err(e) => { + warn!("DoH: resolve error for {}: {}", src, e); + let mut resp = DnsPacket::new(); + resp.header.id = query_id; + resp.header.response = true; + resp.header.recursion_desired = query_rd; + resp.header.recursion_available = true; + resp.header.rescode = ResultCode::SERVFAIL; + resp.questions = questions; + serialize_response(&resp) + } + } +} + +fn extract_min_ttl(wire: &[u8]) -> u32 { + let mut buf = BytePacketBuffer::from_bytes(wire); + match DnsPacket::from_buffer(&mut buf) { + Ok(pkt) => pkt + .answers + .iter() + .map(|r| r.ttl()) + .min() + .unwrap_or(0), + Err(_) => 0, + } +} + +fn dns_response(wire: &[u8], min_ttl: u32) -> Response { + ( + StatusCode::OK, + [ + (hyper::header::CONTENT_TYPE, DOH_CONTENT_TYPE), + (hyper::header::CACHE_CONTROL, &format!("max-age={}", min_ttl)), + ], + Bytes::copy_from_slice(wire), + ) + .into_response() +} + +fn serialize_response(pkt: &DnsPacket) -> Response { + let mut buf = BytePacketBuffer::new(); + match pkt.write(&mut buf) { + Ok(_) => dns_response(buf.filled(), 0), + Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::buffer::BytePacketBuffer; + use crate::header::ResultCode; + use crate::packet::DnsPacket; + use crate::record::DnsRecord; + + #[test] + fn is_doh_host_matches_tld() { + assert!(is_doh_host(Some("numa"), "numa")); + assert!(is_doh_host(Some("numa.numa"), "numa")); + assert!(!is_doh_host(Some("foo.numa"), "numa")); + assert!(!is_doh_host(None, "numa")); + } + + #[test] + fn extract_min_ttl_from_response() { + let mut pkt = DnsPacket::new(); + pkt.header.response = true; + pkt.answers.push(DnsRecord::A { + domain: "example.com".to_string(), + addr: std::net::Ipv4Addr::new(1, 2, 3, 4), + ttl: 300, + }); + pkt.answers.push(DnsRecord::A { + domain: "example.com".to_string(), + addr: std::net::Ipv4Addr::new(5, 6, 7, 8), + ttl: 60, + }); + let mut buf = BytePacketBuffer::new(); + pkt.write(&mut buf).unwrap(); + assert_eq!(extract_min_ttl(buf.filled()), 60); + } + + #[test] + fn extract_min_ttl_no_answers() { + let mut pkt = DnsPacket::new(); + pkt.header.response = true; + let mut buf = BytePacketBuffer::new(); + pkt.write(&mut buf).unwrap(); + assert_eq!(extract_min_ttl(buf.filled()), 0); + } + + #[test] + fn serialize_formerr_response() { + let mut pkt = DnsPacket::new(); + pkt.header.id = 0xABCD; + pkt.header.response = true; + pkt.header.rescode = ResultCode::FORMERR; + let resp = serialize_response(&pkt); + assert_eq!(resp.status(), StatusCode::OK); + } +} diff --git a/src/health.rs b/src/health.rs index b2359c4..e55c569 100644 --- a/src/health.rs +++ b/src/health.rs @@ -73,11 +73,15 @@ impl HealthMeta { recursive_enabled: bool, mdns_enabled: bool, blocking_enabled: bool, + doh_enabled: bool, ) -> Self { let ca_path = data_dir.join("ca.pem"); let ca_fingerprint_sha256 = compute_ca_fingerprint(&ca_path); let mut features = Vec::new(); + if doh_enabled { + features.push("doh".to_string()); + } if dot_enabled { features.push("dot".to_string()); } diff --git a/src/lib.rs b/src/lib.rs index 066c7ca..be71125 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod cache; pub mod config; pub mod ctx; pub mod dnssec; +pub mod doh; pub mod dot; pub mod forward; pub mod header; diff --git a/src/main.rs b/src/main.rs index cee680a..903be9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -243,6 +243,7 @@ async fn main() -> numa::Result<()> { None }; + let doh_enabled = initial_tls.is_some(); let health_meta = numa::health::HealthMeta::build( &resolved_data_dir, config.dot.enabled, @@ -252,6 +253,7 @@ async fn main() -> numa::Result<()> { resolved_mode == numa::config::UpstreamMode::Recursive, config.lan.enabled, config.blocking.enabled, + doh_enabled, ); let ca_pem = std::fs::read_to_string(resolved_data_dir.join("ca.pem")).ok(); @@ -431,6 +433,13 @@ async fn main() -> numa::Result<()> { if config.dot.enabled { row("DoT", g, &format!("tls://:{}", config.dot.port)); } + if doh_enabled { + row( + "DoH", + g, + &format!("https://:{}/dns-query", config.proxy.tls_port), + ); + } if config.lan.enabled { row("LAN", g, "mDNS (_numa._tcp.local)"); } diff --git a/src/proxy.rs b/src/proxy.rs index 244e597..d945260 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use axum::body::Body; use axum::extract::{Request, State}; use axum::response::IntoResponse; -use axum::routing::any; +use axum::routing::{any, post}; use axum::Router; use http_body_util::BodyExt; use hyper::StatusCode; @@ -18,6 +18,14 @@ use crate::ctx::ServerCtx; type HttpClient = Client; +/// State passed to the DoH handler. Includes the remote address so +/// `resolve_query` can log the client IP. +#[derive(Clone)] +pub struct DohState { + pub ctx: Arc, + pub remote_addr: Option, +} + #[derive(Clone)] struct ProxyState { ctx: Arc, @@ -74,9 +82,17 @@ pub async fn start_proxy_tls(ctx: Arc, port: u16, bind_addr: Ipv4Addr // Hold a separate Arc so we can access tls_config after ctx moves into ProxyState let tls_holder = Arc::clone(&ctx); - let state = ProxyState { ctx, client }; + let proxy_state = ProxyState { + ctx: Arc::clone(&ctx), + client, + }; - let app = Router::new().fallback(any(proxy_handler)).with_state(state); + // DoH route (RFC 8484) served only on the TLS listener. + // DohState.remote_addr is set per-connection below. + let doh_state = DohState { + ctx, + remote_addr: None, + }; loop { let (tcp_stream, remote_addr) = match listener.accept().await { @@ -91,7 +107,14 @@ pub async fn start_proxy_tls(ctx: Arc, port: u16, bind_addr: Ipv4Addr // unwrap safe: guarded by is_none() check above let acceptor = TlsAcceptor::from(Arc::clone(&*tls_holder.tls_config.as_ref().unwrap().load())); - let app = app.clone(); + + let mut conn_doh_state = doh_state.clone(); + conn_doh_state.remote_addr = Some(remote_addr); + + let app = Router::new() + .route("/dns-query", post(crate::doh::doh_post).with_state(conn_doh_state)) + .fallback(any(proxy_handler)) + .with_state(proxy_state.clone()); tokio::spawn(async move { let tls_stream = match acceptor.accept(tcp_stream).await { @@ -232,7 +255,7 @@ pre .str {{ color: #d48a5a }} ) } -fn extract_host(req: &Request) -> Option { +pub fn extract_host(req: &Request) -> Option { req.headers() .get(hyper::header::HOST) .and_then(|v| v.to_str().ok()) diff --git a/tests/integration.sh b/tests/integration.sh index 473356e..92da878 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -622,6 +622,54 @@ CONF "10.0.0.1" \ "$($KDIG +short dot-test.example A 2>/dev/null)" + echo "" + echo "=== DNS-over-HTTPS (RFC 8484) ===" + + DOH_QUERY_FILE=/tmp/numa-doh-query.bin + DOH_RESP_FILE=/tmp/numa-doh-resp.bin + + # Build DNS wire-format query for dot-test.example A + printf '\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x08dot-test\x07example\x00\x00\x01\x00\x01' > "$DOH_QUERY_FILE" + + # POST valid DoH query + DOH_CODE=$(curl -sk -X POST \ + --resolve "numa.numa:$PROXY_HTTPS_PORT:127.0.0.1" \ + -H "Content-Type: application/dns-message" \ + --data-binary @"$DOH_QUERY_FILE" \ + --cacert "$CA" \ + -o "$DOH_RESP_FILE" \ + -w "%{http_code}" \ + "https://numa.numa:$PROXY_HTTPS_PORT/dns-query") + check "DoH POST returns HTTP 200" "200" "$DOH_CODE" + + # Check response contains IP 10.0.0.1 (hex: 0a000001) + DOH_HEX=$(xxd -p "$DOH_RESP_FILE" | tr -d '\n') + if echo "$DOH_HEX" | grep -q "0a000001"; then + check "DoH response resolves dot-test.example → 10.0.0.1" "found" "found" + else + check "DoH response resolves dot-test.example → 10.0.0.1" "0a000001" "$DOH_HEX" + fi + + # Wrong Content-Type → 415 + DOH_CT_CODE=$(curl -sk -X POST \ + -H "Host: numa.numa" \ + -H "Content-Type: text/plain" \ + --data-binary @"$DOH_QUERY_FILE" \ + -o /dev/null -w "%{http_code}" \ + "https://127.0.0.1:$PROXY_HTTPS_PORT/dns-query") + check "DoH wrong Content-Type → 415" "415" "$DOH_CT_CODE" + + # Wrong host → 404 (DoH only serves numa.numa) + DOH_HOST_CODE=$(curl -sk -X POST \ + -H "Host: foo.numa" \ + -H "Content-Type: application/dns-message" \ + --data-binary @"$DOH_QUERY_FILE" \ + -o /dev/null -w "%{http_code}" \ + "https://127.0.0.1:$PROXY_HTTPS_PORT/dns-query") + check "DoH wrong host → 404" "404" "$DOH_HOST_CODE" + + rm -f "$DOH_QUERY_FILE" "$DOH_RESP_FILE" + echo "" echo "=== Proxy TLS works with DoT enabled ===" -- 2.34.1 From 1bae69681020795dcb258ca4d0cc349a9bb1bf8e Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sat, 11 Apr 2026 03:04:50 +0300 Subject: [PATCH 3/5] style: cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) --- src/doh.rs | 31 +++++++++++++++---------------- src/proxy.rs | 5 ++++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/doh.rs b/src/doh.rs index 44e417f..cf50b31 100644 --- a/src/doh.rs +++ b/src/doh.rs @@ -14,10 +14,7 @@ use crate::packet::DnsPacket; const MAX_DNS_MSG: usize = 4096; const DOH_CONTENT_TYPE: &str = "application/dns-message"; -pub async fn doh_post( - State(state): State, - req: Request, -) -> Response { +pub async fn doh_post(State(state): State, req: Request) -> Response { let host = super::proxy::extract_host(&req); if !is_doh_host(host.as_deref(), &state.ctx.proxy_tld) { return StatusCode::NOT_FOUND.into_response(); @@ -34,7 +31,9 @@ pub async fn doh_post( let body = match axum::body::to_bytes(req.into_body(), MAX_DNS_MSG).await { Ok(b) => b, - Err(_) => return (StatusCode::PAYLOAD_TOO_LARGE, "body exceeds 4096 bytes").into_response(), + Err(_) => { + return (StatusCode::PAYLOAD_TOO_LARGE, "body exceeds 4096 bytes").into_response() + } }; if body.is_empty() { @@ -51,10 +50,12 @@ pub async fn doh_post( fn is_doh_host(host: Option<&str>, tld: &str) -> bool { match host { Some(h) if h == tld => true, - Some(h) => h.len() == 2 * tld.len() + 1 - && h.starts_with(tld) - && h.as_bytes().get(tld.len()) == Some(&b'.') - && h.ends_with(tld), + Some(h) => { + h.len() == 2 * tld.len() + 1 + && h.starts_with(tld) + && h.as_bytes().get(tld.len()) == Some(&b'.') + && h.ends_with(tld) + } None => false, } } @@ -103,12 +104,7 @@ async fn resolve_doh(dns_bytes: &[u8], src: SocketAddr, ctx: &ServerCtx) -> Resp fn extract_min_ttl(wire: &[u8]) -> u32 { let mut buf = BytePacketBuffer::from_bytes(wire); match DnsPacket::from_buffer(&mut buf) { - Ok(pkt) => pkt - .answers - .iter() - .map(|r| r.ttl()) - .min() - .unwrap_or(0), + Ok(pkt) => pkt.answers.iter().map(|r| r.ttl()).min().unwrap_or(0), Err(_) => 0, } } @@ -118,7 +114,10 @@ fn dns_response(wire: &[u8], min_ttl: u32) -> Response { StatusCode::OK, [ (hyper::header::CONTENT_TYPE, DOH_CONTENT_TYPE), - (hyper::header::CACHE_CONTROL, &format!("max-age={}", min_ttl)), + ( + hyper::header::CACHE_CONTROL, + &format!("max-age={}", min_ttl), + ), ], Bytes::copy_from_slice(wire), ) diff --git a/src/proxy.rs b/src/proxy.rs index d945260..b158d9b 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -112,7 +112,10 @@ pub async fn start_proxy_tls(ctx: Arc, port: u16, bind_addr: Ipv4Addr conn_doh_state.remote_addr = Some(remote_addr); let app = Router::new() - .route("/dns-query", post(crate::doh::doh_post).with_state(conn_doh_state)) + .route( + "/dns-query", + post(crate::doh::doh_post).with_state(conn_doh_state), + ) .fallback(any(proxy_handler)) .with_state(proxy_state.clone()); -- 2.34.1 From bec3b53830ec6d76b14b91196097cc1f326cf382 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sat, 11 Apr 2026 03:49:23 +0300 Subject: [PATCH 4/5] chore: remove GoatCounter analytics from site GoatCounter domains (goatcounter.com, gc.zgo.at) are blocked by Hagezi Pro, which is Numa's default blocklist. A DNS privacy tool should not embed analytics that its own resolver blocks. Co-Authored-By: Claude Opus 4.6 --- site/blog-template.html | 2 -- site/blog/index.html | 2 -- site/index.html | 2 -- 3 files changed, 6 deletions(-) diff --git a/site/blog-template.html b/site/blog-template.html index 85e854b..54f0eae 100644 --- a/site/blog-template.html +++ b/site/blog-template.html @@ -298,7 +298,5 @@ $body$ Blog - diff --git a/site/blog/index.html b/site/blog/index.html index 10d62a7..993c166 100644 --- a/site/blog/index.html +++ b/site/blog/index.html @@ -197,7 +197,5 @@ body::before { Home - diff --git a/site/index.html b/site/index.html index 27ea8fb..0231e0a 100644 --- a/site/index.html +++ b/site/index.html @@ -1769,7 +1769,5 @@ const observer = new IntersectionObserver((entries) => { document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); - -- 2.34.1 From 730c400ddbdc31991b3eff510ad5dad1db1f7ff4 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sat, 11 Apr 2026 04:01:18 +0300 Subject: [PATCH 5/5] feat: enable DoT listener by default DoT now starts automatically with `sudo numa`, matching the proxy and DoH which are already on by default. The self-signed CA infrastructure is shared with the proxy, so there is no additional setup. This makes `numa setup-phone` work out of the box. Co-Authored-By: Claude Opus 4.6 (1M context) --- numa.toml | 2 +- src/config.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/numa.toml b/numa.toml index 5ca95f8..92b5411 100644 --- a/numa.toml +++ b/numa.toml @@ -93,7 +93,7 @@ tld = "numa" # DNS-over-TLS listener (RFC 7858) — encrypted DNS on port 853 # [dot] -# enabled = false # opt-in: accept DoT queries +# enabled = true # on by default; set false to disable # port = 853 # standard DoT port # bind_addr = "0.0.0.0" # IPv4 or IPv6; unspecified binds all interfaces # cert_path = "/etc/numa/dot.crt" # PEM cert; omit to use self-signed (proxy CA if available) diff --git a/src/config.rs b/src/config.rs index 708ed4f..6480883 100644 --- a/src/config.rs +++ b/src/config.rs @@ -411,7 +411,7 @@ pub struct DnssecConfig { #[derive(Deserialize, Clone)] pub struct DotConfig { - #[serde(default)] + #[serde(default = "default_dot_enabled")] pub enabled: bool, #[serde(default = "default_dot_port")] pub port: u16, @@ -428,7 +428,7 @@ pub struct DotConfig { impl Default for DotConfig { fn default() -> Self { DotConfig { - enabled: false, + enabled: default_dot_enabled(), port: default_dot_port(), bind_addr: default_dot_bind_addr(), cert_path: None, @@ -437,6 +437,9 @@ impl Default for DotConfig { } } +fn default_dot_enabled() -> bool { + true +} fn default_dot_port() -> u16 { 853 } -- 2.34.1