Skip to main content

pharos_server/
server.rs

1use std::{future::Future, net::SocketAddr, sync::Arc, time::Instant};
2
3use crate::{
4    Action, Admission, Context, Error, Handler, ListenerKey, Metadata, Outcome, Protocol, Router,
5    admit, finish_outcome,
6    metrics::{BlockMetrics, Registry},
7    response_limit,
8};
9use pharos_proto::Rcode;
10
11/// Statically dispatched Plugin Chain supplied by the Plugin registry.
12pub trait Chain: Send + Sync + 'static {
13    fn serve(&self, context: &mut Context<'_>) -> impl Future<Output = Outcome> + Send;
14    fn is_empty(&self) -> bool;
15    fn parallel_query(
16        &self,
17        _query: &pharos_proto::Message<'_>,
18    ) -> Option<tokio::sync::watch::Receiver<bool>> {
19        None
20    }
21    fn visit_enabled(&self, _visit: &mut dyn FnMut(&'static str)) {}
22    fn metrics_enabled(&self) -> bool {
23        true
24    }
25    /// Whether contained Plugin panics should include a stacktrace.
26    fn stacktrace(&self) -> bool {
27        false
28    }
29}
30
31impl<C: Chain> Chain for Arc<C> {
32    fn serve(&self, context: &mut Context<'_>) -> impl Future<Output = Outcome> + Send {
33        self.as_ref().serve(context)
34    }
35    fn is_empty(&self) -> bool {
36        self.as_ref().is_empty()
37    }
38    fn parallel_query(
39        &self,
40        query: &pharos_proto::Message<'_>,
41    ) -> Option<tokio::sync::watch::Receiver<bool>> {
42        self.as_ref().parallel_query(query)
43    }
44    fn visit_enabled(&self, visit: &mut dyn FnMut(&'static str)) {
45        self.as_ref().visit_enabled(visit);
46    }
47    fn metrics_enabled(&self) -> bool {
48        self.as_ref().metrics_enabled()
49    }
50    fn stacktrace(&self) -> bool {
51        self.as_ref().stacktrace()
52    }
53}
54
55/// One Listener's routing, admission and Plugin Chain execution.
56pub struct Server<C> {
57    pub router: Router<C>,
58    pub key: ListenerKey,
59    pub allow_non_in: bool,
60    pub debug: bool,
61    pub udp_workers: usize,
62    pub logger: fn(Error),
63    metrics: Registry,
64    dropped: Option<BlockMetrics>,
65}
66
67impl<C: Chain> Server<C> {
68    pub fn new(router: Router<C>, key: ListenerKey) -> Self {
69        let metrics = Registry::default();
70        Self::with_registry(router, key, metrics)
71    }
72
73    pub fn with_registry(mut router: Router<C>, key: ListenerKey, metrics: Registry) -> Self {
74        let scheme = match key.transport {
75            crate::Transport::Dns => "dns",
76            crate::Transport::Tls => "tls",
77            crate::Transport::Quic => "quic",
78            crate::Transport::Grpc => "grpc",
79            crate::Transport::Https => "https",
80            crate::Transport::Https3 => "https3",
81        };
82        let address = if key.address.ip().is_unspecified() {
83            format!(":{}", key.address.port())
84        } else {
85            key.address.to_string()
86        };
87        let server = format!("{scheme}://{address}");
88        router.prepare_metrics(&key, &metrics, &server);
89        let dropped = router
90            .metrics_enabled(&key)
91            .then(|| metrics.block(&server, "dropped", ""));
92        Self {
93            router,
94            key,
95            allow_non_in: false,
96            debug: false,
97            udp_workers: crate::quota::worker_count(),
98            logger: log_error,
99            metrics,
100            dropped,
101        }
102    }
103
104    pub fn metrics(&self) -> &Registry {
105        &self.metrics
106    }
107}
108
109fn log_error(error: Error) {
110    log_plugin_error("server", error);
111}
112
113fn log_plugin_error(plugin: &str, error: Error) {
114    crate::logging::log("ERROR", plugin, &format!("Plugin error: {error}"));
115}
116
117impl<C: Chain> Handler for Server<C> {
118    fn udp_workers(&self) -> usize {
119        self.udp_workers
120    }
121    fn parallel_query(
122        &self,
123        wire: &[u8],
124        peer: SocketAddr,
125    ) -> Option<tokio::sync::watch::Receiver<bool>> {
126        let Ok(query) = pharos_proto::Message::parse(wire) else {
127            return None;
128        };
129        // shortcut: only loop probes require parallel dispatch; extend for other self-queries.
130        if query
131            .questions()
132            .next()
133            .is_none_or(|question| question.qtype != pharos_proto::Type::HINFO)
134        {
135            return None;
136        }
137        self.router
138            .route(&self.key, query, peer, Protocol::Udp)
139            .and_then(|block| block.chain())
140            .and_then(|chain| chain.parallel_query(&query))
141    }
142    async fn handle(
143        &self,
144        wire: &[u8],
145        peer: SocketAddr,
146        protocol: Protocol,
147        response: &mut Vec<u8>,
148    ) -> Action {
149        self.handle_with_depth(wire, peer, protocol, response, 0, self)
150            .await
151    }
152
153    async fn handle_shared(
154        self: &Arc<Self>,
155        wire: &[u8],
156        peer: SocketAddr,
157        protocol: Protocol,
158        response: &mut Vec<u8>,
159    ) -> Action {
160        self.handle_with_depth(wire, peer, protocol, response, 0, self)
161            .await
162    }
163}
164
165impl<C: Chain> crate::SelfLookupServer for Server<C> {
166    fn lookup<'a>(
167        &'a self,
168        wire: &'a [u8],
169        peer: SocketAddr,
170        protocol: Protocol,
171        depth: u8,
172        response: &'a mut Vec<u8>,
173    ) -> std::pin::Pin<Box<dyn Future<Output = Action> + Send + 'a>> {
174        // shortcut: static re-entry boxes a future, remove if recursive dispatch no longer needs it.
175        Box::pin(self.handle_with_depth(wire, peer, protocol, response, depth, self))
176    }
177}
178
179impl<C: Chain> crate::SelfLookupServer for Arc<Server<C>> {
180    fn lookup<'a>(
181        &'a self,
182        wire: &'a [u8],
183        peer: SocketAddr,
184        protocol: Protocol,
185        depth: u8,
186        response: &'a mut Vec<u8>,
187    ) -> std::pin::Pin<Box<dyn Future<Output = Action> + Send + 'a>> {
188        Box::pin(self.handle_with_depth(wire, peer, protocol, response, depth, self))
189    }
190
191    fn owned(&self) -> Option<Arc<dyn crate::OwnedSelfLookupServer>> {
192        Some(Arc::clone(self) as Arc<dyn crate::OwnedSelfLookupServer>)
193    }
194}
195
196impl<C: Chain> crate::OwnedSelfLookupServer for Server<C> {
197    fn lookup<'a>(
198        self: Arc<Self>,
199        wire: &'a [u8],
200        peer: SocketAddr,
201        protocol: Protocol,
202        depth: u8,
203        response: &'a mut Vec<u8>,
204    ) -> std::pin::Pin<Box<dyn Future<Output = Action> + Send + 'a>> {
205        Box::pin(async move {
206            self.handle_with_depth(wire, peer, protocol, response, depth, &self)
207                .await
208        })
209    }
210}
211
212impl<C: Chain> Server<C> {
213    async fn handle_with_depth(
214        &self,
215        wire: &[u8],
216        peer: SocketAddr,
217        protocol: Protocol,
218        response: &mut Vec<u8>,
219        depth: u8,
220        lookup_server: &dyn crate::SelfLookupServer,
221    ) -> Action {
222        let start = Instant::now();
223        let mut query = match admit(wire, self.allow_non_in, response) {
224            Admission::Query(query) => query,
225            Admission::Respond => {
226                if let Ok(query) = pharos_proto::Message::parse(wire) {
227                    let rcode = pharos_proto::Message::parse(response)
228                        .map_or(0, |message| message.rcode().0);
229                    if rcode != Rcode::BADVERS.0
230                        && let Some(dropped) = &self.dropped
231                    {
232                        dropped.record(
233                            query.questions().next().map_or(0, |q| q.qtype.0),
234                            protocol == Protocol::Tcp,
235                            peer_is_ipv6(peer),
236                            query.opt().is_some_and(|opt| opt.dnssec_ok()),
237                            wire.len(),
238                            response.len(),
239                            rcode,
240                            start.elapsed().as_secs_f64(),
241                        );
242                    }
243                }
244                return Action::Respond;
245            }
246            Admission::Drop => return Action::Drop,
247        };
248        let block = self.router.route(&self.key, query, peer, protocol);
249        let chain_start = Instant::now();
250        let mut producer = None;
251        let mut error_plugin = None;
252        let mut error_logged = false;
253        let outcome = if let Some(block) = block {
254            if let Some(chain) = block.chain() {
255                let mut slots = [None; 18];
256                slots[0] = Some((crate::metrics::WRITER_METADATA, ""));
257                slots[1] = Some((crate::logging::PLUGIN_METADATA, ""));
258                slots[2] = Some((crate::logging::ERROR_LOGGED_METADATA, ""));
259                let mut metadata = Metadata::new(&mut slots);
260                metadata.self_lookup = Some(crate::SelfLookupState {
261                    server: lookup_server,
262                    depth,
263                });
264                let mut context = Context {
265                    query,
266                    client: peer,
267                    local: self.key.address,
268                    transport: self.key.transport,
269                    protocol,
270                    domain: block.domain(),
271                    metadata,
272                    response,
273                    max_response_size: response_limit(query, protocol),
274                };
275                let outcome = crate::panic::contain_panic_with_stacktrace(
276                    chain.serve(&mut context),
277                    self.debug,
278                    chain.stacktrace(),
279                )
280                .await;
281                if let Some(opt) = context.query.opt() {
282                    query = query.with_udp_payload_limit(opt.udp_payload_size());
283                }
284                producer = context
285                    .metadata
286                    .get(crate::metrics::WRITER_METADATA)
287                    .and_then(|name| block.plugin_metrics(name));
288                error_plugin = context
289                    .metadata
290                    .get(crate::logging::PLUGIN_METADATA)
291                    .and_then(|name| block.plugin_name(name));
292                error_logged =
293                    context.metadata.get(crate::logging::ERROR_LOGGED_METADATA) == Some("true");
294                outcome
295            } else {
296                Outcome::Unanswered(Rcode::REFUSED)
297            }
298        } else {
299            Outcome::Unanswered(Rcode::REFUSED)
300        };
301        let recorded_size = if outcome == Outcome::Response {
302            response.len()
303        } else {
304            0
305        };
306        let recorded_code = match outcome {
307            Outcome::Response => {
308                pharos_proto::Message::parse(response).map_or(0, |message| message.rcode().0)
309            }
310            Outcome::Unanswered(code) => code.0,
311            Outcome::Error(_) => Rcode::SERVFAIL.0,
312            Outcome::Drop | Outcome::Stream(_) => 0,
313        };
314        let duration = chain_start.elapsed().as_secs_f64();
315        let default_logger = std::ptr::fn_addr_eq(self.logger, log_error as fn(Error));
316        let mut first_error = true;
317        let action = finish_outcome(query, protocol, outcome, response, |error| {
318            let suppress = first_error
319                && (error_logged || default_logger)
320                && matches!(outcome, Outcome::Error(e) if e != Error::PluginPanic);
321            first_error = false;
322            if suppress {
323                return;
324            }
325            if let Some(plugin) = error_plugin.filter(|_| default_logger) {
326                log_plugin_error(plugin, error);
327            } else {
328                (self.logger)(error);
329            }
330        });
331        let code = if (block.is_none() || outcome == Outcome::Error(Error::PluginPanic))
332            && action == Action::Respond
333        {
334            pharos_proto::Message::parse(response).map_or(0, |message| message.rcode().0)
335        } else {
336            recorded_code
337        };
338        // shortcut: compressed Query lengths differ until decoded Message lengths are available.
339        let qtype = query.questions().next().map_or(0, |q| q.qtype.0);
340        let dnssec = query.opt().is_some_and(|opt| opt.dnssec_ok());
341        let response_len = if (block.is_none() || outcome == Outcome::Error(Error::PluginPanic))
342            && action == Action::Respond
343        {
344            response.len()
345        } else {
346            recorded_size
347        };
348        if let Some(producer) = producer {
349            producer.record(
350                qtype,
351                protocol == Protocol::Tcp,
352                peer_is_ipv6(peer),
353                dnssec,
354                wire.len(),
355                response_len,
356                code,
357                duration,
358            );
359        } else {
360            let metrics = if outcome == Outcome::Error(Error::PluginPanic) {
361                self.dropped.as_ref()
362            } else {
363                block.map_or(self.dropped.as_ref(), |block| block.metrics())
364            };
365            if let Some(metrics) = metrics {
366                metrics.record(
367                    qtype,
368                    protocol == Protocol::Tcp,
369                    peer_is_ipv6(peer),
370                    dnssec,
371                    wire.len(),
372                    response_len,
373                    code,
374                    duration,
375                );
376            }
377        }
378        action
379    }
380}
381
382fn peer_is_ipv6(peer: SocketAddr) -> bool {
383    match peer.ip() {
384        std::net::IpAddr::V4(_) => false,
385        std::net::IpAddr::V6(ip) => ip.to_ipv4_mapped().is_none(),
386    }
387}