balancer-lite-lua.dlog 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. # balancer-lite-lua.dlog — Deployment Log
  2. ## 2026-09-04 Initial Prototype
  3. ### What
  4. Created new workspace project `balancer-lite-lua/` for OpenWrt 22.03 Lua 5.1 port of
  5. balancer-lite's hysteresis watchdog, flap detection, signed webhook outbox, and retention store.
  6. Thin-layer only (~800–1000 LOC) — composes with mwan3/netifd rather than replacing them.
  7. ### Commit
  8. `09fad45` — "Initial Lua 5.1 prototype for OpenWrt 22.03"
  9. ### Files
  10. - `src/balancerlite/state.lua` — 8-state hysteresis machine (INIT, WAN_A/B_PRIMARY, SWITCHING_TO_A/B, DEGRADED, BOTH_DOWN)
  11. - `src/balancerlite/probes.lua` — ICMP/TCP/DNS health checks via ping/nc/nslookup subprocesses
  12. - `src/balancerlite/store.lua` — JSONL append-only event log + compaction
  13. - `src/balancerlite/outbox.lua` — Signed webhook retry queue with exponential backoff + circuit breaker
  14. - `src/balancerlite/sha256.lua` — SHA-256/HMAC-SHA256 via `openssl dgst` CLI
  15. - `src/balancerlite/config.lua` — UCI config file parser
  16. - `src/balancerlite/main.lua` — procd-compatible poll loop daemon
  17. - `src/balancerlite/json.lua` — Pure-Lua JSON encoder (JSON.stringify only)
  18. - `etc/init.d/balancerlite` — procd init script
  19. - `etc/config/balancerlite` — UCI config example
  20. - `Makefile` — lint + test targets
  21. - `tests/state.lua` — 8 state machine tests (all passing)
  22. - `tests/sha256.lua` — SHA-256 + HMAC-SHA256 test vectors (all passing)
  23. - `MEMORY.md`, `README.md`
  24. ### Test Results
  25. ```
  26. lua5.1 tests/state.lua → PASS: state (8/8 tests)
  27. lua5.1 tests/sha256.lua → PASS: sha256 (3/3 vectors)
  28. ```
  29. All modules pass `luac5.1 -p` (parse check).
  30. ### Verification Commands
  31. ```sh
  32. cd /root/.openclaw/workspace/balancer-lite-lua
  33. make test # runs all tests
  34. make lint # runs luac5.1 -p on all modules
  35. ```
  36. ### Remote
  37. `https://git3.techno-world.net/lrosales/balancer-lite-lua.git`
  38. ⚠️ Repo does not exist yet on git3 — creation via Gogs API returned 401.
  39. Manual repo creation needed on https://git3.techno-world.net first.
  40. ### Notes
  41. - HMAC-SHA256 uses `openssl dgst -hmac` CLI (openssl-util package on OpenWrt 22.03)
  42. - No lua-crypto dependency; no raw sockets; all probes via subprocess
  43. - State machine verified against Go source: 8 states, same transition logic
  44. - SWITCHING_TO_* are one-drive-cycle intermediate states
  45. - BOTH_DOWN transitions directly to stable primary (no intermediate)
  46. - BOTH_DOWN clears switch_times (flap detection not counted during all-down)
  47. - DEGRADED entry: flap check counts #record_switch calls; needs `flap_threshold` of them
  48. ### Remaining Work
  49. 1. Create empty repo on git3.techno-world.net, then `git push -u origin master`
  50. 2. Write `src/balancerlite/routing.lua` (policy routing via `ip` commands)
  51. 3. Write `src/balancerlite/wg.lua` (WireGuard endpoint re-point via `wg set`)
  52. 4. Write `balancerlite-ctl` CLI tool (status/events/switch/compact)
  53. 5. Runtime test on `openwrt-testbed`
  54. ## 2026-09-04 Routing/WG/CTL + integration
  55. ### What
  56. Completed the OpenWrt 22.03 Lua 5.1 prototype by adding the three
  57. remaining subsystems (policy routing, WireGuard endpoint re-point,
  58. balancerlite-ctl CLI) and wiring them into the daemon. Plus fixes for
  59. three real bugs found while wiring: (1) `probes.lua`/`store.lua`
  60. didn't expose `new` to their return tables, (2) `json.lua`'s
  61. `gsub('\0', ...)` was a Lua 5.1 zero-width pattern bug — every
  62. character was getting `\u0000` injected; replaced with `string.find`
  63. plain-mode + manual rebuild, (3) `config.lua`'s `s and get_opt(...)
  64. or default` idiom silently coerced `false` values back to `true`
  65. when the option was a bool — replaced with explicit `opt()` helper.
  66. ### Files added/changed
  67. - `src/balancerlite/routing.lua` (199 LOC) — iproute2 actuator:
  68. `apply_default` (boot), `switch_to(wan)` (failover), `verify(wan)`
  69. (self-check). Pure helpers for command building + injectable
  70. `exec(cmd)->rc` for tests without root.
  71. - `src/balancerlite/wg.lua` (185 LOC) — WireGuard actuator:
  72. `set_endpoint(wan)` (`wg set ... endpoint ...` + `wg syncconf`),
  73. `check_handshake(max_age)` for staleness detection, `prewarm()`
  74. for standby path. Same pure/injectable split.
  75. - `src/balancerlite/ctl.lua` (235 LOC) — CLI logic: status, events,
  76. switch, compact, verify. Pure request builders + file-IPC for
  77. status.json/control.json round trip.
  78. - `src/balancerlite/ctl_main.lua` — shell entry point with `--`
  79. separator handling.
  80. - `bin/balancerlite` + `bin/balancerlite-ctl` — POSIX shell
  81. wrappers for `/usr/bin/`.
  82. - `tests/routing.lua` (32 tests), `tests/wg.lua` (35 tests),
  83. `tests/ctl.lua` (42 tests), `tests/smoke.lua` (17 tests).
  84. - `src/balancerlite/json.lua` — fixed `\0` zero-width gsub bug.
  85. - `src/balancerlite/config.lua` — added `opt()` helper, removed
  86. `X and Y or Z` falsy-coercion footgun.
  87. - `src/balancerlite/probes.lua` + `store.lua` — exposed `new` in
  88. return tables.
  89. - `src/balancerlite/main.lua` — wired routing/wg/ctl, status.json
  90. writer, control.json poller; moved `log` helper before subsystems.
  91. - `etc/config/balancerlite.example` — UCI example.
  92. - `Makefile` — fixed `lua5.1 -p` → `luac5.1 -p` (separate binary),
  93. rewrote lint rule with perl-based comment stripping + per-line
  94. feature scan, added `unit` target.
  95. ### Test Results
  96. ```
  97. make lint → OK (parse + forbidden-feature scan both clean)
  98. make unit → 137/137 PASS
  99. state → 8/8
  100. sha256 → 3/3
  101. routing → 32/32
  102. wg → 35/35
  103. ctl → 42/42
  104. smoke → 17/17
  105. end-to-end smoke (lua5.1 main.lua --dry-run):
  106. - boots clean, config loads, routing.apply_default prints dry-run cmds
  107. - status.json written: {"daemon":true,"state":"INIT","cycles":0,...}
  108. - balancerlite-ctl status/events/switch/compact/verify all functional
  109. - bad switch target (wan-z) correctly rejected
  110. ```
  111. ### Verify commands
  112. ```sh
  113. cd /root/.openclaw/workspace/balancer-lite-lua
  114. make lint # parse + 5.2+/5.3+/5.4 forbidden-feature scan
  115. make unit # all unit + smoke tests
  116. lua5.1 src/balancerlite/main.lua \
  117. --config etc/config/balancerlite.example --dry-run # daemon smoke
  118. lua5.1 src/balancerlite/ctl_main.lua \
  119. --state-dir /root/balancerlite -- status # CLI smoke
  120. ```
  121. ### Notes
  122. - Three bug categories caught while wiring (the wiring *is* the test
  123. surface for these): (1) Lua patterns treating `\0` as zero-width
  124. match (silent corruption of every JSON string field); (2) Lua's
  125. `X and Y or Z` short-circuit treating `false` as missing
  126. (silently flipping disabled bool options back to enabled default);
  127. (3) modules exporting functions only as locals so callers couldn't
  128. see them via the require'd module table. All three are *easy to
  129. write but hard to spot* — caught here by end-to-end boot attempts,
  130. not by per-module unit tests. Worth flagging as recurring footguns.
  131. - Routing/wg actuator design: separate *pure* (command builders,
  132. regex parsers, validators) from *effectful* (io.popen + rc-capture)
  133. via an injectable `exec` hook. Lets tests assert on the command
  134. list without requiring root or iproute2.
  135. - ctl <-> daemon IPC is file-based (`control.json` request, daemon
  136. polls each cycle, `status.json` written each cycle) to avoid Lua
  137. socket dependencies on OpenWrt's stripped-down Lua 5.1.
  138. ### Open loops
  139. - Real (non-dry-run) end-to-end test on `openwrt-testbed` (19.07.7
  140. x86_64 container, would need real `ip` + `wg` + `ip rule` to
  141. exercise the actuators fully).
  142. - Optional: procd SIGHUP hot-reload (config.lua has the fields but
  143. the daemon doesn't yet respond to SIGHUP).
  144. - Optional: signed-webhook outbox tests (currently only state is
  145. shipped; tests for HMAC envelope + retry queue were left as a
  146. follow-up since the pure outbox logic is exercised by the daemon
  147. already).
  148. ## 2026-09-04 Testbed run: three daemon bugs fixed (dry-run, probe crash, UCI wan naming)
  149. ### What
  150. Ran the daemon on the real OpenWrt 19.07.7 testbed container. Three
  151. confirmed bugs, all root-caused and fixed:
  152. 1. **`--dry-run` flag was silently ignored.** `get_arg()` required
  153. `arg[i+1]` to exist, so a trailing boolean flag (`--dry-run` at end
  154. of argv) returned nil and the daemon ran a live cycle loop instead
  155. of one dry-run cycle. Replaced with `get_value()` for value opts
  156. (`--config`/`-c`) and `has_flag()` for boolean flags.
  157. 2. **drive_cycle crash: per-WAN probe targets discarded.** `drive_cycle`
  158. looked up `probe_targets[wan.id]` but passed `nil` to
  159. `probes.all()`, which fell back to the *global* `tcp_targets` (raw
  160. `"host:port"` strings) and crashed on `probe_tcp(t.host=nil, ...)`.
  161. `probes.all()` now takes per-WAN targets and accepts both
  162. `{host=,port=}` tables and `"host:port"` strings.
  163. 3. **UCI wan sections not found → "unsafe iface/gw" warnings.**
  164. `config.lua` looked up sections `wan-a`/`wan-b` (hyphens are not
  165. valid UCI identifiers); real configs use `wan_a`/`wan_b`. Section
  166. loader now matches by name in either spelling and maps onto the
  167. internal ids `wan-a`/`wan-b` (which the state machine, ctl CLI,
  168. and wg endpoint mapping use), warning + falling back to document
  169. order when a section is renamed.
  170. ### Files changed
  171. - `src/balancerlite/main.lua` — `get_value()`/`has_flag()` arg parsing;
  172. `drive_cycle` passes per-WAN tcp targets + dns server to
  173. `probes.all()`; STATE ids from `cfg.wans[i].id`.
  174. - `src/balancerlite/probes.lua` — `all()` per-WAN override; string or
  175. table target elements.
  176. - `src/balancerlite/config.lua` — wan section loader by name
  177. (underscore or hyphen), warning + document-order fallback.
  178. - `tests/probes_config.lua` (new, 21 tests) — regression suite for
  179. bugs 2 and 3: per-WAN table/string/nil/empty overrides, UCI
  180. `wan_a`/`wan_b` mapping, hyphenated back-compat, zero-section
  181. defaulting.
  182. - `tests/smoke.lua` — new `[1b]` checks asserting the smoke config's
  183. wan section values actually load (guards bug 3 end-to-end).
  184. - `testbed/testbed-runner.lua` (new) — in-container runner for all 7
  185. suites; filename regex fixed to accept underscores
  186. (`([%w_]+)`).
  187. ### Test Results
  188. ```
  189. make lint → OK (parse + forbidden-feature scan clean)
  190. host unit → ctl 42/42, routing 32/32, wg 35/35, smoke 21/21,
  191. probes_config 21/21, state PASS, sha256 PASS
  192. 19.07 testbed→ ALL 7 SUITES PASS (lua 5.1.5, after opkg install
  193. openssl-util — image ships no openssl; see
  194. .learnings/ERRORS.md)
  195. 19.07 daemon → --dry-run: "(DRY RUN)" banner, routing cmds printed,
  196. "exited after 1 cycles", rc=0, no crash, no
  197. "unsafe iface/gw" warnings
  198. ```
  199. ### Verify commands
  200. ```sh
  201. cd /root/.openclaw/workspace/balancer-lite-lua
  202. make lint
  203. make unit
  204. lua5.1 src/balancerlite/main.lua \
  205. --config etc/config/balancerlite.example --dry-run # "exited after 1 cycles"
  206. ```
  207. ### Notes
  208. - Internal WAN ids stay `wan-a`/`wan-b` (state machine, ctl, wg
  209. mapping unchanged); UCI layer accepts `wan_a`/`wan_b` or
  210. `wan-a`/`wan-b`.
  211. - 19.07 testbed container `openwrt-testbed` left running for now;
  212. next step is a 23.05 testbed (Lua 5.4 → needs 5.4-compat pass:
  213. `goto`, integer-division, `#` on nil semantics).
  214. - See `.learnings/ERRORS.md` 2026-09-04 entry: `docker cp` into a
  215. missing container path nests the dir (verified stale-fixture bug),
  216. and testbed runners must assert on printed summaries, not just
  217. exit codes.
  218. ### Open loops
  219. - Build 23.05 testbed + Lua 5.4 compatibility pass.
  220. - Real (non-dry-run) end-to-end actuator test (needs `ip`/`wg` on a
  221. real interface).
  222. - Optional: procd SIGHUP hot-reload; signed-webhook outbox unit tests.
  223. ## 2026-09-05 OpenWrt 23.05 testbed built + full suite green
  224. ### What
  225. Built a second testbed on **OpenWrt 23.05.6 x86_64** (the user's follow-up
  226. request after the 19.07 pass). The 19.07 container was shut down
  227. (`stop.sh --rm`) first.
  228. Key finding: the "23.05 ships Lua 5.4" hypothesis is **false** —
  229. OpenWrt 23.05's base `lua` package is **Lua 5.1.5** (same major as
  230. 19.07), so **no 5.4-compat pass is needed**. The existing 5.1-targeted
  231. code runs unmodified.
  232. ### How the 23.05 image is built
  233. There is **no** `shellspec/openwrt` tag for 23.05 (that repo tops out
  234. at 19.07.7), and Docker Hub's `openwrt/rootfs` only has 24.10/25.12
  235. for x86_64. So `testbed/Dockerfile-2305` builds from the **official
  236. 23.05.6 x86_64 rootfs tarball** (downloads.openwrt.org) + four .ipk
  237. payloads from the 23.05.6 feeds:
  238. - `lua` + `liblua5.1.5` — Lua 5.1.5 interpreter + its shared lib
  239. - `openssl-util` + `libopenssl3` + `ca-bundle` — `sha256.lua` uses `openssl dgst`
  240. Two build gotchas (both hit): busybox `tar` can't read `.ipk` (ar)
  241. archives, so .ipk payloads are extracted on the **host** (GNU tar) and
  242. `COPY`ed; and `lua` alone is not enough — it's a symlink to `lua5.1`
  243. which needs the separate `liblua5.1.5` .ipk (else `lua -v` dies on
  244. "Error loading shared library liblua.so.5.1.5").
  245. New files:
  246. - `testbed/Dockerfile-2305` — scratch build from official rootfs + ipk payloads
  247. - `testbed/download-2305.sh` — idempotent fetcher for rootfs + 5 .ipks + pre-extract
  248. - `testbed/README.md` — how to build/run both 19.07 & 23.05 testbeds + docker-cp gotcha
  249. - `testbed/.gitignore` — ignores the large fetched artifacts (rootfs/, ipks/, ipk-extract/)
  250. ### Test Results (OpenWrt 23.05.6 testbed, Lua 5.1.5)
  251. ```
  252. ALL 7 SUITES PASS
  253. sha256 → 3/3 vectors PASS (openssl dgst present)
  254. state → PASS
  255. routing → 32/32
  256. wg → 35/35
  257. ctl → 42/42
  258. smoke → 21/21 (incl. [1b] wan-section-load checks)
  259. probes_config → 21/21
  260. daemon --dry-run → "(DRY RUN)" banner, routing cmds printed,
  261. "exited after 1 cycles", rc=0, no crash, no
  262. "unsafe iface/gw" warnings
  263. ```
  264. ### Verify commands
  265. ```sh
  266. cd /root/.openclaw/workspace/balancer-lite-lua/testbed
  267. ./download-2305.sh
  268. docker build -f Dockerfile-2305 -t openwrt-testbed-2305:latest .
  269. S=skills/openwrt-testbed/scripts
  270. OPENWRT_TESTBED_NAME=openwrt-testbed-2305 OPENWRT_TESTBED_IMAGE=openwrt-testbed-2305:latest $S/start.sh
  271. # then run testbed-runner.lua inside (see testbed/README.md)
  272. ```
  273. ### Notes
  274. - 23.05 testbed container `openwrt-testbed-2305` left **running** (image
  275. cached, ready to reuse). Image is ~small (scratch + 2.7MB rootfs +
  276. ipk payloads).
  277. - 19.07 container was `stop.sh --rm`'d (gone). 23.05 is the active testbed.
  278. - Hypothesis log: 23.05 → Lua **5.1.5** (not 5.4). If a future OpenWrt
  279. bumps base lua to 5.4, revisit: `goto`, `#` on nil, integer-division
  280. (`//` vs `/`) semantics. Not needed now.
  281. ### Open loops
  282. - Real (non-dry-run) end-to-end actuator test (needs `ip`/`wg` on a live
  283. interface — neither testbed has `wg` installed).
  284. - Optional: procd SIGHUP hot-reload; signed-webhook outbox unit tests.