1use 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 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}