11

I'm trying to do a simple DNS lookup in swift. So far, here is the code that I have:

let hostRef = CFHostCreateWithName(kCFAllocatorDefault, "google.com").takeRetainedValue()
var resolved = CFHostStartInfoResolution(hostRef, CFHostInfoType.Addresses, nil)
let addresses = CFHostGetAddressing(hostRef, &resolved).takeRetainedValue() as NSArray

At this point, each element in the "addresses" NSArray is a CFDataRef object wrapping a sockaddr struct.

Since CFDataRef can be toll-free bridged to NSData, I can loop through them like so:

for address: AnyObject in addresses {
  println(address)  // address is of type NSData.
}

So far so good (I think). This prints out valid looking data when I run it in a unit test. Here is where I get stuck though. For the life of me, I can't figure out how to convert the bytes in the NSData object into a sockaddr struct.

How can I convert address.bytes, which is of type COpaquePointer?, into a c struct? Any help appreciated. I'm banging my head against the wall trying to figure this out.

kgreenek
  • 4,986
  • 3
  • 19
  • 30

1 Answers1

16

For a simpler solution using getnameinfo, see Martin's answer here: How can I get a real IP address from DNS query in Swift?

Updated for Swift 5 / IPv6:

The objects returned by CFHostGetAddressing can be bridged to Swift as Data, and cast to in_addr/in6_addr by using withUnsafeBytes and assumingMemoryBound(to:).

Here's a complete example that uses inet_ntop to convert IPv4/IPv6 addresses to strings:

import CFNetwork
import Foundation

protocol NetworkAddress {
    static var family: Int32 { get }
    static var maxStringLength: Int32 { get }
}
extension in_addr: NetworkAddress {
    static let family = AF_INET
    static let maxStringLength = INET_ADDRSTRLEN
}
extension in6_addr: NetworkAddress {
    static let family = AF_INET6
    static let maxStringLength = INET6_ADDRSTRLEN
}

extension String {
    init<A: NetworkAddress>(address: A) {
        // allocate a temporary buffer large enough to hold the string
        var buf = ContiguousArray<Int8>(repeating: 0, count: Int(A.maxStringLength))
        self = withUnsafePointer(to: address) { rawAddr in
            buf.withUnsafeMutableBufferPointer {
                String(cString: inet_ntop(A.family, rawAddr, $0.baseAddress, UInt32($0.count)))
            }
        }
    }
}

func addressToString(data: Data) -> String? {
    return data.withUnsafeBytes {
        let family = $0.baseAddress!.assumingMemoryBound(to: sockaddr_storage.self).pointee.ss_family
        // family determines which address type to cast to (IPv4 vs IPv6)
        if family == numericCast(AF_INET) {
            return String(address: $0.baseAddress!.assumingMemoryBound(to: sockaddr_in.self).pointee.sin_addr)
        } else if family == numericCast(AF_INET6) {
            return String(address: $0.baseAddress!.assumingMemoryBound(to: sockaddr_in6.self).pointee.sin6_addr)
        }
        return nil
    }
}

let host = CFHostCreateWithName(kCFAllocatorDefault, "google.com" as CFString).takeRetainedValue()
var resolved = DarwinBoolean(CFHostStartInfoResolution(host, .addresses, nil))
let addresses = CFHostGetAddressing(host, &resolved)?.takeUnretainedValue() as! [Data]?

print(addresses?.compactMap(addressToString))

You can use the NSData method getBytes(_, length:) method and pass the sockaddr struct to the inout parameter using the prefix & operator:

var data: NSData ...
var address: sockaddr ...

data.getBytes(&address, length: MemoryLayout<sockaddr>.size)

Updated for Swift 3:

let host = CFHostCreateWithName(kCFAllocatorDefault, "google.com" as CFString).takeRetainedValue()
var resolved = DarwinBoolean(CFHostStartInfoResolution(host, .addresses, nil))
let addresses = CFHostGetAddressing(host, &resolved)?.takeUnretainedValue() as! [NSData]?

if let data = addresses?.first {
    var storage = sockaddr_storage()
    data.getBytes(&storage, length: MemoryLayout<sockaddr_storage>.size)

    if Int32(storage.ss_family) == AF_INET {
        let addr4 = withUnsafePointer(to: &storage) {
            $0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
                $0.pointee
            }
        }

        // prints 74.125.239.132
        print(String(cString: inet_ntoa(addr4.sin_addr), encoding: .ascii))
    }
}


Updated 6/3/2015: Now that C structs can be easily zero-initialized, this becomes much simpler:

let host = CFHostCreateWithName(kCFAllocatorDefault, "google.com").takeRetainedValue()
var resolved = CFHostStartInfoResolution(host, .Addresses, nil)
let addresses = CFHostGetAddressing(host, &resolved)?.takeUnretainedValue() as! [NSData]?

if let data = addresses?.first {
    var storage = sockaddr_storage()
    data.getBytes(&storage, length: sizeof(sockaddr_storage))

    if Int32(storage.ss_family) == AF_INET {
        let addr4 = withUnsafePointer(&storage) { UnsafePointer<sockaddr_in>($0).memory }

        // prints 74.125.239.132
        println(String(CString: inet_ntoa(addr4.sin_addr), encoding: NSASCIIStringEncoding))
    }
}


Unfortunately this requires sockaddr to be initialized first. To avoid that, you could do something like this:

func makeWithUnsafePointer<T>(body: UnsafePointer<T> -> ()) -> T {
    let ptr = UnsafePointer<T>.alloc(sizeof(T))
    body(ptr)
    return ptr.move()
}

let addr: sockaddr = makeWithUnsafePointer {
    data.getBytes($0 as UnsafePointer<sockaddr>, length: sizeof(sockaddr))
}

Or this:

func makeWithUninitialized<T>(body: inout T -> ()) -> T {
    let ptr = UnsafePointer<T>.alloc(sizeof(T))
    body(&ptr.memory)
    return ptr.move()
}

let addr = makeWithUninitialized { (inout addr: sockaddr) in
    data.getBytes(&addr, length: sizeof(sockaddr))
}

For more discussion, see Swift: Pass Uninitialized C Structure to Imported C function

jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • This can be even simplified to just: data.getBytes(&addr, length:sizeof(sockaddr)) – kgreenek Jul 17 '14 at 06:00
  • Good point. I updated my answer, and also added information about doing the same thing without initializing the struct first. – jtbandes Jul 17 '14 at 06:06
  • 2
    For what it's worth, you likely don't want to use sockaddr - but the actual sockaddr structure you get. Like sockaddr_in or sockaddr_un. Otherwise you'll likely get a buffer overflow. Maybe you find this extension useful: https://github.com/AlwaysRightInstitute/SwiftSockets/blob/master/ARISockets/SocketAddress.swift – hnh Jul 21 '14 at 13:48
  • It seems that `ptr.withUnsafePointer(body)` does no longer compile with Xcode 6 beta 4. – Martin R Jul 24 '14 at 17:17
  • @MartinR Looks like you're right. If you can figure out a solution, feel free to edit — otherwise I'll take a look later and update my answer. Perhaps `body(ptr)` will be sufficient. – jtbandes Jul 24 '14 at 17:20
  • @jtbandes Thnkssss :) – itsji10dra Jun 03 '15 at 21:18
  • @jtbandes Swift3 breaks this again – Cameron Lowell Palmer Sep 24 '16 at 21:57
  • 4
    I'm getting: '&' used with non-inout argument of type 'sockaddr_storage' – david72 May 08 '17 at 19:34
  • @david72 I had that issue, too. I guess Swift 4 breaks this answer again. This worked for me: Replace `data.getBytes(&storage, length: MemoryLayout.size)` with `withUnsafeMutableBytes(of: &storage) { storagePointer in data.withUnsafeBytes { addressBuffer in if addressBuffer.count <= MemoryLayout.size { storagePointer.copyMemory(from: addressBuffer) } } }` (Sorry about Stack Overflow's comment formatting.) – bugloaf Apr 22 '20 at 19:14
  • Thanks for the ping. Updated for Swift 5. – jtbandes Apr 23 '20 at 01:44