firewalla-snmp-proxy
Monitor your Firewalla Switch with any SNMP monitoring system.
The Firewalla Switch SE has no SNMP agent, so tools like Observium, LibreNMS, Zabbix, Checkmk, PRTG and Grafana can't see it. But the Firewalla MSP cloud API does expose full per-port data. This proxy polls that API and republishes it as a standards-compliant SNMP agent — one UDP port per switch — so your existing monitoring system discovers it like any other managed switch.
You get real per-port traffic graphs, error counters, PoE draw, link speeds, STP state, SFP transceiver detail, and a topology link to your Firewalla box.
┌──────────────┐ HTTPS ┌──────────────────┐ SNMP ┌─────────────┐
│ Firewalla │ ─────────► │ firewalla- │ ◄──────── │ Observium │
│ MSP cloud │ poll │ snmp-proxy │ v1/v2c │ LibreNMS │
│ API │ │ (your Linux box) │ │ Zabbix, ... │
└──────────────┘ └──────────────────┘ └─────────────┘
Nothing is installed on the Firewalla itself, and the proxy is strictly read-only — see Safety.
Requirements
-
A Linux host with Python 3.9+, reachable by your monitoring system.
-
A Firewalla MSP account. The per-port data comes from the MSP cloud API, so a standalone box with no MSP subscription has nothing to read.
This does not have to cost anything. Firewalla offers MSP Lite free for a single box, which is all this proxy needs — one box, one API token. Sign up at firewalla.net and you get an MSP domain of the form
dn-abc123.firewalla.net, which is what--domainbelow wants. -
Only two Python dependencies (
pysnmp,PyYAML) — both pulled in automatically below.
Quick start
# 1. Install
pipx install firewalla-snmp-proxy # or: pip install firewalla-snmp-proxy
firewalla-snmp-proxy --version
# 2. Get an API token from the Firewalla MSP web console
# (account settings -> API tokens), then:
export FIREWALLA_MSP_TOKEN=<your token>
# 3. Discover your switches and write a config
firewalla-snmp-proxy init --domain dn-abc123.firewalla.net
# 4. Check it works
firewalla-snmp-proxy check -c config.yaml
# 5. Run it
firewalla-snmp-proxy run -c config.yaml
pipx is suggested first because current distros are
PEP 668-managed and will refuse a bare
pip install into system Python. To install it system-wide so a systemd unit
can find the binary:
sudo apt install pipx
sudo pipx --global install firewalla-snmp-proxy
A plain venv works just as well if you prefer to manage it yourself:
sudo python3 -m venv /opt/firewalla-snmp-proxy-venv
sudo /opt/firewalla-snmp-proxy-venv/bin/pip install firewalla-snmp-proxy
sudo ln -sf /opt/firewalla-snmp-proxy-venv/bin/firewalla-snmp-proxy /usr/local/bin/
Step 4 binds no sockets, so it is safe to run while something else is already listening on the port. Step 5 runs in the foreground, which is what you want while trying it out — for anything permanent see Install as a service, which restarts on failure and survives a reboot.
Then, from anywhere that can reach the proxy:
snmpwalk -v2c -c public 127.0.0.1:16100 1.3.6.1.2.1.2.2
--domain is the hostname of your MSP web console: if you reach MSP at
https://dn-abc123.firewalla.net/, use dn-abc123.firewalla.net.
If the walk times out, check listen.address — it defaults to 127.0.0.1,
which accepts local traffic only. See
Monitoring system recipes if your NMS is on
another host.
Install as a service
You almost certainly want this. Running run in a shell means it dies with
your session and does not come back after a reboot; a monitoring source that
quietly stops is worse than one that was never there.
Install the binary system-wide first. The generated unit sets
ProtectHome=yes, so systemd cannot execute anything under /home or /root
— a per-user pipx install produces a binary the service is unable to start,
even though it runs fine from your shell:
pipx uninstall firewalla-snmp-proxy # if you installed it as yourself
sudo pipx --global install firewalla-snmp-proxy
Then, from a clone of this repo (for install.sh itself):
sudo ./install.sh --service # or: --service --user snmpproxy
install.sh refuses to proceed with a home-directory binary rather than
writing a unit that cannot start.
The systemd unit is generated from the detected binary path, service user
and config location — there is nothing to hand-edit. It creates a locked-down
firewalla-snmp-proxy system user, a 0640 root-owned environment file for your
token, and a state directory for counter persistence:
| Config | /etc/firewalla-snmp-proxy/config.yaml |
Token (EnvironmentFile) |
/etc/firewalla-snmp-proxy/env |
| Counter state | /var/lib/firewalla-snmp-proxy/counters.json |
Put your token in the env file as FIREWALLA_MSP_TOKEN=<token>, then generate
the config at the system path and start it:
export FIREWALLA_MSP_TOKEN=<token>
sudo firewalla-snmp-proxy init --domain <your>.firewalla.net \
-o /etc/firewalla-snmp-proxy/config.yaml --force
firewalla-snmp-proxy check -c /etc/firewalla-snmp-proxy/config.yaml
sudo systemctl enable --now firewalla-snmp-proxy
journalctl -u firewalla-snmp-proxy -f
install.sh prints these same steps when it finishes, and
sudo ./install.sh --uninstall reverses it.
The token is never accepted as a command-line argument, so it cannot leak into
your shell history or ps output. It is resolved from FIREWALLA_MSP_TOKEN,
then token_file, then the config file — with a warning if the file holding it
is group- or world-readable.
Installing from a clone instead
The PyPI package is everything you need to run the proxy. Clone the repo as
well if you want install.sh (which writes the systemd unit, service user and
sandbox for you) or the vendor MIB as a file to load into your NMS:
git clone https://github.com/kdesch5000/firewalla-snmp-proxy
cd firewalla-snmp-proxy
sudo ./install.sh --service
The MIB also ships inside the wheel, at mibs/FIREWALLA-SNMP-PROXY-MIB.txt
under your site-packages directory.
What you get
Almost everything lands in a standard MIB, which is the whole point: your
monitoring system already knows how to graph an ifEntry, so per-port traffic
works with zero custom configuration.
| Firewalla data | Published as | MIB |
|---|---|---|
rxBytes / txBytes |
ifHCInOctets / ifHCOutOctets (64-bit) + 32-bit equivalents |
IF-MIB |
| unicast / multicast / broadcast frames | ifHCIn*Pkts / ifHCOut*Pkts |
IF-MIB |
rxErrorFrames / txErrorFrames |
ifInErrors / ifOutErrors |
IF-MIB |
rxDiscardFrames / txDiscardFrames |
ifInDiscards / ifOutDiscards |
IF-MIB |
linkUp |
ifOperStatus |
IF-MIB |
linkSpeed |
ifSpeed + ifHighSpeed |
IF-MIB |
| port role / VLAN name | ifAlias (e.g. Access: IoTVLan) |
IF-MIB |
statsSinceTs |
ifCounterDiscontinuityTime |
IF-MIB |
poeStatus |
pethPsePortDetectionStatus |
POWER-ETHERNET-MIB |
poeMode |
pethPsePortType |
POWER-ETHERNET-MIB |
| total PoE draw | pethMainPseConsumptionPower |
POWER-ETHERNET-MIB |
stp.portState |
dot1dStpPortState |
BRIDGE-MIB |
| switch MAC, port count | dot1dBaseBridgeAddress, dot1dBaseNumPorts |
BRIDGE-MIB |
| serial, hardware rev, firmware rev | entPhysicalTable |
ENTITY-MIB |
| SFP cage / transceiver | entPhysicalTable (module entities) |
ENTITY-MIB |
uplink |
lldpRemTable — draws the topology link |
LLDP-MIB |
| model, firmware, uptime | sysDescr, sysUpTime |
SNMPv2-MIB |
Four things have no standard home, so they live in a small vendor subtree
(mibs/FIREWALLA-SNMP-PROXY-MIB.txt, load it into your NMS for named output):
| Firewalla data | Object | Why it isn't standard |
|---|---|---|
poePower per port |
fwPortPoePower (mW) |
RFC 3621 models PoE status but has no per-port wattage object at all |
stp.portRole |
fwPortStpRole |
Lives in IEEE8021-SPANNING-TREE-MIB, which few NMSes implement |
sfpInfo |
fwPortSfp* |
No standard transceiver MIB in common use |
| ACL usage | fwSwitchAcl* |
Entirely Firewalla-specific |
Plus a proxy health group (fwProxy*) — poll status, seconds since last
successful poll, API latency, last error. This is what you alert on: without it
you can't tell an idle switch from a proxy that lost API access and is serving
frozen counters. See Monitoring the proxy itself.
Monitoring system recipes
The proxy is a normal SNMP v2c agent, so "add a device" works as usual. The one non-default thing is the port — each switch listens on its own, starting at 16100.
Some systems can only poll UDP 161. If yours is one, either run the proxy with
listen.base_port: 161 (one switch only, and it needs a privileged port), or
give the proxy host an extra IP and DNAT 161 to 16100.
Observium
Observium keys devices on hostname, so give the proxy a name in /etc/hosts
(or DNS) first:
127.0.0.1 fwswitch
cd /opt/observium
./add_device.php fwswitch public v2c 16100
./discovery.php -h fwswitch
./poller.php -h fwswitch
For named vendor objects, copy the MIB in and rediscover:
cp mibs/FIREWALLA-SNMP-PROXY-MIB.txt /opt/observium/mibs/rfc/
LibreNMS
cd /opt/librenms
./addhost.php fwswitch public v2c 16100
./discovery.php -h fwswitch
Or in the web UI: Devices → Add Device, set Port to 16100 and
SNMP version to v2c.
Zabbix
Create a host with an SNMP agent interface, IP of the proxy host and port
16100. Set the macro {$SNMP_COMMUNITY} to your community, then attach the
built-in template "Network Generic Device by SNMP" — it will discover the
ports through IF-MIB automatically.
Checkmk
cmk -v --add-host fwswitch
Set the SNMP port to 16100 in the host's properties (SNMP → Port), then
run a service discovery. The if64 check plugin picks up the ports.
Nagios
Untested. Everything below follows from the proxy being a plain SNMP v2c agent, and the OIDs are verified to respond, but nobody has yet reported running this under Nagios end to end. Reports welcome, working or not.
Nagios is a different shape of tool from the others here — it alerts on thresholds rather than autodiscovering and graphing — so there are two things to set up.
1. The proxy's own health. This is the part Nagios is genuinely good at, and it is more important than watching traffic: when the MSP API is unreachable the proxy keeps serving the last known good data, so counters freeze and every derived rate reads as a legitimate zero. Alert on these instead.
# Poll status: 1 ok, 2 stale, 3 never polled. Alert on anything but 1.
check_snmp -H 127.0.0.1 -p 16100 -P 2c -C public \
-o .1.3.6.1.4.1.99999.1.3.0 -c 1:1 -l "proxy poll status"
# Seconds since the last good poll: warn at 30 min, critical at 1 hour.
check_snmp -H 127.0.0.1 -p 16100 -P 2c -C public \
-o .1.3.6.1.4.1.99999.1.2.0 -w 1800 -c 3600 -l "since poll"
# ICMP reachability of the real switch: 1 up, 2 down, 3 unknown, 4 disabled.
check_snmp -H 127.0.0.1 -p 16100 -P 2c -C public \
-o .1.3.6.1.4.1.99999.1.7.0 -c 1:1 -l "switch icmp"
2. The ports. Core Nagios has no autodiscovery, so define each port as a
service, or generate the config. Per-port link state is a one-liner —
ifOperStatus is .1.3.6.1.2.1.2.2.1.8.<port>, and the ifIndex is just the
physical port number:
check_snmp -H 127.0.0.1 -p 16100 -P 2c -C public \
-o .1.3.6.1.2.1.2.2.1.8.3 -c 1:1 -l "Port 3 link"
For traffic, check_snmp_int.pl from the manubulon
plugin set is the usual choice — it reads the 64-bit counters and does the rate
calculation itself. Check its port flag against your version. Nagios XI users
can point the SNMP wizard at the proxy and let it discover the interfaces.
check_snmp emits perfdata, so PNP4Nagios or Grafana will graph any of the
above without extra work.
Set the check interval with the poll interval in mind. The proxy refreshes
from the cloud every poll_interval seconds (default 900). Checking more often
than that is harmless but returns the same numbers repeatedly — see
Rate limits for
why the default is what it is. Counter ramping keeps rate-based checks smooth
rather than producing a 0/0/0/spike sawtooth.
PRTG
Add an SNMP Library or SNMP Traffic sensor against the proxy host, and set the port to 16100 in the device's SNMP settings.
Plain snmpwalk (start here when debugging)
# Everything
snmpwalk -v2c -c public 127.0.0.1:16100 1
# Just the interfaces
snmpwalk -v2c -c public 127.0.0.1:16100 1.3.6.1.2.1.2.2
# With the vendor MIB loaded
snmpwalk -m +FIREWALLA-SNMP-PROXY-MIB -M +./mibs \
-v2c -c public 127.0.0.1:16100 1.3.6.1.4.1.99999
If a plain walk is clean and in order, every NMS above will work — that's the real smoke test.
Monitoring the proxy itself
The proxy serves the last known good data if the MSP API becomes unreachable. That's deliberate — blanking counters would look like a dead switch — but it means you should alert on the proxy's own health:
| Object | OID | Meaning |
|---|---|---|
fwProxyPollStatus |
.1.3.6.1.4.1.99999.1.3.0 |
1 ok, 2 stale, 3 never polled |
fwProxySecondsSincePoll |
.1.3.6.1.4.1.99999.1.2.0 |
seconds since last good poll |
fwProxyLastError |
.1.3.6.1.4.1.99999.1.5.0 |
last error text |
fwProxyApiLatency |
.1.3.6.1.4.1.99999.1.4.0 |
last API round-trip, ms |
Alert on fwProxyPollStatus != 1. stale(2) means no successful poll for
three poll intervals.
Counters, resets and why your graphs stay sane
SNMP counters must only ever increase; wrapping is fine and every NMS handles it. Firewalla's port counters, though, can be reset to zero — by a firmware event, or by someone clicking "reset statistics" in the MSP UI. A naive proxy would republish that as a huge negative delta, which your NMS renders as a garbage spike or a gap.
This proxy handles it two ways at once:
- Persistent monotonic offsets (
state_file). On a detected reset, the last value is folded into an offset, so what you see over SNMP keeps rising across resets, proxy restarts and switch reboots. ifCounterDiscontinuityTimeis published from Firewalla'sstatsSinceTs. NMSes that honour it discard the suspect delta themselves.
Resets are detected from both an advancing statsSinceTs and a decreasing
raw value, because neither alone is sufficient — if enough traffic passes
between polls, a reset counter can already be back above its previous reading,
and only the timestamp reveals it.
Deleting state_file is safe but causes one spurious spike on next start.
Safety
This is a monitoring tool and it is aggressively read-only:
- The API client can only issue GETs. It also refuses, client-side, any URL
containing a state-changing path segment (
reboot,upgrade,reset-stats,power-cycle,restart,switch-branch,check-status,force-sync,detach,attach,delete). The MSP API really does expose those; a monitoring tool must never reach them.reset-statsin particular would destroy your counter history, andpower-cycledrops traffic. - SNMP SET is refused — by access control, and again in the instrumentation layer. There is no code path from SNMP to the Firewalla API.
- Nothing is installed on the Firewalla or the switch.
- The systemd unit runs as a dedicated non-root user under
ProtectSystem=strictwith an empty capability set and exactly one writable path (the state directory).
Your API token
The token grants full read access to your Firewalla account, so treat it like a password:
- It is never accepted as a command-line argument — that would leak it into
your shell history and into
psoutput for every other user on the box. - Precedence is
FIREWALLA_MSP_TOKEN→msp.token_file→msp.token. - The proxy warns if a file containing your token is group- or world-readable.
install.shputs it in a 0640 root-ownedEnvironmentFile, so yourconfig.yamlstays free of credentials and can be shared in a bug report.
How fresh is the data?
Your NMS is polling a cloud API, not the switch. This is the most important thing to understand before you set alert thresholds, so here are measured numbers rather than hand-waving.
Port counters update roughly every 5 minutes. Sampling the API every 20s for
13 minutes, rxBytes on a busy trunk changed exactly 3 times — at 123s, 409s and
717s, i.e. intervals of 286s and 308s. Between those points the API returns byte
for byte the same value.
That is a property of the Firewalla cloud, not of this proxy, and it has consequences no amount of proxy polling can fix:
- Your effective graph resolution is ~5 minutes, however fast your NMS polls.
- Rate spikes shorter than the poll interval are invisible. The counters are cumulative and accurate, so totals over an hour are right; a 30-second burst gets averaged across its bucket.
- Don't build fast link-flap detection on this. A port going down appears on the first poll after the cloud notices.
/topology and /switches/<mac> were observed changing at the same instant
with identical values, so they share one cache — there is no fresher endpoint
to prefer, and the proxy makes one topology call per cycle rather than one per
switch.
fwProxyApiLatency reports what each API round-trip actually costs.
Rate limits, and why poll_interval defaults to 15 minutes
The MSP API enforces a request quota. Each poll cycle costs several calls
(/topology, /switch-settings, /switches/<mac>, plus a periodic
/devices), so a fast interval burns through it steadily. Once the quota is
exhausted the API returns HTTP 429 to everything until it resets.
That failure is dangerous precisely because it is quiet. The proxy keeps answering SNMP, the counters simply stop advancing, and every rate your NMS derives from them is a legitimate-looking zero. Nothing looks broken; the graph just goes flat. This is not hypothetical — it is what prompted the 2.1.0 release, after a 429 lockout silently flatlined a port's graph for hours.
Two mechanisms address it:
1. A 15-minute default interval. poll_interval: 900 keeps well clear of
the quota. The old 60s default was chosen to minimise latency behind the
cloud's ~5-minute data cadence, which was the wrong thing to optimise: it
bought a few minutes of freshness at the cost of ~4,300 API calls a day.
2. Exponential backoff on 429. On a rate-limit response the poller stops
issuing requests entirely until the backoff expires — retrying through a 429
keeps the quota pinned and can extend the lockout. Backoff starts at
poll_interval, doubles per consecutive 429, is jittered (so several proxies
sharing a token don't retry in lockstep and re-trip the quota together), and is
capped by max_backoff_seconds (default 3600). A Retry-After header from the
API always wins over the computed delay. Recovery needs no operator: the first
successful cycle resets the schedule.
While backed off, fwProxyLastError says so explicitly and
fwProxySecondsSincePoll keeps climbing — alert on those, not on traffic
rates, which cannot distinguish a rate limit from a quiet switch.
What is actually known about the quota
Firewalla publishes no rate limit anywhere. As of 2026-08-31 the
docs.firewalla.net sitemap is 18 pages, all api-reference/* and
data-models/*, with no limits, quota or errors page at all; the
msp-api-examples repo says nothing either. The closest thing to an official
statement is a Firewalla staff reply in an MSP pricing thread noting there "may
be some restrictions on rate limiting the API calls that Firewalla is still
testing" — so the limit is both undocumented and evolving. Treat every
number below as observed behaviour, not a specification.
Measured, from a real lockout:
| Observation | Value |
|---|---|
| Call volume that tripped it | ~4,300/day (60s interval x ~3 calls) |
| How long the lockout lasted | 6+ hours |
Retry-After sent on every 429 |
3600 — and the lockout outlasted it by 5+ hours |
| Rate-limit headers on a successful response | none |
That last row is the important one for anyone else building against this API.
A successful GET /v2/boxes returns only Content-Type, Content-Length,
Connection, Date, vary, X-Amzn-Trace-Id, x-amzn-RequestId,
x-amzn-Remapped-content-length, x-powered-by, and CloudFront's X-Cache /
Via / X-Amz-Cf-*. There is no x-ratelimit-*, no ratelimit-*, no quota
counter of any kind. A client cannot read its remaining headroom — the limit
is only discoverable by tripping it. Don't waste time looking for those
headers.
Inferred, from the serving stack: those x-amzn-* headers plus
x-powered-by: Express mean the API is served by CloudFront in front of AWS
API Gateway. API Gateway usage plans have exactly two knobs — throttle
(requests/second plus burst) and quota — and quota granularity is DAY, WEEK
or MONTH only; there is no hourly option. That independently explains the
contradiction above: Retry-After: 3600 is a generic canned hint, while the
real reset is a day or week boundary. API Gateway also emits no rate-limit
headers by default, matching the measurement. This is inference from the
infrastructure, not confirmation from Firewalla, but it fits every observation.
The practical consequences, which the defaults already encode:
- Assume a daily quota. At
poll_interval: 900the proxy spends roughly 290 calls a day, about 7% of what tripped the limit. - Never restart to retry sooner. A restart re-issues a request, collects a
fresh
Retry-After: 3600, and pushes recovery out another hour. - Don't trust
Retry-Afteras a reset time. The proxy honours it because the server is the only party that might know, butdark_probe_max_secondsexists precisely so a canned 3600 cannot strand a cacheless proxy for an hour after the quota already cleared.
Surviving an API outage without going down
Backing off politely is not enough on its own, because the SNMP sockets are
built from the port layout — and that used to come only from a live /topology
call. So a proxy restarted mid-lockout stayed up but never listened, and the
NMS saw the device as down for the whole lockout.
Two things prevent that:
A topology cache. Every successful poll writes the merged API payload to
topology_cache. If the API is unavailable at startup, agents are rebuilt from
that file and serve immediately, publishing the full port set. Counters are
frozen at their cached values, so derived rates read zero — which is honest, and
fwProxyServingCache is what tells you the difference between that and an idle
switch. The poller keeps retrying in the background and upgrades to live data
the moment the API recovers.
Serving a truncated ifTable during a lockout was considered and rejected: Observium marks ports absent from ifTable as deleted, which would discard the very port history the cache exists to protect. Either every port is served or none is — which is why a first-ever run with no cache still has to wait.
An ICMP reachability check. ping_host pings the real switch on its own
cadence (ping_interval, default 60s), independently of the poll loop. This is
what makes serving cached data safe: the counters are admittedly old, but
fwSwitchOnline and fwProxyIcmpStatus still reflect reality, so a
stale-but-alive switch stays distinguishable from one that has actually gone
away. It costs no API quota, so it keeps working through a lockout — during
which it is the only live signal you have.
Reachability is tri-state, and the third state matters. A check that could
not be carried out — unresolvable name, no ICMP permission — is unknown, not
down; folding the two together would report a fake outage every time the host
rebooted before its resolver was ready. Transitions are debounced, and
asymmetrically: ping_fail_threshold defaults to 3 while
ping_recover_threshold defaults to 1, because coming back is unambiguous and
going away is not.
No privileges are needed. An unprivileged ICMP datagram socket is used where
net.ipv4.ping_group_range permits, falling back to the ping binary
otherwise; both paths work under this project's hardened systemd unit.
Prefer a hostname over a literal IP for ping_host — if the switch's
address changes via DHCP, a hardcoded IP silently starts reporting a dead
switch.
What each state looks like to an NMS:
| Situation | fwProxyPollStatus |
fwProxyServingCache |
fwProxyIcmpStatus |
Rates |
|---|---|---|---|---|
| Normal | ok(1) |
false(2) |
up(1) |
real |
| API rate limited, switch alive | stale(2) |
true(1) if restarted |
up(1) |
zero |
| API rate limited, switch gone | error(3) |
true(1) if restarted |
down(2) |
zero |
| Ping target unresolvable | unchanged | — | unknown(3) |
— |
No ping_host set |
age-based | — | disabled(4) |
— |
Two things are deliberately not faked. Per-port ifOperStatus keeps its
last-known value while serving cache, because ICMP to the switch says nothing
about individual port link state — reporting down(2) would invent an outage
and unknown(4) would churn port-state history. And recovery from a long
lockout lands the accumulated delta in one overstated sample, because the ramp
is skipped past max_ramp_seconds; spreading an hour of old traffic across the
following hour would mask what the switch is doing now, and RRA consolidation
fixes the long-run average either way.
Counter smoothing: a slow poll for a fast NMS
A 15-minute interval creates a cadence mismatch, because most NMSes poll faster and on a schedule you can't change — Observium's poller is a fixed 5-minute cron. Served raw, that produces a sawtooth: two of every three polls see byte-identical counters and derive a rate of zero, and the third divides 900s of traffic by 300s, overstating the rate by 3x. Every individual sample is wrong, and only after RRA consolidation does the average return to the truth.
counter_smoothing: ramp (the default) fixes this in the proxy, so you don't
have to touch your NMS's polling at all. Each newly-learned increment is spread
evenly across the window following the poll that revealed it, so a 5-minute
poller sees a third of it each time and derives the same correct rate every
sample. Specifically, for counter values C1 at T1 and C2 at T2, a
request at t is served C1 + (C2 - C1) * (t - T2) / (T2 - T1), clamped.
What this buys and what it costs:
- Byte totals stay exact. Nothing is invented or discarded; the same bytes are redistributed across the window instead of dumped into one sample.
- The window is measured, not configured.
T2 - T1is observed, so if a backoff stretches the real interval to 40 minutes, the ramp stretches with it automatically. - Output stays monotonically non-decreasing, as SNMP counters must be.
- The graph lags by up to one interval. At
T2we know how many bytes arrived sinceT1but nothing about what is arriving now, so this is the price of not extrapolating. - Sub-window bursts are flattened. A 30-second 900 Mbps burst inside a 15-minute window reads as ~30 Mbps sustained. The API does not carry within-window traffic shape, so the alternative isn't better resolution — it's the sawtooth above, which is a worse misrepresentation.
- A stalled API still reads as zero, not as invented traffic. The ramp advances on counter changes, not on the poll loop firing, so an idle port and a frozen upstream both correctly plateau.
Set counter_smoothing: raw to publish values unmodified — correct if your NMS
polls at or slower than poll_interval. max_ramp_seconds caps the observed
window past which ramping is skipped (0 derives it as 2.5x poll_interval), so
a multi-hour outage's accumulated bytes aren't smeared across an equally long
ramp, which would hide the recovery.
Configuration
Generated by init; see config.example.yaml for every
option with comments. The essentials:
msp:
domain: "dn-abc123.firewalla.net"
poll_interval: 900 # 15 min; see "Rate limits" above
counter_smoothing: ramp # or "raw"; see "Counter smoothing" above
max_backoff_seconds: 3600 # ceiling on 429 backoff
topology_cache: "/var/lib/firewalla-snmp-proxy/topology.json"
ping_host: "switch.example.com" # live reachability; prefer a hostname
ping_interval: 60
listen:
address: "127.0.0.1" # 0.0.0.0 to allow a remote NMS
base_port: 16100 # one port per switch
community: "public"
enterprise_oid: "1.3.6.1.4.1.99999"
state_file: "/var/lib/firewalla-snmp-proxy/counters.json"
switches:
- mac: "20:6D:31:00:00:01"
port: 16100
On state_file and topology_cache. The paths above are what a deployed
service uses, and are what init writes when it can write to
/var/lib/firewalla-snmp-proxy. Run init as an ordinary user and it writes
per-user paths instead ($XDG_STATE_HOME/firewalla-snmp-proxy/, normally
~/.local/state/firewalla-snmp-proxy/), because the quick start does not
create a root-owned state directory. Both files are just caches: losing them
costs you monotonic counters across a restart and an API-free startup, not
correctness. Set them explicitly if you want them somewhere else.
Replacing an existing SNMP proxy
If you already monitor the switch through some other proxy, your NMS keys its
device and OS detection on sysObjectID. Changing it makes the NMS treat this
as a brand-new device and abandon the historical port graphs. To migrate in
place, pin the old value:
sys_object_id: "1.3.6.1.4.1.8072.3.2.10.99.2" # whatever the old proxy reported
community: "your-old-community"
listen:
base_port: 16100 # the port your NMS already polls
Port RRDs are matched on ifDescr/ifIndex. This proxy names ports Port 1 …
Port N with ifIndex equal to the physical port number, so if your old proxy
did the same, history carries over with no further work.
The enterprise OID
99999 is a placeholder in unassigned IANA space. Assignments are currently
around 66649, so it won't collide for many years, and SNMP doesn't validate
enterprise numbers — everything works.
If you'd rather register your own (free, at
iana.org/assignments/enterprise-numbers),
change enterprise_oid and the matching ::= { enterprises 99999 } line in
mibs/FIREWALLA-SNMP-PROXY-MIB.txt. Note this gives the four vendor objects new
OIDs, so your NMS will start fresh graphs for them; everything in the standard
MIBs is unaffected.
Multiple switches
init finds them all and assigns consecutive ports:
switches:
- mac: "20:6D:31:00:00:01"
port: 16100
- mac: "20:6D:31:00:00:02"
port: 16101
One process serves all of them. Each gets its own UDP port rather than sharing
one port with different community strings, because some monitoring systems
identify a device by IP:port and would otherwise merge them.
What this deliberately does not publish
Absent beats invented. If the API doesn't supply a value, the OID isn't instantiated at all — a sparse table is legal SNMP and reads honestly, whereas a plausible zero shows up on your dashboard as fact.
- No temperature sensor. The Switch SE is fanless and reports
temperature: 0withfanStatus: "none"— it has no thermal sensor. Publishing 0 °C would trip low-temperature alarms. If a future model reports a real reading, the sensor appears automatically. - No
ifPhysAddress— the API gives no per-port MAC. - No PoE fault counters (
pethPsePortMPSAbsentCounterand friends) — zeros would read as "no PoE fault has ever occurred", which can't be substantiated. - No
pethMainPsePower(the PoE budget) — the API reportsbudgetUtilbut never the actual budget. - No Q-BRIDGE-MIB / VLAN tables — ports carry a Firewalla network UUID,
not an 802.1Q VLAN ID, so populating them would mean inventing tag numbers.
The network name is in
ifAliasandfwPortNetworkNameinstead. - No link up/down traps. The proxy polls on an interval and can't observe a
transition as it happens, so
ifLinkUpDownTrapEnablereportsdisabled(2).
Two values are passed through but deliberately uninterpreted, because their
meaning is undocumented and the obvious reading is wrong:
fwSwitchPoeBudgetUtil (observed as 114 alongside 10.5 W of draw — not a
percentage) and fwSwitchAclMax (observed as 256 against a count of 1229, so it
appears to bound only the control ACLs).
Troubleshooting
check fails with "endpoint does not exist"
Your MSP domain is probably wrong. On this API an unknown path returns HTTP 200
with the web console's HTML rather than a 404, so the client validates the
content type. Confirm the hostname you use to reach MSP in a browser.
"MSP API rejected the token" Regenerate it in the MSP web console. Tokens are per-account, not per-box.
"No Firewalla switches found" The account has no switch attached. This proxy needs a Firewalla Switch; it doesn't proxy the firewall box itself.
snmpwalk times out
Check listen.address. The default 127.0.0.1 only accepts local traffic — set
0.0.0.0 for a remote NMS, and open the UDP port in your firewall.
Counters flat at zero
Look at fwProxyPollStatus and fwProxyLastError; the poll is probably
failing. journalctl -u firewalla-snmp-proxy -f has the detail.
Vendor OIDs show as numbers, not names
The MIB isn't loaded in your NMS. Copy mibs/FIREWALLA-SNMP-PROXY-MIB.txt into
its MIB directory. Standard MIB objects resolve without it.
A negative spike in a traffic graph
The Firewalla reset its counters and state_file wasn't writable, so offsets
couldn't persist. Check permissions on the state directory.
Contributing
Bug reports and reports of other Firewalla switch models are the most useful things you can bring — see CONTRIBUTING.md for what to include and how to sanitize it first. Security issues should go through SECURITY.md, privately, not as a public issue.
Development
git clone https://github.com/kdesch5000/firewalla-snmp-proxy
cd firewalla-snmp-proxy
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest
The tests need no network and no Firewalla account — switch data comes from
fixtures in tests/fixtures/. The end-to-end tests bind a loopback UDP port and
drive the agent with a real SNMP client, so they exercise actual BER encoding,
community authentication and GETNEXT walks.
Layout:
firewalla_snmp_proxy/
oid.py sorted OID tree; all walk ordering lives here
model.py normalizes MSP API JSON into a Switch/Port model
counters.py persistent monotonic counter offsets
msp_api.py read-only MSP API client (stdlib HTTP only)
mibs/ one module per MIB
agent.py pysnmp wiring, one agent per switch
poller.py periodic refresh
cli.py init / check / walk / run
mibs/ the shipped vendor MIB
If you're adding a MIB module, the rule is: register nothing you can't substantiate. Values are registered as callables closing over a mutable context, so the tree is built once but always serves live data.
Acknowledgements
Built against the undocumented Firewalla MSP switch endpoints
(/v2/boxes/<gid>/topology and /v2/boxes/<gid>/switches/<mac>), which are not
in the published API reference. They may change without notice; the model layer
is defensive about missing fields for exactly that reason.
Not affiliated with or endorsed by Firewalla.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file firewalla_snmp_proxy-2.2.3.tar.gz.
File metadata
- Download URL: firewalla_snmp_proxy-2.2.3.tar.gz
- Upload date:
- Size: 123.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9a8bf13e6c2d1e46cdeb392a2dd5330d4ceaeed3ddd4d3cb8aa26ce40950afc
|
|
| MD5 |
feb9806d2c37a486a8b37de4b2dbb974
|
|
| BLAKE2b-256 |
7919c85ab8b4db191db46d13fc123c4db01b0844138bb8034b2b20e34dadc22f
|
Provenance
The following attestation bundles were made for firewalla_snmp_proxy-2.2.3.tar.gz:
Publisher:
release.yml on kdesch5000/firewalla-snmp-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
firewalla_snmp_proxy-2.2.3.tar.gz -
Subject digest:
d9a8bf13e6c2d1e46cdeb392a2dd5330d4ceaeed3ddd4d3cb8aa26ce40950afc - Sigstore transparency entry: 2668525186
- Sigstore integration time:
-
Permalink:
kdesch5000/firewalla-snmp-proxy@d388a7092b92d5266e71022979f6ae269c8434f5 -
Branch / Tag:
refs/tags/v2.2.3 - Owner: https://github.com/kdesch5000
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d388a7092b92d5266e71022979f6ae269c8434f5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file firewalla_snmp_proxy-2.2.3-py3-none-any.whl.
File metadata
- Download URL: firewalla_snmp_proxy-2.2.3-py3-none-any.whl
- Upload date:
- Size: 85.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4fe5ae84373581c937c8225d8181c0534edfdc5cc6853057fdd5ecc5a8bf9b6
|
|
| MD5 |
a3652fdaabe013db4ea7bf568dc82e21
|
|
| BLAKE2b-256 |
dec199ae8c00f64c41bc27eeb604df69b873eecff1c9a0e03e8899215fce9702
|
Provenance
The following attestation bundles were made for firewalla_snmp_proxy-2.2.3-py3-none-any.whl:
Publisher:
release.yml on kdesch5000/firewalla-snmp-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
firewalla_snmp_proxy-2.2.3-py3-none-any.whl -
Subject digest:
f4fe5ae84373581c937c8225d8181c0534edfdc5cc6853057fdd5ecc5a8bf9b6 - Sigstore transparency entry: 2668525196
- Sigstore integration time:
-
Permalink:
kdesch5000/firewalla-snmp-proxy@d388a7092b92d5266e71022979f6ae269c8434f5 -
Branch / Tag:
refs/tags/v2.2.3 - Owner: https://github.com/kdesch5000
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d388a7092b92d5266e71022979f6ae269c8434f5 -
Trigger Event:
push
-
Statement type: