Module domain::net::client

source ·
Expand description

Sending requests and receiving responses.

This module provides DNS transport protocols that allow sending a DNS request and receiving the corresponding reply.

Currently the following transport protocols are supported:

  • dgram DNS over a datagram protocol, typically UDP.
  • stream DNS over an octet stream protocol, typically TCP or TLS. Only a single connection is supported. The transport works as long as the connection continues to exist.
  • multi_stream This is a layer on top of stream where new connections are established as old connections are closed (or fail).
  • dgram_stream This is a combination of dgram and multi_stream. This is typically needed because a request over UDP can receive a truncated response, which should be retried over TCP.
  • redundant This transport multiplexes requests over a collection of transport connections. The redundant transport favors the connection with the lowest response time. Any of the other transports can be added as upstream transports.
  • cache This is a simple message cache provided as a pass through transport. The cache works with any of the other transports.
  • tsig: This is a TSIG request signer and response verifier provided as a pass through transport. The tsig transport works with any upstream transports so long as they don’t modify the message once signed nor modify the response before it can be verified.
  • validator: This is a DNSSEC validator provided as a pass through transport. The validator works with any of the other transports.

Sending a request and receiving the reply consists of four steps:

  1. Creating a request message,
  2. Creating a DNS transport,
  3. Sending the request, and
  4. Receiving the reply or replies.

The first and second step are independent and can happen in any order. The third step uses the resuts of the first and second step. Finally, the fourth step uses the result of the third step.

§Creating a request message

The DNS transport protocols expect a request message that implements the ComposeRequest trait. This trait allows transports to add ENDS(0) options, set flags, etc. The RequestMessage type implements this trait. The new method of RequestMessage creates a new RequestMessage object based an existing messsage (that implements Into<Message<Octs>>).

For example:

let mut msg = MessageBuilder::new_vec();
msg.header_mut().set_rd(true);
let mut msg = msg.question();
msg.push(
    (Name::vec_from_str("example.com").unwrap(), Rtype::AAAA)
).unwrap();
let req = RequestMessage::new(msg);

§Creating a DNS transport

Creating a DNS transport typically involves creating a configuration object, creating the underlying network connection, creating the DNS transport and running a run method as a separate task. This is illustrated in the following example:

let mut multi_stream_config = multi_stream::Config::default();
multi_stream_config.stream_mut().set_response_timeout(
    Duration::from_millis(100),
);
let tcp_connect = TcpConnect::new(server_addr);
let (tcp_conn, transport) = multi_stream::Connection::with_config(
    tcp_connect, multi_stream_config
);
tokio::spawn(transport.run());

§Sending the request

A connection implements the SendRequest trait. This trait provides a single method, send_request and returns an object that provides the response.

For example:

let mut request = tls_conn.send_request(req);

where tls_conn is a transport connection for DNS over TLS.

§Receiving the response

The send_request method returns an object that implements the GetResponse trait. This trait provides a single method, get_response, which returns the DNS response message or an error. This method is intended to be cancelation safe.

For example:

let reply = request.get_response().await;

Support for multiple responses:

RequestMessage is designed for the most common use case: single request, single response.

However, zone transfers (e.g. using the AXFR or IXFR query types) can result in multiple responses. Attempting to create a RequestMessage for such a query will result in Error::FormError.

For zone transfers you should use RequestMessageMulti instead which can be used like so:

while let Ok(reply) = request.get_response().await {
    // ...
}

§Limitations

The current implementation has the following limitations:

  • The dgram transport does not support DNS Cookies (RFC 7873 Domain Name System (DNS) Cookies).
  • The multi_stream transport does not support timeouts or other limits on the number of attempts to open a connection. The caller has to implement a timeout mechanism.
  • The cache transport does not support:
    • Prefetching. In this context, prefetching means updating a cache entry before it expires.
    • RFC 8767 (Serving Stale Data to Improve DNS Resiliency)
    • RFC 7871 (Client Subnet in DNS Queries)
    • RFC 8198 (Aggressive Use of DNSSEC-Validated Cache)

§Example with various transport connections

/// Using the `domain::net::client` module for sending a query.
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::vec::Vec;

use domain::base::MessageBuilder;
use domain::base::Name;
use domain::base::Rtype;
use domain::net::client::cache;
use domain::net::client::dgram;
use domain::net::client::dgram_stream;
use domain::net::client::multi_stream;
use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect};
use domain::net::client::redundant;
use domain::net::client::request::{
    RequestMessage, RequestMessageMulti, SendRequest,
};
use domain::net::client::stream;

#[cfg(feature = "tsig")]
use domain::net::client::request::SendRequestMulti;
#[cfg(feature = "tsig")]
use domain::net::client::tsig::{self};
#[cfg(feature = "tsig")]
use domain::tsig::{Algorithm, Key, KeyName};

use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::rustls::{ClientConfig, RootCertStore};

#[tokio::main]
async fn main() {
    // Create DNS request message.
    //
    // Transports currently take a `RequestMessage` as their input to be able
    // to add options along the way.
    //
    // In the future, it will also be possible to pass in a message or message
    // builder directly as input but for now it needs to be converted into a
    // `RequestMessage` manually.
    let mut msg = MessageBuilder::new_vec();
    msg.header_mut().set_rd(true);
    msg.header_mut().set_ad(true);
    let mut msg = msg.question();
    msg.push((Name::vec_from_str("example.com").unwrap(), Rtype::AAAA))
        .unwrap();
    let req = RequestMessage::new(msg).unwrap();

    // Destination for UDP and TCP
    let server_addr = SocketAddr::new(IpAddr::from_str("::1").unwrap(), 53);

    let mut stream_config = stream::Config::new();
    stream_config.set_response_timeout(Duration::from_millis(100));
    let multi_stream_config =
        multi_stream::Config::from(stream_config.clone());

    // Create a new UDP+TCP transport connection. Pass the destination address
    // and port as parameter.
    let mut dgram_config = dgram::Config::new();
    dgram_config.set_max_parallel(1);
    dgram_config.set_read_timeout(Duration::from_millis(1000));
    dgram_config.set_max_retries(1);
    dgram_config.set_udp_payload_size(Some(1400));
    let dgram_stream_config = dgram_stream::Config::from_parts(
        dgram_config.clone(),
        multi_stream_config.clone(),
    );
    let udp_connect = UdpConnect::new(server_addr);
    let tcp_connect = TcpConnect::new(server_addr);
    let (udptcp_conn, transport) = dgram_stream::Connection::with_config(
        udp_connect,
        tcp_connect,
        dgram_stream_config.clone(),
    );

    // Start the run function in a separate task. The run function will
    // terminate when all references to the connection have been dropped.
    // Make sure that the task does not accidentally get a reference to the
    // connection.
    tokio::spawn(async move {
        transport.run().await;
        println!("UDP+TCP run exited");
    });

    // Send a query message.
    let mut request = udptcp_conn.send_request(req.clone());

    // Get the reply
    println!("Wating for UDP+TCP reply");
    let reply = request.get_response().await;
    println!("UDP+TCP reply: {reply:?}");

    // The query may have a reference to the connection. Drop the query
    // when it is no longer needed.
    drop(request);

    // Create a cached transport.
    let mut cache_config = cache::Config::new();
    cache_config.set_max_cache_entries(100); // Just an example.
    let cache =
        cache::Connection::with_config(udptcp_conn.clone(), cache_config);

    // Send a request message.
    let mut request = cache.send_request(req.clone());

    // Get the reply
    println!("Wating for cache reply");
    let reply = request.get_response().await;
    println!("Cache reply: {reply:?}");

    // Send the request message again.
    let mut request = cache.send_request(req.clone());

    // Get the reply
    println!("Wating for cached reply");
    let reply = request.get_response().await;
    println!("Cached reply: {reply:?}");

    #[cfg(feature = "unstable-validator")]
    do_validator(udptcp_conn.clone(), req.clone()).await;

    // Create a new TCP connections object. Pass the destination address and
    // port as parameter.
    let tcp_connect = TcpConnect::new(server_addr);

    // A muli_stream transport connection sets up new TCP connections when
    // needed.
    let (tcp_conn, transport) = multi_stream::Connection::with_config(
        tcp_connect,
        multi_stream_config.clone(),
    );

    // Get a future for the run function. The run function receives
    // the connection stream as a parameter.
    tokio::spawn(async move {
        transport.run().await;
        println!("multi TCP run exited");
    });

    // Send a query message.
    let mut request = tcp_conn.send_request(req.clone());

    // Get the reply. A multi_stream connection does not have any timeout.
    // Wrap get_result in a timeout.
    println!("Wating for multi TCP reply");
    let reply =
        timeout(Duration::from_millis(500), request.get_response()).await;
    println!("multi TCP reply: {reply:?}");

    drop(request);

    // Some TLS boiler plate for the root certificates.
    let root_store = RootCertStore {
        roots: webpki_roots::TLS_SERVER_ROOTS.into(),
    };

    // TLS config
    let client_config = ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_no_client_auth();

    // Currently the only support TLS connections are the ones that have a
    // valid certificate. Use a well known public resolver.
    let google_server_addr =
        SocketAddr::new(IpAddr::from_str("8.8.8.8").unwrap(), 853);

    // Create a new TLS connections object. We pass the TLS config, the name of
    // the remote server and the destination address and port.
    let tls_connect = TlsConnect::new(
        client_config,
        "dns.google".try_into().unwrap(),
        google_server_addr,
    );

    // Again create a multi_stream transport connection.
    let (tls_conn, transport) = multi_stream::Connection::with_config(
        tls_connect,
        multi_stream_config,
    );

    // Start the run function.
    tokio::spawn(async move {
        transport.run().await;
        println!("TLS run exited");
    });

    let mut request = tls_conn.send_request(req.clone());
    println!("Wating for TLS reply");
    let reply =
        timeout(Duration::from_millis(500), request.get_response()).await;
    println!("TLS reply: {reply:?}");

    drop(request);

    // Create a transport connection for redundant connections.
    let (redun, transp) = redundant::Connection::new();

    // Start the run function on a separate task.
    let run_fut = transp.run();
    tokio::spawn(async move {
        run_fut.await;
        println!("redundant run terminated");
    });

    // Add the previously created transports.
    redun.add(Box::new(udptcp_conn)).await.unwrap();
    redun.add(Box::new(tcp_conn)).await.unwrap();
    redun.add(Box::new(tls_conn)).await.unwrap();

    // Start a few queries.
    for i in 1..10 {
        let mut request = redun.send_request(req.clone());
        let reply = request.get_response().await;
        if i == 2 {
            println!("redundant connection reply: {reply:?}");
        }
    }

    drop(redun);

    // Create a new datagram transport connection. Pass the destination address
    // and port as parameter. This transport does not retry over TCP if the
    // reply is truncated. This transport does not have a separate run
    // function.
    let udp_connect = UdpConnect::new(server_addr);
    let dgram_conn =
        dgram::Connection::with_config(udp_connect, dgram_config);

    // Send a message.
    let mut request = dgram_conn.send_request(req.clone());
    //
    // Get the reply
    let reply = request.get_response().await;
    println!("Dgram reply: {reply:?}");

    // Create a single TCP transport connection. This is useful for a
    // single request or a small burst of requests.
    let tcp_conn = match TcpStream::connect(server_addr).await {
        Ok(conn) => conn,
        Err(err) => {
            println!(
                "TCP Connection to {server_addr} failed: {err}, exiting",
            );
            return;
        }
    };

    let (tcp, transport) =
        stream::Connection::<_, RequestMessageMulti<Vec<u8>>>::new(tcp_conn);
    tokio::spawn(async move {
        transport.run().await;
        println!("single TCP run terminated");
    });

    // Send a request message.
    let mut request = SendRequest::send_request(&tcp, req.clone());

    // Get the reply
    let reply = request.get_response().await;
    println!("TCP reply: {reply:?}");

    drop(tcp);

    #[cfg(feature = "tsig")]
    {
        let tcp_conn = TcpStream::connect(server_addr).await.unwrap();
        let (tcp, transport) =
            stream::Connection::<RequestMessage<Vec<u8>>, _>::new(tcp_conn);
        tokio::spawn(async move {
            transport.run().await;
            println!("single TSIG TCP run terminated");
        });

        let mut msg = MessageBuilder::new_vec();
        msg.header_mut().set_rd(true);
        msg.header_mut().set_ad(true);
        let mut msg = msg.question();
        msg.push((Name::vec_from_str("example.com").unwrap(), Rtype::AXFR))
            .unwrap();
        let req = RequestMessageMulti::new(msg).unwrap();

        do_tsig(tcp.clone(), req).await;

        drop(tcp);
    }
}

#[cfg(feature = "unstable-validator")]
async fn do_validator<Octs, SR>(conn: SR, req: RequestMessage<Octs>)
where
    Octs: AsRef<[u8]>
        + Clone
        + std::fmt::Debug
        + domain::dep::octseq::Octets
        + domain::dep::octseq::OctetsFrom<Vec<u8>>
        + Send
        + Sync
        + 'static,
    <Octs as domain::dep::octseq::OctetsFrom<Vec<u8>>>::Error:
        std::fmt::Debug,
    SR: Clone + SendRequest<RequestMessage<Octs>> + Send + Sync + 'static,
{
    // Create a validating transport
    let anchor_file = std::fs::File::open("examples/root.key").unwrap();
    let ta =
        domain::validator::anchor::TrustAnchors::from_reader(anchor_file)
            .unwrap();
    let vc = Arc::new(domain::validator::context::ValidationContext::new(
        ta,
        conn.clone(),
    ));
    let val_conn = domain::net::client::validator::Connection::new(conn, vc);

    // Send a query message.
    let mut request = val_conn.send_request(req);

    // Get the reply
    println!("Wating for Validator reply");
    let reply = request.get_response().await;
    println!("Validator reply: {:?}", reply);
}

#[cfg(feature = "tsig")]
async fn do_tsig<Octs, SR>(conn: SR, req: RequestMessageMulti<Octs>)
where
    Octs: AsRef<[u8]>
        + Send
        + Sync
        + std::fmt::Debug
        + domain::dep::octseq::Octets
        + 'static,
    SR: SendRequestMulti<
            tsig::RequestMessage<RequestMessageMulti<Octs>, Arc<Key>>,
        > + Send
        + Sync
        + 'static,
{
    // Create a signing key.
    let key_name = KeyName::from_str("demo-key").unwrap();
    let secret = domain::utils::base64::decode::<Vec<u8>>(
        "zlCZbVJPIhobIs1gJNQfrsS3xCxxsR9pMUrGwG8OgG8=",
    )
    .unwrap();
    let key = Arc::new(
        Key::new(Algorithm::Sha256, &secret, key_name, None, None).unwrap(),
    );

    // Create a signing transport. This assumes that the server being
    // connected to is configured with a key with the same name, algorithm and
    // secret and to allow that key to be used for the request we are making.
    let tsig_conn = tsig::Connection::new(key, conn);

    // Send a query message.
    let mut request = tsig_conn.send_request(req);

    // Get the reply
    loop {
        println!("Waiting for signed reply");
        let reply = request.get_response()
                .await
                .expect("Failed while getting a TSIG signed response. This is probably expected as the server will not know the TSIG key we are using unless you have ensured that is the case.");
        match reply {
            Some(reply) => {
                println!("Signed reply: {:?}", reply);
            }
            None => break,
        }
    }
}

Modules§

  • A client cache.
  • A client over datagram protocols.
  • A UDP transport that falls back to TCP if the reply is truncated
  • A DNS over multiple octet streams transport
  • Underlying transport protocols.
  • A transport that multiplexes requests over multiple redundant transports.
  • Constructing and sending requests.
  • A client transport using a stream socket.