Skip to content

Instantly share code, notes, and snippets.

@fikeminkel
Created April 26, 2017 20:56
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fikeminkel/a9c4bc4d0348527e8df3690e242038d3 to your computer and use it in GitHub Desktop.
Save fikeminkel/a9c4bc4d0348527e8df3690e242038d3 to your computer and use it in GitHub Desktop.
Swift 3.1 DNS TXT record lookup
import dnssd
struct DNSTxtRecord {
typealias DNSLookupHandler = ([String: String]?) -> Void
static func lookup(_ domainName: String, completionHandler: @escaping DNSLookupHandler) {
var mutableCompletionHandler = completionHandler // completionHandler needs to be mutable to be used as inout param
let callback: DNSServiceQueryRecordReply = {
(sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in
// dereference completionHandler from pointer since we can't directly capture it in a C callback
guard let completionHandlerPtr = context?.assumingMemoryBound(to: DNSLookupHandler.self) else { return }
let completionHandler = completionHandlerPtr.pointee
// map memory at rdata to a UInt8 pointer
guard let txtPtr = rdata?.assumingMemoryBound(to: UInt8.self) else {
completionHandler(nil)
return
}
// advancing pointer by 1 to skip bad character at beginning of record
let txt = String(cString: txtPtr.advanced(by: 1))
// parse name=value txt record into dictionary
var record: [String: String] = [:]
let recordParts = txt.components(separatedBy: "=")
record[recordParts[0]] = recordParts[1]
completionHandler(record)
}
// MemoryLayout<T>.size can give us the necessary size of the struct to allocate
let serviceRef: UnsafeMutablePointer<DNSServiceRef?> = UnsafeMutablePointer.allocate(capacity: MemoryLayout<DNSServiceRef>.size)
// pass completionHandler as context object to callback so that we have a way to pass the record result back to the caller
DNSServiceQueryRecord(serviceRef, 0, 0, domainName, UInt16(kDNSServiceType_TXT), UInt16(kDNSServiceClass_IN), callback, &mutableCompletionHandler);
DNSServiceProcessResult(serviceRef.pointee)
DNSServiceRefDeallocate(serviceRef.pointee)
}
}
@ethan-gerardot
Copy link

ethan-gerardot commented Mar 2, 2018

I'm trying to use this code but it doesn't ever run the callback method even though kDNSServiceErr_NoError is being returned by DNSServiceQueryRecord (see return value in Apple's documentation). Did something change in dnssd to break this code? It's just getting stuck at DNSServiceProcessResult(serviceRef.pointee) for me (it never finishes processing).

@ethan-gerardot
Copy link

ethan-gerardot commented Mar 12, 2018

I didn't realize I had to create a socket with DNSServiceRefSockFD(serviceRef.pointee) and then a read source with the socket (DispatchSource.makeReadSource(fileDescriptor: socket!, queue: ...)) and call readSource.setEventHandler with a block that calls DNSServiceProcessResult(serviceRef.pointee). Then the callback will get called (as long as you pass the right domain for whatever you're trying to look up*).

*In my case, I was doing a CNAME lookup instead of TXT. If you do a CNAME lookup on a ".local" domain, you have to do it differently due to Bonjour reserving ".local" for multi-cast DNS calls...".local" doesn't seem to work with a CNAME lookup (Here's one of several articles/forums on Apple's site about it). If you need to support lookup for a domain that has ".local", instead of passing kDNSServiceType_CNAME, you pass kDNSServiceType_A for the record type and for flags, pass kDNSServiceFlagsReturnIntermediates so that your callback gets called for each record in the process of getting the A record - then just parse the record from rdata / rdlen in your callback each time you get called (see the processRecord:length: method in this example, but use kDNSServiceType_CNAME instead of kDNSServiceType_SRV) and see if CNAME is there on the record you parsed...if so, that's your CNAME and you're done. If not, wait for the next callback to get called, parse the record again, and see if it has a CNAME...etc. (It's possible for an A record to have multiple CNAMEs along the way, so my code just looks for the first one and quits.)

Also note, it's a good idea to have a timer in case you get blocked due to a bad domain name or never finding a CNAME - you can create a timer very similar to a read source except do DispatchSource.makTimer()

Last thing, I found it best to pass self as context like this (because I needed to call a local method if the callback had a CNAME record):

let unsafeMutableSelf = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
let error = DNSServiceQueryRecord(serviceRef,
                                  UInt32(kDNSServiceFlagsReturnIntermediates),
                                  UInt32(kDNSServiceInterfaceIndexAny),
                                  "some.domain.local",
                                  UInt16(kDNSServiceType_A),
                                  UInt16(kDNSServiceClass_IN),
                                  callback,
                                  unsafeMutableSelf)

and to get self safely in the callback like this:

let callback: DNSServiceQueryRecordReply = { (sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in
    // Dereference self from pointer since we can't directly capture it
    // in a C callback
    guard let context = context else {
        return
    }
    let unmanagedSelf = Unmanaged<WhateverTheNameOfYourClassIs>.fromOpaque(context).takeUnretainedValue()

Other approaches I tried had issues, but this one worked well.

@mosen
Copy link

mosen commented Mar 24, 2018

@ethan-gerardot I'm looking at this, trying to use it to resolve SRV records. I don't really have any knowledge of socket programming but i did see a mention about using select() in the obj-c implementation. Is there any chance you have example code with the finished implementation?

@atomicbird
Copy link

Thanks for this, but there's an issue some people may have. This code:

  // advancing pointer by 1 to skip bad character at beginning of record
  let txt = String(cString: txtPtr.advanced(by: 1))

The pointer needs to be advanced, but the number to skip depends on the type of record. For example when looking up MX records, the first two bytes are a 16-bit integer indicating record preference. So for MX the pointer needs to advance by 2. Without that, you can end up getting empty strings, since advancing by 1 can still mean that txtPtr starts with a null. Other record types may need other changes. See RFC 1035 for details.

@juanheyns
Copy link

Adapted the above version to the following which works for me. Had to add some error handling and a timeout, see below: kDNSServiceErr_NoError, kDNSServiceFlagsTimeout

    import Foundation
    import dnssd

    // ...

    typealias DNSLookupHandler = ([String: String]?) -> Void

    func query(domainName: String) -> [String: String]? {
        var result: [String: String] = [:]
        var recordHandler: DNSRecordHandler = {
            (record) -> Void in
            if (record != nil) {
                for (k, v) in record! {
                    result.updateValue(v, forKey: k)
                }
            }
        }

        let callback: DNSServiceQueryRecordReply = {
            (sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in
            guard let handlerPtr = context?.assumingMemoryBound(to: DNSLookupHandler.self) else {
                return
            }
            let handler = handlerPtr.pointee
            if (errorCode != kDNSServiceErr_NoError) {
                return
            }
            guard let txtPtr = rdata?.assumingMemoryBound(to: UInt8.self) else {
                return
            }
            let txt = String(cString: txtPtr.advanced(by: 1))
            var record: [String: String] = [:]
            let parts = txt.components(separatedBy: "=")
            record[parts[0]] = parts[1]
            handler(record)
        }
        
        let serviceRef: UnsafeMutablePointer<DNSServiceRef?> = UnsafeMutablePointer.allocate(capacity: MemoryLayout<DNSServiceRef>.size)
        let code = DNSServiceQueryRecord(serviceRef, kDNSServiceFlagsTimeout, 0, domainName, UInt16(kDNSServiceType_TXT), UInt16(kDNSServiceClass_IN), callback, &recordHandler)
        if (code != kDNSServiceErr_NoError) {
            return nil
        }
        DNSServiceProcessResult(serviceRef.pointee)
        DNSServiceRefDeallocate(serviceRef.pointee)
        
        return result
    }

@eclair4151
Copy link

@mosen or anyone looking for an example parsing SRV records, heres an example I found that worked very well for me: https://github.com/jamf/NoMAD-2/blob/main/NoMAD/SRVLookups/SRVResolver.swift

@rgkobashi
Copy link

Thank you very much! @fikeminkel and @juanheyns both snipped worked for me.

If someone is having an issue like @ethan-gerardot where it gets stuck at DNSServiceProcessResult, is most likely because the domainName is invalid, adding a time out like @juanheyns might help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment