Get DNS server addresses for each interface on iOS

Hello,


I would like to get the DNS servers for each interface on an iOS device. Currently I am using libresolv, however this only returns the DNS server of the default route. So if WiFi is connected, I will not get the DNS servers for the cellular interface. I couldn't find any API on iOS that does this, (if I'm wrong please let me know - that would be awesome!)


It would seem that the new Network API in iOS12 looks promising. There seems to be a function that is not documented in the header file


nw_path_get_dns_servers


It would seem that getting the DNS servers for each interface would be a matter of creating a nw_path_monitor_t object to monitor network changes. An update handler would be called with a nw_path_t object. nw_path_t has a bunch of exposed functions - nw_path_has_dns, nw_path_has_ipv6, etc. however the function nw_path_get_dns_servers is not available, but looks like it could offer up the dns servers for the path/interface.


Why would I want this information? IPv6. If the cell radio has an IPv6 dns server, I'll need to use that DNS server to resolve the IP address (to use the NAT64 provided by the cell company).


The product we have running on the iPhone is written in C++ and is multiplatform (runs on Windows, Linux, BSD, iOS, MacOS, Android, etc.) Our existing code handles all the dns lookups, source IP routing, etc. The only thing missing is to supply the dns servers for each interface. This wasn't really too important when we were dealing with all the interfaces having IPv4 addresses, but now with IPv6 this changes everything.

If the cell radio has an IPv6 dns server, I'll need to use that DNS server to resolve the IP address (to use the NAT64 provided by the cell company).

Let me clarify your goals here. Given the above scenario, if you pass the target DNS name to the system DNS resolver does it resolve properly?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for replying, please allow me to clarify, with a little more preamble:


The product we make is an appliance (we make both the hardware and software) that encodes broadcast quality video and transmits live video across multiple internet connections (up to 6 cell radios, ethernet, wifi, satellite). By blending the connectivity of all the connections we can use the aggregate bandwidth while maintaining a low latency (as low as 800ms) and quality (congestion avoidance). Our product is used by all major TV broadcasters to get live video from the field back to the TV station for on air broadcasts.


Since our software is platform independent we have ported the software to the iOS platform. Our customers love it and the iPhone is used for quick breaking news when camera crews haven't arrived yet. By blending multiple connections (cell radio & wifi which could be connected to a personal hotspot for a second cell radio) the iPhone can transmit low latency, 720p/1080p video, and the quality of the iPhone camera is so good that most people don't notice the live video is coming from an iPhone and not a traditional broadcast TV camera.


All of our networking code from interface discovery, dns lookups, reachability, bandwidth/latency/congestion prediction and data transmission is done at a low level API (in the case of the iPhone, BSD sockets). We use multiple platform independent libraries, such as c-ares for DNS lookups, so the bulk of the code we write is not for any particular platform. The amount of code specific to a platform is kept to a minimum so we can have a large common code base.


One of the platform specific bits of code is retrieving the DNS servers that were assigned to a specific interface (usually done by the DHCP client). We use this information to tell c-ares to resolve the destination IP on a per interface level. There is a remote possibility that an interface will have a different IP address with IPv4, but with IPv6 it will always be different if the connection is from an IPv6 only interface to a destination that is IPv4 (because of the NAT64 on the IPv6 network).


This is the heart of our code - we use source IP routing (by binding a socket to a source IP and destination IP) to control which interface each packet is sent so we can blend all the interfaces to create an aggregate connection to a remote server that concentrates the multiple connections back into a single video stream.


So to summarize, we are using all available connections simultaneously to transmit live video.


---------------


The easy solution to get blended connections working on the iPhone with IPv6 is to get the DNS servers that were assigned to the interface. It would seem that the new Network API has an undocumented function available (based on the name of the function - nw_path_get_dns_servers).


So the question is - is it possible to get the DNS servers for each interface? If it is possible, but only with the new Network API and the undocumented function, can we get the functionality by documenting it (put in the header file).

please allow me to clarify

Thanks for all that fascinating info about your product, but you didn’t answer my specific question: If you pass the target DNS name to the system DNS resolver does it resolve properly?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Will do some tests with DNSServiceGetAddrInfo. It has been awhile since we looked at it.

So I did some tests with DNSServiceGetAddrInfo, but then realized we need to get SRV records as well. So I performed some initial tests using DNSServiceQueryRecord. Here is the sample code I used for the test-

#import <dns_sd.h>
#include <stdio.h>
#include <string.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <dns_util.h>

// no memory management, all vars valid for the lifetime of the app
static FILE *logFile;
static struct ifaddrs *ifaddr;
#pragma pack(push, 1)
typedef struct resource_record
{
    uint8_t zero;
    uint16_t rrtype;
    uint16_t rrclass;
    uint32_t ttl;
    uint16_t rdlen;
    char rdata[1];
} resource_record;
#pragma pack(pop)

void dnsQueryRecordCallback (DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context)
{
    fprintf(logFile, "Response from index:%d error:%d context:%s", interfaceIndex, errorCode, context);

    uint32_t resource_size = sizeof(resource_record) + rdlen;
    resource_record *resource = (resource_record *)calloc(resource_size, 1);
    resource->zero = 0;
    resource->rrtype = htons(rrtype);
    resource->rrclass = htons(rrclass);
    resource->ttl = htonl(ttl);
    resource->rdlen = htons(rdlen);
    memcpy(resource->rdata, rdata, rdlen);

    dns_resource_record_t * rr = dns_parse_resource_record((const char *)resource, resource_size);
    if (rr)
    {
        switch(rrtype)
        {
            case kDNSServiceType_A:
            {
                char buf[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &(rr->data.A->addr.s_addr), buf, INET_ADDRSTRLEN);
                fprintf(logFile, " IPv4:%s", buf);

            }
                break;
            case kDNSServiceType_AAAA:
            {
                char buf[INET6_ADDRSTRLEN];
                inet_ntop(AF_INET6, &(rr->data.AAAA->addr.s6_addr), buf, INET6_ADDRSTRLEN);
                fprintf(logFile, " IPv6:%s", buf);
            }
                break;
            case kDNSServiceType_SRV:
                fprintf(logFile, " target:%s port:%d priority:%d weight:%d", rr->data.SRV->target, rr->data.SRV->port, rr->data.SRV->priority, rr->data.SRV->weight);
                break;
            default:
                fprintf(logFile, " type:%u", rrtype);
                break;
        }
    }

    fprintf(logFile, "\n");
}

void logQuery(const char *fqdn, uint16_t rrtype)
{
    fprintf(logFile, "Querying %s ", fqdn);
    switch (rrtype)
    {
        case kDNSServiceType_A:
            fprintf(logFile, "A");
            break;
        case kDNSServiceType_AAAA:
            fprintf(logFile, "AAAA");
            break;
        case kDNSServiceType_SRV:
            fprintf(logFile, "SRV");
            break;
        default:
            fprintf(logFile, "%u", rrtype);
            break;
    }
    fprintf(logFile, " Record on ");
}

void dnsQueryRecord(FILE *outLogFile, const char *fqdn, uint16_t rrtype, bool queryAny, bool queryByIndex)
{
    logFile = outLogFile ? outLogFile : stdout;
    rrtype = rrtype == 0 ? kDNSServiceType_AAAA : rrtype;
    logQuery(fqdn, rrtype);

    if (queryAny)
    {
        DNSServiceRef sdRefAll;
        DNSServiceErrorType error;
        if ((error = DNSServiceQueryRecord(&sdRefAll,
                                           0,
                                           0,
                                           fqdn,
                                           rrtype,
                                           kDNSServiceClass_IN,
                                           dnsQueryRecordCallback,
                                           "Any")) == kDNSServiceErr_NoError)
        {
            DNSServiceSetDispatchQueue(sdRefAll, dispatch_get_main_queue());
        }
        fprintf(logFile, "Any retcode:%d\n", error);
    }

    if (queryByIndex)
    {
        if (getifaddrs(&ifaddr) != -1)
        {
            struct ifaddrs *ifa;
            char *last_if_name = "";
            for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
            {
                if (strcmp(last_if_name, ifa->ifa_name) != 0)
                {
                    logQuery(fqdn, rrtype);

                    last_if_name = ifa->ifa_name;
                    unsigned int index = if_nametoindex(ifa->ifa_name);
                    unsigned short flags = ifa->ifa_flags;
                    fprintf(logFile, "index:%d name:%s", index, ifa->ifa_name);

                    int fd = socket(AF_INET, SOCK_DGRAM, 0);
                    if (fd != -1)
                    {
                        struct ifreq ifr;
                        ifr.ifr_addr.sa_family = AF_INET;
                        strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1);
                        if (ioctl(fd, SIOCGIFFLAGS, &ifr) != -1)
                        {
                            flags = ifr.ifr_flags;
                        }
                        close(fd);
                    }
                    fprintf(logFile, " isUp:%s", flags & IFF_UP ? "Yes" : "No");

                    if (flags & IFF_UP)
                    {

                        DNSServiceRef sdRef;
                        DNSServiceErrorType error;
                        if ((error = DNSServiceQueryRecord(&sdRef,
                                                           0,
                                                           index,
                                                           fqdn,
                                                           rrtype,
                                                           kDNSServiceClass_IN,
                                                           dnsQueryRecordCallback,
                                                           ifa->ifa_name)) == kDNSServiceErr_NoError)
                        {
                            DNSServiceSetDispatchQueue(sdRef, dispatch_get_main_queue());
                            fprintf(logFile, " start");
                        }
                        fprintf(logFile, " retcode:%d", error);
                    }
                    fprintf(logFile, "\n");
                }
            }
        }
    }
}


My initial tests where done with an IPv6 address on Cell and IPv4 address on Wifi. I made two different calls


dnsQueryRecord(NULL, "apple.com", 0, true, true);


This will return the AAAA records for Any as well as all interfaces that are up. Since apple.com doesn't have a AAAA record, it will return a synthesized record for the NAT64. The results were-

Querying apple.com AAAA Record on Any retcode:0

Querying apple.com AAAA Record on index:1 name:lo0 isUp:Yes retcode:-65540

Querying apple.com AAAA Record on index:2 name:pdp_ip0 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:3 name:pdp_ip3 isUp:No

Querying apple.com AAAA Record on index:4 name:pdp_ip1 isUp:No

Querying apple.com AAAA Record on index:5 name:pdp_ip2 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:6 name:pdp_ip4 isUp:No

Querying apple.com AAAA Record on index:7 name:ap1 isUp:No

Querying apple.com AAAA Record on index:8 name:en0 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:9 name:en1 isUp:No

Querying apple.com AAAA Record on index:10 name:en3 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:11 name:awdl0 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:12 name:ipsec0 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:13 name:ipsec1 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:14 name:ipsec2 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:15 name:utun0 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:16 name:ipsec3 isUp:Yes start retcode:0

Querying apple.com AAAA Record on index:17 name:ipsec4 isUp:Yes start retcode:0

Response from index:0 error:0 context:pdp_ip0 IPv6:2604:5580:22::118e:a03b

Response from index:0 error:0 context:pdp_ip0 IPv6:2604:5580:22::11ac:e02f

Response from index:0 error:0 context:pdp_ip0 IPv6:2604:5580:22::11b2:603b


So the response came back from only the IPv6 interface. Take note: the value interfaceIndex in the callback is always zero, but the documentation states that it should be the index of interface responding (in this case 2). I noticed the same behavior with DNSServiceGetAddrInfo as well that the passed value is always 0.


Next I called with: (which queries the A record)


dnsQueryRecord(NULL, "apple.com", kDNSServiceType_A, true, true);


Querying apple.com A Record on index:3 name:pdp_ip3 isUp:No

Querying apple.com A Record on index:4 name:pdp_ip1 isUp:No

Querying apple.com A Record on index:5 name:pdp_ip2 isUp:Yes start retcode:0

Querying apple.com A Record on index:6 name:pdp_ip4 isUp:No

Querying apple.com A Record on index:7 name:ap1 isUp:No

Querying apple.com A Record on index:8 name:en0 isUp:Yes start retcode:0

Querying apple.com A Record on index:9 name:en1 isUp:No

Querying apple.com A Record on index:10 name:en3 isUp:Yes start retcode:0

Querying apple.com A Record on index:11 name:awdl0 isUp:Yes start retcode:0

Querying apple.com A Record on index:12 name:ipsec0 isUp:Yes start retcode:0

Querying apple.com A Record on index:13 name:ipsec1 isUp:Yes start retcode:0

Querying apple.com A Record on index:14 name:ipsec2 isUp:Yes start retcode:0

Querying apple.com A Record on index:15 name:utun0 isUp:Yes start retcode:0

Querying apple.com A Record on index:16 name:ipsec3 isUp:Yes start retcode:0

Querying apple.com A Record on index:17 name:ipsec4 isUp:Yes start retcode:0

Response from index:0 error:0 context:Any IPv4:17.178.96.59

Response from index:0 error:0 context:Any IPv4:17.142.160.59

Response from index:0 error:0 context:Any IPv4:17.172.224.47

Response from index:0 error:0 context:pdp_ip0 IPv4:17.172.224.47

Response from index:0 error:0 context:pdp_ip0 IPv4:17.178.96.59

Response from index:0 error:0 context:pdp_ip0 IPv4:17.142.160.59

Response from index:0 error:0 context:en0 IPv4:17.142.160.59

Response from index:0 error:0 context:en0 IPv4:17.172.224.47

Response from index:0 error:0 context:en0 IPv4:17.178.96.59


This time the Any, cell and WiFi interface responded with an A record. This seems correct. The Any interface seems to be the default route (in this case WiFi). Also the interfaceIndex parameters is always 0 again.


-----------------


So to answer your question, I can use the system DNS to query on a per interface. Except for the interfaceIndex parameter being always zero (this can be worked around by using the context parameter to determine the interface), it seems to be working. I'll do more tests with two IPv6 interfaces to make sure I get proper synthesized AAAA records.


It still would be nice to get the DNS server IPs from the interface as well. Specifically for logging purposes to diagnose network setups where the DNS server could return a different IPv4 address for each interface.

So to answer your question, I can use the system DNS to query on a per interface.

OK, that’s good news.

Coming back to your original question, iOS does not provide APIs to get per-interface DNS configuration information. That’s because we much prefer folks using the system DNS. There are a bunch of reasons for this but the most critical one is that it allows us to evolve DNS resolution over time in order to meet system goals.

In my experience most third-party DNS resolvers are very naïve, simply taking a list of DNS server addresses. Such resolvers are not fit for purpose on iOS. Your resolver is more sophisticated, taking a list of servers per interface, but it still won’t handle various edge cases that the system DNS resolver handles (if you look around for info on

kSCPropNetDNSSupplementalMatchDomains
, aka
SupplementalMatchDomains
, aka the
matchDomains
property of
NEDNSSettings
, you can learn more about this). If you do your own DNS resolution, you will inevitably run into compatibility problems. Even if you had all the info necessary to do it correctly, like you do on macOS, you still have to update your code every time the system DNS changes.

So, I strongly recommend that you use the system DNS resolver.

Finally, I want to clarify one more thing. Earlier you wrote:

There seems to be a function that is not documented in the header file

nw_path_get_dns_servers

I originally interpreted this to mean that

nw_path_get_dns_servers
was listed in the header file but didn’t have any doc comments, but I took another look and saw that by “not documented” you meant “not included”. I want to stress that functions not included in our platform SDK headers are considered private, and using them is not supported by Apple (and is likely to get you rejected by App Review).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Get DNS server addresses for each interface on iOS
 
 
Q