16 KiB
Repeater Flood Filtering and Moderation
This guide explains the Keymind repeater forwarding filters. The filters decide whether this repeater retransmits a packet. They do not stop local reception, packet logging, or MQTT observation.
Only flood routes are filtered:
0x00/ROUTE_TYPE_TRANSPORT_FLOOD— flood routing with transport codes0x01/ROUTE_TYPE_FLOOD— unscoped flood routing
Direct routes 0x02 and 0x03 are never affected by these rules.
The route and payload values follow the upstream
packet-format reference and
payload layouts, with this fork's LoRa
OTA assignment noted below.
Before making changes
Show the current forwarding controls:
get repeat
get flood.max
get flood.max.unscoped
get flood.max.advert
get flood.channel.data
get flood.channel.data.hops
get flood.channel.block
get flood.channel.scope
get flood.filter
get flood.moderation
The flood.filter and flood.moderation tables each have 16 persistent slots
and start empty. A row can opt into suspend=tempradio; temporary radio is not
synonymous with OTA and can carry normal packet types too. A corrupt or
truncated table fails open, so corrupt storage does not silently enable
blocking.
Force unscoped floods into a transport scope
flood.channel.scope can convert a received unscoped flood into a
transport-scoped flood before this repeater forwards it:
set flood.channel.scope <channel|txt:*|login:*|other:*> <region>
set flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region>
get flood.channel.scope
get flood.channel.scope.<slot>
del flood.channel.scope.<slot>
del flood.channel.scope all
The channel may be public, #channel, or a 128/256-bit hex key. The region
must already exist and provide a usable transport key. Keyed rules first check
the one-byte channel hash carried in the packet, then validate the MAC by
decrypting with the configured channel key. A hash collision alone cannot
force a scope.
There are three independent wildcard classes:
txt:*handles otherwise-unmatchedGRP_TXTandGRP_DATA; plain*is its alias.login:*handlesREQ,RESPONSE,TXT_MSG,ANON_REQ, andPATH.other:*handles every remaining flood payload type except TRACE, including OTA. TRACE is deliberately exempt from forced-scope wildcards.
login:* and other:* classify only the visible outer payload type; they do
not authenticate its contents. Exact channel rows with usable target regions
always take precedence over txt:*, even if that wildcard has a lower slot
number. A missing or unusable target is skipped, so later exact rows and then
the applicable wildcard are tried. Within a wildcard class, the lowest usable
duplicate row wins.
On a successful match, the route changes from ROUTE_TYPE_FLOOD to
ROUTE_TYPE_TRANSPORT_FLOOD. Transport code 0 is calculated with the target
region key over the payload type and payload; code 1 remains zero. The change
happens before region enforcement, forwarding filters, and deduplication.
Consequently flood.max.unscoped no longer applies to the rewritten packet,
but flood.max, target-region permissions, flood.filter, channel blocking,
loop detection, and moderation still do.
Already-scoped floods and direct routes are never rewritten. TRACE is never rewritten even in flood form; its existing code, if any, is preserved and it bypasses region/unknown-code enforcement. Scope assignment also does not override normal payload validation or make an otherwise non-forwardable packet type forwardable.
LoRa OTA (0x0C) falls under other:*. A matching row adds the selected
transport code, but OTA still operates normally during the temporary-radio
window because the OTA handler accepts both unscoped and transport-scoped flood
routes. The target region must allow flooding. The OTA core is already dormant
outside the temporary-radio window, so no default OTA filter row is installed.
Capacity is selected at build time:
- Roomy ESP32 builds: 255 slots, 9,180 bytes RAM, 9,185-byte file.
- DRAM-tight classic ESP32 LoRa-OTA repeaters, nRF52, and other normal constrained builds: 31 slots, 1,116 bytes RAM, 1,121-byte file.
- Very-tight STM32WL builds: 15 slots, 540 bytes RAM, 545-byte file.
- The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 slots, 144 bytes RAM, 149-byte file. This minimum holds the three wildcard classes and one exact channel mapping.
The region map still has 32 named-region entries. Large ESP32 tables can map many channels to the same targets, but cannot reference more than 32 distinct configured region names.
Interaction with duplicate detection
The seen-packet hash contains the payload type and exact payload bytes. It does
not contain the route type, either transport code, or the ordinary flood path.
For TRACE only, the encoded path_len byte is also included. Therefore an
unscoped packet and the same packet after this repeater adds a transport code
are the same duplicate. A later copy with a different scope is also the same
duplicate; changing or adding scope cannot evade the seen table.
When equivalent non-TRACE flood copies overlap in rxdelay, the normal receive-quality timing
still chooses the packet to process, but that winner takes a scope from the
queued copies whose transport code matches an allowed region in this repeater.
Unknown and denied scopes are ignored. If eligible copies have different
scopes, the shortest received path supplies the scope. Equal path lengths
prefer the deepest matching child region (the narrowest configured scope). A
remaining tie keeps queue order. The winner keeps its own path, SNR, and delay
schedule; only its route and transport codes can change, including replacement
of a less-preferred scope it already carried.
Scope selection happens at dequeue so the original scopes remain available for comparison. It applies only while copies are queued and cannot alter a copy already processed into the seen table. TRACE is excluded from scope arbitration entirely, so rxdelay never adds or replaces a trace transport code.
Filter by payload type and received hop count
Use flood.filter when the packet type and its current path length are enough
to make the decision:
set flood.filter <type> [hops] [suspend=tempradio]
set flood.filter.<slot> <type> [hops] [suspend=tempradio]
get flood.filter
get flood.filter.<slot>
del flood.filter.<slot>
del flood.filter all
Without a slot number, set reuses an identical rule, including its suspension
setting, or selects the first empty slot. With a slot number, it replaces that
slot. Omitting the hop expression means all (0-63).
No flood-filter row is created automatically. Operators may add
suspend=tempradio to any row that should be skipped while the radio is on a
temporary channel.
Suspension does not approve a packet or bypass the rest of the filter table. It
skips that row, then evaluation continues with the next row and the remaining
forwarding gates. An ordinary any row therefore still applies during the
temporary-radio window, subject to the short-path remote-admin protection
below. repeat, flood.max*, region, loop-detection, and the OTA subsystem's
own hop limit also remain in force.
Standard traceroute uses direct routing and never enters flood.filter. For a
custom flood-form trace, catch-all any rows are deliberately ignored; only an
explicit trace row can match it. The stock core does not normally
flood-forward TRACE packets.
Remote administration cannot be type-filtered on short paths
flood.filter uses two minimum filterable hop counts:
anon_req,path, andresponsecannot be blocked at received hops0-6; configured rules begin applying at hop7.- Flood
txt_msgcannot be blocked at received hops0-4; configured rules begin applying at hop5.
req, ack, and multipart ACK have no special floor and remain filterable from
hop 0.
A flooded login starts as ANON_REQ; its reply is commonly a PATH packet
carrying an encrypted RESPONSE. Before a direct return path is established,
administrative replies and CLI text can also be flooded. Transit repeaters do
not have the session key and cannot distinguish those encrypted admin exchanges
from ordinary peer packets with the same outer type. Each hop floor therefore
covers the complete outer packet class, not only packets that ultimately
authenticate as administrators.
This protects only against the configurable flood.filter table. It does not
override repeat, flood.max*, region enforcement, loop detection, or other
forwarding gates.
Hop expressions are based on the path count when this repeater receives the packet:
Nmatches exactlyNreceived hops.N+matchesNor more received hops.N-Mmatches the inclusive range.allmatches0through63hops.0+,all, and omitting the hop expression are equivalent.getreports the stored range using the canonical spellingall.
Examples:
# Stop forwarding group data once it arrives with four or more path entries.
set flood.filter grp_data 4+
# Stop long adverts, while still allowing shorter adverts.
set flood.filter.2 advert 6+
# Keep LoRa OTA floods from crossing this repeater at path counts 2 through 4.
set flood.filter.3 ota 2-4
# Apply a hard ceiling to flood payload types at 12 or more received hops.
set flood.filter any 12+
Accepted payload names are:
| Value | Short name | Full name |
|---|---|---|
0x00 |
req |
PAYLOAD_TYPE_REQ |
0x01 |
response |
PAYLOAD_TYPE_RESPONSE |
0x02 |
txt_msg |
PAYLOAD_TYPE_TXT_MSG |
0x03 |
ack |
PAYLOAD_TYPE_ACK |
0x04 |
advert |
PAYLOAD_TYPE_ADVERT |
0x05 |
grp_txt |
PAYLOAD_TYPE_GRP_TXT |
0x06 |
grp_data |
PAYLOAD_TYPE_GRP_DATA |
0x07 |
anon_req |
PAYLOAD_TYPE_ANON_REQ |
0x08 |
path |
PAYLOAD_TYPE_PATH |
0x09 |
trace |
PAYLOAD_TYPE_TRACE |
0x0A |
multipart |
PAYLOAD_TYPE_MULTIPART |
0x0B |
control |
PAYLOAD_TYPE_CONTROL |
0x0C |
ota |
PAYLOAD_TYPE_OTA in this fork |
0x0D |
13 |
reserved |
0x0E |
14 |
reserved |
0x0F |
raw_custom |
PAYLOAD_TYPE_RAW_CUSTOM |
Decimal values 0 through 15, hexadecimal values 0x00 through 0x0F, the
full PAYLOAD_TYPE_* names, and any are also accepted. Upstream currently
reserves 0x0C; this fork assigns it to LoRa OTA. Rows are suspended during
temporary-radio operation only when explicitly configured that way.
Moderate group text by channel and username
Use flood.moderation for flood GRP_TXT messages. The repeater validates and
decrypts the selected channel, extracts the display name before the first :,
then applies the rule:
set flood.moderation <channel> <sender> <action> [action...]
set flood.moderation.<slot> <channel> <sender> <action> [action...]
get flood.moderation
get flood.moderation.<slot>
del flood.moderation.<slot>
del flood.moderation all
Channels can be specified as:
publicfor the built-in Public channel#namefor a well-known hashtag channel- a 128-bit or 256-bit channel key in hexadecimal
The key is stored locally so packets can be authenticated and decrypted. It is
not included in get flood.moderation output.
Available actions are:
drop— do not retransmit any matching messagerate=X/min— retransmit at mostXmessages per local 60-second windowhops=N— do not retransmit when the received path count isNor higherpath=H1[,H2,H3]— require the first one to three path hashes to matchpath=*— match every path; this is the default
At least one of drop, rate=X/min, or hops=N is required. Rate and hop
limits can be combined. rate=0/min is equivalent to drop.
Per-user, per-channel rate limits
Rate limits require an exact username; * is not accepted for a rate rule.
Username comparison is ASCII case-insensitive, and names containing spaces must
be quoted. A rule's counter is independent from rules for the same name on
other channels, so this directly supports “X messages per minute from user X
on channel Y.” Counters are local to this repeater and reset on reboot.
# At most five Public-channel messages per minute from this display name.
set flood.moderation public "Noisy User" rate=5/min
# A separate limit for the same name on #local.
set flood.moderation #local "Noisy User" rate=10/min
# Combine a rate limit with a maximum forwarding distance.
set flood.moderation public alice rate=4/min hops=5
Match the start of a path
Path matching accepts one-, two-, or three-byte hashes. Every hash in one rule must use the same width, and matching always starts at the beginning of the received path:
set flood.moderation #local bot drop path=A1B2C3,D4E5F6
set flood.moderation public alice rate=3/min path=71
A path-qualified rule cannot match a zero-hop packet and does not match until the packet contains all path entries listed by the rule.
How the forwarding controls combine
A flood packet is retransmitted only if it passes every applicable control. In other words, the controls combine as deny rules:
flood.channel.scopefirst rewrites a matching unscoped flood packet.repeat,flood.max*, and the channel-data gate are checked.flood.filterchecks payload type and hop range, subject to the login floor of7and flood-text floor of5described above.flood.channel.blockchecks keyed channels.- Region and loop-detection rules are checked.
flood.moderationchecks decrypted group text, username, rate, hops, and path.
The first denial is enough to prevent retransmission. A packet that is denied can still appear in local logs or MQTT output. Moderation runs last because its rate counters are charged only for packets that pass every other forwarding control and will actually be retransmitted.
Delegate filter management
ACL permission 5 is the filter-manager role:
setperm <companion-public-key-hex> 5
A filter manager can read non-secret operational status and manage repeat,
loop.detect, flood.max*, flood.channel.data*, flood.channel.block*,
flood.filter*, and flood.moderation*. Delegated get access uses an
explicit allowlist: it cannot retrieve guest, WiFi, MQTT, bridge, or other
credentials, and it cannot change regions, ACL entries, radio settings, or
unrelated administrator settings.
ACL permission 4, the region/scope-manager role, can read, add, replace, and
delete flood.channel.scope rows and manage regions. This lets the same
delegate create target regions and assign them to forced-scope rows.
Security limitations
Public and hashtag channels use shared, well-known keys. A valid channel MAC
proves that the sender knew the channel key; it does not identify a person.
The <sender> value is an unverified display name and can be spoofed. Path
hashes are truncated routing hints and can collide or be manipulated; they are
not authenticated user identities.
Use username and path rules as traffic moderation, not as an authorization boundary. For a strict network boundary, combine these tools with region ACLs, private transport/channel keys, and controlled device access.
Restore the factory-seeded channel block
The repeater's factory-seeded forwarding row can be restored through the CLI:
set flood.channel.block.1 #wardriving h=4
This explicitly replaces slot 1. Inspect the slot first if it may now contain
another rule. To preserve existing slot assignments, omit .1; the command
then updates an identical channel row or uses the first empty slot.
Remove the custom rules
To save both tables in an empty state:
del flood.filter all
del flood.moderation all
get flood.filter
get flood.moderation
This does not change the older flood.max*, channel-block, loop-detection, or
region settings; inspect or reset those separately when troubleshooting.