systemd-resolved-rs 0.1.1

A compatibility-oriented reimplementation of systemd-resolved
#[cfg(test)]
mod test_03_synthetic_and_parallel_scopes {
    use super::*;
    use crate::routing::ScopeKind;
    use crate::wire::LocalRecord;
    use std::sync::{Arc, Barrier};

    #[test]
    fn synthetic_answers_do_not_depend_on_reading_etc_hosts() {
        let config = Config {
            upstreams: Vec::new(),
            fallback_upstreams: Vec::new(),
            read_etc_hosts: false,
            ..Config::default()
        };
        let lookup = Resolver::new(config)
            .lookup_name("localhost", 2)
            .expect("synthetic lookup");
        assert_eq!(lookup.addresses, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
    }

    #[test]
    fn fragmented_edns_udp_retries_over_tcp() {
        assert!(udp_requires_tcp_retry(false, 1280, FeatureLevel::Edns0));
        assert!(udp_requires_tcp_retry(
            false,
            1280,
            FeatureLevel::DnssecOk
        ));
        assert!(!udp_requires_tcp_retry(false, 1280, FeatureLevel::Udp));
        assert!(udp_requires_tcp_retry(true, 0, FeatureLevel::Udp));
    }

    #[test]
    fn equivalent_scopes_dispatch_queries_in_parallel() {
        let first_socket = UdpSocket::bind("127.0.0.1:0").expect("first mock DNS bind");
        let second_socket = UdpSocket::bind("127.0.0.1:0").expect("second mock DNS bind");
        let first_server = first_socket.local_addr().expect("first mock DNS address");
        let second_server = second_socket.local_addr().expect("second mock DNS address");
        for socket in [&first_socket, &second_socket] {
            socket
                .set_read_timeout(Some(Duration::from_secs(2)))
                .expect("mock DNS timeout");
        }

        let barrier = Arc::new(Barrier::new(2));
        let first_barrier = Arc::clone(&barrier);
        let first_thread = thread::spawn(move || {
            reply_after_both_scopes_arrive(
                &first_socket,
                first_barrier.as_ref(),
                Ipv4Addr::new(192, 0, 2, 21),
            );
        });
        let second_thread = thread::spawn(move || {
            reply_after_both_scopes_arrive(
                &second_socket,
                barrier.as_ref(),
                Ipv4Addr::new(192, 0, 2, 22),
            );
        });

        let resolver = Resolver::new(Config {
            upstreams: Vec::new(),
            fallback_upstreams: Vec::new(),
            cache: false,
            attempts: 1,
            query_timeout: Duration::from_secs(1),
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let scopes = vec![
            RouteScope {
                kind: ScopeKind::Link(2),
                servers: vec![first_server],
            },
            RouteScope {
                kind: ScopeKind::Link(3),
                servers: vec![second_server],
            },
        ];
        let query = make_query("parallel.example", TYPE_A, 0x7200).expect("client query");
        let started = Instant::now();
        let (response, winning_server) = resolver
            .query_scopes(&scopes, &query, 0)
            .expect("parallel scoped query");
        assert!(started.elapsed() < Duration::from_millis(750));
        assert!(winning_server == first_server || winning_server == second_server);

        let records = extract_address_records(&response, Some(2)).expect("address records");
        assert!(records.addresses.iter().any(|address| {
            matches!(
                address,
                IpAddr::V4(address)
                    if *address == Ipv4Addr::new(192, 0, 2, 21)
                        || *address == Ipv4Addr::new(192, 0, 2, 22)
            )
        }));

        first_thread.join().expect("first mock DNS thread");
        second_thread.join().expect("second mock DNS thread");
    }

    #[test]
    fn localhost_upstream_is_not_cached_by_default() {
        let socket = UdpSocket::bind("127.0.0.1:0").expect("mock DNS bind");
        socket
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("mock DNS timeout");
        let server = socket.local_addr().expect("mock DNS address");
        let server_thread = thread::spawn(move || {
            for _ in 0..2 {
                reply_once(&socket, Ipv4Addr::new(192, 0, 2, 31));
            }
        });

        let resolver = Resolver::new(Config {
            upstreams: vec![server],
            fallback_upstreams: Vec::new(),
            attempts: 1,
            query_timeout: Duration::from_secs(1),
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let first = make_query("local-cache.example", TYPE_A, 0x7300).expect("first query");
        resolver
            .query(&first, QueryMode::Full)
            .expect("first response");
        assert!(resolver.cache.is_empty());

        let second = make_query("local-cache.example", TYPE_A, 0x7301).expect("second query");
        resolver
            .query(&second, QueryMode::Full)
            .expect("second response");
        assert!(resolver.cache.is_empty());
        server_thread.join().expect("mock DNS thread");
    }

    #[test]
    fn cache_from_localhost_allows_explicit_local_caching() {
        let socket = UdpSocket::bind("127.0.0.1:0").expect("mock DNS bind");
        socket
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("mock DNS timeout");
        let server = socket.local_addr().expect("mock DNS address");
        let server_thread = thread::spawn(move || {
            reply_once(&socket, Ipv4Addr::new(192, 0, 2, 32));
        });

        let resolver = Resolver::new(Config {
            upstreams: vec![server],
            fallback_upstreams: Vec::new(),
            cache_from_localhost: true,
            attempts: 1,
            query_timeout: Duration::from_secs(1),
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let first = make_query("local-cache.example", TYPE_A, 0x7400).expect("first query");
        let (_, first_flags) = resolver
            .query_on_link_with_flags(&first, QueryMode::Full, None, 0)
            .expect("first response");
        assert_eq!(resolver.cache.len(), 1);
        assert_ne!(
            first_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_NETWORK,
            0
        );
        assert_eq!(
            first_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_CACHE,
            0
        );

        let second = make_query("local-cache.example", TYPE_A, 0x7401).expect("second query");
        let (response, second_flags) = resolver
            .query_on_link_with_flags(&second, QueryMode::Full, None, 0)
            .expect("cached response");
        assert_eq!(&response[..2], &0x7401u16.to_be_bytes());
        assert_ne!(
            second_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_CACHE,
            0
        );
        assert_eq!(
            second_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_NETWORK,
            0
        );

        let third = make_query("local-cache.example", TYPE_A, 0x7402).expect("third query");
        let (_, third_flags) = resolver
            .query_on_link_with_flags(
                &third,
                QueryMode::Full,
                None,
                crate::dbus_resolve1_abi::flags::SD_RESOLVED_NO_NETWORK,
            )
            .expect("network-disabled cache hit");
        assert_ne!(
            third_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_CACHE,
            0
        );

        let fourth = make_query("local-cache.example", TYPE_A, 0x7403).expect("fourth query");
        assert!(matches!(
            resolver.query_on_link_with_flags(
                &fourth,
                QueryMode::Full,
                None,
                crate::dbus_resolve1_abi::flags::SD_RESOLVED_NO_NETWORK
                    | crate::dbus_resolve1_abi::flags::SD_RESOLVED_NO_CACHE,
            ),
            Err(ResolveError::NoNameServers)
        ));
        server_thread.join().expect("mock DNS thread");
    }

    #[test]
    fn configured_stub_loop_is_skipped_or_reported() {
        let socket = UdpSocket::bind("127.0.0.1:0").expect("mock DNS bind");
        socket
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("mock DNS timeout");
        let healthy = socket.local_addr().expect("mock DNS address");
        let stub: SocketAddr = "127.0.0.1:53535".parse().expect("stub address");
        let extra = crate::config::DnsStubListenerExtra::parse(&stub.to_string())
            .expect("extra stub listener");

        let loop_only = Resolver::new(Config {
            upstreams: vec![stub],
            fallback_upstreams: Vec::new(),
            dns_stub_listener_extra: vec![extra],
            cache: false,
            attempts: 1,
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let query = make_query("loop.example", TYPE_A, 0x7450).expect("loop query");
        assert!(matches!(
            loop_only.query(&query, QueryMode::Full),
            Err(ResolveError::StubLoop)
        ));

        let server_thread = thread::spawn(move || {
            reply_once(&socket, Ipv4Addr::new(192, 0, 2, 35));
        });
        let resolver = Resolver::new(Config {
            upstreams: vec![stub, healthy],
            fallback_upstreams: Vec::new(),
            dns_stub_listener_extra: vec![extra],
            cache: false,
            attempts: 1,
            query_timeout: Duration::from_secs(1),
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let response = resolver
            .query(&query, QueryMode::Full)
            .expect("healthy peer response");
        assert_eq!(
            extract_address_records(&response, Some(2))
                .expect("address records")
                .addresses,
            vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 35))]
        );
        server_thread.join().expect("mock DNS thread");
    }

    #[test]
    fn no_cache_bypasses_lookup_but_stores_the_network_answer() {
        let socket = UdpSocket::bind("127.0.0.1:0").expect("mock DNS bind");
        socket
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("mock DNS timeout");
        let server = socket.local_addr().expect("mock DNS address");
        let server_thread = thread::spawn(move || {
            reply_once(&socket, Ipv4Addr::new(192, 0, 2, 33));
        });

        let resolver = Resolver::new(Config {
            upstreams: vec![server],
            fallback_upstreams: Vec::new(),
            cache_from_localhost: true,
            attempts: 1,
            query_timeout: Duration::from_secs(1),
            read_etc_hosts: false,
            read_static_records: false,
            dnssec: ValidationMode::No,
            ..Config::default()
        });
        let first = make_query("bypass-cache.example", TYPE_A, 0x7410).expect("first query");
        let (_, first_flags) = resolver
            .query_on_link_with_flags(
                &first,
                QueryMode::Full,
                None,
                crate::dbus_resolve1_abi::flags::SD_RESOLVED_NO_CACHE,
            )
            .expect("network response");
        assert_ne!(
            first_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_NETWORK,
            0
        );
        assert_eq!(resolver.cache.len(), 1);

        let second = make_query("bypass-cache.example", TYPE_A, 0x7411).expect("second query");
        let (_, second_flags) = resolver
            .query_on_link_with_flags(
                &second,
                QueryMode::Full,
                None,
                crate::dbus_resolve1_abi::flags::SD_RESOLVED_NO_NETWORK,
            )
            .expect("cached response");
        assert_ne!(
            second_flags & crate::dbus_resolve1_abi::flags::SD_RESOLVED_FROM_CACHE,
            0
        );
        server_thread.join().expect("mock DNS thread");
    }

    fn reply_after_both_scopes_arrive(socket: &UdpSocket, barrier: &Barrier, address: Ipv4Addr) {
        let mut buffer = [0; 2048];
        let (length, peer) = socket.recv_from(&mut buffer).expect("mock scoped query");
        barrier.wait();
        let response = local_response(&buffer[..length], &[LocalRecord::A(address)], 30)
            .expect("mock scoped response");
        socket
            .send_to(&response, peer)
            .expect("mock scoped response send");
    }

    fn reply_once(socket: &UdpSocket, address: Ipv4Addr) {
        let mut buffer = [0; 2048];
        let (length, peer) = socket.recv_from(&mut buffer).expect("mock DNS query");
        let response = local_response(&buffer[..length], &[LocalRecord::A(address)], 30)
            .expect("mock DNS response");
        socket
            .send_to(&response, peer)
            .expect("mock DNS response send");
    }
}