Skip to main content

pharos_plugins/
log.rs

1//! Corefile access logging rules.
2
3use crate::log_format::{COMMON_LOG_FORMAT, Format};
4use pharos_corefile::{Cursor, Error};
5use pharos_proto::{Message, Name, NameRef, Opcode, Rcode, Type};
6use pharos_server::{Context, Next, Outcome, Plugin};
7use std::{cell::RefCell, time::Instant};
8
9thread_local! {
10    static BUFFER: RefCell<String> = RefCell::new(String::with_capacity(512));
11}
12
13#[derive(Debug)]
14struct Rule {
15    scope: Option<Name>,
16    format: Format,
17    classes: u8,
18}
19
20#[derive(Debug)]
21pub struct Log {
22    rules: Vec<Rule>,
23}
24
25pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Log, Error> {
26    let mut rules = Vec::new();
27    while cursor.next() {
28        let mut args = cursor.remaining_args();
29        let format = if args.len() > 1 && args.last().is_some_and(|arg| arg.contains('{')) {
30            args.pop().unwrap()
31        } else {
32            COMMON_LOG_FORMAT.to_owned()
33        };
34        if args.is_empty() {
35            args.push(".".to_owned());
36        }
37        let mut classes = 0;
38        while cursor.next_block() {
39            if cursor.val() != "class" {
40                return Err(cursor.error(format!("unknown property '{}'", cursor.val())));
41            }
42            let values = cursor.remaining_args();
43            if values.is_empty() {
44                return Err(cursor.arg_err());
45            }
46            for value in values {
47                classes |= match value.as_str() {
48                    "all" => 1,
49                    "success" => 2,
50                    "denial" => 4,
51                    "error" => 8,
52                    _ => return Err(cursor.error(format!("invalid Class: {value}"))),
53                };
54            }
55        }
56        if classes == 0 {
57            classes = 1;
58        }
59        for scope in args {
60            let fqdn = if scope.ends_with('.') {
61                scope
62            } else {
63                format!("{scope}.")
64            };
65            rules.push(Rule {
66                scope: fqdn.parse().ok(),
67                format: Format::compile(&format),
68                classes,
69            });
70        }
71    }
72    Ok(Log { rules })
73}
74
75impl Plugin for Log {
76    async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
77        let rule = self.rules.iter().find(|rule| {
78            let Some(scope) = &rule.scope else {
79                return false;
80            };
81            let labels = cx
82                .query
83                .questions()
84                .next()
85                .map_or_else(|| NameRef::parse(&[0], 0).unwrap(), |q| q.name);
86            let count = labels.labels().count();
87            let scope_count = scope.label_count();
88            // RFC 4343 ยง3: DNS label comparisons ignore ASCII letter case.
89            count >= scope_count
90                && labels
91                    .labels()
92                    .skip(count - scope_count)
93                    .zip(NameRef::parse(scope.as_wire(), 0).unwrap().labels())
94                    .all(|(a, b)| a.eq_ignore_ascii_case(b))
95        });
96        let Some(rule) = rule else {
97            return next.run(cx).await;
98        };
99        let start = Instant::now();
100        let outcome = next.run(cx).await;
101        let (kind, class, bit) = classify(cx, outcome);
102        if cx.metadata.enabled() {
103            cx.metadata.insert("log/type", kind);
104            cx.metadata.insert("log/class", class);
105        }
106        if rule.classes & (1 | bit) != 0 {
107            BUFFER.with_borrow_mut(|buffer| {
108                buffer.clear();
109                rule.format.format(buffer, cx, outcome, start.elapsed());
110                pharos_server::logging::log("INFO", "log", buffer);
111            });
112        }
113        outcome
114    }
115}
116
117fn classify(cx: &Context<'_>, outcome: Outcome) -> (&'static str, &'static str, u8) {
118    let message = (outcome == Outcome::Response)
119        .then(|| Message::parse(cx.response).ok())
120        .flatten();
121    let code = match outcome {
122        Outcome::Response => message.map(|m| m.rcode()),
123        Outcome::Error(_) => Some(Rcode::SERVFAIL),
124        Outcome::Unanswered(code)
125            if matches!(
126                code,
127                Rcode::SERVFAIL | Rcode::FORMERR | Rcode::REFUSED | Rcode::NOTIMP
128            ) =>
129        {
130            Some(code)
131        }
132        _ => None,
133    };
134    let Some(code) = code else {
135        return ("OTHERERROR", "error", 8);
136    };
137    let opcode = message.map_or(cx.query.opcode(), |m| m.opcode());
138    let question = match message {
139        Some(m) => m.questions().next(),
140        None => cx.query.questions().next(),
141    };
142    if opcode == Opcode::UPDATE {
143        return ("UPDATE", "error", 8);
144    }
145    if opcode == Opcode::NOTIFY
146        || question.is_some_and(|q| matches!(q.qtype, Type::AXFR | Type::IXFR))
147    {
148        return ("META", "error", 8);
149    }
150    if let Some(msg) = message {
151        if msg.opt().is_some_and(|opt| opt.dnssec_ok()) && expired_signature(msg) {
152            return ("OTHERERROR", "error", 8);
153        }
154        if msg.answer_count() > 0 {
155            if code == Rcode::NOERROR {
156                return ("NOERROR", "success", 2);
157            }
158            if code == Rcode::NXDOMAIN {
159                return ("OTHERERROR", "error", 8);
160            }
161        }
162        if code == Rcode::NOERROR && msg.authorities().any(|r| r.rtype == Type::SOA) {
163            return ("NODATA", "denial", 4);
164        }
165        if code == Rcode::NXDOMAIN {
166            return ("NXDOMAIN", "denial", 4);
167        }
168        if code == Rcode::NOERROR && msg.authorities().any(|r| r.rtype == Type::NS) {
169            return ("DELEGATION", "success", 2);
170        }
171    }
172    match code {
173        Rcode::NOERROR => ("NOERROR", "success", 2),
174        Rcode::NXDOMAIN => ("NXDOMAIN", "denial", 4),
175        Rcode::SERVFAIL | Rcode::NOTIMP => ("SERVERERROR", "error", 8),
176        _ => ("OTHERERROR", "error", 8),
177    }
178}
179
180fn expired_signature(message: Message<'_>) -> bool {
181    let now = std::time::SystemTime::now()
182        .duration_since(std::time::UNIX_EPOCH)
183        .map_or(0, |time| time.as_secs() as i64);
184    let adjust = |value: u32| {
185        let value = i64::from(value);
186        value + (value - now) / (1_i64 << 31) * (1_i64 << 31)
187    };
188    message
189        .answers()
190        .chain(message.authorities())
191        .chain(message.additionals())
192        .any(|record| {
193            if record.rtype != Type::RRSIG || record.rdata.len() < 16 {
194                return false;
195            }
196            let expiry = u32::from_be_bytes(record.rdata[8..12].try_into().unwrap());
197            let inception = u32::from_be_bytes(record.rdata[12..16].try_into().unwrap());
198            adjust(inception) > now || now > adjust(expiry)
199        })
200}