Step 41 of 56
DNS and NTP: portable by construction
Two clients written entirely against GNAT.Sockets, so the same source runs on a desktop and on the board — and one shared concurrency wrinkle worth knowing.
One line to resolve a name
function Resolve (...) return ...; -- Net_Resolver
Net_Resolver turns a host name into an address whatever is
carrying the traffic. Resolution is a real DNS query of our own
— a UDP A-record request that DNS_Client issues over
GNAT.Sockets — so it works identically over
Ethernet, Wi-Fi, cellular, or whatever the routing table points at.
Why not use the modem's own resolver? Because
one was tried and removed. The BG95's AT+QIDNSGIP silently refused
answers whose shape it did not like — a CNAME chain onto several A records
failed where a bare A record resolved. Doing DNS ourselves means one code path
with predictable behaviour, rather than a per-modem set of quirks.
DNS_Client offers both Resolve (UDP) and
Resolve_TCP, the latter for answers too large for a datagram.
NTP_Client.Query is the same shape — a UDP query reading the
transmit timestamp out of the reply — with To_UTC to convert
it.
Portability is the point
Neither client contains anything chip-specific. On the board you call
GNAT.Sockets.Initialize (Device) once during bring-up; on a desktop
sockets are always usable. The same source then compiles in both places, which
is what lets these be tested on a host rather than only on hardware.
The shared wrinkle
Both keep package-global rotors — the transaction id and the default source port for DNS, a source-port counter for NTP. Concurrent calls from several tasks corrupt nothing, but two in-flight queries can land on the same source port, and one then fails its reply check. That surfaces as a failed lookup, not an exception or corruption. If you resolve from more than one task, either serialise the calls or accept the retry.
The source-port rotation is not incidental — a fixed source port is what made an earlier cellular setup fail, because the carrier's NAT poisoned the flow. Rotating ports is a deliberate hardening measure, and the collision above is its small cost.
Encrypted DNS
Plain DNS is readable by anything on the path, and on a shared or hostile
network the names a device looks up leak what it is doing.
DNS_TLS adds the two encrypted transports over the
pure-Ada TLS 1.3 stack:
| Transport | How |
|---|---|
| DoT (RFC 7858) | The ordinary DNS message, inside TLS, on port 853. |
| DoH (RFC 8484) | A minimal HTTP/1.1 POST of
application/dns-message over HTTPS, port 443 —
so it survives networks that block 853. |
The message bytes are the same proven ones every transport shares. Only the carriage differs, so the parser and builder under UDP, TCP, DoT and DoH are one implementation with one set of tests (step 52) — not four chances to get a message wrong.
Trust stays with the application, exactly as in the HTTPS examples: the caller establishes the TCP connection and supplies the trust anchors. The resolver does not carry a built-in root store or decide for you which resolver is trustworthy. Note also that a root-pinned DoT or DoH endpoint may need P-384 rather than P-256, depending on whose certificate chain you are anchoring to.