Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

A short tale on PHP's dns_get_record
Fri 02 August 2019 — download

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 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:

$ 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 revealed that dns_get_record on Linux is a simple wrapper to res_nsearch, 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:

/* {{{ 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 query, and this behaviour is mentioned in the relevant documentation.

Apparently, PHP didn't get the memo that since ANY records are mostly (only?) used to conduct DNS-based amplification attacks, they are in the process of being deprecated: cloudflare is not answering them anymore, RFC9482 is sunsetting them too, people are patching BIND 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.