[GH-ISSUE #1653] Custom ConnectionProvider #725

Open
opened 2026-03-16 00:00:15 +03:00 by kerem · 21 comments
Owner

Originally created by @Noah-Kennedy on GitHub (Feb 28, 2022).
Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/1653

Is there a way to write custom connection providers for the AsyncResolver in trust-dns-resolver? It looks like AsyncResolver only implements its methods for GenericConnection and GenericConnectionProvider<TokioRuntime>, which is unfortunate, as those types do not seem to be publicly exposed.

I am looking to attach custom headers (authorization) to DOH requests, as well as mess around with DNS over QUIC.

Originally created by @Noah-Kennedy on GitHub (Feb 28, 2022). Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/1653 Is there a way to write custom connection providers for the `AsyncResolver` in `trust-dns-resolver`? It looks like `AsyncResolver` only implements its methods for `GenericConnection` and `GenericConnectionProvider<TokioRuntime>`, which is unfortunate, as those types do not seem to be publicly exposed. I am looking to attach custom headers (authorization) to DOH requests, as well as mess around with DNS over QUIC.
Author
Owner

@bluejekyll commented on GitHub (Mar 1, 2022):

Theoretically, yes it is possible. If you look at some of the test code, you'll notice more usage patterns for the ConnectionProvider type.

But the last time that this came up, folks have tended to find it simpler to implement the reimplement from the Runtime up, see the AsyncStdRuntime for an example: https://github.com/bluejekyll/trust-dns/blob/main/crates/async-std-resolver/src/runtime.rs

<!-- gh-comment-id:1054846183 --> @bluejekyll commented on GitHub (Mar 1, 2022): Theoretically, yes it is possible. If you look at some of the test code, you'll notice more usage patterns for the `ConnectionProvider` type. But the last time that this came up, folks have tended to find it simpler to implement the reimplement from the `Runtime` up, see the `AsyncStdRuntime` for an example: https://github.com/bluejekyll/trust-dns/blob/main/crates/async-std-resolver/src/runtime.rs
Author
Owner

@bluejekyll commented on GitHub (Mar 1, 2022):

Btw, for DoQ, I've been thinking for a while about implementing that, perhaps @djc has some suggestions? It would be really nice to add that, I would think the DoH implementation would be a good starting point for how to do it: https://github.com/bluejekyll/trust-dns/blob/main/crates/proto/src/https/https_client_stream.rs

<!-- gh-comment-id:1054847396 --> @bluejekyll commented on GitHub (Mar 1, 2022): Btw, for DoQ, I've been thinking for a while about implementing that, perhaps @djc has some suggestions? It would be really nice to add that, I would think the DoH implementation would be a good starting point for how to do it: https://github.com/bluejekyll/trust-dns/blob/main/crates/proto/src/https/https_client_stream.rs
Author
Owner

@djc commented on GitHub (Mar 1, 2022):

I'm guessing @Noah-Kennedy will want to use Quiche as the DoQ substrate? @Noah-Kennedy also if you're looking into building tokio-uring support for trust-dns, that's probably something we'd like to have upstreamed.

<!-- gh-comment-id:1055457266 --> @djc commented on GitHub (Mar 1, 2022): I'm guessing @Noah-Kennedy will want to use Quiche as the DoQ substrate? @Noah-Kennedy also if you're looking into building tokio-uring support for trust-dns, that's probably something we'd like to have upstreamed.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 1, 2022):

@bluejekyll I'd like to start first by trying to implement some custom connection providers in a crate consuming this library. Is this possible to do in a manner which would allow me to leverage the AsyncResolver?

<!-- gh-comment-id:1055643543 --> @Noah-Kennedy commented on GitHub (Mar 1, 2022): @bluejekyll I'd like to start first by trying to implement some custom connection providers in a crate consuming this library. Is this possible to do in a manner which would allow me to leverage the AsyncResolver?
Author
Owner

@bluejekyll commented on GitHub (Mar 1, 2022):

Yes, it's possible. See my first response to you able examples for that. I'm not sure which is your best option, i.e. defining a new Runtime or implementing a custom ConnectionProvider. Here's a mocked ConnectionProvider example that might be simpler to follow than the actual implementations. Since it's implemented as an integration test all the pieces you need should be in the public API: github.com/bluejekyll/trust-dns@aaf726e556/tests/integration-tests/tests/name_server_pool_tests.rs (L37)

<!-- gh-comment-id:1055746518 --> @bluejekyll commented on GitHub (Mar 1, 2022): Yes, it's possible. See my first response to you able examples for that. I'm not sure which is your best option, i.e. defining a new Runtime or implementing a custom ConnectionProvider. Here's a mocked ConnectionProvider example that might be simpler to follow than the actual implementations. Since it's implemented as an integration test all the pieces you need should be in the public API: https://github.com/bluejekyll/trust-dns/blob/aaf726e556aa5a69079c9bb5c32b72f6fb10d38d/tests/integration-tests/tests/name_server_pool_tests.rs#L37
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 1, 2022):

I've implemented a custom connection provider. How do I construct an AsyncResolver which uses it? I don't see anything in those tests which demonstrates how to do this.

<!-- gh-comment-id:1055870234 --> @Noah-Kennedy commented on GitHub (Mar 1, 2022): I've implemented a custom connection provider. How do I construct an `AsyncResolver` which uses it? I don't see anything in those tests which demonstrates how to do this.
Author
Owner

@bluejekyll commented on GitHub (Mar 1, 2022):

Maybe the "trick" is that ConnectionProvider is a static parameter on the declared types, e.g. here we have it bound to GenericConnectionProvider and this new method is used: github.com/bluejekyll/trust-dns@8515a4321f/crates/resolver/src/async_resolver.rs (L164)

Such that when the TokioAsyncResolver calls here: github.com/bluejekyll/trust-dns@8515a4321f/crates/resolver/src/async_resolver.rs (L131)

So the ConnectionProvider is associated through the type bounds on this impl: github.com/bluejekyll/trust-dns@8515a4321f/crates/resolver/src/async_resolver.rs (L148)

And that is GenericConnection, GenericConnectionProvider<R> where R is the Runtime in this case Tokio...

Does that help?

<!-- gh-comment-id:1055925121 --> @bluejekyll commented on GitHub (Mar 1, 2022): Maybe the "trick" is that `ConnectionProvider` is a static parameter on the declared types, e.g. here we have it bound to `GenericConnectionProvider` and this new method is used: https://github.com/bluejekyll/trust-dns/blob/8515a4321f796aaccc84ed1c3954e3f302941d9f/crates/resolver/src/async_resolver.rs#L164 Such that when the TokioAsyncResolver calls here: https://github.com/bluejekyll/trust-dns/blob/8515a4321f796aaccc84ed1c3954e3f302941d9f/crates/resolver/src/async_resolver.rs#L131 So the ConnectionProvider is associated through the type bounds on this impl: https://github.com/bluejekyll/trust-dns/blob/8515a4321f796aaccc84ed1c3954e3f302941d9f/crates/resolver/src/async_resolver.rs#L148 And that is `GenericConnection, GenericConnectionProvider<R>` where `R` is the `Runtime` in this case Tokio... Does that help?
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

Maybe it's best for me to show what I did for my proof of concept intended to show how to use custom providers for the AsyncResolver:

use futures_util::stream::Stream;
use futures_util::FutureExt;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use trust_dns_proto::op::Message;
use trust_dns_proto::serialize::binary::BinEncodable;
use trust_dns_proto::xfer::{DnsRequest, DnsResponse};
use trust_dns_proto::{DnsHandle, TokioTime};
use trust_dns_resolver::config::{NameServerConfig, ResolverOpts};
use trust_dns_resolver::error::ResolveError;
use trust_dns_resolver::ConnectionProvider;

#[derive(Clone)]
pub struct CustomDnsProvider {}

impl ConnectionProvider for CustomDnsProvider {
    type Conn = CustomDnsHandle;
    type FutureConn = CustomDnsConnFuture;
    type Time = TokioTime;

    fn new_connection(
        &self,
        _config: &NameServerConfig,
        _options: &ResolverOpts,
    ) -> Self::FutureConn {
        let (tx, rx) = mpsc::unbounded_channel();

        tokio::spawn(run_backend(rx));

        let inner = tokio::spawn(async move { Ok(CustomDnsHandle { tx }) });

        CustomDnsConnFuture { inner }
    }
}

async fn run_backend(
    mut rx: mpsc::UnboundedReceiver<(
        DnsRequest,
        mpsc::UnboundedSender<Result<DnsResponse, ResolveError>>,
    )>,
) -> Result<(), ResolveError> {
    let mut sock = UdpSocket::bind("0.0.0.0:0").await?;

    sock.connect("1.1.1.1:53").await.unwrap();

    while let Some((req, responder)) = rx.recv().await {
        let res = do_query(&mut sock, req).await;
        responder.send(res).unwrap();
    }

    Ok(())
}

async fn do_query(sock: &mut UdpSocket, req: DnsRequest) -> Result<DnsResponse, ResolveError> {
    let mut buf = vec![0; 4096];
    let encoded = req.to_bytes()?;

    sock.send(&encoded).await;
    let len = sock.recv(&mut buf).await?;

    let msg = Message::from_vec(&buf[..len])?;

    Ok(DnsResponse::from(msg))
}

pub struct CustomDnsConnFuture {
    inner: JoinHandle<Result<CustomDnsHandle, ResolveError>>,
}

impl Future for CustomDnsConnFuture {
    type Output = Result<CustomDnsHandle, ResolveError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.inner.poll_unpin(cx).map(|x| x.unwrap())
    }
}

#[derive(Clone)]
pub struct CustomDnsHandle {
    tx: mpsc::UnboundedSender<(
        DnsRequest,
        mpsc::UnboundedSender<Result<DnsResponse, ResolveError>>,
    )>,
}

pub struct CustomStream<S> {
    rx: mpsc::UnboundedReceiver<S>,
}

impl DnsHandle for CustomDnsHandle {
    type Error = ResolveError;
    type Response = CustomStream<Result<DnsResponse, Self::Error>>;

    fn send<R: Into<DnsRequest> + Unpin + Send + 'static>(&mut self, request: R) -> Self::Response {
        let (tx, rx) = mpsc::unbounded_channel();
        let _ = self.tx.send((request.into(), tx));

        CustomStream { rx }
    }
}

impl<S> Stream for CustomStream<S> {
    type Item = S;

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        self.rx.poll_recv(cx)
    }
}

I implemented a provider and a handle, however I cannot meaningfully use AsyncResolver with them, as I cannot construct an AsyncResolver with any type parameters other than GenericConnection or GenericConnectionProvider, as there are no constructors in trustdns for other type parameters and the fields within AsyncResolver are private.

Is there a different way that I should go about doing this?

<!-- gh-comment-id:1057308245 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): Maybe it's best for me to show what I did for my proof of concept intended to show how to use custom providers for the AsyncResolver: ```rust use futures_util::stream::Stream; use futures_util::FutureExt; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::net::UdpSocket; use tokio::sync::mpsc; use tokio::task::JoinHandle; use trust_dns_proto::op::Message; use trust_dns_proto::serialize::binary::BinEncodable; use trust_dns_proto::xfer::{DnsRequest, DnsResponse}; use trust_dns_proto::{DnsHandle, TokioTime}; use trust_dns_resolver::config::{NameServerConfig, ResolverOpts}; use trust_dns_resolver::error::ResolveError; use trust_dns_resolver::ConnectionProvider; #[derive(Clone)] pub struct CustomDnsProvider {} impl ConnectionProvider for CustomDnsProvider { type Conn = CustomDnsHandle; type FutureConn = CustomDnsConnFuture; type Time = TokioTime; fn new_connection( &self, _config: &NameServerConfig, _options: &ResolverOpts, ) -> Self::FutureConn { let (tx, rx) = mpsc::unbounded_channel(); tokio::spawn(run_backend(rx)); let inner = tokio::spawn(async move { Ok(CustomDnsHandle { tx }) }); CustomDnsConnFuture { inner } } } async fn run_backend( mut rx: mpsc::UnboundedReceiver<( DnsRequest, mpsc::UnboundedSender<Result<DnsResponse, ResolveError>>, )>, ) -> Result<(), ResolveError> { let mut sock = UdpSocket::bind("0.0.0.0:0").await?; sock.connect("1.1.1.1:53").await.unwrap(); while let Some((req, responder)) = rx.recv().await { let res = do_query(&mut sock, req).await; responder.send(res).unwrap(); } Ok(()) } async fn do_query(sock: &mut UdpSocket, req: DnsRequest) -> Result<DnsResponse, ResolveError> { let mut buf = vec![0; 4096]; let encoded = req.to_bytes()?; sock.send(&encoded).await; let len = sock.recv(&mut buf).await?; let msg = Message::from_vec(&buf[..len])?; Ok(DnsResponse::from(msg)) } pub struct CustomDnsConnFuture { inner: JoinHandle<Result<CustomDnsHandle, ResolveError>>, } impl Future for CustomDnsConnFuture { type Output = Result<CustomDnsHandle, ResolveError>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { self.inner.poll_unpin(cx).map(|x| x.unwrap()) } } #[derive(Clone)] pub struct CustomDnsHandle { tx: mpsc::UnboundedSender<( DnsRequest, mpsc::UnboundedSender<Result<DnsResponse, ResolveError>>, )>, } pub struct CustomStream<S> { rx: mpsc::UnboundedReceiver<S>, } impl DnsHandle for CustomDnsHandle { type Error = ResolveError; type Response = CustomStream<Result<DnsResponse, Self::Error>>; fn send<R: Into<DnsRequest> + Unpin + Send + 'static>(&mut self, request: R) -> Self::Response { let (tx, rx) = mpsc::unbounded_channel(); let _ = self.tx.send((request.into(), tx)); CustomStream { rx } } } impl<S> Stream for CustomStream<S> { type Item = S; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { self.rx.poll_recv(cx) } } ``` I implemented a provider and a handle, however I cannot meaningfully use AsyncResolver with them, as I cannot construct an AsyncResolver with any type parameters other than GenericConnection or GenericConnectionProvider, as there are no constructors in trustdns for other type parameters and the fields within AsyncResolver are private. Is there a different way that I should go about doing this?
Author
Owner

@bluejekyll commented on GitHub (Mar 2, 2022):

Ah, sorry, I missed this. I'm not sure why this isn't public (I guess I wasn't sure if people would use it, so didn't want to expose it unnecessarily), you need this constructor:

github.com/bluejekyll/trust-dns@8515a4321f/crates/resolver/src/async_resolver.rs (L212-L216)

or this one:

github.com/bluejekyll/trust-dns@8515a4321f/crates/resolver/src/async_resolver.rs (L262-L265)

Actually, that now has me wondering how the integration tests have access to those methods... Anyway, we'll need to make those public for you.

<!-- gh-comment-id:1057369328 --> @bluejekyll commented on GitHub (Mar 2, 2022): Ah, sorry, I missed this. I'm not sure why this isn't public (I guess I wasn't sure if people would use it, so didn't want to expose it unnecessarily), you need this constructor: https://github.com/bluejekyll/trust-dns/blob/8515a4321f796aaccc84ed1c3954e3f302941d9f/crates/resolver/src/async_resolver.rs#L212-L216 or this one: https://github.com/bluejekyll/trust-dns/blob/8515a4321f796aaccc84ed1c3954e3f302941d9f/crates/resolver/src/async_resolver.rs#L262-L265 Actually, that now has me wondering how the integration tests have access to those methods... Anyway, we'll need to make those public for you.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

Should I file an issue and open a PR for making these public?

<!-- gh-comment-id:1057404096 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): Should I file an issue and open a PR for making these public?
Author
Owner

@djc commented on GitHub (Mar 2, 2022):

Just go straight for the PR!

<!-- gh-comment-id:1057415737 --> @djc commented on GitHub (Mar 2, 2022): Just go straight for the PR!
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

https://github.com/bluejekyll/trust-dns/pull/1654

<!-- gh-comment-id:1057432034 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): https://github.com/bluejekyll/trust-dns/pull/1654
Author
Owner

@bluejekyll commented on GitHub (Mar 2, 2022):

@Noah-Kennedy can you work off of main for your needs or do you need a release for those interfaces?

<!-- gh-comment-id:1057478774 --> @bluejekyll commented on GitHub (Mar 2, 2022): @Noah-Kennedy can you work off of `main` for your needs or do you need a release for those interfaces?
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

Since #1654 is in, I will point my code at master and have a go again.

<!-- gh-comment-id:1057479622 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): Since #1654 is in, I will point my code at master and have a go again.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

At least for the POC.

<!-- gh-comment-id:1057479840 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): At least for the POC.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 2, 2022):

A release would probably be best though IMO, since others may be interested in using custom providers as well.

<!-- gh-comment-id:1057480317 --> @Noah-Kennedy commented on GitHub (Mar 2, 2022): A release would probably be best though IMO, since others may be interested in using custom providers as well.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 11, 2022):

@bluejekyll when does the AsyncResolver create a new connection from a provider?

<!-- gh-comment-id:1065569297 --> @Noah-Kennedy commented on GitHub (Mar 11, 2022): @bluejekyll when does the AsyncResolver create a new connection from a provider?
Author
Owner

@bluejekyll commented on GitHub (Mar 12, 2022):

It should be on first use, ie the first time a query is made and when the connection is disconnected.

are you seeing something unexpected?

<!-- gh-comment-id:1065813920 --> @bluejekyll commented on GitHub (Mar 12, 2022): It should be on first use, ie the first time a query is made and when the connection is disconnected. are you seeing something unexpected?
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 12, 2022):

Nope, just wanted to know if it ever dropped the connections for whatever reason.

<!-- gh-comment-id:1065828178 --> @Noah-Kennedy commented on GitHub (Mar 12, 2022): Nope, just wanted to know if it ever dropped the connections for whatever reason.
Author
Owner

@Noah-Kennedy commented on GitHub (Mar 12, 2022):

What exactly does a "connection" mean in the context of DNS?

<!-- gh-comment-id:1065828586 --> @Noah-Kennedy commented on GitHub (Mar 12, 2022): What exactly does a "connection" mean in the context of DNS?
Author
Owner

@bluejekyll commented on GitHub (Mar 12, 2022):

Depends on the connection type. For UDP it doesn't mean much, an open socket awaiting a response, but a new socket will be used for every request to ensure we are rotating ports (to help reduce DNS response spoofing). For TCP, HTTPS, and TLS, those are long lived and if I remember correctly, we have a timeout that keeps them open for a period of time in case they will be reused.

<!-- gh-comment-id:1065985146 --> @bluejekyll commented on GitHub (Mar 12, 2022): Depends on the connection type. For UDP it doesn't mean much, an open socket awaiting a response, but a new socket will be used for every request to ensure we are rotating ports (to help reduce DNS response spoofing). For TCP, HTTPS, and TLS, those are long lived and if I remember correctly, we have a timeout that keeps them open for a period of time in case they will be reused.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hickory-dns#725
No description provided.