From f56b68d6f5f893d75d32d49a5606ad194d0d6f50 Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 00:33:18 +0000 Subject: [PATCH] =?UTF-8?q?resolver:=20close=20an=20off-path=20DNS=20spoof?= =?UTF-8?q?=20=E2=80=94=20connect()=20the=20UDP=20socket=20so=20the=20kern?= =?UTF-8?q?el=20drops=20replies=20from=20any=20IP=20but=20the=20nameserver?= =?UTF-8?q?,=20and=20use=20a=20CSPRNG=20per-query=20transaction=20id=20ins?= =?UTF-8?q?tead=20of=20the=20hardcoded=200x4543/0x4544=20(which=20were=20i?= =?UTF-8?q?n=20the=20public=20source,=20so=20rDNS/DNSBL=20answers=20could?= =?UTF-8?q?=20be=20forged=20with=20no=20guessing)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/resolver.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/resolver.rs b/src/resolver.rs index 803f779..b46e2b0 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -174,6 +174,17 @@ fn read_nameserver() -> String { "1.1.1.1:53".to_string() } +/// A random 16-bit DNS transaction id from the CSPRNG. Combined with the +/// connected socket, an off-path attacker can neither guess the id nor deliver a +/// reply from a spoofed source, so cache-poisoning of rDNS/DNSBL is infeasible. +fn rand_txid() -> u16 { + let mut b = [0u8; 2]; + match openssl::rand::rand_bytes(&mut b) { + Ok(()) => u16::from_be_bytes(b), + Err(_) => 0x4543, // RNG failure (never observed) — still validated on parse + } +} + /// Build a `qtype` query for `qname`, send it to `ns`, and return the raw reply /// (with the transaction id validated). fn send_query(ns: &str, qname: &str, qtype: u16, id: u16, timeout: Duration) -> Option> { @@ -181,6 +192,10 @@ fn send_query(ns: &str, qname: &str, qtype: u16, id: u16, timeout: Duration) -> .or_else(|_| UdpSocket::bind("[::]:0")) .ok()?; sock.set_read_timeout(Some(timeout)).ok()?; + // Connect to the nameserver: the kernel then drops any datagram from a + // different source, so an off-path attacker can't inject a spoofed reply. + // (An unconnected `recv` would accept a forged answer from any IP.) + sock.connect(ns).ok()?; let mut query = Vec::with_capacity(qname.len() + 18); query.extend_from_slice(&id.to_be_bytes()); query.extend_from_slice(&[0x01, 0x00]); // flags: RD=1 @@ -189,7 +204,7 @@ fn send_query(ns: &str, qname: &str, qtype: u16, id: u16, timeout: Duration) -> encode_name(&mut query, qname); query.extend_from_slice(&qtype.to_be_bytes()); query.extend_from_slice(&QCLASS_IN.to_be_bytes()); - sock.send_to(&query, ns).ok()?; + sock.send(&query).ok()?; let mut buf = [0u8; 1500]; let n = sock.recv(&mut buf).ok()?; if n < 12 || u16::from_be_bytes([buf[0], buf[1]]) != id { @@ -200,8 +215,9 @@ fn send_query(ns: &str, qname: &str, qtype: u16, id: u16, timeout: Duration) -> /// Send a PTR query for `qname` to `ns` and return the first PTR answer name. fn ptr_lookup(ns: &str, qname: &str, timeout: Duration) -> Option { - let reply = send_query(ns, qname, QTYPE_PTR, 0x4543, timeout)?; - parse_ptr_reply(&reply, 0x4543) + let id = rand_txid(); + let reply = send_query(ns, qname, QTYPE_PTR, id, timeout)?; + parse_ptr_reply(&reply, id) } /// Resolve `qname`'s first A record. Generic — the DNSBL module builds a @@ -216,8 +232,9 @@ pub fn a_lookup(qname: &str, timeout: Duration) -> Option { } } } - let val = send_query(&nameserver(), qname, QTYPE_A, 0x4544, timeout) - .and_then(|reply| parse_a_reply(&reply, 0x4544)); + let id = rand_txid(); + let val = send_query(&nameserver(), qname, QTYPE_A, id, timeout) + .and_then(|reply| parse_a_reply(&reply, id)); let ttl = if val.is_some() { A_TTL_HIT } else { A_TTL_MISS }; if let Ok(mut g) = cache.lock() { evict_if_full(&mut g);