Skip to content

Instantly share code, notes, and snippets.

@bohwaz
Last active January 22, 2025 22:18
Show Gist options
  • Save bohwaz/ddc61c4f7e031c3221a89981e70b830c to your computer and use it in GitHub Desktop.
Save bohwaz/ddc61c4f7e031c3221a89981e70b830c to your computer and use it in GitHub Desktop.
PHP script to retrieve a DNS record from a custom nameserver
<?php
/**
* Make a DNS a request to a custom nameserver, this is similar to dns_get_record, but allows you to query any nameserver
* Usage: dns_get_record_from('ns.server.tld', 'A', 'mydomain.tld');
* => ['42.42.42.42']
* @author bohwaz
*/
function dns_get_record_from(string $server, string $type, string $record, string $protocol = 'udp'): array
{
// Source: https://github.com/metaregistrar/php-dns-client/blob/master/DNS/dnsData/dnsTypes.php
static $types = [
1 => 'A',
2 => 'NS',
5 => 'CNAME',
6 => 'SOA',
12 => 'PTR',
15 => 'MX',
16 => 'TXT',
28 => 'AAAA',
255 => 'ANY',
];
$typeid = array_search($type, $types, true);
if (!$typeid) {
throw new \InvalidArgumentException('Invalid type');
}
$host = $protocol . '://' . $server;
if (!$socket = @fsockopen($host, 53, $errno, $errstr, 10)) {
throw new \RuntimeException('Failed to open socket to ' . $host);
}
//stream_set_chunk_size($socket, 0xffff);
//stream_set_blocking($socket, false);
$labels = explode('.', $record);
$question_binary = '';
foreach ($labels as $label) {
$question_binary .= pack("C", strlen($label)); // size byte first
$question_binary .= $label; // then the label
}
$question_binary .= pack("C", 0); // end it off
$id = rand(1,255) | (rand(0,255)<<8); // generate the ID
// Set standard codes and flags
$flags = (0x0100 & 0x0300) | 0x0020; // recursion & queryspecmask | authenticated data
$opcode = 0x0000; // opcode
// Build the header
$header = "";
$header .= pack("n", $id);
$header .= pack("n", $opcode | $flags);
$header .= pack("nnnn", 1, 0, 0, 0);
$header .= $question_binary;
$header .= pack("n", $typeid);
$header .= pack("n", 0x0001); // internet class
$headersize = strlen($header);
$headersizebin = pack("n", $headersize);
$request_size = fwrite($socket, $header, $headersize);
$rawbuffer = fread($socket, 4096);
fclose($socket);
if (strlen($rawbuffer) < 12) {
throw new \UnderflowException("DNS query return buffer too small");
}
$pos = 0;
$read = function ($len) use (&$pos, $rawbuffer) {
$out = substr($rawbuffer, $pos, $len);
$pos += $len;
return $out;
};
$read_name_pos = function ($offset_orig, $max_len=65536) use ($rawbuffer) {
$out = [];
$offset = $offset_orig;
while (($len = ord(substr($rawbuffer, $offset, 1))) && $len > 0 && ($offset+$len < $offset_orig+$max_len ) ) {
if ($len >= 64) {
$offset = (($len & 0x3f) << 8) + ord(substr($rawbuffer, $offset + 1, 1));
continue;
}
$out[] = substr($rawbuffer, $offset + 1, $len);
$offset += $len + 1;
}
return $out;
};
$read_name = function() use (&$read, $read_name_pos) {
$out = [];
while (($len = ord($read(1))) && $len > 0) {
if ($len >= 64) {
$offset = (($len & 0x3f) << 8) + ord($read(1));
$out = array_merge($out, $read_name_pos($offset));
break;
}
else {
$out[] = $read($len);
}
}
return implode('.', $out);
};
$header = unpack("nid/nfields/nqdcount/nancount/nnscount/narcount", $read(12));
$fields = $header['fields'];
$flags = new \stdClass;
$flags->rcode = $fields & 0xf;
$flags->ra = (($fields >> 7) & 1) === 1;
$flags->rd = (($fields >> 8) & 1) === 1;
$flags->tc = (($fields >> 9) & 1) === 1;
$flags->aa = (($fields >> 10) & 1) === 1;
$flags->opcode = ($fields >> 11) & 0xf;
$flags->qr = (($fields >> 15) & 1) === 1;
if ($flags->tc) {
throw new \OverflowException('The DNS server returned a truncated result for a UDP query');
}
// No answers
if (!$header['ancount']) {
return [];
}
$is_authorative = $flags->aa;
// Question section
if ($header['qdcount']) {
// Skip name
$read_name();
// skip question part
$pos += 4; // 4 => QTYPE + QCLASS
}
$responses = [];
for ($a = 0; $a < $header['ancount']; $a++) {
$read_name(); // Skip name
$ans_header = unpack("ntype/nclass/Nttl/nlength", $read(10));
$t = $types[$ans_header['type']] ?? null;
if ($type !== 'ANY' && $t !== $type) {
// Skip type that was not requested
$t = null;
}
switch ($t) {
case 'A':
$responses[] = implode(".", unpack("Ca/Cb/Cc/Cd", $read(4)));
break;
case 'AAAA':
$responses[] = implode(':', unpack("H4a/H4b/H4c/H4d/H4e/H4f/H4g/H4h", $read(16)));
break;
case 'MX':
$prio = unpack('nprio', $read(2)); // priority
$responses[$prio['prio']] = $read_name();
break;
case 'NS':
case 'CNAME':
case 'PTR':
$responses[] = $read_name();
break;
case 'TXT':
$data = '';
for ($strCount = 0; strlen($data) + (1 + $strCount) < $ans_header['length']; $strCount++) {
$data .= $read(ord($read(1)));
}
$responses[] = $data;
break;
default:
// Skip
$read($ans_header['length']);
break;
}
}
return $responses;
}
@tbreuss
Copy link

tbreuss commented Nov 24, 2024

On macOS with PHP 8.3 I get a "PHP Fatal error: Uncaught RuntimeException: DNS query return buffer too small in..." here:
https://gist.github.com/bohwaz/ddc61c4f7e031c3221a89981e70b830c#file-dns_get_record_from-php-L68

@bohwaz
Copy link
Author

bohwaz commented Dec 1, 2024

@tbreuss please provide the exact function call you are using. Have you tried on Linux?

@bohwaz
Copy link
Author

bohwaz commented Dec 1, 2024

@Rixafy thank you I used your patches.

I also updated the function to correctly detect a truncated response, which happens when there are too many records, eg. when requesting TXT records of github.com.

The next step would be for the function to call back itself using TCP instead of UDP, but it's not ready for TCP yet, there's probably some stuff to change, but I don't have time to look into it.

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