pharos-plugins 0.1.0

CoreDNS-compatible Plugins for the Pharos DNS server
Documentation
#![forbid(unsafe_code)]
#![cfg(feature = "plugin-bufsize")]
#![recursion_limit = "512"]

use pharos_corefile::{configure, parse};
use pharos_plugins::{Chain, directives};
use pharos_proto::{Message, Name, Rcode};
use pharos_server::{Context, Metadata, Next, Outcome, Plugin, Protocol, Transport};

fn configured(text: &str) -> Result<Chain, String> {
    let source = format!(".:53 {{\n{text}\n}}");
    let blocks = parse("Corefile", &source).map_err(|e| e.to_string())?;
    let mut blocks = configure::<Chain>(&blocks, directives()).map_err(|e| e.to_string())?;
    Ok(blocks.remove(0).config)
}

#[test]
fn coredns_setup_and_boundaries() {
    for directive in [
        "bufsize",
        "bufsize \"1220\"",
        "bufsize 512",
        "bufsize 4096",
        "bufsize +1024",
    ] {
        assert!(configured(directive).is_ok(), "{directive}");
    }
    for directive in [
        "bufsize\nbufsize 1200",
        "bufsize {\n foo\n}",
        "bufsize {\n}",
    ] {
        assert!(configured(directive).is_ok(), "{directive}");
    }
    for directive in [
        "bufsize 5000",
        "bufsize \"512 512\"",
        "bufsize 511",
        "bufsize abc123",
        "bufsize 1024 1025",
    ] {
        assert!(configured(directive).is_err(), "{directive}");
    }
}

#[cfg(feature = "plugin-whoami")]
#[tokio::test]
async fn listener_uses_clamped_opt_for_udp_and_tcp_responses() {
    use pharos_plugins::servers;
    use pharos_server::{Action, Handler};

    let blocks = parse("Corefile", ".:53 {\n bufsize\n whoami\n}").unwrap();
    let (servers, _) = servers(configure::<Chain>(&blocks, directives()).unwrap()).unwrap();
    let server = servers.values().next().unwrap();
    for protocol in [Protocol::Udp, Protocol::Tcp] {
        for (size, expected) in [(4096_u16, 1232), (1024, 1024)] {
            let mut wire = vec![0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 41];
            wire.extend_from_slice(&size.to_be_bytes());
            wire.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
            let mut response = Vec::with_capacity(65535);
            assert_eq!(
                server
                    .handle(
                        &wire,
                        "127.0.0.1:1234".parse().unwrap(),
                        protocol,
                        &mut response
                    )
                    .await,
                Action::Respond
            );
            assert_eq!(
                Message::parse(&response)
                    .unwrap()
                    .opt()
                    .unwrap()
                    .udp_payload_size(),
                expected
            );
        }
    }
}

#[cfg(feature = "plugin-hosts")]
#[tokio::test]
async fn clamped_udp_response_truncates_but_tcp_does_not() {
    use pharos_plugins::servers;
    use pharos_server::{Action, Handler};

    let mut source = String::from(".:53 {\n bufsize 512\n hosts /nonexistent {\n");
    for n in 1..=80 {
        source.push_str(&format!(" 192.0.2.{n} large.example\n"));
    }
    source.push_str(" }\n}");
    let blocks = parse("Corefile", &source).unwrap();
    let (servers, _) = servers(configure::<Chain>(&blocks, directives()).unwrap()).unwrap();
    let server = servers.values().next().unwrap();
    let wire = b"\0\x01\x01\0\0\x01\0\0\0\0\0\x01\x05large\x07example\0\0\x01\0\x01\0\0\x29\x10\0\0\0\0\0\0\0";
    for protocol in [Protocol::Udp, Protocol::Tcp] {
        let mut response = Vec::with_capacity(65535);
        assert_eq!(
            server
                .handle(
                    wire,
                    "127.0.0.1:1234".parse().unwrap(),
                    protocol,
                    &mut response
                )
                .await,
            Action::Respond
        );
        let message = Message::parse(&response).unwrap();
        assert_eq!(message.opt().unwrap().udp_payload_size(), 512);
        assert_eq!(message.tc(), protocol == Protocol::Udp);
        assert_eq!(response.len() <= 512, protocol == Protocol::Udp);
    }
}

#[cfg(feature = "plugin-forward")]
#[tokio::test]
async fn forward_sends_clamped_wire_query_to_upstream() {
    use tokio::net::UdpSocket;

    let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
    let chain = configured(&format!(
        "bufsize 1100\nforward . {}",
        upstream.local_addr().unwrap()
    ))
    .unwrap();
    let task = tokio::spawn(async move {
        let mut wire = [0; 512];
        let (len, peer) = upstream.recv_from(&mut wire).await.unwrap();
        assert_eq!(
            Message::parse(&wire[..len])
                .unwrap()
                .opt()
                .unwrap()
                .udp_payload_size(),
            1100
        );
        wire[2] |= 0x80;
        upstream.send_to(&wire[..len], peer).await.unwrap();
    });
    let wire = [
        0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0,
    ];
    let domain: Name = ".".parse().unwrap();
    let mut response = Vec::with_capacity(65535);
    let mut slots = [];
    let mut cx = Context {
        query: Message::parse(&wire).unwrap(),
        client: "127.0.0.1:53".parse().unwrap(),
        local: "127.0.0.1:53".parse().unwrap(),
        transport: Transport::Dns,
        protocol: Protocol::Udp,
        domain: &domain,
        metadata: Metadata::new(&mut slots),
        response: &mut response,
        max_response_size: 4096,
    };
    assert_eq!(chain.serve(&mut cx).await, Outcome::Response);
    task.await.unwrap();
}

struct Inspect {
    expected: Option<u16>,
    max_response_size: usize,
    outcome: Outcome,
}

impl Next for Inspect {
    async fn run(self, cx: &mut Context<'_>) -> Outcome {
        assert_eq!(cx.query.opt().map(|o| o.udp_payload_size()), self.expected);
        assert_eq!(cx.max_response_size, self.max_response_size);
        let mut wire = Vec::new();
        cx.query.copy_wire(&mut wire);
        assert_eq!(
            Message::parse(&wire)
                .unwrap()
                .opt()
                .map(|o| o.udp_payload_size()),
            self.expected
        );
        self.outcome
    }
}

#[tokio::test]
async fn coredns_clamps_only_larger_opt_and_preserves_outcome() {
    let chain = configured("bufsize 1024").unwrap();
    for (protocol, requested, expected, outcome) in [
        (Protocol::Udp, Some(1152_u16), Some(1024), Outcome::Response),
        (Protocol::Tcp, Some(1152), Some(1024), Outcome::Response),
        (Protocol::Udp, Some(896), Some(896), Outcome::Drop),
        (Protocol::Udp, Some(1), Some(1), Outcome::Drop),
        (
            Protocol::Udp,
            Some(1024),
            Some(1024),
            Outcome::Unanswered(Rcode::REFUSED),
        ),
        (
            Protocol::Udp,
            None,
            None,
            Outcome::Error(pharos_server::Error::NoNextPlugin),
        ),
    ] {
        let mut wire = vec![
            0,
            1,
            1,
            0,
            0,
            1,
            0,
            0,
            0,
            0,
            0,
            u8::from(requested.is_some()),
            0,
            0,
            1,
            0,
            1,
        ];
        if let Some(size) = requested {
            wire.extend_from_slice(&[0, 0, 41]);
            wire.extend_from_slice(&size.to_be_bytes());
            wire.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
        }
        let domain: Name = ".".parse().unwrap();
        let mut response = Vec::with_capacity(4096);
        let mut slots = [];
        let mut cx = Context {
            query: Message::parse(&wire).unwrap(),
            client: "127.0.0.1:53".parse().unwrap(),
            local: "127.0.0.1:53".parse().unwrap(),
            transport: Transport::Dns,
            protocol,
            domain: &domain,
            metadata: Metadata::new(&mut slots),
            response: &mut response,
            max_response_size: if protocol == Protocol::Udp {
                usize::from(requested.unwrap_or(512).max(512))
            } else {
                usize::from(u16::MAX)
            },
        };
        let max_response_size = if protocol == Protocol::Udp {
            usize::from(expected.unwrap_or(512).max(512))
        } else {
            usize::from(u16::MAX)
        };
        assert_eq!(
            chain
                .bufsize
                .as_ref()
                .unwrap()
                .serve(
                    &mut cx,
                    Inspect {
                        expected,
                        max_response_size,
                        outcome
                    }
                )
                .await,
            outcome
        );
    }
}