Files
claudetools/session-logs/2026-06/2026-06-21-howard-unifi-pfsense-control-verbs.md
Howard Enos aefbc8fce2 sync: auto-sync from HOWARD-HOME at 2026-06-21 16:14:48
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 16:14:48
2026-06-21 16:15:20 -07:00

16 KiB

Session — unifi-wifi pfSense SSH gateway-control verbs (ROADMAP §E)

User

  • User: Howard Enos (howard)
  • Machine: Howard-Home
  • Role: tech

Session Summary

Picked up the unifi-wifi skill build-out, specifically the open WIP in ROADMAP §E — the pfSense gateway "compatibility layer". The skill already had a working read-only SSH backend (pfsense-ssh.sh: audit/dhcp/run/shell) and a superseded REST backend (pfsense-backend.sh) that held the full control-verb contract. Per Mike's 2026-06-16 decision (SSH, not the REST API package), the remaining work was to implement gated CONTROL verbs on the SSH backend mirroring the gw-control contract, then rewire gw-audit.sh/gw-control.sh dispatch to the SSH backend.

Before writing code, ran read-only discovery against the live Cascades pfSense (Plus 25.07-RELEASE, reachable at 192.168.0.1 over the Cascades site VPN) to pin the exact config schema rather than guess. Discovery established: pfSsh.php does NOT eval piped ad-hoc code (only its built-in playback scripts), so the reliable primitive is php with a require_once("config.inc") bootstrap, which loads $config + write_config() + filter_configure(). Filter rules are keyed on tracker (the id field is empty on 25.07); enabled/disabled is the PRESENCE of a disabled key. easyrule supports block/unblock/showblock <interface> <source> and auto-manages its alias

  • WAN rule. Cascades has 20 filter rules and 0 NAT port-forwards.

Built a new versioned PHP helper scripts/pfsense-gwc.php (argv-driven, no operator data interpolated into PHP source) for list/toggle/set/delete against $config, with a config backup + write_config() + filter_configure() on every write. Extended pfsense-ssh.sh with 12 verbs (pf-list, fw-list, showblock, pf-disable/enable/delete, pf-set-ports, pf-set-src, fw-disable/enable, block-ips, unblock) — DRY-RUN by default, --apply to commit. The helper ships to the box via base64 | openssl base64 -A -d and runs with argv. Rewired dispatch in both gw-control.sh and gw-audit.sh to prefer the SSH backend (keyed on clients/<slug>/pfsense-firewall), running the dispatch BEFORE UOS site resolution so a pfSense-only client slug works; the REST backend is now a dormant fallback.

Validated live on Cascades: reads (fw-list all 20 rules with correct schema, pf-list, showblock), tracker- and descr-based matching, the full block-ips/unblock cycle on a TEST-NET documentation IP (192.0.2.123), and the pfsense-gwc.php write path via a fw-disable/fw-enable round-trip on an inert rule (config backup + reload confirmed, rule returned to original state). Updated ROADMAP §E status to done, recorded the pfSense PHP gotchas, and updated SKILL.md's verb reference + dispatch description.

Key Decisions

  • php + config.inc bootstrap, not config.xml text parsing. Robust against version drift and gives pfSense's own write_config()/filter_configure() for safe commit + reload. Chosen after confirming pfSsh.php only runs its built-in playback scripts, not piped code.
  • Versioned PHP helper file (pfsense-gwc.php) instead of a bash heredoc. Keeps the control logic readable/lintable; shipped to the box per-call via base64 over the existing SSH session.
  • argv-driven helper. All operator data passed as positional args, never interpolated into PHP source — no shell/PHP injection surface.
  • Match filter rules by tracker or exact descr, not id. The id field is empty on pf25.07; tracker is the stable unique key.
  • Dispatch runs before UOS site resolution. A pfSense-only site is keyed by client slug (not a UOS site name), so resolving the UOS site first would hard-exit before dispatch could fire.
  • SSH backend preferred, REST kept dormant. Honors Mike's 2026-06-16 decision; REST (pfsense-backend.sh + clients/<slug>/pfsense-api) stays in-tree only as a fallback.
  • block-ips via easyrule, toggles via the PHP path. easyrule is the cleanest, canonical pfSense block mechanism (auto alias + rule); rule toggles need direct $config edits.

Problems Encountered

  • pfSsh.php piped code did nothing. Expected echo '...; exec' | pfSsh.php to eval; it only prints its banner + playback command list. Switched to php with a temp-file script.
  • php_rc=255 with no error output. pfSense runs display_errors=Off, so fatals are silent. Fixed by running php with 2>&1 AND ini_set("display_errors","1") in the helper.
  • Cannot redeclare backup_config() fatal. pfSense's config.lib.inc already defines it (and many generic names). Prefixed all helper functions with gwc_. Also dropped the extra require_once of util/functions/filter — config.inc already pulls them; re-requiring caused the redeclare fatal.
  • Empty output on first fw-list runs. Root-caused via step markers in the remote script (the three issues above), not by guessing.
  • No such file or directory on a verb test. Working dir had persisted from an earlier cd into the scripts dir; relative script path broke. Re-ran with cd /c/claudetools + path.

Configuration Changes

Created:

  • .claude/skills/unifi-wifi/scripts/pfsense-gwc.php — argv-driven pfSense config control helper.

Modified:

  • .claude/skills/unifi-wifi/scripts/pfsense-ssh.sh — added 12 control verbs, --apply/--if parsing, sq() shell-quote + run_gwc() ship-and-run helper, fixed run to use preserved args.
  • .claude/skills/unifi-wifi/scripts/gw-control.sh — SSH-first dispatch (pfsense-firewall cred), moved above UOS site resolution; REST kept as fallback.
  • .claude/skills/unifi-wifi/scripts/gw-audit.sh — same SSH-first dispatch for audit.
  • .claude/skills/unifi-wifi/references/ROADMAP.md — §E status → done; verb list; pfSense PHP gotchas added to cross-platform notes.
  • .claude/skills/unifi-wifi/SKILL.md — pfSense section rewritten (read/write verb tables, dispatch); gw-control usage block corrected (routes to pfsense-ssh.sh, match by tracker/descr).

Credentials & Secrets

No new credentials created or discovered. Used existing vault entries (read-only):

  • clients/cascades-tucson/pfsense-firewall — host 192.168.0.1, user admin, password stored. Used by pfsense-ssh.sh (system OpenSSH via askpass).
  • clients/cascades-tucson/pfsense-openvpn-howard — the Cascades site VPN profile (referenced for reachability; not directly used by the scripts).

Infrastructure & Servers

  • Cascades pfSense — 192.168.0.1 (LAN), pfSense Plus 25.07-RELEASE, admin SSH drops straight to a shell. Reachable over the Cascades site VPN (16ms). 20 filter rules, 0 NAT port-forwards.
  • PHP include_path on the box: .:/etc/inc:/usr/local/pfSense/include:...require_once("config.inc") resolves from anywhere.
  • Test residue left on Cascades: an inert 'Blocked via EasyRule' WAN block rule (tracker 1782065739) + empty EasyRuleBlockHostsWAN alias, created by the block-ips validation. Blocks nothing (empty alias); returned to enabled state. Pending operator decision to remove or keep.

Commands & Outputs

# Discovery (read-only) — schema + tooling
pfsense-ssh.sh cascades-tucson run 'cat /etc/version'                     # 25.07-RELEASE
# php bootstrap works; pfSsh.php only runs playback scripts (not piped code)
php /tmp/q.php  ->  NAT_RULES=0  FILTER_RULES=20
easyrule  ->  block|unblock|showblock <if> <src>; pass <if> <proto> <src> <dst> [port]

# New read verbs
pfsense-ssh.sh cascades-tucson fw-list      # 20 rules, keyed on tracker
pfsense-ssh.sh cascades-tucson pf-list      # (no NAT port-forwards configured)
pfsense-ssh.sh cascades-tucson showblock    # No entries are blocked on interface: wan

# Live write validations
pfsense-ssh.sh cascades-tucson block-ips 192.0.2.123 --apply   # Block added successfully
pfsense-ssh.sh cascades-tucson showblock                       # 192.0.2.123/32
pfsense-ssh.sh cascades-tucson unblock 192.0.2.123 --apply     # Entry unblocked successfully
pfsense-ssh.sh cascades-tucson fw-disable 1782065739 --apply   # backup + write_config + filter reload -> [off]
pfsense-ssh.sh cascades-tucson fw-enable  1782065739 --apply   # -> [on] (restored)

# Dispatch validation
gw-control.sh cascades-tucson fw-list       # -> dispatches to pfsense-ssh.sh
gw-control.sh cascades-tucson fw-disable 1772841904   # dry-run via dispatch

# Syntax check
bash -n pfsense-ssh.sh gw-control.sh gw-audit.sh   # all [OK]

Key gotcha encoded in scripts: pfSense display_errors=Off (run php with 2>&1 + ini_set), backup_config() collision (prefix gwc_*), config.inc-only bootstrap (no re-require).

Pending / Incomplete Tasks

  • pf-* (NAT port-forward) verbs are built but NOT live-verified. Cascades has 0 port-forwards, so they were coded against the documented pfSense NAT schema (destination.port, target, local-port, associated-rule-id). Need a pfSense box with an actual port-forward + a vaulted clients/<slug>/pfsense-firewall cred to confirm field names before trusting --apply. Marked live-verify-pending in ROADMAP §E.
  • Cascades easyrule test residue — decide whether to remove the inert 'Blocked via EasyRule' rule (tracker 1782065739) + empty EasyRuleBlockHostsWAN alias.
  • Optional pf-add/create verbs — not needed today (we only close/scope existing exposure); noted in ROADMAP as future.
  • Other §B/§C/§D ROADMAP items remain (per-client AP creds, gateway-hosted VPN server, read-only vault cred) — untouched this session.

Reference Information

  • Session-log path: session-logs/2026-06/2026-06-21-howard-unifi-pfsense-control-verbs.md
  • Skill: .claude/skills/unifi-wifi/ — ROADMAP §E is the design/verb-map + pfSense PHP gotchas.
  • Verb contract source (UniFi side): scripts/gw-control.sh header.
  • Prior sync commits this session: 5ede4fe (earlier auto-sync), 96a5dd6 (the pfSense build).
  • pfSense filter-rule schema: keyed on tracker; type = pass/block/reject; disabled key presence = off; source/destination are objects ({any:""} or {network,port}).

Update: 16:14 PT — finished §E build-out, conformance, prep, coord triage

Continued the same session well past the initial pfSense-control-verbs build. Net: the entire unifi-wifi pfSense gateway compatibility layer (ROADMAP §E) is now complete, validated, documented; the whole skill is brought into skill-module conformance; and the remaining blocked items are prepped.

Session Summary (continuation)

  • --port support added to pfsense-ssh.sh (precedence: --port flag > vault port/credentials.port field > default 22). Caught + fixed a regression where the vault returns the literal string "null" for a missing field (so :- defaulting failed → ssh -p null); normalized ""/null. Also fixed run to use POS not the removed RAWARGS so flags don't leak into ad-hoc commands.
  • Site→gateway map: sites.sh now prints a live gateway map (each UOS site classified UniFi-gw vs pfSense/third-party + vaulted pfSense creds with host:port). 12 UniFi-gw / 36 no-UniFi-gw sites; 1 cred (Cascades).
  • Auto-select: new references/site-gateways.tsv (site_id→cred map, seeded Cascades) + new scripts/gateway-map.sh (lookup/list/validate/suggest). gw-audit/gw-control now auto-route a site to its pfSense cred with NO --pfsense — validated gw-control Cascades fw-list + gw-audit Cascades.
  • Mike's cred-path answer = option A (coord msg 0bca380f): 1st arg with / = full vault path, else a client slug; fail loud [ERROR] no cred at vault:<path> on a bad path. Implemented in pfsense-ssh.sh + gw-audit/gw-control dispatch; validated path mode, slug mode, fail-loud. Unblocks the ACG office box (--pfsense infrastructure/pfsense-firewall). Closed the coord loop (todo e0ba933f done, reply fc96afba).
  • Skill error-logging conformance (skill-creator mandatory rule — the whole skill had none): added logerr (canonical log-skill-error.sh) to the live scripts (pfsense-ssh/gw-control/gw-audit) + SKILL.md guideline, then delegated the remaining ~20 scripts to a sub-agent (verified: all bash -n clean, exit codes preserved, conservative functional-only logging, no false positives). New gateway-map.sh self-logs.
  • pf- NAT verbs live-verified* on Cascades via a temp source-locked (TEST-NET) disabled port-forward: create→pf-list→set-ports→set-src→enable→disable→delete; pf-delete removed the associated filter rule; box back to baseline (nat=0, filter=21). Verb mechanics + documented NAT schema confirmed.
  • §D least-privilege prepped: gw-audit now prefers a read-only controller cred (infrastructure/uos-server-network-api) and auto-uses it once vaulted (falls back to RW with a hint).
  • Prep docs: new references/onboarding.md (copy-paste §B AP-cred + pfSense + §D RO-cred procedures); ROADMAP §C now carries a build-ready WireGuard VPN-server design.
  • Coord inbox triage: 41 messages, all already read; surfaced 4 possibly-open (BUG-016/017, log-analysis interview, LHM, billing), rest resolved/FYI.
  • BUG-016/017 verification (user asked to take them): both ALREADY FIXED in guru-rmm (commit 30da053, Mike 2026-06-01) — StateDirectory=gururmm in the systemd template (016) + OnceLock CACHED_ID in device_id.rs (017). Verified against working tree ed8cad3 (== origin/main). Nothing to take.

Key Decisions (continuation)

  • Auto-select keyed on the stable 24-hex UOS site_id, not a fuzzy name (lookup also accepts a name as a fallback). Map generated/consulted live; the TSV is the only persisted state.
  • Error-logging delegated to a sub-agent for the ~20-script sweep (high-volume, mechanical-with-judgment, independent) — but the 3 live entry-point scripts done by hand first as the template, and the agent's output verified (syntax + exit-code preservation + no false-positive logging) rather than trusted.
  • pf-* verification used a hand-built forward source-locked to TEST-NET (inert even if enabled) on the designated test box (Cascades) — reversible, no exposure; noted that a GUI-created forward is a future belt-and-suspenders schema check.

Problems Encountered (continuation)

  • ssh -p null regression from the vault returning "null" for a missing port field → normalized.
  • Repeated cd into the guru-rmm submodule left the working dir there; a later sync.sh relative-path call failed (exit 127) → re-ran from /c/claudetools. (Recurring Bash-cwd-persistence friction.)

Configuration Changes (continuation)

Created: scripts/gateway-map.sh, references/site-gateways.tsv, references/onboarding.md. Modified: scripts/pfsense-ssh.sh, scripts/gw-control.sh, scripts/gw-audit.sh, scripts/sites.sh, scripts/pfsense-gwc.php (gwc_ prefixes earlier), the other ~20 scripts/*.sh (logerr), SKILL.md, references/ROADMAP.md; wiki systems/pfsense.md, systems/uos-server.md, index.md.

Pending / Incomplete Tasks (continuation)

  • §B / §D: blocked only on external inputs (per-client AP creds + site reach; the read-only controller account). Procedures ready in references/onboarding.md; gw-audit already RO-cred-ready.
  • §C: VPN-server stand-up — build-ready design in ROADMAP §C; not externally blocked (Cascades reachable).
  • BUG-018 (guru-rmm, P2, Open): DELETE /api/agents/:id resets connection (HTTP 000), FK-cascade slow; server/src/api/agents.rs:127-150server/src/db/agents.rs:186-190. Offered to take; awaiting go.
  • Optional pf-* GUI-created-forward schema spot-check; optional pf-add verbs (not needed today).

Reference Information (continuation)

  • Coord: Mike's answer 0bca380f; reply fc96afba; todo done e0ba933f.
  • guru-rmm: BUG-016/017 fixed in commit 30da053; BUG-018 open (FEATURE_ROADMAP.md ~line 411); repo HEAD ed8cad3.
  • Sync commits this continuation: 5ede4febe9d6c3 (multiple auto-syncs across the build).