Files
luci-app-zt-gateway/docs/PROGRESS.md
Malar Invention 581d625044 fix: routing ip rule missing after reboot, busybox compat, skill restructure
Routing fixes (2026-07-14):
- Add missing ip rule 'from 10.11.13.0/24 lookup 100' to hotplug ifup case
- Add UCI network rule persistence so netifd restores it on boot
- Verify ip rule exists in zt-gateway-switch do_force/do_graceful
- Fix BRIDGE_PORTS auto-detect: use /proc/net/dev instead of broken
  awk-over-ip pipeline (busybox awk mishandles exit in compound if)
- Validate bridge port candidate exists as network interface
- Fix setup-routing: use dev br-zt not dev ztX (ZT iface has no IP
  when enslaved to bridge, causing 'Nexthop has invalid gateway')
- Replace ip rule replace (GNU-only) with del+add for busybox

Infrastructure:
- Fix deploy:install stdin starvation: ssh/scp consume pipe data in
  find|while loop; add </dev/null to prevent truncation
- Move luci-dev skill from root/ to skills/ with .agents/skills/ symlink
- Add policy routing and busybox gotcha sections to SKILL.md
- Add diagnostics doc for the routing fix session
2026-07-14 18:01:30 +05:30

11 KiB

Progress: Playwright E2E Tests for luci-app-zt-gateway

Date: 2026-06-26/2026-06-27

Completed Fixes

1. login() Helper (e2e/utils/auth.ts)

  • Root cause: LuCI login form lives inside <section hidden>; the visible form is rendered by JS after ui.instantiateView('bootstrap.sysauth'). The raw #luci_username exists in hidden DOM before the view is ready.
  • Fix: After page.goto('/cgi-bin/luci'), wait for the spinner to disappear:
    await page.waitForFunction(() => {
      const view = document.querySelector('#view');
      return view && !view.querySelector('.spinning');
    }, { timeout: 30_000, polling: 500 });
    await page.waitForSelector('#luci_username', { state: 'visible', timeout: 30_000 });
    
  • Post-login navigation: LuCI with empty root password redirects to /cgi-bin/luci/ (not /cgi-bin/luci/admin/...). Changed waitForURL to match /cgi-bin/luci/, then wait for the admin page spinner to disappear.
  • resetToAmsterdam: Changed from page.evaluate(fetch) to page.request.post('/ubus') to avoid "Execution context destroyed" errors when the page navigates during test teardown.

2. overview.js Parenthesis Mismatch

  • Lines 67 & 208: _('UP (%sms').format(latencyMs) had a missing closing parenthesis.
  • Fix: Changed to _('UP (%sms)').format(latencyMs) in both locations.

3. Missing luci-compat Package

  • Symptom: After clicking login, the page returned a 500 error: module 'luci.ucodebridge' not found.
  • Cause: Modern OpenWrt snapshots moved luci.ucodebridge out of luci-base into a separate package luci-compat.
  • Fix:
    • Updated Dockerfile.openwrt to include luci-compat.
    • Because podman build in this environment cannot fetch packages at build time (network is blocked inside the build container), a new image was produced by:
      1. Running a temporary container with --network host --entrypoint sh from the existing image.
      2. Installing luci-compat (and its dependencies) inside the temp container using apk add.
      3. Committing the temp container to a new image tag.
      4. Replacing the localhost/zt-gateway-luci:dev tag with the committed image.

4. Missing Volume Mounts for Menu & ACL JSONs

  • Symptom: Navigating to /cgi-bin/luci/admin/services/zt-gateway returned 404: "No page is registered".
  • Fix: Added menu.d and acl.d mounts to docker-compose.yml.

5. Stale ubus Socket on Container Restart

  • Fix: Modified docker/openwrt-luci-entrypoint.sh to remove stale sockets and lock files before starting ubusd.

6. Network Setup (docker/openwrt-luci-entrypoint.sh)

  • Symptom: br-zt had no IPv4 address because ZTG_PING_IFACE=br-zt caused the entrypoint to look for the IP on br-zt before it was created.
  • Fix: Changed entrypoint to independently detect SOURCE_IFACE (the interface holding 10.99.12.x) and move its IP to BRIDGE (br-zt), regardless of ZTG_PING_IFACE.

7. UCI Config Not Writable

  • Symptom: uci commit zt-gateway failed with "I/O error". The backend could not persist active_gateway changes.
  • Root cause: File-level bind mount (./root/etc/config/zt-gateway:/etc/config/zt-gateway) prevents atomic rename on btrfs-backed host filesystem.
  • Fix: Changed compose to mount ./root/etc/config:/host-config:ro and added entrypoint logic to copy zt-gateway config from /host-config into the container's writable overlay at startup.

8. UCI Config IPs Updated

  • Updated root/etc/config/zt-gateway to use 10.99.12.x (test network) instead of 10.11.12.x.

9. Added State Reset to Test beforeEach

  • drain.spec.ts, switch.spec.ts, and health.spec.ts now call resetToAmsterdam(page) in both beforeEach and afterEach to ensure each test starts from a clean state.

Current Blocker: Extreme LuCI Cold-Load Latency

After installing luci-compat, login and page rendering work correctly, but first-load times are extremely long in fresh browser contexts:

Step Time (fresh context) Notes
Login form render ~11s Wait for bootstrap.sysauth instantiation
Admin overview render ~54s admin_status/index view compilation
Admin overview (2nd try) ~37s Partial cache warm-up
ZT gateway page render >120s* zt-gateway/overview view never renders

*The ZT gateway page spinner (Loading view…) persists beyond 120 seconds. Manual ui.instantiateView('zt-gateway/overview') returns instantly but does not replace the spinner—#zt-gateway-root is never created. There are no JavaScript console errors (sessionid and token are correctly set). The ubus status RPC returns correct data when called manually. Root cause of the silent render failure is unknown—possibly related to luci-compat's Lua compatibility mode altering the ui.instantiateView pipeline, or a race condition in the module loader that only manifests on cold loads.

Test Results

Spec Status Notes
login.spec.ts PASS (when run alone on warm container) Full form submission + admin navigation works.
overview.spec.ts PASS (when reached) Table, radios, active marker assertions pass.
switch.spec.ts UNKNOWN Suite times out before reaching it reliably.
drain.spec.ts FAIL beforeEach times out during login() cold load.
health.spec.ts UNKNOWN Reaches test body but suite often times out first.

Infrastructure State

  • Running image: localhost/zt-gateway-luci:dev (committed from temp container with luci-compat).
  • Container: openwrt-luci (from compose, uses staging-dir volume mounts).
  • Networks: luci-app-zt-gateway_zt-exit-net and luci-app-zt-gateway_zt-gateway-lan active.
  • br-zt: Has 10.99.12.2/24, routing works, ping to gateways succeeds.

Remaining Work

  1. Diagnose ZT page cold-load hang: Investigate why ui.instantiateView('zt-gateway/overview') silently fails to render on fresh browser contexts. Compare with a non-luci-compat image or test a vanilla LuCI page to isolate whether luci-compat is the cause.
  2. Evaluate test-timeout strategy: Options:
    • Increase playwright.config.ts timeout to 180s+ and accept 10+ minute suite runs.
    • Implement a globalSetup that logs in once via browser to warm the server cache, then share storageState across tests.
    • Revert luci-compat installation and find an alternative way to provide luci.ucodebridge (e.g., copy only the missing file into the image).
  3. Fix drain.spec.ts graceful drain panel: Once login is stable, verify zt-gateway-switch in graceful mode actually creates /var/run/zt-gateway-drain.pid; if not, the test expectation may need to be relaxed for the containerized environment.
  4. Run full suite end-to-end after the above blocker is resolved.
  5. Clean up debug scripts: debug-pw.js, debug-pw2.js, debug-pw3.js, debug-login-dom.js.
  6. Rebuild image properly once build-time network is restored so luci-compat is baked in without manual commits.

Date: 2026-07-13

Setup Wizard Implementation

Completed

  1. Created /usr/sbin/zt-gateway-setup (700+ lines) with subcommands:

    • status, setup-bridge, setup-routing, setup-dhcp, setup-wifi-ap, setup-hotplug, setup-persistence, setup-all
  2. Added setup ubus method to zt-gateway.uc with regex validation (ucode lacks Array.indexOf()).

  3. Added Setup panel to overview.js with buttons for each setup command.

  4. Auto-discover deploy taskdeploy:install now discovers files from root/ and htdocs/ instead of listing them individually.

Bugs Found and Fixed During Setup

  1. _cidr_to_mask produced 3 octets for /24 (255.255.255 instead of 255.255.255.0). Fixed by padding to 4 octets.

  2. UCI values had embedded quotesuci set "proto='static'" stored 'static' instead of static. Fixed by removing shell quotes.

  3. Bridge ports hardcoded to ztabc0 — ZeroTier interface names are randomized. Fixed by auto-detecting zt* interfaces.

  4. DHCP/WiFi AP referenced device name instead of interface name — dnsmasq and hostapd bind to interfaces, not devices. Fixed to use zt_wiblan.

  5. Firewall zone missing — nftables fw4 has policy drop. Fixed by adding zt_wiblan to the LAN zone.

  6. ZT interface lost IP when added to bridge — ARP responses failed. Fixed by ensuring ZT-assigned IP stays on the interface.

  7. Table 100 routed to self — Default route pointed to WIBLAN_GW (local) instead of exit gateway. Fixed by detecting exit gateway from UCI/routes.

  8. Bridge netmask /24 instead of /23 — Couldn't reach ZT subnet. Fixed by defaulting to /23 (255.255.254.0).

  9. ZeroTier Ethernet Bridging disabled — L2 frames couldn't traverse tunnel. Fixed by enabling in ZT network controller.

  10. DHCP range wrong in /23 — Clients got 10.11.12.x instead of 10.11.13.x. Fixed by calculating correct offset (356 for /23 base).

Key Learnings

  • OpenWrt UCI uses interface names (not device names) for DHCP and WiFi
  • Bridge netmask must be /23 to cover both ZT (10.11.12.x) and WIBLAN (10.11.13.x)
  • ZeroTier requires "Allow Ethernet Bridging" for L2 traffic
  • nftables fw4 zones must explicitly include bridge interfaces
  • Policy routing table 100 must route via exit gateway, not local IP

Date: 2026-07-14

Post-Reboot Routing Fixes

Problem

After rebooting the production OpenWrt router, WiBLAN clients (10.11.13.x) could not route traffic through the active exit gateway (Amsterdam, 10.11.12.3). The ip rule directing WIBLAN traffic to policy table 100 was missing entirely.

Root Cause

Three gaps in the hotplug/setup-routing/switch pipeline:

  1. Hotplug ifup case never installed the from 10.11.13.0/24 ip rule
  2. UCI persistence wrote routes but not the network rule section
  3. zt-gateway-switch never verified the rule existed

Fixes

  1. Added ip rule to hotplug heredoc (zt-gateway-setup)
  2. Added network.zt_wiblan_rule UCI section to setup-routing persistence
  3. Added rule verification to do_force()/do_graceful() in zt-gateway-switch

Additional fixes during the same session

  • BRIDGE_PORTS auto-detect: Replaced broken awk-over-ip pipeline with UCI lookup + /proc/net/dev fallback + interface existence validation (busybox compatibility)
  • Route dev param: Changed dev $BRIDGE_PORTS to dev $BRIDGE in setup-routing (ZT interface has no IP when enslaved to bridge)
  • ip rule replace: Replaced GNU-only ip rule replace with ip rule del + ip rule add for busybox compatibility
  • Deploy stdin starvation: Added </dev/null to ssh/scp in deploy:install pipe loop
  • Deploy overwriting UCI: Noted that deploy:install overwrites /etc/config/zt-gateway with repo version, clobbering production customizations (e.g. real Amsterdam IP 10.11.12.3 vs placeholder 10.99.12.3)