238 lines
16 KiB
Markdown
238 lines
16 KiB
Markdown
# 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-150` → `server/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: `5ede4fe`→`be9d6c3` (multiple auto-syncs across the build).
|