DNS queries (at least those generated by macOS's dig
) when sent over TCP, are prefixed with a 2-byte length.
Observe this by firing up a network monitor (e.g., WireShark), and then generating a DNS query over TCP:
% dig @8.8.8.8 <some domain> +tcp
"Normal" DNS queries (sent over UDP) are not prefixed in this manner.
% dig @8.8.8.8 <some domain>
Though WireShark has no problem with these differences, this poses a problem for libresolv
. Specifically the dns_parse_packet
function will return NULL
for DNS queries over TCP. 😤
A simple workaround is to pass dnsPacket +2
to dns_parse_packet
() ...but this feels dirty? 😅 Ideally libresolv
would be made a little more robust, able to handle both packet styles.
DNS queries … when sent over TCP, are prefixed with a 2-byte length.
Yes. That’s the standard DNS framing for TCP.
Specifically the
dns_parse_packet
function will return NULL for DNS queries over TCP.
That’s expected. dns_parse_packet
parses DNS messages. If you want to use it with TCP, you must first remove the framing.
A simple workaround is to pass
dnsPacket + 2
todns_parse_packet
Ah, um, that seems like you’re missing a key point here. Given that these bytes are coming in over TCP, I presume you’re reading them using a streaming API, like the BSD Sockets read
system call. Such APIs do not guarantee to preserve record boundaries. So, imagine the sender calls write
with the 2-byte length and the DNS message in one system call. There’s no guarantee that your call to read
will return those same bytes. You might get them all in one read, you might get some fraction of the bytes and then have to read again, or you might get extra bytes that are for the next framed DNS message.
To deal with this you need an ‘unframer’, that is, something that reads the byte stream and produces unframed DNS messages. An unframer will naturally remove the 2-byte length, and thus you don’t need to remove it again when you call dns_parse_packet
.
Pasted in below you’ll find unframer and framer functions. These assume the existence of a QDNSMessage
type, with a single data
property holding the bytes of the DNS message itself.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
public func dnsUnframer(_ data: Data) throws -> (message: QDNSMessage, residual: Data)? {
guard data.count >= 2 else { return nil }
let length16 = data.prefix(2).reduce(0) { soFar, next in (soFar << 8) | UInt16(next) }
let messageCount = Int(length16)
let frameCount = Int(2 + messageCount)
guard data.count >= frameCount else { return nil }
let message = QDNSMessage(data: data.dropFirst(2).prefix(messageCount))
return (message, Data(data.dropFirst(frameCount)))
}
public func dnsFramer(_ message: QDNSMessage) throws -> Data {
guard let count16 = UInt16(exactly: message.data.count) else {
throw POSIXError(.EMSGSIZE)
}
let header = (0..<2).reversed().map { UInt8((count16 >> ($0 * 8)) & 0xff) }
return header + message.data
}