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

netd

The network manager as built: how it reads the interface layer and profiles, reconciles links, addresses and routes, runs its DHCPv4, router-discovery and DHCPv6 clients, identifies networks, and publishes readiness and DNS facts.

Single-page view · as markdown

1.1 Overview

Peios / Peios technical reference / netd / Introduction

netd is the Peios network manager. It is the executor of the interface layer of PNP, the Peios Network Policy: it reads which interfaces join which networks, and how they stand there, from Machine\System\Network, and makes the kernel match. It runs as a SYSTEM service started at boot, and it is the only program on a Peios machine that configures links, addresses and routes.

Three decisions shape everything else in this manual.

It is a reconciler, not a command interpreter. netd keeps no list of things it has done. On every input — a kernel link or address event, a registry change, a DHCP or router-discovery packet, a timer, a control request — it re-derives what each interface should look like and applies the difference from what the kernel reports. Nothing happens imperatively in a handler. Two consequences follow directly: a manual change to an interface netd manages lasts until the next pass, and a restarted netd derives its desired state afresh. While its first INIT-REBOOT is pending, it preserves the requested IPv4 address already installed and applicable netd-owned routes (§2.1). Static configuration is desired from the first pass and stays.

It owns addresses wholly and routes only by mark. On an interface netd has joined, every IPv4 address and every non-link-local IPv6 address is netd's: anything else is removed. Routes are netd's only when they carry routing protocol 200, the number netd stamps on every route it adds, so a route another program adds for itself survives. The kernel's own IPv6 link-local address is never touched.

It is the only router-advertisement listener. netd switches the kernel's own RA processing off on every interface before it does anything else, and acts on advertisements itself, so exactly one place decides what an advertisement means. The kernel still makes link-local addresses and answers neighbour solicitations; everything an advertisement could configure — addresses, the default route, DNS, the MTU — is netd's decision, filtered through the interface's profile.

netd builds and judges the interface layer with pnp-core, the same engine the kernel's NTFE uses for the packet layers, so a rule means the same thing in every layer. The kernel never reads Rules\Interface; netd never reads the packet layers. Each executor validates and refuses its own layers on its own.

1.1.1 Where netd sits #

   registry (Machine\System\Network)          peinit
        │ Rules\Interface, Profiles,            ▲ READY=1, LEVEL=<level>
        │ Hostname, Duid, ControlSecurity       │
        ▼                                       │
   ┌─────────────────────── netd ──────────────────────┐
   │ interface layer (pnp-core) → profile per interface │
   │ DHCPv4 · router discovery/SLAAC · DHCPv6           │──▶ rtnetlink (links,
   │ reconciler · network identification                │     addresses, routes)
   └────────────────────────────────────────────────────┘
        │ Interfaces\<id>\Status, Networks\<id>,        │ /run/netd/control.sock
        │ Readiness                                     ▼
        ▼                                         net, resolvd (subscribe)
   registry (written back)

What netd writes back to the registry is read by others: the kernel reads each interface's Status Network to give the packet layers their Network.* facts (PKM §6.5), services and scripts watch Readiness, and resolvd subscribes to netd's DNS snapshots over the control socket (PSPU §6.9).

1.1.2 What netd does not do #

  • Answer names. netd reports what each interface contributes to name resolution; resolvd routes and answers queries.
  • Filter packets. The packet layers are NTFE's. netd's own DHCP and router-discovery traffic passes them only because the shipped baseline policy permits it.
  • Decide who can bind a port. That is the kernel's port reservation table.
  • Associate with wireless networks. A supplicant brings a wireless link up; netd then treats it as any other interface.
  • Hold leases across a reboot. Nothing about a lease is persisted; a rebooted machine asks for the address its network last gave it (§5.7).

1.2 What this manual covers

Peios / Peios technical reference / netd / Introduction

This manual describes netd as built: the netd workspace's netd daemon, its dhcp4, dhcp6 and ndp state-machine crates, the libnetd wire crate, and the net command. It describes version 0.1.7.

1.2.1 What is covered elsewhere #

SubjectWhere
The interface layer's vocabulary — facts, verdicts, the collation laws — and every profile value, as an operator writes themThe network policy reference (~peios/networking/network-policy-reference)
The rule engine itself: ingestion, conditions, collation, refusalKernel TRM chapter 6 (PKM §6.4, §6.5); netd links the same pnp-core crate
The kernel's side of the network context — how Status Network becomes Network.* facts in the packet layersPKM §6.5
The netd → resolver channel's contractPSPU §6.9
What a resolver does with the snapshotsPSPU book 6 and the resolvd manual
The query-channel framing the control socket usesPSPU book 3
How peinit holds a service until a readiness level is publishedThe peinit manual
Every registry value netd reads or writes, one entry eachregman 'Machine\System\Network' on a Peios machine

Where this manual and the network policy reference both mention a profile value, the reference owns the meaning and this manual owns what netd does with it: how the value is parsed, when it takes effect, and how it combines with what a network offers.

1.2.2 Conventions #

Section numbers in this manual are written §n.m. A reference into another book is qualified with the book's short name: PKM §6.5 is the Kernel TRM, PSPU §6.9 the userspace protocols book.

Times are given as netd's monotonic clock sees them. Every timer netd runs — DHCP retransmission, lease boundaries, advertisement lifetimes, solicitation backoff — is measured on CLOCK_MONOTONIC, so setting the wall clock moves none of them.

1.3 Terminology

Peios / Peios technical reference / netd / Introduction

Terms from the policy language — rule, fact, verdict, backstop, priority, exception — mean what the network policy reference says. The terms below are netd's own.

TermMeaning
interfaceA kernel network link netd has seen, keyed by its kernel index while it exists and named in the registry by its interface id.
interface idA UUID-shaped digest of an interface's bus path and MAC (§4.1). Stable across boots and kernel renames.
verdictWhat the interface layer said about an interface: JOIN(profile), IGNORE or DOWN.
joined (managed)An interface whose verdict is JOIN. Loopback is never joined. Only joined interfaces get clients, contribute readiness, and appear in DNS snapshots.
profileA resolved key under Profiles\, inheritance applied: how a joined interface stands on its network.
generationOne build of the interface layer and the profiles from one reading of the registry. netd holds the last good generation; a refused one changes nothing.
passOne run of netd's converge sequence (§2.2): identify networks, judge links, start or stop clients, reconcile, publish.
desired stateWhat one interface should look like: up or down, its MTU, its addresses and its routes (§4.2).
reconcileDiffing desired state against the kernel's and applying the difference (§4.3).
networkWhat is on the other side of a joined interface's carrier, identified from what it has offered (§7.1) and recorded under Networks\<id>.
offerEverything a network has said: a DHCPv4 lease, router advertisements, a stateless DHCPv6 reply.
levelHow far an interface has got towards being usable: absent, link, addressed or routed (§8.2).
scopeWhat one interface contributes to name resolution, as sent to resolvd (§8.3).

2.1 Startup

Peios / Peios technical reference / netd / The Daemon

2.1.1 The service definition #

The netd package ships its service definition as a registry seed, netd-service.reg, applied by an image that names it. The definition:

ValueSettingWhy
ImagePath/usr/sbin/netd
Triggersboot
IdentitySYSTEMrtnetlink changes, the packet socket and a bind to udp/68 each need a privilege only SYSTEM holds
Providesnetworkthe role a dependent names: Requires = ["network:routed"]
Readiness0 (Notify)readiness is READY=1 on the notify socket
RuntimeDirectoriesnetdpeinit creates /run/netd for the control socket
RestartPolicy2 (Always)a stopped network manager is a machine whose leases silently expire
ErrorControl0

A second seed, netd-default-profile.reg, ships the baseline policy: the profile default and the rule wired (§3.1). It is separate so an image can run netd without agreeing that every wired interface joins whatever it is plugged into.

2.1.2 The startup sequence #

netd does these steps in this order:

  1. Opens rtnetlink: one socket for requests and dumps, and a second subscribed to the link, IPv4 and IPv6 address, and IPv4 and IPv6 route multicast groups. Failure is fatal.
  2. Opens the control socket at /run/netd/control.sock, removing a stale one first (§9.1). Failure is fatal.
  3. Opens the udp/68 absorber (§5.6). Failure is a warning: netd runs without it, and a DHCP server's unicast reply can draw an ICMP port-unreachable from the kernel.
  4. Switches the kernel's RA processing off on every interface that exists, and in all and default (§6.1). This happens before any link is brought up.
  5. Reads the configuration (§2.3) and builds a generation from it (§3.2). A generation that fails to build at startup is logged as refused, and netd runs with an empty policy, so the backstop ignores every interface until a good generation is written.
  6. Arms the registry watch on Machine\System\Network. Failure is a warning, and the configuration is then read once, never again.
  7. Dumps the kernel's state (links, addresses and routes, both families). Failure is fatal.
  8. Runs the first pass (§2.2): every interface is judged, every static address applied, every client that is due started.
  9. Sends READY=1 on the notify socket.

Readiness is reported after the first pass, not after any lease arrives. A lease is a network event, not a startup one. A service that needs an address waits on a readiness level (§8.2), not on netd being ready.

A tie between two rules is not checked at startup: the startup build refuses a generation only for the build errors of §3.2. A tie present at startup is judged per interface as a conflict (§3.3), so the interfaces it covers are ignored with a warning rather than the generation being refused.

If Machine\System\Network does not exist when netd starts, the configuration is empty and the watch cannot be armed, so a key created later is never read; a restart of netd picks it up.

2.1.3 Restart #

netd re-derives everything from the registry and the kernel. Its DHCP clients start again, and a client remembers nothing across a restart: each one begins with an INIT-REBOOT request for the address its network last leased (§5.7), and holds no lease until that request is answered.

During an interface's first DHCP start in this process, netd preserves its requested IPv4 address at the prefix already installed in the kernel. It also preserves its netd-marked IPv4 routes while Route.Offered allows them; an explicit Route.Gateway takes precedence and the current profile's metric applies. This provisional state lasts only while INIT-REBOOT is pending. An unchanged ACK replaces it with the confirmed lease without deleting the address or route, so IPv4 readiness stays in place.

A NAK or two unanswered requests end revalidation and discard the provisional state as the client enters fresh discovery (§5.2). Carrier loss or a changed profile clears it too. Later client restarts in the same process do not adopt kernel state again. A requested address that is not already installed is never added merely because it was remembered.

This is not a restored lease: no lease duration, expiry, DNS servers or other offered information is inferred from the installed address. Status still reports no lease until the ACK. DHCP-provided DNS information and IPv6 router-discovery state must be reacquired; this does not promise that every aspect of a daemon restart is invisible. IPv6 addresses and routes from advertisements remain desired only after discovery hears the routers again (§6.1). Static addresses are desired from the first pass and stay.

2.2 The loop

Peios / Peios technical reference / netd / The Daemon

netd is one thread around one poll(2). Every input is a descriptor in the poll set or a timer deadline, and every handler records what it learnt and lets the end of the iteration act on it. Because there is one thread, a handler that blocks holds everything up: no DHCP timer fires and no kernel event is read until it returns.

2.2.1 Inputs #

InputDescriptorWhat it leads to
Kernel eventsthe rtnetlink multicast socketa fresh dump, then a full pass
Registry changesthe watch on Machine\System\Networka reload (§2.3), then a full pass
udp/68 absorberthe absorber socketdrained and discarded
DHCPv4 repliesone packet socket per running clientthe client's actions; a reconcile if the network changed
Router advertisementsone ICMPv6 socket per interface doing router discoverythe engine's actions; a full pass if anything changed
DHCPv6 repliesone UDP socket per running DHCPv6 clientthe client's actions; a reconcile if the DNS facts changed
Control requeststhe listener and accepted client socketsthe request (§9.2); reconcile runs a full pass
Timethe earliest deadline of any clientevery client's timers, each acted on as above

The poll timeout is the earliest deadline any DHCPv4, router-discovery or DHCPv6 client holds, or a pending control request or reply holds (§9.1); with none, netd waits indefinitely. A deadline already acted on is not held again: once an address is deprecated, its preferred lifetime is behind it, and its next deadline is the end of its valid lifetime. So netd sleeps while addresses sit deprecated, which on an IPv6 network with temporary addresses is most of the time.

After a kernel event netd does not fold the event into its model. It dumps the whole state again and always runs a full pass, because a dump taken after an apply can already show a state whose consequences have not been decided yet.

2.2.2 A full pass #

A full pass runs these steps, in this order:

  1. identify networks on joined interfaces with carrier (§7.1);
  2. bring the interface table in line with the kernel's links and judge every non-loopback interface (§3.3), stopping the clients of an interface that lost carrier or whose outcome changed;
  3. start DHCPv4 clients where due (§5.1);
  4. start or stop router discovery and DHCPv6 (§6.1, §6.3);
  5. reconcile every interface (§4.3) and dump the kernel's state again.

It then repeats steps 1 to 5 while the dump differs from the one before the reconcile. The steps run at most four times in one pass, the first run included, even if the dump is still changing after the fourth.

Applying changes alters what the next decisions see: bringing a link up gives it carrier, carrier starts a DHCP client, and a lease adds an address. One pass would leave that chain waiting for a later event that, on a virtual NIC, has already arrived.

After the repeats, the pass applies the hostname (§8.4) and publishes: the machine level to peinit and the registry, every joined interface's Status, and a DNS snapshot to subscribers if it changed (§8.2, §8.3).

2.2.3 Reconcile without a full pass #

A DHCPv4 lease arriving, a lease being lost, link-local fallback, and a DHCPv6 answer each change only what an interface should carry, so they lead to a reconcile of every interface, the hostname, and a publish, without re-judging anything. If the same iteration also needs a full pass, only the full pass runs.

A DHCPv4 lease also marks its interface for re-judgement, because the network on the other side might now be identifiable (§7.1). Any interface so marked makes the iteration run a full pass.

2.3 Reading the registry

Peios / Peios technical reference / netd / The Daemon

netd reads its configuration from Machine\System\Network, whole, every time it reads it.

2.3.1 What is read #

Value or keyUsed for
Hostname (REG_SZ)the machine's name (§8.4). An empty string counts as unset.
ControlSecurity (REG_BINARY)the control object's descriptor (§9.1). An empty value counts as unset.
Duid (REG_SZ)the machine's DHCP unique identifier, as hex (§5.7). A value that is not hex is ignored with a warning, and replaced when netd next decides the DUID (§5.7).
Rules\Interface\ and everything under itthe interface layer (§3.2)
Profiles\ and everything under itthe profiles (§3.1)

Nothing else under the key is configuration to netd. Interfaces\ and Networks\ are read only for the values netd shares with the operator (§5.7, §7.2), at the moment it needs them.

A missing Machine\System\Network means no configuration: no hostname, no rules (so the backstop ignores every interface), no profiles.

2.3.2 Lowering #

Every value in the two trees is lowered to one of four shapes before anything interprets it:

Registry typeLowered to
REG_DWORD (4 bytes)an integer
REG_QWORD (8 bytes)an integer
REG_SZ, REG_EXPAND_SZa string, cut at the first NUL; other if not UTF-8
REG_MULTI_SZa list of strings, empty items dropped, invalid UTF-8 items dropped
anything else, or a DWORD or QWORD of the wrong lengthother

The rule builder refuses an other by name (§3.2), and the profile parser refuses it as a value of the wrong shape (§3.1), so an unusable value is never silently dropped. The default (unnamed) value of a key is skipped in both trees.

Values and subkeys are sorted by name, so two readings of an unchanged tree compare equal.

A tree is read at most 16 levels deep. Below that the read stops with a warning, and whatever lies there is not part of the configuration. Nothing else bounds the depth of a rule tree or a profile tree (§3.2).

2.3.3 A registry change #

The watch is armed on the whole Machine\System\Network subtree. On any event, netd reads the whole configuration again and compares it with what it holds:

  • If anything differs, the control object is rebuilt from ControlSecurity. If the rules or the profiles differ, a new generation is built (§3.2), and taken or refused.
  • Whatever differs, a full pass follows.

netd's own writes to Interfaces\ and Networks\ come back through the same watch. They cause a reload that finds nothing changed, and a pass that plans nothing.

A failed read of the watch's events re-arms the watch.

There is no reload command: a write is noticed within the time it takes the watch to deliver and a pass to run.

3.1 Profiles

Peios / Peios technical reference / netd / The Interface Layer

The profile vocabulary — what each value means to an operator — is the network policy reference's (~peios/networking/network-policy-reference#profiles). This article is what netd does with it.

3.1.1 Resolution #

Every key under Profiles\ is a profile, at every depth, named by its path below Profiles with / between levels. The Profiles key's own values are ignored.

Profiles are looked up case-insensitively: netd keys them by the lower-cased path and keeps the path as written for display and for Status Profile.

A key whose name is empty or contains / or \ refuses the generation.

3.1.1.1 Inheritance #

A profile's effective values are its ancestors' values with its own laid over them, per value name. A value a profile names replaces the inherited one whole — a list replaces a list, never appends to it — and a value it does not name is inherited. Value names compare case-insensitively, so address.offered overrides Address.Offered.

Each profile is parsed from its own effective values. A malformed value in a parent therefore refuses the generation at the parent, before any child is reached.

3.1.1.2 Enabled #

Enabled is a profile's switch, not a value: it is never inherited as one. A profile is enabled when its own Enabled and every ancestor's say so; absent means enabled. A child cannot re-enable itself under a disabled parent. A disabled profile is still resolved and parsed — a malformed value in it still refuses the generation — but a rule that names it abstains (§3.2).

Enabled accepts the boolean shapes below; anything else refuses the generation.

3.1.2 Accepted shapes #

Every value is parsed from its lowered form (§2.3):

ShapeAccepted
booleanan integer (non-zero is true), or a string on, yes, true, 1, off, no, false, 0 in any case
numbera non-negative integer that fits 32 bits, or a string of one, surrounding space trimmed
lista multi-string (empty items dropped), or a single string as a one-item list; an empty string is the empty list
stringa string, or an integer written as its decimal text

A value of any other shape — a list where a boolean belongs, a binary value — refuses the generation with the message profile <path>: <name> has the wrong shape, where <name> is the value's name in lower case.

3.1.3 Values #

ValueShapeWhat netd does with it
Address.Offeredbooleanruns a DHCPv4 client while IPv4 is in the families, and router discovery while IPv6 is
Address.Familieslistitems ipv4 and ipv6, in any case. The list replaces the default of both, so an empty list means neither family. Any other item refuses the generation.
Address.Staticlisteach item address/prefix, either family, prefix at most 32 or 128. An item that does not parse refuses the generation.
Address.LinkLocalbooleanlink-local fallback (§5.5)
Address.Temporarybooleantemporary IPv6 addresses (§6.2)
Address.OnExpirystringDrop or Keep, in any case. Anything else refuses the generation.
Route.Offeredbooleantakes the lease's gateway and classless routes and the routers' default route (§4.2)
Route.Gatewaylisteach item an address of either family. The last IPv4 item is the IPv4 gateway, the last IPv6 item the IPv6 gateway. An item that does not parse refuses the generation.
Route.Metricnumberthe metric of every route netd adds for the interface, and its place in DNS scope order
Mtu.Offeredbooleantakes the lease's MTU, else the routers'
Mtu.Valuenumberthe MTU to set. Below 68 refuses the generation.
Hostname.Announcebooleansends the machine's Hostname in DHCP option 12
Hostname.Offeredbooleanadopts a lease's hostname when Hostname is unset (§8.4)
Dns.Offeredbooleanadds the network's DNS servers and search domains after the profile's own (§8.3)
Dns.Serverslisteach item an address of either family. An item that does not parse refuses the generation.
Dns.Domainslisttaken as written; nothing validates a domain
Dns.Defaultbooleanpresent sets the scope's default-route flag; absent follows the interface's level (§8.3)
Dns.Exclusivebooleansets the scope's exclusive flag

A value name not in this table (case-insensitively) refuses the generation with profile <path>: unknown value <name>, the name as written.

After parsing, any Address.Static entry outside the profile's families is dropped without an error: the family switch wins.

3.1.4 A bare profile #

Every compiled default is false, empty or absent, apart from the families (both) and OnExpiry (Drop). A profile that sets nothing therefore brings its interface up and does nothing else: no clients, no addresses, no routes, no DNS. The friendly behaviour of the shipped default profile is four values in its seed: Address.Offered, Address.LinkLocal, Route.Offered and Dns.Offered, each 1.

3.2 Building a generation

Peios / Peios technical reference / netd / The Interface Layer

A generation is built whenever the rules or the profiles have changed (§2.3), and once at startup.

3.2.1 The order of the build #

  1. Profiles are resolved (§3.1). Any error refuses the generation.
  2. If Rules\Interface does not exist, the generation has no forest. Every interface meets the backstop, IGNORE, and the profiles are kept for nothing to name.
  3. Each subkey of Rules\Interface is a root rule. Its tree is lowered to pnp-core's input and built as the Interface layer with build_forest — the same ingestion, condition parsing and lints the kernel applies to the packet layers (PKM §6.5). Every build error refuses the generation.
  4. Every profile a JOIN names is looked up. A JOIN naming no profile refuses the generation with JOIN(<path>) names no profile.

3.2.2 Lowering a rule #

Integers and strings pass through. A value of any other lowered shape (§2.3) refuses the generation with rule <path>: value <name> has an unsupported type.

A list is passed through item by item, except in Actions. There, an item that is a JOIN naming a profile that exists and is disabled is replaced by NULL before the build, so the rule abstains and the forest's own collation answers without it: its parent, another tree, or the backstop. A JOIN of a disabled profile is not a fault.

A JOIN target is recognised loosely. Whitespace anywhere is ignored, JOIN( matches in any case, \ in the path is read as /, and the path is compared case-insensitively. So join ( Office\London ) names the profile office/london.

3.2.3 What refuses a generation #

Everything pnp-core refuses at the interface layer refuses it here, with netd's wording:

CauseMessage
An unknown fact in a conditionrule <path>: unknown fact <key>
A malformed operatorrule <path>: bad operator in <key>
A malformed patternrule <path>: bad pattern in <key>
Actions not a listrule <path>: Actions is not a list
A malformed actionrule <path>: bad action (…)
An action the interface layer does not speak (PASS, DROP, TAG, …)rule <path>: an action the interface layer does not speak
A key that does not exist at the interface layerrule <path>: <key> does not exist at the interface layer
Present on a fact never present at this layerrule <path>: <key> on a fact that never exists at this layer
Priority not an integerrule <path>: Priority is not an integer
Enabled not 0 or 1rule <path>: Enabled is not 0 or 1
A malformed rule namerule <path>: bad name
A JOIN naming no profileJOIN(<path>) names no profile
A malformed profilethe profile parser's message (§3.1)

The depth of a rule tree refuses nothing. It is bounded only by netd's registry read, which takes rules to 16 levels below Rules\Interface and ignores anything deeper (§2.3).

A condition on a fact that exists in the vocabulary but can never be present at the interface layer, such as DstPort, is not refused. It is a lint: logged as a warning, rule <path>: <key> can never hold at the interface layer, and the generation is taken.

3.2.4 A tie refuses at runtime #

A generation built while netd is running is judged against every non-loopback interface before it is taken. If any interface meets a conflict (two rules tied on priority naming different profiles, §3.3), the generation is refused with rules <a> vs <b> tie on interface <name>.

3.2.5 Taken or refused #

A taken generation replaces the old one whole, clears any recorded refusal, and is logged with its counts of rule trees and profiles. The pass that follows judges every interface against it.

A refused generation changes nothing. The last good generation stays in force, netd logs interface layer refused: <why>; the last good generation stands at error level, and records the reason, which status reports as refusal and net status prints as a policy REFUSED line. The refusal stays recorded until a generation builds.

The interface layer's refusal is netd's alone. The kernel reads the packet layers from the same registry state and refuses or takes them on its own (PKM §6.5): a dangling JOIN never stalls the firewall, and a bad Flow rule never stops an interface joining.

3.3 Judging an interface

Peios / Peios technical reference / netd / The Interface Layer

3.3.1 The facts #

An interface is judged on a snapshot carrying only interface and network facts:

FactValue
Interfacethe kernel's name for the link
Interface.Kindwired, wireless, loopback or other (§4.1)
Interface.Idthe interface id (§4.1)
Interface.Macthe hardware address, when the link has a 6-byte one
Interface.Paththe bus path; absent when sysfs gives none
Interface.Driverthe driver; absent when sysfs gives none
Network.Idthe id of the network identified on the link (§7.1); absent until one is
Network.Namethe record's Name; absent when unset or empty
Network.Trustthe record's Trust; absent when unset or empty
Network.Kindthe interface's kind, once a network is identified; absent before

A fact the interface lacks is absent, which makes a condition on it false, never an error.

3.3.2 When an interface is judged #

Every non-loopback interface is judged on every full pass (§2.2). Loopback is never judged and never managed: it has no verdict, no record under Interfaces\, and netd never changes it.

3.3.3 The verdict #

pnp-core evaluates the forest and netd reads the result:

  • JOIN(i): the profile the forest's ith JOIN names, looked up case-insensitively. netd stands the interface in that profile.
  • DOWN: the interface is kept administratively down.
  • anything else, including the backstop: IGNORE.

The attribution is the evaluation's: the path of the rule that spoke, or backstop when none did.

3.3.4 Conflicts #

When the evaluation reports a conflict — the highest-priority candidates tie and name different profiles — netd does not choose. The interface's outcome is IGNORE, its attribution is the tied rules' paths, sorted and joined with vs, and its warning is rules <a> vs <b> tie; the interface is ignored. The first time an interface meets a conflict, netd logs a warning.

A conflict reaches an interface in two ways. A generation taken at startup is not checked for ties (§2.1), and an interface that appears after a generation was taken is judged against it with no check. A generation built at runtime that ties on an interface already present is refused instead (§3.2).

3.3.5 A change of verdict #

The outcome compares the verdict and the profile's resolved values. When either differs from the interface's previous outcome, netd logs the new verdict and its rule, and stops the interface's clients:

  • the DHCPv4 client sends a RELEASE if it holds a lease (§5.4) and is discarded, along with its lease and any link-local address;
  • router discovery and DHCPv6 are discarded, and their addresses go at the next reconcile.

The pass then starts whatever clients the new outcome wants. Editing any value of the profile an interface stands in is a change of outcome, so it restarts the interface's clients against the new profile, rather than trusting a lease obtained under the old one.

3.3.6 What each verdict does #

VerdictClientsDesired state (§4.2)
JOIN(profile)started as the profile asksup, with the profile's addresses and routes
DOWNnonedown, carrying no addresses and no netd routes
IGNOREnonenone: netd plans nothing for the interface

An IGNOREd interface is left exactly as it is. If it was joined before, the addresses and routes netd gave it stay where they are, because nothing desires their removal.

4.1 Identity and carrier

Peios / Peios technical reference / netd / Interfaces

4.1.1 Kind #

netd classifies each kernel link once per dump:

LinkInterface.Kind
the loopback flag setloopback
link type Ethernet, with /sys/class/net/<name>/wireless or /sys/class/net/<name>/phy80211 presentwireless
link type Ethernet otherwisewired
any other link typeother

A bridge, a veth or a TAP device has link type Ethernet and is wired.

4.1.2 Bus path and driver #

The bus path comes from sysfs. netd resolves /sys/class/net/<name>/device and walks up from it to the nearest ancestor whose name is a PCI address (dddd:bb:dd.f), and names it pci-<address>. A device with no PCI ancestor is named by the device's own directory name. A link with no device (a virtual device) has no bus path. A virtio NIC sits one level below its PCI function, so it is named by the function.

The driver is the name the device/driver link points at (virtio_net, for example), and empty when there is none.

Both are read once, when netd first sees the link.

4.1.3 The interface id #

The interface id is a SHA-1 digest of the bytes peios-netd-ifid|, then the bus path if there is one, then |, then the six MAC bytes — or, for a link with no 6-byte hardware address, its name. The first 16 bytes of the digest are stamped as a version 5, RFC 4122 variant UUID and written in lower-case hyphenated form.

So the same card in the same slot has the same id on every boot, however the kernel names it, and a renamed link keeps its id. A virtual device has no bus path, so its id follows its MAC alone.

The id is the key of the interface's record under Interfaces\ (§8.1), the Interface.Id fact, the key of the RFC 7217 address derivation (§6.2), and the basis of its DHCP IAID (§5.7).

4.1.4 Appearing and disappearing #

When netd first sees a link, it reads the link's identity, logs interface <name> (<path>) is <id>, and, unless it is the loopback, writes accept_ra = 0 for it (§6.1).

When a link disappears from the kernel's dump, netd drops it and everything it held: its clients, lease, network and judgement. Nothing is sent on the wire. Its record under Interfaces\ stays.

4.1.5 Carrier #

netd counts an interface as having carrier when it is administratively up and the kernel reports IFF_LOWER_UP. When an interface that had carrier loses it, netd logs interface <name>: carrier lost and:

  • stops its DHCPv4 client: a RELEASE is sent if a lease was held, and the lease and any link-local address are forgotten (§5.4);
  • stops router discovery and DHCPv6;
  • forgets the network identified on it (§7.1), so Status Network is cleared and the Network.* facts go absent, for netd's own judgement and the kernel's packet layers alike.

The next reconcile then removes the leased and autoconfigured addresses and the routes that depended on them, because nothing desires them any more. Static addresses stay.

When carrier returns, the pass starts the clients again from scratch (§5.1, §6.1), and the DHCPv4 client asks first for the address the network last gave (§5.7).

4.2 Desired state

Peios / Peios technical reference / netd / Interfaces

On every reconcile netd computes, for each interface, what it should look like. That desired state is a link state, an MTU, a set of addresses and a set of routes. It is derived from the interface's verdict, its profile, and its clients' current state. During the first INIT-REBOOT it also includes the provisional IPv4 kernel state described in §2.1; netd keeps no general record of what it applied.

4.2.1 By verdict #

  • Loopback, or an IGNOREd interface: there is no desired state, and netd plans nothing for the interface.
  • DOWN: the link down, with no addresses and no routes.
  • JOIN: the link up, and everything below.

4.2.2 The metric #

Every route netd adds for an interface carries the interface's metric: the profile's Route.Metric when it is set, otherwise 600 for a wireless interface and 100 for anything else.

4.2.3 The MTU #

The profile's Mtu.Value when set. Otherwise, with Mtu.Offered, the lease's MTU (DHCP option 26), else the routers' advertised MTU (§6.1). Otherwise no MTU is desired and the link's MTU is left as it is.

4.2.4 IPv4 addresses #

  • every IPv4 entry of Address.Static, at its prefix;
  • the lease's address at the lease's prefix (§5.3), when a lease is held;
  • while the first INIT-REBOOT is pending, the requested address at its already-installed prefix (§2.1);
  • the link-local address (§5.5) at prefix 16, only when the client has fallen back to one and no lease is held and the profile has no static address of either family.

The order in which they are listed, or written in Address.Static, has no effect. netd adds the addresses an interface lacks in ascending address order (§4.3), and the kernel makes an address primary only when the interface holds no other address in the same subnet, so the first one added in a subnet is its primary. Of two statics in one subnet written 10.0.0.9/24, 10.0.0.3/24, both added in one pass, 10.0.0.3 is the primary.

While a lease that carried a broadcast address (option 28) is held, that broadcast is the one netd sets on every IPv4 address it adds to the interface: the lease's own address, and a static added in that time, whatever its subnet (§4.3).

4.2.5 IPv4 routes #

  • The default route goes via Route.Gateway's IPv4 entry when it is set. Otherwise, with Route.Offered, it goes via the lease's gateway: the first router of option 3, or, when the lease carried classless routes, the gateway of the classless 0.0.0.0/0 route. With neither, there is no IPv4 default route.
  • Classless static routes (option 121) with a prefix above zero are added, each via its gateway, only with Route.Offered.

During the first INIT-REBOOT, existing netd-marked IPv4 routes accompany the provisional address when Route.Offered is enabled. The profile's explicit gateway and current metric still take precedence (§2.1).

Route.Gateway is used as written; netd does not check that the gateway is on a subnet the interface has. A gateway the kernel cannot reach makes the route's add fail on every reconcile, logged each time (§11).

4.2.6 IPv6 addresses #

  1. every IPv6 entry of Address.Static, at its prefix, preferred, with a prefix route;
  2. every address router discovery currently wants (§6.2): stable-privacy and temporary addresses, each deprecated or not, each with or without a prefix route according to the prefix's on-link flag.

4.2.7 IPv6 default route #

Via Route.Gateway's IPv6 entry when it is set. Otherwise, with Route.Offered, via the default router router discovery has chosen (§6.1). With neither, there is no IPv6 default route.

4.2.8 Not desired #

netd never desires the kernel's own IPv6 link-local address, the kernel's prefix routes, or any route without netd's protocol mark. Those are outside what reconciliation compares (§4.3).

4.3 Reconciliation

Peios / Peios technical reference / netd / Interfaces

4.3.1 What netd owns #

On an interface with a desired state:

  • Every IPv4 address, including a 169.254 address, and every IPv6 address except a link-local one is netd's. An address netd does not desire is removed, whoever added it.
  • A route is netd's only if it carries routing protocol 200 (RTPROT_NETD). netd stamps it on every route it adds, and removes no route without it.
  • The kernel's IPv6 link-local addresses (fe80::/10) are never compared, added or removed. Every up interface has one, and neighbour discovery needs it.

Only routes in the main table, of unicast type and with an output interface, are read from the kernel at all.

4.3.2 The plan #

For each interface with a desired state, netd plans these operations, in this order:

  1. Link up, if it is desired up and is down.
  2. MTU, if one is desired, differs from the link's, and is at least 68.
  3. Address deletions: every address netd owns whose address and prefix length together match no desired address.
  4. Address additions: every desired address the kernel does not hold with the same flags, in ascending address order, IPv4 before IPv6.
  5. Route deletions: every protocol-200 route on the interface that is not desired.
  6. Route additions: every desired route the kernel does not hold.
  7. Link down, if it is desired down and is up — last, after its addresses and routes have gone.

Addresses compare by address and prefix length, plus the two flags netd sets: deprecated and no-prefix-route. A tentative flag (duplicate address detection still running) is erased before comparing. A change of flags alone is therefore an addition of the same address, which replaces it (below), never a delete and re-add that would reset standing connections.

An IPv4 address's broadcast address is not compared. An address keeps the broadcast it was added with: a lease whose broadcast changes, or a lease that arrives after a static was added, changes nothing on an address the interface already holds.

Routes compare on everything: destination, prefix length, gateway, metric and protocol. A change of metric or gateway is a deletion and an addition. For IPv6 routes, configured metric zero uses the kernel default of 1024: netd sends and compares that effective value, so an unchanged route converges. IPv4 metric zero and all nonzero metrics are unchanged. The configured Route.Metric still controls DNS scope ordering.

An interface whose desired state the kernel already matches gets an empty plan, and nothing is sent.

4.3.3 Applying #

Operations are applied one at a time, each as an rtnetlink request with an acknowledgement:

OperationRequest
link up or downRTM_SETLINK changing only IFF_UP
MTURTM_SETLINK with IFLA_MTU
address addRTM_NEWADDR with NLM_F_CREATE and NLM_F_REPLACE
address deleteRTM_DELADDR
IPv4 route addRTM_NEWROUTE with NLM_F_CREATE and NLM_F_APPEND, main table, protocol 200
IPv6 route addRTM_NEWROUTE with NLM_F_CREATE and NLM_F_EXCL, main table, protocol 200
route deleteRTM_DELROUTE

IPv4 route additions append a distinct gateway/interface at the configured metric. Two joined interfaces may therefore both hold default routes at the same metric; adding one preserves the other, and withdrawing one removes only its own route. Exact duplicates remain EEXIST. Equal-metric route selection follows the kernel's routing behavior; set distinct metrics when one interface should have a defined preference.

How addresses are phrased:

  • An IPv4 address carries IFA_LOCAL and IFA_ADDRESS set to itself, link scope for 169.254/16 and universe scope otherwise. Below a /31 it also carries a broadcast address: the broadcast of the lease the interface holds, if that lease gave one, otherwise the address's own subnet's all-ones address. The lease's broadcast goes on every IPv4 address added while the lease is held, statics included.
  • An IPv6 address carries universe scope and, when its prefix is not on-link, IFA_F_NOPREFIXROUTE. Its lifetimes are netd's to manage, so the kernel is told a valid lifetime of forever and a preferred lifetime of forever, or of zero when the address is deprecated. Source address selection then stops choosing a deprecated address, and connections already using it keep it.

A route with a gateway has universe scope, and one without has link scope. A route carries a metric only when it is above zero.

A failed operation is logged as reconcile: <operation> failed: <error> and the rest of the plan carries on, so one bad route never stops an address landing. EEXIST on an add and ENOENT or ESRCH on a delete mean the kernel got there first, and are not logged.

After every interface's plan is applied, netd dumps the kernel's state again, so readiness and the status reply describe what actually landed.

5.1 The client

Peios / Peios technical reference / netd / Dhcpv4

netd's DHCPv4 client is the dhcp4 crate: the RFC 2131 state machine as a pure, clock-injected type, driven by netd's loop and sockets.

5.1.1 Starting and stopping #

On every full pass, for each joined interface:

  • the interface wants a client when its profile has Address.Offered and IPv4 in its families;
  • it can run one when it is up with carrier.

A running client is stopped (§5.4) in the step of the pass that brings the interface table in line with the kernel's links, before clients are started (§2.2):

  • when the link loses carrier or goes down, logged as interface <name>: carrier lost;
  • when the interface's outcome changes, logged as the new verdict (§3.3). Any edit to the profile the interface stands in is a change of outcome, so it stops the client even when the edited profile still wants one, and the same pass starts a new one.

Only a change of outcome changes what an interface wants, so a client its interface no longer wants is always stopped the second way. A link that leaves the kernel takes its client with it (§5.4).

An interface that wants and can run a client but has none starts one, logging interface <name>: dhcp starting, provided:

  • the link has a 6-byte hardware address — otherwise no client is started, silently;
  • its packet socket opens (§5.6) — otherwise netd logs no packet socket and tries again on the next pass.

A new client starts with no lease. It is configured with:

SettingValue
chaddrthe interface's MAC
client identifier (option 61)the interface's ClientId (§5.7)
hostname (option 12)the machine's Hostname, only when the profile has Hostname.Announce and Hostname is set
randomness8 bytes of /dev/urandom XORed with the interface index, seeding transaction ids and jitter
a previous addressthe RequestedAddress of the interface's network, if one is known; otherwise that of the network named by its Status LastNetwork (§5.7)

With a previous address the client begins in Rebooting (RFC 2131 §3.2, INIT-REBOOT). Without one it begins in Selecting.

5.1.2 States #

StateEnteredLeaves on
Selectingstart without a previous address; a NAK while requesting or rebooting; a request that went stale; a reboot nobody confirmed; a lost leasean acceptable OFFER
Requestingan OFFER in Selectingan ACK (Bound), a NAK (Selecting), four unanswered transmissions (Selecting)
Rebootingstart with a previous addressan ACK (Bound), a NAK (Selecting), two unanswered transmissions (Selecting)
Boundan ACKT1 (Renewing)
RenewingT1; or an operator renew while bound, renewing or rebindingan ACK (Bound), a NAK (lease lost, Selecting), T2 (Rebinding)
RebindingT2an ACK (Bound), a NAK or the lease's end (lease lost, Selecting)

The status reply shows a client's state only inside its lease, which is present only while the client holds a lease (§9.2), and net status prints it on the lease line. So the state shown is always bound, renewing or rebinding. A client in Selecting, Requesting or Rebooting holds no lease, and neither shows a state for it. The init state exists only between a stop and the client being discarded.

A new transaction id is drawn on entering Selecting, Rebooting, Renewing and Rebinding. Requesting keeps the id of the Selecting exchange it came from.

5.1.3 What every message carries #

Every message is a BOOTREQUEST with hardware type 1 and length 6, the current transaction id, chaddr, and secs set to the whole seconds since the client started, capped at 65535. Every message except a RELEASE carries, in this order:

OptionContent
53the message type
61the client identifier
57maximum message size 1500
55the parameter request list: 1, 3, 6, 12, 15, 26, 28, 42, 51, 58, 59, 119, 121
12the hostname, when configured

The encoded message ends with option 255 and is padded with zeros to 300 bytes. An option longer than 255 bytes would be cut to 255.

5.1.4 The messages #

MessageWhenBroadcast flagciaddrOption 50Option 54Sent
DISCOVERSelectingset0the address of a lease that expired, until a NAK or a reboot timeout forgets it—broadcast
REQUESTRequestingset0the offered addressthe offer'sbroadcast
REQUESTRebootingset0the previous address—broadcast
REQUESTRenewingclearthe lease address——unicast to the server
REQUESTRebindingclearthe lease address——broadcast
RELEASEa stop while holding a leaseclearthe lease address—the serverunicast to the server

A RELEASE carries only options 53, 54 and 61.

"Broadcast" and "unicast" are the paths of §5.6: a broadcast leaves the packet socket from 0.0.0.0 to 255.255.255.255, rebinding included; a unicast leaves an ordinary UDP socket bound to the lease address.

5.2 Timers

Peios / Peios technical reference / netd / Dhcpv4

Every timer is on netd's monotonic clock.

5.2.1 Retransmission while acquiring #

In Selecting, Requesting and Rebooting each transmission schedules the next one after the current backoff plus a jitter drawn uniformly from −1 s to +1 s, then doubles the backoff, up to 64 s. The backoff starts at 4 s on entering each of the three states. So transmissions follow at roughly 4, 8, 16, 32, 64, 64, … seconds.

StateTransmissions before it gives upThen
Selectingnever gives up—
Requestingfour REQUESTs (about 0, 4, 12 and 28 s)at the next timer, about 60 s, Selecting with a new DISCOVER
Rebootingtwo REQUESTs (about 0 and 4 s)at the next timer, about 12 s, Selecting with a new DISCOVER and no option 50

5.2.2 "Nobody answered" #

On the third retransmission of a DISCOVER — the fourth DISCOVER, about 28 s after the first — the client reports that no offer came. netd then records the interface warning asked for an address; nobody answered and, when the profile wants it, falls back to a link-local address (§5.5). The client keeps discovering. The warning survives full reconciliation passes, including the pass triggered by adding the link-local fallback. It clears when a lease binds or that DHCP client stops (for example, on carrier loss or a profile change). A rule-conflict warning takes display precedence; rejudging an unchanged client does not erase its discovery warning.

In source 23af1f6, the report is made at most once between client starts: the flag is reset only when a client is started. A client that reported it, then got a lease, then lost it, does not report it again, so it does not fall back to a link-local address a second time.

The proposed source correction resets the flag after a valid ACK produces a lease. If that lease is later lost, unanswered discovery can report again at the same fourth-DISCOVER threshold, allowing the profile's link-local fallback to be requested again. An uninterrupted unanswered acquisition still reports only once: a malformed ACK, a NAK while Requesting, or a Requesting timeout does not rearm it. This describes the proposed source behavior, not a released package or an installed image; check the installed build before relying on repeat fallback.

5.2.3 Lease boundaries #

A lease's T1, T2 and length are measured from the moment its ACK arrived, and are fixed when the lease is read (§5.3).

  • At T1 a Bound client enters Renewing and sends a unicast REQUEST.
  • At T2 a Renewing client enters Rebinding and sends a broadcast REQUEST.
  • At the lease's end a Rebinding client loses the lease (§5.4) and enters Selecting.

A boundary is acted on before any retransmission due at the same moment.

5.2.4 Retransmission while renewing and rebinding #

Each REQUEST in Renewing or Rebinding schedules the next for half the time remaining to the state's next boundary (T2 while renewing, the lease's end while rebinding), but never sooner than 60 s from now. With a short lease the 60 s floor means a state's boundary usually arrives before its second REQUEST: a 20 s lease renews once at 10 s and rebinds once at 17 s, then lapses at 20 s.

An ACK in either state starts the lease's clock again from that moment.

5.3 Replies and leases

Peios / Peios technical reference / netd / Dhcpv4

5.3.1 Which replies are looked at #

A received message is ignored unless all of these hold:

  • it is a BOOTREPLY;
  • its transaction id is the client's current one;
  • its chaddr is the interface's MAC;
  • it carries a message type (option 53) the client knows.

5.3.2 The server lock #

Once a server has been chosen — the server of the lease held, or else the server identifier of the offer being requested — an ACK or NAK is accepted only if its option 54 names that server. A stranger who guesses the transaction id cannot take a lease away or substitute one.

In Rebooting, with no lease and no offer, no server has been chosen, so an ACK or NAK from any server is accepted.

5.3.3 What each state accepts #

StateMessageResult
SelectingOFFER with a non-zero yiaddr and a server identifiertaken: Requesting. The first acceptable OFFER wins; there is no comparison between offers.
SelectingOFFER without eitherignored
Requesting, Rebooting, Renewing, RebindingACK that reads as a lease (below)Bound, with the lease
Requesting, Rebooting, Renewing, RebindingACK that does not read as a leaseignored; retransmission continues
Requesting, RebootingNAKSelecting; the previous address and the offer are forgotten
Renewing, RebindingNAKthe lease is lost (§5.4); Selecting
any other combinationignored

An ACK in Renewing or Rebinding that grants a different address replaces the lease. The next reconcile removes the old address and adds the new.

5.3.4 Reading an ACK #

An ACK is a lease only when it has:

  • a non-zero yiaddr;
  • a server identifier (option 54);
  • a lease time (option 51) of exactly 4 bytes, and at least 4 seconds.

A lease time under 4 s is refused: it cannot hold T1 < T2 < lease, and a server asking for it wants the client to spin.

5.3.4.1 The prefix #

From the subnet mask (option 1) when it is a non-zero run of contiguous ones. Otherwise, absent or malformed, from the address's class: /8 for a first octet up to 127, /16 up to 191, /24 above.

5.3.4.2 Routes #

Classless static routes (option 121, RFC 3442) are read as a sequence of prefix length, the significant destination octets, and a 4-byte gateway. In source 23af1f6, a prefix length above 32, or an entry running past the end of the option, makes the whole ACK fail to read as a lease: it is ignored like any other unusable ACK.

The proposed source correction instead discards the entire malformed optional route list, including any valid entries before the malformed one, and keeps an otherwise-valid lease. The existing router-option (3) fallback then applies; without that option, the lease has no routes or gateway. This recovery does not relax whole-message or option framing, the mandatory lease requirements above, or the client and server-identity checks. It describes the proposed source policy, not a released package or installed image, and does not assert an RFC requirement to recover malformed route lists.

When the decoded classless route list is nonempty, the router option (3) is ignored. The lease's gateway is the first router of option 3, or, when classless routes are present, the gateway of the classless route with prefix 0.

5.3.4.3 T1 and T2 #

  • T1 is option 58 when it is above 0 and below the lease time minus 2. Otherwise it is half the lease time.
  • T2 is option 59 when it is above T1 and below the lease time. Otherwise it is seven-eighths of the lease time, rounded down, raised to at least T1 + 1 and capped at the lease time minus 1.

So T1 < T2 < the lease time always holds, whatever the server sent.

5.3.4.4 Other options kept #

OptionKept as
6DNS servers, every 4-byte entry
15the domain name: the bytes up to the first NUL; dropped if not UTF-8
119the domain search list, RFC 1035 names with compression pointers followed, at most 16 jumps per name
12the hostname offered: the bytes up to the first NUL; dropped if not UTF-8
26the MTU, when it is at least 68
28the broadcast address
42NTP servers, every 4-byte entry

Where an option appears twice, its first occurrence is used. Nothing else in the ACK is kept.

How each of these is used — and whether, by the profile — is in §4.2 (addresses, routes, MTU), §8.3 (DNS and NTP) and §8.4 (the hostname).

5.3.5 The record of a lease #

A lease is logged as interface <name>: lease <address>/<prefix> from <server> for <seconds>s. Binding clears the interface's link-local address and its warning, and marks the interface for re-judgement, because the network might now be identifiable (§7.1).

5.4 Losing a lease

Peios / Peios technical reference / netd / Dhcpv4

5.4.1 Lost by the network #

A lease is lost when a Rebinding client reaches the lease's end (§5.2), or when a NAK arrives while renewing or rebinding (§5.3). The client forgets the lease and enters Selecting, and netd acts on the interface's Address.OnExpiry:

  • Drop (the default): netd logs interface <name>: lease lost and forgets the lease. The next reconcile removes the address and the routes that came with it.
  • Keep: netd logs interface <name>: lease lost; keeping the address (Address.OnExpiry = Keep) at warning level and goes on desiring the lost lease — its address, gateway and routes — while the client looks for a new one. The kept lease is replaced the moment a new one binds.

With Keep, the status reply's lease describes the client, not the kept lease: it disappears with the client's lease, while the address stays on the interface.

Keep lasts while the client lasts. A stop (below) forgets the kept lease as it would any other.

5.4.2 Stopped by netd #

netd stops a client when:

  • the interface's outcome changes: another verdict, another profile, or any value of its profile edited (§3.3);
  • the interface loses carrier (§4.1);
  • the profile no longer wants a client (§5.1).

A client in Bound, Renewing or Rebinding sends a RELEASE to its server first: unicast from the lease address (§5.1, §5.6). After a carrier loss that RELEASE goes out on a link with no carrier, so the server never sees it. The client is then discarded, and the interface's lease and link-local address are forgotten, so the next reconcile removes both addresses.

A link that disappears from the kernel takes its client with it and sends nothing.

5.4.3 The operator's renew #

The control request renew (§9.2), and so net renew <interface>, moves a client in Bound, Renewing or Rebinding into Renewing at once: a new transaction id and a unicast REQUEST to the lease's server. The lease is not released first, and its address stays on the interface throughout. A client that holds no lease is left as it is, and the request still answers ok.

5.5 Link-local fallback

Peios / Peios technical reference / netd / Dhcpv4

With Address.LinkLocal in the interface's profile, an interface whose DHCPv4 discovery goes unanswered gets an IPv4 link-local address (RFC 3927), so two machines on a cable with no server can still reach each other.

5.5.1 When #

At the client's "nobody answered" report (§5.2), about 28 s after discovery began, netd logs `interface : no DHCP offer; link-local

` and the interface desires the address. Discovery carries on throughout.

The address is desired at prefix 16 only while no lease is held and the profile has no Address.Static entry of either family (§4.2). A profile with a static address therefore never shows one, even after the report.

5.5.2 Which address #

A pure function of the interface, so it is the same every time:

  • the seed is the last four bytes of the MAC, read as a big-endian 32-bit number, or, for a link with no MAC, the interface index times 2654435761, wrapping at 32 bits;
  • the host part is 256 plus the seed modulo 64768;
  • the address is 169.254.<host / 256>.<host % 256>.

That lands in 169.254.1.0 to 169.254.254.255, the range RFC 3927 allows for self-assignment.

netd does not probe for a conflicting holder or announce the address (RFC 3927 §2.2 and §2.4). Two machines whose MACs share their last four bytes choose the same address.

5.5.3 When it goes #

The link-local address is forgotten when a lease binds, and when the client is stopped (§5.4). The next reconcile removes it, because netd owns every IPv4 address on a joined interface (§4.3).

5.6 Sockets

Peios / Peios technical reference / netd / Dhcpv4

Before an interface has an address, nothing but a packet socket can send from 0.0.0.0. So every DHCPv4 broadcast leaves through AF_PACKET, with an IPv4 and UDP header netd builds itself, and every reply is read from the same socket.

5.6.1 The packet socket #

One per running client: AF_PACKET, SOCK_DGRAM, protocol ETH_P_IP, bound to the interface. In the proposed corrected source, a classic BPF filter passes only an IPv4 UDP datagram that is not a fragment and is addressed to port 68. It refuses both a non-zero fragment offset and the more-fragments flag; the don't-fragment flag is allowed. Everything read is checked again for an IPv4 header and UDP port 68, then decoded as a DHCP message.

In versions 0.1.7 (described by this manual) and 0.1.8, the filter tests only the fragment offset: a later fragment is refused but a first fragment passes and is decoded as if it were the whole datagram. The proposed source correction above is not present in those releases.

The socket sees every frame on the interface that passes the filter, so it receives both broadcast replies and replies unicast to the lease address.

5.6.2 The broadcast path #

DISCOVER, REQUEST in Requesting and Rebooting, and REQUEST in Rebinding go out on the packet socket to Ethernet ff:ff:ff:ff:ff:ff, as one IPv4 datagram:

FieldValue
source0.0.0.0
destination255.255.255.255
TOS0x10
identification0
flagsdon't-fragment
TTL64
UDP ports68 → 67
UDP checksum0 (none, which IPv4 permits)

5.6.3 The unicast path #

REQUEST in Renewing, and RELEASE, go out on a transient AF_INET UDP socket with SO_REUSEADDR and SO_BINDTODEVICE set to the interface, bound to the lease address on port 68, sent to the server on port 67, and closed. The kernel resolves the server's link-layer address. The reply is read from the packet socket.

Binding port 68 meets the kernel's port reservation for ports 1 to 1023, which the shipped seed grants to SYSTEM, netd's identity.

5.6.4 The absorber #

At startup netd binds one more UDP socket, to 0.0.0.0:68 with SO_REUSEADDR, and discards whatever arrives on it. Its only purpose is that a server's unicast reply finds a bound port, so the kernel does not answer it with ICMP port-unreachable. The packet sockets see the same datagrams. If it cannot be bound, netd runs without it (§2.1).

5.7 Identity and memory

Peios / Peios technical reference / netd / Dhcpv4

A DHCP server recognises a client by its client identifier, and a reservation on the server matches on it. netd keeps the identifier stable across boots and NIC swaps in the same slot, and remembers per network what it was given.

5.7.1 The DUID #

The machine's DHCP unique identifier, shared by DHCPv4 and DHCPv6 (RFC 4361), is decided once per netd process, the first time a client needs it:

  1. the registry's Machine\System\Network Duid, as hex, when it is set and parses;
  2. otherwise the file /var/state/netd/duid, when it holds at least 4 bytes;
  3. otherwise a new DUID-LL: 00 03 00 01 followed by the MAC of the interface whose client is starting. It is written to /var/state/netd/duid; a failure to write is logged and changes nothing else.

When the registry's Duid is absent, empty or not hex, the result of 2 or 3 is written to it, as colon-separated lower-case hex, replacing a value that was not hex.

Hex in Duid (and ClientId) can be written with :, - or spaces between digits, or none. A value with any other character, an odd number of digits, or no digits is not hex.

5.7.2 The client identifier #

Per interface, Interfaces\<id> ClientId:

  • when the value is set and is non-empty hex, it is sent as written;
  • when it is absent, the identifier is generated and written back: ff, then a 4-byte IAID, then the DUID. The IAID is the interface id's text folded into 4 bytes by XOR, byte i of the text into byte i mod 4;
  • when it is set but not hex, netd logs <id>: ClientId "<value>" is not hex; regenerating, where <id> is the interface id, generates one, and overwrites the value.

ClientId is a value both netd and the operator write. Writing it is how an operator makes netd present the identifier a server reservation expects. It is read when a client starts, so it takes effect at the next start (a carrier loss, a profile edit, a restart of netd).

5.7.3 The address a network gave #

While an interface holds a lease on an identified network (§7.1), netd writes the lease address to that network's Networks\<id> RequestedAddress whenever it differs from what is there.

When a client starts, it takes as its previous address (§5.1):

  1. the RequestedAddress of the network currently identified on the interface, when there is one; otherwise
  2. the RequestedAddress of the network the interface's Status LastNetwork names (§8.1).

With one, the client opens with an INIT-REBOOT REQUEST for it. A server that agrees answers with an ACK and the machine keeps the address in one round trip. One that refuses answers with a NAK and the client discovers afresh. Silence for two transmissions does the same (§5.2).

RequestedAddress is also the operator's: writing it is a soft reservation, an address the client asks for first and the server can refuse.

Nothing about a lease itself — its timers, its server, its options — is persisted. A machine that reboots asks for its address again.

6.1 Router discovery

Peios / Peios technical reference / netd / Ipv6

6.1.1 The kernel's RA handling is off #

netd writes 0 to /proc/sys/net/ipv6/conf/<name>/accept_ra for all, for default, and for every interface listed there, at startup before any link is brought up (§2.1). It writes it again for each non-loopback interface the first time it sees it. A missing file — a kernel without IPv6 — is a warning only for all.

The kernel still creates link-local addresses and answers neighbour solicitations. That is the protocol, not policy. Everything an advertisement could configure is netd's to decide.

6.1.2 When router discovery runs #

On every full pass, for each joined interface:

  • it wants router discovery when its profile has Address.Offered and IPv6 in its families;
  • it can run it when it is up with carrier and the kernel's link-local address on it has finished duplicate address detection. A tentative address cannot source a solicitation.

Running discovery that is no longer wanted or possible is stopped (ipv6 stopping): the engine and any DHCPv6 client are discarded, and the next reconcile removes their addresses and route. Nothing is sent on the wire, because neither protocol has a goodbye. An interface that wants and can run it starts it (soliciting routers), provided its ICMPv6 socket opens — otherwise netd logs no icmpv6 socket and tries again on the next pass.

6.1.3 The socket #

One raw ICMPv6 socket per interface, bound to it with SO_BINDTODEVICE, filtered to receive only type 134 (router advertisement), sending with a multicast hop limit of 255, and asking for each received packet's hop limit. The kernel computes and checks ICMPv6 checksums.

6.1.4 Solicitation #

A router solicitation is type 133, code 0, with a source link-layer address option carrying the interface's MAC, sent to ff02::2 with hop limit 255.

The first is sent as soon as discovery starts. The interval is 4 s for the next two, then doubles before each later one, up to 3600 s. Each interval carries a jitter of ±10%. So solicitations go at about 0, 4, 8, 16, 32, 64, … s.

Soliciting stops when an advertisement arrives with a router lifetime above zero. It begins again, from the first interval, as soon as every default router has expired.

6.1.5 Which advertisements count #

An advertisement is dropped unless it arrived with hop limit 255 and from a link-local source (RFC 4861 §6.1.2). A lower hop limit has crossed a router, and a non-link-local source is not a router on this link.

It is then decoded. The whole advertisement is refused when:

  • it is shorter than 16 bytes, or its type is not 134 or its code not 0;
  • an option has length zero, or runs past the end of the message.

Within an accepted advertisement, an option that does not parse is skipped and the rest stand:

OptionAccepted whenLimits
Prefix information (3)32 bytes long; prefix length ≤ 128; the prefix neither multicast nor link-localthe first 16 per advertisement
MTU (5)8 bytes long
RDNSS (25)at least 24 bytes, and a whole number of addresses16 servers per option; unspecified, multicast and loopback addresses dropped
DNSSL (31)at least 16 bytes16 domains per option; names uncompressed, lower-cased, labels of printable ASCII, 253 characters at most; a malformed name ends the list

Any other option is ignored. So are the advertisement's hop limit, reachable time and retransmission timer.

6.1.6 What an advertisement does #

  • Router lifetime above zero: the source becomes, or stays, a default router until that many seconds from now. Zero: the source is removed as a default router. Its prefixes, servers and domains stay.
  • M or O flag: stateless DHCPv6 is wanted (§6.3). Once set it stays set until discovery restarts; a later advertisement without the flags does not withdraw it.
  • MTU option: remembered as the link's advertised MTU. It is used only with Mtu.Offered, only when no lease gave one, and only when it is at least 68 (§4.2, §4.3).
  • Prefixes: §6.2. RDNSS and DNSSL: §6.3.

Whenever what the interface should carry changes — an address, its deprecation, the default router, the servers, the domains, the MTU, the DHCPv6 flag — netd runs a full pass. That includes a change caused by time alone: a lifetime running out is a change.

6.1.7 The default router #

Of the routers whose lifetime has not run out, netd uses the one with the numerically lowest link-local address, so the choice is stable while the set is. Router preference (RFC 4191) is not read.

6.2 Addresses

Peios / Peios technical reference / netd / Ipv6

6.2.1 Which prefixes form addresses #

A prefix information option forms addresses only when its A flag is set, its prefix length is exactly 64, and its preferred lifetime does not exceed its valid lifetime. Any other prefix option is ignored whole. A prefix with only the L flag gets no address, and no on-link route, because the kernel's RA handling is off.

A prefix seen for the first time is taken only with a valid lifetime above zero.

6.2.2 The stable address #

Each accepted prefix gets one stable address (RFC 7217): the prefix's first 64 bits, followed by the first 8 bytes of a SHA-1 digest of:

  1. the bytes peios-ndp-stable-iid|;
  2. the machine's 32-byte secret;
  3. the prefix's first 8 bytes;
  4. the interface id's text;
  5. a one-byte counter, starting at 0.

When the resulting interface identifier is reserved (RFC 5453: all zeros, 0200:5eff:fe00::/40, or fdff:ffff:ffff:ff80 to fdff:ffff:ffff:ffff), the counter is incremented and the digest taken again, up to 8 times.

So the same machine on the same network has the same address on every boot, a different network sees an unrelated address, and no MAC address ever appears in it. The address is a function of the interface id, so a kernel rename does not change it either.

6.2.2.1 The secret #

The 32-byte secret is read from /var/state/netd/secret the first time an interface starts router discovery. When the file does not hold exactly 32 bytes, a new secret is drawn from /dev/urandom and written there; a failure to write is logged and the new secret used for the rest of the process's life. Losing the secret renumbers the machine, and does nothing else.

6.2.3 Lifetimes #

A lifetime of 0xffffffff is forever. Others count from the moment the advertisement arrived.

When an advertisement updates a prefix already held:

  • the on-link flag and the preferred lifetime are taken as advertised;
  • the valid lifetime uses the two-hour rule from RFC 4862 §5.5.3(e), with the infinite-lifetime exception described below:
Advertised valid lifetimeRemaining valid lifetimeResult
above two hours, or above what remainsany finitethe advertised lifetime
two hours or less, not above what remainstwo hours or lessunchanged
two hours or less, not above what remainsabove two hourstwo hours from now
above two hoursforeverthe advertised lifetime
two hours or lessforeverforever

The table records the 0.1.7 behaviour this manual describes; it is also present in netd 0.1.8 source. The final row is a known deviation: infinity is above two hours, so RFC 4862 §5.5.3(e)(3) requires two hours from the new advertisement, including when the advertised lifetime is zero. A source build containing the proposed source correction uses that deadline; it still accepts advertised lifetimes above two hours, including infinity, and takes the preferred lifetime as advertised. This source correction does not establish that an installed package or image has changed.

When a prefix's preferred lifetime runs out, its addresses are deprecated: kept on the interface for standing connections, with a preferred lifetime of zero so that nothing new chooses them (§4.3). When its valid lifetime runs out, the prefix and its addresses are removed.

6.2.4 Flags on the address #

An address in a prefix advertised on-link (L set) is added normally, so the kernel adds the prefix route. One advertised off-link is added with IFA_F_NOPREFIXROUTE, so the kernel adds no prefix route for it.

6.2.5 Temporary addresses #

With Address.Temporary in the profile, each prefix also carries temporary addresses (RFC 8981):

  • one is formed with the prefix when the prefix arrives with a preferred lifetime above zero;
  • its interface identifier is random, avoiding the reserved identifiers, the stable address and the prefix's other temporaries;
  • its preferred lifetime is one day minus a random 0 to 599 s, so a rack of machines does not regenerate together, and its valid lifetime is two days. Each is cut to the prefix's own lifetimes.

When the newest temporary is no longer preferred while the prefix still is, a new one is formed. The old one stays, deprecated, until its valid lifetime ends. A prefix keeps at most four temporaries, dropping the oldest beyond that.

A temporary is deprecated when its own preferred lifetime or its prefix's has run out, and removed when its own valid lifetime or its prefix's has.

Address.Temporary is read when router discovery starts. Changing it is a profile edit, which restarts discovery (§3.3).

6.3 DNS and DHCPv6

Peios / Peios technical reference / netd / Ipv6

6.3.1 RDNSS and DNSSL #

Each server of an RDNSS option, and each domain of a DNSSL option, is held for the option's lifetime from the moment the advertisement arrived. 0xffffffff is forever, and a lifetime of zero removes that server or domain at once. A server or domain advertised again has its lifetime replaced.

6.3.2 Stateless DHCPv6 #

Peios gets IPv6 addresses by SLAAC alone. DHCPv6 is used for configuration only: an INFORMATION-REQUEST (RFC 8415 §18.2.6) for DNS servers and search domains. The stateful address exchange is not implemented, and an advertisement's M flag is treated as O.

6.3.2.1 When it runs #

A DHCPv6 client starts on an interface once its router-discovery engine has seen an advertisement with M or O (§6.1), provided the interface has a non-tentative link-local address and a MAC, and its socket opens. netd logs interface <name>: dhcpv6 information request. It stops with router discovery.

6.3.2.2 The socket #

UDP, bound with SO_BINDTODEVICE to the interface and to its link-local address on port 546, so two interfaces' clients never share a port. Messages go to ff02::1:2 port 547.

6.3.2.3 The request #

An INFORMATION-REQUEST (type 11) with a random 3-byte transaction id and these options:

OptionContent
1, client identifierthe machine's DUID (§5.7)
8, elapsed timehundredths of a second since this exchange began, capped at 65535
6, option request23 (DNS servers), 24 (domain list), 32 (information refresh time)

The first is sent when the exchange begins. Retransmissions follow after 1 s, doubling up to 3600 s, each with a jitter of ±10%, and never stop until a reply is accepted.

6.3.2.4 The reply #

A datagram is accepted only while an exchange is running, and only when:

  • it is a REPLY (type 7) with the exchange's transaction id;
  • it carries a client identifier equal to the machine's DUID;
  • it carries a non-empty server identifier.

Anything else is ignored, and retransmission continues. From an accepted reply, option 23 gives the DNS servers — when its length is a whole number of addresses, up to 16, with unspecified, multicast and loopback addresses dropped — and option 24 the search domains, read as a DNSSL's are (§6.1).

6.3.2.5 Refresh #

After a reply, the client asks again after the reply's information refresh time (option 32), or one day when there is none, but never sooner than 600 s. Each refresh is a new exchange with a new transaction id.

An answer that differs from the last logs dhcpv6 answered and leads to a reconcile and a publish. An identical one changes nothing.

6.3.3 Where these go #

Advertised and DHCPv6 servers and domains reach name resolution only through the profile's Dns.Offered, including link-local servers with the interface supplied by their DNS scope (§8.3). They are always part of what a network has shown for its identity and its record (§7).

7.1 Identification

Peios / Peios technical reference / netd / Networks

netd has to say "this is the same network as last time" with nothing on the wire that says so. The identity is a guess from what the network has offered, recorded with every signal it was made from, so that a later proof can replace the guess without changing the record's key.

7.1.1 Signals #

What a joined interface's network has shown, at the moment of the pass:

SignalFrom
kindthe interface's Interface.Kind
serverthe DHCPv4 lease's server identifier
subnetthe lease address masked to the lease prefix
gatewaythe lease's gateway (§5.3)
routerthe default router router discovery chose (§6.1)
prefixesthe subnet, then every non-deprecated autoconfigured address's prefix, each once
DNS serversthe lease's, then the routers' RDNSS servers, link-local ones included

7.1.2 The identity #

  • With a lease (a server and a subnet), the basis is dhcp:<server>|<subnet>/<prefix>.
  • Otherwise, with a default router and at least one autoconfigured prefix, the basis is ra:<router>|<prefix>/<length>, using the first prefix.
  • Otherwise there is no identity yet: nothing has been offered, or the interface is static only.

The network id is a SHA-1 digest of the bytes peios-netd-network|, the kind, |, and the basis, stamped and written as a version 5 UUID exactly as an interface id is (§4.1).

So two leases from the same server in the same subnet are the same network, whatever the address. A different server, a different subnet, or a different kind of interface is a different network. An IPv4 lease always wins over the IPv6 basis, so a dual-stack network is identified by its DHCPv4 server.

7.1.3 When #

Identification is the first step of every full pass (§2.2). It runs only on a joined interface that is up with carrier, and only when the signals yield an identity.

For each identity it creates or refreshes the network's record (§7.2) and reads back what the operator wrote on it. While a lease is held, it writes the lease address to the record's RequestedAddress (§5.7).

7.1.4 A newly identified network #

When the identity differs from the network the interface already had — including when it had none — netd logs interface <name>: network <name or id>, with (trust <trust>) when the record has one. It then marks the interface for re-judgement, so the same pass judges it with the Network.* facts present (§3.3). A rule conditioned on Network.Name or Network.Trust takes effect then. If it moves the interface to another profile, the interface's clients restart under that profile (§3.3).

The network an interface stands on is kept while it has carrier, even when a profile switch restarts the clients that identified it. A switch that its own identification triggered would otherwise forget the network and switch back. Losing carrier forgets it (§4.1).

The interface's Status Network names the identified network (§8.1). The kernel reads it to give the packet layers their Network.* facts (PKM §6.5), so it never names a network the link is not on.

7.2 Records

Peios / Peios technical reference / netd / Networks

Every network a joined interface has been identified on gets a record, Machine\System\Network\Networks\<id>. Records are never deleted by netd.

7.2.1 What netd writes #

On every identification (§7.1), netd creates the record if it is missing and writes these values under its Status subkey, each only when it differs from what is there:

ValueTypeContent
KindREG_SZthe interface kind it was seen on
ServerREG_SZthe DHCPv4 server; removed when there is none
GatewayREG_SZthe lease's gateway; removed when there is none
RouterREG_SZthe IPv6 default router; removed when there is none
PrefixesREG_MULTI_SZthe subnet and the autoconfigured prefixes, CIDR form
DnsServersREG_MULTI_SZthe DNS servers offered, either family
LastSeenREG_SZthe wall-clock time of the identification, in Unix seconds
LastInterfaceREG_SZthe kernel name of the interface it was seen on

LastSeen is rewritten on every pass that identifies the network, so a record's Status changes whenever netd runs a pass on a link that is on it.

Status is created with the descriptor O:SYG:SYD:P(A;;KA;;;SY)(A;;KR;;;WD): SYSTEM has full control and Everyone has read access. It is protected, so the hive root's inheritable grants do not reach it, and a hand edit is refused rather than silently reverted at the next pass. The descriptor is set only when netd creates the key; a Status key that already exists keeps whatever descriptor it has.

7.2.2 What the operator writes #

On the record itself, not under Status:

ValueRead asMeaning
NameREG_SZ, empty is unsetthe Network.Name fact
TrustREG_SZ, empty is unsetthe Network.Trust fact
RequestedAddressREG_SZ, an IPv4 addressthe address the next DHCPv4 client asks for first (§5.7); netd writes it too

Name and Trust are read at every identification, so an edit reaches the interface layer on the pass that follows the registry write (§2.3). They are opaque: netd compares nothing about them and gives them no vocabulary.

The kernel reads Name and Trust itself, from the record that an interface's Status Network names (PKM §6.5).

8.1 The inventory

Peios / Peios technical reference / netd / What Netd Publishes

8.1.1 The record #

Every non-loopback interface netd has seen has a key Machine\System\Network\Interfaces\<id>, named by its interface id (§4.1). netd creates it on the first pass that sees the interface. The key is the operator's, apart from ClientId, a value both write (§5.7). netd's findings are under its Status subkey.

Records are never deleted. The record of a card that has been removed stays, with what netd last wrote.

8.1.2 Status #

ValueContentPresent
Namethe kernel's name for the linkalways
Kindwired, wireless or otheralways
Macthe hardware address, lower-case, colon-separatedwhen the link has one
Paththe bus pathwhen there is one
Driverthe driverwhen there is one
VerdictJOIN, IGNORE or DOWNwhen a rule spoke; removed when the backstop answered or rules tied
Rulethe path of the rule that spoke, or the tied rules as <a> vs <b>when a rule spoke or rules tied; removed when the backstop answered
Profilethe profile path as writtenJOIN only
Readinessthe interface's level (§8.2)JOIN only
Networkthe id of the network identified on the link nowwhile one is identified; removed when the carrier goes
LastNetworkthe id of the network the link last stood onfrom the first identification; never removed

Every value is REG_SZ. A value is written only when it differs from what is there, and removed only when it is there, so a pass that changes nothing writes nothing to the registry.

Status is refreshed on every full pass for every non-loopback interface, and again at each publish (§2.2) for joined ones, so Readiness follows a reconcile that moved it without any link event.

Status Network is read by the kernel to give the packet layers their Network.* facts (PKM §6.5). It names the network only while netd has identified one on the link (§7.1).

8.1.3 The descriptor #

Status is created with the descriptor O:SYG:SYD:P(A;;KA;;;SY)(A;;KR;;;WD): SYSTEM has full control, Everyone has read access, and the DACL is protected, so the hive root's inheritable grant to Administrators does not reach it. Only netd, as SYSTEM, can write there. An administrator's hand edit is refused at the write, not reverted at the next pass. The descriptor is set only when netd creates the key.

8.1.4 The machine's values #

On Machine\System\Network itself netd writes:

  • Readiness, the machine level (§8.2);
  • Duid, when it was absent and netd generated or read one (§5.7).

8.2 Readiness

Peios / Peios technical reference / netd / What Netd Publishes

"The network is up" means different things to different services, so netd reports a level, not a flag. Levels are ordered: absent < link < addressed < routed.

8.2.1 An interface's level #

From the kernel's state after the last reconcile:

LevelWhen
absentthe interface is not up, or has no carrier
linkup with carrier, and no usable address
addresseda usable address, and no default route
routeda usable address, and a default route on the interface

A usable address is any IPv4 address other than a loopback one — a 169.254 link-local address counts, because it is the outcome of address acquisition — and any IPv6 address that is neither loopback nor link-local. Every up interface has an IPv6 link-local address, so counting one would make addressed mean nothing. Deprecated and tentative addresses count.

A default route is any route with prefix length 0, of either family, in the main table, out of the interface. It need not be one of netd's.

8.2.2 The machine's level #

The highest level among joined interfaces, or absent when none is joined. An IGNOREd or DOWN interface contributes nothing, whatever state it is in.

8.2.3 Publishing it #

At every publish (§2.2) netd computes the machine level. When it differs from the last one published — and at the first publish after startup, which precedes READY=1 — netd:

  1. logs machine readiness is <level>;
  2. sends the complete readiness set below in one datagram on the notify socket named by NOTIFY_SOCKET;
  3. writes Readiness on Machine\System\Network.

The notify set is determined by the highest machine level:

Machine levelLEVEL values, in wire order
absentabsent
linklink
addressedlink, addressed
routedlink, addressed, routed

Each value has its own LEVEL= line in the same datagram. The highest level comes last, so an older manager that retains only the last value continues to see the highest level. Upgrade peinit before or alongside netd to make every member available to dependents.

The set is sent only on a change. The notify channel is lossy by design and unacknowledged (PSPU §4.16), and a level is a statement of a current condition, so a lost one is corrected by the next change.

Each joined interface's own level is written as Status Readiness (§8.1).

8.2.4 What peinit makes of it #

peinit records the set netd last sent against the sending service, and resolves the network role to that service. A dependency on network:<level> waits until the service is active and its published set contains the requested level (peinit TRM §7.5).

A DHCPv4 lease that installs an address and default route in the same pass takes the highest machine level directly from link to routed. The notify datagram contains link, addressed and routed, so both network:addressed and network:routed dependents can start. peinit needs no knowledge of network level ordering; netd publishes every condition that holds. Losing the link replaces this set with absent, preventing new starts on any of the three previous conditions. Dependents already running keep running.

The registry, log and control status continue to report the highest machine level. net wait <level> polls over the control socket and succeeds when that level is at least the one named (§10.1).

8.3 The resolver channel

Peios / Peios technical reference / netd / What Netd Publishes

The channel's contract — that a resolver subscribes, and receives whole snapshots, never deltas — is PSPU §6.9. This article is netd's half of it.

8.3.1 Subscribing #

A subscribe request on the control socket (§9.2), which needs the query right, is answered with a snapshot at once, and the connection is kept: switched to non-blocking and added to the subscriber list. It is the one request that does not end its connection.

The first snapshot is the last one published, or, before any has been, one computed on the spot.

8.3.2 When snapshots are sent #

At every publish (§2.2) netd computes the snapshot and compares it with the last one sent. If it differs, it is sent to every subscriber. If it is equal, nothing is sent.

A subscriber that cannot take the whole frame at once — closed, or so far behind that its socket buffer is full — is dropped. A resolver reconnects and receives a fresh snapshot.

8.3.3 The snapshot #

A reply map with ok = true, kind = snapshot, hostname — the name netd last set (§8.4), empty if none — and scopes: one scope per joined interface whose level is above absent, ordered by the interface's metric, then its kernel index.

Each scope:

FieldContent
ifidthe interface id
namethe kernel name
serversDNS servers, as text, in the order to try them (below)
domainssearch domains, likewise
ntpthe lease's NTP servers (option 42)
addressesevery address the kernel holds on the interface, address/prefix, link-local ones included
default_routethe profile's Dns.Default when set; otherwise whether the interface's level is routed
exclusivethe profile's Dns.Exclusive
metricthe interface's metric (§4.2)
levelthe interface's level (§8.2)

8.3.4 Merging DNS facts #

Servers, in this order:

  1. the profile's Dns.Servers, always;
  2. with Dns.Offered: the lease's servers (option 6);
  3. with Dns.Offered: the routers' RDNSS servers, including link-local IPv6;
  4. with Dns.Offered: the DHCPv6 reply's servers, including link-local IPv6.

Domains, in this order:

  1. the profile's Dns.Domains, always;
  2. with Dns.Offered: the lease's domain search list (option 119) if it is not empty, otherwise its domain name (option 15);
  3. with Dns.Offered: the routers' DNSSL domains;
  4. with Dns.Offered: the DHCPv6 reply's domains.

Each list removes duplicates across the entire list, retaining the first occurrence and the source order above. This includes repeated values with other values between them.

The containing scope supplies the interface for link-local IPv6 servers. resolvd uses its index on UDP and TCP upstream sockets; the address text does not need a zone suffix. Deploy this publication change with a resolver that supports interface-scoped upstreams (resolvd TRM §4.7).

NTP servers are reported whatever Dns.Offered says. Whether to believe a lease's time servers is timed's decision, made against its own registry value. A snapshot describes what the network said.

The same merge feeds the dns and search fields of the status reply (§9.2), for every non-loopback interface, joined or not.

8.4 The hostname

Peios / Peios technical reference / netd / What Netd Publishes

8.4.1 Which name #

  1. Machine\System\Network Hostname, when it is set and not empty;
  2. otherwise the hostname option (12) of a lease. netd takes the first joined interface, in kernel index order, whose profile has Hostname.Offered and whose lease carries a hostname;
  3. otherwise none.

The selected name is checked by libnetd::hostname, the same rule used by System Settings and first-boot setup: after trimming outer whitespace, it must be one nonempty label of at most 63 ASCII letters, digits or hyphens, with no leading or trailing hyphen, and must not be localhost (case-insensitive). This applies to registry and lease names alike.

An invalid selected name leaves the kernel's name and netd's last applied name unchanged. It is logged as hostname <quoted name> refused: <quoted reason>, with control characters escaped, once while the same rejected candidate remains selected. A different candidate, a valid name, or no name clears that suppression. A nonempty but invalid registry name still takes precedence: it does not silently fall through to a lease. Likewise, the first selected lease name is checked without searching later leases for a replacement.

A valid name that the kernel nevertheless refuses is logged as sethostname(<name>): <error> and tried again at the next pass.

8.4.2 When #

After every full pass and every reconcile-only iteration (§2.2), netd calls sethostname(2) if the name differs from the last one it set, and logs hostname is <name>. It does not compare against the kernel's current hostname. A change made by anything else is not reverted until netd's own name changes.

When there is no name — the registry value removed and no lease offering one — netd does nothing: the kernel keeps whatever hostname it had.

The status reply and the resolver snapshot report the name netd last set, empty if it has set none.

8.4.3 What is recorded #

Each sethostname(2) that changes the kernel's name writes a netd.hostname.changed event, which is essential: the emission policy cannot switch it off. It carries config.name (always Hostname, whether the name came from the registry or from a lease), config.text (the name now in force) and config.text-previous (the kernel's name, read just before the call; the kernel's own (none) on the first change after boot). A call that leaves the kernel's name as it was — netd setting a name the kernel already holds, as after a restart of netd or a change made behind its back — records nothing, and neither does a name the kernel refuses.

The record's subject.token.sid is the principal that acted (PGSS §6.4). When the change is made during the pass a reconcile request (§9.2) asked for, that is the requester's user SID; netd refuses a request whose user it cannot read. A change netd makes on its own authority — after a registry change, a lease, or at startup — names netd's own user, SYSTEM; who wrote the registry value is LCS's to record.

8.4.4 Telling the network #

With Hostname.Announce in an interface's profile, a newly started DHCPv4 client sends the validated, trimmed registry Hostname in option 12 (§5.1). An invalid registry name is omitted. An existing client's announcement configuration is unchanged until it restarts. A name adopted from a lease is never announced.

9.1 The socket and its object

Peios / Peios technical reference / netd / The Control Socket

9.1.1 The socket #

netd listens on /run/netd/control.sock, a SOCK_STREAM Unix socket. At startup it:

  1. creates /run/netd if it is missing, sets its mode to 0755, and gives it the descriptor below;
  2. removes a socket file left by a previous run, logging removed a stale /run/netd/control.sock;
  3. binds, sets the socket file's mode to 0666, and gives it the same descriptor.

The descriptor on both: owner and group SYSTEM, and a DACL allowing SYSTEM GENERIC_ALL and Everyone GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE. peinit creates /run SYSTEM-only and inheritable, so without it nobody else could reach the socket. Everyone can connect; the control object decides what each connection can do. A failure to set it is logged as an error and netd carries on.

9.1.2 The control object #

Every request is checked against the control object, a security descriptor netd holds in memory. It is Machine\System\Network ControlSecurity, a self-relative descriptor in a REG_BINARY value, when that is set and valid. When it is not valid, netd logs ControlSecurity is not a valid descriptor (…); using the default.

The compiled default: owner and group SYSTEM, and a DACL of:

TrusteeRights
SYSTEMNETWORK_ALL_ACCESS
AdministratorsNETWORK_ALL_ACCESS
EveryoneNETWORK_QUERY, READ_CONTROL

The object is rebuilt whenever the configuration changes (§2.3), so a written ControlSecurity applies to the next connection.

9.1.3 Rights #

RightValue
NETWORK_QUERY0x00000001
NETWORK_CONTROL0x00000002
NETWORK_ALL_ACCESS0x000F0003: both, plus the standard rights

The generic mapping:

Generic rightMaps to
readNETWORK_QUERY, READ_CONTROL
writeNETWORK_CONTROL, READ_CONTROL
executeNETWORK_QUERY
allNETWORK_ALL_ACCESS

9.1.4 The check #

For each request, netd opens the connecting peer's token through KACS and runs a real access check of the request's right (§9.2) against the control object. It never looks at SO_PEERCRED, so a deny-only group or a filtered token is judged as the kernel would judge it anywhere else. A peer whose token cannot be opened is denied, and so is one whose token's user cannot be read, since a change made for it is recorded under that user (§8.4).

A denied request is answered {ok: false, error: "access denied"}, and nothing else happens.

9.1.5 Nonblocking client I/O #

The control socket is served on netd's one thread (§2.2). Accepted connections are nonblocking and polled alongside network and registry inputs. An idle client, partial request, or client that does not read its reply does not hold the loop in a socket read or write.

At most 64 requests or replies are pending at once. While full, netd stops polling the listener until a slot becomes free; connections still waiting in the kernel backlog have not been accepted. Each iteration accepts at most 16 connections and reads or writes at most 4 096 bytes per ready pending connection. The 65 536-byte message ceiling is checked from the header before reading a request payload (§9.2).

A request must complete within two seconds of acceptance. A normal reply must be written within two seconds of being queued. Partial progress does not extend either deadline. Expiry closes the connection without a timeout reply. These deadlines participate in the same poll timeout as network-client timers. Completed subscriptions leave this pending-client set and use the existing nonblocking snapshot publisher (§8.3).

9.2 Requests

Peios / Peios technical reference / netd / The Control Socket

9.2.1 Framing #

The control socket is a PSPU observability query channel: one length-prefixed MessagePack map in each direction, one request and one reply per connection, except subscribe.

  • A frame is a 4-byte little-endian payload length, then the payload.
  • A length above 65536 is refused before any of the payload is read, with {ok: false, error: "message of <n> bytes exceeds the ceiling"}.
  • A request is a map with a string query. Unknown keys are ignored, and a key that appears twice refuses the request.
  • A reply is a map with a boolean ok, and either the result's fields or a string error.

An oversized, incomplete-at-EOF, or undecodable request is answered with an error reply and the connection ends:

Problemerror
no querymissing field query
an unknown queryunknown query "<query>"
a duplicate keyduplicate field <key>
renew without interfacemissing field interface
malformed MessagePackmalformed message: …

Expired client deadlines and failed socket I/O close the connection without a reply (§9.1). A normal reply that exceeds the message ceiling is replaced with an error reporting its size.

9.2.2 The requests #

queryRightDoesReply
statusNETWORK_QUERYnothingthe status (below)
subscribeNETWORK_QUERYkeeps the connection as a subscriber (§8.3)a snapshot now, and another on each change
reconcileNETWORK_CONTROLa full pass (§2.2){ok: true}
renew, with interfaceNETWORK_CONTROLthe client renews (§5.4){ok: true}, or an error

renew finds its interface by kernel name or by interface id. It answers no interface <name> when there is none, and <name> is not using DHCP when the interface has no DHCPv4 client.

9.2.3 The status reply #

FieldContent
hostnamethe name netd last set, empty if none (§8.4)
levelthe machine level (§8.2)
refusalwhy the newest generation was refused, or nil (§3.2)
interfacesone map per non-loopback interface netd has seen, joined or not, in kernel index order

Each interface:

FieldContent
ifid, name, indexthe interface id, kernel name and index
mac, path, driveridentity (§4.1); empty strings when absent
verdictJOIN, IGNORE or DOWN; nil when the backstop answered or rules tied
rulethe rule that spoke, backstop, or the tied rules <a> vs <b>
profilethe profile path, JOIN only
network, network_name, network_trustthe network identified now (§7)
warningdiscovery unanswered (§5.2), or a tie (§3.3); nil otherwise
up, carrierthe link's flags
levelthe interface's level (§8.2), computed for every interface
addressesevery address the kernel holds, address/prefix, link-local ones included
gateway, gateway6the gateway of the interface's first IPv4 and IPv6 default route, any owner's
dns, searchthe merged DNS facts (§8.3); empty for an interface not joined
leasewhile the client holds a lease: server, expires_in (whole seconds), state (§5.1), duration (the lease's length), and renew_at and rebind_at (T1 and T2), the last three in whole seconds after the lease was bound; nil otherwise. A client reading a netd that predates the last three finds them absent.

10.1 Commands

Peios / Peios technical reference / netd / The Net Command

net is netd's operator command. It talks to netd over the control socket, and reads the registry directly for rules and profiles, because those are configuration that netd merely executes. Changing the network is a registry write; net shows state and pokes the daemon. The task-oriented guide is ~peios/networking/the-net-command.

10.1.1 Subcommands #

CommandDoes
net statusprints the status reply (§9.2)
net renew <interface>sends renew
net reconcilesends reconcile
net rulesprints Rules\Interface from the registry: one line per rule, indented by depth, with its priority, (disabled) for Enabled = 0, its conditions, and its actions (NULL when it has none)
net profiles, net profile listprints Profiles\ from the registry: one line per profile, indented by depth, with the values it sets itself ((inherits only) when none) and (disabled)
net wait <link|addressed|routed> [seconds]waits for the machine level
net policyprints the kernel engine's status: enforcing or not, the generation, unwalked changes, a refused last walk, the context count
net policy wait [seconds]waits for the packet policy to be in force
net version, net --versionprints net <version>

rules and profiles fail when their key does not exist. Neither needs netd running.

10.1.2 net status output #

A hostname line ((unset) when empty) and a readiness line, then policy REFUSED: <why> (the last good generation stands) when there is a refusal. Then a block per interface: <name> [<id>], then verdict (<VERDICT>(<profile>) by <rule>, or (none) by <rule>), state, and readiness for a joined interface. Then, where present, hardware, network (<name> [<id>] trust <trust>), each address, gateway, gateway6, dns, search, lease (<state> from <server>, <n>s left) and warning.

10.1.3 net wait #

It asks for status every 500 ms and succeeds as soon as the machine level is at least the one named. An error reaching netd is printed and the wait goes on. The timeout is the second argument, or 60 s when absent or not a number. When it lapses, net prints net: timed out waiting for <level> and fails.

absent is accepted as a level, and is met at once.

10.1.4 net policy wait #

It reads /dev/peios-ntfe's status (ABI 5) once and takes the number of policy changes the engine has noted as its target. It then polls every 5 ms until the engine's count of changes walked reaches the target. If the last walk was refused, it fails, naming the errno and the generation that stands; otherwise it succeeds. The timeout is the argument, or 10 s. The counters are the kernel's (PKM §6.5). A device that answers with another ABI fails at once.

10.1.5 Exit status #

CodeMeaning
0success
1netd unreachable, an error reply, a timeout, a refused policy, or a registry key that cannot be opened
2usage: an unknown subcommand, a wrong argument count, an unknown level, or a non-numeric policy wait timeout

11.1 Failure modes

Peios / Peios technical reference / netd / Failure Modes

11.1.1 Where the evidence is #

  • net status: the policy refusal, each interface's verdict and the rule that gave it, its warning, its addresses and gateways, and its lease's state.
  • netd's log. Informational lines go to standard output; warnings and errors go to standard error. peinit forwards both to eventd (evctl 'LOGS FROM netd SINCE 1h ago TAKE 40'). Each line is also mirrored to /dev/kmsg at KERN_INFO, KERN_WARNING or KERN_ERR, so it reaches the kernel log and, at a permissive console log level, the console. If the mirror cannot be opened, netd says so once on standard error and tries again on the next line. Each mirrored line opens and closes its own /dev/kmsg descriptor, so boot and reconciliation bursts do not lose lines to the kernel's per-open rate limit (normally ten lines in five seconds). All three severities are retained. The mirror remains best-effort if opening or writing the device fails; stdout/stderr delivery is independent.
  • The registry: Interfaces\<id>\Status, Networks\<id>\Status, Readiness.

11.1.2 No address on a joined interface #

What net status showsLikely cause
warning asked for an address; nobody answered, a 169.254 address or noneno DHCPv4 server answered. Either there is none, or NTFE refused the exchange: the shipped baseline's DHCP rules were deleted or overridden. netd cannot see the verdict stream.
no lease line, no warningthe profile does not offer the address (Address.Offered, Address.Families), the interface has no MAC, or the packet socket would not open (logged). Or a server is answering but its ACK does not read as a lease: no server identifier, a lease time under 4 s, or a malformed classless route option (§5.3). The client then cycles through Selecting and Requesting, neither of which is shown (§5.1).
no IPv6 addressno advertisement arrived, or none was accepted: hop limit under 255, a non-link-local source, a prefix that is not an autonomous /64 (§6.1, §6.2). Also check Address.Families.

11.1.3 The wrong interfaces are configured #

What net status showsMeaning
policy REFUSED: …the newest generation was refused (§3.2). Everything shown is still the last good generation. Fix the named rule or profile; the next write is read at once.
verdict (none) by backstopno rule speaks for the interface. It is left as the kernel left it.
verdict (none) by <a> vs <b>, warning rules … tietwo rules tied on priority and named different profiles (§3.3). The interface is ignored until one outranks the other.
an interface that was joined is now IGNOREd, with its old addresses still on itby design: IGNORE touches nothing, removal included (§3.3)

11.1.4 Changes that do not stick #

  • An address added by hand disappears. netd owns every address on a joined interface, and the next pass removes it (§4.3). Add it to Address.Static instead.
  • A route added by hand stays, unless it carries protocol 200.
  • A pinned gateway never appears. Route.Gateway is not checked against the interface's subnets. When the kernel cannot reach it, every reconcile logs reconcile: AddRoute(…) failed, and the interface stays addressed.
  • The hostname does not change back when Hostname is removed. netd never unsets a name (§8.4).

11.1.5 Losing what was working #

  • A cable pull drops everything offered — the lease, autoconfigured addresses, routes, the network identity — and stops the clients (§4.1). When the carrier returns, the DHCPv4 client asks for the old address first, so a cooperative server restores it in one exchange.
  • A lease that cannot be renewed drops its address at the lease's end, unless the profile says Address.OnExpiry = Keep (§5.4). After that, the link-local fallback does not happen again in the same client's life (§5.2).
  • Editing a profile restarts its interfaces' clients (§3.3), so a DHCPv4 lease is released and asked for again.

11.1.6 netd is slow or unresponsive #

  • Everything pauses for seconds at a time. A program that connects to the control socket and does not send its request holds netd's single loop for up to 2 s per connection (§9.1). Look for a misbehaving client of /run/netd/control.sock.
  • net: cannot reach netd at /run/netd/control.sock. netd is not running, or /run/netd lost its descriptor. The service restarts netd always; evctl shows why it stopped.

11.1.7 Configuration not read #

  • netd ignores every registry change. The watch could not be armed at startup (registry watch unavailable … configuration is read once), or Machine\System\Network did not exist then (§2.1). Restart netd.

11.1.8 Getting back to a known state #

  1. Undo the last registry write. A refused generation names what to undo, and the next good write is taken at once.
  2. net reconcile runs a full pass without waiting for an event.
  3. net renew <interface> renews a lease without releasing it.
  4. Restarting netd re-derives everything from the registry and the kernel, and asks for the remembered IPv4 address with INIT-REBOOT (§2.1). The requested address already installed and applicable netd-owned routes stay while that request is pending. A NAK or unanswered revalidation discards this provisional state and starts fresh discovery; an unchanged ACK keeps IPv4 connectivity in place. Offered DNS and IPv6 information still have to be reacquired.

Appendix A Constants

Peios / Peios technical reference / netd

A.1 Paths #

PathUse
Machine\System\Networkconfiguration root, readiness, DUID (§2.3, §8)
/run/netd/control.sockcontrol socket (§9.1)
/run/netdruntime directory, created by peinit (§2.1)
/var/state/netd/duidthe DUID, when the registry has none (§5.7)
/var/state/netd/secretthe RFC 7217 secret (§6.2)
/usr/sbin/netdthe daemon
/proc/sys/net/ipv6/conf/<name>/accept_raset to 0 (§6.1)

A.2 Numbers #

ConstantValue§
Routing protocol stamped on netd's routes200§4.3
Default route metric100; 600 for wireless§4.2
Minimum MTU applied68§4.3
Converge repetitions per passat most 4§2.2
Registry tree depth read16§2.3
Control message ceiling65536 bytes§9.2
Control read and write timeout2 s§9.1
DHCPv4 first backoff, ceiling4 s, 64 s, ±1 s jitter§5.2
DISCOVER retransmissions before "nobody answered"3§5.2
REQUESTs in Requesting before rediscovery4§5.2
REQUESTs in Rebooting before rediscovery2§5.2
Renewing and rebinding retransmission floor60 s§5.2
Shortest lease accepted4 s§5.3
DHCPv4 maximum message size advertised1500§5.1
DHCPv4 message paddingto 300 bytes§5.1
Domain-search compression jumps per name16§5.3
Link-local range169.254.1.0–169.254.254.255§5.5
Router solicitation interval, ceiling4 s, 3600 s, ±10% jitter§6.1
Solicitations before backoff3§6.1
Prefixes, RDNSS servers, DNSSL domains per advertisement option16§6.1
SLAAC prefix length64§6.2
Two-hour rule7200 s§6.2
Temporary address preferred lifetime86400 s minus 0–599 s§6.2
Temporary address valid lifetime172800 s§6.2
Temporary addresses per prefix4§6.2
Stable address counter attempts8§6.2
DHCPv6 first retransmission, ceiling1 s, 3600 s, ±10% jitter§6.3
DHCPv6 refresh default, floor86400 s, 600 s§6.3
DHCPv6 servers and domains kept16 each§6.3

A.3 Identifiers #

IdentifierValue§
Interface id digest prefixpeios-netd-ifid|§4.1
Network id digest prefixpeios-netd-network|§7.1
Stable address digest prefixpeios-ndp-stable-iid|§6.2
DUID-LL header00 03 00 01§5.7
Status descriptorO:SYG:SYD:P(A;;KA;;;SY)(A;;KR;;;WD)§8.1
NETWORK_QUERY, NETWORK_CONTROL, NETWORK_ALL_ACCESS0x1, 0x2, 0x000F0003§9.1

Peios Learn — documentation for the Peios project.

Built with Trail.