You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Repeater flood control: reserve non-flood airtime, and contain floods by measured channel utilisation rather than by fields in the packet
Summary
The mesh in the Netherlands has been hit by repeated flood episodes, and it is wearing people down. During a burst the network stops being usable for ordinary messaging: normal traffic on my node drops from 11–20 packets per 5 min to 2, and every chat message arrives two, five, sometimes thirty times over. Genuine users are visibly frustrated — the public channel during an episode fills up with people asking why everything repeats, working out between themselves that the duplicates are all 1-byte-path copies, and calling it a new level of spam. Several have started muting the public channel entirely, which is the outcome nobody wants.
I captured four episodes in full detail. At peak, a single transmitter consumed roughly 1.4 kB/min of airtime on its own, and paths ballooned to 61 hops against a median of 5–7 for genuine traffic.
Every identifier the current design could rate-limit on turned out to be controlled by the sender: source hash, destination hash, ciphertext, node name, and the path itself. What was not controllable was how busy each repeater's own radio got.
This issue proposes flood control built on that observation, in three layers, none of which requires identifying anyone:
A reserved airtime floor for non-flood traffic, so the mesh keeps working during a flood.
A flood-forwarding budget that shrinks as measured local channel utilisation rises — this is what actually contains a flood to a radius, and it keys on a quantity that cannot be forged.
A drop policy inside that budget that prefers to shed high-hop-count packets first.
Layer 3 is the idea in #1588 and it is a good one — but on its own it is invertible, and layers 1 and 2 are what make it safe. More on that below.
A note on scope. I have deliberately left the source's location and the identifying packet-level detail out of this issue. The design argument does not depend on either, and a public bug tracker is not the place for them. Everything below is reproducible on any mesh.
Impact on users
This is the part that does not show up in a packet capture, so it is worth stating plainly.
During bursts the public channel becomes unreadable. A single message is delivered up to 32 times; across one 100-minute window, 76 unique messages arrived as 223 packets, and at worst 134 of 180 public-channel packets my node heard were duplicates rather than originals.
Users notice, and they discuss it in-channel while it is happening — "why all these repetitions?", "looks like messages are being duplicated as 1-byte", "this is a new level of spam". (Paraphrased and unattributed; they are ordinary users, not participants in this.)
Some have concluded the network is simply unreliable and have stopped using the public channel. That is a slow, quiet loss that will not reverse on its own.
Operators currently have no tooling to tell a flood from ordinary congestion. Diagnosing this took six days of manual work on raw rxlog dumps. Most operators will never do that, so they experience it as "the mesh is broken today".
The technical proposal below is aimed at the first and last of those: keep the mesh usable during an episode, and let operators see what is happening.
What was measured
Four episodes, all from the same transmitter — confirmed by the set of repeaters observed to receive it directly off the air, which did not change across any of the four. These are the ones I captured in full; they are not isolated incidents.
episode 1
episode 3
episode 4
rate
25.6 pkt/min
23.6 pkt/min
17.6 pkt/min
airtime proxy
1369 B/min
1316 B/min
986 B/min
flood path length
median 14, max 30
median 14, max 32
median 14, max 41
unique ciphertexts
161 / 161
174 / 174
108 / 108
distinct source hashes
121
123
94
Separately, a public-channel re-broadcast campaign from the same source produced 3× amplification — the 76-into-223 figure quoted above.
Genuine traffic on the same network, same logs, for comparison: REQ, RESPONSE, GRP_TXT, ANON_REQ, PATH and ADVERT all ran a median of 5–7 hops. The longest verified genuine path I recorded anywhere on the mesh was 23 hops, on a 2-byte-path packet crossing most of the region.
Observed failure modes
Path inflation towards flood_max. Flood paths reached 61 hops against a default _prefs.flood_max of 64. Genuine traffic never came close. Whatever the mechanism — dedup exhaustion, loops, or deliberate padding — the effect is that a single packet is retransmitted dozens of times across the mesh.
Duplicate suppression appears to break down under load. One ciphertext arrived at my node twice, 8 minutes apart, having taken two different routes. Several paths contain the same hop byte 2–6 times, including six consecutive identical bytes in one case, which no legitimate relay chain produces.
Question for maintainers: does the duplicate-detection key include the path bytes and transport codes? If so, a packet whose path has mutated is treated as new, which converts a linear flood into a superlinear storm. If it already excludes them, then the observed re-arrivals suggest the table is being exhausted by volume and needs a time-based rather than slot-count-based window.
Path metadata is forged. Partway through, the transmitter began pre-loading a fabricated path before transmitting. This is verifiable against any independent observer network that records full paths: on one message, all 275 observations from 101 separate observers begin with an identical 4-byte block, and no observer anywhere heard it at fewer than 4 hops. On another, all 366 observations share an identical 3-byte block and only the fourth byte varies by observer. The fabricated block ranged from 1 to 10 bytes across episodes.
This matters for design: path length and path contents are sender-controlled inputs.
Why the obvious controls did not hold
control
why it failed here
flood_max (default 64)
far above anything genuine traffic needs; the flood used almost all of it
a user in the channel noted this during the event: change the name, block is gone
block by public key
raises cost, but keys rotate; also needs every operator to act
rate limit by sender hash
94 distinct source hashes across 108 packets
dedup by content
every ciphertext unique, even for identical plaintext
The specific problem with hop-count-only prioritisation
Dropping longer paths before shorter ones is intuitively right: further away means less important to relay, and it naturally contains a flood to a radius.
But the sender chooses the hop count of their own packets. Under a global "prefer short paths" rule, transmitting with path_len = 0 makes that traffic the highest priority in the entire network — strictly worse than today. We already know this transmitter manipulates the path field deliberately, so this is not a hypothetical.
The fix is not to abandon the idea. It is to cap the flood budget first, and only then use hop count to decide what to drop within it. Gaming the drop policy then buys nothing beyond a cap that has already been hit.
Proposal
1. Reserve airtime for non-flood traffic
Flood forwarding must never be able to consume the whole duty cycle. Introduce something like flood_budget_pct (suggest 50% default) as the maximum share of the repeater's duty-cycle allowance that may go to forwarding flood packets. Direct/routed traffic, ACKs, non-flood adverts and path replies draw from the reserved remainder and cannot be starved. Flood adverts should also be dropped in the flood budget, or perhaps even sooner as they are not that important to continue under load.
This alone would have prevented the symptom users actually complain about: the mesh becoming unusable for ordinary messaging during bursts.
2. Shrink the flood budget as the channel gets busy
Make flood_budget_pct a function of measured local channel utilisation over a rolling window, rather than a constant. Something monotonically decreasing — full budget below ~20% utilisation, tapering towards a floor above ~60%.
This is the part that achieves containment, and it is the reason to prefer it over any packet-field-based scheme:
It cannot be forged. A sender controls every byte they transmit; they do not control how busy your receiver observes the channel to be.
It self-limits by radius. Near the source the channel saturates, budgets collapse, and propagation stops. Far away the channel is quiet, budgets are full, and normal traffic is unaffected. The flood is contained to roughly the area it is actually harming — which is exactly the goal, arrived at without any policy about who is transmitting.
It helps with honest congestion too, not just abuse.
The same signal should feed the rebroadcast delay: back off harder when busy, in the spirit of CSMA. There is already precedent for a limiter of this shape in the codebase — discover_limiter.allow() enforcing 4 responses per 120 s.
3. Drop policy within the budget
When the flood budget is exhausted, shed load using:
hop count, probabilistically — drop probability rising with path_len, RED-style rather than a hard cliff, so behaviour degrades smoothly and the boundary is not a sharp exploitable edge. This is [Feature Request] Repeater limit channel flooding #1588's idea, made safe by being bounded by layers 1 and 2.
best-effort fairness across the claimed previous hop — deficit round-robin over the last path byte, so one busy neighbour cannot starve the others. Worth stating plainly: the previous hop is also sender-controlled, so treat this as protection against accidental floods and honest congestion, not as an anti-abuse control.
4. Lower the flood_max default
64 is very generous. Against measured data — genuine traffic median 5–7 hops, longest verified genuine path 23 — a default in the region of 32 would leave real traffic untouched while halving the ceiling on flood amplification. This is a one-line change and probably the highest value-per-effort item in this issue.
5. Counters
Please expose per-repeater counters: packets dropped by reason, flood vs non-flood airtime used, dedup insertions and evictions, and measured channel utilisation. Working this out took six days of reverse-engineering from raw rxlog dumps and observer data. With counters it would have been visible in an afternoon, and the operators fielding "why is the mesh broken today" could answer it themselves.
Why this is neutral
No blocklists, no allowlists, no identity, no reputation, no per-user policy, nothing that requires knowing who is responsible. Every repeater independently protects its own airtime and its own neighbourhood. A well-behaved node in a quiet area sees no change at all. A node in a saturated area throttles flooding regardless of whether the cause is malice, a misconfiguration, or a genuinely busy day.
That last point matters: the same mechanism handles the accidental case. A large part of what users are experiencing as "spam" is not the direct-message flood at all but a re-broadcast loop — a device re-originating public-channel messages it heard, with a clock reading roughly 823 days in the past and sender names truncated to exactly 23 bytes. That looks far more like broken firmware than an attack, and no identity-based control would have helped, because the operator almost certainly does not know it is happening. (If those two fingerprints ring a bell for anyone, I would be glad to hear it — a fix upstream would remove a meaningful share of the traffic, and a good share of the frustration with it.)
Relationship to existing work
[Feature Request] Repeater limit channel flooding #1588 — configurable hop limit for channel flooding. Complementary; this issue argues for the budget layers underneath it and explains why hop count should not be the primary key.
_prefs.flood_max, _prefs.disable_fwd, RegionMap / REGION_DENY_FLOOD already provide coarse controls. disable_fwd and region denial are all-or-nothing; what is missing is graceful degradation under load.
Open questions
Does the dedup key include the mutable path and transport-code bytes?
How is the dedup table sized and evicted, and what happens when it is full under sustained load?
Is channel-utilisation measurement already available on the supported radios at low enough cost to sample continuously?
Would maintainers prefer this split into separate issues per layer?
Repeater flood control: reserve non-flood airtime, and contain floods by measured channel utilisation rather than by fields in the packet
Summary
The mesh in the Netherlands has been hit by repeated flood episodes, and it is wearing people down. During a burst the network stops being usable for ordinary messaging: normal traffic on my node drops from 11–20 packets per 5 min to 2, and every chat message arrives two, five, sometimes thirty times over. Genuine users are visibly frustrated — the public channel during an episode fills up with people asking why everything repeats, working out between themselves that the duplicates are all 1-byte-path copies, and calling it a new level of spam. Several have started muting the public channel entirely, which is the outcome nobody wants.
I captured four episodes in full detail. At peak, a single transmitter consumed roughly 1.4 kB/min of airtime on its own, and paths ballooned to 61 hops against a median of 5–7 for genuine traffic.
Every identifier the current design could rate-limit on turned out to be controlled by the sender: source hash, destination hash, ciphertext, node name, and the path itself. What was not controllable was how busy each repeater's own radio got.
This issue proposes flood control built on that observation, in three layers, none of which requires identifying anyone:
Layer 3 is the idea in #1588 and it is a good one — but on its own it is invertible, and layers 1 and 2 are what make it safe. More on that below.
Impact on users
This is the part that does not show up in a packet capture, so it is worth stating plainly.
rxlogdumps. Most operators will never do that, so they experience it as "the mesh is broken today".The technical proposal below is aimed at the first and last of those: keep the mesh usable during an episode, and let operators see what is happening.
What was measured
Four episodes, all from the same transmitter — confirmed by the set of repeaters observed to receive it directly off the air, which did not change across any of the four. These are the ones I captured in full; they are not isolated incidents.
Separately, a public-channel re-broadcast campaign from the same source produced 3× amplification — the 76-into-223 figure quoted above.
Genuine traffic on the same network, same logs, for comparison: REQ, RESPONSE, GRP_TXT, ANON_REQ, PATH and ADVERT all ran a median of 5–7 hops. The longest verified genuine path I recorded anywhere on the mesh was 23 hops, on a 2-byte-path packet crossing most of the region.
Observed failure modes
Path inflation towards
flood_max. Flood paths reached 61 hops against a default_prefs.flood_maxof 64. Genuine traffic never came close. Whatever the mechanism — dedup exhaustion, loops, or deliberate padding — the effect is that a single packet is retransmitted dozens of times across the mesh.Duplicate suppression appears to break down under load. One ciphertext arrived at my node twice, 8 minutes apart, having taken two different routes. Several paths contain the same hop byte 2–6 times, including six consecutive identical bytes in one case, which no legitimate relay chain produces.
Path metadata is forged. Partway through, the transmitter began pre-loading a fabricated path before transmitting. This is verifiable against any independent observer network that records full paths: on one message, all 275 observations from 101 separate observers begin with an identical 4-byte block, and no observer anywhere heard it at fewer than 4 hops. On another, all 366 observations share an identical 3-byte block and only the fourth byte varies by observer. The fabricated block ranged from 1 to 10 bytes across episodes.
This matters for design: path length and path contents are sender-controlled inputs.
Why the obvious controls did not hold
flood_max(default 64)The specific problem with hop-count-only prioritisation
Dropping longer paths before shorter ones is intuitively right: further away means less important to relay, and it naturally contains a flood to a radius.
But the sender chooses the hop count of their own packets. Under a global "prefer short paths" rule, transmitting with
path_len = 0makes that traffic the highest priority in the entire network — strictly worse than today. We already know this transmitter manipulates the path field deliberately, so this is not a hypothetical.The fix is not to abandon the idea. It is to cap the flood budget first, and only then use hop count to decide what to drop within it. Gaming the drop policy then buys nothing beyond a cap that has already been hit.
Proposal
1. Reserve airtime for non-flood traffic
Flood forwarding must never be able to consume the whole duty cycle. Introduce something like
flood_budget_pct(suggest 50% default) as the maximum share of the repeater's duty-cycle allowance that may go to forwarding flood packets. Direct/routed traffic, ACKs, non-flood adverts and path replies draw from the reserved remainder and cannot be starved. Flood adverts should also be dropped in the flood budget, or perhaps even sooner as they are not that important to continue under load.This alone would have prevented the symptom users actually complain about: the mesh becoming unusable for ordinary messaging during bursts.
2. Shrink the flood budget as the channel gets busy
Make
flood_budget_pcta function of measured local channel utilisation over a rolling window, rather than a constant. Something monotonically decreasing — full budget below ~20% utilisation, tapering towards a floor above ~60%.This is the part that achieves containment, and it is the reason to prefer it over any packet-field-based scheme:
The same signal should feed the rebroadcast delay: back off harder when busy, in the spirit of CSMA. There is already precedent for a limiter of this shape in the codebase —
discover_limiter.allow()enforcing 4 responses per 120 s.3. Drop policy within the budget
When the flood budget is exhausted, shed load using:
path_len, RED-style rather than a hard cliff, so behaviour degrades smoothly and the boundary is not a sharp exploitable edge. This is [Feature Request] Repeater limit channel flooding #1588's idea, made safe by being bounded by layers 1 and 2.4. Lower the
flood_maxdefault64 is very generous. Against measured data — genuine traffic median 5–7 hops, longest verified genuine path 23 — a default in the region of 32 would leave real traffic untouched while halving the ceiling on flood amplification. This is a one-line change and probably the highest value-per-effort item in this issue.
5. Counters
Please expose per-repeater counters: packets dropped by reason, flood vs non-flood airtime used, dedup insertions and evictions, and measured channel utilisation. Working this out took six days of reverse-engineering from raw
rxlogdumps and observer data. With counters it would have been visible in an afternoon, and the operators fielding "why is the mesh broken today" could answer it themselves.Why this is neutral
No blocklists, no allowlists, no identity, no reputation, no per-user policy, nothing that requires knowing who is responsible. Every repeater independently protects its own airtime and its own neighbourhood. A well-behaved node in a quiet area sees no change at all. A node in a saturated area throttles flooding regardless of whether the cause is malice, a misconfiguration, or a genuinely busy day.
That last point matters: the same mechanism handles the accidental case. A large part of what users are experiencing as "spam" is not the direct-message flood at all but a re-broadcast loop — a device re-originating public-channel messages it heard, with a clock reading roughly 823 days in the past and sender names truncated to exactly 23 bytes. That looks far more like broken firmware than an attack, and no identity-based control would have helped, because the operator almost certainly does not know it is happening. (If those two fingerprints ring a bell for anyone, I would be glad to hear it — a fix upstream would remove a meaningful share of the traffic, and a good share of the frustration with it.)
Relationship to existing work
_prefs.flood_max,_prefs.disable_fwd,RegionMap/REGION_DENY_FLOODalready provide coarse controls.disable_fwdand region denial are all-or-nothing; what is missing is graceful degradation under load.Open questions
More information about the Dutch mesh
https://settings.dutchmeshcore.nl/en/
https://mc-spamdetector.nl/