Add repeater flood filters and transport scopes

This commit is contained in:
mikecarper committed 2026-07-17 00:35:51 -07:00
1 parent 5e030ce77b
commit 09c8aaba9c
25 files changed
+1222 -111

No files matched your search

+172 -12
View File
@@ -982,6 +982,14 @@ writes that slot.
`set flood.channel.block #wardriving h=<all|1-7|default>` or removed with
`del flood.channel.block #wardriving`. Once the block list has been saved, the
firmware uses the saved list and does not recreate the default after deletion.
Restore the exact seeded row with:
```text
set flood.channel.block.1 #wardriving h=4
```
This explicitly writes slot 1. Omit `.1` to update an existing wardriving row
or use the first empty slot instead.
**Forwarding behavior:** Repeater firmware only. This only affects received
flood `GRP_TXT` and `GRP_DATA` channel packets. The repeater still receives and
@@ -1032,6 +1040,132 @@ del flood.channel.block.2
---
#### Force a transport scope onto unscoped floods
**Usage:**
- `get flood.channel.scope`
- `get flood.channel.scope.<n>`
- `set flood.channel.scope <channel|txt:*|login:*|other:*> <region>`
- `set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region>`
- `del flood.channel.scope.<n>`
- `del flood.channel.scope all`
**Parameters:**
- `n`: Slot number within the table compiled for the target. Roomy ESP32 builds
provide `1-255`; DRAM-tight classic ESP32 LoRa-OTA repeaters and nRF52/other
normal constrained builds provide `1-31`; very-tight STM32WL repeaters provide
`1-15`; the no-PSRAM LilyGo T-LoRa V2.1 repeater/observer provides `1-4`.
- `channel`: `public`, a public `#channel`, or a 128/256-bit channel key in hex.
- `txt:*`: Unauthenticated fallback for otherwise-unmatched `GRP_TXT` and
`GRP_DATA`. Plain `*` is an alias for `txt:*`.
- `login:*`: Type-based wildcard for the remote-login/admin family: `REQ`,
`RESPONSE`, `TXT_MSG`, `ANON_REQ`, and `PATH` (`0x00`, `0x01`, `0x02`,
`0x07`, and `0x08`). It classifies the outer type; a transit repeater cannot
authenticate whether a packet is actually part of a login session.
- `other:*`: Type-based wildcard for every remaining flood payload type except
TRACE, including ACK, advert, multipart, control, OTA, reserved types, and
raw custom. TRACE is deliberately exempt from forced-scope wildcards.
- `region`: Existing named region with a usable transport key. A unique region
name prefix is accepted; wildcard region `*` is not a scope target.
**Default:** No forced scopes.
Remote ACL permission `4` (region/scope manager) can use all `get`, `set`, and
`del flood.channel.scope` forms. Filter managers and other non-admin roles
cannot change this table.
Without `.n`, `set` updates the row for the same exact channel key or wildcard
class, otherwise it uses the first empty slot. With `.n`, it replaces that
slot. The three wildcard classes are independent and consume one slot each.
`get flood.channel.scope` reports active/total slot counts; use the numbered
form for row detail. Keyed rows are displayed by the first four bytes of their
derived channel hash because channel secrets are never returned.
This acts on received, unscoped `ROUTE_TYPE_FLOOD` packets. For `GRP_TXT` and
`GRP_DATA`, all exact channel-key rows are tried first and must validate the
packet MAC/decryption. A row whose target region is missing or unusable is
skipped; later exact rows and then `txt:*` are tried. Exact keyed rows with a
usable target therefore beat `txt:*` regardless of slot number. `login:*` and
`other:*` select their non-overlapping outer-type families without decrypting
the payload. Duplicate rows within the same class are permitted with numbered
slots; the lowest usable slot wins.
Standard traceroute is direct-routed and is therefore outside this flood-only
table. A custom flood-form `TRACE` is also left unchanged: no wildcard adds a
scope, an existing transport code is preserved, and region/unknown-code gates
do not block it.
On a match, the repeater changes the route to
`ROUTE_TYPE_TRANSPORT_FLOOD`, computes transport code 0 from the selected
region key and packet payload, and leaves transport code 1 as zero. This occurs
before region enforcement, forwarding filters, and the seen-packet lookup.
Already-scoped packets and direct routes are never rewritten. The rewritten
packet is no longer subject to `flood.max.unscoped`, but remains subject to
normal payload handling, `flood.max`, region allow/deny, `flood.filter`,
`flood.channel.block`, loop detection, and moderation. Assigning a scope does
not make a packet type forwardable if the core would otherwise reject it.
If a row's target region has been removed or has no usable key, the repeater
tries the next applicable row. For group packets this means later authenticated
exact rows followed by `txt:*`; wildcard duplicates likewise fall through to
the next usable slot. The packet remains unscoped only when no usable mapping
exists.
LoRa OTA remains functional when `other:*` is configured. OTA packets are
given that region's transport code, but the OTA handler still accepts and
re-floods them during the temporary-radio window. The target region must allow
flooding. The OTA core itself is dormant outside that window; no default flood
filter row is needed for that behavior. Forced scope does not make OTA operate
outside the window.
**Capacity cost:** Each slot uses 36 bytes of runtime RAM and persistent
storage, plus a 5-byte file header. The four-slot minimum uses 144 bytes RAM and
a 149-byte file; it has room for the three wildcard classes plus one exact
channel mapping. Very-tight 15-slot builds use 540 bytes RAM and a 545-byte
file. The 31-slot table uses 1,116 bytes RAM and a 1,121-byte file. Roomy ESP32
builds use a 255-slot table: 9,180 bytes RAM and a 9,185-byte file. Classic
ESP32 LoRa-OTA builds that cannot afford that RAM use 31 slots instead. The
existing region table still permits 32 named regions, so a 255-slot channel
table may reuse targets but cannot name more than 32 distinct configured region
scopes.
**Duplicate behavior:** Mesh dedup hashes payload type and payload bytes; it
does not hash route type, transport codes, or the ordinary flood path. Adding
a transport scope therefore does not create a new duplicate identity. If the
same payload later arrives scoped, unscoped, or through a different region, it
is still the same seen packet. `TRACE` is the exception only in that its
encoded `path_len` byte is also hashed.
While equivalent non-TRACE flood copies are waiting in `rxdelay`, the normal receive-quality
timing still selects the packet to process, but that winner receives a scope
from the queued scoped copies with the same dedupe identity. If the copies
carry different locally allowed scopes, the scope from the shortest received
path wins. Unknown and denied transport codes are not candidates and therefore
cannot overwrite an unscoped winner. With equal path lengths, the deeper child
region wins because it is narrower. A remaining tie keeps queue order. The
winner's own path, SNR reading, and scheduled time are not changed, and an
already-scoped winner may have its code replaced by the better queued scope.
The comparison is deferred until dequeue so each copy retains its original
scope and path for arbitration. It can only use copies still present in
`rxdelay`; it cannot replace a packet that already won the dedupe race. TRACE
is excluded from scope arbitration entirely, so rxdelay never adds or replaces
a trace transport code.
**Examples:**
```text
region put west
region save
set flood.channel.scope #local west
set flood.channel.scope.2 txt:* west
set flood.channel.scope.3 login:* west
set flood.channel.scope.4 other:* west
get flood.channel.scope
get flood.channel.scope.1
del flood.channel.scope.2
```
---
#### Filter flood packets by payload type and hop
For setup guidance, interactions with the existing forwarding controls, and
@@ -1040,19 +1174,24 @@ worked moderation examples, see [Repeater Flood Filtering and Moderation](flood_
**Usage:**
- `get flood.filter`
- `get flood.filter.<n>`
- `set flood.filter <type> <hops>`
- `set flood.filter.<n> <type> <hops>`
- `set flood.filter <type> [hops] [suspend=tempradio]`
- `set flood.filter.<n> <type> [hops] [suspend=tempradio]`
- `del flood.filter.<n>`
- `del flood.filter all`
**Parameters:**
- `n`: Rule slot from `1` to `16`.
- `type`: Payload type name, full `PAYLOAD_TYPE_*` name, numeric value `0-15`, or `any`.
- `hops`:
- `type`: Payload type name, full `PAYLOAD_TYPE_*` name, decimal value `0-15`,
hexadecimal value `0x00-0x0F`, or `any`.
- `hops`: Optional; omitted means `all`.
- `N`: Block only at received hop count `N`.
- `N+`: Block at received hop count `N` and higher.
- `N-M`: Block the inclusive received-hop range.
- `all`: Block at every received hop count (`0-63`).
- `0+`, `all`, and an omitted hop expression are equivalent. The CLI displays
the saved range as `all`.
- `suspend=tempradio`: Optional. Skip this row only while the temporary radio
is actually active.
The payload names follow the [MeshCore packet-format allocation](https://docs.meshcore.io/packet_format/):
@@ -1081,19 +1220,39 @@ The payload names follow the [MeshCore packet-format allocation](https://docs.me
are never affected.
**Behavior:** A match prevents retransmission by this repeater. The packet is
still received and can still be logged. Rules are persistent and default to an
empty table, so upgrading does not change forwarding. A malformed persisted
table fails open (no general rules are applied).
still received and can still be logged. Rules are persistent and the table is
empty by default. While the temporary radio is active, only rows explicitly
marked `suspend=tempradio` are skipped. `tempradio` is a radio state, not an OTA
mode; normal payload types can also use the temporary channel. Other rows
remain in force. A malformed persisted table fails open (no general rules are
applied).
Without `.n`, `set` returns an identical existing rule or uses the first empty
slot. With `.n`, it replaces that slot. `get flood.filter` gives a compact list;
use `get flood.filter.<n>` for full details.
**Remote-admin protection:** `flood.filter` cannot block `anon_req`, `path`, or
`response` at received hop counts `0-6`; those login-capable types become
filterable at hop `7`. Flood `txt_msg` cannot be blocked at hops `0-4` and
becomes filterable at hop `5`. `req`, `ack`, and multipart ACK have no special
protection. Transit repeaters cannot decrypt these packets to distinguish an
admin exchange from ordinary peer traffic, so each floor necessarily covers
all flood packets of that outer type. These exceptions apply only to
`flood.filter`; `repeat`, `flood.max*`, region, loop-detection, and other
forwarding gates remain authoritative.
Without `.n`, `set` returns an identical existing rule, including its suspension
setting, or uses the first empty slot. With `.n`, it replaces that slot.
`get flood.filter` gives a compact list. Use `get flood.filter.<n>` for full
details, including `suspend=tempradio`.
Standard traceroute is direct-routed and therefore outside `flood.filter`
entirely. For a custom flood-form trace, catch-all `any` rows are deliberately
ignored; only a deliberately explicit `set flood.filter trace ...` row can
match it.
**Examples:**
```text
set flood.filter grp_data 4+
set flood.filter.2 PAYLOAD_TYPE_ADVERT 6+
set flood.filter ota 2-4
set flood.filter grp_data all suspend=tempradio
set flood.filter any 12+
get flood.filter
get flood.filter.2
@@ -1174,7 +1333,7 @@ del flood.moderation.3
- `1`: Read-only
- `2`: Read-write
- `3`: Admin
- `4`: Region manager (repeater delegated region management)
- `4`: Region/scope manager (repeater delegated region and forced-scope management)
- `5`: Filter manager (repeater delegated forwarding-filter management)
**Filter manager scope:** Permission `5` can use an explicit allowlist of
@@ -1183,7 +1342,8 @@ controls `repeat`, `loop.detect`, `flood.max*`, `flood.channel.data*`,
`flood.channel.block*`, `flood.filter*`, and `flood.moderation*`. It cannot read
guest, WiFi, MQTT, bridge, or other credentials, and it cannot change regions,
ACL entries, radio settings, or other admin configuration. Permission `4`
remains limited to region commands and the same non-secret status allowlist.
is limited to region commands, `flood.channel.scope*`, and the same non-secret
status allowlist.
Both delegated manager roles are protected from least-recently-active ACL
eviction like administrators.
+175 -17
View File
@@ -27,13 +27,109 @@ 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.
They are empty by default. A corrupt or truncated table fails open, so corrupt
storage does not silently enable blocking.
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:
```text
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
@@ -41,16 +137,57 @@ Use `flood.filter` when the packet type and its current path length are enough
to make the decision:
```text
set flood.filter <type> <hops>
set flood.filter.<slot> <type> <hops>
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 or selects the first empty
slot. With a slot number, it replaces that slot.
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:
@@ -59,6 +196,8 @@ packet:
- `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:
@@ -72,7 +211,7 @@ 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 every flood payload type.
# Apply a hard ceiling to flood payload types at 12 or more received hops.
set flood.filter any 12+
```
@@ -97,9 +236,10 @@ Accepted payload names are:
| `0x0E` | `14` | reserved |
| `0x0F` | `raw_custom` | `PAYLOAD_TYPE_RAW_CUSTOM` |
The numeric values `0` through `15`, the full `PAYLOAD_TYPE_*` names, and
`any` are also accepted. Upstream currently reserves `0x0C`; this fork assigns
it to LoRa OTA.
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
@@ -174,11 +314,13 @@ the packet contains all path entries listed by the rule.
A flood packet is retransmitted only if it passes every applicable control. In
other words, the controls combine as deny rules:
1. `repeat`, `flood.max*`, and the channel-data gate are checked.
2. `flood.filter` checks payload type and hop range.
3. `flood.channel.block` checks keyed channels.
4. Region and loop-detection rules are checked.
5. `flood.moderation` checks decrypted group text, username, rate, hops, and path.
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
@@ -200,6 +342,10 @@ 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
@@ -212,9 +358,21 @@ 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:
```text
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 return the two new tables to their default empty state:
To save both tables in an empty state:
```text
del flood.filter all
+2 -1
View File
@@ -83,7 +83,8 @@ set flood.retry.ignore none
| `flood.channel.data.hops` | Separate hop gate used only when `flood.channel.data` is `off`; `all` blocks `GRP_DATA` at any hop count, `1`-`7` repeats at that hop count or lower and blocks longer paths. | `get flood.channel.data.hops`, `set flood.channel.data.hops <all|1-7>` | `set flood.channel.data.hops 7` |
| `flood.channel.block` | Blocks selected flood `GRP_TXT`/`GRP_DATA` channels when the key validates the packet. New repeater block lists start with editable/deletable `#wardriving h=4`. Add `h=<all|1-7|default>` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] <key|#channel> [name] [h=...]`, `del flood.channel.block[.n]` | `set flood.channel.block #wardriving h=4` |
| `flood.channel.block.hops` | Limits keyed channel-block matches to short flood paths. `all` blocks matching packets at any hop count; `1`-`7` repeats packets at that hop count or lower and blocks longer matches. This does not restrict unkeyed `GRP_DATA`; use `flood.channel.data.hops` for that. | `get flood.channel.block.hops`, `set flood.channel.block.hops <all|1-7>` | `set flood.channel.block.hops 3` |
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and received hop count/range. Direct routing and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> <N|N+|N-M|all>`, `del flood.filter.<n>|all` | `set flood.filter grp_data 4+` |
| `flood.channel.scope` | Assigns a transport-region scope to received unscoped floods. Exact channel keys beat `txt:*`; `login:*` covers the remote-login family, and `other:*` covers every remaining flood type except TRACE, including OTA. TRACE remains unchanged across scope boundaries. ACL permission `4` can manage the table. | `get flood.channel.scope[.n]`, `set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region>`, `del flood.channel.scope.<n>|all` | `set flood.channel.scope login:* west` |
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and optional received hop count/range (omitted means `all`). The table starts empty; only rows marked `suspend=tempradio` are skipped during temporary-radio operation. Login-capable `anon_req`/`path`/`response` types become filterable at hop `7`; flood `txt_msg` becomes filterable at hop `5`. Standard direct traceroute, other direct routing, and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.<n>|all` | `set flood.filter grp_data 4+` |
| `flood.moderation` | Decrypts keyed `GRP_TXT` channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports `public`, `#channel`, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. | `get flood.moderation[.n]`, `set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...]`, `del flood.moderation.<n>|all` | `set flood.moderation public "Noisy User" rate=5/min hops=4` |
| `clock.sync.mesh` | After 30 minutes of uptime, estimates UTC from a configurable consensus of fresh signed-advert or valid Public-channel sources. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `get clock.sync.status` | `set clock.sync.mesh on` |
| `clock.sync.internet` | Adds a read-only internet/NTP estimate at the same delayed check on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. | `get clock.sync.internet`, `set clock.sync.internet <on|off>` | `set clock.sync.internet on` |