DoH encapsulates DNS queries inside HTTPS requests, making them:
Encrypted: ISP can’t read or modify queries
Authenticated: Server identity verified via TLS
Indistinguishable: Looks like regular HTTPS traffic
Normal DNS: Client → [UDP port 53, plaintext] → ISP DNS ↑ ISP can read & modifyDNS-over-HTTPS: Client → [HTTPS POST to 1.1.1.1/dns-query] → Cloudflare ↑ ISP sees encrypted HTTPS
// From engine/src/dns.rs:84-92fn get_cached(&self, hostname: &str) -> Option<Vec<IpAddr>> { let cache = self.cache.read().ok()?; let (ips, expiry) = cache.get(hostname)?; if Instant::now() < *expiry { Some(ips.clone()) } else { None // Expired }}
Fast path: If hostname was recently resolved and TTL hasn’t expired, return cached IPs immediately.
// From engine/src/dns.rs:103-149 (simplified)async fn doh_query(&self, server: &str, path: &str, hostname: &str) -> std::io::Result<Vec<IpAddr>> { // Connect via TCP let addr: SocketAddr = format!("{}:443", server).parse().unwrap(); let stream = tokio::time::timeout( Duration::from_secs(5), TcpStream::connect(addr) ).await??; // Establish TLS let connector = tokio_native_tls::TlsConnector::from( native_tls::TlsConnector::new()? ); let mut tls_stream = tokio::time::timeout( Duration::from_secs(5), connector.connect(server, stream) ).await??; // Send HTTP GET request let request = format!( "GET {}?name={}&type=A HTTP/1.1\r\n\ Host: {}\r\n\ Accept: application/dns-json\r\n\ Connection: close\r\n\r\n", path, hostname, server ); tls_stream.write_all(request.as_bytes()).await?; tls_stream.flush().await?; // Read response let mut response = Vec::new(); tls_stream.read_to_end(&mut response).await?; // Parse JSON response let response_str = String::from_utf8_lossy(&response); self.parse_doh_response(&response_str)}
// From engine/src/dns.rs:151-172fn parse_doh_response(&self, response: &str) -> std::io::Result<Vec<IpAddr>> { // Split HTTP headers from body let body = response.split("\r\n\r\n").nth(1).unwrap_or(""); let mut ips = Vec::new(); // Simple JSON parsing (extract "data":"IP" fields) for part in body.split("\"data\"") { if let Some(start) = part.find(":\"") { let rest = &part[start + 2..]; if let Some(end) = rest.find('"') { let ip_str = &rest[..end]; if let Ok(ip) = ip_str.parse::<IpAddr>() { ips.push(ip); } } } } Ok(ips)}
This is a simple, dependency-free parser that extracts IPs from JSON without a full JSON library. Robust enough for well-formed DoH responses.
// From engine/src/dns.rs:94-101fn cache_result(&self, hostname: &str, ips: &[IpAddr]) { if let Ok(mut cache) = self.cache.write() { cache.insert( hostname.to_string(), (ips.to_vec(), Instant::now() + self.ttl), ); }}
Another 5-second timeout for TLS handshake.Total worst-case time per provider: 10 seconds (5s connect + 5s TLS) Total worst-case with 3 providers: 30 seconds before giving up
Why such long timeouts?
DoH is critical for bypass. We prefer to wait longer rather than fail prematurely.In practice:
// From engine/src/dns.rs:151-172fn parse_doh_response(&self, response: &str) -> std::io::Result<Vec<IpAddr>> { let body = response.split("\r\n\r\n").nth(1).unwrap_or(""); let mut ips = Vec::new(); // Find all "data":"<IP>" fields for part in body.split("\"data\"") { if let Some(start) = part.find(":\"") { let rest = &part[start + 2..]; if let Some(end) = rest.find('"') { let ip_str = &rest[..end]; if let Ok(ip) = ip_str.parse::<IpAddr>() { ips.push(ip); } } } } Ok(ips)}
Algorithm:
Split HTTP headers from body at \r\n\r\n
Find all occurrences of "data"
Extract string between :" and "
Parse as IP address
Collect all valid IPs
Robustness: Even if JSON is malformed, this will extract valid IPs. Won’t be confused by extra fields or formatting.