Skip to content

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.

/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 state

The 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:

Terminal window
curl -s "http://172.30.10.2:9123/snapshot?seconds=120" \
| python3 -c '
import sys, json, collections
rows=[json.loads(l) for l in sys.stdin if l.strip()]
secs=sum(r["dt_ns"] for r in rows)/1e9
ev=[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:

Terminal window
# gaps between consecutive occurrences of one message
... | python3 -c '
import sys, json
ts=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] once

Both 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

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.

A real fault, not provoked ·

  • A steady, periodic stream of kernel events at 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=false while its neighbours are true.
  • /log/print and /interface/bridge/port/monitor both clean.
  • 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.