pharos-plugins 0.1.0

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

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

fn configured(directives_text: &str) -> Result<Chain, String> {
    let source = format!(".:53 {{\n{directives_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_accepts_data_and_rejects_duplicate() {
    for text in [
        "nsid",
        "nsid ps0",
        "nsid worker1",
        "nsid \"tf 2\"",
        "nsid Use The Force",
    ] {
        assert!(configured(text).is_ok(), "{text}");
    }
    assert!(configured("nsid\nnsid").is_err());
    assert!(configured("nsid {\n foo\n}").is_err());
    assert!(configured("nsid {\n}").is_err());
}

async fn query(chain: &Chain, options: Option<&[u8]>) -> Vec<u8> {
    let mut wire = vec![
        0,
        1,
        1,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        u8::from(options.is_some()),
        0,
        0,
        1,
        0,
        1,
    ];
    if let Some(options) = options {
        wire.extend_from_slice(&[0, 0, 41, 0x10, 0, 0, 0, 0, 0]);
        wire.extend_from_slice(&(options.len() as u16).to_be_bytes());
        wire.extend_from_slice(options);
    }
    let domain: Name = ".".parse().unwrap();
    let mut response = Vec::with_capacity(65535);
    let mut slots = [None; 8];
    let mut context = Context {
        query: Message::parse(&wire).unwrap(),
        client: "127.0.0.1:1234".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 context).await, Outcome::Response);
    response
}

#[tokio::test]
async fn coredns_nsid_and_cache_return_requested_identifier() {
    for config in ["nsid NSID\nwhoami", "cache\nnsid NSID\nwhoami"] {
        let mut chain = configured(config).unwrap();
        for iteration in 0..2 {
            let response = query(&chain, Some(&[0, 3, 0, 0])).await;
            let message = Message::parse(&response).unwrap();
            let opt = message.opt().unwrap();
            assert_eq!(opt.udp_payload_size(), 4096);
            assert_eq!(
                opt.options().iter().collect::<Vec<_>>(),
                [EdnsOption::Nsid(b"NSID")]
            );
            if iteration == 0 && config.starts_with("cache") {
                chain.whoami = None;
                let response = query(&chain, Some(&[])).await;
                let message = Message::parse(&response).unwrap();
                assert!(!message.opt().is_some_and(|opt| {
                    opt.options()
                        .iter()
                        .any(|option| matches!(option, EdnsOption::Nsid(_)))
                }));
            }
        }
    }
}

#[tokio::test]
async fn query_nsid_payload_is_ignored() {
    let chain = configured("nsid NSID\nwhoami").unwrap();
    let response = query(&chain, Some(&[0, 3, 0, 3, 1, 2, 3])).await;
    let message = Message::parse(&response).unwrap();
    assert_eq!(
        message.opt().unwrap().options().iter().next(),
        Some(EdnsOption::Nsid(b"NSID"))
    );
}

#[tokio::test]
async fn oversized_nsid_returns_servfail_after_listener_scrubbing() {
    let chain = configured(&format!("nsid {}\nwhoami", "x".repeat(500))).unwrap();
    let mut response = query(&chain, Some(&[0, 3, 0, 0])).await;
    let wire = [
        0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 41, 2, 0, 0, 0, 0, 0, 0, 4, 0, 3,
        0, 0,
    ];
    let query = Message::parse(&wire).unwrap();
    assert_eq!(
        pharos_server::finish_outcome(
            query,
            Protocol::Udp,
            Outcome::Response,
            &mut response,
            |_| {}
        ),
        pharos_server::Action::Respond
    );
    assert_eq!(
        Message::parse(&response).unwrap().rcode(),
        pharos_proto::Rcode::SERVFAIL
    );
}

#[tokio::test]
async fn no_nsid_is_added_without_an_nsid_query() {
    let chain = configured("nsid NSID\nwhoami").unwrap();
    for options in [None, Some(&[][..]), Some(&[0, 12, 0, 0][..])] {
        let response = query(&chain, options).await;
        let message = Message::parse(&response).unwrap();
        assert!(!message.opt().is_some_and(|opt| {
            opt.options()
                .iter()
                .any(|o| matches!(o, EdnsOption::Nsid(_)))
        }));
    }
}

#[tokio::test]
async fn default_identifier_is_the_hostname() {
    let chain = configured("nsid\nwhoami").unwrap();
    let response = query(&chain, Some(&[0, 3, 0, 0])).await;
    let message = Message::parse(&response).unwrap();
    let expected = nix::unistd::gethostname()
        .map(|s| s.into_encoded_bytes())
        .unwrap_or_else(|_| b"localhost".to_vec());
    assert_eq!(
        message.opt().unwrap().options().iter().collect::<Vec<_>>(),
        [EdnsOption::Nsid(&expected)]
    );
}

#[tokio::test]
async fn coredns_setup_values_are_returned_verbatim() {
    for (directive, expected) in [
        ("nsid ps0", "ps0"),
        ("nsid worker1", "worker1"),
        ("nsid \"tf 2\"", "tf 2"),
        ("nsid Use The Force", "Use The Force"),
        ("nsid \"\"", ""),
    ] {
        let chain = configured(&format!("{directive}\nwhoami")).unwrap();
        let response = query(&chain, Some(&[0, 3, 0, 0])).await;
        let message = Message::parse(&response).unwrap();
        assert_eq!(
            message.opt().unwrap().options().iter().next(),
            Some(EdnsOption::Nsid(expected.as_bytes())),
            "{directive}"
        );
    }
}