Tag Archives: dns

Net_DNS2 v2: Breaking a Decade of Backwards Compatibility

I have maintained Net_DNS2 since 2010. It started as a cleanup of the old PEAR Net_DNS library, and for most of its life the guiding rule was simple: do not break anyone. That rule held through PHP 5.3, 5.4, 7.x, and into 8.x. With v2.0 I broke it deliberately, and I think it was overdue.

What PEAR-Era Naming Costs You

Net_DNS2 predates PSR-4, namespaces, and any modern autoloading convention. The class naming reflected that:

$r = new Net_DNS2_Resolver(['nameservers' => ['8.8.8.8']]);
$result = $r->query('example.com', 'MX');

Underscores as a pseudo-namespace worked, and it left the library carrying an autoloader that mapped underscores to directory separators, class names that grew to Net_DNS2_RR_OPENPGPKEY, and no way to use any of the type machinery PHP had spent a decade adding. Every resource record type was a loosely typed bag of public properties. Every enumerated value was an integer or a bare string, validated by convention.

The practical cost was in the bug reports. A meaningful share were people passing the wrong type into something, getting no error, and finding out later when the wire format came out malformed. The library could not tell them, because it had no way to say what it expected.

v2.0 Requires PHP 8.1

The v2.0 rewrite moved to PSR-4 and real namespaces, with the parallel rename you would expect:

$r = new \NetDNS2\Resolver(['nameservers' => ['8.8.8.8']]);
$result = $r->query('example.com', 'MX');

The floor is PHP 8.1, which is what enums require. That version choice is the entire reason the break was worth making. DNS is a protocol built almost entirely out of small enumerated sets: record types, classes, opcodes, response codes, DNSSEC algorithms, digest types. Modelling those as integer constants means every function that accepts one accepts any integer. Modelling them as backed enums means the type system rejects nonsense before a packet is ever assembled.

Most class, method, and property names survived the move. Someone upgrading is mostly changing Net_DNS2_Thing to \NetDNS2\Thing and raising their PHP requirement, which is a mechanical change a search and replace handles. That was the design constraint I set for myself: break the naming, keep the shape.

Deciding to Break Compatibility

The argument against was straightforward. Net_DNS2 has a long tail of users on old PHP, often embedded in something they inherited and do not want to touch. A hard PHP 8.1 floor strands all of them.

What changed my mind was noticing that I was writing new code to work around the absence of types, then writing tests to catch the bugs that the absent types would have caught, then answering issues from people who hit those bugs anyway. The library was subsidising PHP 5 compatibility with permanent complexity, and the people paying that tax were mostly not the people benefiting from it.

The compromise is that v1.x still exists and still gets fixes. That is the part I would recommend to anyone in the same position. A hard break is much easier to justify when the old branch does not disappear the same day, and tagging a final v1 release that people can pin to costs almost nothing.

What I Would Do Differently

I would have done it sooner. The signal was there for years: every feature request that involved better validation ran into the same wall, and I kept routing around it. Deprecation cycles have a cost too, and carrying a compatibility promise you have quietly stopped believing in is worse than announcing the break.

I would also have been more aggressive about the enum conversion in one pass. Doing it incrementally meant a stretch where some values were enums and some were still integers, and the mixed state was more confusing than either endpoint. If you are going to break, break cleanly and finish.

If you use the library and are still on v1, there is no urgency. If you are starting something new on PHP 8.1 or later, start on v2. The DNS lookup tool on mrdns.com runs on this code, so it gets exercised against real-world responses continuously, which has caught more parsing edge cases than my test suite ever did.

Fanning Out 140 DNS Queries per Host Without Melting the Server

A blacklist check is not one DNS query. Checking a single IP against our source list means somewhere north of 140 DNS lookups, and the checker runs continuously across a large number of hosts. The naive implementation of that is a loop calling getaddrinfo(), and it is catastrophically slow for a reason that has nothing to do with CPU.

DNS Is Almost Entirely Waiting

A DNSBL lookup is a small UDP packet out and a small UDP packet back. The work is negligible. What dominates is latency, and the distribution is ugly: most zones answer in tens of milliseconds, a few take hundreds, and some fraction never answer at all and burn your full timeout. Serialized, 140 lookups at an average of 60ms is over eight seconds for one host, and the tail is far worse than the average.

The instinct is a thread per query. That works until it doesn’t. Threads are expensive relative to the work being done, and a thread blocked on a 5 second DNS timeout is a thread doing nothing while holding a stack. Scale that across concurrent hosts and you are context-switching more than resolving.

One Socket, Many Queries in Flight

DNS over UDP is already designed for multiplexing. Every query carries a 16-bit ID, and the response echoes it back. That gives you everything you need to run hundreds of queries over a single socket and demultiplex the answers as they arrive:

//
// send all queries first, then collect
//
for (auto const &source : this->m_sources)
{
    uint16_t id = this->_next_query_id();

    this->m_pending[id] = { source.id, now_ms() + source.timeout_ms };
    this->_send_query(id, source, _host);
}

Then a single epoll_wait() loop reads whatever comes back, looks up the ID, and retires that entry. The whole batch costs one socket and one thread, and wall clock becomes the slowest response rather than the sum of all of them.

Two details make this correct rather than merely fast. The query ID has to be random, not sequential, because a predictable ID plus a known source port is the cache-poisoning setup from 2008. And you have to verify that the response actually matches the question you asked, since the socket is unconnected and will happily hand you a packet from anyone.

Timeouts Are the Hard Part

Nothing tells you a UDP query failed. The absence of a response is the signal, which means every in-flight query needs a deadline and something has to sweep for expired ones. Checking all 140 pending entries on every loop iteration is fine at this scale. At larger scale, keep them in a structure ordered by deadline so the sweep only touches what has actually expired.

int64_t now = now_ms();

for (auto it = this->m_pending.begin(); it != this->m_pending.end(); )
{
    if (it->second.deadline <= now)
    {
        this->_record_timeout(it->second.source_id);
        it = this->m_pending.erase(it);
    } else
    {
        ++it;
    }
}

The timeout value itself is a judgment call with real consequences. Too short and you record false negatives for slow-but-working zones, which quietly degrades your results. Too long and one dead source sets the floor for every check. Per-source timeouts, tuned from that source’s observed behavior, beat one global number.

The Failure Modes That Only Show Up in Production

Source port exhaustion is the first one. If you open a socket per query, you will run out of ephemeral ports under load, and the symptom is intermittent bind() failures that look like nothing else. A small pool of sockets with many queries multiplexed over each avoids it entirely.

Rate limiting is the second. Public DNSBL operators will throttle or block you, and they are right to. Anything running at volume belongs on a paid data feed or a local mirror, and the checker needs per-source concurrency caps so one aggressive batch cannot trip a limit that then poisons results for every subsequent check.

The third one took me longest to appreciate. A source that returns NXDOMAIN and a source that never answers look similar in a naive implementation, and they mean completely different things. The first is a confirmed “not listed”. The second is “no data”, and recording it as “not listed” turns an outage at one provider into a silent gap in your results. Track them as distinct outcomes from the start, because retrofitting that distinction after the fact means every historical row is ambiguous.

Mr. DNS: Free DNS and Network Diagnostic Tools for Sysadmins and Email Teams

Mr. DNS is a free collection of DNS and network diagnostic tools built for sysadmins, email administrators, and infrastructure teams. The site has been around for years, went offline for a while, and recently relaunched with an expanded tool set. Everything runs in the browser with no account required. If you work with DNS records, mail servers, or IP reputation, there is something here you will use regularly.

Mr. DNS homepage showing DNS and network diagnostic tools

DNS Tools

The DNS lookup tool handles all common record types: A, AAAA, MX, TXT, NS, SOA, CNAME, PTR, CAA, SRV, TLSA, HTTPS, MTA-STS, and BIMI. Results include TTL, geolocation data for nameservers, and flag icons for quick visual scanning.

The DNS propagation checker queries seven global resolvers simultaneously: Cloudflare, Google, Quad9, OpenDNS, AdGuard, NextDNS, and DNS.SB. Useful when you have just made a DNS change and need to see where it has landed without waiting or querying each resolver manually.

The DNSSEC checker validates the full chain of trust: DS records, DNSKEY records, RRSIG presence, and expiry. Good for confirming a DNSSEC deployment before and after changes.

Email Tools

The email tools are where Mr. DNS gets most of its daily use. The email health checker runs a combined SPF and DMARC evaluation and returns a letter grade (A through F) for your domain. One URL, one result, easy to share with a client or manager who needs a status report.

Mr. DNS email health checker showing an A grade for generatorlabs.com

Individual checkers are also available for SPF, DMARC, and DKIM when you need to dig into a specific record. The email header analyzer parses raw RFC 2822 headers and maps the full relay chain with per-hop timing and authentication results, useful for tracing a delivery failure or diagnosing a spam classification issue.

For teams managing outbound mail infrastructure, the MTA-STS checker validates DNS records and policy files, and the BIMI checker verifies SVG logos and VMC certificates for domains using brand indicators in supported mail clients.

Blacklist Checker

The blacklist checker queries your IP or domain against 15+ major RBLs and returns results in seconds. It is a solid first step when a client reports deliverability problems or when you are onboarding a new IP range and want a quick baseline.

For teams that need ongoing coverage rather than one-off checks, blacklist monitoring from Generator Labs runs continuous checks against hundreds of data sources and sends immediate alerts when a listing is detected. The free tier covers one host with no credit card required.

SSL and Network Tools

The SSL certificate checker inspects certificate details, expiry dates, SANs, issuer chain, and key type for any domain. Useful for a quick manual check before or after a certificate renewal.

For automated tracking across many domains, certificate monitoring from Generator Labs handles the ongoing work: scheduled checks, configurable expiry alert thresholds, and multi-channel notifications before anything expires.

Other network tools include ping, traceroute, port checker, HTTP headers inspector, HTTP/2 and HTTP/3 checker, and a what is my IP tool that detects both IPv4 and IPv6 with geolocation and ASN data.

Generators

Mr. DNS includes generators for SPF records and DMARC records for teams setting up email authentication from scratch. Both walk through the options and output a ready-to-paste DNS record.

Bottom Line

Mr. DNS covers the diagnostic side of DNS and email infrastructure without requiring an account or payment. For the monitoring side, Generator Labs provides continuous blacklist monitoring and certificate monitoring with alerting, picking up where the one-shot tools leave off. Both are worth bookmarking if you manage any kind of mail or DNS infrastructure.

Net_DNS2 v1.4.4 – Bugfixes and Updates for PHP 7.2

I’ve released version 1.4.4 of the PEAR Net_DNS2 library- this release is primarily just bug fixes.

You can install it now through the command line PEAR installer:

pear install Net_DNS2

Or, you can also add it to your project using composer:

composer require pear/net_dns2

Version 1.4.4

  • Bugfix when returning an empty bitmap-type in BitMap.php – patch from BugMaster510945.
  • Added the BIND 9 private record RR (TYPE65534) – patch from BugMaster510945.
  • Added DNSSEC algorithms 13-16 (ECDSAP256SHA256, ECDSAP384SHA384, ED25519, and ED448).
  • Added SSHFP algoritm ED25519.
  • Modified Net_DNS2::sendPacket() to use current()/next() rather than the deprecated each() (deprecated in 7.2).

Net_DNS2 v1.4.3 – Interim Bugfix Release

I’ve released version 1.4.3 of the PEAR Net_DNS2 library- this release is primarily just bug fixes.

You can install it now through the command line PEAR installer:

pear install Net_DNS2

Or, you can also add it to your project using composer:

composer require pear/net_dns2

Version 1.4.3

  • fixed an issue when looking up . or com., when using the strict_query_mode flag.
  • fixed a bug in the caching logic where I was loading the content more than once per instance, when really I only need to do it once.
  • changed the Net_DNS2::sock array to use the SOCK_DGRAM and SOCK_STREAM defines, rather than the strings ‘tcp’ or ‘udp’.
  • fixed a bug in the Net_DNS2_Header and Net_DNS2_Question classes, where I was using the wrong bit-shift operators when parsing some of the values. This only became apparent when somebody was trying to use the CAA class (id 257); it was causing this to roll over to the next 8 bit value, and returning 1 (RR A) instead of the CAA class.
  • fixed a bug that occurs when a DNS lookup request times out, and then the same class is reused for a subsequent request. Because I’m caching the sockets, the timed out data could eventually come in, and end up being seen as the result for a subsequent lookup.
  • fixed a couple cases in NSAP.php where I was comparing a string to an integer.