Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

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):

DoorWhereWho uses it
Native socket/run/resolvd/resolv.sockThe resolv command, the NSS shim, Peios tools
Stub listener127.0.0.53:53, UDP and TCPPrograms that speak DNS themselves
NSS shimlibnss_peios_net.so.2, loaded into every glibc processgetaddrinfo, 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:

ComponentPackageInstalled as
The daemondev.peios.resolvd/usr/sbin/resolvd
The operator commanddev.peios.resolv/usr/bin/resolv
The NSS shimdev.peios.resolvd-nss/usr/lib/x86_64-linux-peios/libnss_peios_net.so.2

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:

CrateWhat it is
dnsThe DNS wire codec: names, messages, records. No I/O and no policy.
libresolvThe native channel's types, MessagePack codec and framing. No I/O beyond framing.
resolvdThe engine, the cache, the stub door's DNS rendering, and the daemon around them.
resolvThe operator command.
nssThe 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 #

ValueTypeDataEffect
ImagePathREG_SZ/usr/sbin/resolvdThe binary. resolvd reads no command-line arguments.
TriggersREG_MULTI_SZbootStarted at boot.
IdentityREG_SZServiceRuns under its own virtual service account.
ReadinessREG_DWORD0Notify: ready when resolvd says so.
RuntimeDirectoriesREG_MULTI_SZresolvdpeinit creates /run/resolvd.
RestartPolicyREG_DWORD2Always restarted.
LimitNOFILEREG_DWORD16384Descriptor headroom for 4 096 upstream transactions, their client waiters, 512 pending clients and daemon descriptors.
ErrorControlREG_DWORD0
DisplayNameREG_SZStub resolver
DescriptionREG_SZ(one sentence)

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:

KeyValueType
Machine\System\Network\TcpIp\PortReservationstcp,udp:53REG_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=1 to the socket named by the NOTIFY_SOCKET environment 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_SOCKET is unset, and logs a failure to send as readiness 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:

evctl 'LOGS FROM resolvd SINCE 1h ago TAKE 40'

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 #

  1. 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).
  2. 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 logs stub listener on 127.0.0.53:53: <error> and exits with status 1.
  3. The registry. Machine\System\Network\Dns is read whole (§2.3). A missing or unreadable key is not an error; every value takes its default.
  4. The registry watch. A subtree watch is armed on Machine\System\Network (§2.3). If it cannot be armed, resolvd logs registry watch unavailable (<error>); configuration is read once and runs on the configuration it read in step 3 until it restarts.
  5. 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.
  6. 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.
  7. The control object is built from ControlSecurity (§5.2).
  8. netd. The first attempt to subscribe is made immediately (§3.1). Its failure is not fatal and is retried with backoff (§3.2).
  9. The listening line. resolvd logs listening on /run/resolvd/resolv.sock and 127.0.0.53:53 at info level.
  10. Readiness. READY=1 is sent (§2.1).
  11. 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.

ValueType readDefaultUsed for
FallbackServersREG_MULTI_SZ, or one REG_SZ / REG_EXPAND_SZnoneThe fallback scope's servers (§4.4)
ExtraSearchDomainsREG_MULTI_SZ, or one REG_SZ / REG_EXPAND_SZnoneSearch domains after every interface's (§4.3)
ControlSecurityREG_BINARYthe compiled defaultThe control object (§5.2)
Hosts\a subkeyemptyStatic names (§4.2)

2.3.2 Reading strings #

  • A REG_SZ or REG_EXPAND_SZ is 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_SZ is read as it is stored; nothing in it is expanded.
  • A REG_MULTI_SZ is 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 lone REG_SZ that is empty or only whitespace, a whitespace-only string in a multi-string, and an address with a zone such as fe80::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_BINARY value 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 default each 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\Hosts is one static name. The value's name is the host name and its data is one address (REG_SZ or REG_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 as Dns 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 Printer and printer are 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:

  1. rebuilds the control object from ControlSecurity;
  2. 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;
  3. replaces the static names;
  4. logs configuration changed at 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-arming and 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:

  1. creates /run/resolvd if peinit has not, and sets its mode to 0755;
  2. replaces the directory's DACL (below);
  3. removes any existing /run/resolvd/resolv.sock, logging removed a stale /run/resolvd/resolv.sock at warn level when there was one, and a removal that fails is fatal (§2.2);
  4. binds the socket, sets its mode to 0666, and replaces its DACL in the same way;
  5. 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:

TrusteeAccess
SYSTEMGENERIC_ALL
resolvd's own account (its token's user)GENERIC_ALL
EveryoneGENERIC_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.53 port 53 and both nonblocking (PSPU §6.8).
  • Nothing is bound on ::1 or on any other address.

Binding port 53 depends on the port reservation (§2.1).

2.4.3 Other descriptors #

DescriptorCountLifetime
The registry watch on Machine\System\Network0 or 1From startup; replaced on a watch error (§2.3)
The netd channel0 or 1While subscribed (§3.1)
Native connections reading or awaiting resolutionup to 256 totalReading: at most 5 s (§5.1); resolving: until completion or definite departure (§4.1)
Stub TCP connections reading or awaiting resolutionup to 256 totalReading: at most 10 s (§6.1); resolving: until completion or definite departure (§4.1)
Queued native reply writersup to 256Until written, failed, or the one-second write deadline (§2.5)
Queued stub TCP reply writersup to 256Until written, failed, or the one-second write deadline (§2.5)
Upstream socketsone per transaction, at most 4 096 including retriesOne 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:

  1. the native listener, except during its accept-error backoff;
  2. the stub UDP socket;
  3. the stub TCP listener, except during its accept-error backoff;
  4. the netd channel, while connected;
  5. the registry watch, while armed;
  6. every native connection still sending its request;
  7. every stub TCP connection still sending its query;
  8. every upstream socket — for reading, or for writing while a TCP connection is still being established or its query is still being sent;
  9. native and stub TCP replies waiting to write, for writability;
  10. 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:

  1. waiting native and stub TCP streams with definite hangup/error, cancelling their questions and upstream transactions;
  2. upstream sockets — replies that have arrived are answers already owed;
  3. queued native replies, then queued stub TCP replies, whose sockets are writable;
  4. the stub UDP socket, reading at most 64 datagrams;
  5. the stub TCP listener, accepting at most 64 connections;
  6. stub TCP connections still sending queries;
  7. the native listener, accepting at most 64 connections;
  8. native connections still sending requests;
  9. the netd channel;
  10. 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:

  1. connects to /run/netd/control.sock;
  2. sets a two-second write timeout and writes the subscribe request in netd's framing — a four-byte little-endian length and a MessagePack map;
  3. 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:

FrameWhat resolvd does
A snapshot replyApplies it (§3.3), once the whole read has been taken. Several in one read are applied one after another, so the last wins.
An error replyLogs netd refused the subscription: <message>, and drops the channel once the whole read has been taken.
Any other well-formed replyIgnores it.
A payload that does not decodeLogs netd sent something unreadable: <error>, and drops the channel once the whole read has been taken.
A length above 65 536 bytesStops taking frames: the rest of the buffer is discarded and the channel dropped.

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:

AttemptTime after the first
20.5 s
31.5 s
43.5 s
57.5 s
615.5 s
7 and laterevery 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 reachable warning is not logged for the failures that follow, however many there are; the lost the netd channel; reconnecting line 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 hostname is 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 status reports an empty hostname.
  • 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:

Snapshot fieldBecomes
ifidThe scope key
nameThe interface name reported in answers and in status
serversServer addresses, in order. Malformed addresses are logged and dropped. Link-local IPv6 addresses use the containing interface; an optional %name or %index must match it (§4.7). There is no port; servers are asked on port 53.
domainsSearch domains, in order. A string that does not parse as a name, and the root, are dropped.
addresses(address, prefix length) pairs. A string that is not address/prefix is dropped.
default_route, exclusive, metric, levelCarried as they are
ntpIgnored

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 #

  1. 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 notfound at once.
  2. Synthetic names. The name as asked is checked against the synthetic names (§4.2). A match answers the task.
  3. 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.
  4. Routing. Each candidate in turn is routed to one scope (§4.4). When no scope can take it, the task is answered unavailable.
  5. The cache. Unless the request asked to bypass the cache, the cache is consulted under that scope (§4.5).
  6. The network. Otherwise the candidate is sent upstream (§4.6 to §4.8).
  7. The next candidate. A notfound for a candidate — from the cache or from a server — moves to the next candidate when there is one. Any other result answers the task.
  8. The last candidate. When the last candidate is notfound, the task is answered notfound.

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 #

Ends atOutcomesourceserverinterfacercode
Name does not parsenotfoundlocalnilnil0
Synthetic nameper §4.2synthetic or hostsnilnil0
No candidatesnotfoundlocalnilnil0
No scope can take the candidateunavailablelocalnilnil0
Cache hitas cachedcachethe server whose reply was cachedthe scope's interfacethe cached reply's response code
A server's replyfound or notfounddnsthe serverthe scope's interfacethe server's response code
Every candidate notfoundnotfoundas the last candidate's cache hit or server's reply, in the two rows aboveas that rowas that rowas that row
Attempts exhaustedunavailablelocalnilthe scope's interface0
No servers left in the scopeunavailablelocalnilnil0
In-flight ceiling reachedunavailablelocalnilnil0

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 #

  1. localhost, or any name under it. 127.0.0.1 for A, ::1 for AAAA, both for ANY. Source synthetic.
  2. A static name. An exact, case-insensitive match against the Hosts\ values (§2.3). The name's addresses of the asked family — IPv4 for A, IPv6 for AAAA, both for ANY. Source hosts.
  3. The machine's hostname. An exact, case-insensitive match against the hostname (§3.3), answered with the machine's own addresses (below). Source synthetic.
  4. .local. The name local or any name under it is notfound, whatever the type asked. Source synthetic. No query is sent.
  5. A reverse-mapping name, for PTR or ANY only. 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 addressed or 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.1 and ::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.arpa label parses as a decimal number from 0 to 255. Leading zeros and a leading + are accepted, so 1.0.0.0127.in-addr.arpa and +1.0.0.127.in-addr.arpa both name 127.0.0.1.
  • Each ip6.arpa label 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:

AddressAnswerSource
A loopback address — anything in 127.0.0.0/8, or ::1PTR localhostsynthetic
Listed under a static namePTR that namehosts
One of the machine's own addresses, when there is a hostnamePTR the hostnamesynthetic

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_cache flag has no effect on them.
  • Each one adds one to the synthetic counter (§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 as printer is.
  • A top-level domain such as com is 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.

WhenDomains, in order
Routing's step 1 applies: a routable exclusive scope is at addressed or better (§4.4)That scope's search domains, and nothing else
OtherwiseThe search domains of every routable scope (§1.3), scopes ordered by metric, then ExtraSearchDomains

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.
  • ExtraSearchDomains is 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.
  • unavailable is the answer, and later candidates are not asked.
  • When the last candidate is notfound, the question is notfound (§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 #

  1. Exclusive. The routable scopes that are exclusive and at level addressed or better. The one with the lowest metric takes the candidate; among equal metrics, the first in snapshot order.
  2. 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.
  3. 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.
  4. Default route. The routable scopes with default_route set. The lowest metric wins, then the earliest.
  5. 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.
  6. Fallback. When there are no routable scopes at all, the fallback scope, provided FallbackServers names at least one server. Its scope key is fallback.

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 addressed or 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 unavailable without 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 matching IN answer 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:

ReplyStored asLifetime
NOERROR with relevant answer records of class IN (§4.8)found with those recordsThe least TTL among those records, capped at 86 400 s
NOERROR with no relevant answer records of class INfound with noneNegative lifetime
NXDOMAINnotfoundNegative lifetime

4.5.2.1 The negative lifetime #

  • The negative lifetime comes from the first IN SOA record 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's MINIMUM field 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 SOA in 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 notfound hit with further candidates waiting moves to the next candidate, as a notfound from a server would (§4.1).

4.5.4 Bypassing the cache #

  • A resolve request with no_cache set skips the lookup for each of its candidates. The reply it gets is still stored.
  • Stub queries, lookup and reverse always 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_entries count in status includes 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:

  1. every entry whose expiry is at or before the current time;
  2. 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 #

EventDiscards
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 restartEverything

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:

BoundValue
Transaction timeout2 s
Attempts per question (all candidates and transports)3
Demotion period30 s
Upstream transactions in flight4 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 NOERROR nor NXDOMAIN — 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 NOERROR or NXDOMAIN that resolvd uses — a UDP reply without TC, or a TCP reply — removes the server's mark at once. A UDP reply with TC set 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.
  • status lists, 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 NXDOMAIN that 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 unavailable at once, and the refused counter goes up by one. A question whose scope has no servers left is answered unavailable before 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 #

FieldValue
ID16 bits from the kernel CSPRNG, fresh for every transaction
FlagsRD set; CD clear; everything else clear
QuestionThe candidate, case-randomised; the record type asked; class IN
AdditionalOne OPT record: payload size 1 232, DO clear, extended response code 0, version 0, no options

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 TC set 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 is unavailable.
  • A TCP reply with TC set 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:

  1. it decodes as a DNS message — bytes after its last section are tolerated;
  2. its ID equals the transaction's;
  3. its QR bit is set;
  4. it has exactly one question;
  5. that question's type and class equal those sent;
  6. 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:

ReplyResult
UDP, TC setTCP retry to the same server (§4.7)
Response code NOERRORfound
Response code NXDOMAINnotfound
Any other response codeThe 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 NXDOMAIN none 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 IN CNAME 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 IN types 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:

familyTasks
anyA and AAAA, started together
inetA
inet6AAAA

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' outcomesoutcome
At least one found, with or without recordsfound
None found, at least one unavailableunavailable
Every one notfoundnotfound

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.

  1. 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 the CNAME's target.
  2. This repeats at most 16 times; a loop stops there.
  3. The addresses are the A (for the A task) or AAAA (for the AAAA task) 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 #

  • addresses lists 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.
  • canonical and source come from the same selected found task. Prefer a task that produced addresses; when both or neither did, prefer A over AAAA. Completion order does not affect this choice. canonical is the name that task's CNAME chase ended at, after search expansion. Without a found task it is the name asked, as parsed, in presentation form without a trailing dot (the root is .).
  • A found with no addresses still reports the expanded or chased name.
  • A name that does not parse is answered at once with outcome notfound, canonical ., no addresses and source local; no task is started.
  • source is the source of the selected task, and local when no task was found. It describes that task; the other family's records can have a different source.
  • validation is unvalidated.
  • A lookup adds one to queries however 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 #

CounterGoes up by one for
queriesEvery resolve, lookup and reverse request that passes the access check, and every syntactically accepted stub query, including UDP budget refusals and requests whose name does not parse
syntheticEvery task answered by a synthetic name (§4.2), including .local's notfound; an any lookup of localhost adds two
cache_hitsEvery candidate found live in the cache (§4.5), including a notfound hit that moves on to the next candidate
upstream_sentEvery transaction started, UDP or TCP, including TCP retries. It is counted before the socket is opened, so a transaction whose socket cannot be opened, connected or sent on (§4.7) counts here as well as in upstream_failed, though nothing reached the wire
upstream_answeredEvery matching reply (§4.8), whatever its response code, including a truncated one
upstream_failedEvery failed transaction (§4.6): a timeout, a transport failure, or a reply with a response code other than NOERROR and NXDOMAIN
refusedEvery question refused at the in-flight ceiling (§4.6), the UDP pending budgets (§6.1), or because fresh query entropy is unavailable (§4.7)

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.

  1. Read exactly the four-byte little-endian payload length. A length above 65 536 gets the reply request too large and the connection closes, without reading any payload bytes.
  2. 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.
  3. 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 (a uint 64 of 2^63 or more) anywhere inside the value, a map key included, fails the request with malformed 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.
  • query names the request; a missing query is an error, and an unknown one is an error.
  • Every key resolvd knows — query, name, type, no_cache, family and address — 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; type is checked against 65 535 in every request, so {query: status, type: 70000} is refused too. family takes any string (§5.3), and address is parsed as an address only in a reverse.
  • 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:

errorCause
request too largeThe size checks above
malformed message: truncated messageThe payload ends inside a value, or an array or map claims more items than bytes remain, a map's keys and values counted separately
malformed message: unexpected value typeA known field, a key, or the payload itself has the wrong MessagePack type; this includes a type that is negative or above 9 223 372 036 854 775 807, and an integer above 9 223 372 036 854 775 807 anywhere inside an unknown key's value
malformed message: unsupported value typeA floating-point or extension value
malformed message: string is not UTF-8A string that is not UTF-8
malformed message: nested too deeplyNesting beyond 32 levels inside a skipped value
missing or malformed field <name>A field the request needs is absent; a type, in any request, from 65 536 to 9 223 372 036 854 775 807; or the address of a reverse that is not an IP address
duplicate field <name>A repeated key
unknown query "<name>"A query resolvd does not know
access deniedThe access check (§5.2)
reply too largeThe encoded reply would exceed the 65 536-byte payload ceiling; it is replaced by this error

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 #

  1. 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.
  2. 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.
  3. A denied request is answered access denied (§5.1).
RequestRight
resolve, lookup, reverse, statusRESOLVER_QUERY (0x0001)
flushRESOLVER_CONTROL (0x0002)

5.2.1.1 The generic mapping #

The check uses this generic mapping:

Generic rightMaps to
GENERIC_READRESOLVER_QUERY, READ_CONTROL
GENERIC_WRITERESOLVER_CONTROL, READ_CONTROL
GENERIC_EXECUTERESOLVER_QUERY
GENERIC_ALLRESOLVER_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):

ComponentValue
OwnerSYSTEM
GroupSYSTEM
DACLSYSTEM: 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:

  • status and flush are 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 #

FieldAccepted
nameAny string: no name is refused at decoding. It is handed to the engine as it is, which parses it (§4.1).
typeAny value from 0 to 65 535, forwarded as it is — ANY (255), OPT (41) and types resolvd has no parser for included.
no_cacheBoolean; absent means false. Skips the cache lookup (§4.5).

The reply's outcome, source, server, interface and rcode are as §4.1 describes. Each record carries:

KeyContent
nameThe owner name in presentation form, without a trailing dot; the root is .. A dot or backslash inside a label is written \. or \\, and any byte outside ! to ~ as a backslash and three decimal digits.
typeThe record type number
ttlAs sent by the server, lowered on a cache hit (§4.5); 0 for a synthetic record
dataThe record data in uncompressed wire form
textThe record data in presentation form (below)
Typetext
A, AAAAThe address
CNAME, PTR, NSThe target name, written as name is
SOAmname rname serial refresh retry expire minimum
MXpreference exchange
SRVpriority weight port target
TXTEach 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 #

KeyContent
hostnameThe hostname in use (§3.3), or an empty string when there is none
netdWhether the netd channel is connected at this moment
scopesEvery scope in the latest snapshot, in snapshot order — including scopes with no servers and scopes that take no part in routing
fallback_serversFallbackServers as parsed (§2.3)
cache_entriesThe number of entries held, expired ones included (§4.5)
counters§4.10

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:

  • subnets holds the interface's addresses as address/prefix — the address itself, not the network it is in;
  • demoted holds those of the scope's servers that are demoted at this moment (§4.6);
  • a scope's level is not reported.

5.3.6 flush #

  • Every cache entry, for every scope, is discarded, resolvd logs cache flushed at info level, and the reply is ok: true with no kind.
  • Demotions and counters are left as they are.

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:

  1. A datagram whose source address is not a loopback address — anything outside 127.0.0.0/8 — is dropped without a reply.
  2. It is checked as a query (below). A refused query gets its error reply, or nothing.
  3. 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 both queries and refused. This admission check precedes synthetic and cache lookup.
  4. 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 #

The messageResult
Does not decode, and is at least 12 bytes longFORMERR: the query's ID, QR set, every other header bit clear, no sections
Does not decode, and is shorter than 12 bytesDropped without a reply
Has QR setDropped without a reply
Has an opcode other than QUERYNOTIMP
Has no question, or more than oneFORMERR
Has a question class other than INNOTIMP; the question is echoed unchanged and no upstream query is made
OtherwiseAccepted

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, RD and CD bits, and sets QR and RA; every other bit is clear;
  • the question section is the query's, whatever it holds;
  • when the query carried an OPT record, the reply carries one whose payload size is the size the query advertised, with DO clear and no options; without one, the reply has none.

6.1.4 What an accepted query contributes #

  • Only IN questions 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, the DO bit 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, RD and CD bits and its question section.
  • QR and RA are set. AA, AD and TC are clear, except that TC is 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 CNAME from 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 found with no records at an expanded name is NOERROR with only that leading CNAME. Its TTL is the negative cache lifetime: the lesser of the enclosing upstream SOA record's TTL and minimum field, capped at 300 seconds and at the TTL of any CNAME path the reply followed (§4.5). A cache hit uses the remaining lifetime, rounded down to whole seconds. With no enclosing SOA, the TTL is zero; no lifetime is invented. The authority section remains empty. An unexpanded empty found has no CNAME.

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 OPT advertised, or 512 bytes when that is smaller or there is no OPT.
  • A reply over the limit is replaced by a truncated one, which keeps the same header with TC set, and the question.
  • The truncated reply keeps the OPT record 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.2 is installed by dev.peios.resolvd-nss at /usr/lib/x86_64-linux-peios/libnss_peios_net.so.2.
  • It is built from the nss crate, with the native channel codec compiled in. Its only shared-object dependencies are libc.so.6 and 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 load libgcc_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 pointAsks
_nss_peios_net_gethostbyname4_rlookup, family any
_nss_peios_net_gethostbyname3_rlookup, family from af
_nss_peios_net_gethostbyname2_ras gethostbyname3_r, with no TTL or canonical name out
_nss_peios_net_gethostbyname_ras gethostbyname3_r, with af AF_INET
_nss_peios_net_gethostbyaddr2_rreverse
_nss_peios_net_gethostbyaddr_ras 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 #

ResultNSS statuserrnoh_errno
Addresses, or names, foundSUCCESS——
found, but no address of the asked familyNOTFOUNDENOENTHOST_NOT_FOUND
notfoundNOTFOUNDENOENTHOST_NOT_FOUND
unavailableTRYAGAINEAGAINTRY_AGAIN
No reply within ten secondsTRYAGAINEAGAINTRY_AGAIN
resolvd cannot be connected toUNAVAILENOENTNO_RECOVERY
An error reply, including access deniedUNAVAILENOENTNO_RECOVERY
A reply of the wrong kind, a reply that does not decode, or any other I/O errorUNAVAILENOENTNO_RECOVERY
The caller's buffer is too smallTRYAGAINERANGENETDB_INTERNAL (-1), required for retry; see version note below
gethostbyname3_r or 2_r with an af other than AF_INET or AF_INET6UNAVAILEAFNOSUPPORTNO_DATA
gethostbyaddr2_r or _r with an af and length other than AF_INET/4 or AF_INET6/16UNAVAILEAFNOSUPPORTNO_RECOVERY
A null name, a name that is not UTF-8, or a null addressNOTFOUNDENOENTHOST_NOT_FOUND

Note

The buffer-exhaustion row specifies the glibc-compatible retry contract. resolvd 0.1.6 source instead sets h_errno to 0; that implementation does not satisfy the retry contract. The corrected behavior requires the NSS buffer-retry fix. Check the installed module's source revision before relying on it.

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 localhost or 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.1 for IPv4, ::1 for IPv6, or both, IPv4 first, with canonical name localhost and 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 is NOTFOUND (§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:

FieldContent
nextThe next tuple: one tuple per address, in reply order
nameOne copy of the canonical name, shared by every tuple
familyAF_INET or AF_INET6, per address
addrThe address
scopeid0, so an IPv6 link-local address is returned without the interface it belongs to

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:

FieldContent
h_nameThe reply's canonical name
h_aliasesEmpty
h_addrtypeThe af asked for
h_length4 or 16
h_addr_listEvery 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). A found reply with no records is NOTFOUND (§7.1).
  • In source b4f7085, a reply whose records include no PTR record — only CNAMEs, for instance — is NOTFOUND with HOST_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:

FieldContent
h_nameThe first PTR record's name
h_aliasesThe remaining PTR records' names
h_addrtypeThe af asked for
h_length4 or 16
h_addr_listThe 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 #

CommandRequest
resolv statusstatus
resolv query <name> [<type>] [--no-cache]resolve
resolv lookup <name>lookup, family any
resolv reverse <address>reverse
resolv flushflush
resolv version, resolv --versionnone: prints resolv <version>

8.1.2.1 The command line #

  • --no-cache is recognised anywhere on the command line and removed before the rest is matched; it affects only query.
  • 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> of query is A when omitted. It is one of A, NS, CNAME, SOA, PTR, MX, TXT, AAAA, SRV or ANY, in any case, or TYPE<n> for any type number from 0 to 65 535.
  • Any other <type> prints resolv: unknown record type "<type>" and exits 1 without contacting resolvd.
  • The <address> of reverse is checked in the same way: anything that is not an IPv4 or IPv6 address prints resolv: "<address>" is not an address and 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 #

StatusMeaning
0found, or status, flush or version succeeded
1resolvd unreachable, a protocol failure, an error reply (including access denied), or a bad type or address argument; a reply of the wrong kind exits 1 with no message
2notfound
3unavailable
64A command line resolv does not recognise

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 #

SignalTells you
resolv statusWhether 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-cacheWhere 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 resolv2 for notfound, 3 for unavailable, 1 when resolvd could not be asked (§8.1)
The door's own errorNXDOMAIN 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:

LineLevelMeaning
native socket: <error>errorStartup failed at the native socket; resolvd exits (§2.2)
stub listener on 127.0.0.53:53: <error>errorStartup failed at port 53; resolvd exits (§2.2)
poll: <error>errorThe loop failed; resolvd exits (§2.2)
could not set a descriptor on <path> (<error>); …errorThe native socket is reachable only by SYSTEM and administrators (§2.4)
removed a stale /run/resolvd/resolv.sockwarnA previous instance left its socket behind and this one removed it; expected on every restart of the service (§2.4)
could not build a descriptor: <error>warnAs could not set a descriptor …, but before writing was attempted (§2.4)
registry watch unavailable (<error>); configuration is read oncewarnConfiguration changes will not be seen (§2.3)
registry watch: <error>; re-armingwarnThe watch failed and is being replaced (§2.3)
Dns FallbackServers: ignoring malformed address "<s>"warn§2.3
Dns ExtraSearchDomains: ignoring malformed domain "<s>"warn§2.3
Dns Hosts: ignoring malformed name "<s>"warn§2.3
Dns Hosts: ignoring malformed address "<s>"warn§2.3
ControlSecurity is not a valid descriptor (<error>); using the defaultwarn§2.3
readiness notify: <error>warnpeinit was not told resolvd is ready (§2.1)
netd not reachable (<error>); retryingwarnFirst failure to reach netd (§3.2)
netd refused the subscription: <message>warnnetd answered subscribe with an error (§3.1)
netd sent something unreadable: <error>warn§3.1
lost the netd channel; reconnectingwarn§3.1
upstream <server>: <error>warnA transaction could not be sent (§4.7)
control: no peer token: <error>warnA native request was denied because its token could not be read (§5.2)
control: reply failed: <error>warnA native reply could not be written (§5.1)
stub udp: <error>, stub tcp accept: <error>, native accept: <error>warnA listener read or accept failed. Accept errors pause that listener for 250 ms before retrying; other descriptors continue to be serviced
listening on /run/resolvd/resolv.sock and 127.0.0.53:53infoStartup reached readiness (§2.2)
subscribed to netdinfo§3.1
netd: <summary>infoA snapshot was applied (§3.3)
configuration changedinfoA registry change was applied (§2.3)
cache flushedinfoA flush request was carried out (§5.3)
/dev/kmsg mirror unavailable (<error>): log lines go to stdout/stderr onlywarnThe kernel log mirror could not be opened, which for resolvd is always; written once, directly after the first line logged (§2.1)

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 connected and scopes (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_failed rising 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:

svctl stop resolvd; rm -rf /run/resolvd; svctl start resolvd

9.3 resolvd Cannot Be Reached

Peios / Peios technical reference / resolvd / Failure Modes

9.3.1 resolvd is not running #

DoorWhat a client sees
NSS shimlocalhost and loopback reverses still answer; everything else is UNAVAIL with NO_RECOVERY (§7.1)
Stub listenerNothing listens on 127.0.0.53; UDP queries go unanswered or are refused by the kernel, TCP connections are refused
resolvExit 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 for tcp,udp:53 is not in the registry — its seed is inert until the image applies it (§2.1) — or something else already holds 127.0.0.53:53. The error text says which.
  • native socket: <error>: /run/resolvd could 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 descriptor line. Ordinary programs cannot reach the socket at all.
  • ControlSecurity holds a valid descriptor that does not grant RESOLVER_QUERY to them (§5.2). They reach the socket and are denied: resolv query prints resolv: 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 #

BoundValueDescribed in
Native request size ceiling (payload)65 536 bytes§5.1
Maximum buffered native frame65 540 bytes§5.1
Native connection read chunk4 096 bytes§5.1
Native request delivery bound5 s§5.1
Native connections still sending a request256§5.1
Queued native replies256§2.5
Queued stub TCP replies256§2.5
Native reply write deadline1 s total§5.1
Stub TCP connections still sending a query256§6.1
Stub TCP query delivery bound10 s§6.1
Stub TCP message buffer65 537 bytes§6.1
Stub TCP read chunk4 096 bytes§6.1
Stub UDP read buffer4 096 bytes§6.1
Stub TCP reply write deadline1 s total§6.2
Classic UDP reply limit, no OPT512 bytes§6.2
EDNS0 payload size advertised, upstream and stub1 232 bytes§4.7, §6.2
Transaction timeout2 s§4.6
Attempts per question (all candidates and transports)3§4.6
Demotion period30 s§4.6
Service descriptor limit16 384§2.1
Listener retry delay after accept error250 ms§2.5, §9.1
Upstream transactions in flight4 096§4.6
Upstream UDP read buffer4 096 bytes§4.7
Cache entries8 192§4.5
Positive lifetime cap86 400 s§4.5
Negative lifetime cap300 s§4.5
CNAME chase depth in lookup16§4.9
netd reconnect backoff0.5 s doubling to 10 s§3.2
netd channel write timeout2 s§3.1
netd frame ceiling65 536 bytes§3.1
netd channel read chunk8 192 bytes§3.1
Registry watch event buffer16 384 bytes§2.3
Kernel hostname read buffer256 bytes§3.3
Unknown-value nesting depth, native requests32§5.1
NSS shim read and write timeout10 s§7.1
DNS label63 bytes§4.1
DNS name on the wire255 bytes§4.1

A.2 Rights #

RightValueDescribed in
RESOLVER_QUERY0x00000001§5.2
RESOLVER_CONTROL0x00000002§5.2
RESOLVER_ALL_ACCESS0x000F0003§5.2

A.3 Paths, addresses and identities #

ItemValueDescribed 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 listener127.0.0.53:53, UDP and TCP§2.4
netd control socket/run/netd/control.sock§3.1
Upstream port53§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 SIDS-1-5-80-3864064249-1823296737-2008945602-1354971773-2894779966§2.1
Fallback scope keyfallback§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 #

KeyValueTypeDefaultDescribed in
Machine\System\Network\DnsFallbackServersREG_MULTI_SZ (or one REG_SZ or REG_EXPAND_SZ)none§2.3
Machine\System\Network\DnsExtraSearchDomainsREG_MULTI_SZ (or one REG_SZ or REG_EXPAND_SZ)none§2.3
Machine\System\Network\DnsControlSecurityREG_BINARYthe compiled default of §5.2§2.3, §5.2
Machine\System\Network\Dns\Hostsany name: a static nameREG_SZ, REG_EXPAND_SZ or REG_MULTI_SZnone§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).

SeedKeyValues
resolvd-service.regMachine\System\Services\resolvdImagePath, Triggers, Identity, Readiness, RuntimeDirectories, RestartPolicy, ErrorControl, DisplayName, Description
resolvd-port.regMachine\System\Network\TcpIp\PortReservationstcp,udp:53

Peios Learn — documentation for the Peios project.

Built with Trail.