Title: A short tale on PHP's dns_get_record
Date: 2019-08-02 13:15

Some times ago, a friend of mine asked for my opinion on a peculiar issue involving DNS and PHP:

```
$ php -r "var_dump(dns_get_record('riseup.net'));"
PHP Warning:  dns_get_record(): A temporary server error occurred. in Command line code on line 1
```

This is odd, since using `dig` gives a satisfactory answer:

```
$ dig +short riseup.net
198.252.153.70
$
```

Instead of looking at [PHP's documentation](https://secure.php.net/manual/en/function.dns-get-record.php)
and finding the issue immediately, I did a bunch of nonsenses that are now
exposed on this blogpost.

I started by *stracing*, and found nothing suspicious:

```C
$ strace -f -yyyyyyy php -r "var_dump(dns_get_record('riseup.net'));"
[…]
open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3</run/resolvconf/resolv.conf>
fstat(3</run/resolvconf/resolv.conf>, {st_mode=S_IFREG|0644, st_size=2265, ...}) = 0
read(3</run/resolvconf/resolv.conf>, "# Begin /etc/resolvconf/resolv.c"..., 4096) = 2265
read(3</run/resolvconf/resolv.conf>, "", 4096) = 0
close(3</run/resolvconf/resolv.conf>)   = 0
socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3<UDP:[466784]>
connect(3<UDP:[466784]>, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
poll([{fd=3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, "\373\367\1\0\0\1\0\0\0\0\0\0\6riseup\3net\0\0\377\0\1", 28, MSG_NOSIGNAL, NULL, 0) = 28
poll([{fd=3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 0 (Timeout)
poll([{fd=3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, "\373\367\1\0\0\1\0\0\0\0\0\0\6riseup\3net\0\0\377\0\1", 28, MSG_NOSIGNAL, NULL, 0) = 28
poll([{fd=3<UDP:[127.0.0.1:36054->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 0 (Timeout)
close(3<UDP:[127.0.0.1:36054->127.0.0.1:53]>) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3<UDP:[467429]>
connect(3<UDP:[467429]>, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
poll([{fd=3<UDP:[127.0.0.1:38206->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:38206->127.0.0.1:53]>, "[L\1\0\0\1\0\0\0\0\0\0\6riseup\3net\4corp\6goo"..., 44, MSG_NOSIGNAL, NULL, 0) = 44
poll([{fd=3<UDP:[127.0.0.1:38206->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
recvfrom(3<UDP:[127.0.0.1:38206->127.0.0.1:53]>, "[L\201\203\0\1\0\0\0\1\0\1\6riseup\3net\4corp\6goo"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [28->16]) = 116
close(3<UDP:[127.0.0.1:38206->127.0.0.1:53]>) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3<UDP:[467430]>
connect(3<UDP:[467430]>, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
poll([{fd=3<UDP:[127.0.0.1:35542->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:35542->127.0.0.1:53]>, "\317\311\1\0\0\1\0\0\0\0\0\0\6riseup\3net\4prod\6goo"..., 44, MSG_NOSIGNAL, NULL, 0) = 44
poll([{fd=3<UDP:[127.0.0.1:35542->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
recvfrom(3<UDP:[127.0.0.1:35542->127.0.0.1:53]>, "\317\311\201\203\0\1\0\0\0\1\0\1\6riseup\3net\4prod\6goo"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [28->16]) = 105
close(3<UDP:[127.0.0.1:35542->127.0.0.1:53]>) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3<UDP:[467431]>
connect(3<UDP:[467431]>, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
poll([{fd=3<UDP:[127.0.0.1:50620->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:50620->127.0.0.1:53]>, "\223:\1\0\0\1\0\0\0\0\0\0\6riseup\3net\5prodz\6go"..., 45, MSG_NOSIGNAL, NULL, 0) = 45
poll([{fd=3<UDP:[127.0.0.1:50620->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
recvfrom(3<UDP:[127.0.0.1:50620->127.0.0.1:53]>, "\223:\201\203\0\1\0\0\0\1\0\1\6riseup\3net\5prodz\6go"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [28->16]) = 111
close(3<UDP:[127.0.0.1:50620->127.0.0.1:53]>) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3<UDP:[468069]>
connect(3<UDP:[468069]>, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
poll([{fd=3<UDP:[127.0.0.1:35668->127.0.0.1:53]>, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
sendto(3<UDP:[127.0.0.1:35668->127.0.0.1:53]>, "\317\315\1\0\0\1\0\0\0\0\0\0\6riseup\3net\6google\3c"..., 39, MSG_NOSIGNAL, NULL, 0) = 39
poll([{fd=3<UDP:[127.0.0.1:35668->127.0.0.1:53]>, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
recvfrom(3<UDP:[127.0.0.1:35668->127.0.0.1:53]>, "\317\315\201\203\0\1\0\0\0\1\0\1\6riseup\3net\6google\3c"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.1")}, [28->16]) = 105
close(3<UDP:[127.0.0.1:35668->127.0.0.1:53]>) = 0
write(2</dev/pts/1>, "PHP Warning:  dns_get_record(): "..., 98PHP Warning:  dns_get_record(): A temporary server error occurred. in Command line code on line 1
) = 98
write(1</dev/pts/1>, "bool(false)\n", 12bool(false)
) = 12
close(2</dev/pts/1>)                    = 0
close(1</dev/pts/1>)                    = 0
close(0</dev/pts/1>)                    = 0
```

A quick glance at [PHP's source
code](https://github.com/php/php-src/blob/master/ext/standard/dns.c) revealed
that `dns_get_record` on Linux is a *simple* wrapper to
[`res_nsearch`](http://man7.org/linux/man-pages/man3/res_nsearch.3.html), so
nothing fancy here.

So I `straced` again, thought about caching issues because on some machines,
after doing a `dig riseup.net`, the php snippet would successfully resolve.

It also worked on some other domains on first try, but not all, so … caching?

Nope, not caching. It took me way too much time to realise that of course, it wasn't cache-related.

This issue was right in front of me the whole time:

```c
/* {{{ proto array|false dns_get_record(string hostname [, int type[, array &authns[, array &addtl[, bool raw]]]])
   Get any Resource Record corresponding to a given Internet host name */
PHP_FUNCTION(dns_get_record)
{
	char *hostname;
	size_t hostname_len;
	zend_long type_param = PHP_DNS_ANY;
	zval *authns = NULL, *addtl = NULL;
	int type_to_fetch;
```

By default, `dns_get_record` will issue a [`ANY`](https://tools.ietf.org/html/rfc1035#page-12) query,
and this behaviour is mentioned in the relevant [documentation](https://www.php.net/manual/en/function.dns-get-record.php).

Apparently, PHP didn't get the memo that since `ANY` records are mostly (only?)
used to conduct [DNS-based amplification
attacks](https://www.us-cert.gov/ncas/alerts/TA13-088A), they are in the
process of being deprecated: [cloudflare](
https://blog.cloudflare.com/deprecating-dns-any-meta-query-type/ ) is not
answering them anymore, [RFC9482](https://tools.ietf.org/html/rfc8482) is
sunsetting them too, people are
[patching](https://fanf.livejournal.com/140566.html)
[BIND](https://gist.github.com/hdais/25cb3fc86335026d40f0) to remove them, …

Even PHP's documentation itself suggest to avoid them:

> Because of eccentricities in the performance of libresolv between platforms,
**DNS_ANY** will not always return every record, the slower **DNS_ALL** will collect
all records more reliably.

Moreover, when people are resolving domains via PHP (why would
anyone do that is an entirely separate question), they're almost always
interested in the `A`/`AAAA` records, not in getting as much data as possible, 
including the `NSEC3PARAM`, `RRSIG`, `CDS`, … records,
to walk an array to find the IP of a FQDN.


