Files
HaloKeymind/docs/flood_filtering.md
T

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 codes
  • 0x01 / 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-unmatched GRP_TXT and GRP_DATA; plain * is its alias.
  • login:* handles REQ, RESPONSE, TXT_MSG, ANON_REQ, and PATH.
  • 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, and response cannot be blocked at received hops 0-6; configured rules begin applying at hop 7.
  • Flood txt_msg cannot be blocked at received hops 0-4; configured rules begin applying at hop 5.

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:

  • N matches exactly N received hops.
  • N+ matches N or more received hops.
  • N-M matches the inclusive range.
  • all matches 0 through 63 hops.
  • 0+, all, and omitting the hop expression are equivalent. get reports the stored range using the canonical spelling all.

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:

  • public for the built-in Public channel
  • #name for 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 message
  • rate=X/min — retransmit at most X messages per local 60-second window
  • hops=N — do not retransmit when the received path count is N or higher
  • path=H1[,H2,H3] — require the first one to three path hashes to match
  • path=* — 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:

  1. flood.channel.scope first rewrites a matching unscoped flood packet.
  2. repeat, flood.max*, and the channel-data gate are checked.
  3. flood.filter checks payload type and hop range, subject to the login floor of 7 and flood-text floor of 5 described above.
  4. flood.channel.block checks keyed channels.
  5. Region and loop-detection rules are checked.
  6. flood.moderation checks 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.