Skip to content

DNS stub resolver (dmdns) + nslookup tool - #1

Merged
JohnAmadis merged 3 commits into
mainfrom
claude/zealous-ramanujan-nnsy9w
Oct 5, 2026
Merged

JohnAmadis merged 3 commits into
mainfrom
claude/zealous-ramanujan-nnsy9w

Conversation

@JohnAmadis

@JohnAmadis JohnAmadis commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Until now the DMOD network stack had no way to turn a host name like google.com into an IP address (ping even says so in its help text). This PR turns the dmdns template into a DNS stub resolver (RFC 1034 5.3.1) that runs on top of the existing dmudp → dmip → dmnetbridge → dmnetif stack. It also adds:

  • a dns dmsystem service that brings the resolver up at boot
  • an nslookup CLI tool
          nslookup / any module that has a host name
                            |
                    dmdns_resolve()
                            |
   IP literal -> localhost -> hosts table -> cache -> name servers
                                                         |
                       static (dmdns_add_server())  +  every module
                                                       implementing the
                                                       dmdns_provide_servers DIF
                                                       (e.g. DHCP)
                            |
                 dmudp -> dmip -> dmnetbridge -> dmnetif

Architecture

Each layer can be used and tested on its own (src/):

Layer File What it does
Codec dmdns_codec.c dmdns_build_query() / _parse_response() / _is_valid_name(). Pure functions, no network. Handles compression pointers (with a jump limit) and CNAME chains, and accepts an answer only if its ID and its question section match ours
Literals dmdns_address.c dmdns_parse_address() / _address_to_string() for IPv4 and IPv6 (including ::, RFC 5952 output)
Servers dmdns_servers.c Static list, plus the dmdns_provide_servers DIF
Hosts dmdns_hosts.c Static table (/etc/hosts), with built-in localhost
Cache dmdns_cache.c Keyed by (name, type). Positive entries last for the TTL, NXDOMAIN/NODATA for 30 s. Holds up to 16 entries and evicts the oldest first
Query dmdns_query.c One question to one server. Each query gets its own ephemeral port (dmudp_bind_any) and a random ID. The dmudp handler only copies the reply (at its real length) and posts a semaphore; parsing happens on the calling thread
Resolver dmdns.c dmdns_resolve(): tries a literal, then localhost, hosts, cache, and finally the servers (2 rounds). Definitive answers are cached

Service: configs/dns.ini

dmdns is a Library module, so something has to load and enable it. configs/dns.ini is a dmsystem unit for that:

description=DNS resolver (dmdns)
exec=dmdns
type=library
  • service start dns loads and enables dmdns, service stop dns disables and unloads it. This is the same pattern as dmicmp's icmp.ini.
  • The unit ships in the release package through dmdns.dmr. With dmod-boot: dmdns service=dns.ini.

It is one unit for the whole system, with no [class=netif] rules, unlike networkd@ and dhcp@:

  • The resolver's state (servers, hosts, cache) is global, and the outgoing interface is chosen by routing.
  • Per-interface servers are pulled through the DIF on every lookup, so an interface that appears or disappears needs nothing from this unit.
  • A library module exists only once. dns@eth0 and dns@wlan0 would share the same module, so stopping one of them would unload the resolver for all interfaces.

See docs/service.md.

Memory: nothing is reserved up front

No buffer is sized from a protocol maximum (253-byte names, 512-byte UDP messages), and no thread's stack has to grow for dmdns. Everything is allocated with Dmod_Malloc() at exactly its size, once that size is known:

  • dmdns_build_query() computes the wire length first.
  • dmdns_parse_response() counts the matching records, then allocates an array of exactly that size.
  • Server, hosts and resolve results grow one entry at a time (Dmod_Realloc).
  • dmdns_address_to_string() measures the text before allocating it.

Results belong to the caller, who frees them with Dmod_Free(). nslookup and the tests run with the default 1024-byte stack.

Key decision: name servers come in through a DIF

dmdns owns no network configuration. The servers are known by whoever configured the interface: today that is dmdhcp, which already stores DHCP option 6 (dmdhcp_get_dns_server()); later it could be a modem driver or a static config module. dmdns_get_servers() merges the static list with every module that implements:

typedef int (*dmdns_server_sink_t)( void* sink_ctx, const dmip_addr_t* server );
dmod_dmdns_dif(1.0, void, _provide_servers, ( dmdns_server_sink_t add, void* sink_ctx ));

A provider calls add() once per server. There is no caller-sized array and no limit on the count.

Providers are discovered on every lookup (Dmod_GetNextDifModule), the same way dmip finds its protocol handlers. This gives:

  • no link-time dependency in either direction
  • no stale server list (an expired lease or an unloaded module simply stops being reported)
  • no dependency cycle

The full rationale is in docs/dmdns.md.

nslookup

nslookup [-4|-6] [-t timeout_ms] <name> [server]
  • Without [server] it goes through dmdns_resolve(), the same path as any other module.
  • With [server] it asks only that server, via dmdns_query().

Tests

  • test_dmdns covers:
    • name validation and the exact bytes of a built query
    • parsing of hand-built answers: multiple A records with min TTL, CNAME with compression, AAAA, NXDOMAIN/SERVFAIL/REFUSED/NOTIMP/NODATA/TC, wrong ID, foreign question, truncated message, pointer loop
    • real answers captured from 8.8.8.8 (www.github.com CNAME→A, ipv6.google.com CNAME→4×AAAA)
    • IPv4/IPv6 literals
    • servers, and DIF provider discovery through a dmdns_test_provider fixture module (a Library-type module, the same pattern dmip uses)
    • hosts, localhost, literals
    • resolving with no servers configured, and an unreachable server (must fail fast, not hang)
  • test_nslookup runs the real tool through Dmod_RunModule().
  • CI now also runs test_nslookup.

Locally, both tests pass under ctest with dmf-get and dmod_loader built from dmod develop. I also checked that the release package contains configs/dns.ini. The unit itself has not been started under libsystemd on a board yet.

Worth knowing

  • dmod runtime pitfall: the loader relocates only the GOT, not pointers in .data. If the DIF signature (const char* const) is defined in a different TU from the one that uses it, it ends up as an unrelocated pointer and segfaults. So it is defined in dmdns_servers.c, the only file that uses it, as dmip and dmnetbridge already do.
  • Limits:
    • UDP only: no TCP fallback, EDNS0 or DNSSEC
    • IPv6 servers are skipped until dmudp_send() gets an IPv6 path; AAAA records are still resolved
  • dmdns_resolve() and dmdns_query() block, so they must not be called from a network receive callback.

Proposed next steps (separate PRs)

  1. dmdhcp: implement dmdns_provide_servers for leases in BOUND/RENEWING/REBINDING (there is a sketch in docs/dmdns.md). After that, DNS works on the board with no configuration.
  2. dmod-boot: add dmdns service=dns.ini and nslookup to flash.dmd, and add a DNS responder to test_renode_network.py. This comes after the dmdns release.
  3. ping (dmicmp): accept host names via dmdns_resolve().

🤖 Generated with Claude Code

https://claude.ai/code/session_01SVi4u4Lf8HPw1emy5uyGSB

claude added 3 commits October 5, 2026 13:09
dmdns resolves host names to IPv4/IPv6 addresses over the existing
dmudp/dmip stack:

- codec: RFC 1035 query build and response parse (compression pointers,
  CNAME chains, answer validated against ID and question)
- address literal parse/format (IPv4, IPv6 incl. "::")
- name servers: static list plus a dmdns_provide_servers DIF, so a DHCP
  client (or any other module) can supply servers without a link-time
  dependency in either direction
- hosts table with built-in localhost, TTL cache with negative entries
- dmdns_query(): one ephemeral UDP port and random ID per query
- dmdns_resolve(): literal -> localhost -> hosts -> cache -> servers
- tools/nslookup CLI
- tests incl. real 8.8.8.8 answers and a DIF provider fixture module

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVi4u4Lf8HPw1emy5uyGSB
No buffer is sized from a protocol maximum any more - everything is
allocated with Dmod_Malloc() at exactly the size it needs:

- dmdns_build_query() computes the wire length and allocates it
- dmdns_parse_response(), dmdns_query(), dmdns_resolve() and
  dmdns_get_servers() return address arrays of exactly the found size
- dmdns_format_address() is replaced by dmdns_address_to_string()
- the dmdns_provide_servers DIF reports servers through a callback
  instead of filling a caller-sized array
- hosts/cache lookups return heap copies

DMDNS_MAX_QUERY_LEN, DMDNS_MAX_MESSAGE_LEN, DMDNS_MAX_SERVERS,
DMDNS_MAX_ADDRESSES and DMDNS_ADDRESS_STRLEN are gone; nslookup and the
tests are back to the default 1024-byte stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVi4u4Lf8HPw1emy5uyGSB
configs/dns.ini (type=library) loads and enables dmdns at boot and
disables/unloads it on "service stop dns", the same way dmicmp's icmp.ini
does. It is a single system-wide unit without device rules: the
resolver's state is global, and per-interface name servers are pulled
through the dmdns_provide_servers DIF at lookup time. The unit ships in
the release package via dmdns.dmr; docs/service.md describes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVi4u4Lf8HPw1emy5uyGSB
@JohnAmadis
JohnAmadis merged commit 63b9c2e into main Oct 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants