-
Notifications
You must be signed in to change notification settings - Fork 0
Installing on OPNsense
Tier 1 port, built every release between the BSD packages and OpenWrt. It is a
proper plugin — a page under Services, service control through configd, an rc
script — not a bare pkg add of the FreeBSD package.
Three things about OPNsense, each checked against their repository rather than assumed. They are the whole reason this port has its own build.
| Finding | Consequence | |
|---|---|---|
| ABI | OPNsense 24.7 → 25.7 are all FreeBSD:14:amd64
|
our FreeBSD VM is 15.1 → FreeBSD:15:amd64. pkg refuses it outright, and forcing it would install extensions linked against 15-era libraries. FreeBSD promises forward compatibility, not backward, so this is the wrong direction. Everything must build in a FreeBSD 14 userland. |
| Python | they ship python311 (3.11.14) | the venv must be 3.11. A 3.12 tree cannot import a single cpython-311.so. |
| Dependencies |
none of ours are packaged — no fastapi, uvicorn, pydantic, pydantic-core, starlette, msmart-ng, pycryptodome — and no rust, and no py311-pip
|
nothing can be compiled or fetched on the firewall. The plugin must vendor a complete tree and depend on python311 alone. |
They do ship py311-cryptography, py311-sqlite3 and py311-setuptools, which
is not enough to matter: without pip there is no way to install the rest anyway.
os-breeze-core-<ver>.pkg ABI FreeBSD:14:amd64, depends: python311
├── /usr/local/lib/breeze-core/ vendored venv + meow_ac + static (~19 MB)
│ └── serve.sh service launcher (sets its own environment)
├── /usr/local/bin/breeze-core CLI wrapper (setup / approve / diag)
├── /usr/local/etc/rc.d/breeze_core
└── /usr/local/opnsense/ MVC page, menu, ACL, configd actions, template
The venv is built at its final path inside the chroot, because a venv bakes
absolute paths into pyvenv.cfg and every console script — building elsewhere
and relocating gives a tree that only appears to work.
Settings kept deliberately small: enabled, listen address, port. The API key
and the paired-unit list live in Breeze Core's own config.json, written by its
panel and its pairing flow. Templating over that from the GUI would clobber
credentials on every save.
packaging/opnsense/build-plugin.sh # -> packaging/out/opnsense/os-breeze-core-<ver>.pkg
packaging/opnsense/verify-plugin.sh # installs it into a clean FreeBSD 14 rootThe verifier deliberately reproduces what OPNsense actually offers: a FreeBSD 14 userland with python311 and nothing else — no rust, no pip, no compiler. If the package needs any of them at install time, it is broken for the target and this catches it.
What cannot be verified here: the GUI. The MVC page, menu entry and configd wiring need OPNsense's own PHP stack, so the PHP is lint-checked and the XML parsed, but "the page renders and the toggle works" has to be confirmed on a real install. Anything below the GUI line — install, ABI, dependency resolution, the vendored runtime, the rc script, the HTTP endpoint — is verified.
The GUI is the part no chroot can exercise, so each place the plugin has to agree
with OPNsense's own PHP was read out of opnsense/core rather than assumed. All
four are cheap to get wrong and silent when wrong.
| What | The contract | Where it bites |
|---|---|---|
| Rendered settings |
load_rc_config() sources ${_d}/rc.conf.d/$name for _d in /etc and ${local_startup%/rc.d} = /usr/local/etc, and nowhere else |
the template must land on /usr/local/etc/rc.conf.d/breeze_core. Anywhere tidier and the rc script keeps its built-in defaults: the GUI's address and port never reach uvicorn, with no error anywhere, because every value is still valid |
| Service buttons |
updateServiceControlUI(name) fills #service_status_container, which layouts/default.volt already provides beside the page title |
the page needs no markup of its own — its absence is correct, not an omission |
| Status light |
statusAction() is strpos($configd_output, 'is running') / 'not running'
|
the rc script's wording is an API. Reword it and the service works while the GUI shows "unknown" forever |
| Save → apply |
reconfigureAction() calls stop, template reload <template>, then start or reload
|
a reload configd action has to exist. uvicorn has no config-reload signal, so it is honestly mapped to onerestart
|
The status wording is asserted by verify-plugin.sh — it greps the rc script's
own output for the two strings the PHP looks for, which is the one GUI contract
that can be checked without a GUI. The rendered-settings path is covered too: the
rc test writes what configd would render and passes no environment, so the
service must pick the port up from the file or the HTTP check fails.
Recorded because none of them announce themselves, and several look like a different problem entirely.
-
devfs must be mounted in the chroot. Without it cargo fails absurdly: it
pipes source to
rustc -on stdin, that read misbehaves with no/dev, and rustc ends up parsing an error message as source — surfacing asE0554: #![feature] may not be used on the stable release channel, which sends you hunting for a nightly/toolchain problem that does not exist (rustc was 1.96.1). -
pkg createneeds an explicit plist. Given only-Mand-rit packages the manifest and nothing else, exits 0, and hands you a 1160-byte "package". -
Stage only our own tree. Copying
$ROOT/usr/local/libwholesale drags in everythingpkginstalled in the build root, rust's libraries included: a 168 MB plugin from a 19 MB payload. -
daemon -udrops privileges before writing the pidfile, so a pidfile in/var/runfails withppidfile: Permission denied. It lives in/var/run/breeze_core/, created owned by the service account. -
daemon -ualso wipes the environment (it callssetusercontext), soPYTHONPATHandAC_CONFIGexported fromstart_precmdnever reach uvicorn and it dies withModuleNotFoundErrorbefore anything is logged.serve.shsets its own environment for that reason. -
daemon -cchdirs to/, so relying on cwd for the import path does not work either — hencePYTHONPATH, notcd. -
daemon -fsends the failure to/dev/null. Every one of the above looked identical from the outside:breeze_core is not running. Debug with-o /tmp/x.log, or the cause is invisible. -
meow_achas no__main__— the CLI module ismeow_ac.cli, which is what the BSD installer uses.-m meow_acfails outright, and the GUI page tells the admin to runbreeze-core pair. - The source tar is built on Windows, which records no POSIX execute bit, so anything that must be executable is chmod-ed explicitly in the build.
-
Test on a port that cannot collide. The builder VM may be running Breeze
Core itself from the FreeBSD package work; a chroot shares the host network
stack, so
8420givesaddress already in useand looks like a broken rc script. The verifier uses 18420. -
Tearing down a FreeBSD test root needs
umountof devfs first andchflags -R noschg— base files like/sbin/initandlibexec/ld-elf.so.1are immutable andrmfails as root without it. -
${name}_useris rc.subr magic. Settingbreeze_core_usermakes rc.subr wrap the command insu -m, which then collides withdaemon -uand the service never starts. The knob is namedbreeze_core_runasfor that reason. -
procnamemust be/usr/sbin/daemon, not the interpreter: the pid in the pidfile is daemon's, so with the interpreter asprocnamethe status check compares the wrong binary and always says "not running" — while the service is up and serving. -
pkillunprivileged fails silently. The service runs as its own account, so a barepkillgets "Operation not permitted",|| trueswallows it, and the stray from the previous run keeps the port. The next run then fails to bind and the stray answers the HTTP check — a 500 from a server whose tree was deleted underneath it, which reads as a broken package. Kill withdoas, and matchlib/breeze-corespecifically: the builder also runs the FreeBSD package from/usr/local/breeze-core, and a loose pattern kills that too. -
pkill -f "$TEST"matches nothing. Inside the chroot the argv is/usr/local/lib/...— the test root's path appears nowhere in it. -
A failure between start and teardown poisons the next run. Under
set -ethe script exits before unmounting devfs, so the following run inherits a mounted root, andrm -rfrecurses into live devfs ("Operation not supported" per node, then "Device busy"). The rc block ends in|| badso teardown always runs, and teardown refuses tormwhile devfs is mounted. -
Don't use
fetch(1)for the HTTP check. Its--headersupport varies by release, and an unsupported option is indistinguishable from a dead server — this check printed nothing at all while the service was fine. The vendored interpreter is already there; use it. -
Run the probe from a file, not
python -c. It sits insidechroot ... sh -c "...", and a nested double quote ends the outer string, leaving python a bare-c: "Argument expected for the -c option". -
Don't hand the rc script its settings through the environment in tests.
Exporting
breeze_core_port=bypasses the entire config path, which is how a template rendered to a pathload_rc_confignever reads went unnoticed. The test renders the file and sets nothing.
# OPNsense shell, as root
fetch https://bolero.salataputarica.hr.eu.org/opnsense/os-breeze-core-latest.pkg
pkg add os-breeze-core-latest.pkgA version-stamped copy and .sha256 files sit alongside it in
/opnsense/. The plugin is
deliberately outside the signed FreeBSD repository: that repo is
FreeBSD:15:amd64 and this package is 14, so pkg would refuse it there.
Then:
- Services → Breeze Core in the GUI: set the listen address and enable it.
-
breeze-core pair— discover and pair the air conditioners. -
breeze-core approve— admit a phone or browser. Approval is LAN-only by design.
Keep the bind address on a LAN interface (or 127.0.0.1 behind the OPNsense reverse proxy for TLS). Do not expose it on WAN: pairing approval trusts the local network.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
Use it
Reference
Run it safely
Develop and port