Skip to content

fix(snmp): stop the pfsense module walking pfTablesAddrTable - #18

Merged
Gerrrt merged 1 commit into
mainfrom
fix/pfsense-walk-scope
Aug 18, 2026
Merged

fix(snmp): stop the pfsense module walking pfTablesAddrTable#18
Gerrrt merged 1 commit into
mainfrom
fix/pfsense-walk-scope

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Symptom

morpheus reported down with context deadline exceeded at exactly 45.00s — Prometheus's scrape_timeout — while its SNMP community worked fine when tested directly from the monitoring host. The credential was never the problem; the walk simply could not finish.

Cause

The module walked 1.3.6.1.4.1.12325.1, commented # BEGEMOT-PF-MIB::begemotPf. The comment states the intent correctly — the OID is one level too high. begemotPf is 12325.1.**200**, so 12325.1 also swept in begemotSnmpd (12325.1.1).

The real cost is inside pfTables (.1.9), which holds two tables of wildly different size. Measured against morpheus:

OID Table Rows Time
.1.9.1 pfTablesTblNumber 1 0.1s
.1.9.2 pfTablesTblTable 323 0.4s
.1.9.3 pfTablesAddrTable 50,917+ >20s, unfinished

pfTablesAddrTable is one row per address in every pf table — every entry of every blocklist. A full walk of 12325.1 returned 116,360 rows in 90s without completing, against ~1,800 rows for all of begemotPf otherwise. It grows with every blocklist added.

This could not be fixed by raising scrape_timeout: already 45s against a 60s scrape_interval, and the walk needed well over 90s.

Fix

Enumerate begemotPf's subtrees, descending into .1.9 to keep the table summaries while excluding the address table.

Nothing queries the dropped metrics — alerts use pfStatusRunning, pfStateTableCount, pfLimitsStates, pfCounterMemDrop; dashboards add pfSrcNodes* and pfStatusRuntime. All 21 pfTablesTbl* metrics are retained.

Note on the metric-count guard

make snmp-generate warns here:

metrics: 1805 -> 1792
warning: regeneration LOST 13 metric(s)

That is intended. The 13 are exactly the pfTablesAddr* set — verified by diffing the metric-name sets, not by trusting the count:

pfTablesAddrBytesInBlock   pfTablesAddrBytesInPass    pfTablesAddrBytesOutBlock
pfTablesAddrBytesOutPass   pfTablesAddrIndex          pfTablesAddrNet
pfTablesAddrNetType        pfTablesAddrPktsInBlock    pfTablesAddrPktsInPass
pfTablesAddrPktsOutBlock   pfTablesAddrPktsOutPass    pfTablesAddrPrefix
pfTablesAddrTZero

Result

Deployed and verified on the monitoring host:

Device Before After
morpheus 45.00s → timeout, up=0 0.19s, up=1

Metrics arriving: pfStatusRunning=1, pfStateTableCount=932, pfLimitsStates=3248000.

Deploy note

snmp.yaml is a bind-mounted file and snmp_exporter reads it at startup. docker compose up -d does not recreate a container when only a mounted file's contents change, so make up left the old config running in memory. docker compose restart snmp-exporter was required. Worth addressing separately — make up currently cannot apply a rendered-config change to this service.

Also explains SnmpScrapeSlow (network.rules.yaml:28) at its 30s threshold — the slowness was alerted on for a long time but never traced.

🤖 Generated with Claude Code

morpheus reported down with "context deadline exceeded" at exactly 45.00s —
Prometheus's scrape_timeout — while its SNMP community worked fine from the
monitoring host. The walk could not finish.

The module walked 1.3.6.1.4.1.12325.1, commented "BEGEMOT-PF-MIB::begemotPf".
The comment states the intent correctly; the OID is one level too high.
begemotPf is 12325.1.200, so 12325.1 also swept in begemotSnmpd (12325.1.1).

The real cost, though, is inside pfTables (.1.9), which holds two tables of very
different size. Measured against morpheus:

  .1.9.1  pfTablesTblNumber        1 row     0.1s
  .1.9.2  pfTablesTblTable       323 rows    0.4s
  .1.9.3  pfTablesAddrTable    50,917+ rows  >20s, unfinished

pfTablesAddrTable is one row per address in every pf table — every entry of
every blocklist. A full walk of 12325.1 returned 116,360 rows in 90s without
completing, against ~1,800 rows for all of begemotPf otherwise.

So the walk enumerates begemotPf's subtrees and descends into .1.9 to take the
table summaries while excluding the address table. Nothing queries the
pfTablesAddr* metrics: the alerts use pfStatusRunning, pfStateTableCount,
pfLimitsStates and pfCounterMemDrop; the dashboards add pfSrcNodes* and
pfStatusRuntime. The 21 pfTablesTbl* metrics are retained.

This could not be fixed by raising scrape_timeout: it was already 45s against a
60s scrape_interval, and the walk needed well over 90s.

Regeneration drops exactly the 13 pfTablesAddr* metrics (1805 -> 1792), which is
why `make snmp-generate` warns about metric loss here. That is intended.

Result: pfSense scrape went from a 45.00s timeout to 0.19s.

Also explains SnmpScrapeSlow (network.rules.yaml, 30s threshold) — the slowness
was alerted on but never traced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Gerrrt
Gerrrt merged commit 78aa981 into main Aug 18, 2026
3 checks passed
@Gerrrt
Gerrrt deleted the fix/pfsense-walk-scope branch August 18, 2026 22:30
Gerrrt added a commit that referenced this pull request Aug 19, 2026
fix(snmp): stop the pfsense module walking pfTablesAddrTable
Gerrrt added a commit that referenced this pull request Aug 19, 2026
…oad`

`docker compose up -d` recreates a container only when its *service definition*
changes — image, command, mounts, environment. The contents of a bind-mounted
file are invisible to it. snmp-exporter parses its config once at startup, so
`scripts/render-config.sh` could write a brand-new snmp.yaml, `make up` report
success, and the exporter go on serving the config it parsed minutes earlier.

That is not hypothetical. In PR #18 the corrected pfSense walk was rendered to
disk, `make up` ran clean, and the exporter kept timing out at 45s until someone
ran `docker compose restart snmp-exporter` by hand. The failure mode is the bad
one: the tool says done, and only the target disagrees.

The mechanism to fix it already existed. `make reload` has been POSTing
/-/reload to all three config-reading services since the rotation tooling
landed; `make up` simply never called it. So this is wiring, not a new
capability — no unconditional `docker compose restart`, which would drop the
TSDB head on Prometheus and is heavier than the reload each service already
serves.

Extracted into scripts/reload-config.sh because the retry does not fit in
Makefile recipe lines, and because `up` and `reload` now need the same logic.
Three outcomes, deliberately distinguished:

  wget exit 4     running but not yet listening — retry 1s, up to
                  RELOAD_TIMEOUT (60s). `up -d` returns when containers are
                  started, not when they are ready.
  wget exit 8     the service answered and refused: the config on disk does not
                  parse. Waiting cannot fix that, so fail immediately and say
                  it is still serving the previous config.
  not running     stopped or crash-looping (inspect reports `restarting`) —
                  fail immediately with `make ps` advice rather than burning
                  the timeout rediscovering it.

Failing is the point. The obvious way to stop a slow box breaking `make up` is
to swallow reload errors, which rebuilds the exact defect this closes: `make up`
reporting success over a stale config. A reload that never lands is a failed
deploy and now says so.

`compose ps -q` plus `docker inspect -f`, not `compose ps --format
'{{.State}}'`: custom Go templates only reached `compose ps` in a later Compose
v2, and on an older one the template is read as a literal format name, so every
service looks stopped and `make up` fails on a healthy stack.

Widened to prometheus and alertmanager as well. Their configs are bind mounts
with the identical staleness problem, `make reload` already treated the three as
a unit, and alertmanager.yaml changes do need a reload even though the rendered
webhook_url does not (url_file is read at notify time).

Docs carried the inverse of the truth. rotate-snmp-community.md said `make up`
"also works and is not wrong, it just does more than is needed" — it did less
than was needed, and would have walked the next operator into PR #18.
deploy-stack.md's "recreates only what changed" was the misconception itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant