systemd-resolved-rs 0.1.1

A compatibility-oriented reimplementation of systemd-resolved
#[cfg(test)]
mod test_02_lookup_and_server_failover {
    use super::*;

    fn append_test_answer(packet: &mut Vec<u8>, owner: &[u8], rr_type: u16, rdata: &[u8]) {
        packet.extend_from_slice(owner);
        packet.extend_from_slice(&rr_type.to_be_bytes());
        packet.extend_from_slice(&wire::CLASS_IN.to_be_bytes());
        packet.extend_from_slice(&60u32.to_be_bytes());
        packet.extend_from_slice(
            &u16::try_from(rdata.len())
                .expect("test RDATA length")
                .to_be_bytes(),
        );
        packet.extend_from_slice(rdata);
    }

    #[test]
    fn lookup_name_follows_cname_and_ignores_unrelated_addresses() {
        use crate::wire::{encode_name, question_end, TYPE_CNAME};
        use std::thread;

        let socket = UdpSocket::bind("127.0.0.1:0").expect("bind test DNS server");
        socket
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("set test timeout");
        let server = socket.local_addr().expect("test DNS server address");
        let worker = thread::spawn(move || {
            let mut buffer = [0; 4096];
            let (length, peer) = socket.recv_from(&mut buffer).expect("receive query");
            let query = &buffer[..length];
            let end = question_end(query).expect("question end");
            let mut response = query[..end].to_vec();
            response[2..4].copy_from_slice(&0x8180u16.to_be_bytes());
            response[6..8].copy_from_slice(&3u16.to_be_bytes());
            response[8..12].fill(0);

            let canonical = encode_name("real.example.test").expect("canonical name");
            append_test_answer(&mut response, &[0xc0, 0x0c], TYPE_CNAME, &canonical);
            append_test_answer(
                &mut response,
                &encode_name("unrelated.example.test").expect("unrelated owner"),
                TYPE_A,
                &[203, 0, 113, 9],
            );
            append_test_answer(&mut response, &canonical, TYPE_A, &[192, 0, 2, 42]);
            socket.send_to(&response, peer).expect("send DNS response");
        });

        let config = Config {
            upstreams: vec![server],
            fallback_upstreams: Vec::new(),
            query_timeout: Duration::from_secs(1),
            attempts: 1,
            cache: false,
            read_etc_hosts: false,
            ..Config::default()
        };
        let lookup = Resolver::new(config)
            .lookup_name("alias.example.test", 2)
            .expect("CNAME lookup");
        worker.join().expect("test DNS worker");

        assert_eq!(lookup.canonical_name, "real.example.test");
        assert_eq!(
            lookup.addresses,
            vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 42))]
        );
    }

    #[test]
    fn refused_response_retries_another_server() {
        use std::thread;

        let refused_socket = UdpSocket::bind("127.0.0.1:0").expect("bind refusing DNS server");
        let success_socket = UdpSocket::bind("127.0.0.1:0").expect("bind succeeding DNS server");
        let refused_server = refused_socket.local_addr().expect("refusing DNS address");
        let success_server = success_socket.local_addr().expect("succeeding DNS address");
        for socket in [&refused_socket, &success_socket] {
            socket
                .set_read_timeout(Some(Duration::from_secs(2)))
                .expect("set mock DNS timeout");
        }

        let refused_worker = thread::spawn(move || {
            let mut buffer = [0; 2048];
            let (length, peer) = refused_socket
                .recv_from(&mut buffer)
                .expect("receive refused query");
            let mut response = local_response(&buffer[..length], &[], 0).expect("REFUSED response");
            let flags = u16::from_be_bytes([response[2], response[3]]);
            response[2..4].copy_from_slice(&((flags & !0x000f) | RCODE_REFUSED).to_be_bytes());
            refused_socket
                .send_to(&response, peer)
                .expect("send REFUSED response");
        });
        let success_worker = thread::spawn(move || {
            let mut buffer = [0; 2048];
            let (length, peer) = success_socket
                .recv_from(&mut buffer)
                .expect("receive retry query");
            let response = local_response(
                &buffer[..length],
                &[crate::wire::LocalRecord::A(Ipv4Addr::new(192, 0, 2, 77))],
                30,
            )
            .expect("success response");
            success_socket
                .send_to(&response, peer)
                .expect("send success response");
        });

        let resolver = Resolver::new(Config {
            upstreams: vec![refused_server, success_server],
            fallback_upstreams: Vec::new(),
            query_timeout: Duration::from_secs(1),
            attempts: 2,
            cache: false,
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        {
            let mut states = resolver.states();
            states
                .entry(ServerKey::new(ScopeKind::Global, refused_server))
                .or_default()
                .metric
                .round_trip_ms = 1.0;
            states
                .entry(ServerKey::new(ScopeKind::Global, success_server))
                .or_default()
                .metric
                .round_trip_ms = 1000.0;
        }

        let query = make_query("refused.example.test", TYPE_A, 0x7300).expect("client query");
        let response = resolver
            .query(&query, QueryMode::Full)
            .expect("retry succeeds");
        let records = extract_address_records(&response, Some(2)).expect("address records");
        assert_eq!(
            records.addresses,
            vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 77))]
        );

        refused_worker.join().expect("refusing DNS worker");
        success_worker.join().expect("succeeding DNS worker");
    }
}