Skip to content

Config Generation

The agent turns a device config into the files OpenBSD reads: pf.conf and its anchors, hostname.if files, unbound.conf, dhcpd.conf, dhcpleased.conf, ifstated.conf, iked.conf, ospfd.conf, newsyslog.conf and a managed block in root's crontab. This page covers the parts whose design is not obvious from the output. See PF Generation for the ruleset itself.

Before anything is generated, config.Validate rejects configs that have no valid output: group cycles, a block rule bound to a schedule, and the other checks in internal/config/validate*.go.

Network objects

Named objects render as pf tables (host, network, group, FQDN) or macros (port objects, services). A render-time index decides which (internal/pf/objects.go):

type objectIndex struct {
    tables       map[string]bool
    macros       map[string]bool
    services     map[string]config.ServiceConfig
    groupMembers map[string][]string
}

Resolution is a lookup against the index (resolveAddr, resolvePort), with the literal value as the fallback. A host object named 10-camera resolves to its table, not to the literal string.

Groups are flattened transitively to their leaf values (flattenGroup), because pf tables cannot nest. This is why the UI shows what a group expands to on the device, and why a cycle is rejected when it is saved.

Services supply protocol and destination port to any rule naming them. An explicit value on the rule wins, so a rule can narrow a service without redefining it.

Schedules

pf has no notion of time. Each schedule owns an anchor; the rules bound to it live in an anchor file that cron loads when the window opens and flushes when it closes. Outside the window the anchor is empty.

Piece Value
Anchor sched_<name> (sanitised)
Anchor file /etc/pf.anchors/sched_<name>
Open pfctl -a sched_<name> -f /etc/pf.anchors/sched_<name>
Close pfctl -a sched_<name> -F rules
Crontab block between # BEGIN digilan-agent schedules and # END digilan-agent schedules; lines outside are preserved

A window that crosses midnight closes on the following day. At apply time the agent loads every anchor whose window is open now and removes anchors of schedules that no longer exist.

A scheduled rule does not exist outside its window

A block bound to a schedule would stop blocking when the window closes, the opposite of what an operator expects. Schedules therefore go on pass rules, with the default deny covering the rest of the day; the agent's validation refuses a scheduled block, and the UI says so at the point of binding.

Macros are redeclared in each anchor file. An anchor loaded with pfctl -a name -f file is parsed on its own and does not inherit macros from the main ruleset. Without the redeclaration the anchor fails to parse and stays empty, and an empty anchor fails silently.

DNS: unbound

internal/dnsfilter renders /var/unbound/etc/unbound.conf. Filtering blocks at the resolver rather than through an intercepting proxy: no CA to distribute, nothing breaks on certificate pinning, nothing decrypts customer traffic.

What it does not cover

A client with its own resolver, or using DNS-over-HTTPS, bypasses it. It is a policy control, not a containment boundary.

Views. Each filtering policy becomes an unbound view, and networks are bound to it with access-control-view.

  • access-control-view is a server-clause option. Outside server: it is a syntax error and unbound-checkconf rejects the whole file.
  • A view answers only what it lists unless view-first: yes is set, which falls through to the server clause for everything else. The generator sets it.

When nothing is generated. Generate returns an empty string only when filtering is off, no LAN uses the gateway as its resolver, and no service is published. The caller then leaves unbound and its file as they are; it does not write an empty config.

DHCP

internal/dhcp writes /etc/dhcpd.conf for the interfaces that serve DHCP, including reservations, and drives dhcpd. internal/dhcpleased writes /etc/dhcpleased.conf for the client side, so that only the intended uplink installs a default route; see Uplink Failover.

Validating the output

Unit tests pin the generated text; they cannot tell whether OpenBSD accepts it.

cmd/gencheck writes every generated file for a representative config that exercises every construct: hostname.<if>, pf.conf and the sched_* anchors, unbound.conf (with published names), ospfd.conf, newsyslog.conf, the detection scripts and rc.d scripts. It is not part of the agent binary.

dev/bench-check.sh runs gencheck, copies the output to the development VM and runs the real parsers: pfctl -nf on pf.conf and on each anchor, unbound-checkconf, ospfd -n, newsyslog -n, ksh -n and rcctl check on rc scripts, and ifconfig on each hostname.if. Every check is an explicit pass/fail, and the script fails on any of them. Run it on any change to a generator.