pharos-server 0.1.0

Listeners, Plugin Chain contract and metrics for the Pharos DNS server
Documentation
#![forbid(unsafe_code)]

use pharos_proto::{Message, Rcode};
use pharos_server::{
    Action, Chain, Context, Error, Handler, ListenerKey, Outcome, Protocol, Router, Server,
    Transport, logging::ERROR_LOGGED_METADATA,
};
use std::process::Command;
use std::sync::Mutex;

static LOGGED: Mutex<Vec<Error>> = Mutex::new(Vec::new());

struct Broken {
    marked: bool,
    panic: bool,
    response: bool,
    error: Error,
}

impl Chain for Broken {
    async fn serve(&self, cx: &mut Context<'_>) -> Outcome {
        if self.marked {
            for key in [
                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14",
            ] {
                assert!(cx.metadata.insert(key, "used"));
            }
            assert!(cx.metadata.insert(ERROR_LOGGED_METADATA, "true"));
        }
        if self.panic {
            panic!("plugin panic");
        }
        if self.response {
            cx.response.extend_from_slice(&[0; 12]);
            return Outcome::Response;
        }
        Outcome::Error(self.error)
    }
    fn is_empty(&self) -> bool {
        false
    }
}

fn log(error: Error) {
    LOGGED.lock().unwrap().push(error);
}

#[tokio::test]
async fn suppresses_only_marked_plugin_error_not_panics_or_scrub_errors() {
    let key = ListenerKey {
        transport: Transport::Dns,
        address: "127.0.0.1:53".parse().unwrap(),
    };
    let mut wire = vec![0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, b'a', 0, 0, 1, 0, 1];
    wire.extend_from_slice(&[0, 0, 41, 4, 208, 0, 0, 0, 0, 0, 0]);
    let peer = "127.0.0.1:1234".parse().unwrap();
    for (marked, panic, response, capacity, error, expected) in [
        (
            false,
            false,
            false,
            512,
            Error::NoNextPlugin,
            vec![Error::NoNextPlugin],
        ),
        (true, false, false, 512, Error::NoNextPlugin, vec![]),
        (
            true,
            false,
            false,
            19,
            Error::ResponseBufferTooSmall,
            vec![Error::ResponseBufferTooSmall],
        ),
        (
            true,
            true,
            false,
            512,
            Error::NoNextPlugin,
            vec![Error::PluginPanic],
        ),
        (
            true,
            false,
            true,
            0,
            Error::NoNextPlugin,
            vec![Error::ResponseBufferTooSmall],
        ),
    ] {
        LOGGED.lock().unwrap().clear();
        let mut router = Router::default();
        router.add(
            key,
            ".".parse().unwrap(),
            Some(Broken {
                marked,
                panic,
                response,
                error,
            }),
            vec![],
        );
        let mut server = Server::new(router, key);
        server.logger = log;
        let mut reply = Vec::with_capacity(capacity);
        assert_eq!(
            server.handle(&wire, peer, Protocol::Udp, &mut reply).await,
            Action::Respond
        );
        assert_eq!(*LOGGED.lock().unwrap(), expected);
        assert_eq!(Message::parse(&reply).unwrap().rcode(), Rcode::SERVFAIL);
    }
}

#[test]
fn default_logger_only_reports_panics_and_scrub_errors() {
    if let Ok(mode) = std::env::var("PHAROS_TEST_DEFAULT_LOGGER") {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();
        runtime.block_on(async {
            let key = ListenerKey {
                transport: Transport::Dns,
                address: "127.0.0.1:53".parse().unwrap(),
            };
            let mut wire = vec![0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, b'a', 0, 0, 1, 0, 1];
            wire.extend_from_slice(&[0, 0, 41, 4, 208, 0, 0, 0, 0, 0, 0]);
            let mut router = Router::default();
            router.add(
                key,
                ".".parse().unwrap(),
                Some(Broken {
                    marked: false,
                    panic: mode == "panic",
                    response: false,
                    error: Error::NoNextPlugin,
                }),
                vec![],
            );
            let server = Server::new(router, key);
            let mut response = Vec::with_capacity(if mode == "scrub" { 19 } else { 512 });
            assert_eq!(
                server
                    .handle(
                        &wire,
                        "127.0.0.1:1234".parse().unwrap(),
                        Protocol::Udp,
                        &mut response
                    )
                    .await,
                Action::Respond
            );
        });
        return;
    }
    for (mode, expected) in [
        ("ordinary", None),
        ("scrub", Some("Response buffer too small")),
        ("panic", Some("plugin panicked")),
    ] {
        let output = Command::new(std::env::current_exe().unwrap())
            .args([
                "--exact",
                "default_logger_only_reports_panics_and_scrub_errors",
                "--nocapture",
            ])
            .env("PHAROS_TEST_DEFAULT_LOGGER", mode)
            .env("RUST_BACKTRACE", "0")
            .output()
            .unwrap();
        assert!(
            output.status.success(),
            "{}",
            String::from_utf8_lossy(&output.stderr)
        );
        let stdout = String::from_utf8_lossy(&output.stdout);
        assert_eq!(
            stdout.matches("Plugin error:").count(),
            usize::from(expected.is_some()),
            "{stdout}"
        );
        if let Some(expected) = expected {
            assert!(stdout.contains(expected), "{stdout}");
        }
    }
}