| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- # 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).
- ## 2026-09-04 Testbed run: three daemon bugs fixed (dry-run, probe crash, UCI wan naming)
- ### What
- Ran the daemon on the real OpenWrt 19.07.7 testbed container. Three
- confirmed bugs, all root-caused and fixed:
- 1. **`--dry-run` flag was silently ignored.** `get_arg()` required
- `arg[i+1]` to exist, so a trailing boolean flag (`--dry-run` at end
- of argv) returned nil and the daemon ran a live cycle loop instead
- of one dry-run cycle. Replaced with `get_value()` for value opts
- (`--config`/`-c`) and `has_flag()` for boolean flags.
- 2. **drive_cycle crash: per-WAN probe targets discarded.** `drive_cycle`
- looked up `probe_targets[wan.id]` but passed `nil` to
- `probes.all()`, which fell back to the *global* `tcp_targets` (raw
- `"host:port"` strings) and crashed on `probe_tcp(t.host=nil, ...)`.
- `probes.all()` now takes per-WAN targets and accepts both
- `{host=,port=}` tables and `"host:port"` strings.
- 3. **UCI wan sections not found → "unsafe iface/gw" warnings.**
- `config.lua` looked up sections `wan-a`/`wan-b` (hyphens are not
- valid UCI identifiers); real configs use `wan_a`/`wan_b`. Section
- loader now matches by name in either spelling and maps onto the
- internal ids `wan-a`/`wan-b` (which the state machine, ctl CLI,
- and wg endpoint mapping use), warning + falling back to document
- order when a section is renamed.
- ### Files changed
- - `src/balancerlite/main.lua` — `get_value()`/`has_flag()` arg parsing;
- `drive_cycle` passes per-WAN tcp targets + dns server to
- `probes.all()`; STATE ids from `cfg.wans[i].id`.
- - `src/balancerlite/probes.lua` — `all()` per-WAN override; string or
- table target elements.
- - `src/balancerlite/config.lua` — wan section loader by name
- (underscore or hyphen), warning + document-order fallback.
- - `tests/probes_config.lua` (new, 21 tests) — regression suite for
- bugs 2 and 3: per-WAN table/string/nil/empty overrides, UCI
- `wan_a`/`wan_b` mapping, hyphenated back-compat, zero-section
- defaulting.
- - `tests/smoke.lua` — new `[1b]` checks asserting the smoke config's
- wan section values actually load (guards bug 3 end-to-end).
- - `testbed/testbed-runner.lua` (new) — in-container runner for all 7
- suites; filename regex fixed to accept underscores
- (`([%w_]+)`).
- ### Test Results
- ```
- make lint → OK (parse + forbidden-feature scan clean)
- host unit → ctl 42/42, routing 32/32, wg 35/35, smoke 21/21,
- probes_config 21/21, state PASS, sha256 PASS
- 19.07 testbed→ ALL 7 SUITES PASS (lua 5.1.5, after opkg install
- openssl-util — image ships no openssl; see
- .learnings/ERRORS.md)
- 19.07 daemon → --dry-run: "(DRY RUN)" banner, routing cmds printed,
- "exited after 1 cycles", rc=0, no crash, no
- "unsafe iface/gw" warnings
- ```
- ### Verify commands
- ```sh
- cd /root/.openclaw/workspace/balancer-lite-lua
- make lint
- make unit
- lua5.1 src/balancerlite/main.lua \
- --config etc/config/balancerlite.example --dry-run # "exited after 1 cycles"
- ```
- ### Notes
- - Internal WAN ids stay `wan-a`/`wan-b` (state machine, ctl, wg
- mapping unchanged); UCI layer accepts `wan_a`/`wan_b` or
- `wan-a`/`wan-b`.
- - 19.07 testbed container `openwrt-testbed` left running for now;
- next step is a 23.05 testbed (Lua 5.4 → needs 5.4-compat pass:
- `goto`, integer-division, `#` on nil semantics).
- - See `.learnings/ERRORS.md` 2026-09-04 entry: `docker cp` into a
- missing container path nests the dir (verified stale-fixture bug),
- and testbed runners must assert on printed summaries, not just
- exit codes.
- ### Open loops
- - Build 23.05 testbed + Lua 5.4 compatibility pass.
- - Real (non-dry-run) end-to-end actuator test (needs `ip`/`wg` on a
- real interface).
- - Optional: procd SIGHUP hot-reload; signed-webhook outbox unit tests.
|