Skip to main content

pharos_plugins/cache/
mod.rs

1//! Cache configuration and DNS Response storage.
2
3mod setup;
4mod store;
5
6use std::{
7    fmt::Write,
8    net::SocketAddr,
9    sync::{Arc, Mutex, OnceLock, Weak},
10    time::{Instant, SystemTime},
11};
12
13use pharos_proto::{
14    Message, MessageBuilder, MessageMut, Name, NameRef, Opcode, Rcode, Rdata, ResponseType, Type,
15};
16use pharos_server::{Context, Next, Outcome, Plugin};
17
18/// Configuration for the cache Plugin.
19pub struct Cache {
20    config: setup::Config,
21    success: store::Store,
22    denial: store::Store,
23    metrics: Vec<(SocketAddr, pharos_server::metrics::CacheMetrics)>,
24    chain: OnceLock<Weak<crate::Chain>>,
25    writes: Mutex<()>,
26}
27
28struct RefreshGuard {
29    chain: Arc<crate::Chain>,
30    key: u64,
31    stored: Instant,
32    denial: bool,
33    recheck: std::time::Duration,
34    success: bool,
35}
36
37struct LocalRefreshGuard<'a> {
38    store: &'a store::Store,
39    key: u64,
40    stored: Instant,
41    recheck: std::time::Duration,
42    success: bool,
43}
44
45struct QueryGuard<'a, 'b> {
46    context: &'a mut Context<'b>,
47    query: Message<'b>,
48}
49
50impl Drop for QueryGuard<'_, '_> {
51    fn drop(&mut self) {
52        self.context.query = self.query;
53    }
54}
55
56impl Drop for LocalRefreshGuard<'_> {
57    fn drop(&mut self) {
58        self.store
59            .finish_refresh(self.key, self.stored, self.recheck, self.success);
60    }
61}
62
63impl Drop for RefreshGuard {
64    fn drop(&mut self) {
65        if let Some(cache) = &self.chain.cache {
66            let store = if self.denial {
67                &cache.denial
68            } else {
69                &cache.success
70            };
71            store.finish_refresh(self.key, self.stored, self.recheck, self.success);
72        }
73    }
74}
75
76impl std::fmt::Debug for Cache {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("Cache")
79            .field("config", &self.config)
80            .finish_non_exhaustive()
81    }
82}
83
84impl Cache {
85    pub(crate) fn parse(
86        cursor: &mut pharos_corefile::Cursor<'_>,
87        zones: &[String],
88    ) -> Result<Self, pharos_corefile::Error> {
89        let zones: Vec<&str> = zones.iter().map(String::as_str).collect();
90        let config = setup::parse(cursor, &zones)?;
91        Ok(Self {
92            success: store::Store::new(config.success_capacity),
93            denial: store::Store::new(config.denial_capacity),
94            metrics: Vec::new(),
95            chain: OnceLock::new(),
96            writes: Mutex::new(()),
97            config,
98        })
99    }
100
101    pub(crate) fn bind(&self, chain: &Arc<crate::Chain>) {
102        let _ = self.chain.set(Arc::downgrade(chain));
103    }
104
105    pub(crate) fn set_metrics(
106        &mut self,
107        registry: &pharos_server::metrics::Registry,
108        listeners: &[pharos_server::ListenerKey],
109    ) {
110        let zones = self
111            .config
112            .zones
113            .iter()
114            .map(Name::canonical)
115            .collect::<Vec<_>>()
116            .join(",");
117        for listener in listeners {
118            let address = listener.address;
119            let server = if address.ip().is_unspecified() {
120                format!("dns://:{}", address.port())
121            } else {
122                format!("dns://{address}")
123            };
124            self.metrics
125                .push((address, registry.cache(&server, &zones, "")));
126        }
127    }
128}
129
130impl Plugin for Cache {
131    async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
132        let query = cx.query;
133        let Some(question) = query.questions().next() else {
134            return next.run(cx).await;
135        };
136        if query.opcode() != Opcode::QUERY
137            || query.additionals().any(|record| record.rtype == Type::TSIG)
138            || question.name.wire_len()
139                != question
140                    .name
141                    .labels()
142                    .map(|label| label.len() + 1)
143                    .sum::<usize>()
144                    + 1
145            || !self
146                .config
147                .zones
148                .iter()
149                .any(|zone| contains(question.name, zone))
150        {
151            return next.run(cx).await;
152        }
153        let metrics = self
154            .metrics
155            .iter()
156            .find_map(|(address, metrics)| (*address == cx.local).then_some(metrics));
157        if let Some(metrics) = metrics {
158            metrics.request();
159        }
160        let key = key(&query);
161        let can_grow = cx.protocol == pharos_server::Protocol::Tcp;
162        let positive_first = self.config.prefer_positive && !self.config.stale.is_zero();
163        let positive = positive_first
164            .then(|| {
165                self.success
166                    .serve(key, &query, cx.response, &self.config, true, can_grow)
167            })
168            .flatten();
169        let hit = if let Some(hit) = positive {
170            Some((false, hit))
171        } else if let Some(denial) =
172            self.denial
173                .serve(key, &query, cx.response, &self.config, false, can_grow)
174        {
175            if cx.response[3] & 15 == 2 {
176                if let Some(positive) =
177                    self.success
178                        .serve(key, &query, cx.response, &self.config, false, can_grow)
179                {
180                    if let Some(stored) = denial.refresh {
181                        self.denial
182                            .finish_refresh(key, stored, std::time::Duration::ZERO, true);
183                    }
184                    Some((false, positive))
185                } else {
186                    Some((true, denial))
187                }
188            } else {
189                Some((true, denial))
190            }
191        } else {
192            self.success
193                .serve(key, &query, cx.response, &self.config, false, can_grow)
194                .map(|hit| (false, hit))
195        };
196        if let Some((denial, hit)) = hit {
197            if let Some(metrics) = metrics {
198                metrics.hit(denial);
199            }
200            if let Some(stored) = hit.refresh {
201                let store = if denial { &self.denial } else { &self.success };
202                if !query.can_set_ad_in_wire() {
203                    store.finish_refresh(key, stored, self.config.stale_recheck, false);
204                } else if let Some(chain) = self.chain.get().and_then(Weak::upgrade) {
205                    if (!hit.stale || !self.config.verify)
206                        && let Some(metrics) = metrics
207                    {
208                        metrics.prefetch();
209                    }
210                    let task = self.spawn_refresh(chain, cx, key, denial, hit);
211                    if hit.stale && self.config.verify {
212                        let result = if self.config.verify_timeout.is_zero() {
213                            task.await.ok()
214                        } else {
215                            tokio::time::timeout(self.config.verify_timeout, task)
216                                .await
217                                .ok()
218                                .and_then(Result::ok)
219                        };
220                        if let Some(Some(mut response)) = result {
221                            // RFC 6840 §5.8: only signal AD when the query requested AD or DO.
222                            if !query.ad() && !query.opt().is_some_and(|opt| opt.dnssec_ok()) {
223                                response[3] &= !0x20;
224                            }
225                            *cx.response = response;
226                            return Outcome::Response;
227                        }
228                    }
229                } else if hit.stale && self.config.verify {
230                    let mut guard = LocalRefreshGuard {
231                        store,
232                        key,
233                        stored,
234                        recheck: self.config.stale_recheck,
235                        success: false,
236                    };
237                    let stale = cx.response.clone();
238                    let outcome = {
239                        cx.query = query.with_ad();
240                        let guard = QueryGuard {
241                            context: &mut *cx,
242                            query,
243                        };
244                        next.run(&mut *guard.context).await
245                    };
246                    let valid =
247                        outcome == Outcome::Response && self.accept_refresh(&query, cx.response);
248                    if valid
249                        || (self.config.prefer_positive
250                            && outcome == Outcome::Response
251                            && Self::matches_refresh(&query, cx.response))
252                    {
253                        self.process_response(cx, outcome, Some((denial, hit.generation)));
254                    }
255                    if !valid {
256                        cx.response.clear();
257                        cx.response.extend_from_slice(&stale);
258                    }
259                    guard.success = valid;
260                    if valid {
261                        return Outcome::Response;
262                    }
263                } else {
264                    store.finish_refresh(key, stored, self.config.stale_recheck, false);
265                }
266            }
267            if hit.stale
268                && let Some(metrics) = metrics
269            {
270                metrics.served_stale();
271            }
272            return Outcome::Response;
273        }
274        if let Some(metrics) = metrics {
275            metrics.miss();
276        }
277        if !query.can_set_ad_in_wire() {
278            return next.run(cx).await;
279        }
280        let outcome = {
281            cx.query = query.with_ad();
282            let guard = QueryGuard {
283                context: &mut *cx,
284                query,
285            };
286            next.run(&mut *guard.context).await
287        };
288        self.process_response(cx, outcome, None)
289    }
290}
291
292impl Cache {
293    fn matches_refresh(query: &Message<'_>, wire: &[u8]) -> bool {
294        let Ok(response) = Message::parse(wire) else {
295            return false;
296        };
297        let (Some(expected), Some(actual)) =
298            (query.questions().next(), response.questions().next())
299        else {
300            return false;
301        };
302        if response.tc()
303            || !response.qr()
304            || response.question_count() != 1
305            || response
306                .additionals()
307                .any(|record| record.rtype == Type::TSIG)
308            || expected.name != actual.name
309            || expected.qtype != actual.qtype
310            || expected.qclass != actual.qclass
311        {
312            return false;
313        }
314        true
315    }
316
317    fn accept_refresh(&self, query: &Message<'_>, wire: &[u8]) -> bool {
318        if !Self::matches_refresh(query, wire) {
319            return false;
320        }
321        let Ok(response) = Message::parse(wire) else {
322            return false;
323        };
324        if !matches!(response.rcode(), Rcode::NOERROR | Rcode::NXDOMAIN) {
325            return false;
326        }
327        let now = SystemTime::now()
328            .duration_since(SystemTime::UNIX_EPOCH)
329            .map_or(0, |duration| duration.as_secs().min(i64::MAX as u64) as i64);
330        let kind = pharos_proto::typify_response(Some(&response), now);
331        if !matches!(
332            kind,
333            ResponseType::Positive
334                | ResponseType::Referral
335                | ResponseType::NoData
336                | ResponseType::NxDomain
337        ) {
338            return false;
339        }
340        !self.config.prefer_positive
341            || (response.rcode() == Rcode::NOERROR && answers_question(&response))
342    }
343
344    fn spawn_refresh(
345        &self,
346        chain: Arc<crate::Chain>,
347        cx: &Context<'_>,
348        key: u64,
349        denial: bool,
350        hit: store::Hit,
351    ) -> tokio::task::JoinHandle<Option<Vec<u8>>> {
352        let stored = hit.refresh.expect("refresh requested");
353        let generation = hit.generation;
354        let stale = hit.stale;
355        let safe_query = cx.query.can_set_ad_in_wire();
356        let mut wire = Vec::with_capacity(cx.query.as_wire().len());
357        cx.query.copy_wire(&mut wire);
358        wire[3] |= 0x20;
359        let domain = cx.domain.clone();
360        let synthetic = !stale || !self.config.verify;
361        let client = if synthetic {
362            ([0, 0, 0, 0], 0).into()
363        } else {
364            cx.client
365        };
366        let local = cx.local;
367        let transport = cx.transport;
368        let protocol = if synthetic {
369            pharos_server::Protocol::Tcp
370        } else {
371            cx.protocol
372        };
373        let max_response_size = if synthetic {
374            65535
375        } else {
376            cx.max_response_size
377        };
378        let self_lookup = cx.self_lookup_handle();
379        let guard = RefreshGuard {
380            chain,
381            key,
382            stored,
383            denial,
384            recheck: if stale {
385                self.config.stale_recheck
386            } else {
387                std::time::Duration::ZERO
388            },
389            success: false,
390        };
391        tokio::spawn(async move {
392            let mut guard = guard;
393            if !safe_query {
394                return None;
395            }
396            let Some(cache) = &guard.chain.cache else {
397                return None;
398            };
399            let Ok(query) = Message::parse(&wire) else {
400                return None;
401            };
402            let mut response = Vec::with_capacity(65535);
403            let mut slots = [None; 16];
404            let mut context = Context {
405                query,
406                client,
407                local,
408                transport,
409                protocol,
410                domain: &domain,
411                metadata: pharos_server::Metadata::with_self_lookup(
412                    &mut slots,
413                    self_lookup.as_ref(),
414                ),
415                response: &mut response,
416                max_response_size,
417            };
418            let outcome = guard.chain.cache_refresh_next(&mut context).await;
419            let valid = outcome == Outcome::Response
420                && cache.accept_refresh(&context.query, context.response);
421            if valid
422                || (!stale && outcome == Outcome::Response)
423                || (stale
424                    && cache.config.prefer_positive
425                    && outcome == Outcome::Response
426                    && Self::matches_refresh(&context.query, context.response))
427            {
428                cache.process_response(&mut context, outcome, Some((denial, generation)));
429                if valid
430                    && denial
431                    && Message::parse(context.response).is_ok_and(|message| {
432                        message.rcode() == Rcode::NOERROR
433                            && message.answer_count() > 0
434                            && answers_question(&message)
435                    })
436                {
437                    let _write = cache.writes.lock().unwrap();
438                    if cache.denial.remove_if_stored(key, stored)
439                        && let Some(metrics) = cache
440                            .metrics
441                            .iter()
442                            .find_map(|(address, metrics)| (*address == local).then_some(metrics))
443                    {
444                        metrics.entries(true, cache.denial.len());
445                    }
446                }
447            }
448            guard.success = valid;
449            valid.then_some(response)
450        })
451    }
452
453    fn insert_response(
454        &self,
455        key: u64,
456        response: &Message<'_>,
457        ttl: u32,
458        denial: bool,
459        expected: Option<(bool, Instant)>,
460    ) -> Option<bool> {
461        let _write = self.writes.lock().unwrap();
462        if let Some((old_denial, stored)) = expected {
463            let old = if old_denial {
464                &self.denial
465            } else {
466                &self.success
467            };
468            if !old.is_current(key, stored) {
469                return None;
470            }
471        }
472        if denial {
473            self.denial
474                .insert(key, response, ttl, false, expected.is_some())
475        } else {
476            self.success.insert(
477                key,
478                response,
479                ttl,
480                self.config.prefer_positive,
481                expected.is_some(),
482            )
483        }
484    }
485
486    fn process_response(
487        &self,
488        cx: &mut Context<'_>,
489        outcome: Outcome,
490        expected: Option<(bool, Instant)>,
491    ) -> Outcome {
492        let query = cx.query;
493        let Some(question) = query.questions().next() else {
494            return outcome;
495        };
496        let key = key(&query);
497        let metrics = self
498            .metrics
499            .iter()
500            .find_map(|(address, metrics)| (*address == cx.local).then_some(metrics));
501        if outcome != Outcome::Response {
502            if matches!(
503                outcome,
504                Outcome::Unanswered(Rcode::SERVFAIL) | Outcome::Error(_)
505            ) && !self.config.fail_ttl.is_zero()
506                && !self
507                    .config
508                    .disable_denial
509                    .iter()
510                    .any(|zone| contains(question.name, zone))
511            {
512                let ttl = self.config.fail_ttl.as_secs() as u32;
513                if ttl > 0
514                    && let Ok(mut builder) = MessageBuilder::new(
515                        cx.response,
516                        65535,
517                        query.id(),
518                        0x8002 | (query.flags() & 0x0110),
519                    )
520                    && matches!(
521                        builder.question(question.name, question.qtype, question.qclass),
522                        Ok(true)
523                    )
524                {
525                    let _ = builder.finish();
526                    if let Ok(response) = Message::parse(cx.response)
527                        && let Some(evicted) =
528                            self.insert_response(key, &response, ttl, true, expected)
529                        && let Some(metrics) = metrics
530                    {
531                        if evicted {
532                            metrics.evict(true);
533                        }
534                        metrics.entries(false, self.success.len());
535                        metrics.entries(true, self.denial.len());
536                    }
537                }
538            }
539            return outcome;
540        }
541        let Ok(response) = Message::parse(cx.response) else {
542            return outcome;
543        };
544        if response
545            .additionals()
546            .any(|record| record.rtype == Type::TSIG)
547        {
548            return outcome;
549        }
550        let now = SystemTime::now()
551            .duration_since(SystemTime::UNIX_EPOCH)
552            .map_or(0, |duration| duration.as_secs().min(i64::MAX as u64) as i64);
553        let mut response_type = pharos_proto::typify_response(Some(&response), now);
554        let has_soa = response.authorities().any(|rr| rr.rtype == Type::SOA);
555        let incomplete = response_type == ResponseType::Positive
556            && response.answer_count() > 0
557            && !answers_question(&response);
558        // RFC 2308 §2.2: NODATA can include CNAMEs and an SOA in the authority section.
559        if incomplete && has_soa {
560            response_type = ResponseType::NoData;
561        }
562        let denial = matches!(
563            response_type,
564            ResponseType::NxDomain | ResponseType::NoData | ResponseType::ServerError
565        );
566        let (minimum, maximum) = if denial {
567            (self.config.denial_min_ttl, self.config.denial_max_ttl)
568        } else {
569            (self.config.success_min_ttl, self.config.success_max_ttl)
570        };
571        let minimum = minimum.as_secs().min(u64::from(u32::MAX)) as u32;
572        let maximum = maximum.as_secs().min(u64::from(u32::MAX)) as u32;
573        let mut ttl = maximum;
574        let mut found = false;
575        for record in response
576            .answers()
577            .chain(response.authorities())
578            .chain(response.additionals())
579        {
580            if record.rtype == Type::OPT {
581                continue;
582            }
583            found = true;
584            ttl = ttl.min(record.ttl);
585            // RFC 2308 §5: the negative cache TTL is bounded by the SOA MINIMUM field.
586            if denial && let Ok(Rdata::Soa { minimum, .. }) = record.data() {
587                ttl = ttl.min(minimum);
588            }
589        }
590        if !found
591            || !matches!(
592                response_type,
593                ResponseType::Positive | ResponseType::NxDomain | ResponseType::NoData
594            )
595        {
596            ttl = 5;
597        }
598        ttl = ttl.max(minimum).min(maximum);
599        if response_type == ResponseType::ServerError {
600            ttl = self.config.fail_ttl.as_secs() as u32;
601        }
602        let matches = response.questions().next().is_some_and(|q| {
603            q.name == question.name && q.qtype == question.qtype && q.qclass == question.qclass
604        });
605        let eligible = !response.tc()
606            && ttl > 0
607            && (!incomplete || has_soa)
608            && (matches!(
609                response_type,
610                ResponseType::Positive
611                    | ResponseType::Referral
612                    | ResponseType::NoData
613                    | ResponseType::ServerError
614            ) || (response_type == ResponseType::NxDomain && has_soa));
615        if eligible
616            && !matches
617            && let Some(metrics) = metrics
618        {
619            metrics.drop_response();
620        }
621        let cacheable = eligible && matches;
622        let disabled = if denial {
623            &self.config.disable_denial
624        } else {
625            &self.config.disable_success
626        };
627        let cacheable = cacheable && !disabled.iter().any(|zone| contains(question.name, zone));
628        if let Ok(mut mutable) = MessageMut::parse(cx.response) {
629            mutable.rewrite_ttls(|_| ttl);
630        }
631        if cacheable && let Ok(response) = Message::parse(cx.response) {
632            let inserted = self.insert_response(key, &response, ttl, denial, expected);
633            if let Some(evicted) = inserted
634                && let Some(metrics) = metrics
635            {
636                if evicted {
637                    metrics.evict(denial);
638                }
639                metrics.entries(false, self.success.len());
640                metrics.entries(true, self.denial.len());
641            }
642        }
643        // RFC 6840 §5.8: only signal AD when the query requested AD or DO.
644        if !query.ad() && !query.opt().is_some_and(|opt| opt.dnssec_ok()) {
645            cx.response[3] &= !0x20;
646        }
647        strip_trailing_opt(cx.response);
648        outcome
649    }
650}
651
652fn strip_trailing_opt(wire: &mut Vec<u8>) {
653    let Ok(message) = Message::parse(wire) else {
654        return;
655    };
656    if message.rcode().0 > 15 {
657        return;
658    }
659    let Some(opt) = message
660        .additionals()
661        .last()
662        .filter(|record| record.rtype == Type::OPT)
663    else {
664        return;
665    };
666    let start = opt.ttl_offset - 4 - opt.name.wire_len();
667    let count = message.additional_count() - 1;
668    wire.truncate(start);
669    wire[10..12].copy_from_slice(&count.to_be_bytes());
670}
671
672fn answers_question(message: &Message<'_>) -> bool {
673    let Some(question) = message.questions().next() else {
674        return false;
675    };
676    let matches_class =
677        |class| question.qclass == pharos_proto::Class::ANY || question.qclass == class;
678    if question.qtype == Type::ANY {
679        return message
680            .answers()
681            .any(|rr| rr.name == question.name && matches_class(rr.class));
682    }
683    // RFC 1034 §3.6.2: follow CNAME chains and detect loops.
684    let mut name = question.name.to_name();
685    let mut visited = Vec::new();
686    loop {
687        if visited.contains(&name) {
688            return false;
689        }
690        visited.push(name.clone());
691        let mut target: Option<Name> = None;
692        for rr in message.answers() {
693            if rr.rtype != Type::CNAME || rr.name != name || !matches_class(rr.class) {
694                continue;
695            }
696            let Ok(Rdata::Cname(alias)) = rr.data() else {
697                return false;
698            };
699            if target.as_ref().is_some_and(|existing| alias != *existing) {
700                return false;
701            }
702            target = Some(alias.to_name());
703        }
704        if question.qtype == Type::CNAME {
705            return target.is_some();
706        }
707        match target {
708            Some(alias) => name = alias,
709            None => {
710                return message.answers().any(|rr| {
711                    rr.rtype == question.qtype && rr.name == name && matches_class(rr.class)
712                });
713            }
714        }
715    }
716}
717
718fn contains(name: NameRef<'_>, domain: &Name) -> bool {
719    let domain = NameRef::parse(domain.as_wire(), 0).expect("configured Domain");
720    let count = name.labels().count();
721    let suffix = domain.labels().count();
722    count >= suffix
723        && name
724            .labels()
725            .skip(count - suffix)
726            .zip(domain.labels())
727            .all(|(a, b)| a.eq_ignore_ascii_case(b))
728}
729
730struct KeyHash(u64);
731impl KeyHash {
732    fn bytes(&mut self, bytes: &[u8]) {
733        for byte in bytes {
734            self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x100000001b3);
735        }
736    }
737}
738impl std::fmt::Write for KeyHash {
739    fn write_str(&mut self, value: &str) -> std::fmt::Result {
740        for byte in value.bytes() {
741            self.bytes(&[byte.to_ascii_lowercase()]);
742        }
743        Ok(())
744    }
745}
746
747fn key(query: &Message<'_>) -> u64 {
748    let question = query.questions().next().expect("Query has a Question");
749    let mut hash = KeyHash(0xcbf29ce484222325);
750    hash.bytes(&[if query.opt().is_some_and(|opt| opt.dnssec_ok()) {
751        b'1'
752    } else {
753        b'0'
754    }]);
755    hash.bytes(&[if query.cd() { b'1' } else { b'0' }]);
756    hash.bytes(&question.qtype.0.to_be_bytes());
757    hash.bytes(&question.qclass.0.to_be_bytes());
758    write!(hash, "{}", question.name).expect("hash writer never fails");
759    hash.0
760}