-
-
Save fikeminkel/a9c4bc4d0348527e8df3690e242038d3 to your computer and use it in GitHub Desktop.
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 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?
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.
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
}
@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
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.
https://gist.github.com/fikeminkel/a9c4bc4d0348527e8df3690e242038d3#file-dnstxtrecord-swift-L20
Unfortunately this code produces incorrect behaviour on concatenated TXT entries, leaving a garbage byte in place of the concatenation:
// advancing pointer by 1 to skip bad character at beginning of record
let txt = String(cString: txtPtr.advanced(by: 1))
Quoting https://kb.isc.org/docs/aa-00356: per RFC 4408 a TXT or SPF record is allowed to contain multiple strings, which should be concatenated together by the reading application
See apple/swift-async-dns-resolver#43 and apple/swift-async-dns-resolver#44
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):
and to get self safely in the callback like this:
Other approaches I tried had issues, but this one worked well.