Implement PFUI server in RUST - #5
Merged
Merged
Conversation
added 9 commits
September 3, 2026 02:29
The persist files hold daemon state that must survive a reboot, which is what /var/db is for; /var/spool is for queued work awaiting processing. The filenames drop the redundant prefix at the same time. They keep a directory of their own because file_push and file_pop need to create a .lock sidecar and rename a tempfile over the persist file, so the directory itself has to be writable, not just the files. An existing file from an earlier layout is now copied rather than moved. pf.conf loads its tables from whatever path it names, and moving the file out from under it leaves the next ruleset load unable to find it.
The resolver imports lz4 and yaml through its embedded interpreter, which is the system one, so they cannot go in a virtualenv. On OpenBSD that interpreter is externally managed and pip refuses to write to it, which aborted the installer outright; py3-lz4 and py3-yaml are the supported route. An import check follows, because an unimportable dependency otherwise surfaces as the resolver failing to start. cmake is dropped from the dependency list. Unbound builds with autotools and never used it, but asking for it aborts that checked block on any host where the cmake package conflicts.
A drop-in replacement for the Python daemon: it reads the same /etc/pfui_firewall.yml, keeps the same Redis key schema so it can take over a database the Python daemon wrote, sends the same reply strings, and installs to the same path under the same rc.d service name. The component this most wanted rewriting is the one that parses unauthenticated network input and then writes PF tables, so it is built without a memory-unsafe language and without an async runtime: an accept thread per listener and a bounded worker pool that sheds beyond twice MAX_WORKERS, which is the shape the Python daemon already had. DIOCRGETADDRS is implemented, so under CTL: IOCTL the scan loop reads the table through the kernel interface instead of shelling out to pfctl once per period. The subprocess survives as the CTL: PFCTL path and as the fallback when an ioctl fails. Routability is decided by explicit prefix tables rather than a standard library predicate, which is unstable in Rust and has shifted across CPython releases. Parity with the Python validator is one-directional: everything it rejects is rejected here, and the extra rows only refuse more egress. unveil(2) restricts the daemon to the config, /dev/pf, the persist and socket directories and pfctl. pledge(2) is deliberately not enabled yet. Verified on OpenBSD 7.9: the struct layouts and all three ioctl numbers match net/pfvar.h exactly, and the table ioctls, the rc.d lifecycle and the whole path from a resolver's answer to the PF tables, Redis and the persist files were exercised against a live kernel.
The installer builds with the rustc in ports, which is the version Cargo.toml pins, and keeps the crate cache and target tree under /usr/local: on a default disklayout / is under a gigabyte, and /usr/local is the only partition mounted wxallowed. PFUI_BINARY installs a prebuilt artifact instead, for a firewall that should not carry a toolchain. An existing /etc/pfui_firewall.yml is kept rather than replaced. This daemon reads the same keys, so a working deployment's settings are still correct and overwriting them with the example would break a live firewall. Group membership is merged rather than substituted, because usermod -G replaces the set and an upgrade finds the account already in another group. The CI job runs on the toolchain OpenBSD ships, so code needing a newer compiler fails there rather than on a firewall. framing.tsv now runs in three implementations and messages.json in two. The end-to-end test runs the real resolver against this daemon over both transports at once, with real lz4 on the wire, real Redis, and a stateful pfctl stub standing in for a kernel Linux does not have.
bind_unix narrows the process umask, which is global, so a directory created by a parallel test inside that window came out without its execute bit and nothing could be bound in it. Three tests failed for it on a non-root runner, while the same tests passed as root, which ignores the missing bit.
The line reported addresses and TTLs without saying which answer they came
from, and a TTL cannot be read without the kind: it is seconds remaining
under rr and an absolute expiry under cache.
PF Table updated for www.bing.com. (rr): AF4 23.55.129.60=300 AF6 none
An exception reaching pythonmod fails the query, so a PFUI fault takes DNS resolution with it. operate now catches everything, keeps the query moving, and logs the type, message and call site. pythonmod's own reporter cannot always import io to format a traceback and then reports that instead of the fault, which left an error visible with no way to tell what raised it. The call site is built from the traceback object with no imports at all.
Complete firewall and resolver configs for a TCP socket to a resolver on another host, for a unix socket to one on the firewall itself, and for a CARP node serving both at once, with the pf.conf rule the network transport needs.
Re-running an installer is how the code is upgraded, so it must not disturb a working deployment. Both installers now keep /etc/pfui_firewall.yml, pfui_unbound.yml and the resolver's pfui_unbound.conf as they are, take a timestamped backup, and name the shipped example to diff against for keys added since. Only a first install lays the examples down. This also removes the two prompts that asked whether to replace a config, which made an unattended upgrade impossible to answer safely.
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.
Implement PFUI server in RUST