A DNS Playground Grows Up: Mess With DNS Moves to PowerDNS

Mess With DNS, the interactive playground for learning how DNS works, has been running for about three years. While fun, the original implementation was not very strict about following DNS RFCs, and that procrastination eventually caught up with the project.

Users began reporting problems stemming from these shortcuts. Underscores in domain names were rejected, even though they’re valid. CNAME records weren’t being enforced properly, allowing conflicts like multiple CNAMEs for the same name or mixing CNAMEs with other record types. The server also lacked support for newer record types like SVCB and HTTPS, and couldn't upgrade connections from UDP to TCP for large responses. More fundamental issues lurked beneath the surface, like incorrect handling of delegated subdomains via NS records. These were more than simple bugs; they were architectural limitations that made fixing each individual issue a daunting prospect.

The solution arrived in the form of PowerDNS, an open-source DNS server with a robust HTTP API. The idea was straightforward: ditch the custom, incomplete DNS logic entirely and rely on PowerDNS to handle the complex, standards-compliant heavy lifting. The project’s focus could then shift to the frontend and user experience, but integrating a full-featured DNS server wasn't without its own hurdles.

Gaining Visibility Into Every Query

A core feature of Mess With DNS is showing users a live stream of every query their subdomain receives via a websocket. The previous server handled this directly, but a PowerDNS backend would be processing queries internally, away from the application’s direct view. The DNS queries need to be intercepted before they're handed off to PowerDNS.

Two approaches were viable. The first was to put dnsdist, a DNS load balancer from the PowerDNS project, in front of the main server and use its dnstap logging to mirror the queries. The second was to write a custom proxy in the Go backend, which would listen on port 53 and forward queries to PowerDNS.

While the dnstap option was implemented first, it inexplicably introduced a one-second delay before queries appeared in the logs. Rather than debug that deep-seated issue, the decision was made to go with the more manual approach and build the proxy directly into the Go server, giving full control over the query stream.

Designing an API for the Frontend

The initial plan was to let the frontend interact with the PowerDNS API directly, but this proved architecturally awkward. The frontend was already burdened with too much DNS logic, from converting punycode to translating numeric query types like 1 into human-readable names like A. Relying on it to also understand the complexities of PowerDNS’s zone-based API was a good way to accumulate more untestable, bug-prone JavaScript.

The better path was to move all DNS logic to the Go backend and create a simplified API tailored specifically for the frontend's operations:

  • GET /records
  • DELETE /records/<ID>
  • DELETE /records/
  • POST /records/
  • POST /records/<ID>

This shift meant all the complex logic could live in Go, a language where the developer is comfortable writing and running tests, leading to more reliable code.

Handling Duplicate Information in Output

During the API’s design, a problem arose with displaying MX records. A single API response might describe a record as having preference 10 and mail server mail.example.com. The frontend needs to render this data in two ways: as separate fields in an edit form, and as a single string like "10 mail.example.com" for a summary view. The "clean" solution of returning only structured data forced the frontend to reformat things, complicating its logic.

The breakthrough was the realization that because this API is only used by the frontend, it was perfectly fine to return the same piece of information in two different formats, embedding the pre-formatted display string directly into the response. This allowed the frontend to simply display the string when needed and parse the individual fields for the form, greatly simplifying its code.

{
  values: {'Preference': 10, 'Server': 'mail.example.com'},
  content: '10 mail.example.com',
  ...
}

Establishing Usable Record IDs

A significant source of friction was that the Mess With DNS UI is built around managing individual records, but the PowerDNS API operates on a zone made of record sets. PowerDNS records have no inherent API ID, making it difficult to refer to a specific record for updating or deletion.

To bridge this gap, the application generates a synthetic ID for each record, combining its name, type, and base64-encoded content to form a unique-ish key. For instance, an NS record’s ID might look like brooch225.messwithdns.com.|NS|bnMxLm1lc3N3aXRoZG5zLmNvbS4=. When a user wants to change a record, the API can simply look up that whole string as the key.

Though this design leads to record IDs changing whenever the record content is updated—an unusual quirk for an ID system—it's functionally sufficient for the application's needs.

Translating Technical Errors for Users

The errors returned by the PowerDNS API are developer-centric and not meant for a typical web user. Messages like Name 'new\032site.island358.messwithdns.com.' contains unsupported characters, which encodes a space as \032, or RRset ... Conflicts with pre-existing RRset, which mentions the unfamiliar concept of an RRset, are confusing. Others even suggest utilities like pdnsutil, which are inaccessible to the user.

Mess With DNS now handles these errors in two stages. First, it performs basic validation on user input before it even reaches PowerDNS, catching issues like invalid IP addresses and returning clean messages like Invalid IPv4 address: "1.2.3.4$. If a request still gets through and triggers a PowerDNS error, the application applies a layer of "hacky translation" to its code to map the technical jargon to more user-friendly language. If users do see an untranslated error, it's logged so the translation list can be improved over time.

Refactoring the Data Layer

The project also shed its Postgres database in favor of SQLite, which had been suffering from near-daily OOM kills due to the server being over-allocated. The application's low traffic made SQLite a more appropriate choice, but the switch brought its own set of considerations.

To prevent SQLITE_BUSY errors, the backend now opens only a single database connection. To further reduce contention, the three main tables—users, records, and requests—were placed in separate database files. The implementation also uses the modernc.org/sqlite package, a cgo-free port of SQLite to Go. This choice avoids the difficulties of debugging errors that can come with cgo. While post-deployment backups are not yet set up, a system like litestream isn't considered necessary for this non-critical service; a simple daily backup plan is seen as more than sufficient.

Overhauling the Frontend

The refactor also prompted needed improvements outside of the DNS layer. The frontend was modernized by upgrading it from Vue 2 to Vue 3. This forced a change in form validation libraries, as the previous one had completely changed its API. Instead of learning the new system, the team switched to using built-in browser form tools like required and oninvalid.

State management was also overhauled. The old system relied on calling refresh records manually in every place an API request mutated the data, a pattern that proved easy to get wrong. A single global state management store now handles all application state. Components can call a simple action like store.createRecord(record), letting the store automatically resynchronize the entire view, eliminating the headache of manual, ad-hoc refreshes.

The entire project was sequenced to keep things manageable, with the website deployed between major phases:

  1. Upgrade the frontend framework and state management.
  2. Implement the new, backend-centric API and shift DNS logic away from the client.
  3. Integrate PowerDNS as the authoritative DNS server.

The updated Mess With DNS is now live and operational. Relying on PowerDNS resolved a host of long-standing DNS issues and, perhaps more importantly, removed a whole class of potential problems, freeing the developer to focus on the user experience instead of the low-level accuracy of its DNS engine. The work remains targeted at improving the clarity of system-generated error messages to better serve the user community.