Skip to main content

routerd architecture overview

This document is an introduction to routerd for operators and contributors. It covers the design intent and the major moving parts. For day-to-day usage, start with the tutorials and the how-to guides. For resource definitions, see the API reference.

routerd architecture diagram showing router YAML and routerctl flowing through validation, effective config, controllers, SQLite state, renderers, and owned host artifacts


1. Where routerd fits

routerd is a declarative router framework. Its goal is to let you build a home router, a SOHO router, or a small data-center edge router from the same set of primitives.

The three deployment targets we design for:

TargetScopeRequired tier
Home-router replacementOne host, one or two uplinks, one to three LAN VLANsH
Hypervisor SDN routerVXLAN / EVPN / underlay routing inside a clusterC
Kubernetes cluster edgeAdvertise Pod CIDR / LoadBalancer IP via BGP, terminate ingressS → C

All three are expressible with the same declarative primitives. The applicable feature set scales with the deployment.

1.1 Capability tiers

TierUse caseHeadline features
H (Home)Home or small officeWAN acquire (PD/RA/PPPoE/DHCPv4/DS-Lite), LAN service (RA/DHCPv6/dnsmasq), NAT44, firewall, EgressRoutePolicy
S (SOHO/branch)Several sites with VPN+ WireGuard / IPsec, VRF, dynamic routing across VPN, commit-confirmed
C (Campus / small DC)Tens of nodes+ EVPN-VXLAN, iBGP RR, BFD, RouteMap DSL, richer routing policy
E (Enterprise / SP)Hundreds of nodes+ Full BGP, MP-BGP L3VPN, segment routing, HA leader election

The primitives are the same from H to E. Higher tiers add more routing and policy controllers on top of the same model.


2. Runtime environment

2.1 Deployment shape

routerd targets virtual machines. Embedded appliances are out of scope for now.

Requirements for the host environment:

  • virtio NICs (vmxnet, ne2k, etc. are out of scope)
  • No dependency on privileged kernel modules (DPDK / XDP optional, host passthrough not required)
  • Console + SSH for operations
  • For lab work, snapshots and clones are encouraged

2.2 OS strategy

routerd is designed to be cross-OS. The same binary and the same configuration target multiple operating systems.

OSStrengthsRole
Linux (Ubuntu / Debian)systemd standard, easy to obtain, recent kernelsPrimary platform for development and production
NixOSDeclarative OS aligns with declarative routerd configuration; reproduciblePrimary platform for declarative operations
FreeBSDStable base, small footprint, jail isolationLong-running and low-resource deployments
AlpineMinimal footprint, musl, apkMinimal profile (future)

OS-specific differences are absorbed in the pkg/platform layer. Mappings such as nftables ↔ pf, systemd-networkd ↔ rc.conf, and systemd unit ↔ rc.d script are owned by per-OS renderers.

Versioning policy: routerd uses date-and-time-based release versions in vYYYYMMDD.HHmm format; the previous 0.x.y and yyyymmdd.N pre-release numbering is discontinued.


3. End-to-end picture

┌─────────────────────────────────────────────────────────────────┐
│ User │
│ /etc/routerd/*.yaml + routerctl CLI │
└─────────┬─────────────────────────────────────────┬───────────────┘
│ inotify HTTP+JSON
│ (notify only) (explicit apply)
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ routerd (1 binary, multi-OS) │
│ │
│ ConfigWatcher ──notify only──▶ Bus │
│ ConfigLoader ◀──explicit trigger───── routerctl apply │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Bus (in-process channel + persistent SQLite event log) │ │
│ │ topics: routerd.<area>.<subject>.<verb> │ │
│ │ cursor: events.id (autoincrement) │ │
│ │ fanout: subscribe pattern match → controller channel │ │
│ └─────┬─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ Controllers (in-process reactors) │
│ PrefixDelegationCtrl / LANAddressCtrl / RAAnnouncerCtrl │
│ DNSAnswerCtrl / DNSResolverCtrl / FirewallCtrl / RouteCtrl │
│ EgressRouteCtrl / ServiceLifecycleCtrl / ConfigLoaderCtrl │
│ EventRuleEngine / DerivedEventEngine │
│ │ │
│ ▼ SQLite state DB (objects/events/artifacts/generations) │
└─────────┬─────────────────────────────────────────────────────────┘
│ Unix socket HTTP+JSON fsnotify (lease/snapshot)
▼ ▲
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1 source daemons (one process each) │
│ routerd-dhcpv6-client / routerd-dhcpv4-client │
│ routerd-pppoe-client / routerd-dns-resolver │
│ routerd-healthcheck@<resource> / routerd-firewall-logger │
└─────────────────────────────────────────────────────────────────┘

4. Resource model

routerd configuration is a set of resources. The shape is similar to Kubernetes but the apiVersion hierarchy and the controller plumbing are simpler.

- apiVersion: net.routerd.net/v1alpha1
kind: DSLiteTunnel
metadata:
name: ds-lite-primary
spec:
aftrFQDN: gw.transix.jp

4.1 Major apiVersions

apiVersionResponsibility
net.routerd.net/v1alpha1Networking (Interface, IPv4Static, DSLite, PPPoE, EgressRoute, HealthCheck, etc.)
dns.routerd.net/v1alpha1DNS (DNSZone, DNSResolver, DHCPv4Reservation, etc.)
firewall.routerd.net/v1alpha1Firewall (FirewallZone, FirewallPolicy, FirewallRule, NAT44Rule, etc.)
system.routerd.net/v1alpha1OS bootstrap intent and overrides (Package, SysctlProfile, WebConsole, etc.); host runtime artifacts are derived from resources
control.routerd.net/v1alpha1controller chain and routerctl control surface

The full list is in the API reference.

4.2 Cross-resource references

When one resource refers to the status of another, use a typed *From field instead of a literal value.

- apiVersion: net.routerd.net/v1alpha1
kind: WebConsole
spec:
listenAddressFrom:
resource: Interface/mgmt
field: ipv4Addresses
port: 8080

addressFrom, ipv4From, ipv6From, prefixFrom, rdnssFrom, gatewayFrom, and upstreamFrom follow the same shape. Dependencies (dependsOn) use the same mechanism.

A *From reference whose target has not published a value yet is a normal bootstrap condition, not an error: the consuming controller reports the resource as Pending (with a reason naming the unresolved reference) and re-reconciles when the referenced status changes — no explicit dependsOn is required. For example a DNSResolver forward source whose upstreamFrom points at a DHCPv6Information server stays Pending until that server learns its DNS servers, then becomes Applied on the next reconcile. A source that declares no upstream at all (neither upstreams nor upstreamFrom) is a real misconfiguration and is rejected by validation instead.

For details, see resource model and state and ownership.


5. Event bus and controller chain

routerd combines an in-process event bus with a set of controllers to converge to the desired state declared in configuration.

5.1 Event bus

  • in-process channels backed by a SQLite event log for persistence
  • topics use the pattern routerd.<area>.<subject>.<verb> (for example routerd.dhcpv6.bind.changed)
  • subscribers receive events via pattern match
  • every event has an events.id cursor so re-evaluation is possible after a restart

5.2 Controller chain

Every controller follows the common framework.FuncController shape:

  • Subscriptions: topics this controller cares about
  • Bootstrap: one-shot initialisation at startup
  • PeriodicFunc: idempotent periodic re-evaluation
  • ReconcileFunc: state convergence on event arrival

The eventedStore wrapper guarantees that every persisted state change emits routerd.resource.status.changed, which downstream controllers consume to resolve cross-resource dependencies.

Kubernetes edge resources use this status flow directly. IngressService health checks choose an active backend and the NAT renderer uses that status on the next reconcile. BGPRouter / BGPPeer status is observed from the long-lived routerd-bgp daemon with typed ListPeer / ListPath API calls and can lower VirtualAddress VRRP priority through track. BGP config changes are applied to that daemon with GoBGP API objects instead of rendering FRR-style text config or shelling out to reload tools. VirtualAddress and IngressService hostnames feed DNSResolver-served zones as derived A/AAAA records, and BGP/VRRP/Ingress status is also surfaced through dedicated routerctl show views and low-cardinality OTel metrics for transitions and backend health.

5.3 Daemon contract

Long-running OS processes (DHCPv6 client, DNS resolver, healthcheck, etc.) live as daemons rather than as controllers. Each daemon talks to the controller chain over a Unix domain socket using JSON, and persists its own state under files such as lease.json.

For details, see reconcile loop behaviour.


6. Operating the configuration file

The routerd configuration file (default /usr/local/etc/routerd/router.yaml) is rolled out as follows.

edit → routerctl validate → routerctl apply

└─ observe host state
→ plan
→ render host artifacts
→ record state and exit

routerd serve
→ consumes state/events
→ starts / enables / reloads managed daemons
→ updates OS state (nftables / netlink / systemd) continuously

We strongly recommend keeping the configuration in git. Apply changes to production via routerd; do not run ad hoc commands such as nft add rule, ip route add, or sysctl -w directly on the host. Ad hoc changes are either reverted by the next reconcile or, worse, create drift between the routerd state DB and what the kernel actually has.

The right response to drift is to express the new desired state in configuration and apply it again. apply must return quickly and hand daemon lifecycle to the controller runtime; the long-running serve process keeps the configuration ↔ state DB ↔ OS state triangle aligned.


7. Observability and debugging

routerd exposes its operating state through several surfaces.

  • routerctl status — phase per resource
  • routerctl describe <kind>/<name> — spec, status, and recent events for one resource
  • routerctl events --topic <pattern> --resource <kind>/<name> — tail the bus
  • routerctl plan --diff — preview the diff a future apply would produce
  • Web Console (default http://<mgmt-ip>:8080/) — summary, events, connections, clients, firewall, configuration in a browser
  • journalctl -u routerd.service -f | grep "routerd event" — bus events through the systemd journal

Logs are persisted across four databases by purpose: events.db (controller-driven), dns-queries.db (DNS resolver), traffic-flows.db (conntrack/pf), and firewall-logs.db (NFLOG/pflog). For details, see log storage.

OpenTelemetry export is configured by the Telemetry resource in observability.routerd.net/v1alpha1. routerd does not bundle an OTLP collector. When an endpoint is declared, generated systemd, NixOS, and FreeBSD rc.d units receive the matching OTEL_* environment variables and the existing SDK path sends logs, metrics, and traces to that endpoint.