DNS: turning names into addresses
The dns unit resolves a host name to its IPv4 or IPv6 addresses. By default
it does the work itself: it reads /etc/hosts and /etc/resolv.conf and
asks the configured nameserver over UDP, falling back to TCP when an answer
is too long, so a program needs no C library. The http unit uses it for
every URL with a host name. dns_async does the same inside the coroutine
scheduler, and dns_libc is an optional backend that asks the C library
instead.
Every example here was built with pin v451 (compiler sha256 d9b7226769cc)
through ./pxx and run on Linux x86-64 on 2026-09-29, a machine whose
/etc/resolv.conf points at systemd-resolved. Python's
socket.getaddrinfo was the reference.
Resolving a name
DnsResolveHost(name, ips, count) returns 0 and fills ips[0..count-1],
or returns an error code. Each address is a LongWord in host byte order,
so 127.0.0.1 is $7F000001. A positive code is the DNS server's answer
code (3 means the name does not exist), and a negative one is a local failure
such as a timeout; DNS_ERR_NOCONFIG (-4) means there is no nameserver in
/etc/resolv.conf and no match in /etc/hosts. PXX never falls back to a
public resolver on its own.
program dns_demo;
uses dns, dns_wire_core, sysutils;
function IpStr(ip: LongWord): string;
begin
IpStr := IntToStr((ip shr 24) and 255) + '.' + IntToStr((ip shr 16) and 255) + '.' +
IntToStr((ip shr 8) and 255) + '.' + IntToStr(ip and 255);
end;
var
ips: TDnsIpv4Array;
n, i, k, rc: Integer;
begin
for k := 1 to ParamCount do
begin
rc := DnsResolveHost(ParamStr(k), ips, n);
write(ParamStr(k), ':');
if rc <> 0 then
write(' not resolved (code ', rc, ')')
else
for i := 0 to n - 1 do write(' ', IpStr(ips[i]));
writeln;
end;
end.
./pxx dns_demo.pas dns_demo
./dns_demo localhost example.com no-such-name.invalid
localhost: 127.0.0.1
example.com: 104.20.23.154 172.66.147.243
no-such-name.invalid: not resolved (code 3)
Python's getaddrinfo gave the same addresses for these names, and for
github.com. The addresses you see depend on your network and on the day,
and their order is the order the server sent them in.
DnsResolveHost6(name, ips6, count) is the IPv6 form. Each entry of a
TDnsIpv6Array is 16 bytes, most significant first. For example.com it
returned the same two addresses as getaddrinfo(..., AF_INET6), and for the
literal ::1 it returns ::1 without asking the network.
The resolver follows /etc/resolv.conf's search and ndots settings,
tries each nameserver in turn, and follows CNAME aliases. Answers are cached
for the lifetime the server gave them. The cache is on by default,
DnsCacheSetEnabled(False) turns it off, and DnsCacheFlush empties it. It
is shared by the whole process and not safe for threads, so a program that
resolves names from several threads should turn it off.
DnsResolveService('https', 'tcp', port) looks a service name up in
/etc/services and sets port to 443, as Python's getservbyname does. A
numeric string is returned as a number, and an unknown name gives
DNS_ERR_NOCONFIG.
Choosing a backend
The same DnsResolveHost call can be answered in three ways, chosen when
you build the program:
| Build flags | Who answers | Needs |
|---|---|---|
| none (the default) | PXX's own resolver: /etc/hosts, then the nameservers in /etc/resolv.conf |
nothing |
-dPXX_DNS_RESOLVED |
systemd-resolved, asked directly | systemd-resolved running |
-dPXX_DNS_LIBC -dPXX_DYNLIB_LIBC |
the C library's getaddrinfo |
glibc at run time; the program then links libc.so.6 |
Use the C library backend when names come from somewhere other than DNS:
/etc/nsswitch.conf modules such as mDNS (.local names), LDAP or a VPN's
name service. Only that backend sees them. -dPXX_DNS_LIBC alone stops the
build and names the missing flag: "PXX_DNS_LIBC needs the runtime loader
too: add -dPXX_DYNLIB_LIBC". Choosing two backends at once is a compile
error too.
dns_demo above, built each of the three ways, printed the same addresses
for the same names. The systemd-resolved build listed example.com's two
addresses in the other order.
On the ESP32 (ESP-IDF builds) the default is different: names go to lwIP's
resolver, which holds the nameservers the network's DHCP server handed out.
examples/esp32/dns-c3 shows it.
Resolving without blocking: dns_async
DnsResolveHostAsync(name, ips, count) and DnsResolveHost6Async take the
same arguments and give the same answers, but they are meant to be called
inside a coroutine (see Async). While one waits for the
nameserver, the other coroutines run.
program dns_async_demo;
uses scheduler, dns, dns_async, dns_wire_core, sysutils;
var
names: array[0..1] of string = ('example.com', 'localhost');
results: array[0..1] of string;
ticks: Integer = 0;
done: Integer = 0;
function IpStr(ip: LongWord): string;
begin
IpStr := IntToStr((ip shr 24) and 255) + '.' + IntToStr((ip shr 16) and 255) + '.' +
IntToStr((ip shr 8) and 255) + '.' + IntToStr(ip and 255);
end;
procedure Resolve(arg: Pointer);
var
k, n, rc: Integer;
ips: TDnsIpv4Array;
begin
k := PtrInt(arg);
rc := DnsResolveHostAsync(names[k], ips, n);
if rc = 0 then results[k] := IpStr(ips[0]) + ' (' + IntToStr(n) + ' found)'
else results[k] := 'code ' + IntToStr(rc);
Inc(done);
end;
procedure Ticker(arg: Pointer);
begin
while done < 2 do
begin
Inc(ticks);
CoSleep(1);
end;
end;
begin
Spawn(@Resolve, Pointer(PtrInt(0)));
Spawn(@Resolve, Pointer(PtrInt(1)));
Spawn(@Ticker, nil);
RunUntilDone;
writeln(names[0], ': ', results[0]);
writeln(names[1], ': ', results[1]);
writeln('the ticker ran while they waited: ', ticks > 0);
end.
example.com: 104.20.23.154 (2 found)
localhost: 127.0.0.1 (1 found)
the ticker ran while they waited: TRUE
ips[0] for example.com is either of its two addresses: on the next run
the server listed them the other way round.
Wait with CoSleep, not a CoYield loop. The scheduler checks the
network only when no coroutine is ready to run. A coroutine that loops on
CoYield is always ready, so the lookups never finish: with CoYield in
place of CoSleep(1) above, the program ran at full CPU for five minutes
and was stopped by hand. The waiting on sockets and timers is done with
epoll on x86-64 Linux; the unit's source says other targets fall back to
polling.