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.