resolvd
The stub resolver as built: how resolvd takes its scopes from netd and its fallbacks from the registry, routes, expands, caches and forwards every question, and answers it at the native socket, the 127.0.0.53 stub listener and the NSS shim.
Single-page view · as markdown
1.1 Overview
Peios / Peios technical reference / resolvd / Introduction
resolvd is the Peios stub resolver. It is the one process on a machine that answers names: a DNS client that answers from what it knows about the machine, from its cache, or by forwarding to an upstream server. It is not a recursive resolver and not an authoritative server.
The contract it implements is the Name Resolution Interface, PSPU §6. This manual describes resolvd 0.1.5 as built: the order it does things in, the limits it applies, and what it does where the contract leaves a choice.
1.1.1 Three doors, one engine #
A question reaches resolvd by one of three doors (PSPU §6.3):
| Door | Where | Who uses it |
|---|---|---|
| Native socket | /run/resolvd/resolv.sock | The resolv command, the NSS shim, Peios tools |
| Stub listener | 127.0.0.53:53, UDP and TCP | Programs that speak DNS themselves |
| NSS shim | libnss_peios_net.so.2, loaded into every glibc process | getaddrinfo, gethostbyname and friends |
The shim is not a separate resolver: it is a client of the native socket. The native socket and the stub listener both hand their questions to the same engine inside the daemon, and the engine is the only thing that decides an answer.
1.1.2 What is surprising about it #
It owns no network configuration. The servers, search domains and
addresses of each interface arrive from netd over netd's control socket,
as a whole snapshot each time anything changes. The registry holds only
the fallbacks — servers to use when no interface offers any, extra
search domains — and the static names that replace /etc/hosts. resolvd
never writes /etc/resolv.conf; the file it ships is a constant that
points at the stub listener.
A question goes to one interface. Each interface's contribution is a scope, and every name is routed to exactly one of them. Nothing is ever asked of every interface's servers at once. A single-label name is never sent upstream bare, and the cache is partitioned by scope, so an answer learned through a VPN disappears when the VPN's servers change or the VPN goes away.
One thread, and an engine with no I/O. The daemon is a single
poll loop. The engine inside it is a pure state machine: it takes
questions, upstream replies and the passage of time, and returns actions
— bytes to send, transactions to abandon, answers to deliver. Every
socket is the loop's. Everything is therefore serialised, which is why a
reply that cannot be written promptly holds up every other client
(§9.4).
It holds no privilege. resolvd runs under its own virtual service account. Port 53 is reached through a port reservation granted to its service SID, not through a capability.
It does not wait for the network. resolvd reports itself ready as
soon as its two listening doors are open. localhost, the machine's own
name and the registry's static names are answered with no network and no
netd at all.
It does not validate. No DNSSEC validation is performed, and every
answer from every door is reported unvalidated.
1.2 What This Manual Covers
Peios / Peios technical reference / resolvd / Introduction
This manual covers the resolvd source tree as of release 0.1.5:
1.2.1 Other packaged files #
dev.peios.resolvd also ships the constant /usr/etc/resolv.conf, the
service definition and port reservation seeds under /usr/share/regim/,
the registry reference /usr/share/regman/resolvd.regman, and the
resolvd(8) page; dev.peios.resolv ships resolv(1).
Each package has a matching -debuginfo package, and
dev.peios.resolvd-debugsource carries the sources for all three.
1.2.2 The source tree #
The source is one workspace of five crates, and the split is visible in what each part of the system can do:
| Crate | What it is |
|---|---|
dns | The DNS wire codec: names, messages, records. No I/O and no policy. |
libresolv | The native channel's types, MessagePack codec and framing. No I/O beyond framing. |
resolvd | The engine, the cache, the stub door's DNS rendering, and the daemon around them. |
resolv | The operator command. |
nss | The shim. Built on libresolv and libc, without libpeios (§7.1). |
1.2.3 Covered elsewhere #
The contract. What any resolver standing in resolvd's place has to do is PSPU §6: the doors (PSPU §6.3), the native channel's framing and control object (PSPU §6.4), the requests (PSPU §6.5), outcomes and validation (PSPU §6.6), the resolution model (PSPU §6.7), the stub door (PSPU §6.8), the network manager channel (PSPU §6.9), the shim (PSPU §6.10), and the limits (PSPU §6.B). Where a chapter here touches the contract it cites it and adds only what resolvd does.
netd's side of the channel. Which interfaces appear in a snapshot, how their servers and domains are merged from profile and lease, and when a snapshot is sent, are netd's, and are described in the netd TRM. This manual covers what resolvd does with a snapshot once it has one.
Kernel mechanisms. Registry watches, access checks, file descriptors and port reservations are described in the Peios Kernel TRM. This manual says which of them resolvd uses and how.
1.3 Terminology
Peios / Peios technical reference / resolvd / Introduction
The protocol's terms — name, question, record, scope, route, outcome, source, door, synthetic answer, control object, stub door — are used as PSPU §6.2 defines them. The terms below are resolvd's own.
Task. One question the engine is working on: a name and a record
type. A resolve or reverse request, and every stub query, is one
task. A lookup is one task per address family it asks for.
Candidate. One name a task asks. A multi-label name has exactly one candidate, itself. A single-label name has one candidate per applicable search domain (§4.3), asked in turn.
Transaction. One query sent to one server over one transport, with its own DNS message identifier, its own case pattern, its own socket and its own deadline.
Attempt. A transaction counted against a candidate's attempt limit. Every UDP transaction is an attempt; the TCP transaction that follows a truncated reply is not (§4.6).
Routable scope. In source b4f7085,
a scope that is up — at level link or better, in
the sense of PSPU §6.2 — and has at least one server. Only routable
scopes take part in routing and contribute search domains (§4.3, §4.4);
an up scope with no servers takes part in neither.
The proposed source correction separates
being up from having DNS servers, as PSPU §6.2 requires. All up scopes
participate in search expansion and routing steps 1–4, with step 1 still
requiring addressed or better and the exclusive flag. Only the ordinary
lowest-metric choice in step 5 requires servers (§4.4). This is proposed
source behavior, not a released package or an installed resolver; check
the installed source revision before relying on it.
Scope key. The string that identifies a scope in the cache and in
routing: the interface's ifid from netd, or fallback for the
registry's fallback servers.
Demoted server. A server address whose last failure was less than 30 seconds ago, and which has not given a usable reply since; it is tried after its healthy peers (§4.6).
In-flight ceiling. The bound on outstanding upstream transactions,
beyond which a question that needs the network is answered unavailable
at once (§4.6).
The loop. resolvd's single thread and its poll (§2.5).
2.1 The Service
Peios / Peios technical reference / resolvd / Startup And Configuration
2.1.1 The service definition seed #
resolvd is started by peinit from Machine\System\Services\resolvd. The
package ships the definition as a registry seed,
/usr/share/regim/resolvd-service.reg, which is inert until an image
names it in its [registry] autoapply list.
2.1.2 The service definition values #
2.1.3 Identity and privilege #
resolvd holds no privilege and needs none. Its one claim on the machine that an ordinary process could not make — binding UDP and TCP port 53 — is met by a port reservation rather than a capability (PSPU §6.8).
Without the reservation applied, the stub listener cannot bind and resolvd exits at startup (§2.2, §9.3).
2.1.3.1 The port reservation seed #
The reservation is a second seed, /usr/share/regim/resolvd-port.reg,
also inert until an image applies it. It writes one value:
| Key | Value | Type |
|---|---|---|
Machine\System\Network\TcpIp\PortReservations | tcp,udp:53 | REG_BINARY |
2.1.3.2 Who holds the reservation #
The data is a security descriptor granting the reservation to SYSTEM and
to resolvd's service SID,
S-1-5-80-3864064249-1823296737-2008945602-1354971773-2894779966, so it
follows the service rather than whichever account the service happens to
run as.
The selector tcp,udp:53 is more specific than the kernel package's
tcp,udp:1-1023 and so decides port 53 alone. The descriptor bytes are
generated from the SID by a tool in the source tree, not written by
hand.
2.1.4 Readiness #
With Readiness 0 peinit waits for a notification. resolvd:
- sends the datagram
READY=1to the socket named by theNOTIFY_SOCKETenvironment variable once the native socket and both stub sockets are open and the registry has been read; - does not wait for netd: readiness is sent after the first attempt to reach netd whether that attempt succeeded or not;
- sends nothing when
NOTIFY_SOCKETis unset, and logs a failure to send asreadiness notify: <error>and carries on starting.
2.1.5 Restart #
RestartPolicy 2 restarts resolvd whenever it exits. A restarted
resolvd removes the socket file its predecessor left, logs
removed a stale /run/resolvd/resolv.sock, and binds a fresh one
(§2.4). Everything resolvd holds is in memory, so a restarted resolvd
has:
- an empty cache;
- no demoted servers;
- every counter at zero (§4.10);
- no scopes, and the kernel's hostname, until the first snapshot after netd is reached again (§3.3).
2.1.6 Log lines #
2.1.6.1 The log line format #
Every line resolvd logs has the form
resolvd: <level>: <text>
where the level is info, warn or error. Informational lines go to
standard output; warnings and errors go to standard error. peinit forwards
both streams to eventd, marking only standard error as is_error=true.
The lines are read with:
2.1.6.2 The kernel log mirror #
Each line is also offered to /dev/kmsg, at kernel priority 6, 4 or 3
for info, warn and error. The mirror is opened when the first line is
logged, after that line has been written to its output stream, and the open
is never retried.
/dev/kmsg is writable by SYSTEM only, so for resolvd the open fails,
and resolvd writes one line to standard error saying so, directly after
the first log line:
resolvd: warn: /dev/kmsg mirror unavailable (<error>): log lines go to stdout/stderr only
The mirror warning follows the first log call. That first line may be on
stdout or stderr, so readers must not assume a cross-stream ordering.
On a start that fails, the first logged line is the fatal one, such as
native socket: <error> (§2.4).
On a start that succeeds, it is a warning from earlier in startup when
there is one, such as removed a stale /run/resolvd/resolv.sock (§2.4)
or a Dns … warning (§2.3), and otherwise the result of
the first attempt to reach netd, subscribed to netd or
netd not reachable (<error>); retrying, which comes before
listening on … (§2.2). The mirror line is written once, and nothing
further about the mirror is written. resolvd's lines never reach the console or
dmesg.
2.2 Startup Sequence
Peios / Peios technical reference / resolvd / Startup And Configuration
2.2.1 The sequence #
- The native socket. The runtime directory and the socket are
created and given their modes and descriptors (§2.4). A failure to
create the directory, set a mode, remove a stale socket file, bind,
or make the listener nonblocking is fatal: resolvd logs
native socket: <error>at error level and exits with status 1. A failure to write a descriptor is not fatal (§2.4). - The stub listener. A UDP socket and then a TCP listener are bound
to
127.0.0.53:53. A failure of either is fatal: resolvd logsstub listener on 127.0.0.53:53: <error>and exits with status 1. - The registry.
Machine\System\Network\Dnsis read whole (§2.3). A missing or unreadable key is not an error; every value takes its default. - The registry watch. A subtree watch is armed on
Machine\System\Network(§2.3). If it cannot be armed, resolvd logsregistry watch unavailable (<error>); configuration is read onceand runs on the configuration it read in step 3 until it restarts. - Query entropy. The engine is given a kernel-randomness provider.
Every upstream transaction obtains fresh bytes through nonblocking
getrandom; startup does not seed a userspace generator or read/dev/urandom. There is no clock fallback. - The engine's inputs. The engine is given the kernel's hostname,
the fallback servers and search domains, and the static names. A
kernel hostname that is empty or
(none)counts as no hostname. - The control object is built from
ControlSecurity(§5.2). - netd. The first attempt to subscribe is made immediately (§3.1). Its failure is not fatal and is retried with backoff (§3.2).
- The listening line. resolvd logs
listening on /run/resolvd/resolv.sock and 127.0.0.53:53at info level. - Readiness.
READY=1is sent (§2.1). - The loop is entered (§2.5).
2.2.2 Answering from the moment of readiness #
Both doors are open, and answering, before readiness is reported, and
neither waits for netd. A question asked the moment resolvd is ready is
answered at once: synthetic and static names are answered, and anything
that needs a server is answered unavailable until either a snapshot
arrives or fallback servers are configured (§3.2).
2.2.3 Exiting #
resolvd has no shutdown path of its own. Apart from the startup failures above, nothing a client or an upstream server sends makes it exit.
2.2.3.1 Ending on a signal #
resolvd installs no signal handlers, so it ends when peinit signals it,
with the default disposition, and leaves /run/resolvd/resolv.sock
behind. The next start removes the stale socket file before binding
(§2.4).
2.2.3.2 A poll failure exits #
When poll fails with anything other than EINTR, resolvd logs
poll: <error> at error level and exits with status 1. EINTR is
retried.
2.3 Registry Configuration
Peios / Peios technical reference / resolvd / Startup And Configuration
The registry holds resolvd's fallbacks and its static names, under
Machine\System\Network\Dns; which keys a resolver reads is fixed by
PSPU §6.9. Nothing about the live network is read from the registry;
that arrives from netd (§3).
2.3.1 The Dns key and its defaults #
The key is opened for query and enumerate-subkey access. When it does not exist or cannot be opened, every value below takes its default.
| Value | Type read | Default | Used for |
|---|---|---|---|
FallbackServers | REG_MULTI_SZ, or one REG_SZ / REG_EXPAND_SZ | none | The fallback scope's servers (§4.4) |
ExtraSearchDomains | REG_MULTI_SZ, or one REG_SZ / REG_EXPAND_SZ | none | Search domains after every interface's (§4.3) |
ControlSecurity | REG_BINARY | the compiled default | The control object (§5.2) |
Hosts\ | a subkey | empty | Static names (§4.2) |
2.3.2 Reading strings #
- A
REG_SZorREG_EXPAND_SZis one string: its data up to the first NUL, read as UTF-8. When those bytes are not UTF-8, the value is treated as absent, with no log line. - A
REG_EXPAND_SZis read as it is stored; nothing in it is expanded. - A
REG_MULTI_SZis split at every NUL. Empty strings are skipped, and a string that is not UTF-8 is skipped silently, with no log line, while the other strings are used. - A value of any other type is treated as absent.
2.3.3 FallbackServers #
- Each string is trimmed of surrounding whitespace and parsed as an IPv4
or IPv6 address. A string that does not parse is skipped and logged as
Dns FallbackServers: ignoring malformed address "<string>", with the string as stored — untrimmed, in double quotes, with quotes, backslashes and control characters escaped. A loneREG_SZthat is empty or only whitespace, a whitespace-only string in a multi-string, and an address with a zone such asfe80::1%eth0(§4.7) are all logged this way. - There is no way to give a port; every server is asked on port 53.
The order of the list is the order servers are tried in (§4.6).
2.3.4 ExtraSearchDomains #
Each string is parsed as a domain name, with or without a trailing dot.
It is not trimmed, so a leading or trailing space becomes part of a
label. A string that does not parse, or that is the root — a lone empty
REG_SZ, or . — is skipped and logged as
Dns ExtraSearchDomains: ignoring malformed domain "<string>", quoted
as for FallbackServers.
2.3.5 ControlSecurity #
- A
REG_BINARYvalue with at least one byte is taken as a self-relative security descriptor. An empty value, a value of another type, or an absent value selects the compiled default, with no log line. - Bytes that do not form a valid descriptor also select the compiled
default, and resolvd logs
ControlSecurity is not a valid descriptor (<error>); using the defaulteach time it builds the control object from them: at startup, and at every configuration change (below).
2.3.6 Hosts\ #
- Every value under
Machine\System\Network\Dns\Hostsis one static name. The value's name is the host name and its data is one address (REG_SZorREG_EXPAND_SZ) or several (REG_MULTI_SZ), read as above. - A value name that is not UTF-8 is skipped silently. The registry refuses such a name when the value is written, so the check is defensive.
- A value name that does not parse as a domain name is skipped and
logged as
Dns Hosts: ignoring malformed name "<name>". - A value name that parses to the root — an empty name, or
.— is skipped silently. - Each address is parsed as for
FallbackServers, and a malformed one is skipped and logged asDns Hosts: ignoring malformed address "<string>". - A name left with no valid address, including one whose value has any other type, is not a static name at all.
- Names match case-insensitively, so
Printerandprinterare the same static name.
Where the same address is listed under several names, the reverse lookup
of that address returns one of them (§4.2). A static name named
localhost, or under it, never answers a forward question (§4.2).
2.3.7 Changes at run time #
2.3.7.1 The watch on the network key #
resolvd watches Machine\System\Network, recursively and for every kind
of change, rather than the Dns key itself: the Dns key need not
exist at boot, and a watch on a missing key cannot see it appear. Watch
events are read into a 16 384-byte buffer.
2.3.7.2 What triggers a re-read #
Every non-empty batch of watch events, from anywhere under
Machine\System\Network, causes the whole Dns key to be read again,
as at startup. netd's own writes under that key cause such re-reads too.
Each re-read logs the Dns FallbackServers, Dns ExtraSearchDomains
and Dns Hosts warnings above again for every malformed string still
present, whether or not anything has changed.
2.3.7.3 An unchanged configuration #
The fresh configuration is compared with the one in use. When nothing differs, nothing is applied and nothing more is logged.
2.3.7.4 A changed configuration #
When anything differs, resolvd, in this order:
- rebuilds the control object from
ControlSecurity; - replaces the fallback servers and search domains, discarding every answer cached through the fallback scope if the server list differs in content or in order;
- replaces the static names;
- logs
configuration changedat info level.
2.3.7.5 When a change takes effect #
- Every change applies to the questions and requests that arrive after it; nothing needs a restart.
- Static names are never cached, so a new or removed
Hosts\value is visible to the next question. - Answers already cached through an interface scope are not discarded by a registry change.
2.3.7.6 When the watch fails #
- If reading watch events fails, resolvd logs
registry watch: <error>; re-armingand arms a new watch. The key is not read again when the new watch is armed, so a change made in between is applied only with the next event. - If arming the new watch also fails, watching stops without a further message, and the configuration in use stays as it is until resolvd restarts.
2.4 Sockets and Descriptors
Peios / Peios technical reference / resolvd / Startup And Configuration
2.4.1 The native socket #
The native door is a SOCK_STREAM Unix socket at
/run/resolvd/resolv.sock (PSPU §6.4), inside resolvd's runtime
directory rather than directly in /run, which resolvd's account cannot
write.
At startup resolvd:
- creates
/run/resolvdif peinit has not, and sets its mode to0755; - replaces the directory's DACL (below);
- removes any existing
/run/resolvd/resolv.sock, loggingremoved a stale /run/resolvd/resolv.sockat warn level when there was one, and a removal that fails is fatal (§2.2); - binds the socket, sets its mode to
0666, and replaces its DACL in the same way; - makes the listener nonblocking.
2.4.1.1 A socket left by a previous run #
resolvd leaves its socket behind when it ends (§2.2), so a start that
follows an earlier run finds one unless something else has removed it.
The previous run gave its own account full access to the directory and
the socket (below), so the new process removes the file in step 3, logs
the removed a stale … line, and binds a fresh socket. This holds for
every restart of the service: svctl restart, a stop and a start, and
a restart by the policy after resolvd exits or is killed.
2.4.1.2 The DACL on the directory and socket #
The DACL written to both the directory and the socket is:
| Trustee | Access |
|---|---|
| SYSTEM | GENERIC_ALL |
| resolvd's own account (its token's user) | GENERIC_ALL |
| Everyone | GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE |
resolvd's own entry is what lets the next run start. Everyone's read,
write and execute include neither DELETE nor deleting a child, so
without it the next start could not remove the stale socket: it would
exit with native socket: Permission denied (os error 13), and the
restart policy would loop on that until the file was removed by hand.
If resolvd cannot read its own identity it logs at warn level
could not read resolvd's own identity (<error>); a restart may not be able to replace <path>
and writes the DACL without that entry.
2.4.1.3 The owner of the directory and socket #
Only the DACL is written, so each object keeps the owner it was created
with. peinit creates /run/resolvd before resolvd starts (§2.1), so the
directory is owned by SYSTEM; resolvd creates the socket, which is owned
by resolvd's account. Reaching the socket grants nothing: every request
is checked against the control object (§5.2).
2.4.1.4 A descriptor that cannot be written #
If the descriptor cannot be written, resolvd logs at error level
could not set a descriptor on <path> (<error>); programs other than SYSTEM and administrators will not be able to reach the socket
and carries on. The socket then keeps whatever descriptor it was created with, and for ordinary processes the native door — and with it the NSS shim — is unreachable while the stub listener still works (§9.3).
2.4.2 The stub listener #
- Two sockets, a UDP socket and a TCP listener, are both bound to
127.0.0.53port 53 and both nonblocking (PSPU §6.8). - Nothing is bound on
::1or on any other address.
Binding port 53 depends on the port reservation (§2.1).
2.4.3 Other descriptors #
| Descriptor | Count | Lifetime |
|---|---|---|
The registry watch on Machine\System\Network | 0 or 1 | From startup; replaced on a watch error (§2.3) |
| The netd channel | 0 or 1 | While subscribed (§3.1) |
| Native connections reading or awaiting resolution | up to 256 total | Reading: at most 5 s (§5.1); resolving: until completion or definite departure (§4.1) |
| Stub TCP connections reading or awaiting resolution | up to 256 total | Reading: at most 10 s (§6.1); resolving: until completion or definite departure (§4.1) |
| Queued native reply writers | up to 256 | Until written, failed, or the one-second write deadline (§2.5) |
| Queued stub TCP reply writers | up to 256 | Until written, failed, or the one-second write deadline (§2.5) |
| Upstream sockets | one per transaction, at most 4 096 including retries | One transaction (§4.7) |
2.4.3.1 Connections whose request has been read #
A connection whose request has been read keeps occupying a slot in its door's 256-connection allowance until resolution completes or a definite hangup/error abandons it. The engine's attempts and timeouts (§4.6) govern this phase; the request-delivery deadline no longer applies. A completed answer that cannot be written immediately moves to the separate bounded reply-writer queue (§2.5).
UDP questions share the one listener socket, but retain at most 256 waiting origins in total and 16 per source address and port (§6.1).
2.5 The Event Loop
Peios / Peios technical reference / resolvd / Startup And Configuration
2.5.1 One thread and one poll #
resolvd runs on one thread. Every input is a descriptor in one poll
call, and the engine sees time only when the loop hands it the current
instant.
2.5.2 What is polled #
Each iteration builds the poll set afresh:
- the native listener, except during its accept-error backoff;
- the stub UDP socket;
- the stub TCP listener, except during its accept-error backoff;
- the netd channel, while connected;
- the registry watch, while armed;
- every native connection still sending its request;
- every stub TCP connection still sending its query;
- every upstream socket — for reading, or for writing while a TCP connection is still being established or its query is still being sent;
- native and stub TCP replies waiting to write, for writability;
- native and stub TCP streams awaiting engine answers, for hangup and error indications only. Readability and read-half-close are not requested, preserving supported half-closes without spinning on EOF.
2.5.2.1 The poll timeout #
The poll timeout is the earliest of: the next upstream transaction deadline, the next netd reconnection time while disconnected, the moment the oldest stub TCP connection reaches its 10-second bound, the moment the oldest native connection reaches its 5-second bound, the earliest queued reply write deadline, and the end of either listener's 250 ms accept-error backoff. With none of these pending, the loop sleeps until a descriptor is ready.
The timeout is the time to that moment in whole milliseconds, rounded down. A moment less than a millisecond away gives a timeout of zero, so the loop polls again without sleeping until the moment has passed.
2.5.3 Service order #
After poll returns, ready descriptors are serviced in this order:
- waiting native and stub TCP streams with definite hangup/error, cancelling their questions and upstream transactions;
- upstream sockets — replies that have arrived are answers already owed;
- queued native replies, then queued stub TCP replies, whose sockets are writable;
- the stub UDP socket, reading at most 64 datagrams;
- the stub TCP listener, accepting at most 64 connections;
- stub TCP connections still sending queries;
- the native listener, accepting at most 64 connections;
- native connections still sending requests;
- the netd channel;
- the registry watch.
The fixed datagram/accept batches yield to the other doors and timers even while one listener remains continuously ready.
2.5.4 Timers after descriptors #
Once the ready descriptors have been serviced, the timers run, in this order: transactions at or past their deadline fail over to their next attempt (§4.6), a due netd reconnection is attempted (§3.2), stub TCP connections 10 seconds old or older are closed, expired reply writes are closed, and native connections 5 seconds old or older are closed.
2.5.5 Answers within an iteration #
A question that can be answered without the network — synthetic, from the cache, or refused — has its answer prepared and its first write attempted within the iteration that read it.
A question that needs the network sends its first transaction within that iteration too, and has its answer prepared in the iteration that sees its deciding reply or its last deadline. Delivery may continue over later writable events for a slow native or TCP reader.
2.5.6 Nonblocking reply writes #
Native and stub TCP replies get one nonblocking write attempt immediately. If the socket cannot accept the whole frame, the unwritten suffix is queued and resumed when that socket is writable. Each ready event makes one write attempt; partial progress is retained without duplicating bytes. The connection closes after the whole frame is written, on a write error, or when its fixed one-second write deadline expires. Progress does not extend that deadline. Other inputs and timers continue to be serviced.
At most 256 unfinished native replies and 256 unfinished stub TCP replies are queued. Each frame is bounded by its protocol ceiling (§5.1, §6.2). Even when a queue is full, a new reply gets its immediate write attempt; if it too needs queue space, that connection closes. Ready readers can therefore still receive replies while stalled readers occupy the queue.
Stub UDP replies and upstream queries are not written this way. The netd subscription request is written with a two-second timeout when the channel is opened (§3.1).
3.1 Subscription
Peios / Peios technical reference / resolvd / The Netd Channel
The channel is the subscribe request of PSPU §6.9, on netd's control
socket. This chapter describes resolvd's end of it; what netd puts in a
snapshot and when it sends one is described in the netd TRM.
3.1.1 Connecting #
3.1.1.1 The subscribe request #
When a connection attempt is due (§3.2), resolvd:
- connects to
/run/netd/control.sock; - sets a two-second write timeout and writes the
subscriberequest in netd's framing — a four-byte little-endian length and a MessagePack map; - switches the socket to nonblocking mode.
The request needs netd's NETWORK_QUERY right for resolvd's account;
whether that is granted is netd's decision.
3.1.1.2 A failed attempt #
A failure at any of the three steps is a failed attempt, and the next one is scheduled by the backoff of §3.2.
3.1.1.3 A successful attempt #
On success resolvd logs subscribed to netd at info level, discards
anything buffered from an earlier connection, and waits for the first
snapshot as an ordinary read on the channel. Only a successfully decoded
snapshot resets its backoff (§3.2); connection success alone does not.
3.1.1.4 Nothing else is written to netd #
resolvd writes nothing more on the channel after the request.
3.1.2 Reading the stream #
When the channel is readable, resolvd reads everything available, in 8 192-byte chunks, and then takes every complete frame from what it holds, in order:
3.1.2.1 Frames behind an error are still taken #
An error reply or an unreadable payload does not stop the frames behind it. Every complete frame already read is still taken, in order, and any snapshot among them is applied. The same holds when the read ended at end of file or a read error: the frames read before it are all taken. Only a length above 65 536 stops the taking, and the frames before it have been taken by then.
3.1.2.2 The channel is dropped before the read's snapshots are applied #
When a read ends with the channel to be dropped, it is dropped — and
lost the netd channel; reconnecting logged — after every frame has
been taken and before any snapshot from that read is applied. In the
log, the lost … line therefore comes before the netd: … summary
lines (§3.3) of the snapshots read with it, and status then shows
netd as false together with the scopes of those snapshots, until
the channel is reconnected.
3.1.2.3 A partial frame #
A partial frame stays buffered until the rest arrives.
3.1.2.4 End of file or a read error #
End of file, or any read error other than EINTR or EAGAIN, drops the
channel. EINTR is retried, and EAGAIN ends the read.
3.1.2.5 Dropping the channel #
Dropping the channel closes the socket, discards the buffer, logs
lost the netd channel; reconnecting at warn level — once, however many
of the causes above occurred in the read — and schedules a reconnection
(§3.2).
3.2 Reconnection and Backoff
Peios / Peios technical reference / resolvd / The Netd Channel
Failure and reconnection are covered by PSPU §6.9. resolvd's backoff starts at 0.5 seconds and doubles to a ceiling of 10 seconds.
3.2.1 The backoff sequence #
The first attempt is made during startup, before readiness (§2.2). Each failed attempt schedules the next one the current backoff later, then doubles the backoff up to 10 seconds. Measured from a first attempt that fails, attempts are made at:
| Attempt | Time after the first |
|---|---|
| 2 | 0.5 s |
| 3 | 1.5 s |
| 4 | 3.5 s |
| 5 | 7.5 s |
| 6 | 15.5 s |
| 7 and later | every 10 s |
3.2.2 The unreachable warning #
A failed attempt made before any connection has succeeded is logged as
netd not reachable (<error>); retrying when it is the first failure,
and not logged otherwise. Together with the rule after a loss (below),
the warning is written at most once in the life of the process: for the
first failure, when that failure comes before the first successful
connection. A resolvd whose first attempt succeeds never writes it.
3.2.3 A good snapshot resets the backoff #
A successfully decoded snapshot resets the backoff to 0.5 seconds. Connecting, writing the subscription, or receiving another reply kind does not reset it. A partial snapshot does not reset it until complete.
3.2.4 After a loss #
When an established channel is dropped (§3.1):
- the next attempt is due after the current backoff, then the delay doubles up to 10 seconds. After a channel that delivered a snapshot, successive gaps are 0.5, 1, 2, 4, 8, then 10 seconds. A connection lost before its first snapshot continues the existing sequence;
- the
netd not reachablewarning is not logged for the failures that follow, however many there are; thelost the netd channel; reconnectingline that preceded them is the record.
3.2.4.1 A refused subscription also backs off #
A netd that accepts the connection and then answers the subscription
with an error has not delivered a snapshot. Its retries follow the same
growing delay, up to ten seconds, as other failures. Each connection
still logs subscribed to netd at info level and two warnings (§9.4).
An accept-then-close or malformed reply likewise preserves the growing
delay until a good snapshot arrives.
3.2.5 Disconnected from netd #
Losing netd changes nothing in the engine. The scopes from the last
snapshot, the hostname taken from it, and every cached answer stay in
use until the next snapshot replaces them; questions are answered
exactly as before, and the servers in those scopes are still asked.
status reports netd as false (§5.3).
3.2.6 Before the first snapshot #
When resolvd has never received a snapshot it has no scopes. It answers
synthetic and static names, uses the fallback servers if any are
configured, and answers everything else unavailable (§4.4).
3.3 Applying a Snapshot
Peios / Peios technical reference / resolvd / The Netd Channel
Each snapshot replaces resolvd's scopes whole (PSPU §6.9). Applying one has three parts: the hostname, the scope list, and the cache.
3.3.1 The hostname #
- The snapshot's
hostnameis used when it is not empty. When it is empty, resolvd reads the kernel's hostname at that moment, into a 256-byte buffer; a kernel hostname that is empty or(none)counts as none. - The result is parsed as a domain name. A name that does not parse, or
the root, leaves resolvd with no hostname: no synthetic answer is
given for it, and
statusreports an emptyhostname. - The kernel's hostname is otherwise read only at startup. A change to
it is not seen until the next snapshot with an empty
hostname.
3.3.2 The scope list #
Each DnsScope in the snapshot becomes one scope, in snapshot order:
The ifid is used as it arrives. netd's interface ids are UUIDs, so
none is fallback, the key of the fallback scope (§4.4); a scope that
did arrive with that key would share the fallback scope's cache
entries, would be asked through the fallback servers, and would lend
its interface name to the fallback scope's answers.
3.3.2.1 Every scope is kept #
Dropped entries are dropped silently. Every scope in the snapshot is kept, whatever its level or server count; which of them take part in routing is decided per question (§4.4).
3.3.2.2 The snapshot summary line #
resolvd then logs one line at info level summarising the snapshot:
netd: eth0: 1 server(s), 1 domain(s), default; wg0: 1 server(s), 0 domain(s)
with , default on each scope that claims the default route, or
netd: no scopes for an empty snapshot. The counts are of the servers
and domains left after parsing.
3.3.3 The cache #
Comparing the new scope list with the old one, by scope key:
- a scope whose key is new loses any cached answers under that key;
- a scope whose server list differs from before — in content or in order — loses its cached answers;
- a scope missing from the new snapshot loses its cached answers;
- a change to a scope's domains, addresses, flags, metric or level leaves its cache alone.
Demotion is kept per server address, not per scope, and survives every snapshot (§4.6).
3.3.4 Questions already in flight #
- A candidate is routed when it is first asked (§4.4), and its retries
stay with the scope key it was routed to. When that scope's servers
change, retries use the new list; when the scope has gone, the next
retry finds no servers and the question ends
unavailable. - A transaction already sent is not cancelled by a snapshot, and its reply is cached under the scope key it was routed to.
4.1 How a Question Is Answered
Peios / Peios technical reference / resolvd / The Engine
Every door ends here. The native socket's resolve, lookup and
reverse, and every stub query, become one or more tasks, and each task
takes the same path.
4.1.1 The path #
- Parse. The name is parsed from presentation form. A trailing dot
makes no difference. Escapes are not interpreted; a label is the
bytes between dots. A name with an empty label, a label over 63
bytes, or more than 255 bytes on the wire does not parse, and the
task is answered
notfoundat once. - Synthetic names. The name as asked is checked against the synthetic names (§4.2). A match answers the task.
- Candidates. The name becomes its list of candidates (§4.3). An
empty list — a single label with no applicable search domain —
answers the task
notfound. - Routing. Each candidate in turn is routed to one scope (§4.4).
When no scope can take it, the task is answered
unavailable. - The cache. Unless the request asked to bypass the cache, the cache is consulted under that scope (§4.5).
- The network. Otherwise the candidate is sent upstream (§4.6 to §4.8).
- The next candidate. A
notfoundfor a candidate — from the cache or from a server — moves to the next candidate when there is one. Any other result answers the task. - The last candidate. When the last candidate is
notfound, the task is answerednotfound.
4.1.2 The empty name is the root #
An empty name, or . alone, parses as the root. The root is not a
single label, so it is not expanded (§4.3): it is its own one
candidate, routed and asked like any other name.
4.1.3 Each candidate is routed separately #
Each candidate is routed on its own, so the expansions of one single label can go to different scopes. Each has its own attempt budget (§4.6).
4.1.4 What each way out reports #
A single label whose every expansion came back NXDOMAIN from a server
therefore reports dns, the server that answered the last candidate,
that candidate's scope's interface, and response code 3; when the last
candidate's NXDOMAIN came from the cache, it reports cache with the
cached server and code.
4.1.4.1 Answers from the fallback scope #
interface is nil for every answer from the fallback scope, which has
no interface: its cache hits, its servers' replies and its exhausted
attempts alike.
4.1.4.2 Validation #
validation is unvalidated in every case: resolvd performs no DNSSEC
validation (PSPU §6.6).
4.1.5 What an answer carries #
An answer's records are the answer-section records of the reply that
decided it, after the filtering of §4.8, or the synthetic records of
§4.2. The engine also carries the candidate the answer was found at; the
stub door uses it to add a CNAME (§6.2) and lookup uses it as the
start of its CNAME chase (§4.9).
4.1.6 An asker that leaves #
While a native or stub TCP question is waiting for the engine, its stream
is polled for hangup and errors. A definite hangup, error or invalid
socket abandons the question before ready upstream replies are processed:
all of its transactions are cancelled, including both families of an
any lookup. Other questions continue normally.
Replies to cancelled transactions are ignored; they do not populate the cache. Answers already cached before cancellation remain cached.
An abandoned question is removed from the waiting clients and no answer write is attempted for it. Cancellation does not count as an upstream failure, refusal or answer.
A write-half-close is supported: the client may stop sending while still waiting for its answer. Readable end of file alone therefore does not cancel a question. In particular, a TCP FIN cannot reliably distinguish a complete departure from a write-half-close. Without a definite hangup or error, the request retains its admission slot and runs through the usual engine attempts and deadlines. UDP has no disconnect notification.
4.2 Synthetic Names
Peios / Peios technical reference / resolvd / The Engine
PSPU §6.7 lists the names a resolver answers before any network. resolvd checks them in a fixed order, on the name as asked and before any search expansion, and the first match answers the task.
4.2.1 The order #
localhost, or any name under it.127.0.0.1forA,::1forAAAA, both forANY. Sourcesynthetic.- A static name. An exact, case-insensitive match against the
Hosts\values (§2.3). The name's addresses of the asked family — IPv4 forA, IPv6 forAAAA, both forANY. Sourcehosts. - The machine's hostname. An exact, case-insensitive match against
the hostname (§3.3), answered with the machine's own addresses
(below). Source
synthetic. .local. The namelocalor any name under it isnotfound, whatever the type asked. Sourcesynthetic. No query is sent.- A reverse-mapping name, for
PTRorANYonly. See below.
A name that matches none of these goes on to search expansion and the network.
4.2.1.1 Static names beat the hostname and .local #
Because the order is fixed, a static name beats the hostname and
.local: a Hosts\ value named printer.local is answered with its
addresses, and one named after the machine overrides the machine's own
addresses.
4.2.1.2 localhost beats a static name #
localhost is checked first, so a Hosts\ value named localhost, or
a name under it such as printer.localhost, never answers a forward
question: the synthetic loopback answer is given instead. Its addresses
still answer reverse questions through the static-name row below,
except loopback addresses, which the loopback row takes first.
4.2.1.3 Record types #
For the three address-bearing kinds, the answer is found whatever type
was asked; a type other than A, AAAA or ANY, or a family the name
has no address in, gives found with no records. localhost asked for
MX is an empty found, not a notfound, and is not forwarded.
4.2.1.4 The machine's own addresses #
- They are every address of every scope at
addressedor better, whether or not the scope has servers, in snapshot order, link-local addresses included and loopback addresses excluded. - Repeated addresses are removed only when they are adjacent in that list, so an address on two interfaces that are not consecutive in the snapshot appears twice.
- When there are none, the hostname is answered with
127.0.0.1and::1.
4.2.2 Reverse names #
4.2.2.1 Reverse-mapping names #
A reverse-mapping name is one under in-addr.arpa with exactly four
labels before the suffix, or one under ip6.arpa with exactly 32. The
suffix is matched case-insensitively.
- Each
in-addr.arpalabel parses as a decimal number from 0 to 255. Leading zeros and a leading+are accepted, so1.0.0.0127.in-addr.arpaand+1.0.0.127.in-addr.arpaboth name127.0.0.1. - Each
ip6.arpalabel is a single hexadecimal digit, in either case.
A name of either shape whose labels do not parse is not a reverse-mapping name.
4.2.2.2 Synthetic reverse answers #
Asked for PTR or ANY, a reverse-mapping name is answered
synthetically when its address is:
The rows are tried in that order.
4.2.2.3 An address shared by static names #
When several static names list the same address, resolvd returns one PTR:
the name whose ASCII-lowercased presentation form sorts first in byte order.
For example, alpha.home wins over beta.home. The answer is independent
of registry insertion order, hash-table order, reloads and restarts while
the set of matching names stays the same. It changes when that set changes
and another name sorts first. The selected name retains its configured
spelling in the reply. This rule applies to IPv4 and IPv6, for both PTR
and ANY questions.
4.2.2.4 Other reverse questions #
A reverse name asked for any other type, and a reverse name whose address matches none of the rows, goes to the network.
4.2.3 Properties of every synthetic answer #
- Every synthetic record has TTL 0.
- Synthetic answers are never cached, and the
no_cacheflag has no effect on them. - Each one adds one to the
syntheticcounter (§4.10).
4.2.4 Expanded candidates are not checked #
Only the name as asked is checked. An expanded candidate is never
compared with the synthetic names, so with a static name printer.lan
and the search domain lan, a question for printer is expanded and
sent to the network as printer.lan.
4.3 Single-Label Expansion
Peios / Peios technical reference / resolvd / The Engine
PSPU §6.7 sets the search expansion rules. This article is how resolvd builds and asks the list of candidates.
4.3.1 What counts as a single label #
- A name is single-label when it parses to exactly one label. The
trailing dot plays no part:
printer.is single-label and is expanded exactly asprinteris. - A top-level domain such as
comis a single label too, and cannot be asked for directly: it is asked only as its expansions. - Any other name — a multi-label name, or the root — has one candidate, itself.
4.3.2 The applicable domains #
The table and serverless exclusion below describe
source b4f7085.
The exclusive scope is the one routing would choose (§4.4).
- An up scope that has search domains and no servers is not routable, and contributes none of them.
- Scopes with equal metrics keep their snapshot order, and each scope's domains keep their own order.
ExtraSearchDomainsis not applied while routing's step 1 applies.
4.3.2.1 Proposed serverless-scope correction #
The proposed source correction uses all
scopes at link or better, including scopes with no DNS servers. Without
a qualifying exclusive scope, their domains are ordered by metric and
then followed by ExtraSearchDomains; equal metrics retain snapshot
order and each scope keeps its domain order.
An exclusive scope at addressed or better supplies only its own domains,
even without servers: other scopes' domains and ExtraSearchDomains are
not used. For a name not already answered synthetically, if that scope
has no domains, a bare single-label name has no candidates and remains
notfound without a query. If expansion produces a candidate
that selects a serverless scope and needs upstream work, it is
unavailable; later candidates are not tried. Synthetic-answer and
cache handling remain unchanged.
This describes proposed source behavior, not a released package or an installed resolver. The source-revision qualification in §4.4 applies.
4.3.3 Building the candidates #
Each domain in turn is appended to the label.
- A candidate already in the list, compared case-insensitively, is not added again.
- A candidate longer than 255 bytes on the wire is left out.
With no candidates left the name is notfound without a query (§4.1).
4.3.4 The case of a candidate #
A candidate keeps the case of the label as asked and of the domain as configured; that is the case its records are reported at (§4.8).
4.3.5 Asking the candidates #
- Candidates are asked one at a time, in order. Each is routed on its own (§4.4) and has its own attempt budget (§4.6).
- A
notfound— from a server or from the cache — moves to the next candidate (§4.1). found, with records or without, is the answer, and later candidates are not asked.unavailableis the answer, and later candidates are not asked.- When the last candidate is
notfound, the question isnotfound(§4.1).
4.4 Scope Routing
Peios / Peios technical reference / resolvd / The Engine
PSPU §6.7 gives the routing order. resolvd applies it to each candidate separately, when the candidate is first asked, and the candidate's retries stay with the scope it chose.
The server-availability exclusions below describe
source b4f7085.
They differ from PSPU's definition of up. The proposed correction is
summarised after the historical exclusive-scope case.
4.4.1 Which scopes take part #
Only routable scopes (§1.3) take part: those that are up — at level
link or better (PSPU §6.2) — and have at least one server. An up scope
with no servers is skipped at every step: it is never chosen, it never
wins a tie, and its flags have no effect on routing. The same set
decides which search domains apply (§4.3).
4.4.2 The steps #
- Exclusive. The routable scopes that are exclusive and at level
addressedor better. The one with the lowest metric takes the candidate; among equal metrics, the first in snapshot order. - Search domain. Among the routable scopes, every search domain the candidate equals or ends with, compared label by label and case-insensitively. The scope holding the matching domain with the most labels takes the candidate.
- Reverse subnet. When the candidate is a reverse-mapping name (§4.2), the routable scopes that hold an address of the same family whose prefix contains the named address; a prefix length beyond the family's width counts as the full width. The lowest metric wins, then the earliest.
- Default route. The routable scopes with
default_routeset. The lowest metric wins, then the earliest. - Lowest metric. Otherwise, the routable scope with the lowest metric, then the earliest. This step is reached whenever no routable scope claims the default route — including when the only claimant has no servers.
- Fallback. When there are no routable scopes at all, the fallback
scope, provided
FallbackServersnames at least one server. Its scope key isfallback.
When none of the steps yields a scope, the candidate is unavailable
without a query (§4.1).
4.4.2.1 An exclusive scope that does not qualify #
An exclusive scope at level link, or one with no servers, is not
exclusive for routing, and routing continues with step 2. The first
takes part in the later steps as an ordinary scope; the second, not
being routable, in none of them.
4.4.2.2 Proposed serverless-scope correction #
The proposed source correction treats
level link or better as up, independently of DNS servers:
- Steps 1–4 consider up scopes even when their server lists are empty.
Exclusive selection still requires
addressedor better. Domain, subnet, metric and snapshot-order rules are unchanged. - When one of those steps selects a scope without servers and upstream
work is needed, the candidate is
unavailablewithout a query. It does not fall through to another interface or the fallback scope. - Step 5 is reached only when earlier steps select no scope. This ordinary lowest-metric choice still requires an up scope with servers. A serverless default-route claimant therefore blocks step 5, while a serverless scope that matches no earlier rule does not block it.
- Fallback is considered only if no earlier step selects a scope and no up scope has servers. A serverless winner at an earlier step blocks fallback too.
An exclusive scope at link alone still does not qualify for step 1;
it can participate in later steps as an ordinary up scope. Synthetic
answers and the selected scope's cache retain their existing precedence;
this correction does not change cache invalidation or retry policy.
These are proposed source semantics, not a released-package or
installed-resolver guarantee. Check the installed source revision.
4.4.2.3 Ties between search domains #
Between matching domains with the same number of labels, the one in the scope with the lower metric wins. With equal metrics too, the first match found wins: the earlier scope in snapshot order, and within one scope the earlier domain.
4.4.3 Consequences #
4.4.3.1 Step 1 takes every candidate #
Step 1 is absolute while it applies: with an exclusive scope chosen, every candidate goes to it, whatever its name, and step 2 never runs.
4.4.3.2 Search domains beat the subnet rule #
Step 2 runs before steps 3 and 4, so a scope whose search domain is
in-addr.arpa or 10.in-addr.arpa takes reverse names ahead of the
subnet rule.
4.4.3.3 Fallback servers are a last resort #
Fallback servers are used only while no scope is routable. As soon as
one scope at link or better has a server, the fallback scope takes
nothing. Answers cached through it are then not consulted, and stay in
the cache until they are flushed, overwritten or evicted (§4.5).
4.4.3.4 A candidate asks only its own scope's servers #
Every attempt for a candidate, and the TCP retry after a truncated
reply, goes to a server of the one scope the candidate was routed to.
When those attempts fail, the candidate is unavailable (§4.6); the
servers of other scopes, and the fallback servers, are not asked for
it.
4.4.3.5 Demotion does not affect routing #
Routing reads only the current scope list. Which servers are demoted has no effect on which scope a candidate goes to: a scope whose every server is demoted still takes the candidates routing gives it.
4.5 The Cache
Peios / Peios technical reference / resolvd / The Engine
The cache's key and lifetimes are set by PSPU §6.7. This article is how resolvd computes them and what it does at the edges.
4.5.1 The cache key #
- An entry is keyed by the candidate in lower case, the record type, and
the scope key (§1.3). The class is not part of the key; every question
resolvd asks is class
IN. The stub rejects other classes before consulting the cache (§6.1), even when a matchingINanswer is held. - The same name asked through two scopes is two entries, and a single label's expansions are each cached under the expanded name.
4.5.2 What is stored #
Only replies a server sent with response code NOERROR or NXDOMAIN
are stored, under the candidate and the scope the candidate was routed
to:
4.5.2.1 The negative lifetime #
- The negative lifetime comes from the first
INSOArecord in the reply's authority section whose owner is the effective name or an enclosing domain. The effective name is the candidate after following its relevant CNAME path (§4.8); suffix matching respects label boundaries. Use the lesser of the SOA'sMINIMUMfield and its own TTL, capped at 300 s. When an alias leads to NXDOMAIN, also cap the lifetime at the least TTL of that alias path. - A reply with no qualifying
SOAin its authority section has a negative lifetime of zero.
4.5.2.2 A zero lifetime is not stored #
An entry whose lifetime is zero is not stored. A positive answer with a
zero TTL, and a negative answer without an SOA, are therefore never
cached.
4.5.2.3 What is never stored #
Never stored: synthetic answers, unavailable, transport failures, a
UDP reply with TC set (the reply to its TCP retry is the one
considered), and
replies with any other response code.
4.5.2.4 The TTLs inside an entry #
The records in an entry are kept with the TTLs the server sent. Only the entry's lifetime is capped.
4.5.3 Hits #
- An entry is live while its expiry is in the future. A live entry is returned with each record's TTL lowered to the whole number of seconds the entry has left, rounded down, when that is less than the record's own.
- A
notfoundhit with further candidates waiting moves to the next candidate, as anotfoundfrom a server would (§4.1).
4.5.4 Bypassing the cache #
- A
resolverequest withno_cacheset skips the lookup for each of its candidates. The reply it gets is still stored. - Stub queries,
lookupandreversealways consult the cache.
4.5.5 Capacity and eviction #
- The cache holds at most 8 192 entries.
- Expiry alone does not remove an entry. Looking up an expired key
removes it immediately, before any replacement answer arrives. Otherwise
it stays until overwritten, flushed, or removed during admission at
capacity. The
cache_entriescount instatusincludes expired entries that have not yet been removed.
4.5.5.1 Eviction #
Storing an entry under a key the cache does not already hold, when it already holds 8 192 entries, first evicts:
- every entry whose expiry is at or before the current time;
- then, if the cache still holds 8 192 entries, the one entry with the earliest expiry — any one of them, when several share it.
4.5.5.2 A long-lived answer preserves other live entries #
The sweep compares against the current time, independently of the new answer's lifetime. A day-long answer arriving at a full cache of live, shorter-lived answers replaces only the entry with the earliest expiry; the cache still holds 8 192 entries. If removing expired entries makes room, no live entry is evicted.
Older builds compared against the incoming answer's expiry and could empty a cache of live answers.
4.5.5.3 Overwriting a held key #
Storing under a key the cache already holds replaces that entry, live or expired, and evicts nothing else.
4.5.6 Flushes #
| Event | Discards |
|---|---|
| A snapshot in which a scope is new, has a different server list, or is missing (§3.3) | That scope's entries |
A registry change to FallbackServers in content or order (§2.3) | The fallback scope's entries |
A flush request (§5.3) | Everything |
| A restart | Everything |
Nothing else discards entries. Changes to static names, search domains,
the hostname, ControlSecurity, scope flags and metrics leave the cache
as it is.
4.5.6.1 A reply after its scope was flushed #
In source b4f7085,
a reply that arrives after its scope was flushed is still stored, under
the same scope key.
4.5.6.2 Proposed admission rule for invalidated tasks #
The proposed source correction
also invalidates pending tasks' permission to store answers. A global
flush affects every pending task. Removing an interface scope or
changing its server list affects tasks currently associated with that
scope; changing the fallback-server list affects tasks associated with
the fallback scope.
An invalidated task still completes its original requester normally, but cannot store any further answers. This restriction persists through UDP-to-TCP fallback, timeout retries and later search candidates, even if a later attempt uses a new server list. Removing and re-adding a scope does not restore the task's permission. Fresh requests can cache normally.
Changes only to metrics, search domains or other scope properties with unchanged server lists do not invalidate admission; unrelated scopes' pending tasks are unaffected. This changes cache admission, not request cancellation or routing. Cache keys, TTLs and eviction policy are unchanged. It describes proposed source behavior, not a released package or an installed resolver; check the installed source revision before relying on it.
4.6 Servers, Attempts and Timeouts
Peios / Peios technical reference / resolvd / The Engine
Server order, demotion and the attempt limit are set by PSPU §6.7, and the contract's mainline values are in PSPU §6.B. resolvd's values are:
| Bound | Value |
|---|---|
| Transaction timeout | 2 s |
| Attempts per question (all candidates and transports) | 3 |
| Demotion period | 30 s |
| Upstream transactions in flight | 4 096 |
4.6.1 Choosing the server #
- For each attempt, the scope's servers — or the fallback servers — are put in order: the servers not currently demoted, in their configured order, followed by the demoted ones, in their configured order.
- The attempt goes to the first server in that order not yet asked for this candidate. When every server has been asked, it goes to the server at position n mod count in that order, where n is the number of attempts already made for the candidate.
- With one server, all three attempts go to it.
- The order is computed afresh for each attempt, so a server demoted by the previous attempt is already behind its peers for the next.
- Attempts for one candidate are made one at a time; a transaction is sent only after the previous one for the same candidate has ended.
With two healthy servers A and B, the attempts go to A, B, then
whichever of them is first in the order at that moment — A if both
have since been demoted, since demotion keeps configured order within
the demoted group.
4.6.2 What fails an attempt #
Each of the following ends the transaction, adds one to
upstream_failed, demotes the server, and moves to the next attempt:
- no matching reply within 2 seconds of the transaction being sent;
- an error opening, binding, connecting or sending on the transaction's socket, except the local resource and permission errors below — an unreachable network is known at once, not after the timeout;
- an error receiving on a UDP socket, such as the refusal that follows an ICMP port-unreachable;
- for TCP, the connection failing to complete: an error or hang-up while it is being established;
- for TCP, an error, a hang-up or a failed write before the whole query has been sent;
- for TCP, the connection closing, or a read error, before a whole matching reply has arrived;
- a matching reply whose response code is neither
NOERRORnorNXDOMAIN—SERVFAIL,REFUSED,FORMERR,NOTIMP, and every other code, including EDNS extended codes.
A synchronous failure to open or send a transaction caused by exhausted
local descriptors (EMFILE, ENFILE), memory or buffers (ENOMEM,
ENOBUFS), local address allocation (EADDRNOTAVAIL, EADDRINUSE), or
permission (EACCES, EPERM) still adds one upstream_failed and advances
the bounded retry sequence, but does not demote the server. A different
configured address or address family may still be usable. If all attempts
fail, the result is unavailable and is not cached.
A reply that does not match the transaction is ignored while the socket remains open for a matching reply until the original deadline (§4.8).
4.6.3 Demotion #
- Demoting a server marks its address until 30 seconds from now; a further failure moves that mark forward.
- A matching reply with response code
NOERRORorNXDOMAINthat resolvd uses — a UDP reply withoutTC, or a TCP reply — removes the server's mark at once. A UDP reply withTCset does not remove it, whatever its response code; the reply to the TCP retry decides. - The mark belongs to the address, so a server listed by two scopes is demoted in both.
- A demoted server is still asked, after the healthy ones.
statuslists, for each scope, which of its servers are demoted at that moment (§5.3); demotion of the fallback servers is not reported.
4.6.4 The attempt limit #
- A question gets three upstream attempts across all search candidates and
transports. Failures spend them: a timeout, a failure response code, or
a transport failure. When its last attempt fails, the question is
unavailable(§4.1). - The budget is not reset when the question moves to another candidate,
but a candidate's
NXDOMAINthat moves it on is an answer, not a failure, and does not spend an attempt. Every search candidate is therefore asked, however many there are. Cached answers spend no attempt. - The TCP retry following a truncated UDP reply (§4.7) spends one attempt
and gets its own two-second deadline. A truncated reply on the final
attempt cannot start TCP: the question is
unavailable.
The three attempts allow at most six seconds of upstream timeout waiting
in total, including TCP and search-domain expansion. Each NXDOMAIN that
moves to another candidate adds one round trip. Loop scheduling and local
I/O can add elapsed time; the attempt limit does not give those
operations a separate deadline.
4.6.5 The in-flight ceiling #
- When 4 096 or more transactions are outstanding, a question about to
allocate an upstream transaction is answered
unavailableat once, and therefusedcounter goes up by one. A question whose scope has no servers left is answeredunavailablebefore the ceiling is looked at, and is not counted as refused. - Every send checks this ceiling, including later attempts and TCP retries. A retry is normally sent after its previous transaction has ended and been removed, so it can reuse that slot even at capacity. Exhausting the three-attempt budget is checked before allocating a transaction, and is not counted as a capacity refusal.
- Synthetic names and cache hits are answered as usual whatever the count.
4.7 The Upstream Query
Peios / Peios technical reference / resolvd / The Engine
PSPU §6.7 sets the upstream behaviour. This article is what resolvd puts on the wire to meet it.
4.7.1 Sockets #
4.7.1.1 One socket per transaction #
Every transaction has its own socket, opened when the transaction is sent and closed when it ends.
4.7.1.2 The UDP socket #
- A new socket is bound to the unspecified address of the server's family, port 0, so the kernel picks the source address and a fresh ephemeral source port.
- The socket is connected to the server's port 53, so the kernel delivers only datagrams from that address and port.
- Each datagram is read into a 4 096-byte buffer; a longer datagram is truncated by the read.
The query is sent with one send.
4.7.1.3 The TCP connection #
A new nonblocking socket is connected to the server's port 53. When the connection completes, the query is written with its two-byte length prefix; once it is all written, the socket is read until the two-byte length and that many bytes of reply have arrived. Bytes after the first message are ignored. One query is sent per connection, and the connection is closed when the transaction ends.
4.7.1.4 A transaction that cannot be sent #
A failure found while a transaction is being sent — opening or binding
its socket, connecting it, or, for UDP, sending the query — fails the
attempt at once (§4.6) and is logged at warn level as
upstream <server>: <error>. A TCP failure found later, once the
connection is under way, is not logged, and neither is a timeout or a
UDP receive error.
4.7.2 Link-local servers #
A link-local IPv6 server from netd is reached through the interface named
by its DNS scope. Both UDP and TCP upstream sockets carry that interface’s
index in sin6_scope_id; a truncated UDP answer keeps it on the TCP retry.
An address such as fe80::1 can therefore be advertised in RDNSS without a
zone suffix. Failures of that address on one scope do not demote the same
address on another scope.
A snapshot may also spell it fe80::1%eth0 or use a numeric interface
index. An explicit zone must match the containing scope’s interface;
invalid or mismatched zones are logged and ignored. Global addresses
continue to use ordinary unscoped sockets.
FallbackServers has no containing interface scope and continues to accept
plain IP addresses only: a %zone value is logged and skipped. Configure
link-local DNS on the interface profile instead. A bare link-local
fallback cannot be sent; it fails as a local address error, without
marking a remote server unhealthy.
4.7.3 The message #
Authority and answer sections are empty.
4.7.3.1 Query entropy #
Every transaction obtains 34 fresh bytes through getrandom with
GRND_NONBLOCK: two bytes for the message ID, interpreted little-endian,
and 32 bytes of case bits, enough for every letter in a DNS name. This
uses the kernel CSPRNG without a predictable userspace generator or a
clock fallback.
If entropy is unavailable, including before the kernel CSPRNG is ready,
resolvd logs the error, increments refused, and settles the affected
question as unavailable without sending it. The event loop does not wait
for entropy readiness; later requests can try again. Synthetic and cached
answers do not need upstream entropy.
4.7.3.2 Case randomisation #
- Every ASCII letter in the candidate is flipped between upper and lower case when its random bit is 1. Digits, hyphens and other bytes are unchanged.
- Case bytes follow the ID bytes. Bits are taken lowest first from each byte in order. A bit is used only for a letter.
- Each transaction — a retry to the same server, and the TCP retry after truncation, included — gets a new pattern and a new ID.
4.7.4 No fallback without EDNS0 #
Every query carries the OPT record. resolvd does not fall back to a
query without EDNS0: a server that answers an EDNS0 query with
FORMERR has failed the attempt (§4.6).
4.7.5 The TCP retry #
- A matching UDP reply with
TCset is not used. The same candidate is sent to the same server over TCP as a new transaction, with a new ID, a new case pattern and its own two-second deadline, if an attempt remains in the question budget. The TCP transaction spends one attempt (§4.6); without budget the question isunavailable. - A TCP reply with
TCset is used as it is.
4.8 Reply Validation
Peios / Peios technical reference / resolvd / The Engine
4.8.1 Matching #
A reply is matched against the transaction it arrived for. It matches when all of these hold:
- it decodes as a DNS message — bytes after its last section are tolerated;
- its ID equals the transaction's;
- its
QRbit is set; - it has exactly one question;
- that question's type and class equal those sent;
- that question's name is byte-for-byte identical to the name sent, case pattern included.
4.8.1.1 A non-matching reply is ignored #
A reply that fails any of these is ignored: the transaction is not ended by it, nothing is counted, and the server is not demoted for it.
4.8.1.2 A non-matching datagram on UDP #
A datagram that does not match is discarded while the transaction's socket stays open. A later matching datagram can answer the same transaction. Ignoring a datagram does not extend the original two-second deadline: if no matching reply arrives, the attempt times out and the server is demoted (§4.6).
4.8.1.3 An undecodable or cut-short datagram #
The same holds for a datagram that does not decode. A datagram longer than the 4 096-byte read buffer arrives cut short. If the cut falls inside the DNS message, it cannot decode and is discarded; the socket remains available for another reply until the original deadline.
4.8.1.4 A non-matching TCP reply #
An undecodable or non-matching TCP message is discarded while the connection remains open. Later complete frames on that connection are checked, including frames already buffered and a matching frame followed by EOF. Partial frames remain buffered for the next read. Ignoring a frame does not extend the original deadline. If the peer closes before a matching frame completes, the attempt fails; if it stays open without a matching reply, the attempt times out (§4.6).
4.8.2 A matching reply #
A matching reply adds one to upstream_answered and ends the
transaction. Then:
| Reply | Result |
|---|---|
UDP, TC set | TCP retry to the same server (§4.7) |
Response code NOERROR | found |
Response code NXDOMAIN | notfound |
| Any other response code | The attempt fails (§4.6) |
A NOERROR or NXDOMAIN reply that is used removes the server's
demotion; a UDP reply with TC set does not (§4.6).
4.8.2.1 Extended response codes #
The response code is the header's four bits combined with the extended
bits of an OPT record in the reply, so an extended code is never
mistaken for NOERROR or NXDOMAIN.
4.8.3 What is taken from it #
4.8.3.1 Records #
- Relevant answer records of class
IN, in the order the server sent them. - For
NXDOMAINnone are taken, even if the server sent some. - Start at the candidate. Keep records of the requested type at that
owner; otherwise follow the first
INCNAME there, retaining it, and repeat at its target. Stop after sixteen aliases, on a repeated name, or when no matching record remains. Records unrelated to that path or of an unrequested type are discarded before rendering or caching. Their TTLs cannot shorten the cache lifetime. - An explicit CNAME question returns CNAME records at the queried owner.
ANY accepts all
INtypes at that owner and does not follow aliases. Owner comparisons are case-insensitive and use DNS label boundaries. - The authority and additional sections are not returned.
4.8.3.2 The case of returned records #
A record whose owner name equals the candidate, case-insensitively, is given the candidate's case — the case of the question as asked, not the randomised case sent. Other records keep the case the server used.
4.8.3.3 Lifetime #
The cache lifetime of §4.5, computed from the answer records or from the
authority section's SOA.
4.8.3.4 Upstream flags #
The server's AA, AD, RA and CD bits are not used and are not
carried into any answer.
4.8.3.5 An empty NOERROR #
A NOERROR reply with no relevant answer records of class IN — including a
referral from a server that does not recurse — is found with no
records.
A notfound with further candidates moves to the next one (§4.3);
everything else answers the question.
4.9 Lookup and Reverse
Peios / Peios technical reference / resolvd / The Engine
4.9.1 lookup #
A lookup (PSPU §6.5) asks for A, AAAA, or both:
4.9.1.1 Each task is an ordinary question #
Each task is an ordinary question (§4.1): it has its own synthetic
check, its own expansion, its own routing per candidate, its own cache
lookups and its own attempts. The two tasks of an any lookup run at
the same time and can be answered from different sources. The reply is
built when the last of them is done.
4.9.1.2 The outcome #
| Tasks' outcomes | outcome |
|---|---|
At least one found, with or without records | found |
None found, at least one unavailable | unavailable |
Every one notfound | notfound |
An A answer and an AAAA that is unavailable is found, with IPv4
addresses only.
4.9.1.3 Chasing CNAMEs #
For each found task, resolvd looks for addresses in that task's
answer records, starting at the candidate the answer was found at.
Names are compared case-insensitively.
- If the first record at the current name is a
CNAME, and no record of the asked type is at the current name, the chase moves to theCNAME's target. - This repeats at most 16 times; a loop stops there.
- The addresses are the
A(for theAtask) orAAAA(for theAAAAtask) records at the name the chase ended at, in answer order, each with its record's TTL.
4.9.1.4 The chase asks nothing further #
Only the records in the task's own answer are used. A chain that leaves
them — a CNAME whose target's addresses the server did not include —
ends with no addresses, and no further question is asked.
4.9.1.5 The reply fields #
addresseslists each finished task's addresses in turn, in the order the tasks finished, so whether IPv4 or IPv6 addresses come first depends on which family was answered first.canonicalandsourcecome from the same selectedfoundtask. Prefer a task that produced addresses; when both or neither did, preferAoverAAAA. Completion order does not affect this choice.canonicalis the name that task's CNAME chase ended at, after search expansion. Without afoundtask it is the name asked, as parsed, in presentation form without a trailing dot (the root is.).- A
foundwith no addresses still reports the expanded or chased name. - A name that does not parse is answered at once with
outcomenotfound,canonical., no addresses andsourcelocal; no task is started. sourceis the source of the selected task, andlocalwhen no task wasfound. It describes that task; the other family's records can have a different source.validationisunvalidated.- A
lookupadds one toquerieshowever many tasks it makes.
4.9.2 reverse becomes a PTR question #
A reverse turns its address into the reverse-mapping name — the four
octets reversed under in-addr.arpa for IPv4, the 32 nibbles reversed,
in lower-case hexadecimal, under ip6.arpa for IPv6 — and resolves that
name for PTR exactly as a resolve without no_cache would. The
reply is an ordinary answer (§5.3).
4.10 Counters
Peios / Peios technical reference / resolvd / The Engine
The engine keeps seven counters, reported by status (§5.3). Each is an
unsigned 64-bit count.
4.10.1 What each counter counts #
4.10.2 What is not counted #
status and flush requests are not counted, and nothing on the netd
channel is.
An unmatched reply changes neither upstream counter (§4.8). A later
matching reply adds one upstream_answered; if the original deadline
expires without one, the timeout adds one upstream_failed instead.
A reply with SERVFAIL shows up in both. A transaction that could not
be sent shows up in upstream_sent and upstream_failed, and never in
upstream_answered.
4.10.3 Resetting the counters #
The counters start at zero when resolvd starts, and are reset only by a
restart. flush does not reset them.
5.1 Framing and Limits
Peios / Peios technical reference / resolvd / The Native Socket
The framing, the one-request-per-connection rule and the requirement to answer errors with an error reply are PSPU §6.4's. This article is resolvd's reading of them.
5.1.1 Accepting connections #
At most 64 waiting connections are accepted per event-loop iteration. When 256 native connections are either still sending their requests or waiting for engine answers, a newly accepted connection is closed at once, with no reply; so is one that cannot be made nonblocking. Reading a request keeps its slot occupied until resolution completes or the asker is abandoned (§4.1). Reply writers use a separate bounded queue (§2.5).
5.1.2 Reading the request #
Each connection is read without blocking. Partial headers and payloads are retained until more bytes arrive or the delivery bound expires.
- Read exactly the four-byte little-endian payload length. A length
above 65 536 gets the reply
request too largeand the connection closes, without reading any payload bytes. - Read only that many payload bytes, in chunks of at most 4 096 bytes. The buffered frame never exceeds 65 540 bytes, including its header.
- Decode the complete payload immediately. Bytes after this first frame remain unread and do not count against its size ceiling; they cannot make a legal request too large.
5.1.2.1 The length check precedes the payload #
The declared length is checked before any payload is read, even when the client queues the header and a large payload together. A legal request followed by additional bytes is still answered once. This is one request per connection; extra frames are not processed. When extra bytes remain unread, the client may observe a connection reset after its framed reply instead of a clean end of file.
5.1.3 A connection that ends early #
- A connection that closes, or fails to read, before its request is complete is dropped without a reply.
- A client that sends its whole request and then shuts down its writing side is answered as usual, whether the end of file is already waiting behind the request or arrives later. The complete frame is decoded without reading beyond it, and the connection closes after its one reply. A partial frame followed by end of file is dropped immediately, without waiting for the delivery timeout.
- A connection that has not delivered a complete request five seconds after it was accepted is closed without a reply.
5.1.4 Decoding #
The payload is a MessagePack map. The following describes the decoder
in resolvd 0.1.6 at source b4f7085, before the proposed source
correction described below:
- Every key is a string, and a key repeated in the request's top-level map is an error. Keys inside a skipped value are not checked for repeats.
- A key resolvd does not know is skipped when its value is
nil, a boolean, an integer, a string, binary, an array or a map, with one exception among the integers: an integer above 9 223 372 036 854 775 807 (auint 64of 2^63 or more) anywhere inside the value, a map key included, fails the request withmalformed message: unexpected value type. - A floating-point or extension value anywhere inside an unknown key's value fails the decode, and so does a string inside it that is not UTF-8: a skipped string is still checked.
- An array or map nested more than 32 levels deep inside an unknown key's value, counting the value itself as the first level, fails the decode.
querynames the request; a missingqueryis an error, and an unknown one is an error.- Every key resolvd knows —
query,name,type,no_cache,familyandaddress— is read and type-checked in every request, whether that request takes it or not, with the types of PSPU §6.5. A value of the wrong type is an error, so{query: status, name: 5}is refused;typeis checked against 65 535 in every request, so{query: status, type: 70000}is refused too.familytakes any string (§5.3), andaddressis parsed as an address only in areverse. - Anything after the top-level map in the payload is ignored.
The map is read in order, and the first error met is the one reported. A missing field is looked for only after the whole map has been read.
5.1.4.1 Proposed unknown-field correction #
The proposed source correction makes libresolv skip floating-point
values, fixed and variable-length extension values (including their
type byte), and the entire unsigned 64-bit range in unknown fields.
This applies to requests and replies, including unknown status
counter keys and values nested inside skipped arrays and maps. It does
not change the 0.1.6 source behavior described above.
Skipped strings must still be UTF-8, skipped containers retain the
32-level nesting limit, and truncated values still fail. The reserved
MessagePack marker 0xc1 still produces unsupported value type.
Known fields retain their existing type and range checks; in particular,
this does not extend the range accepted by a known integer field.
5.1.5 Error replies #
Every error reply is ok: false with an error string and nothing
else. The causes below describe the same source version before the
proposed correction;
after that change, unknown float, extension and large unsigned integer
values no longer produce the two type errors:
5.1.5.1 The connection after an error reply #
After an error reply the connection is closed.
5.1.6 Writing the reply #
- The reply is framed the same way — four-byte little-endian length,
then the MessagePack map — and written without blocking. An unfinished
write is queued with a fixed one-second deadline and a cap of 256 queued
native replies (§2.5). A failed or expired write is logged as
control: reply failed: <error>. The connection is then closed. - An encoded reply payload may be at most 65 536 bytes, excluding its
four-byte length prefix. A larger reply is replaced by the error
reply too large; no partial answer is sent. This applies to every native reply kind. Exactly 65 536 bytes is allowed.
5.2 The Access Check
Peios / Peios technical reference / resolvd / The Native Socket
The control object and its check are PSPU §6.4's. resolvd makes the check after the request has been decoded and before anything else is done with it.
5.2.1 The check #
- The connecting peer's token is opened from the socket. When it
cannot be, resolvd logs
control: no peer token: <error>and the request is denied. - A KACS access check is made of the request's right against the control object's descriptor, with that token. A check that cannot be completed counts as a denial.
- A denied request is answered
access denied(§5.1).
5.2.1.1 The generic mapping #
The check uses this generic mapping:
| Generic right | Maps to |
|---|---|
GENERIC_READ | RESOLVER_QUERY, READ_CONTROL |
GENERIC_WRITE | RESOLVER_CONTROL, READ_CONTROL |
GENERIC_EXECUTE | RESOLVER_QUERY |
GENERIC_ALL | RESOLVER_ALL_ACCESS (0x000F0003) |
5.2.1.2 Decoding comes before the check #
A request is decoded before it is checked, so a malformed request gets
its decoding error whoever sent it, and an unknown query gets
unknown query rather than access denied.
5.2.1.3 The answer does not depend on the caller #
The peer's identity is used for this check and for nothing else. Every caller allowed to ask gets the same answer to the same question.
5.2.2 The compiled default descriptor #
The descriptor is ControlSecurity when it holds a valid self-relative
descriptor, else the compiled default (§2.3):
| Component | Value |
|---|---|
| Owner | SYSTEM |
| Group | SYSTEM |
| DACL | SYSTEM: RESOLVER_ALL_ACCESS; Administrators: RESOLVER_ALL_ACCESS; Everyone: RESOLVER_QUERY and READ_CONTROL |
5.2.3 A new descriptor applies to later requests #
The descriptor is rebuilt whenever the registry configuration changes (§2.3). A request decoded after the change is checked against the new one; a request already being answered is not checked again.
5.2.4 What the control object does not govern #
The stub listener is not checked against the control object (PSPU §6.8):
anything on the machine that can reach 127.0.0.53 can ask. A program
denied RESOLVER_QUERY on the native socket still resolves names
through the stub listener. The NSS shim uses the native socket, so for
such a program getaddrinfo fails (§7.1) while a resolver that speaks
DNS directly succeeds.
5.3 Requests
Peios / Peios technical reference / resolvd / The Native Socket
The requests, their fields and their reply shapes are PSPU §6.5. This article covers what resolvd accepts and what it fills in.
5.3.1 Common to every request #
A request that passes the access check (§5.2) is handled at once:
statusandflushare answered within the same loop iteration;- the other three become engine questions (§4.1), and the connection is held until the engine answers; the reply is written then.
5.3.2 resolve #
The reply's outcome, source, server, interface and rcode are
as §4.1 describes. Each record carries:
| Type | text |
|---|---|
A, AAAA | The address |
CNAME, PTR, NS | The target name, written as name is |
SOA | mname rname serial refresh retry expire minimum |
MX | preference exchange |
SRV | priority weight port target |
TXT | Each string in double quotes, separated by spaces; inner quotes are escaped as \", backslashes are not escaped, and bytes that are not UTF-8 become U+FFFD |
| Anything else | \# <length> <hex>, the hex in lower case |
5.3.3 The lookup family #
family takes any, inet or inet6. Absent, or any other string,
means any. The reply is built as §4.9 describes.
5.3.4 The reverse address #
address is an IPv4 or IPv6 address in textual form; anything else is
the error missing or malformed field address. The question is built as
§4.9 describes.
5.3.5 status #
5.3.5.1 Each scope in status #
Each scope carries interface (the interface name), servers and
domains as parsed from the snapshot (§3.3), default_route,
exclusive, metric, subnets, and demoted:
subnetsholds the interface's addresses asaddress/prefix— the address itself, not the network it is in;demotedholds those of the scope's servers that are demoted at this moment (§4.6);- a scope's
levelis not reported.
5.3.6 flush #
6.1 Receiving Queries
Peios / Peios technical reference / resolvd / The Stub Listener
The stub listener's address, its loopback-only rule and its rendering are PSPU §6.8. This article covers how resolvd reads queries; the replies are §6.2.
6.1.1 UDP #
When the UDP socket is readable, at most 64 datagrams are read per event-loop iteration, including malformed or dropped datagrams. For each:
- A datagram whose source address is not a loopback address — anything
outside
127.0.0.0/8— is dropped without a reply. - It is checked as a query (below). A refused query gets its error reply, or nothing.
- A valid query is admitted if fewer than 16 UDP questions from its
source address and port, and fewer than 256 UDP questions in total,
are waiting for engine answers. Otherwise it gets
SERVFAIL, with no upstream transaction, and increments bothqueriesandrefused. This admission check precedes synthetic and cache lookup. - An admitted query becomes an engine question (§4.1). Its slot is released when the engine completes it, including timeout completion.
The endpoint is an accounting key, not an authenticated process identity. Changing source ports can consume the aggregate UDP allowance but cannot consume the separate native or TCP admission allowances. These bounds limit retained work; they do not promise fairness between local processes.
6.1.1.1 The UDP read buffer #
Each datagram is read into a 4 096-byte buffer; a longer datagram is truncated by the read to its first 4 096 bytes.
6.1.1.2 A UDP query longer than the buffer #
A query longer than 4 096 bytes therefore arrives cut short. When the
cut falls inside the DNS message, the message no longer decodes and,
being at least 12 bytes long, is answered FORMERR (below). Bytes after
the end of the message are ignored by decoding, so a query whose message
fits in 4 096 bytes and is only padded beyond them is answered as usual.
6.1.1.3 Where UDP replies go #
Replies are sent to the source address and port the query came from.
6.1.2 TCP #
When the TCP listener is readable, at most 64 waiting connections are accepted per event-loop iteration. A connection is closed at once, without a reply:
- when 256 stub TCP connections are still sending queries or waiting for engine answers;
- when its source is not a loopback address.
A connection that cannot be made nonblocking is closed the same way.
6.1.2.1 Reading a TCP query #
Each held connection is read as it becomes readable, everything available each time, in 4 096-byte chunks. A message is a two-byte big-endian length and that many bytes.
- When more than 65 537 bytes are buffered, the connection is closed without a reply.
- When a whole message has arrived, it is checked as a query, and the connection leaves the set of connections still sending, but continues to occupy its admission slot while the engine works.
- A connection carries one query. Anything sent after the first message is ignored, and the connection is closed once the answer, or the error reply, has been written.
- A connection that closes, or fails to read, before its message is whole is closed without a reply.
- A client that sends a complete query and then shuts down its writing side is still answered. This also applies when the read finds end of file immediately after the buffered query. The connection closes after its one reply. A partial frame followed by end of file is dropped immediately, without waiting for the delivery timeout.
- A connection that has not delivered a whole message ten seconds after it was accepted is closed without a reply.
- Once its query has arrived the ten-second bound no longer applies; the connection is held until the engine answers or a definite hangup/error abandons its question (§4.1). Reply writers use a separate bounded queue (§2.5).
6.1.3 Checking a query #
6.1.3.1 The NOTIMP and question-count FORMERR replies #
These two replies are built from the query:
- the header copies the query's ID, opcode,
RDandCDbits, and setsQRandRA; every other bit is clear; - the question section is the query's, whatever it holds;
- when the query carried an
OPTrecord, the reply carries one whose payload size is the size the query advertised, withDOclear and no options; without one, the reply has none.
6.1.4 What an accepted query contributes #
- Only
INquestions are accepted. Their decoded name and type reach the engine; unsupported classes are rejected at the stub door over both UDP and TCP. RD,CD, theDObit and any EDNS options are not looked at.- Stub queries always consult the cache (§4.5).
6.1.4.1 Wire names #
The decoded name reaches the engine directly, without conversion to presentation text and reparsing. Label boundaries and bytes, including embedded dots, backslashes, NUL and other binary bytes, are preserved. The usual upstream ASCII case randomization still applies (§4.3).
Names are subject to DNS wire limits, not the length of a displayed escape sequence. A valid 63-byte binary label or a valid name whose display form would exceed 255 bytes can therefore be resolved normally.
Search expansion (§4.1) uses the number of wire labels: a single label containing a literal dot remains a single-label name.
6.1.5 Which stub queries are counted #
Every valid query counts in queries (§4.10), including a UDP query
refused by the pending budgets (which also increments refused). Queries
rejected by syntax, opcode, question count or class checks do not count.
6.2 Rendering Replies
Peios / Peios technical reference / resolvd / The Stub Listener
The outcome-to-response-code table and the rules for CNAME, RA, AA
and AD are PSPU §6.8. This article is how resolvd builds each reply.
6.2.1 The header and question #
- The reply copies the query's ID, opcode,
RDandCDbits and its question section. QRandRAare set.AA,ADandTCare clear, except thatTCis set on a truncated UDP reply (below).- No bit from an upstream server's reply reaches the stub reply.
6.2.2 By outcome #
6.2.2.1 found replies #
- The response code is
NOERROR, and the answer section holds the engine's records in the order the engine has them (§4.8). - When the answer was found at a candidate other than the question's
name — compared case-insensitively — the section begins with a
CNAMEfrom the question's name, in the query's case, to the candidate. Its TTL is the least TTL among the records when any are present. - A
foundwith no records at an expanded name isNOERRORwith only that leadingCNAME. Its TTL is the negative cache lifetime: the lesser of the enclosing upstreamSOArecord's TTL and minimum field, capped at 300 seconds and at the TTL of anyCNAMEpath the reply followed (§4.5). A cache hit uses the remaining lifetime, rounded down to whole seconds. With no enclosingSOA, the TTL is zero; no lifetime is invented. The authority section remains empty. An unexpanded emptyfoundhas noCNAME.
6.2.2.2 notfound replies #
NXDOMAIN, with empty answer and authority sections. No SOA is
supplied, so a client has nothing from which to cache the absence.
6.2.2.3 unavailable replies #
SERVFAIL, with empty sections.
6.2.3 EDNS in stub replies #
When the query carried an OPT record, the reply carries one
advertising a payload size of 1 232 bytes, with DO clear and no
options. A query without OPT gets a reply without one.
6.2.4 Size and truncation #
- Over UDP the reply's size limit is the payload size the query's
OPTadvertised, or 512 bytes when that is smaller or there is noOPT. - A reply over the limit is replaced by a truncated one, which keeps the
same header with
TCset, and the question. - The truncated reply keeps the
OPTrecord when the reply has one. - The truncated reply carries no records: nothing is partially included.
- A reply that cannot be encoded at all is not sent.
- Over TCP the whole reply is sent, whatever its size up to the 65 535 bytes a DNS message can hold.
- A TCP reply is written with its two-byte length, without blocking. Unfinished writes are queued with a fixed one-second deadline and a cap of 256 queued TCP replies (§2.5); a failed or expired write is not logged.
7.1 The Module
Peios / Peios technical reference / resolvd / The Nss Shim
The shim's obligations are PSPU §6.10. This chapter describes the module resolvd's source tree builds.
7.1.1 The object #
libnss_peios_net.so.2is installed bydev.peios.resolvd-nssat/usr/lib/x86_64-linux-peios/libnss_peios_net.so.2.- It is built from the
nsscrate, with the native channel codec compiled in. Its only shared-object dependencies arelibc.so.6and the dynamic loader,ld-linux-x86-64.so.2. The GCC unwinder required by Rust is linked statically, with its symbols private to the module; name lookups do not loadlibgcc_s.so.1. The identity shim,libnss_peios.so.2, uses the same static-unwinder arrangement. Neither shim links libpeios.
7.1.1.1 The entry points #
The module exports six entry points:
| Entry point | Asks |
|---|---|
_nss_peios_net_gethostbyname4_r | lookup, family any |
_nss_peios_net_gethostbyname3_r | lookup, family from af |
_nss_peios_net_gethostbyname2_r | as gethostbyname3_r, with no TTL or canonical name out |
_nss_peios_net_gethostbyname_r | as gethostbyname3_r, with af AF_INET |
_nss_peios_net_gethostbyaddr2_r | reverse |
_nss_peios_net_gethostbyaddr_r | as gethostbyaddr2_r, with no TTL out |
7.1.2 One connection per call #
- Every call that needs resolvd opens a new connection to
/run/resolvd/resolv.sock, sends one request, reads one reply, and closes the connection. - Nothing is kept between calls: no connection, no cache, no configuration. The module reads no file.
7.1.2.1 The ten-second socket timeouts #
Each connection is given ten-second read and write timeouts, which
apply to each read and write on the socket. Ten seconds is longer than
the six seconds a question's three timed-out attempts take in resolvd,
TCP retries included (§4.6), so a slow upstream normally reaches the
caller as unavailable from resolvd rather than as a timeout here. A
question can still reach the timeout when many search candidates each
answer NXDOMAIN slowly, since those answers do not spend the attempt
budget; that is reported as the table below shows.
7.1.3 Results #
The missing-family row above describes
source b4f7085:
a found name with no address of the family asked is reported exactly as
a name that does not exist. This does not meet PSPU §6.10's requirement
that found without an address of that family return NO_DATA.
The proposed source correction preserves
NOTFOUND and ENOENT, but sets h_errno to NO_DATA when a found
reply has no usable address of the requested family. A found reverse
reply with no PTR record has the same result. Explicit notfound
still returns HOST_NOT_FOUND; unavailable and transport failures keep
their distinct results in the table. This describes proposed source
behavior, not a released package or an installed module. Check the
installed module's source revision before relying on this distinction.
7.1.3.1 AF_UNSPEC #
AF_UNSPEC is accepted only by gethostbyname4_r, which always asks
for both families.
7.1.4 The caller's buffer #
Every string, pointer array and address the module returns is placed in the buffer glibc passes in; the module allocates nothing that outlives the call. When the buffer cannot hold the result, the call returns as in the table, and nothing written to the buffer is valid.
7.2 Forward Lookups
Peios / Peios technical reference / resolvd / The Nss Shim
7.2.1 localhost in the shim #
- A name that, with trailing dots removed, is
localhostor ends in.localhost, compared case-insensitively, is answered inside the calling process without contacting resolvd — whether resolvd is running or not. - The answer is
127.0.0.1for IPv4,::1for IPv6, or both, IPv4 first, with canonical namelocalhostand TTL 0.
7.2.2 Other names #
- Any other name is sent as a
lookup(§4.9), with the family the entry point asks for. The addresses in the reply are filtered to that family, and an empty result isNOTFOUND(§7.1). - Addresses are returned in the order resolvd's reply lists them.
resolvd does not fix which family comes first (§4.9); glibc's own
sorting of getaddrinfo results is what callers normally see.
7.2.3 gethostbyname4_r #
The result is a linked list of gaih_addrtuple, and the pointer glibc
passes is overwritten with the head of the list:
7.2.3.1 The gethostbyname4_r TTL #
When ttlp is given it receives the least TTL among the addresses.
7.2.4 The forward hostent #
gethostbyname3_r, 2_r and _r return a hostent:
| Field | Content |
|---|---|
h_name | The reply's canonical name |
h_aliases | Empty |
h_addrtype | The af asked for |
h_length | 4 or 16 |
h_addr_list | Every address, in reply order |
7.2.5 The gethostbyname3_r TTL and canonical name #
When ttlp is given it receives the least TTL among the addresses, and
when canonp is given it is pointed at h_name.
7.3 Reverse Lookups
Peios / Peios technical reference / resolvd / The Nss Shim
7.3.1 Loopback addresses in the shim #
An address in 127.0.0.0/8, or ::1, is answered inside the calling
process without contacting resolvd: h_name is localhost, with no
aliases and TTL 0.
7.3.2 Other addresses #
- Any other address is sent as a
reverse(§4.9). Afoundreply with no records isNOTFOUND(§7.1). - In source
b4f7085, a reply whose records include noPTRrecord — onlyCNAMEs, for instance — isNOTFOUNDwithHOST_NOT_FOUND.
The proposed source correction returns
NOTFOUND / ENOENT / NO_DATA for a found reply with no records or
no PTR record. Explicit notfound remains HOST_NOT_FOUND; this does
not turn transient failures into negative answers. The source and
installed-module qualification in §7.1 applies here too.
7.3.3 The reverse hostent #
Otherwise the reply's PTR records are taken in order, each record's
text with any trailing dots removed:
| Field | Content |
|---|---|
h_name | The first PTR record's name |
h_aliases | The remaining PTR records' names |
h_addrtype | The af asked for |
h_length | 4 or 16 |
h_addr_list | The address asked about, alone |
7.3.4 The reverse TTL #
When ttlp is given it receives the least TTL among all the reply's
records, PTR or not.
8.1 The resolv Command
Peios / Peios technical reference / resolvd / The Resolv Command
resolv (/usr/bin/resolv, package dev.peios.resolv) owns no state.
8.1.1 One request per verb #
Every verb but version is one request on the native socket, on one
connection, and its output is a rendering of the reply.
8.1.2 Verbs #
8.1.2.1 The command line #
--no-cacheis recognised anywhere on the command line and removed before the rest is matched; it affects onlyquery.- Any other argument list prints a usage line to standard error and exits 64.
8.1.2.2 Arguments checked before contacting resolvd #
- The
<type>ofqueryisAwhen omitted. It is one ofA,NS,CNAME,SOA,PTR,MX,TXT,AAAA,SRVorANY, in any case, orTYPE<n>for any type number from 0 to 65 535. - Any other
<type>printsresolv: unknown record type "<type>"and exits 1 without contacting resolvd. - The
<address>ofreverseis checked in the same way: anything that is not an IPv4 or IPv6 address printsresolv: "<address>" is not an addressand exits 1 without contacting resolvd.
8.1.2.3 No timeout in resolv #
resolv sets no timeout on the connection. It waits for as long as
resolvd takes to answer.
8.1.3 Output #
8.1.3.1 query output #
query prints a summary line, then one line per record:
<outcome> <source>[ via <server>][ on <interface>] validation <validation>
<name> <ttl> <type> <text>
with via present when the reply names a server and on when it names
an interface, and fields on record lines separated by tabs. A line
rcode <name> follows the summary when the outcome is found and the
response code is not zero. resolvd answers found only with response
code zero (§4.8), so against resolvd the line does not appear.
8.1.3.2 lookup output #
lookup prints <outcome> <source> canonical <name>, then
<address> <ttl>, tab-separated, for each address.
8.1.3.3 reverse output #
reverse prints <outcome> <source>, then the text of every record
in the answer, whatever its type.
8.1.3.4 status output #
status prints three lines:
hostname <name, or (unset)>
netd <connected | not connected>
cache <n> entries
then scopes (none) when there are no scopes, or, for each scope, a
blank line and
<interface> metric <n>[ [default-route, exclusive]]
server <address>[ (demoted)]
domain <domain>
subnet <address/prefix>
with the bracketed flags present as they apply and one line per server,
domain and subnet. When fallback servers are configured, a blank line
and a fallback <servers> line follow, the servers separated by single
spaces. Last come a blank line and the counters, on one line:
queries <n> synthetic <n> cache-hits <n> upstream sent <n> answered <n> failed <n> refused <n>
8.1.3.5 flush output #
flush prints nothing on success.
8.1.4 Exit status #
8.1.4.1 The unreachable message #
Errors are printed to standard error prefixed resolv: . resolvd
unreachable reads
resolv: resolvd is not reachable at /run/resolvd/resolv.sock: <error>.
9.1 Reading resolvd's State
Peios / Peios technical reference / resolvd / Failure Modes
9.1.1 Where to look #
| Signal | Tells you |
|---|---|
resolv status | Whether netd is connected, the scopes in use, which servers are demoted, the fallback servers, cache size, counters (§5.3, §8.1) |
resolv query <name> --no-cache | Where a fresh answer comes from: source, server, interface, response code (§4.1) |
evctl 'LOGS FROM resolvd SINCE 1h ago' | Every log line (§2.1) |
The exit status of resolv | 2 for notfound, 3 for unavailable, 1 when resolvd could not be asked (§8.1) |
| The door's own error | NXDOMAIN or SERVFAIL at the stub; HOST_NOT_FOUND, TRY_AGAIN or NO_RECOVERY from getaddrinfo (§6.2, §7.1) |
Two readings of the counters are worth knowing. upstream_failed rising
with upstream_answered flat means no matching reply is arriving: the
servers are not answering, their replies do not match (§4.8), or the
transactions cannot be sent at all (§4.7), which the log shows as
upstream <server>: <error> lines. Both rising together means servers
answer with failure codes. refused above zero means the in-flight
ceiling has been reached (§4.6).
9.1.2 Log lines #
Every line resolvd writes, with the level it is written at:
9.1.3 What a question logs #
Answering a question writes no log line, however it is answered —
synthetic, from the cache, by a server, or unavailable after timeouts —
and a timeout or a demotion writes none either. The only lines a single
question or request can cause are upstream <server>: <error>, for a
transaction that could not be sent (§4.7), and, for a native request,
control: no peer token: <error> (§5.2) and
control: reply failed: <error> (§5.1).
9.2 Names Do Not Resolve
Peios / Peios technical reference / resolvd / Failure Modes
9.2.1 Everything needing the network is unavailable #
Looks like: resolv query example.com exits 3; the stub answers
SERVFAIL; getaddrinfo returns EAI_AGAIN. localhost, the
hostname and static names still answer.
Check resolv status:
netd not connectedandscopes (none). resolvd has never had a snapshot. Without fallback servers nothing can be asked (§4.4). Start netd; resolvd reconnects within ten seconds of netd's socket appearing (§3.2) and the first snapshot brings the scopes.- Scopes listed, but none has a server. Interfaces with no servers
take no part in routing (§4.4). Either the profiles and leases supply
no servers, or the offered addresses or explicit zones were invalid
(§4.7). Configure
FallbackServers(§2.3), or give the profile servers. - Servers listed and marked
(demoted). Every server's last failure was less than 30 seconds ago (§4.6).upstream_failedrising shows it. Questions are still sent to demoted servers, so service returns as soon as one answers. - The servers listed are link-local (
fe80::…). Check that the scope names the intended interface and that it still exists. resolvd sends through that interface for UDP and TCP (§4.7). Link-local servers belong in an interface profile, not the unscoped fallback list.
9.2.2 A single label is notfound at once #
Looks like: resolv query printer exits 2 without delay, with
source local.
No search domain applies (§4.3). Check the domain lines in status.
A scope's domains count only while it has a server; while an exclusive
scope takes every question (§4.4), only its domains apply and
ExtraSearchDomains is ignored.
A single label is never sent bare, so with no domain it is answered
locally.
9.2.3 Names meant for the VPN go elsewhere #
An exclusive scope takes every question only while it is at addressed
or better and has at least one server (§4.4). A VPN interface whose
servers have not arrived yet, or are missing from its profile, is not
exclusive for routing, and names go to the other interfaces' servers in
the meantime. status shows the VPN scope with exclusive and no
server lines.
9.2.4 An answer is wrong or stale #
Cached answers live up to a day (§4.5). They are discarded for a scope
when its server list changes, and for everything by
resolv flush (§5.3), which needs RESOLVER_CONTROL. A static name in
Hosts\ overrides DNS at once, with no flush (§4.2).
To see what the network says now, use
resolv query <name> --no-cache. It skips the cache lookup, but the
answer it gets is still stored, replacing the cached one (§4.5).
9.2.5 Answers are slow #
Each candidate can take up to six seconds of timeouts before it is
unavailable, longer with truncated replies retried over TCP (§4.6).
A dead first server costs two seconds per question until it is demoted;
after that it is asked last, for 30 seconds, and the cycle repeats if
it is still dead.
A UDP reply that does not match its query — a server that does not echo
the question's case exactly, for instance — is ignored (§4.8). A later
matching reply can still answer the transaction. Only if none arrives by
the original two-second deadline does the attempt time out, demote the
server, and add to upstream_failed without adding to upstream_answered.
9.2.6 refused is rising #
At least 4 096 transactions are outstanding (§4.6), and new questions that need
the network are answered unavailable at once. Cached and synthetic
answers are unaffected. The ceiling clears as transactions complete,
within two seconds of each being sent.
9.2.7 Getting back to a known state #
A new resolvd discards the cache, the demotions and the counters,
re-reads the registry, and takes a fresh snapshot from netd; nothing is
persisted. svctl restart resolvd is enough: the new process replaces
the socket its predecessor left (§2.4).
The exception is the first start after an upgrade from a resolvd whose
DACL did not name its own account (§2.4). That older resolvd's directory
and socket shut the new one out, and it exits with native socket: Permission denied (os error 13) until they are gone. Remove them once
as SYSTEM, or reboot:
; ;
9.3 resolvd Cannot Be Reached
Peios / Peios technical reference / resolvd / Failure Modes
9.3.1 resolvd is not running #
| Door | What a client sees |
|---|---|
| NSS shim | localhost and loopback reverses still answer; everything else is UNAVAIL with NO_RECOVERY (§7.1) |
| Stub listener | Nothing listens on 127.0.0.53; UDP queries go unanswered or are refused by the kernel, TCP connections are refused |
resolv | Exit 1, resolv: resolvd is not reachable at /run/resolvd/resolv.sock: <error> |
peinit restarts resolvd whenever it exits (§2.1). The new process removes the socket the old one left and binds a fresh one (§2.4), so an outage after a crash lasts only until the restart completes. If resolvd keeps exiting, the cause is one of the startup failures below.
9.3.2 resolvd will not start #
The startup failures are fatal and logged before the exit (§2.2):
stub listener on 127.0.0.53:53: <error>: either the port reservation fortcp,udp:53is not in the registry — its seed is inert until the image applies it (§2.1) — or something else already holds127.0.0.53:53. The error text says which.native socket: <error>:/run/resolvdcould not be created or given its mode, a stale socket file could not be removed, or the socket could not be bound or set up.
Because the restart policy is Always, a failure that persists is a restart loop: the same line in the log each time.
9.3.3 The native door is closed to ordinary programs #
Looks like: getaddrinfo fails with NO_RECOVERY for every name
but localhost, for every user but SYSTEM and administrators, while
programs that send DNS to 127.0.0.53 themselves work.
Two causes:
- The socket's descriptor could not be written at startup (§2.4), and
the log has the
could not set a descriptorline. Ordinary programs cannot reach the socket at all. ControlSecurityholds a valid descriptor that does not grantRESOLVER_QUERYto them (§5.2). They reach the socket and are denied:resolv queryprintsresolv: access denied. Deleting the value restores the compiled default without a restart (§2.3).
The stub listener is not governed by either, which is why direct DNS clients are unaffected.
9.4 Stalls and Loops
Peios / Peios technical reference / resolvd / Failure Modes
9.4.1 A client that does not read #
Native and stub TCP replies are written without blocking (§2.5). A client
that does not read can retain its own queued reply until the fixed
one-second write deadline, then its connection closes. It does not stop
other lookups. Each door holds at most 256 queued replies; if full, a
new reply that cannot be written immediately is closed too. Stub TCP
write failures are silent; native failures log control: reply failed.
9.4.2 netd refuses the subscription #
When netd accepts the connection and answers subscribe with an error
— resolvd's account lacks NETWORK_QUERY, for instance — resolvd drops
the channel and retries with exponential backoff (§3.2): 0.5 seconds,
then 1, 2, 4, 8, and ten seconds between attempts until a good snapshot
resets the sequence. The log shows subscribed to netd, then netd refused the subscription, then lost the netd channel; reconnecting
for each refusal. resolv status shows netd
as not connected except in the moment between each reconnection and
its refusal, and the scopes are whatever the last accepted snapshot
held, or none.
9.4.3 The registry watch is lost #
When the watch cannot be armed at startup, or fails and cannot be
re-armed, configuration changes are no longer seen (§2.3). resolv status still shows the fallback servers in use, so a change to
FallbackServers that does not appear there is the sign. A restart
reads the key again and re-arms the watch.
9.4.4 Cache capacity pressure #
At 8 192 entries, storing a new key removes expired entries first. If that creates room, it keeps every live entry; otherwise it replaces only the entry with the earliest expiry (§4.5). A long answer lifetime does not cause a sweep of shorter-lived answers that are still valid.
cache_entries can fall sharply when admission removes many expired
entries. Expiry alone does not remove them, so reaching 8 192 does not
mean 8 192 live answers. Looking up an expired key also removes it,
before its replacement arrives.
Older builds compared the sweep against the incoming answer's expiry (§4.5). On those builds a day-long answer could empty a full cache of live answers, causing a burst of upstream requests.
Appendix A Constants and Limits
Peios / Peios technical reference / resolvd
The articles cited are authoritative; this appendix collects their values in one place.
A.1 Limits and timers #
| Bound | Value | Described in |
|---|---|---|
| Native request size ceiling (payload) | 65 536 bytes | §5.1 |
| Maximum buffered native frame | 65 540 bytes | §5.1 |
| Native connection read chunk | 4 096 bytes | §5.1 |
| Native request delivery bound | 5 s | §5.1 |
| Native connections still sending a request | 256 | §5.1 |
| Queued native replies | 256 | §2.5 |
| Queued stub TCP replies | 256 | §2.5 |
| Native reply write deadline | 1 s total | §5.1 |
| Stub TCP connections still sending a query | 256 | §6.1 |
| Stub TCP query delivery bound | 10 s | §6.1 |
| Stub TCP message buffer | 65 537 bytes | §6.1 |
| Stub TCP read chunk | 4 096 bytes | §6.1 |
| Stub UDP read buffer | 4 096 bytes | §6.1 |
| Stub TCP reply write deadline | 1 s total | §6.2 |
Classic UDP reply limit, no OPT | 512 bytes | §6.2 |
| EDNS0 payload size advertised, upstream and stub | 1 232 bytes | §4.7, §6.2 |
| Transaction timeout | 2 s | §4.6 |
| Attempts per question (all candidates and transports) | 3 | §4.6 |
| Demotion period | 30 s | §4.6 |
| Service descriptor limit | 16 384 | §2.1 |
| Listener retry delay after accept error | 250 ms | §2.5, §9.1 |
| Upstream transactions in flight | 4 096 | §4.6 |
| Upstream UDP read buffer | 4 096 bytes | §4.7 |
| Cache entries | 8 192 | §4.5 |
| Positive lifetime cap | 86 400 s | §4.5 |
| Negative lifetime cap | 300 s | §4.5 |
CNAME chase depth in lookup | 16 | §4.9 |
| netd reconnect backoff | 0.5 s doubling to 10 s | §3.2 |
| netd channel write timeout | 2 s | §3.1 |
| netd frame ceiling | 65 536 bytes | §3.1 |
| netd channel read chunk | 8 192 bytes | §3.1 |
| Registry watch event buffer | 16 384 bytes | §2.3 |
| Kernel hostname read buffer | 256 bytes | §3.3 |
| Unknown-value nesting depth, native requests | 32 | §5.1 |
| NSS shim read and write timeout | 10 s | §7.1 |
| DNS label | 63 bytes | §4.1 |
| DNS name on the wire | 255 bytes | §4.1 |
A.2 Rights #
| Right | Value | Described in |
|---|---|---|
RESOLVER_QUERY | 0x00000001 | §5.2 |
RESOLVER_CONTROL | 0x00000002 | §5.2 |
RESOLVER_ALL_ACCESS | 0x000F0003 | §5.2 |
A.3 Paths, addresses and identities #
| Item | Value | Described in |
|---|---|---|
| Daemon | /usr/sbin/resolvd | §2.1 |
| Operator command | /usr/bin/resolv | §8.1 |
| NSS shim | /usr/lib/x86_64-linux-peios/libnss_peios_net.so.2 | §7.1 |
| Runtime directory | /run/resolvd, mode 0755 | §2.4 |
| Native socket | /run/resolvd/resolv.sock, mode 0666 | §2.4 |
| Stub listener | 127.0.0.53:53, UDP and TCP | §2.4 |
| netd control socket | /run/netd/control.sock | §3.1 |
| Upstream port | 53 | §4.7 |
| Constant resolver configuration | /usr/etc/resolv.conf | §2.1 |
| Service definition seed | /usr/share/regim/resolvd-service.reg | §2.1 |
| Port reservation seed | /usr/share/regim/resolvd-port.reg | §2.1 |
| Registry reference | /usr/share/regman/resolvd.regman | §2.1 |
| Service SID | S-1-5-80-3864064249-1823296737-2008945602-1354971773-2894779966 | §2.1 |
| Fallback scope key | fallback | §4.4 |
Appendix B Registry Keys
Peios / Peios technical reference / resolvd
The articles cited are authoritative; this appendix collects the keys in one place.
B.1 Read by resolvd #
| Key | Value | Type | Default | Described in |
|---|---|---|---|---|
Machine\System\Network\Dns | FallbackServers | REG_MULTI_SZ (or one REG_SZ or REG_EXPAND_SZ) | none | §2.3 |
Machine\System\Network\Dns | ExtraSearchDomains | REG_MULTI_SZ (or one REG_SZ or REG_EXPAND_SZ) | none | §2.3 |
Machine\System\Network\Dns | ControlSecurity | REG_BINARY | the compiled default of §5.2 | §2.3, §5.2 |
Machine\System\Network\Dns\Hosts | any name: a static name | REG_SZ, REG_EXPAND_SZ or REG_MULTI_SZ | none | §2.3, §4.2 |
resolvd watches Machine\System\Network and everything under it, and
re-reads Machine\System\Network\Dns whole on any change (§2.3). It
reads no other key.
B.2 Seeded by the package #
Both seeds are installed under /usr/share/regim/ and are inert until
an image applies them (§2.1).
| Seed | Key | Values |
|---|---|---|
resolvd-service.reg | Machine\System\Services\resolvd | ImagePath, Triggers, Identity, Readiness, RuntimeDirectories, RestartPolicy, ErrorControl, DisplayName, Description |
resolvd-port.reg | Machine\System\Network\TcpIp\PortReservations | tcp,udp:53 |