# balancer-lite-lua.dlog — Deployment Log ## 2026-09-04 Initial Prototype ### What Created new workspace project `balancer-lite-lua/` for OpenWrt 22.03 Lua 5.1 port of balancer-lite's hysteresis watchdog, flap detection, signed webhook outbox, and retention store. Thin-layer only (~800–1000 LOC) — composes with mwan3/netifd rather than replacing them. ### Commit `09fad45` — "Initial Lua 5.1 prototype for OpenWrt 22.03" ### Files - `src/balancerlite/state.lua` — 8-state hysteresis machine (INIT, WAN_A/B_PRIMARY, SWITCHING_TO_A/B, DEGRADED, BOTH_DOWN) - `src/balancerlite/probes.lua` — ICMP/TCP/DNS health checks via ping/nc/nslookup subprocesses - `src/balancerlite/store.lua` — JSONL append-only event log + compaction - `src/balancerlite/outbox.lua` — Signed webhook retry queue with exponential backoff + circuit breaker - `src/balancerlite/sha256.lua` — SHA-256/HMAC-SHA256 via `openssl dgst` CLI - `src/balancerlite/config.lua` — UCI config file parser - `src/balancerlite/main.lua` — procd-compatible poll loop daemon - `src/balancerlite/json.lua` — Pure-Lua JSON encoder (JSON.stringify only) - `etc/init.d/balancerlite` — procd init script - `etc/config/balancerlite` — UCI config example - `Makefile` — lint + test targets - `tests/state.lua` — 8 state machine tests (all passing) - `tests/sha256.lua` — SHA-256 + HMAC-SHA256 test vectors (all passing) - `MEMORY.md`, `README.md` ### Test Results ``` lua5.1 tests/state.lua → PASS: state (8/8 tests) lua5.1 tests/sha256.lua → PASS: sha256 (3/3 vectors) ``` All modules pass `luac5.1 -p` (parse check). ### Verification Commands ```sh cd /root/.openclaw/workspace/balancer-lite-lua make test # runs all tests make lint # runs luac5.1 -p on all modules ``` ### Remote `https://git3.techno-world.net/lrosales/balancer-lite-lua.git` ⚠️ Repo does not exist yet on git3 — creation via Gogs API returned 401. Manual repo creation needed on https://git3.techno-world.net first. ### Notes - HMAC-SHA256 uses `openssl dgst -hmac` CLI (openssl-util package on OpenWrt 22.03) - No lua-crypto dependency; no raw sockets; all probes via subprocess - State machine verified against Go source: 8 states, same transition logic - SWITCHING_TO_* are one-drive-cycle intermediate states - BOTH_DOWN transitions directly to stable primary (no intermediate) - BOTH_DOWN clears switch_times (flap detection not counted during all-down) - DEGRADED entry: flap check counts #record_switch calls; needs `flap_threshold` of them ### Remaining Work 1. Create empty repo on git3.techno-world.net, then `git push -u origin master` 2. Write `src/balancerlite/routing.lua` (policy routing via `ip` commands) 3. Write `src/balancerlite/wg.lua` (WireGuard endpoint re-point via `wg set`) 4. Write `balancerlite-ctl` CLI tool (status/events/switch/compact) 5. Runtime test on `openwrt-testbed` ## 2026-09-04 Routing/WG/CTL + integration ### What Completed the OpenWrt 22.03 Lua 5.1 prototype by adding the three remaining subsystems (policy routing, WireGuard endpoint re-point, balancerlite-ctl CLI) and wiring them into the daemon. Plus fixes for three real bugs found while wiring: (1) `probes.lua`/`store.lua` didn't expose `new` to their return tables, (2) `json.lua`'s `gsub('\0', ...)` was a Lua 5.1 zero-width pattern bug — every character was getting `\u0000` injected; replaced with `string.find` plain-mode + manual rebuild, (3) `config.lua`'s `s and get_opt(...) or default` idiom silently coerced `false` values back to `true` when the option was a bool — replaced with explicit `opt()` helper. ### Files added/changed - `src/balancerlite/routing.lua` (199 LOC) — iproute2 actuator: `apply_default` (boot), `switch_to(wan)` (failover), `verify(wan)` (self-check). Pure helpers for command building + injectable `exec(cmd)->rc` for tests without root. - `src/balancerlite/wg.lua` (185 LOC) — WireGuard actuator: `set_endpoint(wan)` (`wg set ... endpoint ...` + `wg syncconf`), `check_handshake(max_age)` for staleness detection, `prewarm()` for standby path. Same pure/injectable split. - `src/balancerlite/ctl.lua` (235 LOC) — CLI logic: status, events, switch, compact, verify. Pure request builders + file-IPC for status.json/control.json round trip. - `src/balancerlite/ctl_main.lua` — shell entry point with `--` separator handling. - `bin/balancerlite` + `bin/balancerlite-ctl` — POSIX shell wrappers for `/usr/bin/`. - `tests/routing.lua` (32 tests), `tests/wg.lua` (35 tests), `tests/ctl.lua` (42 tests), `tests/smoke.lua` (17 tests). - `src/balancerlite/json.lua` — fixed `\0` zero-width gsub bug. - `src/balancerlite/config.lua` — added `opt()` helper, removed `X and Y or Z` falsy-coercion footgun. - `src/balancerlite/probes.lua` + `store.lua` — exposed `new` in return tables. - `src/balancerlite/main.lua` — wired routing/wg/ctl, status.json writer, control.json poller; moved `log` helper before subsystems. - `etc/config/balancerlite.example` — UCI example. - `Makefile` — fixed `lua5.1 -p` → `luac5.1 -p` (separate binary), rewrote lint rule with perl-based comment stripping + per-line feature scan, added `unit` target. ### Test Results ``` make lint → OK (parse + forbidden-feature scan both clean) make unit → 137/137 PASS state → 8/8 sha256 → 3/3 routing → 32/32 wg → 35/35 ctl → 42/42 smoke → 17/17 end-to-end smoke (lua5.1 main.lua --dry-run): - boots clean, config loads, routing.apply_default prints dry-run cmds - status.json written: {"daemon":true,"state":"INIT","cycles":0,...} - balancerlite-ctl status/events/switch/compact/verify all functional - bad switch target (wan-z) correctly rejected ``` ### Verify commands ```sh cd /root/.openclaw/workspace/balancer-lite-lua make lint # parse + 5.2+/5.3+/5.4 forbidden-feature scan make unit # all unit + smoke tests lua5.1 src/balancerlite/main.lua \ --config etc/config/balancerlite.example --dry-run # daemon smoke lua5.1 src/balancerlite/ctl_main.lua \ --state-dir /root/balancerlite -- status # CLI smoke ``` ### Notes - Three bug categories caught while wiring (the wiring *is* the test surface for these): (1) Lua patterns treating `\0` as zero-width match (silent corruption of every JSON string field); (2) Lua's `X and Y or Z` short-circuit treating `false` as missing (silently flipping disabled bool options back to enabled default); (3) modules exporting functions only as locals so callers couldn't see them via the require'd module table. All three are *easy to write but hard to spot* — caught here by end-to-end boot attempts, not by per-module unit tests. Worth flagging as recurring footguns. - Routing/wg actuator design: separate *pure* (command builders, regex parsers, validators) from *effectful* (io.popen + rc-capture) via an injectable `exec` hook. Lets tests assert on the command list without requiring root or iproute2. - ctl <-> daemon IPC is file-based (`control.json` request, daemon polls each cycle, `status.json` written each cycle) to avoid Lua socket dependencies on OpenWrt's stripped-down Lua 5.1. ### Open loops - Real (non-dry-run) end-to-end test on `openwrt-testbed` (19.07.7 x86_64 container, would need real `ip` + `wg` + `ip rule` to exercise the actuators fully). - Optional: procd SIGHUP hot-reload (config.lua has the fields but the daemon doesn't yet respond to SIGHUP). - Optional: signed-webhook outbox tests (currently only state is shipped; tests for HMAC envelope + retry queue were left as a follow-up since the pure outbox logic is exercised by the daemon already).