A loop only the kernel could see
This fault was not provoked. It was found by accident on 2026-09-12, while measuring something else, on the owner’s production RB5009UG+S+ (RouterOS 7.24.2, kernel 5.6.3), and it was diagnosed and fixed the same day. It is the clearest answer this project has to “why not just poll the RouterOS API?”: for its whole life the API reported a healthy device.
The page follows the diagnosis in the order it happened, because the order is the method.
The symptom
Section titled “The symptom”/snapshot carried a steady stream of events — the agent’s kmsg source — at 1.49 /s:
[6] br0: port 2(eth1) entered blocking state[4] br0: received packet on eth1 with own address as source address (addr:00:00:5e:00:53:5d, vlan:0)[6] br0: port 2(eth1) entered learning stateThe address in the second line is the router’s own: the MAC of sfp-sfpplus1,
coming back into the bridge. It is shown here as 00:00:5e:00:53:5d, from the
block RFC 7042 reserves for documentation — on your device it is your bridge’s
address, and that is how you recognise the line.
Step 1 — establish that it is real, and get a number
Section titled “Step 1 — establish that it is real, and get a number”A single alarming log line is an anecdote. A rate is a measurement, and a rate is what tells you later whether a fix worked. Count events per second over a window long enough to be stable:
curl -s "http://172.30.10.2:9123/snapshot?seconds=120" \ | python3 -c 'import sys, json, collectionsrows=[json.loads(l) for l in sys.stdin if l.strip()]secs=sum(r["dt_ns"] for r in rows)/1e9ev=[e for r in rows for e in r.get("events",[])]print(f"{len(ev)/secs:.2f} events/s over {secs:.0f}s")c=collections.Counter(e["msg"].split("(")[0][:60] for e in ev)for k,v in c.most_common(): print(f" {v:5d} {k}")'?seconds= takes 1 to 3600, and the agent can only return what its ring still
holds — 300 s at the default BUFFER_S. The window’s length comes from summing
each sample’s own dt_ns, not from the number you asked for.
Step 2 — read the timing, not just the text
Section titled “Step 2 — read the timing, not just the text”The gaps between the reflected frames were 2.00–2.01 s, every time. That is not a coincidence to be noted and moved past: 2 s is the STP hello interval. The mechanism was therefore “the router emits a BPDU and receives its own BPDU back”, which is a loop, not a misbehaving client.
Timing is usually where the diagnosis actually lives:
# gaps between consecutive occurrences of one message... | python3 -c 'import sys, jsonts=sorted(e["us"]/1e6 for l in sys.stdin if l.strip() for e in json.loads(l).get("events",[]) if "own address" in e["msg"])print([round(ts[i+1]-ts[i],2) for i in range(len(ts)-1)][:12])'us is the kernel’s own timestamp for the record, in microseconds since boot on
the monotonic clock — not the time the agent read it — so the gaps are the
kernel’s, not the sampler’s.
Step 3 — confirm the API really is blind
Section titled “Step 3 — confirm the API really is blind”Worth doing explicitly, because it decides where you spend the next hour:
/log/print where topics~"bridge" or topics~"stp" or topics~"interface"/interface/bridge/port/monitor [find] onceBoth came back clean: zero log rows on those topics, every port
designated-port / in-bridge. RouterOS was not hiding the fault; it genuinely
does not surface this class of kernel event.
Step 4 — find corroborating evidence outside the kernel log
Section titled “Step 4 — find corroborating evidence outside the kernel log”A single source, however convincing, is one source. Two independent observations
pointing the same way is a diagnosis. The MAC in the message belonged to
sfp-sfpplus1, so the obvious suspicion was the SFP+ segment — but the bridge’s
own tables said otherwise:
# hosts learned per port:foreach p in=[/interface/bridge/port/find] do={ \ :local n [/interface/bridge/port/get $p interface]; \ :put ($n . " hosts=" . [:len [/interface/bridge/host/find interface=$n]]) }| Port | Learned MACs | Traffic | RSTP edge |
|---|---|---|---|
sfp-sfpplus1 |
60 | 23.8 GB rx | true |
ether2 |
0 | 9.15 GB rx / 22.4 M packets | false |
| others | 0–2 | — | true |
Scroll sideways to see every column
ether2 was passing 22 million packets on a healthy 1 Gbps link and the bridge
had learned nothing behind it — which is what a bridge does on a port where
it keeps seeing its own addresses. It was also the only port receiving BPDUs.
Both anomalies on the same port.
The kernel log had said eth1, not ether2. Establishing that those are the
same port cost real time that day; RouterOS ports and kernel
names is how to do it in one safe step, and
why the agent does it for you on this board.
Step 5 — make a change that tests the hypothesis
Section titled “Step 5 — make a change that tests the hypothesis”The hypothesis was “the device on ether2 has a second path to the router
through the other APs, which hang off the switch”. Two changes were made on the
switch first, and the honest result is that they did nothing:
| Change | Event rate |
|---|---|
| baseline | 1.49 /s |
| switch change 1 | 1.67 /s |
| switch change 2 | 1.47 /s |
| (noise band) | ±0.2 /s |
One further reading in the same series, 1.50 /s, was also inside that band.
That is a useful outcome, not a wasted step: it ruled out the switch with evidence. The lesson for a playbook is to always re-measure after a change, even one you expect to work, and to know your noise band before you interpret a difference.
Step 6 — the fix, and confirming it two ways
Section titled “Step 6 — the fix, and confirming it two ways”The firmware of the mesh APs (Deco units) was updated; the cabling was not touched. Then:
events 9 -> 0.03/s (baseline 1.49/s)and of those nine, none was the loop message — they were the APs coming back
(eth1: phy link up, eth1: set isolation from 0 to 1). The loop message, which
had appeared every 2 s (30 times a minute, with twice as many blocking/learning
records around it), appeared zero times in 300 s.
The second, independent confirmation is the one worth internalising: the bridge’s view became coherent.
| Before | After | |
|---|---|---|
MACs on ether2 |
0 | 24 |
MACs on sfp-sfpplus1 |
60 | 36 |
ether2 edge |
false | true |
The same 60 devices, redistributed 36/24 instead of 60/0. While the loop existed,
the router was learning nearly every device on the wrong port; the Zigbee
coordinator 00:4B:12:96:80:33, for instance, moved from the SFP+ to ether2,
where it actually lives. A fix that makes a second unrelated measurement fall
into place is a fix you can trust.
The signature
Section titled “The signature”A real fault, not provoked ·
- A steady, periodic stream of kernel
eventsat idle, where the healthy state is zero. received packet on <port> with own address as source address, with the router’s own MAC in it.- Inter-arrival times of 2.00–2.01 s: the STP hello interval.
- A busy port on which the bridge has learned no hosts, with
edge=falsewhile its neighbours aretrue. /log/printand/both clean.interface/ bridge/ port/ monitor
What to take from it
Section titled “What to take from it”- Alarming text is a lead; a rate is evidence; a rate before and after is a conclusion.
- Read the inter-arrival times. They often name the protocol for you.
- Prefer a change that discriminates between hypotheses over a change that merely might fix things.
- Distrust a fix that only your primary instrument can confirm.