Repository navigation
Run PFUI end to end on OpenBSD in CI, and pledge the daemon - #13
Merged
Merged
Conversation
andylemin
force-pushed
the
openbsd-ci
branch
5 times, most recently
from
September 27, 2026 05:11
26a51f9 to
71ee976
Compare
The daemon's ioctl, unveil and pledge code compiles on no other platform, so .github/workflows/openbsd.yml runs tests/openbsd/run.sh inside a real OpenBSD VM: both installers, the crate's tests on the target including the live ioctl suite against real tables, then the installed resolver answering live queries through the installed daemon over TCP, the unix socket and both at once, under each control path, with every answer checked in the PF table, Redis and the persist file. Further passes cover expiry, a pfctl reload repopulating the table from the persist file, a clean stop, and a table missing from the ruleset being refused at start. It runs on every push and pull request touching the daemon, the resolver, the protocol or the installers, and on demand. CTL now selects one control path and the daemon never falls back to the other. A probe reads every configured table through that path before serving and refuses to start otherwise, naming what the errno means on a firewall and the two remedies. A fallback that succeeds hides the fault it worked around: it cost a fork and exec per message, and it masked a table absent from the ruleset, because pfctl -T add creates one no rule references. pledge(2) is applied after the unveil lock, with promises following CTL. No promise set permits DIOCRADDADDRS, DIOCRDELADDRS or DIOCRGETADDRS, so under CTL: IOCTL those run in a PF child forked before the parent pledges: it holds /dev/pf alone, unveils the filesystem away, and answers one ioctl per socketpair request. The parent pledges stdio rpath wpath cpath flock fattr inet, plus unix with a local socket, and never opens the device. CTL: PFCTL runs no child and adds proc exec with the paths pfctl needs. PLEDGE: False disables the call for an operator who needs whitelisting back before a promise gap is fixed, and is logged at error level on every start. The README documents the sandbox and carries PLEDGE in both firewall examples. Both installers take their prompt answers from the environment so an unattended host can run what an operator runs, and their ERR traps name the failing line. install-client-unbound.sh no longer reads unbound-anchor's exit 1 as a failure, which it returns whenever it writes the root key, and no longer keeps the stock example config that make install-all leaves at the configured path. server-c is removed. It held a framing implementation and nothing else, kept as a cross-check against the shared vectors until the Rust daemon had proven itself in production; protocol/python and server-rust both run framing.tsv, which is the cross-check it existed to provide. The README also describes the Rust daemon as the recommended production version, states that PFUI_Firewall is OpenBSD-only and why, and calls OpenBSD's binary packages packages rather than ports. Preparing an accepted connection names the step that failed rather than reporting all four as accept(). Comments across the daemon, the resolver module and the wire module are tightened.
andylemin
force-pushed
the
openbsd-ci
branch
from
September 27, 2026 05:28
71ee976 to
34dbfee
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PFUI_Firewall runs on OpenBSD alone. It drives PF through OpenBSD's table ioctls, and
unveil(2)andpledge(2)are OpenBSD calls. FreeBSD has its own PF interface and could plausibly be supported, but that work has not been done. None of that code compiles elsewhere, so no job has ever built or run it.An OpenBSD job
.github/workflows/openbsd.ymlrunstests/openbsd/run.shin an OpenBSD VM, on pushes and pull requests touching the daemon, the resolver, the protocol or the installers, and on demand.The job runs the shipped installers rather than reproducing their steps.
install-server-rust.shbuilds the daemon with the packaged rustc, andinstall-client-unbound.shbuilds Unbound from the signed 7.9 release sources with the Python module. Both take their prompt answers from the environment so a job can drive them.The crate's tests then run on the target, including the live ioctl suite against real PF tables and the unix-socket suite as root. After that the resolver answers live queries through the daemon over TCP, the unix socket, and both at once, under each control path. Every answer is checked in the PF table, in Redis and in the persist file, and a repeat lookup must relabel the record as a cache hit. Three further passes cover expiry, a
pfctl -freload repopulating the table from the persist file, and a table missing from the ruleset being refused at start.One control path
CTLselects the ioctl orpfctl(8), and the daemon no longer falls back to the other. A probe reads both configured tables through the chosen path before serving, and exits 7 if it cannot, naming what the errno means on a firewall and the two remedies.A fallback that succeeds hides the fault it worked around. This one cost a fork and exec of pfctl on every message, and it made a table absent from the ruleset invisible, because
pfctl -T addcreates a table no rule references.pledge
pledge(2) is applied after the unveil lock. Both the path plan and the promise set are built as data, so their contents are tested on every platform.
No promise set permits
DIOCRADDADDRS,DIOCRDELADDRSorDIOCRGETADDRS. UnderCTL: IOCTLthose run in a child forked before the parent pledges. It holds/dev/pf, unveils the filesystem away, and answers one ioctl per socketpair request. The parent pledgesstdio rpath wpath cpath flock fattr inet, plusunixfor a local socket, and never opens the device.CTL: PFCTLruns no child and addsproc exec.PfDevis the seam, sotable_add,table_delandtable_getare unchanged and the child's loop is tested off OpenBSD against a fake kernel over a socketpair.PLEDGE: Falsedisables the call, for an operator who needs whitelisting back before a promise gap is fixed, and is logged at error level on every start.Removed
server-c/held a framing implementation and nothing else, kept as a cross-check against the shared vectors until the Rust daemon had proven itself in production.protocol/pythonandserver-rustboth runframing.tsv, so the cross-check remains and the scaffolding does not.Smaller changes
install-client-unbound.shstops readingunbound-anchor's exit 1 as a failure, which it returns whenever it writes the root key, and stops keeping the stock configmake install-allleaves at the configured path. All three installers'ERRtraps name the failing line. Preparing an accepted connection names the step that failed rather than reporting all four asaccept(). Comments across the daemon, the resolver module and the wire module are tightened. The README describes the Rust daemon as the recommended production version, documents the sandbox, carriesPLEDGEin both firewall examples, states that PFUI_Firewall is OpenBSD-only and why, and calls OpenBSD's binary packages packages.Verification
121 Rust unit tests, 19 integration tests, 249 Python tests,
cargo fmt --checkandclippy -D warningsclean.The OpenBSD job is green in 8m50s: both installers, 151 tests on the target including 5 live ioctl tests as root, and all nine passes. The daemon reports 221µs to the acknowledgement against 900µs in total, so the resolver is released at about a quarter of the work.