-
Notifications
You must be signed in to change notification settings - Fork 4
fix(net): parse and store DNS response records in cache #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -617,18 +617,181 @@ impl DnsForwarder { | |||||||||||||||
| Err(NetError::Dns("all upstream servers failed".to_string())) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// Caches a DNS response. | ||||||||||||||||
| fn cache_response(&mut self, name: &str, qtype: DnsRecordType, _response: &[u8]) { | ||||||||||||||||
| // Simplified caching - just store the query info | ||||||||||||||||
| /// Caches a DNS response by parsing answer records from the raw bytes. | ||||||||||||||||
| fn cache_response(&mut self, name: &str, qtype: DnsRecordType, response: &[u8]) { | ||||||||||||||||
| let records = Self::parse_answer_records(response); | ||||||||||||||||
| if records.is_empty() { | ||||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Use the minimum TTL from the answer records, falling back to config. | ||||||||||||||||
| let min_ttl = records | ||||||||||||||||
| .iter() | ||||||||||||||||
| .map(|r| r.ttl) | ||||||||||||||||
| .min() | ||||||||||||||||
| .unwrap_or(self.config.cache_ttl.as_secs() as u32); | ||||||||||||||||
| let ttl = Duration::from_secs(u64::from(min_ttl)); | ||||||||||||||||
|
|
||||||||||||||||
| let key = (name.to_lowercase(), qtype); | ||||||||||||||||
| let entry = CacheEntry { | ||||||||||||||||
| records: Vec::new(), // Would parse from response in full implementation | ||||||||||||||||
| records, | ||||||||||||||||
| cached_at: Instant::now(), | ||||||||||||||||
| ttl: self.config.cache_ttl, | ||||||||||||||||
| ttl, | ||||||||||||||||
| }; | ||||||||||||||||
| self.cache.insert(key, entry); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+620
to
642
|
||||||||||||||||
|
|
||||||||||||||||
| /// Parses answer resource records from a raw DNS response. | ||||||||||||||||
| /// | ||||||||||||||||
| /// Skips the header (12 bytes) and question section, then reads each | ||||||||||||||||
| /// answer RR. Returns an empty vec on any parse failure -- the caller | ||||||||||||||||
| /// simply skips caching in that case. | ||||||||||||||||
| fn parse_answer_records(response: &[u8]) -> Vec<DnsRecord> { | ||||||||||||||||
| if response.len() < 12 { | ||||||||||||||||
| return Vec::new(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| let ancount = u16::from_be_bytes([response[6], response[7]]) as usize; | ||||||||||||||||
| if ancount == 0 { | ||||||||||||||||
| return Vec::new(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Skip past the question section. QDCOUNT is at bytes 4-5. | ||||||||||||||||
| let qdcount = u16::from_be_bytes([response[4], response[5]]) as usize; | ||||||||||||||||
| let mut offset = 12; | ||||||||||||||||
| for _ in 0..qdcount { | ||||||||||||||||
| if Self::skip_dns_name(response, &mut offset).is_err() { | ||||||||||||||||
| return Vec::new(); | ||||||||||||||||
| } | ||||||||||||||||
| offset += 4; // QTYPE + QCLASS | ||||||||||||||||
| if offset > response.len() { | ||||||||||||||||
| return Vec::new(); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Parse answer records. | ||||||||||||||||
| let mut records = Vec::with_capacity(ancount); | ||||||||||||||||
|
||||||||||||||||
| let mut records = Vec::with_capacity(ancount); | |
| let mut records = Vec::new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return empty cache parse result on RR parse failure
The answer parser currently breaks on the first RR parse failure and returns whatever records were parsed before that point, which causes partial responses to be cached. If an answer section contains a malformed/unsupported RR after valid ones, subsequent cache hits can return an incomplete record set (for example, only part of a multi-record answer), changing resolver behavior compared with the upstream response.
Useful? React with 👍 / 👎.
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring says this returns an empty vec on any parse failure, but the answer loop breaks on None and returns whatever records were parsed so far. That can lead to caching partial/incomplete answer sets. Either return Vec::new() when any RR fails to parse (to match the doc) or continue parsing by skipping just the problematic RR (after advancing offset).
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For non-A/AAAA record types you store DnsRdata::Raw(rdata_bytes.to_vec()) and later re-emit those bytes in cached responses. This is incorrect for RR types whose RDATA may contain compressed domain names (e.g., CNAME/PTR/MX/SRV), because compression pointers in the original upstream packet will refer to offsets that don’t exist in the reconstructed cached response. Either fully parse + re-encode those RDATA formats without compression pointers, or restrict caching to RR types with self-contained RDATA (A/AAAA/TXT, etc.).
| _ => DnsRdata::Raw(rdata_bytes.to_vec()), | |
| // For all other RR types, do not cache the RDATA. Their RDATA can contain | |
| // compressed domain names, and re-emitting the raw bytes from the upstream | |
| // packet would produce invalid compression pointers in reconstructed replies. | |
| _ => { | |
| return None; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This computes entry expiration from the minimum upstream TTL, but the cached
DnsRecord.ttlvalues are kept at their original values and later emitted unchanged by cached responses. In practice, a record cached with TTL 60 and served at 59 seconds old will still be returned with TTL 60, allowing downstream resolvers to keep stale data beyond the authoritative lifetime.Useful? React with 👍 / 👎.