Why does NSHost.names only return one result?

Hi all,


for a router log analysis programme I'm currently (re-)developing I'd like to use the NSHost class for introspection of IP addresses getting the host name(s). The NSHost class comes in really handy as it supports a very simple and elegant way to obtain the host names for a given IP address. After creating an instance with the initialiser using an IP address we get the opportunity to get either one (representative) host name, or all host names as an array of NSStrings/Strings.


However, when executing the following lines in a playground the result is a little disappointing:

import Cocoa
let host1 = NSHost(address: "17.172.224.47")
host1.names.count // outputs 1, but there are actually 119 host names associated to this IP address
host1.names       // outputs any one of the 119 host names associated with this IP address


On the other hand, solving the addresses for a given host name always ends in the expected result:

import Cocoa
let host2 = NSHost(name: "apple.com")
host2.addresses.count // outputs 3
host2.addresses       // outputs ["17.142.160.59", "17.172.224.47", "17.178.96.59"]


Has anyone experienced the same behaviour or can someone explain why .names does not return an array with the correct number of host names?


Thanks in advance for any comment or suggestion.


Cheers,


Mati

Answered by DTS Engineer in 47736022

There’s a long explanation here but the short answer is that NSHost is not the droid you’re looking for (NSHost is not really the droid anyone is looking for, which is why it’s not part of the iOS SDK). Rather:

  • If you want to a simple interface to the DNS, use CFHost.

  • If you want more control, use DNS-SD (

    <dns_sd.h>
    ).

Share and Enjoy

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

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

There’s a long explanation here but the short answer is that NSHost is not the droid you’re looking for (NSHost is not really the droid anyone is looking for, which is why it’s not part of the iOS SDK). Rather:

  • If you want to a simple interface to the DNS, use CFHost.

  • If you want more control, use DNS-SD (

    <dns_sd.h>
    ).

Share and Enjoy

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

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

Thanks, eskimo, for your honest answer. I really wished to avoid using CFHost or DNS-SD with all the nasty C-Swift-bridging and UnsafePointer business, but it seems that I'm left alone with this solution.


For my old Objective-C-based application I used a "clean" C implementation for the reverse lookup using arpa/inet.h and netdb.h, but for more complete results it seems I have to go the CFHost path anyway. So I'd better pull myself together and get a deep understanding of C-bridging in Swift and how to use UnsafePointer with sockaddr structs. 😉


Currently I'm struggling with bus errors (aka BAD EXEC exceptions) as my pointers fail to be prepared corretly as it seems. Using Core Foundation with pointers is clearly doing away with the beauty of Swift's strong typing and elegant call-by-value and call-by-reference implementation.


So, back to C—again ... 😢


Cheers,


Mati

Honestly, your best option here is to use a thin Objective-C wrapper around CFHost. You can access CFHost directly from Swift (see below) but the whole process is kinda ugly.

As far as calling CFHost directly from Swift, here’s an example:

func startResolve(addressStr: String) {

    // The copyDescription member of the CFHostClientContext struct is not marked as __nullable
    // <rdar://problem/22574088>, so Swift won't let us pass in nil, so we have a second unsafeBitCast
    // in the initialisation below.

    var context = CFHostClientContext(
        version: 0,
        info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self),
        retain: nil,
        release: nil,
        copyDescription: unsafeBitCast(0, CFAllocatorCopyDescriptionCallBack.self)
    )

    self.host = CFHostCreateWithName(nil, addressStr as CFString).takeRetainedValue()
    CFHostSetClient(self.host!, { (host, infoType, unsafeStreamError, info) in
        let obj = unsafeBitCast(info, AppDelegate.self)
        obj.resolveDidFinishWithStreamError(unsafeStreamError.memory)
    }, &context)
    CFHostScheduleWithRunLoop(self.host!, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

    var streamError = CFStreamError()
    let success = CFHostStartInfoResolution(self.host!, .Addresses, &streamError)
    if !success {
        NSLog("could not start resolution: %d / %d", streamError.domain, streamError.error)
        self.stopResolve()
    }
}

func resolveDidFinishWithStreamError(streamError: CFStreamError) {
    if streamError.domain == 0 && streamError.error == 0 {
        let addresses = CFHostGetAddressing(self.host!, nil)!.takeUnretainedValue() as NSArray as! [NSData]
        NSLog("resolution succeeded:")
        for address in addresses {
            NSLog("  %@", stringForIPAddress(address))
        }
    } else {
        NSLog("resolution failed: %d / %d", streamError.domain, streamError.error)
    }
    self.stopResolve()
}

func stopResolve() {
    CFHostSetClient(self.host!, nil, nil);
    CFHostCancelInfoResolution(self.host!, .Addresses)
    CFHostUnscheduleFromRunLoop(self.host!, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)
    self.host = nil;
}

IMPORTANT This code was tested with Xcode 7.0b6. Critically, it requires Swift 2 because that introduced the

@convention(c)
support. Older versions of Swift cannot call CFHost directly.

The code relies on the following helper function:

func stringForIPAddress(address: NSData) -> String {
    var buf = Array<Int8>(count: Int(NI_MAXHOST), repeatedValue: 0)
    let success = getnameinfo(
        UnsafePointer<sockaddr>(address.bytes), socklen_t(address.length),
        &buf, socklen_t(buf.count),
        nil, 0,
        NI_NUMERICHOST | NI_NUMERICSERV
    ) == 0
    assert(success)
    return String.fromCString(buf)!
}

Note that this uses

getnameinfo
in a synchronous fashion, but that’s safe because it’s just dealing with IP address strings and
NI_NUMERICHOST
flag ensures it’ll never hit the network.

Share and Enjoy

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

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

Oh yeah, and feel free to file an enhancement request for a Cocoa-y DNS API to replace NSHost. It’s been on the list of things to do for a while now, but it’s always good to hear from real developers about their requirements.

And if you can suggest a good name for the class, that’d be fab! It’s annoying that NSHost has ‘stolen’ the obvious name.

If you do file a bug, please post your bug number, just for the record.

Share and Enjoy

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

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

Thanks, eskimo, for the supplied code which does not resemble much Swift but seems to be the only solution—for now. 😐 I will modify it to do an IP address reverse lookup and see if the result is what I expect which nobody can tell me right now. I'll submit the code for other developers' reference as soon as I got it working.


Regarding the idea for an enhancement request—I just posted a request with the following bug tracking ID: 22576570


Regards,


Mati

As NSHost does not the trick I came up with two variants that were proposed to work the way I want. But only Variant 1 actually does the trick, and with even less code than Variant 2.


Both variants are thought to be run in a Swift playgound.


Variant 1: Using Goode Olde gethostbyaddr()


//: # How to retrieve a host name and associated aliases from an IP address
//: Rename the file extension to `playground` and run it directly in Xcode 7 with Swift 2.
import Cocoa
//: ## Using the C `struct`s in Swift
//: We can safely use a Swift `String` for storing the IP address in charachter format as Swift supports toll-fee bridging to C strings.
let ip = "17.172.224.47"
//: In order to use the `hostent` C structure as a reference value (pointer) we have to declare it as an `UnsafeMutablePointer` of the corresponding type.
//: We use `let` as the value itself will never change, but only the reference to the value. As such, the value (not the reference, or pointer for that matter) will be a constant.
let he: UnsafeMutablePointer<hostent>
//: We can declare another constant of type `hostent` which will be type-compatible with the memory location of `he`'s pointer to the C structure.
let he2: hostent
//: We create an empty fully initialized `in_addr` C structure by using the convenience initializer.
var addr: in_addr = in_addr()
/*: Now, the following declaration of a constant is a bit awkward. It uses Swift's C-bridging types to denote a `char **` which represents a pointer to a list of `char` pointers, which in turn represents an array of C strings.
Swift translates the C `char` to a `CChar` and the `*` pointer to a `UnsafeMutablePointer`, so the notation
`char **`
is mapped by reading right-to-left to
UnsafeMutablePointer<UnsafeMutablePointer<CChar>>
*/
let host_list: UnsafeMutablePointer<UnsafeMutablePointer<CChar>>
//: To store the host names in a plain Swift array of `String`s we use the following variable.
var host_aliases: [String] = []
//: ## Using the C Standard Library
//: The C Standard Library function `inet_pton` converts an IP address from a string representation to a network number. `AF_INET` tells the function to expect an IP4 address.
//: Caveat: Some functions require the `AF_…` constants to be wrapped around an `sa_family_t()` type cast.
inet_pton(AF_INET, ip, &addr)
//: Now we can all the legacy `gethostbyaddr` C function which returns a pointer to a `hostent` structure.
he = gethostbyaddr(&addr, UInt32(sizeof(in_addr)), AF_INET)
//: For convenience we use the `memory` member of the `he` structure to assign the value of the structure to the `he2` constant. This leaves us with shorter member access names and also shows the type conversion between Swift's C-bridging pointer types and `struct` types.
he2 = he.memory
//: The first resolved host name is present as an optional C string in `hostent`'s member `h_name`.
//: Just print it out, and then append it to the `host_aliases` array of `String`s.
print("First canonical host name: \(String.fromCString(he2.h_name)!)")
host_aliases.append(String.fromCString(he2.h_name)!)
//: Again for convenience, we assign the pointer to the list of C strings containing the alias names of the host to a constant name.
host_list = he2.h_aliases
//: ## Converting the list of C strings to a Swift `String` array
//: We use a zero-initialized Int for looping over the list of C strings.
var i = 0
/*: This is the beauty of Swift's toll-free C-bridging at a max: We can simply refer to the list of C strings using the standard Swift array accessor.
Combined with the conditional optional unwrapping directly in the `while` loop we get a very concise expression for
iterating over the NULL-terminated list, converting the C strings into `String`s and and packing the `String`s into the Swift array of `Strings`.
Notice the `++i` implicit incrementation: We skip the first alias name as it represents the RARP address pointer (`47.224.172.17.in-addr.arpa`) which is of no use to us.
*/
while let h = String.fromCString(host_list[++i]) {
    host_aliases.append(h)
}
//: ## The result
//: Having now a Swift array of `Strings` filled up with the host names, working with it in either the Playground or in code is nice and clean.
print("Found \(host_aliases.count) hosts for IP address \(ip):")
for host in host_aliases {
    print(host)
}


Variant 2: Using Core Foundation's CFHost


//: # How to retrieve a host name and associated aliases from an IP address using Core Fondation's `CFHost`
import Cocoa
import XCPlayground
//: In order to get the callback working we use a simple class to implement the showcase.
class DNSResolve {
//: The IP address may be a Swift `String` thanks to the toll-free bridging to C strings.
    let ip: String = "17.172.224.47"
//: We use an optional `CFHost` variable because CFHost neither comes with an initializer nor is conforming to the Nullable protocol.
    var host: CFHost?
//: We use this array of `String`s to store the resolved host names.
    var names: [String] = []
    func resolve() {
//: Let's set up the `sockaddr_in` C structure using the initializer.
        var sin = sockaddr_in(
            sin_len: UInt8(sizeof(sockaddr_in)),
            sin_family: sa_family_t(AF_INET),
            sin_port: in_port_t(0),
            sin_addr: in_addr(s_addr: inet_addr(ip)),
            sin_zero: (0,0,0,0,0,0,0,0)
        )
//: Now convert the structure into a `CFData` object.
        let data = withUnsafePointer(&sin) { ptr in
            CFDataCreate(kCFAllocatorDefault, UnsafePointer(ptr), sizeof(sockaddr_in))
        }
//: Create the `CFHostRef` with the `CFData` object and store the retained value for later use.
        let hostref = CFHostCreateWithAddress(kCFAllocatorDefault, data)
        self.host = hostref.takeRetainedValue()
//: For the callback to work we have to create a client context.
        var ctx = CFHostClientContext(
            version: 0,
            info: unsafeBitCast(self, UnsafeMutablePointer.self),
            retain: nil,
            release: nil,
            copyDescription: unsafeBitCast(0, CFAllocatorCopyDescriptionCallBack.self)
        )
//: We can now set up the client for the callback using the `CFHostClientCallBack` signature for the closure.
        CFHostSetClient(host!, { (host, infoType, error, info) in
            let obj = unsafeBitCast(info, DNSResolve.self)
            print("Resolving …")
            obj.namesResolved(withError: error.memory)
        }, &ctx)
//: Now schedule the runloop for the host.
        CFHostScheduleWithRunLoop(host!, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
//: Create a `CFStreamError` object for use with the info resolution using `CFHostStartInfoResolution`.
        var error = CFStreamError()
//: Start the info resolution.
        let started: Bool = CFHostStartInfoResolution(host!, .Names, &error)
        print("Name resolution started: \(started)")
    }
//: This function is attachted as `CFHostClientCallBack` in `CFHostSetClient` which should get called during the info resolution.
    func namesResolved(withError error: CFStreamError) {
        print("namesResolved: Resolving …")
//: Create a boolean pointer `DarwinBoolean` for use with the function `CFHostGetNames`.
        var resolved: DarwinBoolean = DarwinBoolean(false)
//: Now get the results of the info resolution.
        let cfNames: CFArrayRef = CFHostGetNames(host!, &resolved)!.takeRetainedValue()
        print("namesResolved: Names resolved: \(resolved) with error \(error.error)")
//: We can use cascading casts from `[AnyObject]` to a force-unwrapped `[String]`. Thank you, Swift.
        self.names = cfNames as [AnyObject] as! [String]
//: **Oh dear—we see only one host name here and no aliases. Stuck again … :-(**
        print("CFArray reports \(CFArrayGetCount(cfNames)) elements, [String] reports \(self.names.count) elements.")
        self.listNames()
//: After the info resolution clean up either way.
        CFHostSetClient(host!, nil, nil);
        CFHostCancelInfoResolution(host!, .Names)
        CFHostUnscheduleFromRunLoop(host!, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)
    }
    func listNames() {
        print(self.names)
    }
}
//: OK, let's create an instance of our `DNSResolve` class and run the `resolve()` method.
let dnsRes = DNSResolve()
dnsRes.resolve()
//: In order to see the callback working we have to set Playground's execution to take on forever.
XCPSetExecutionShouldContinueIndefinitely()


So, Variant 2 does clearly not work as intended as CFHost only resolves one host name without aliases. A lot of code that does not live up to the expected result.


"Enjoy and share", as eskimo would put it. 🙂


Cheers,


Mati

I have a bunch of issues with

gethostbyaddr
:
  • You can make it do (kinda) the right thing with IPv6 but it’s pretty ugly. At the end of this post you’ll find the code I used for this.

    Note On the IPv6 front, it won’t do the right thing with scope identifiers because it can’t ‘see’

    sin6_scope_id
    .
  • It’s synchronous, which is rarely a good idea IMO.

  • It relies on thread local storage to be thread safe.

  • It’s distinction between

    h_name
    and
    h_aliases
    doesn’t really match the way DNS works. DNS doesn’t have an “official name of host”, rather, there are PTR records that map from
    xxx.in-addr.arpa.
    to A records and from
    xxx.ip6.arpa.
    to AAAA records.

Curiously, while digging into this issue I found this in the Wikipedia on reverse DNS lookup:

having multiple PTR records for the same IP address is generally not recommended

It’s is not, then, surprising that NSHost, CFHost and so on all return a single name.

Share and Enjoy

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

let myEmail = "eskimo" + "1" + "@apple.com"
const struct sockaddr * sockAddr;
struct hostent *        host;

… set up sockAddr …

switch (sockAddr->sa_family) {
    case AF_INET: {
        const struct sockaddr_in *  addr;

        addr = (const struct sockaddr_in *) sockAddr;
        host = gethostbyaddr(&addr->sin_addr, sizeof(addr->sin_addr), AF_INET);
    } break;
    case AF_INET6: {
        const struct sockaddr_in6 * addr6;

        addr6 = (const struct sockaddr_in6 *) sockAddr;
        host = gethostbyaddr(&addr6->sin6_addr, sizeof(addr6->sin6_addr), AF_INET6);
    } break;
    default: {
        host = NULL;
    } break;
}

Hi Quinn,


thanks for your insight to which I fully agree. Just to make things clear: My project will likely never see IP6 addresses for the foreseeable time as consumer DSL connections still heavily rely on IP4 (at least here in Europe/Switzerland), so the logs I'm trying to analyse are containing IP4 addresses only. Therefore, for my project I'll stick with gethostbyaddr().


With regards to the Wikipedia article you mentioned I have to admit that in my private data center installation my DNS server makes only use of true aliases with CNAME and PTR references to the same IP address within the internal zones. My routers WAN address has multiple PTR records as it is serving multiple domains with virtual hosts, but I'm now investigating the impact of changing this configuration. I might run into problems where services rely on the fact that a host name that is sent during a network request should match with the reverse-resolved host name of the sending IP address (for example, postfix does some tricks with this—at least in my configuration 😉).


To conclude the discussion I think all existing issues with having no real platform-agnostic, robust and Swift-y/Cocoa-y implementation of a helper class which addresses the most important functionality we all are asking for should be a big enough lever to design a NSDNS class which I requested in tracking ID 22576570.


Where would be the place to collaboratively start the class design describing at least the interface and methods/functions we would like to see? As soon as the interface stands at reason we can discuss who would be in charge of the implementation. 🙂


Cheers,


Mati

My project will likely never see IP6 addresses for the foreseeable time as consumer DSL connections still heavily rely on IP4 (at least here in Europe/Switzerland), so the logs I'm trying to analyse are containing IP4 addresses only.

Honestly, I think that writing new code based on this assumption is a bad idea. You’re constructing a legacy that you’ll have to revisit sooner rather than later.

And, just for the record, my home DSL here in the UK has IPv6.

Where would be the place to collaboratively start the class design describing at least the interface and methods/functions we would like to see?

You should put your suggestions into your bug report.

Share and Enjoy

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

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

Thanks, Quinn, for your thoughtful software-architectural note. Time will eventually come where I have to swap out my Zyxel VDSL-router for one with IP6 support. As a good developer citizen with a robust software architecture in mind I anyway did abstract the portion of log analysis code in a way that I easily can adapt a new address format and use any reverse-loopkup mechanism that's available. New code always has to make some asumptions regarding the current problem it tries to solve, and even if the solution might not be ideal I don't see any problems as long as the design and architecture allows for an adoption in an easy way that does not break the rest of the code. Using protocols and extensions wisely I should be armed for the future.


To cite a widely known zen phase: Protocols and extensions are a great honking idea. Let's do more of those! 😉


Cheers, Mati

Why does NSHost.names only return one result?
 
 
Q