mDNS and DNS-SD for the TRust-DNS Resolver (feedback desired)

For a lot longer than I thought it would take, as all things in software turn out to be :wink: , I've been working on adding multicast DNS to the TRust-DNS libraries. I wrote a post about performing multicast requests in Rust related to this work: Multicasting in Rust. Hopefully others will find that information useful for similar tools.

As to DNS, this work is all need to support multicast DNS. Beyond the additional protocol, I've added the beginnings of a DNS-SD, DNS Service Discovery, implementation. This currently requires the experimental mdns feature to be enabled for both mDNS and DNS-SD to be compiled into the library. DNS-SD does not technically require mDNS and can work against traditional DNS nodes, but mDNS does require DNS-SD for some of it's more interesting features (at some future point we can untether DNS-SD from the mdns feature). Here's the trait for the DNS-SD extension:

/// An extension for the Resolver to perform DNS Service Discovery
pub trait DnsSdFuture {
    /// List all services available
    ///
    /// https://tools.ietf.org/html/rfc6763#section-4.1
    ///
    /// For registered service types, see: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
    fn list_services<N: IntoName>(&self, name: N) -> ListServicesFuture;

    /// Retrieve service information
    ///
    /// https://tools.ietf.org/html/rfc6763#section-6
    fn service_info<N: IntoName>(&self, name: N) -> ServiceInfoFuture;
}

I've put a single "test" in the library, it can be run with this and if you have any mDNS devices on your network you may get results:

$> cd resolver
$> cargo test --features mdns test_list_services -- --nocapture --ignored
...
running 1 test
service: not\040real\040name._http._tcp.local.
service: SRV {
    priority: 0,
    weight: 0,
    port: 80,
    target: Name {
        is_fqdn: true,
        labels: [
            DVR-89B6,
            local
        ]
    }
}
info: {
    "platform": Some(
        "tcd/Series4"
    ),
    "path": Some(
        "/index.html"
    ),
    "swversion": Some(
        "1.2.3.4"
    ),
    "TSN": Some(
        "DEADBEEF"
    )
}
ip: 198.51.100.1
test dns_sd::tests::test_list_services ... ok
...

The result above is from the TiVo on my network (yes, I still have a TiVo, don't ask). I've changed some of the private information to bogus details for this post, so some of those things are fake. Breaking this down here's the test:

    fn test_list_services() {
        let mut io_loop = Core::new().unwrap();

We create a Resolver like normal:

        let resolver = ResolverFuture::new(
            ResolverConfig::default(),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv6thenIpv4,
                ..ResolverOpts::default()
            },
            &io_loop.handle(),
        );

Then we run the new list_services function to find all services, this actually performs a PTR lookup. It uses the default timeout to wait for responses, and then returns any that were found:

        let response = io_loop
            .run(resolver.list_services("_http._tcp.local."))
            .expect("failed to run lookup");

        for name in response.iter() {
            println!("service: {}", name);

Now lookup the service's connection info with the new lookup_srv function (described below):

            let srvs = io_loop
                .run(resolver.lookup_srv(name))
                .expect("failed to lookup name");

            for srv in srvs.iter() {
                println!("service: {:#?}", srv);

Now lookup the service's information with the new service_info function:


                let info = io_loop
                    .run(resolver.service_info(name))
                    .expect("info failed");

The result of the service lookup returns an type that can return the output as a HashMap:

                let info = info.to_map();
                println!("info: {:#?}", info);
            }

            for ip in srvs.ip_iter() {
                println!("ip: {}", ip);
            }
        }
    }

And that's it, mDNS with DNS-SD. Now this isn't a full implementation of the spec. To get here required a lot of work, PR#363. There were many underlying refactorings required in the library. For example, the protocols all used to immediately return on the first response. With the new DnsRequestOptions object passed into the trust-dns-proto requests, we optionally specify to allow more than one DnsResponse. DnsRequest and DnsResponse were added to the trust-dns-proto library, which helps clarify some areas of code where just Message was being used for inbound and outbound Messages. Those changes are in addition to the multicast protocol addition. In theory this will also allow for single multi-query DnsRequests to be sent and received, but that's not implemented yet (this will allow the SRV and TXT lookups to be combined into a single request).

In addition to that, to make the the SRV response processing better for the lookup_srv, the DnsResponse will be searched for IPs matching the target of SRV record. As of now this will not perform a recursive query, as CNAME does, some additional thought needs to be put into when to perform an A or AAAA lookup. The response Message should have this associated, so in most cases this does not matter, but libraries using the Resolver should be aware of this case. This is a general improvement on all SRV lookups in the library, the new ip_iter function on SrvLookup will provide access to these IPs. Also, I've deprecated the lookup_service and srv_lookup functions on ResolverFuture as I realized those were naively and overly simplistic in favor of the new lookup_srv implementation.

Why am I writing this post? I'd love some feedback on these implementations. I plan to land this as is in the master branch (after I review the code), and subsequently release in 0.9 (still feature flagged off) when that's ready. Also, I think I've temporarily reached my limit with with mDNS and DNS-SD, so if others are interested in picking up the torch from here, I'd happily try to help mentor any additional changes.

For now, I think I'm going to switch gears and finalize TLS support in the Resolver since the creation of 1.1.1.1.

Thanks!

8 Likes

Great work! However you'll want an asynchronous API for service discovery. The synchronous list_services with a timeout will cause poor user experience (need to wait for the timeout even if device was discovered in a few milliseconds) and poor reliability (if the device responds right after the end of the timeout).

2 Likes

Thank you!

This is why I love getting feedback! I hadn't considered returning a stream vs. just a single DnsResponse object with all the Messages. What I did was a very straightforward conversion from the single message to the response message and so I ended up thinking about it from that perspective. This would also be useful for the long-lived queries RFC that I'd like to implement at some point.

This would only effect the list_services query, as the service_info query returns immediately on the first response. Thanks for the recommendation, I've filed this issue for tracking: https://github.com/bluejekyll/trust-dns/issues/383

1 Like

I think that's a great idea!

Note that you'll generally want an asynchronous interface for regular DNS as well (even without long-lived queries) since the A and AAAA responses arrive as separate datagrams, and clients would need to receive them as soon as they arrive in order to implement things like Happy Eyeballs v2. Also you should have LookupIpStrategy:: Ipv4AndIpv6 be the default - the most efficient use of DNS is always to send both queries out in parallel and return results as they come.

And one last thing: the stream would need a way to remove results that it had previously given you. This can happen when an mDNS node goes away or you receive a long-lived query remove event.

Yes, for the LookupIp, this would be valuable as well. Thanks for the feedback.

This was originally implemented with the Ipv4AndIpv6 as the default, and that does happen in parallel, though both are waited for instead of the streaming API you're suggesting. The issue that people seemed to have was that it caused issues for some in the order of the returned IP's. So the default was switched to be Ipv4ThenIpv6 so that it was more consistent.

Here's the conversation around that, if you want to reopen the discussion we can.