Extend flood policy and harden message retries
This commit is contained in:
1 parent
687e114828
commit
7f56da7a34
34 files changed
+5297
-1338
No files matched your search
@@ -1907,7 +1907,7 @@ apply_esp32_full_size_profile() {
|
||||
# The FULL artifact uses expanded dual-OTA slots, so restore features that
|
||||
# target or portable profiles disabled only to save application space.
|
||||
append_platformio_build_unflags "-DWEBCONFIG_DISABLED=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UWEBCONFIG_DISABLED -DWIFI_OTA_SEEDER=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UWEBCONFIG_DISABLED -DWIFI_OTA_SEEDER=1 -DMESHCORE_ESP32_FULL_PROFILE=1"
|
||||
|
||||
# Keep ordinary builds at their board-defined neighbor capacity. FULL builds
|
||||
# normally use the largest table supported by the one-byte neighbor discovery
|
||||
|
||||
@@ -117,10 +117,9 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Routing | [`get/set flood.max.unscoped`](cli_commands.md#limit-the-number-of-hops-for-an-unscoped-flood-message) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.advert`](cli_commands.md#limit-the-number-of-hops-for-an-advert-flood-message) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set flood.channel.data`; `get/set flood.channel.data.hops`](cli_commands.md#forward-flood-group-data-packets-on-repeaters) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.block*`](cli_commands.md#block-selected-flood-channel-packets-on-repeaters) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope*`](cli_commands.md#force-a-transport-scope-onto-floods) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope.require*`](cli_commands.md#require-valid-incoming-scopes-only-on-selected-channels) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#filter-flood-packets-by-payload-type-hop-count-and-path) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.rule*`; `get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#change-persistent-flood-rules-in-the-field) | Repeater; `flood.rule`/`flood.filter` also on FULL ESP32 room server (no blacklist) | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.moderation*`](cli_commands.md#moderate-flood-group-text-by-channel-sender-and-source-path) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set outpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes |
|
||||
| Routing | [`get/set altpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes |
|
||||
@@ -273,10 +272,9 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Routing | [`get/set flood.max.unscoped`](cli_commands.md#limit-the-number-of-hops-for-an-unscoped-flood-message) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.max.advert`](cli_commands.md#limit-the-number-of-hops-for-an-advert-flood-message) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set flood.channel.data`; `get/set flood.channel.data.hops`](cli_commands.md#forward-flood-group-data-packets-on-repeaters) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.block*`](cli_commands.md#block-selected-flood-channel-packets-on-repeaters) | Repeater, full common parser | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope*`](cli_commands.md#force-a-transport-scope-onto-floods) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.channel.scope.require*`](cli_commands.md#require-valid-incoming-scopes-only-on-selected-channels) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#filter-flood-packets-by-payload-type-hop-count-and-path) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.rule*`; `get/set/del flood.filter*`; `get/set/del flood.filter.blacklist*`](cli_commands.md#change-persistent-flood-rules-in-the-field) | Repeater role handler; `flood.rule`/`flood.filter` also on FULL ESP32 room server (no blacklist) | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set/del flood.moderation*`](cli_commands.md#moderate-flood-group-text-by-channel-sender-and-source-path) | Repeater role handler | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Routing | [`get/set outpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set altpath`](halo_keymind_settings.md#direct-path-overrides) | Repeater remote-client context | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
+217
-185
@@ -25,7 +25,7 @@ arguments such as node names, passwords, and keys is left unchanged.
|
||||
- [System](#system)
|
||||
- [GPIO](#control-an-exposed-gpio)
|
||||
- [Routing](#routing)
|
||||
- [Flood Filtering](#filter-flood-packets-by-payload-type-and-hop)
|
||||
- [Flood Rules](#change-persistent-flood-rules-in-the-field)
|
||||
- [Group Text Moderation](#moderate-flood-group-text-by-channel-sender-and-source-path)
|
||||
- [ACL](#acl)
|
||||
- [Region Management](#region-management-v110)
|
||||
@@ -1016,10 +1016,10 @@ get clock.sync.status
|
||||
|
||||
**Usage:**
|
||||
|
||||
- `get gpio` — list the Arduino pin numbers this firmware build permits
|
||||
- `get gpio state`, `get gpio states`, or `get gpio status` — list every available pin currently controlled by the user (anything not in `reset`)
|
||||
- `get gpio state <pin>` — show one pin's state; `states` and `status` are accepted here too
|
||||
- `get gpio <pin>` — show `on`, `off`, or `reset`, plus any pending timed transition
|
||||
- `get gpio` - list the Arduino pin numbers this firmware build permits
|
||||
- `get gpio state`, `get gpio states`, or `get gpio status` - list every available pin currently controlled by the user (anything not in `reset`)
|
||||
- `get gpio state <pin>` - show one pin's state; `states` and `status` are accepted here too
|
||||
- `get gpio <pin>` - show `on`, `off`, or `reset`, plus any pending timed transition
|
||||
- `set gpio <pin> on`
|
||||
- `set gpio <pin> off`
|
||||
- `set gpio <pin> reset`
|
||||
@@ -1027,9 +1027,9 @@ get clock.sync.status
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `set gpio 16 on 30 off` — drive GPIO16 high for 30 seconds, then drive it low
|
||||
- `set gpio 16 on 5ms off` — drive GPIO16 high for 5 milliseconds, then drive it low
|
||||
- `set gpio 16 off 5 reset` — drive GPIO16 low for 5 seconds, then return it to high impedance
|
||||
- `set gpio 16 on 30 off` - drive GPIO16 high for 30 seconds, then drive it low
|
||||
- `set gpio 16 on 5ms off` - drive GPIO16 high for 5 milliseconds, then drive it low
|
||||
- `set gpio 16 off 5 reset` - drive GPIO16 low for 5 seconds, then return it to high impedance
|
||||
|
||||
An integer duration has seconds as its default unit, so `5` means 5 seconds. Add `ms` for milliseconds (`5ms`); an explicit `s` suffix is also accepted (`5s`). The maximum duration is 24 hours (86,400 seconds or 86,400,000 milliseconds). `on` or `off` without a duration remains in that state until another command, reset, or reboot.
|
||||
|
||||
@@ -1334,108 +1334,36 @@ reduce false recoveries when no traffic is expected.
|
||||
|
||||
**Forwarding behavior:** Repeater firmware only. The repeater still receives and
|
||||
logs the packet when logging is enabled; this only blocks retransmission.
|
||||
This is checked before `flood.channel.block` and applies to flood `GRP_DATA`
|
||||
packets regardless of channel key. Flood group text (`GRP_TXT`) is unaffected by
|
||||
this setting.
|
||||
|
||||
`flood.channel.data.hops` is separate from `flood.channel.block.hops`.
|
||||
`flood.channel.block.hops` does not restrict unkeyed `GRP_DATA` packets. With
|
||||
the default `flood.channel.data on`, `GRP_DATA` repeats normally even when
|
||||
`flood.channel.block.hops` is set for keyed channel blocks.
|
||||
This is checked before FPF7 and applies to flood `GRP_DATA` packets regardless
|
||||
of channel key. Flood group text (`GRP_TXT`) is unaffected by this setting.
|
||||
|
||||
`get flood.channel.data` includes the active hop gate as `h=all` or `h>N`.
|
||||
|
||||
---
|
||||
|
||||
#### Block selected flood channel packets on repeaters
|
||||
**Usage:**
|
||||
- `get flood.channel.block`
|
||||
- `get flood.channel.block.<n>`
|
||||
- `get flood.channel.block <name|8_hex_prefix>`
|
||||
- `get flood.channel.block.hops`
|
||||
- `set flood.channel.block <key> <name> [h=<all|1-7|default>]`
|
||||
- `set flood.channel.block.<n> <key> <name> [h=<all|1-7|default>]`
|
||||
- `set flood.channel.block #channel [h=<all|1-7|default>]`
|
||||
- `set flood.channel.block.<n> #channel [h=<all|1-7|default>]`
|
||||
- `set flood.channel.block.hops <all|1-7>`
|
||||
- `del flood.channel.block.<n>`
|
||||
- `del flood.channel.block <name|8_hex_prefix>`
|
||||
#### Block selected flood channels with FPF7
|
||||
|
||||
**Parameters:**
|
||||
- `n`: Slot number from `1` to `15`.
|
||||
- `key`: 128-bit or 256-bit channel key as hex.
|
||||
- `#channel`: Public hashtag channel name; derives the 128-bit channel key from the hashtag and is stored as the row name.
|
||||
- `name`: Local label for hex-key rows. Not needed for `#channel`; extra text after `#channel` is ignored unless it is a hop setting.
|
||||
- `8_hex_prefix`: First 4 bytes of the derived channel hash, shown by single-entry `get`.
|
||||
- `all`: Block matching flood channel packets at any received flood hop count.
|
||||
- `1-7`: Maximum received flood path hash count to repeat. Matching packets over this hop count are blocked.
|
||||
- `default`: Row inherits the global `flood.channel.block.hops` setting.
|
||||
|
||||
**Slot behavior:** Without `.n`, `set flood.channel.block` updates an existing
|
||||
row with the same derived channel prefix or name, otherwise it uses the next
|
||||
empty slot. If all 15 slots are full, the command fails. With `.n`, the command
|
||||
writes that slot.
|
||||
|
||||
**Default row:** Repeater firmware seeds a new block list with
|
||||
`#wardriving h=4` in slot 1. This is a normal row, so it can be changed with
|
||||
`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:
|
||||
The separate `flood.channel.block` command and 15-row table have been retired.
|
||||
Generalized repeaters use the 31-row FPF7 table for authenticated channel
|
||||
blocks:
|
||||
|
||||
```text
|
||||
set flood.channel.block.1 #wardriving h=4
|
||||
set flood.rule type=any channel=#test hops=all drop
|
||||
set flood.rule.2 type=any channel=#wardriving hops=5+ drop
|
||||
set flood.rule type=any channel=9cd8fcf22a47333b591d96a2b848b73f hops=4+ drop
|
||||
get flood.rule
|
||||
del flood.rule.2
|
||||
```
|
||||
|
||||
This explicitly writes slot 1. Omit `.1` to update an existing wardriving row
|
||||
or use the first empty slot instead.
|
||||
`type=any` with a channel condition can authenticate only `GRP_TXT` and
|
||||
`GRP_DATA`, so it does not match other payload types. Use `hops=all` to block
|
||||
at every received hop count. To preserve the old `h=N` meaning of repeating
|
||||
through `N` hops and blocking longer paths, use `hops=N+1+`; old `h=4` is
|
||||
therefore `hops=5+`.
|
||||
|
||||
**Forwarding behavior:** Repeater firmware only. This only affects received
|
||||
flood `GRP_TXT` and `GRP_DATA` channel packets. The repeater still receives and
|
||||
logs the packet, but it does not retransmit it when a configured block entry can
|
||||
validate/decode it. If `flood.channel.data` is `off`, `GRP_DATA` packets are
|
||||
checked against the separate `flood.channel.data.hops` gate before this
|
||||
per-channel check runs.
|
||||
|
||||
**Hop gate:** `flood.channel.block.hops` defaults to `all`, which preserves the
|
||||
original behavior. When set to `N` from `1` to `7`, block rows that inherit the
|
||||
global setting only block packets whose received flood path hash count is
|
||||
greater than `N`; packets at `N` hops or lower can still repeat. For example,
|
||||
`set flood.channel.block.hops 1` repeats zero-hop and one-hop matches but blocks
|
||||
two-hop and longer matches.
|
||||
|
||||
Each block row can override the global hop gate with `h=<all|1-7|default>`.
|
||||
For example, the seeded `#wardriving h=4` row blocks `#wardriving` matches
|
||||
above four hops, while `set flood.channel.block #bot h=7` blocks
|
||||
`#bot` matches above seven hops. Use `h=default` to make the row inherit the
|
||||
global setting again.
|
||||
|
||||
`get flood.channel.block` includes the global default first, then adds per-row
|
||||
overrides as `/h>N` or `/h=all`; inherited rows do not show a suffix. Single-row
|
||||
`get` replies include that row's stored hop mode as `h=def`, `h=all`, or `h>N`.
|
||||
List replies truncate displayed row names only when the full list would exceed
|
||||
the remote-management response limit.
|
||||
|
||||
**Matching behavior:** Each block entry stores the first 4 bytes of the derived
|
||||
channel hash for display and lookup. Current group packets carry only the first
|
||||
channel-hash byte, so that byte is used as a cheap prefilter. Only entries whose
|
||||
first hash byte matches the packet try MAC/decrypt with their stored key. If
|
||||
multiple blocked channels share the same first byte, the repeater tries each
|
||||
matching key until one validates; the packet is blocked only after a successful
|
||||
MAC/decrypt.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
set flood.channel.block #test
|
||||
set flood.channel.block.2 9cd8fcf22a47333b591d96a2b848b73f #test
|
||||
set flood.channel.block.hops 3
|
||||
set flood.channel.block #wardriving h=4
|
||||
set flood.channel.block #bot h=7
|
||||
get flood.channel.block
|
||||
get flood.channel.block.hops
|
||||
get flood.channel.block #test
|
||||
del flood.channel.block.2
|
||||
```
|
||||
New generalized repeater tables seed slot 2 with the second example. Existing
|
||||
FCB2 rows are imported once into free FPF7 slots and the retired file is then
|
||||
removed. The fixed-size STM32WL FPF6 build cannot match authenticated channels.
|
||||
|
||||
---
|
||||
|
||||
@@ -1443,8 +1371,8 @@ del flood.channel.block.2
|
||||
**Usage:**
|
||||
- `get flood.channel.scope`
|
||||
- `get flood.channel.scope.<n>`
|
||||
- `set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]`
|
||||
- `set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]`
|
||||
- `set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]`
|
||||
- `set flood.channel.scope.<n> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]`
|
||||
- `del flood.channel.scope.<n>`
|
||||
- `del flood.channel.scope all`
|
||||
|
||||
@@ -1460,11 +1388,16 @@ del flood.channel.block.2
|
||||
`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.
|
||||
- `other:*`: Type-based wildcard for every remaining flood payload type,
|
||||
including flood-form TRACE, ACK, advert, multipart, control, OTA, reserved
|
||||
types, and raw custom.
|
||||
- `region`: Existing named region with a usable transport key. A unique region
|
||||
name prefix is accepted; wildcard region `*` is not a scope target.
|
||||
- `scope=<name>`: Regionless alternative to `region`. The public name is
|
||||
normalized with a leading `#`, and its 128-bit transport key is derived
|
||||
directly from that hashtag exactly as for `flood.filter scope=<name>`. It
|
||||
does not need to exist in the region list. Public names up to 30 characters
|
||||
are accepted; private `$` scopes are not.
|
||||
- `path=blacklist`: Optional. Require the received path to match the passive
|
||||
`flood.filter.blacklist` ID table. No `flood.filter` drop row needs to be
|
||||
enabled. One exact listed ID qualifies a 3-byte path. A 2-byte path requires
|
||||
@@ -1494,15 +1427,16 @@ rows for the same channel. 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.
|
||||
derived channel hash because channel secrets are never returned. Regionless
|
||||
targets are displayed with their normalized leading `#`.
|
||||
|
||||
This acts on received `ROUTE_TYPE_FLOOD` and
|
||||
`ROUTE_TYPE_TRANSPORT_FLOOD` packets. An unscoped packet gains the configured
|
||||
scope; an already-scoped packet has its existing transport codes replaced. For
|
||||
`GRP_TXT` and `GRP_DATA`, all exact channel-key rows are tried first and must
|
||||
validate the packet MAC/decryption. Matching path-qualified exact rows are
|
||||
tried before ordinary exact fallback rows. A row whose target region is
|
||||
missing or unusable is skipped; later exact rows and then `txt:*` are tried.
|
||||
tried before ordinary exact fallback rows. A region-backed row whose target
|
||||
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. Within each wildcard class, path-qualified rows similarly precede
|
||||
ordinary fallback rows. `login:*` and `other:*` select their non-overlapping
|
||||
@@ -1510,19 +1444,18 @@ outer-type families without decrypting the payload. The lowest usable slot
|
||||
wins within each priority tier.
|
||||
|
||||
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.
|
||||
table. A custom flood-form `TRACE` is treated like every other flood: an
|
||||
applicable wildcard may rewrite it and region/unknown-code gates still apply.
|
||||
|
||||
On a match, the repeater sets the route to `ROUTE_TYPE_TRANSPORT_FLOOD`,
|
||||
computes transport code 0 from the selected region key and packet payload, and
|
||||
sets transport code 1 to zero. This occurs before region enforcement,
|
||||
forwarding filters, and the seen-packet lookup. For an already-scoped packet,
|
||||
the selected code replaces both incoming transport-code fields. Direct routes
|
||||
are never rewritten. A packet converted from unscoped is no longer subject to
|
||||
`flood.max.unscoped`; all rewritten packets remain subject to normal payload
|
||||
handling, `flood.max`, region allow/deny, `flood.filter`,
|
||||
`flood.channel.block`, loop detection, and moderation. Assigning a scope does
|
||||
computes transport code 0 from the selected region or direct hashtag key and
|
||||
packet payload, and sets transport code 1 to zero. This occurs before region
|
||||
enforcement, forwarding filters, and the seen-packet lookup. For an
|
||||
already-scoped packet, the selected code replaces both incoming transport-code
|
||||
fields. Direct routes are never rewritten. A packet converted from unscoped is
|
||||
no longer subject to `flood.max.unscoped`; all rewritten packets remain subject
|
||||
to normal payload handling, `flood.max`, `flood.filter`,
|
||||
loop detection, and moderation. Assigning a scope does
|
||||
not make a packet type forwardable if the core would otherwise reject it. By
|
||||
default, if the selected scope differs and the rewritten packet is accepted
|
||||
for forwarding, its initial retransmission uses zero `txdelay` and the highest
|
||||
@@ -1536,29 +1469,39 @@ Neither mode preempts an active radio transmission or bypasses CAD and
|
||||
airtime-budget limits. Selecting the scope already carried by the packet is a
|
||||
no-op and does not grant special transmit treatment.
|
||||
|
||||
If a row's target region has been removed or has no usable key, the repeater
|
||||
A region-backed target must be locally flood-allowed and remains subject to
|
||||
the normal region gate. A `scope=<name>` target is trusted for this matched
|
||||
receive pass even though it has no region-list entry, matching the behavior of
|
||||
`flood.filter scope=<name>`. It does not create a region, consume a region
|
||||
slot, or change the allow/deny state for unrelated packets carrying the same
|
||||
transport code.
|
||||
|
||||
If a region-backed row's target 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. When no usable mapping exists, the packet retains its
|
||||
original unscoped or scoped route.
|
||||
|
||||
LoRa OTA remains functional when `other:*` is configured. OTA packets are
|
||||
given that region's transport code, replacing an existing code when necessary,
|
||||
given that target's transport code, replacing an existing code when necessary,
|
||||
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.
|
||||
window. A region target must allow flooding; a direct target follows the
|
||||
regionless trust behavior above. 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.
|
||||
**Capacity cost:** Each rule slot retains its 36-byte runtime and persistent
|
||||
record. A separate 32-byte-name table holds up to the smaller of the rule count
|
||||
or 32 distinct regionless targets; very-tight STM32WL builds hold one reusable
|
||||
direct target. Region-backed targets do not consume this table.
|
||||
The four-slot minimum uses 272 bytes RAM and a 278-byte file; it has room for
|
||||
the three wildcard classes plus one exact channel mapping. Very-tight 15-slot
|
||||
builds use 572 bytes RAM and a 578-byte file. The 31-slot table uses 2,108
|
||||
bytes RAM and a 2,114-byte file. Roomy ESP32 builds use 255 rule slots and 32
|
||||
direct-target slots: 10,204 bytes RAM and a 10,210-byte file. Classic ESP32
|
||||
LoRa-OTA builds that cannot afford the 255-rule table use 31 slots instead.
|
||||
Both configured regions and regionless targets can be reused by any number of
|
||||
rules.
|
||||
|
||||
**Duplicate behavior:** Mesh dedup hashes payload type and payload bytes; it
|
||||
does not hash route type, transport codes, or the ordinary flood path. Adding
|
||||
@@ -1579,9 +1522,9 @@ 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.
|
||||
`rxdelay`; it cannot replace a packet that already won the dedupe race.
|
||||
Flood-form TRACE participates in this arbitration; direct traceroute does not
|
||||
enter the flood queue.
|
||||
|
||||
A packet that matches a fast `flood.channel.scope` or `flood.filter scope=`
|
||||
action and needs its scope changed bypasses the inbound `rxdelay` queue. A
|
||||
@@ -1601,6 +1544,19 @@ get flood.channel.scope.1
|
||||
del flood.channel.scope.2
|
||||
```
|
||||
|
||||
A regionless exact mapping needs no `region` command:
|
||||
|
||||
```text
|
||||
set flood.channel.scope #rgdata scope=BlackHole86
|
||||
get flood.channel.scope
|
||||
get flood.channel.scope.1
|
||||
```
|
||||
|
||||
For example, if an authenticated `#rgdata` packet arrives carrying scope
|
||||
`#usa`, that rule replaces `#usa` with `#BlackHole86` before forwarding. The
|
||||
rule also assigns `#BlackHole86` when the packet is unscoped or carries any
|
||||
other scope; it is a channel-to-target mapping, not an incoming-scope filter.
|
||||
|
||||
To use bridge bucket 1 to assign `east` to `public` packets whose received
|
||||
3-byte path contains `7576FB`, while assigning `west` to every other
|
||||
authenticated `public` packet:
|
||||
@@ -1641,7 +1597,7 @@ they cannot rescue it or grant special receive/transmit timing.
|
||||
|
||||
Other group channels bypass the region/unknown-code gate while the table is
|
||||
active. They remain subject to every other forwarding control, including
|
||||
`repeat`, `flood.max*`, packet filters, channel blocks, loop detection, payload
|
||||
`repeat`, `flood.max*`, packet filters, loop detection, payload
|
||||
validation, and moderation. Non-channel flood payload types retain normal
|
||||
global region enforcement.
|
||||
|
||||
@@ -1665,12 +1621,17 @@ get flood.channel.scope.require.1
|
||||
|
||||
---
|
||||
|
||||
#### Filter flood packets by payload type, hop count, and path
|
||||
#### Change persistent flood rules in the field
|
||||
|
||||
For setup guidance, interactions with the existing forwarding controls, and
|
||||
worked moderation examples, see [Repeater Flood Filtering and Moderation](flood_filtering.md).
|
||||
worked moderation examples, see [Flood Filtering and Moderation](flood_filtering.md).
|
||||
|
||||
**Usage:**
|
||||
- `get flood.rule`
|
||||
- `get flood.rule.<n>`
|
||||
- `set flood.rule[.<n>] type=<type> [hops=<range>] [channel=<channel>] [prefix=<path-prefix>] [in=<input-scope>] <drop|scope=<name>|region=<name>|rate=<N>/min|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]`
|
||||
- `del flood.rule.<n>`
|
||||
- `del flood.rule all`
|
||||
- `get flood.filter`
|
||||
- `get flood.filter.<n>`
|
||||
- `get flood.filter.blacklist`
|
||||
@@ -1684,40 +1645,71 @@ worked moderation examples, see [Repeater Flood Filtering and Moderation](flood_
|
||||
- `del flood.filter.<n>`
|
||||
- `del flood.filter all`
|
||||
|
||||
The extended table is available on repeaters with the rule engine enabled and
|
||||
on FULL-profile ESP32 room servers. A FULL room server exposes both
|
||||
`flood.rule` and `flood.filter`, has 31 slots, and requires an administrator
|
||||
for remote changes. It does not have the repeater's passive path blacklist, so
|
||||
`flood.filter.blacklist*` and `path=blacklist` are repeater-only; use the
|
||||
ordered `prefix=` match on a room server. Standard room-server profiles do not
|
||||
compile this table.
|
||||
|
||||
**Parameters:**
|
||||
- `n`: Rule slot from `1` to `16`.
|
||||
- `n`: Rule slot in the build's compiled table (normally `1-31`; some
|
||||
constrained profiles use fewer slots).
|
||||
- `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`).
|
||||
- `N`: Match only at received hop count `N`.
|
||||
- `N+`: Match at received hop count `N` and higher.
|
||||
- `N-M`: Match the inclusive received-hop range.
|
||||
- `all`: Match every received hop count (`0-63`).
|
||||
- `0+`, `all`, and an omitted hop expression are equivalent. The CLI displays
|
||||
the saved range as `all`.
|
||||
- `channel=*|public|#name|128-bit-key|256-bit-key`: Optional authenticated
|
||||
group-channel match. It applies only to `GRP_TXT`/`GRP_DATA`; `type=any`
|
||||
plus a channel condition therefore matches only those group types.
|
||||
- `prefix=<ID[,ID...]>`: Optional ordered source-path prefix of one to three
|
||||
pbyte IDs. IDs must all be 2, 4, or 6 hex characters, matching a packet's
|
||||
1-, 2-, or 3-byte pbyte width. `path=<prefix>` is an alias.
|
||||
- `in=any|none|scoped|allowed|unknown|scope:<name>|region:<name>`: Optional
|
||||
condition on the original incoming scope, before any rule rewrites it.
|
||||
`none` is an unscoped flood. `scope:name` is the exact public
|
||||
hashtag-derived scope. `region:name` is an exact allowed region match.
|
||||
- `drop`: Explicit drop action. The `flood.rule` form requires an explicit
|
||||
action. For compatibility, a legacy `flood.filter` row with no rewrite,
|
||||
rate, or stop action is treated as drop.
|
||||
- `region=<name>`: Rewrite action using an existing locally allowed region.
|
||||
- `rate=N/min`: Per-node, per-row fixed one-minute forwarding limit. It can be
|
||||
the only action or accompany `scope=`/`region=`. Counters are charged only
|
||||
for packets that pass all forwarding gates.
|
||||
- `priority=0-255`: Optional processing order. Higher values run first and
|
||||
lower slot number breaks a tie. The default is `0`; `pri=` is an alias.
|
||||
- `stop` or `action=stop`: Apply this matching row, then stop lower-order FPF7
|
||||
rows from processing. It can stand alone or accompany drop, rewrite, or
|
||||
rate. A stop-only row acts as an exception to lower-priority FPF7 rules.
|
||||
- `suspend=tempradio`: Optional. Skip this row only while the temporary radio
|
||||
is actually active.
|
||||
- `scope=<name>`: Optional scope-setting action. The name is normalized with a
|
||||
leading `#` and its 128-bit transport key is derived directly from that
|
||||
hashtag. It does not need to exist in the region list. Public names up to 30
|
||||
characters are accepted; private `$` scopes are not.
|
||||
- `require=region`: Optional and valid only with `scope=`. Apply the scope
|
||||
rewrite only if the original incoming packet already passes this repeater's
|
||||
- `require=region`: Legacy alias for `in=allowed`. Apply the row only if the
|
||||
original incoming packet already passes this repeater's
|
||||
region gate. An incoming transport scope must resolve to a locally allowed
|
||||
region; an unscoped flood must be allowed by the wildcard region. The check
|
||||
occurs before any scope rewrite during this receive pass.
|
||||
- `tx=slow`: Optional and valid only with `scope=`. Use an effective inbound
|
||||
- `tx=slow`: Optional and valid with `scope=` or `region=`. Use an effective inbound
|
||||
`rxdelay` base of `max(2, configured rxdelay * 2)`, keep normal outbound
|
||||
queue priority, and retransmit with the maximum supported `txdelay` factor
|
||||
of `2.0`. Scope rows default to fast; `tx=fast` explicitly restores that
|
||||
default when replacing a slow row.
|
||||
- `path=blacklist`: Optional unordered path condition. The persistent blacklist
|
||||
contains up to 255 unique 3-byte repeater IDs on ESP32 builds and 18 on other
|
||||
builds, each written as six hexadecimal digits. A packet with 3-byte path
|
||||
hashes matches after one exact ID hit. A packet with 2-byte path hashes
|
||||
matches after two path entries match the first two bytes of listed IDs.
|
||||
Packets with 1-byte path hashes never match this condition. Each received
|
||||
path entry is counted at most once.
|
||||
is repeater-only. It contains up to 255 unique 3-byte repeater IDs on ESP32
|
||||
builds and 18 on other builds, each written as six hexadecimal digits. A
|
||||
packet with 3-byte path hashes matches after one exact ID hit. A packet with
|
||||
2-byte path hashes matches after two path entries match the first two bytes
|
||||
of listed IDs. Packets with 1-byte path hashes never match this condition.
|
||||
Each received path entry is counted at most once.
|
||||
|
||||
The payload names follow the [MeshCore packet-format allocation](https://docs.meshcore.io/packet_format/):
|
||||
|
||||
@@ -1745,16 +1737,21 @@ The payload names follow the [MeshCore packet-format allocation](https://docs.me
|
||||
`ROUTE_TYPE_FLOOD` (`0x01`, unscoped flood). Direct routes `0x02` and `0x03`
|
||||
are never affected.
|
||||
|
||||
**Behavior:** A row with `path=blacklist` must meet the path condition as well
|
||||
as its payload-type and hop-range conditions. Blacklist IDs can occur anywhere
|
||||
in the received path and their configured order is irrelevant. A matching row
|
||||
without `scope=` prevents retransmission by this
|
||||
repeater. A matching row with `scope=` instead sets or replaces the packet's
|
||||
transport scope and does not block it. The lowest-numbered matching scope row
|
||||
wins; matching drop rows remain independent and can still block the rewritten
|
||||
packet. Scope rewriting happens before region enforcement and is trusted even
|
||||
**Behavior:** Match fields within one row are ANDed. Every FPF7 row is matched
|
||||
against the same immutable receive-time packet, before any rule changes its
|
||||
scope. Matching rows are processed in descending `priority`, with lower slot
|
||||
number winning a tie. The first matching `stop` row is included and all
|
||||
lower-order FPF7 matches are discarded. A stop cannot undo an earlier drop or
|
||||
bypass hard forwarding gates and separate forwarding tables. A row with
|
||||
`path=blacklist` must meet the path condition as well as its other conditions;
|
||||
blacklist IDs can occur anywhere in the received path and their configured
|
||||
order is irrelevant. In contrast, `prefix=` begins at the first received path
|
||||
entry and preserves order. A matching drop row prevents retransmission. The
|
||||
highest-order remaining matching scope/region row wins; matching drop and rate
|
||||
rows remain independent and can still block the rewritten packet. Scope rewriting
|
||||
happens before region enforcement and is trusted even
|
||||
when its name is absent from the local region list. It does not bypass
|
||||
`repeat`, `flood.max`, other drop rows, channel blocking, loop detection, or
|
||||
`repeat`, `flood.max`, other drop rows, loop detection, or
|
||||
moderation.
|
||||
|
||||
With `require=region`, a failed check makes that scope row ineligible. It leaves
|
||||
@@ -1773,13 +1770,26 @@ to ten packet airtimes. Selecting the scope already carried is a no-op and
|
||||
does not grant special treatment. An active radio transmission is not
|
||||
preempted, and CAD and airtime-budget limits still apply.
|
||||
|
||||
The packet is still received and can still be logged. Rules are persistent.
|
||||
The packet is still received and can still be logged. Rules are persistent
|
||||
data and can be changed over serial or authenticated remote CLI without an OTA
|
||||
or reboot. `flood.rule` and `flood.filter` address the same table on extended
|
||||
builds; FPF6 files are migrated in memory and the next save writes FPF7.
|
||||
FPF1-FPF5 files are rejected and filtering fails open. FPF7 stores canonical
|
||||
region names rather than transient numeric region
|
||||
IDs. Removing, reordering, or reusing a region ID therefore cannot silently
|
||||
retarget a rule. If a saved input or target region name is absent, that input
|
||||
match or rewrite is inert; restoring the same region name reactivates it.
|
||||
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).
|
||||
|
||||
**Default row:** Repeater firmware seeds a new flood-filter table with
|
||||
Within one receive evaluation, rows that use the same channel key share one
|
||||
authentication result. The cache is discarded after that packet and stores
|
||||
neither plaintext nor passwords; different keys are authenticated separately.
|
||||
|
||||
**Default row:** Repeater firmware and FULL ESP32 room-server firmware seed a
|
||||
new flood-filter table with
|
||||
`ota all suspend=tempradio` in slot 1. This blocks repeated LoRa OTA (`0x0C`)
|
||||
floods at every received hop unless temporary radio is actually active. The OTA
|
||||
core independently refuses OTA receive, relay, and transmit outside temporary
|
||||
@@ -1793,24 +1803,28 @@ set flood.filter.1 0x0C all suspend=tempradio
|
||||
Omitting `all` is equivalent. Omit `.1` as well to reuse an identical rule or
|
||||
the first empty slot instead of replacing slot 1.
|
||||
|
||||
**Remote-admin protection:** Drop rows cannot block `anon_req`, `path`, or
|
||||
`response` at received hop counts `0-6`; those login-capable types become
|
||||
blockable at hop `7`. Flood `txt_msg` cannot be blocked at hops `0-4` and
|
||||
becomes blockable at hop `5`. Scope-setting rows are non-blocking and may apply
|
||||
inside these protected ranges. `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` drop actions; `repeat`, `flood.max*`, loop-detection, and other
|
||||
forwarding gates remain authoritative.
|
||||
**Remote-admin lockout warning:** There are no hidden payload-type or short-hop
|
||||
exceptions. FPF7 drop and rate rows may block `req`, `response`, `txt_msg`,
|
||||
`anon_req`, `path`, ACK, and multipart traffic beginning at hop `0` when their
|
||||
match fields say so. Transit repeaters cannot decrypt these outer types to
|
||||
distinguish an admin exchange from ordinary peer traffic. Keep a serial or
|
||||
other recovery path and stage broad deny/rate rules carefully.
|
||||
|
||||
Without `.n`, `set` reuses an existing rule with the same match, scope,
|
||||
requirement, and suspension settings, or uses the first empty slot. This lets
|
||||
`tx=slow` or `tx=fast` change that rule's timing without creating a duplicate.
|
||||
With `.n`, it replaces that slot.
|
||||
`get flood.filter` gives a compact list. Use `get flood.filter.<n>` for full
|
||||
details, including `path=blacklist`, `scope=`, and `suspend=tempradio`.
|
||||
The detail output also includes `require=region` and `tx=slow` when enabled.
|
||||
Without `.n`, `set` reuses an identical rule or uses the first empty slot. With
|
||||
`.n`, it replaces that slot, which is the intended way to change a row's match
|
||||
or action.
|
||||
`get flood.filter` or `get flood.rule` gives a compact list. Use the numbered
|
||||
form for full details, including channel, prefix, original-scope condition,
|
||||
action, timing, rate, and temporary-radio suspension.
|
||||
|
||||
If all of those fields plus long names would exceed one CLI reply, the
|
||||
numbered form automatically switches to a non-truncating compact spelling.
|
||||
The compact aliases are also accepted by `set`: `c=` means `channel=`, `p=`
|
||||
means `prefix=`, `i=*|n|s|a|u|s:<scope>|r:<region>` means the corresponding
|
||||
`in=` condition, `q=N` means `rate=N/min`, `pri=N` means priority, and `f=st`
|
||||
combines slow timing
|
||||
(`s`) with temporary-radio suspension (`t`). Packet type is shown numerically
|
||||
in that fallback. Normal-sized rows keep the descriptive spelling above.
|
||||
|
||||
The blacklist and filter rows are persisted separately. Replacing or deleting
|
||||
the blacklist does not delete rows containing `path=blacklist`; such rows
|
||||
@@ -1827,12 +1841,16 @@ IDs as fit in one reply; use numbered `get` to inspect entries beyond that
|
||||
reply.
|
||||
|
||||
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.
|
||||
entirely. A custom flood-form trace participates normally: `type=any`, explicit
|
||||
`trace`, rewrite, rate, drop, and stop rows can all apply.
|
||||
|
||||
**Examples:**
|
||||
```text
|
||||
set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86
|
||||
set flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86
|
||||
set flood.rule.4 type=any prefix=860C rate=10/min
|
||||
set flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop
|
||||
get flood.rule.2
|
||||
set flood.filter grp_data 4+
|
||||
set flood.filter.2 PAYLOAD_TYPE_ADVERT 6+
|
||||
set flood.filter ota 2-4
|
||||
@@ -1853,6 +1871,19 @@ get flood.filter.2
|
||||
del flood.filter.2
|
||||
```
|
||||
|
||||
The first rule authenticates `#rgdata`, requires more than three received hops,
|
||||
and adds `#BlackHole86` only when no scope was present. The second rewrites the
|
||||
exact incoming `#usa` scope. The third demonstrates a two-byte pbyte source
|
||||
prefix and a global per-row rate cap. The fourth authenticates `#rgdata` at
|
||||
zero through two hops, applies no FPF7 action of its own, and stops lower-order
|
||||
FPF7 rows; hard gates and separate tables still apply.
|
||||
|
||||
The fixed 240 KB STM32WL profiles leave
|
||||
`MESH_ENABLE_FLOOD_RULE_ENGINE=0` and retain the compact, persistent FPF6
|
||||
`flood.filter` and blacklist commands. They still filter floods, but do not
|
||||
expose the `flood.rule` alias or its extended channel, prefix, input-scope,
|
||||
region-action, or rate fields. No partition size changes are required.
|
||||
|
||||
---
|
||||
|
||||
#### Moderate flood group text by channel, sender, and source path
|
||||
@@ -1933,7 +1964,8 @@ del flood.moderation.3
|
||||
**Filter manager scope:** Permission `5` can use an explicit allowlist of
|
||||
non-secret operational/filter status commands and can change the forwarding
|
||||
controls `repeat`, `loop.detect`, `flood.max*`, `flood.channel.data*`,
|
||||
`flood.channel.block*`, `flood.filter*`, and `flood.moderation*`. It cannot read
|
||||
`flood.filter*`, `flood.rule*`, 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`
|
||||
is limited to region commands, `flood.channel.scope*`, and the same non-secret
|
||||
|
||||
+279
-112
@@ -1,9 +1,13 @@
|
||||
# Repeater Flood Filtering and Moderation
|
||||
# Flood Filtering and Moderation
|
||||
|
||||
This guide explains the Keymind repeater forwarding filters. The filters decide
|
||||
whether this repeater retransmits a packet and can assign a transport scope
|
||||
before that decision. They do not stop local reception, packet logging, or MQTT
|
||||
observation.
|
||||
This guide explains the Keymind forwarding filters. Repeaters expose the full
|
||||
set of channel, rule, blacklist, and moderation controls described here.
|
||||
FULL-profile ESP32 room servers expose the generalized `flood.rule` table (and
|
||||
its `flood.filter` alias) with 31 slots, but not the repeater's channel tables,
|
||||
passive blacklist, or text-moderation table. Standard room-server profiles do
|
||||
not compile the rule table. Filters decide whether the node retransmits a
|
||||
packet and can assign a transport scope before that decision. They do not stop
|
||||
local reception, packet logging, or MQTT observation.
|
||||
|
||||
Only flood routes are filtered:
|
||||
|
||||
@@ -18,7 +22,7 @@ OTA assignment noted below.
|
||||
|
||||
## Before making changes
|
||||
|
||||
Show the current forwarding controls:
|
||||
On a repeater, show the current forwarding controls:
|
||||
|
||||
```text
|
||||
get repeat
|
||||
@@ -27,39 +31,63 @@ 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.channel.scope.require
|
||||
get flood.filter
|
||||
get flood.rule
|
||||
get flood.moderation
|
||||
```
|
||||
|
||||
The `flood.filter` and `flood.moderation` tables each have 16 persistent slots.
|
||||
A new `flood.filter` table starts with `ota all suspend=tempradio` in slot 1;
|
||||
`flood.moderation` starts empty. A row can opt into `suspend=tempradio`;
|
||||
`flood.rule` is an alias for `flood.filter`, not another table. The
|
||||
The generalized FPF7 table has 31 persistent slots; compact target profiles
|
||||
may compile fewer FPF6 filter slots. `flood.moderation` has 16 slots. A new
|
||||
repeater FPF7 table starts with `ota all suspend=tempradio` in slot 1 and an
|
||||
authenticated `#wardriving hops=5+` drop in slot 2; FULL room servers seed only
|
||||
the OTA row. `flood.moderation` starts 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.
|
||||
|
||||
On a FULL ESP32 room server, use `get flood.rule` (or `get flood.filter`) for
|
||||
the available table. Remote rule changes require room-server administrator
|
||||
access. `flood.filter.blacklist*` and `path=blacklist` are repeater-only; use
|
||||
the ordered `prefix=` condition on a room server.
|
||||
|
||||
## Force floods into a transport scope
|
||||
|
||||
`flood.channel.scope` can add a scope to a received unscoped flood or replace
|
||||
the scope of a transport-scoped flood before this repeater forwards it:
|
||||
|
||||
```text
|
||||
set flood.channel.scope <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]
|
||||
set flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region> [path=blacklist|path=bucket:1-6] [tx=slow]
|
||||
set flood.channel.scope <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]
|
||||
set flood.channel.scope.<slot> <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]
|
||||
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.
|
||||
The channel may be `public`, `#channel`, or a 128/256-bit hex key. A bare
|
||||
target names an existing region with a usable transport key. Use
|
||||
`scope=<name>` instead to derive a regionless public hashtag scope exactly as
|
||||
`flood.filter scope=<name>` does. The direct name is normalized with a leading
|
||||
`#`, may contain up to 30 characters, and does not need a region-list entry.
|
||||
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.
|
||||
|
||||
For example, this authenticates only `#rgdata` and rewrites it to
|
||||
`#BlackHole86` without creating a region:
|
||||
|
||||
```text
|
||||
set flood.channel.scope #rgdata scope=BlackHole86
|
||||
get flood.channel.scope.1
|
||||
```
|
||||
|
||||
If that channel arrives scoped to `#usa`, the rule replaces `#usa` with
|
||||
`#BlackHole86`. It also handles unscoped packets and replaces any other
|
||||
incoming scope; the source scope is not a condition on the rule.
|
||||
|
||||
Add `path=blacklist` to make a channel-scope row eligible only when the
|
||||
received path matches the passive `flood.filter.blacklist` ID table. It does
|
||||
@@ -81,11 +109,11 @@ 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.
|
||||
- `other:*` handles every remaining flood payload type, including flood-form
|
||||
TRACE and OTA.
|
||||
|
||||
`login:*` and `other:*` classify only the visible outer payload type; they do
|
||||
not authenticate its contents. Exact channel rows with usable target regions
|
||||
not authenticate its contents. Exact channel rows with usable targets
|
||||
always take precedence over `txt:*`, even if that wildcard has a lower slot
|
||||
number. Within the exact class, matching path-qualified rows are tried before
|
||||
ordinary fallback rows. The same qualified-then-fallback order applies within
|
||||
@@ -110,12 +138,14 @@ enabled.
|
||||
On a successful match, an unscoped route changes from `ROUTE_TYPE_FLOOD` to
|
||||
`ROUTE_TYPE_TRANSPORT_FLOOD`; an already-scoped route remains transport-flood
|
||||
but receives replacement codes. Transport code 0 is calculated with the target
|
||||
region key over the payload type and payload, and code 1 becomes zero. The
|
||||
region or direct hashtag key over the payload type and payload, and code 1 becomes zero. The
|
||||
change happens before region enforcement, forwarding filters, and
|
||||
deduplication. Consequently `flood.max.unscoped` no longer applies to a packet
|
||||
converted from unscoped, while `flood.max`, target-region permissions,
|
||||
`flood.filter`, channel blocking, loop detection, and moderation still apply
|
||||
to every rewritten packet. By default, if the selected scope differs and the
|
||||
converted from unscoped, while `flood.max`, `flood.filter`, loop detection, and
|
||||
moderation still apply to every rewritten packet. A region
|
||||
target must be flood-allowed. A regionless target is trusted for this matched
|
||||
receive pass, but it neither creates a region nor changes how unrelated packets
|
||||
with the same transport code pass the region gate. By default, if the selected scope differs and the
|
||||
rewritten packet passes those checks, its initial retransmission uses zero
|
||||
`txdelay` and the highest outbound queue priority so the newly scoped copy can
|
||||
win at the next hop. Add `tx=slow` to use an effective inbound `rxdelay` base
|
||||
@@ -126,17 +156,19 @@ It does not preempt an active radio transmission or bypass CAD and
|
||||
airtime-budget limits. Selecting the scope already present is a no-op and does
|
||||
not grant special treatment.
|
||||
|
||||
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.
|
||||
Direct routes are never rewritten. Standard traceroute is direct-routed and
|
||||
therefore remains outside this flood-only table. A custom flood-form TRACE is
|
||||
treated like every other flood: an applicable wildcard may rewrite it and the
|
||||
normal region/unknown-code gates still apply. Scope assignment 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 or replaces the existing one, 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. A new repeater also seeds `ota all suspend=tempradio` in flood-filter
|
||||
unscoped and transport-scoped flood routes. A region target must allow
|
||||
flooding; a direct target uses the regionless trust behavior above. A new
|
||||
repeater also seeds `ota all suspend=tempradio` in flood-filter
|
||||
slot 1. That visible rule blocks OTA forwarding at every received hop outside
|
||||
temporary-radio operation and is skipped while temporary radio is active.
|
||||
Independently, the OTA core refuses OTA receive, relay, and transmit outside an
|
||||
@@ -145,17 +177,22 @@ replaced.
|
||||
|
||||
Capacity is selected at build time:
|
||||
|
||||
- Roomy ESP32 builds: 255 slots, 9,180 bytes RAM, 9,185-byte file.
|
||||
- Roomy ESP32 builds: 255 rule slots and 32 regionless-target slots, 10,204
|
||||
bytes RAM, 10,210-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.
|
||||
constrained builds: 31 rule and regionless-target slots, 2,108 bytes RAM,
|
||||
2,114-byte file.
|
||||
- Very-tight STM32WL builds: 15 rule slots and one reusable regionless-target
|
||||
slot, 572 bytes RAM, 578-byte file.
|
||||
- The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 rule and
|
||||
regionless-target slots, 272 bytes RAM, 278-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.
|
||||
Each rule retains its 36-byte record. A separate table holds 32-byte normalized
|
||||
names for up to the smaller of the rule count or 32 distinct regionless
|
||||
targets, except that very-tight STM32WL builds retain one reusable direct
|
||||
target. Both configured regions and regionless targets can be reused by any
|
||||
number of rules.
|
||||
|
||||
## Require valid incoming scopes only on selected channels
|
||||
|
||||
@@ -183,7 +220,7 @@ cannot rescue it or grant special receive/transmit timing.
|
||||
|
||||
Group-channel packets that do not authenticate against any listed key bypass
|
||||
the region/unknown-code forwarding gate. They still pass through `repeat`,
|
||||
`flood.max*`, `flood.filter`, `flood.channel.block`, loop detection, payload
|
||||
`flood.max*`, `flood.filter`, loop detection, payload
|
||||
validation, and moderation. Non-channel flood payload types retain the normal
|
||||
global region behavior. A one-byte channel-hash collision is only a prefilter;
|
||||
the packet must also pass MAC validation/decryption with the configured key.
|
||||
@@ -225,8 +262,8 @@ 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.
|
||||
already processed into the seen table. Flood-form TRACE participates in the
|
||||
same arbitration. Direct traceroute never enters this flood queue.
|
||||
|
||||
A packet that already matches a configured fast `flood.channel.scope` or
|
||||
`flood.filter scope=` action and needs its scope changed bypasses this inbound
|
||||
@@ -234,10 +271,130 @@ A packet that already matches a configured fast `flood.channel.scope` or
|
||||
configured base, floored at `2.0`, and participates in normal queued-copy
|
||||
scope arbitration.
|
||||
|
||||
## Filter by payload type, received hop count, and path
|
||||
## Runtime flood rules
|
||||
|
||||
Use `flood.filter` when the packet type, current path length, or listed path
|
||||
identifiers are enough to make the decision:
|
||||
On repeaters with the rule engine enabled and on FULL-profile ESP32 room
|
||||
servers, `flood.rule` and `flood.filter` are two names for the same persistent
|
||||
table. The evaluator is fixed firmware, but every row is data, so an
|
||||
authenticated operator can add, replace, inspect, or delete a row without an
|
||||
OTA or reboot. Existing `flood.filter` commands remain compatible. Only FPF6
|
||||
and FPF7 files are accepted; FPF1-FPF5 files are rejected and filtering fails
|
||||
open. A row saved by the extended engine uses FPF7.
|
||||
|
||||
The former `flood.channel.block` table is now represented by ordinary FPF7
|
||||
rows. On a generalized repeater, an existing FCB2 file is imported once into
|
||||
free FPF7 slots and then removed. For example, an old `#wardriving h=4` row
|
||||
becomes `type=any channel=#wardriving hops=5+ drop`. The 31 FPF7 slots replace
|
||||
the former 16 general-filter plus 15 channel-block slots. Compact STM32WL FPF6
|
||||
builds cannot match authenticated channels.
|
||||
|
||||
FPF7 binds `in=region:<name>` and `region=<name>` to canonical region names,
|
||||
not numeric region IDs. Removing, reordering, or reusing a region ID cannot
|
||||
silently redirect a rule. If the saved name is missing, an input-region match
|
||||
does not match and a target-region rewrite is skipped. Re-adding the same name
|
||||
reactivates the rule.
|
||||
|
||||
The extended form is:
|
||||
|
||||
```text
|
||||
set flood.rule[.<slot>] type=<type> [hops=<range>] [channel=<channel>]
|
||||
[prefix=<ID[,ID...]>] [in=<input-scope>] <action> [rate=<N>/min]
|
||||
[priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]
|
||||
get flood.rule
|
||||
get flood.rule.<slot>
|
||||
del flood.rule.<slot>
|
||||
del flood.rule all
|
||||
```
|
||||
|
||||
The command must be entered on one line. Match fields in one row are ANDed.
|
||||
Every row is matched against the same immutable packet state captured on
|
||||
receive, before any rule rewrites its scope. Matching rows are then processed
|
||||
by descending `priority`; lower slot number wins a priority tie. Priority
|
||||
defaults to `0`.
|
||||
|
||||
The first matching `stop` row ends FPF7 processing after that row. Higher-order
|
||||
matches and the stop row still apply; lower-order matches do not. A stop-only
|
||||
row is therefore an exception to lower-priority FPF7 rows. It cannot undo a
|
||||
higher-priority drop and it does not bypass hard forwarding gates or separate
|
||||
forwarding tables. Without a stop row, matching drop and rate rows remain
|
||||
independent and the highest-order matching scope or region rewrite wins.
|
||||
|
||||
Match fields:
|
||||
|
||||
- `type=` accepts the same packet names and numeric values as legacy
|
||||
`flood.filter`. The positional form remains accepted.
|
||||
- `hops=` accepts `all`, `N`, `N+`, or `N-M`. The positional form remains
|
||||
accepted. Received hops over 3 are written as `hops=4+`.
|
||||
- `channel=*|public|#name|128-bit-key|256-bit-key` authenticates a `GRP_TXT` or
|
||||
`GRP_DATA` packet with that channel key before the row can match. With
|
||||
`type=any`, this condition naturally limits the row to those two group
|
||||
packet types.
|
||||
- `prefix=` is a source-path prefix containing one to three comma-separated
|
||||
pbyte IDs. Every ID must use the packet's pbyte width: 2, 4, or 6 hex
|
||||
characters for 1-, 2-, or 3-byte paths. Order matters and matching begins at
|
||||
the first received path entry. `path=<prefix>` is an alias;
|
||||
`path=blacklist` retains its separate unordered-list behavior.
|
||||
- `in=any|none|scoped|allowed|unknown|scope:<name>|region:<name>` tests the
|
||||
original incoming route before any rewrite. `none` means an unscoped flood;
|
||||
`scoped` means any transport flood; `scope:name` compares the exact public
|
||||
hashtag-derived scope; and `region:name` compares an allowed configured
|
||||
region. `allowed` is the legacy `require=region` test and includes an
|
||||
unscoped packet when the wildcard region allows it. `unknown` means a scoped
|
||||
packet that does not resolve to an allowed local region.
|
||||
|
||||
Actions:
|
||||
|
||||
- `drop` prevents retransmission when the row matches. The strict
|
||||
`flood.rule` form requires an explicit action. For backward compatibility,
|
||||
only a legacy `flood.filter` row with no rewrite, rate, or stop action means
|
||||
drop implicitly.
|
||||
- `scope=<name>` assigns a direct public hashtag scope without requiring a
|
||||
region-list entry.
|
||||
- `region=<name>` assigns an existing locally allowed region and its transport
|
||||
key.
|
||||
- `rate=N/min` is a per-node, per-row fixed one-minute forwarding limit. It can
|
||||
stand alone or accompany a scope/region rewrite. Quota is charged only after
|
||||
every other forwarding gate, including moderation, accepts the packet. It is
|
||||
not keyed per sender; use `flood.moderation` when a group-text rate must be
|
||||
tied to an exact display name.
|
||||
- `priority=0-255` controls processing order. Higher values run first; lower
|
||||
slot number breaks ties. `pri=` is the compact alias.
|
||||
- `stop` (or `action=stop`) applies this row and prevents lower-order FPF7 rows
|
||||
from acting. It can stand alone or accompany drop, rewrite, or rate.
|
||||
|
||||
When several rows use the same channel key, authentication is performed once
|
||||
for that packet and reused by those rows. This cache lives only for the current
|
||||
receive evaluation; it is not persisted and never stores plaintext or a
|
||||
password.
|
||||
|
||||
The exact requested examples are:
|
||||
|
||||
```text
|
||||
# If #rgdata arrives unscoped with more than 3 received hops, add
|
||||
# the regionless #BlackHole86 scope.
|
||||
set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86
|
||||
|
||||
# Rewrite the exact incoming #usa scope to #BlackHole86 for #rgdata.
|
||||
set flood.rule.3 type=grp_data channel=#rgdata in=scope:usa scope=BlackHole86
|
||||
|
||||
# Match a two-byte source-path prefix and cap forwarding at 10 per minute.
|
||||
set flood.rule.4 type=any prefix=860C rate=10/min
|
||||
|
||||
# Keep authenticated #rgdata at two hops or less out of lower-priority FPF7
|
||||
# rules. Hard gates and separate tables still apply.
|
||||
set flood.rule.5 type=grp_data hops=0-2 channel=#rgdata priority=200 stop
|
||||
|
||||
get flood.rule.2
|
||||
get flood.rule.3
|
||||
get flood.rule.4
|
||||
```
|
||||
|
||||
The 240 KB STM32WL profiles keep `MESH_ENABLE_FLOOD_RULE_ENGINE=0` and retain
|
||||
the persistent compact FPF6 `flood.filter` and blacklist syntax below. They
|
||||
still perform filtering, but omit the generalized `flood.rule` parser and
|
||||
extended fields. No partition size is changed by this feature.
|
||||
|
||||
The compatible filter and blacklist commands are:
|
||||
|
||||
```text
|
||||
set flood.filter.blacklist <ID[,ID...]>
|
||||
@@ -277,32 +434,38 @@ of listed IDs. Each received entry is counted at most once. A 1-byte path
|
||||
never qualifies. The IDs may occur anywhere in the received path; neither
|
||||
their list order nor their path order matters.
|
||||
|
||||
Without a slot number, `set` reuses a rule with the same match, scope,
|
||||
requirement, and suspension settings, or selects the first empty slot. This
|
||||
lets `tx=slow` or `tx=fast` change that rule's timing without creating a
|
||||
duplicate. With a slot number, it replaces that slot. Omitting the hop
|
||||
expression means `all` (`0-63`).
|
||||
Without a slot number, `set` reuses an identical row or selects the first empty
|
||||
slot. Use a slot number to replace a row whose match or action is changing.
|
||||
Omitting the hop expression means `all` (`0-63`).
|
||||
|
||||
A row without `scope=` is the existing drop action. A row with `scope=` is a
|
||||
scope-setting action instead: it adds transport scope to an unscoped packet or
|
||||
replaces the codes on an already-scoped packet. The scope name is normalized
|
||||
Numbered `get` normally uses the long field names. If a rule containing
|
||||
several maximum-length names would exceed one CLI reply, it switches to a
|
||||
non-truncating compact spelling that `set` also accepts: `c=` is `channel=`,
|
||||
`p=` is `prefix=`, `i=*|n|s|a|u|s:<scope>|r:<region>` represents `in=`, `q=N`
|
||||
is `rate=N/min`, and `f=st` combines slow timing (`s`) and temporary-radio
|
||||
suspension (`t`). The fallback prints packet type numerically.
|
||||
|
||||
A legacy row without `scope=` is the existing drop action. On extended builds,
|
||||
an explicit `drop` has the same result, while `rate=` by itself creates a
|
||||
rate-only row. A row with `scope=` adds transport scope to an unscoped packet
|
||||
or replaces the codes on an already-scoped packet. The scope name is normalized
|
||||
with a leading `#`, and the 128-bit transport key is derived directly from that
|
||||
hashtag. The name does not need to exist in the region list and is not added to
|
||||
it. Public names up to 30 characters are accepted; private `$` scopes are not.
|
||||
|
||||
Add `require=region` to a scope row when rewriting must not rescue a packet
|
||||
that the incoming-region gate would reject. The repeater evaluates the packet's
|
||||
`require=region` is the legacy spelling of `in=allowed`. The repeater evaluates the packet's
|
||||
original route before any rewrite in that receive pass. An incoming transport
|
||||
scope must match a locally allowed region; an unscoped flood must be allowed by
|
||||
the wildcard region. If the check fails, that scope row is skipped, the filter
|
||||
does not grant its region bypass, and the unchanged packet is allowed to fail
|
||||
the wildcard region. If the check fails, that row is skipped, the filter does
|
||||
not grant a region bypass, and the unchanged packet is allowed to fail
|
||||
normal region enforcement. Other independently configured scope rows still
|
||||
apply in their normal order.
|
||||
|
||||
When multiple scope rows match, the lowest-numbered row wins. Scope rows do not
|
||||
approve a packet: any matching drop row and every remaining forwarding gate can
|
||||
When multiple scope or region rows match, the highest-priority row wins; lower
|
||||
slot number breaks a priority tie.
|
||||
Rewrite rows do not approve a packet: any matching drop row and every remaining forwarding gate can
|
||||
still reject it. A filter-assigned scope is trusted without local region-list
|
||||
validation, but `repeat`, `flood.max`, channel blocking, loop detection, and
|
||||
validation, but `repeat`, `flood.max`, loop detection, and
|
||||
moderation still apply. By default, a changed scope bypasses inbound `rxdelay`,
|
||||
then is retransmitted with zero `txdelay` and the highest outbound queue
|
||||
priority. Add `tx=slow` to use an effective inbound `rxdelay` base of
|
||||
@@ -312,10 +475,13 @@ zero through ten packet airtimes. `tx=fast` explicitly restores the default.
|
||||
Selecting the scope already present does not grant special treatment. Active
|
||||
radio transmission, CAD, and airtime-budget limits are unchanged.
|
||||
|
||||
The blacklist and rule table are persisted separately. Deleting the blacklist
|
||||
leaves `path=blacklist` rows in place but dormant until IDs are configured
|
||||
again. Path hashes are truncated routing identifiers and are not authenticated
|
||||
proof that a particular repeater handled a packet.
|
||||
On repeaters, the blacklist and rule table are persisted separately. FPF7
|
||||
stores compiled match/action fields and canonical region names, not executable
|
||||
code, a script, or numeric region references. Deleting the blacklist leaves
|
||||
`path=blacklist` rows in place but dormant until IDs are configured again.
|
||||
Path hashes are truncated routing identifiers and are not authenticated proof
|
||||
that a particular repeater handled a packet. FULL room servers have only the
|
||||
rule table and reject blacklist commands.
|
||||
|
||||
On first initialization, flood-filter slot 1 is seeded with:
|
||||
|
||||
@@ -333,38 +499,29 @@ 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 drop `any` row therefore still applies during
|
||||
the temporary-radio window, subject to the short-path remote-admin protection
|
||||
below. `repeat`, `flood.max*`, region handling, loop detection, and the OTA
|
||||
the temporary-radio window unless an earlier matching stop row ends FPF7
|
||||
processing. `repeat`, `flood.max*`, region handling, 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.
|
||||
custom flood-form trace, `type=any`, explicit `trace`, scope, region, rate, and
|
||||
drop rows all behave normally. The stock core does not normally flood-forward
|
||||
TRACE packets.
|
||||
|
||||
### Remote administration cannot be type-filtered on short paths
|
||||
### Remote-administration lockout warning
|
||||
|
||||
`flood.filter` drop actions use 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`. Scope-setting rows do not block traffic and may apply within the
|
||||
protected ranges.
|
||||
There are no hidden payload-type or short-hop exemptions in FPF7. Drop and
|
||||
rate rules can block `req`, `response`, `txt_msg`, `anon_req`, `path`, ACK, and
|
||||
multipart traffic beginning at hop `0` when their match fields say so.
|
||||
|
||||
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 configurable `flood.filter` drop actions. It does
|
||||
not override `repeat`, `flood.max*`, loop detection, or other forwarding gates.
|
||||
from ordinary peer packets with the same outer type. A rule therefore affects
|
||||
the complete outer packet class, not only packets that ultimately authenticate
|
||||
as administrators. Keep a serial or other recovery path and stage broad
|
||||
deny/rate rules carefully.
|
||||
|
||||
Hop expressions are based on the path count when this repeater receives the
|
||||
packet:
|
||||
@@ -431,9 +588,9 @@ get flood.filter
|
||||
|
||||
On a new table, the factory OTA rule occupies slot 1, so these unnumbered
|
||||
commands normally fill slots 2 through 7. Existing tables may choose different
|
||||
free slots. The `response`, `anon_req`, and `path` thresholds are above their
|
||||
protected `0-6` range, so all six rules take effect at the thresholds shown.
|
||||
The Control rule allows a flood received with path count `0` to be forwarded
|
||||
free slots. All six rules take effect at the thresholds shown; there are no
|
||||
hidden short-hop exceptions. The Control rule allows a flood received with
|
||||
path count `0` to be forwarded
|
||||
once, then stops it at the next repeater. Normal node-discovery Control packets
|
||||
are direct zero-hop packets and never enter `flood.filter`. These rules affect
|
||||
only retransmission by the repeater; local reception and logging remain
|
||||
@@ -542,18 +699,23 @@ other words, the controls combine as deny rules:
|
||||
original incoming scope; unlisted group channels bypass the later region
|
||||
gate while the table is active.
|
||||
2. `flood.channel.scope` tries a path-qualified channel row before that
|
||||
channel's ordinary fallback, then adds or replaces the scope.
|
||||
3. A matching `flood.filter scope=` row may replace that result; its scope does
|
||||
not require a region-list entry.
|
||||
channel's ordinary fallback, then adds or replaces the scope from either a
|
||||
configured region or a direct `scope=<name>` target.
|
||||
3. All extended `flood.rule` match fields are evaluated against the same
|
||||
original incoming packet. Matches are ordered by descending priority and
|
||||
then ascending slot. The first matching `stop` row removes every later FPF7
|
||||
match. The highest-order remaining `scope=` or `region=` row may replace the
|
||||
channel-scope result; a direct scope does not require a region-list entry.
|
||||
4. `repeat`, `flood.max*`, and the channel-data gate are checked.
|
||||
5. `flood.filter` drop rows check payload type and hop range, subject to the
|
||||
login floor of `7` and flood-text floor of `5` described above.
|
||||
6. `flood.channel.block` checks keyed channels.
|
||||
7. Region and loop-detection rules are checked; a filter-assigned scope is
|
||||
already trusted when it has no region-list match, except that it cannot
|
||||
rescue a channel rejected by `flood.channel.scope.require`.
|
||||
8. `flood.moderation` checks decrypted group text, username, rate, hops, and
|
||||
path.
|
||||
5. Remaining `flood.filter`/`flood.rule` drop and rate rows use that saved
|
||||
match result. No packet type or short-hop range is silently exempted.
|
||||
6. Region and loop-detection rules are checked; a regionless scope assigned by
|
||||
either table is already trusted when it has no region-list match, except
|
||||
that it cannot rescue a channel rejected by
|
||||
`flood.channel.scope.require`.
|
||||
7. `flood.moderation` checks decrypted group text, username, rate, hops, and
|
||||
path. If it accepts the packet, matching general-rule rate counters are
|
||||
charged immediately before retransmission is approved.
|
||||
|
||||
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
|
||||
@@ -562,25 +724,30 @@ control and will actually be retransmitted.
|
||||
|
||||
## Delegate filter management
|
||||
|
||||
ACL permission `5` is the filter-manager role:
|
||||
On repeaters, ACL permission `5` is the filter-manager role:
|
||||
|
||||
```text
|
||||
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
|
||||
`loop.detect`, `flood.max*`, `flood.channel.data*`,
|
||||
`flood.filter*`, `flood.rule*`, 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. Because `flood.filter scope=` derives a
|
||||
public hashtag key directly, a filter manager can configure that action without
|
||||
region-manager permission; it still cannot edit the region hierarchy.
|
||||
|
||||
FULL ESP32 room servers use their existing administrator check for remote
|
||||
`flood.rule` and `flood.filter` commands; they do not grant this table through
|
||||
permission `5`.
|
||||
|
||||
ACL permission `4`, the region/scope-manager role, can read, add, replace, and
|
||||
delete `flood.channel.scope` and `flood.channel.scope.require` rows and manage
|
||||
regions. This lets the same delegate create target regions, assign forced
|
||||
scopes, and select the channels that require valid incoming scopes.
|
||||
scopes from regions or direct hashtag names, and select the channels that
|
||||
require valid incoming scopes.
|
||||
|
||||
## Security limitations
|
||||
|
||||
@@ -599,13 +766,13 @@ private transport/channel keys, and controlled device access.
|
||||
The repeater's factory-seeded forwarding rows can be restored through the CLI:
|
||||
|
||||
```text
|
||||
set flood.channel.block.1 #wardriving h=4
|
||||
set flood.filter.1 0x0C all suspend=tempradio
|
||||
set flood.rule.1 type=ota hops=all drop suspend=tempradio
|
||||
set flood.rule.2 type=any channel=#wardriving hops=5+ drop
|
||||
```
|
||||
|
||||
Each command explicitly replaces slot 1 in its own table. Inspect the slot first
|
||||
if it may now contain another rule. To preserve existing slot assignments, omit
|
||||
`.1`; the command then reuses an identical row or uses the first empty slot.
|
||||
These commands explicitly replace the two seeded generalized-repeater slots.
|
||||
Inspect them first if they may now contain other rules. Compact FPF6 builds use
|
||||
only the first command's `flood.filter.1 0x0C all suspend=tempradio` form.
|
||||
|
||||
## Remove the custom rules
|
||||
|
||||
@@ -618,5 +785,5 @@ 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.
|
||||
This does not change the older `flood.max*`, loop-detection, or region settings;
|
||||
inspect or reset those separately when troubleshooting.
|
||||
@@ -80,13 +80,12 @@ set flood.retry.ignore none
|
||||
| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` |
|
||||
| `battery.alert.critical` | Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` |
|
||||
| `recent.repeater` | Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep. | `get recent.repeater [page]`, `get recent.repeaters search <2|4|6 hex> [page]`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `get recent.repeaters search A1B2` |
|
||||
| `flood.channel.data` | Turns forwarding of flood `GRP_DATA` channel packets on or off. With the default `on`, `GRP_DATA` repeats normally even when `flood.channel.block.hops` is set. | `get flood.channel.data`, `set flood.channel.data on/off` | `set flood.channel.data off` |
|
||||
| `flood.channel.data` | Turns forwarding of all flood `GRP_DATA` channel packets on or off. The default is `on`; use an authenticated FPF7 `channel=` drop when only selected channels should be blocked. | `get flood.channel.data`, `set flood.channel.data on/off` | `set flood.channel.data off` |
|
||||
| `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.channel.scope` | Adds a transport-region scope to received unscoped floods or replaces the scope of already-scoped floods. By default, a changed packet bypasses inbound `rxdelay` and is forwarded at the highest outbound queue priority with zero initial `txdelay`, so the selected scope can win at the next hop. `tx=slow` uses an effective inbound `rxdelay` base of `max(2, configured rxdelay * 2)`, retains normal queue priority, and forces the maximum `txdelay` factor of `2.0`; its actual randomized transmit delay ranges from zero through ten packet airtimes. `path=blacklist` and `path=bucket:<1-6>` make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat `txt:*`; path-qualified rows beat the ordinary channel fallback. `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> [path=blacklist|path=bucket:1-6] [tx=slow]`, `del flood.channel.scope.<n>|all` | `set flood.channel.scope public east path=bucket:1` |
|
||||
| `flood.channel.scope` | Adds a transport scope to received unscoped floods or replaces the scope of already-scoped floods. A bare target uses an existing flood-allowed region; `scope=<name>` derives a public hashtag target directly without creating a region, exactly like `flood.filter scope=`. By default, a changed packet bypasses inbound `rxdelay` and is forwarded at the highest outbound queue priority with zero initial `txdelay`, so the selected scope can win at the next hop. `tx=slow` uses an effective inbound `rxdelay` base of `max(2, configured rxdelay * 2)`, retains normal queue priority, and forces the maximum `txdelay` factor of `2.0`; its actual randomized transmit delay ranges from zero through ten packet airtimes. `path=blacklist` and `path=bucket:<1-6>` make a row path-qualified; bridge buckets remain usable while bridge retry is off. An already-matching scope is a no-op. Exact channel keys beat `txt:*`; path-qualified rows beat the ordinary channel fallback. `login:*` covers the remote-login family, and `other:*` covers every remaining flood type, including flood-form TRACE and OTA. Direct traceroute remains outside the flood table. ACL permission `4` can manage the table. | `get flood.channel.scope[.n]`, `set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region|scope=name> [path=blacklist|path=bucket:1-6] [tx=slow]`, `del flood.channel.scope.<n>|all` | `set flood.channel.scope #rgdata scope=BlackHole86` |
|
||||
| `flood.channel.scope.require` | Switches group-channel region enforcement to opt-in when the table has entries. Listed authenticated `GRP_TXT`/`GRP_DATA` channels must arrive already scoped to a locally allowed region; unscoped, unknown, or denied incoming scopes are dropped before any rewrite can rescue them. Unlisted group channels bypass only the region gate and retain all other forwarding controls. An empty table preserves global region behavior; non-channel payloads are unchanged. ACL permission `4` can manage the table. | `get flood.channel.scope.require[.n]`, `set flood.channel.scope.require[.n] <public|#channel|key>`, `del flood.channel.scope.require.<n>|all` | `set flood.channel.scope.require #bot` |
|
||||
| `flood.filter` | Persistent repeater-only rules for flood routes `0x00`/`0x01`, selected by payload type, optional received hop count/range (omitted means `all`), and optional unordered `path=blacklist`. The separate blacklist stores up to 255 full 3-byte repeater IDs on ESP32 and 18 on other builds; the path condition qualifies on at least one exact 3-byte hit, at least two 2-byte-prefix hits, and never on 1-byte paths. Rows normally drop matching retransmissions; `scope=<name>` instead adds or replaces a public hashtag-derived scope without requiring a region-list entry. Scope rewrites default to bypassing inbound `rxdelay` and using zero initial `txdelay` plus highest queue priority; `tx=slow` uses an effective inbound `rxdelay` base of `max(2, configured rxdelay * 2)`, retains normal queue priority, and forces `txdelay` factor `2.0`. Adding `require=region` makes that rewrite conditional on the original incoming packet already passing local region enforcement, so unknown or denied incoming scopes are not rescued. New tables seed slot 1 with `ota all suspend=tempradio`; only rows marked `suspend=tempradio` are skipped during temporary-radio operation. Login-capable `anon_req`/`path`/`response` drop actions begin at hop `7`; flood `txt_msg` drops begin at hop `5`. Standard direct traceroute, other direct routing, and local receive/logging are unchanged. | `get/set/del flood.filter.blacklist[.n]`, `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [path=blacklist] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]`, `del flood.filter.<n>|all` | `set flood.filter grp_txt all scope=local tx=slow` |
|
||||
| `flood.filter` | Persistent flood-route rules selected by payload type and optional hop range. Extended builds have 31 FPF7 slots and add authenticated channel, ordered pbyte prefix, original scope/region, rewrite, rate, priority, and terminal stop fields; `flood.rule` is an alias for the same table. Repeaters also support a separate unordered `path=blacklist`. FULL ESP32 room servers have the same 31 extended slots but no blacklist and require remote administrator access. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and blacklist commands. New generalized repeater tables seed slot 1 with `ota all suspend=tempradio` and slot 2 with an authenticated `#wardriving hops=5+` drop. Direct routes and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [scope=<name>] [require=region] [tx=slow] [suspend=tempradio]`, `del flood.filter.<n>|all`; repeater only: `get/set/del flood.filter.blacklist[.n]` | `set flood.filter grp_txt all scope=local tx=slow` |
|
||||
| `flood.rule` | Live alias for extended `flood.filter` on rule-engine repeaters and FULL ESP32 room servers. A row can AND packet type, hop range, authenticated group channel, ordered 1/2/3-byte pbyte source prefix, and original scope/region conditions, then drop, rewrite to a direct scope or configured region, enforce a per-row rate, and/or stop lower-priority FPF7 rules. All rows match the original receive-time packet; higher `priority` runs first and lower slot breaks a tie. Repeated rows with one channel key share a per-packet authentication result. Persistent FPF7 stores canonical region names, so region ID reorder or reuse cannot retarget a rule. A missing saved region name makes that match or rewrite inert until the name returns. Fixed 240 KB STM32WL profiles keep FPF6 and do not expose this alias; partition sizes are unchanged. | `get flood.rule[.n]`, `set flood.rule[.n] type=<type> [hops=...] [channel=...] [prefix=...] [in=...] <drop|scope=...|region=...|rate=N/min|stop> [priority=0-255]`, `del flood.rule.<n>|all` | `set flood.rule.2 type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86` |
|
||||
| `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` | Defaults on for nRF52 repeaters and off for other builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result. `clock.sync.mesh now` queues an immediate LoRa-only attempt without bypassing quorum or source suppression. 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. Status reports the reason a clock was not set; its `.table` and `.1` through `.16` forms inspect collected samples. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `clock.sync.mesh now`, `get clock.sync.status[.table|.1-.16]` | `set clock.sync.mesh on` |
|
||||
| `clock.sync.mesh.edge` | Defaults on so edge repeaters can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so `repeat off` and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples. | `get clock.sync.mesh.edge`, `set clock.sync.mesh.edge <on|off>` | `set clock.sync.mesh.edge on` |
|
||||
|
||||
+16
-1
@@ -59,7 +59,15 @@ Appdata Flags
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
|
||||
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. Current `CLI_DATA` commands do not cause acknowledgement responses, neither discrete nor extra; their text reply is the application-level result. Repeaters still ACK the legacy plain-text form before processing it.
|
||||
|
||||
Repeater remote CLI keeps one volatile copy of the most recently completed
|
||||
reply, keyed by the authenticated sender, request timestamp, and command text.
|
||||
Repeating that same logical request re-sends the text reply without executing
|
||||
the command again. A retry must therefore preserve the original timestamp and
|
||||
command text. The cache is cleared by reboot and replaced by the next completed
|
||||
remote command; commands that intentionally produce no text reply remain
|
||||
silent.
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------|--------------|------------------------------------------------------------|
|
||||
@@ -174,6 +182,13 @@ txt_type
|
||||
| `0x01` | CLI command | the command text of the message |
|
||||
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
|
||||
|
||||
For a room post, companion firmware uses its own monotonic clock for the
|
||||
on-air timestamp and preserves that timestamp across application retries. Room
|
||||
servers track post timestamps separately from login, request, and CLI traffic.
|
||||
They also remember recent accepted posts by sender, timestamp, and text: an
|
||||
exact retry is ACKed again without storing a duplicate, while stale or
|
||||
same-timestamp mismatches are rejected.
|
||||
|
||||
## Anonymous request
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|
||||
@@ -2021,9 +2021,23 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
|
||||
expected_ack = 0; // no Ack expected
|
||||
} else {
|
||||
const uint32_t app_timestamp = msg_timestamp;
|
||||
const bool is_room_post = recipient->type == ADV_TYPE_ROOM;
|
||||
if (is_room_post
|
||||
&& !room_message_timestamps.find(text_fingerprint, app_timestamp,
|
||||
&msg_timestamp)) {
|
||||
// Room login/control packets already use the node's monotonic clock.
|
||||
// Give a new post that same clock source, then preserve the mapping
|
||||
// so application retries keep one logical server-side timestamp.
|
||||
msg_timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
}
|
||||
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout,
|
||||
packet_retry_key,
|
||||
replacement_entry != NULL ? replacement_entry->retry_key : NULL);
|
||||
if (result != MSG_SEND_FAILED && is_room_post) {
|
||||
room_message_timestamps.remember(text_fingerprint, app_timestamp,
|
||||
msg_timestamp);
|
||||
}
|
||||
}
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/BaseSerialInterface.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/LogicalMessageCache.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <target.h>
|
||||
@@ -73,6 +74,10 @@
|
||||
#define OFFLINE_QUEUE_SIZE 16
|
||||
#endif
|
||||
|
||||
#ifndef ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE
|
||||
#define ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE 16
|
||||
#endif
|
||||
|
||||
#ifndef BLE_NAME_PREFIX
|
||||
#define BLE_NAME_PREFIX "MeshCore-"
|
||||
#endif
|
||||
@@ -324,6 +329,7 @@ private:
|
||||
};
|
||||
#define EXPECTED_ACK_TABLE_SIZE 8
|
||||
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
|
||||
mesh::LogicalMessageCache<ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE> room_message_timestamps;
|
||||
int next_ack_idx;
|
||||
unsigned long next_ack_expiry;
|
||||
bool has_next_ack_expiry;
|
||||
|
||||
+1810
-603
File diff suppressed because it is too large.
Load diff
@@ -73,6 +73,7 @@
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/DeferredCliCommand.h>
|
||||
#include <helpers/RemoteCliReplyCache.h>
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
#include <helpers/UserGpioReplyTracker.h>
|
||||
#endif
|
||||
@@ -138,8 +139,22 @@ struct NeighbourInfo {
|
||||
|
||||
#define MAX_SCHEDULED_RADIO_SETTINGS (MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE * 2)
|
||||
|
||||
#ifndef MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
// STM32WL repeater images have only 240 KB of application flash. They keep
|
||||
// the established dynamic flood.filter table unless a larger target profile
|
||||
// explicitly opts into the generalized rule parser and persistence format.
|
||||
#if defined(STM32_PLATFORM)
|
||||
#define MESH_ENABLE_FLOOD_RULE_ENGINE 0
|
||||
#else
|
||||
#define MESH_ENABLE_FLOOD_RULE_ENGINE 1
|
||||
#endif
|
||||
#endif
|
||||
#ifndef FLOOD_PACKET_FILTER_SLOTS
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 16
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 31
|
||||
#else
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 16
|
||||
#endif
|
||||
#endif
|
||||
#define FLOOD_PACKET_FILTER_ANY_TYPE 0xFF
|
||||
#define FLOOD_PACKET_FILTER_MAX_HOPS 63
|
||||
@@ -151,6 +166,9 @@ struct NeighbourInfo {
|
||||
#endif
|
||||
#define FLOOD_PACKET_FILTER_BLACKLIST_REPLACE_MAX 18
|
||||
#define FLOOD_PACKET_FILTER_PATH_ID_SIZE 3
|
||||
#define FLOOD_PACKET_FILTER_PATH_PREFIX_HOPS_MAX 3
|
||||
#define FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX \
|
||||
(FLOOD_PACKET_FILTER_PATH_PREFIX_HOPS_MAX * 3)
|
||||
|
||||
#ifndef FLOOD_CHANNEL_SCOPE_SLOTS
|
||||
#if defined(ESP32)
|
||||
@@ -164,6 +182,18 @@ struct NeighbourInfo {
|
||||
#define FLOOD_CHANNEL_SCOPE_TXT_ANY 0
|
||||
#define FLOOD_CHANNEL_SCOPE_LOGIN_ANY 1
|
||||
#define FLOOD_CHANNEL_SCOPE_OTHER_ANY 2
|
||||
#ifndef FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS
|
||||
// STM32WL repeater images and RAM are both exceptionally tight. Keep one
|
||||
// reusable regionless target there; other platforms scale with the rule
|
||||
// table up to the region-map capacity.
|
||||
#if defined(STM32_PLATFORM)
|
||||
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS 1
|
||||
#elif FLOOD_CHANNEL_SCOPE_SLOTS < MAX_REGION_ENTRIES
|
||||
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS FLOOD_CHANNEL_SCOPE_SLOTS
|
||||
#else
|
||||
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS MAX_REGION_ENTRIES
|
||||
#endif
|
||||
#endif
|
||||
#ifndef FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS
|
||||
#define FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS FLOOD_CHANNEL_SCOPE_SLOTS
|
||||
#endif
|
||||
@@ -234,6 +264,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
uint64_t uptime_millis;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
mesh::DeferredCliCommand deferred_cli_command;
|
||||
mesh::RemoteCliReplyCache remote_cli_reply_cache;
|
||||
TransportKey deferred_cli_reply_scope;
|
||||
bool deferred_cli_reply_scoped;
|
||||
uint32_t pending_self_advert_delay;
|
||||
@@ -259,7 +290,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
RegionMap region_map, temp_map;
|
||||
RegionEntry* load_stack[8];
|
||||
RegionEntry* recv_pkt_region;
|
||||
bool recv_pkt_filter_scope_set;
|
||||
bool recv_pkt_regionless_scope_set;
|
||||
bool recv_pkt_channel_scope_bypass;
|
||||
bool recv_pkt_channel_scope_rejected;
|
||||
TransportKey default_scope;
|
||||
@@ -277,14 +308,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
uint8_t prefix_len;
|
||||
uint32_t last_heard_millis;
|
||||
};
|
||||
struct FloodChannelBlockEntry {
|
||||
bool active;
|
||||
uint8_t key_len;
|
||||
uint8_t max_hops;
|
||||
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
};
|
||||
struct FloodPacketFilterEntry {
|
||||
bool active;
|
||||
uint8_t payload_type;
|
||||
@@ -293,12 +316,37 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool suspend_on_temp_radio;
|
||||
char scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
bool match_blacklisted_path;
|
||||
#if !MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
bool scope_requires_region_match;
|
||||
#endif
|
||||
bool scope_uses_slow_timing;
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
uint8_t incoming_scope_kind;
|
||||
char incoming_scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
uint8_t channel_key_len;
|
||||
uint8_t channel_hash;
|
||||
uint8_t channel_secret[PUB_KEY_SIZE];
|
||||
char channel_name[FLOOD_GROUP_MODERATION_NAME_LEN];
|
||||
uint8_t path_hash_size;
|
||||
uint8_t path_hops;
|
||||
uint8_t path[FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX];
|
||||
char target_region_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
bool drop_on_match;
|
||||
bool rate_limit_enabled;
|
||||
uint16_t rate_per_minute;
|
||||
uint8_t priority;
|
||||
bool stop_on_match;
|
||||
uint32_t rate_window_started;
|
||||
uint16_t rate_window_count;
|
||||
bool rate_window_active;
|
||||
#endif
|
||||
};
|
||||
struct FloodChannelScopeEntry {
|
||||
uint16_t region_id; // zero means unused
|
||||
// Low bits select txt:*/login:*/other:* or an exact channel-key length.
|
||||
// Zero means unused. Otherwise this is either a region ID or a one-based
|
||||
// flood_channel_direct_scopes index, as selected by selector.
|
||||
uint16_t target_id;
|
||||
// Encodes txt:*/login:*/other:* or an exact key length, target kind,
|
||||
// optional path selector, and fast/slow timing.
|
||||
uint8_t selector;
|
||||
uint8_t channel_hash;
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
@@ -311,7 +359,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
struct FloodGroupModerationEntry {
|
||||
bool active;
|
||||
uint8_t key_len;
|
||||
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
uint8_t hash_prefix[FLOOD_CHANNEL_KEY_PREFIX_LEN];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
char channel_name[FLOOD_GROUP_MODERATION_NAME_LEN];
|
||||
char sender[FLOOD_GROUP_MODERATION_NAME_LEN];
|
||||
@@ -334,15 +382,17 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
};
|
||||
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
|
||||
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
|
||||
FloodChannelBlockEntry flood_channel_blocks[FLOOD_CHANNEL_BLOCK_SLOTS];
|
||||
FloodPacketFilterEntry flood_packet_filters[FLOOD_PACKET_FILTER_SLOTS];
|
||||
uint8_t flood_packet_filter_blacklist_count;
|
||||
uint8_t flood_packet_filter_blacklist[FLOOD_PACKET_FILTER_BLACKLIST_MAX]
|
||||
[FLOOD_PACKET_FILTER_PATH_ID_SIZE];
|
||||
FloodChannelScopeEntry flood_channel_scopes[FLOOD_CHANNEL_SCOPE_SLOTS];
|
||||
char flood_channel_direct_scopes[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS]
|
||||
[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
FloodChannelScopeRequireEntry
|
||||
flood_channel_scope_requirements[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS];
|
||||
FloodGroupModerationEntry flood_group_moderation[FLOOD_GROUP_MODERATION_SLOTS];
|
||||
uint32_t recv_pkt_filter_match_mask;
|
||||
ClockSyncSample clock_sync_samples[CLOCK_SYNC_SAMPLE_SLOTS];
|
||||
bool clock_sync_mesh_enabled;
|
||||
bool clock_sync_mesh_edge_enabled;
|
||||
@@ -530,32 +580,40 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
void refreshScheduledRadioState();
|
||||
void processScheduledRadioSettings();
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
void loadFloodChannelBlocks();
|
||||
bool saveFloodChannelBlocks();
|
||||
void seedDefaultFloodChannelBlocks();
|
||||
void clearFloodChannelBlockEntry(FloodChannelBlockEntry& entry);
|
||||
void deriveFloodChannelBlockPrefix(const uint8_t* secret, uint8_t key_len,
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) const;
|
||||
uint8_t resolveFloodChannelBlockHops(uint8_t max_hops) const;
|
||||
bool floodChannelBlockHopApplies(const mesh::Packet* packet, uint8_t max_hops) const;
|
||||
bool floodChannelDataHopApplies(const mesh::Packet* packet) const;
|
||||
bool floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const;
|
||||
bool shouldBlockFloodChannelForward(const mesh::Packet* packet) const;
|
||||
void loadFloodPacketFilters();
|
||||
bool loadFloodPacketFilters();
|
||||
bool saveFloodPacketFilters();
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
void migrateLegacyFloodChannelBlocks();
|
||||
#endif
|
||||
void loadFloodPacketFilterBlacklist();
|
||||
bool saveFloodPacketFilterBlacklist();
|
||||
void seedDefaultFloodPacketFilters();
|
||||
bool floodPacketFilterBlacklistMatches(const mesh::Packet* packet) const;
|
||||
bool floodPacketFilterMatches(const FloodPacketFilterEntry& entry,
|
||||
const mesh::Packet* packet) const;
|
||||
bool applyFloodPacketFilterScope(mesh::Packet* packet, bool incoming_region_allowed,
|
||||
bool floodPacketFilterFieldsMatch(const FloodPacketFilterEntry& entry,
|
||||
const mesh::Packet* packet,
|
||||
bool incoming_is_scoped,
|
||||
uint16_t incoming_transport_code,
|
||||
bool incoming_region_allowed,
|
||||
const RegionEntry* incoming_region) const;
|
||||
bool authenticateFloodPacketFilterChannel(
|
||||
const FloodPacketFilterEntry& entry,
|
||||
const mesh::Packet* packet) const;
|
||||
int nextFloodPacketFilterMatch(uint32_t match_mask,
|
||||
uint32_t visited_mask) const;
|
||||
uint32_t applyFloodPacketFilterStop(uint32_t match_mask) const;
|
||||
uint32_t evaluateFloodPacketFilterMatches(
|
||||
const mesh::Packet* packet, bool incoming_region_allowed,
|
||||
const RegionEntry* incoming_region) const;
|
||||
bool applyFloodPacketFilterScope(mesh::Packet* packet, uint32_t match_mask,
|
||||
bool& scope_set, bool& fast_track,
|
||||
bool log_change = true);
|
||||
bool shouldBlockFloodPacketForward(const mesh::Packet* packet) const;
|
||||
void commitFloodPacketFilterRates(const mesh::Packet* packet);
|
||||
void formatFloodPacketFilters(const char* args, char* reply) const;
|
||||
void formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len) const;
|
||||
void setFloodPacketFilter(const char* args, char* reply);
|
||||
void setFloodPacketFilter(const char* args, char* reply,
|
||||
bool require_explicit_action = false);
|
||||
void deleteFloodPacketFilter(const char* args, char* reply);
|
||||
void formatFloodPacketFilterBlacklist(const char* args, char* reply) const;
|
||||
void setFloodPacketFilterBlacklist(const char* args, char* reply);
|
||||
@@ -564,8 +622,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool saveFloodChannelScopes(bool empty_table = false);
|
||||
bool applyFloodChannelScopeTarget(mesh::Packet* packet, const FloodChannelScopeEntry& entry,
|
||||
bool& scope_changed, bool& fast_track,
|
||||
bool& regionless_scope_set,
|
||||
bool log_change = true);
|
||||
bool applyFloodChannelScope(mesh::Packet* packet, bool& fast_track,
|
||||
bool& regionless_scope_set,
|
||||
bool log_change = true);
|
||||
static uint8_t scoreFloodTransportScope(const mesh::Packet* packet, void* context);
|
||||
uint8_t getFloodTransportScopeDepth(const mesh::Packet* packet);
|
||||
@@ -609,9 +669,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
void formatClockSyncStatus(const char* args, char* reply, size_t reply_len) const;
|
||||
void formatClockSyncTable(char* reply, size_t reply_len) const;
|
||||
void formatClockSyncSampleDetail(int index, char* reply, size_t reply_len) const;
|
||||
int findFloodChannelBlockBySelector(const char* selector) const;
|
||||
int findFloodChannelBlockSlot(const uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN], const char* name) const;
|
||||
void formatFloodChannelBlockDetail(char* reply, int idx) const;
|
||||
bool hasScheduledRadioWorkDue() const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_secs) const;
|
||||
@@ -791,11 +848,6 @@ public:
|
||||
void startRegionsLoad() override;
|
||||
bool saveRegions() override;
|
||||
void onDefaultRegionChanged(const RegionEntry* r) override;
|
||||
void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
|
||||
const char* name, uint8_t max_hops, char* reply) override;
|
||||
void formatFloodChannelBlocks(const char* selector, char* reply) override;
|
||||
void deleteFloodChannelBlock(const char* selector, char* reply) override;
|
||||
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
|
||||
class FloodRuleEngine {
|
||||
public:
|
||||
static constexpr uint8_t RULE_SLOTS = 31;
|
||||
static constexpr uint8_t ANY_TYPE = 0xFF;
|
||||
static constexpr uint8_t MAX_HOPS = 63;
|
||||
static constexpr uint8_t NAME_LEN = 32;
|
||||
static constexpr uint8_t PATH_PREFIX_HOPS_MAX = 3;
|
||||
static constexpr uint8_t PATH_PREFIX_BYTES_MAX = 9;
|
||||
static constexpr uint16_t RATE_UNLIMITED = 0xFFFF;
|
||||
|
||||
FloodRuleEngine();
|
||||
|
||||
void begin(FILESYSTEM* fs, RegionMap* regions);
|
||||
|
||||
uint32_t evaluate(const mesh::Packet* packet, bool temp_radio_active,
|
||||
bool incoming_region_allowed,
|
||||
const RegionEntry* incoming_region) const;
|
||||
bool applyScope(mesh::Packet* packet, uint32_t match_mask,
|
||||
bool& scope_set, bool& fast_track,
|
||||
bool log_change = true);
|
||||
bool shouldBlock(const mesh::Packet* packet, uint32_t match_mask,
|
||||
uint32_t now_millis) const;
|
||||
void commitRates(const mesh::Packet* packet, uint32_t match_mask,
|
||||
uint32_t now_millis);
|
||||
|
||||
bool handleCommand(const char* command, char* reply);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
bool active;
|
||||
uint8_t payload_type;
|
||||
uint8_t min_hops;
|
||||
uint8_t max_hops;
|
||||
bool suspend_on_temp_radio;
|
||||
char scope_name[NAME_LEN];
|
||||
bool match_blacklisted_path;
|
||||
bool scope_uses_slow_timing;
|
||||
uint8_t incoming_scope_kind;
|
||||
char incoming_scope_name[NAME_LEN];
|
||||
uint8_t channel_key_len;
|
||||
uint8_t channel_hash;
|
||||
uint8_t channel_secret[PUB_KEY_SIZE];
|
||||
char channel_name[NAME_LEN];
|
||||
uint8_t path_hash_size;
|
||||
uint8_t path_hops;
|
||||
uint8_t path[PATH_PREFIX_BYTES_MAX];
|
||||
char target_region_name[NAME_LEN];
|
||||
bool drop_on_match;
|
||||
bool rate_limit_enabled;
|
||||
uint16_t rate_per_minute;
|
||||
uint8_t priority;
|
||||
bool stop_on_match;
|
||||
uint32_t rate_window_started;
|
||||
uint16_t rate_window_count;
|
||||
bool rate_window_active;
|
||||
};
|
||||
|
||||
FILESYSTEM* _fs;
|
||||
RegionMap* _regions;
|
||||
Entry _entries[RULE_SLOTS];
|
||||
|
||||
void seedDefaults();
|
||||
void load();
|
||||
bool save();
|
||||
bool fieldsMatch(const Entry& entry, const mesh::Packet* packet,
|
||||
bool temp_radio_active, bool incoming_is_scoped,
|
||||
uint16_t incoming_transport_code,
|
||||
bool incoming_region_allowed,
|
||||
const RegionEntry* incoming_region) const;
|
||||
bool authenticateChannel(const Entry& entry,
|
||||
const mesh::Packet* packet) const;
|
||||
int nextMatch(uint32_t match_mask, uint32_t visited_mask) const;
|
||||
uint32_t applyStop(uint32_t match_mask) const;
|
||||
void format(const char* args, char* reply) const;
|
||||
void formatDetail(int index, char* reply, size_t reply_len) const;
|
||||
void set(const char* args, char* reply,
|
||||
bool require_explicit_action = false);
|
||||
void remove(const char* args, char* reply);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
#include <helpers/FloodFilterPolicy.h>
|
||||
#endif
|
||||
#include <helpers/radiolib/RXPowerSaving.h>
|
||||
#include <algorithm>
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
@@ -317,6 +320,69 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
|
||||
}
|
||||
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
bool MyMesh::evaluateFloodRuleTiming(const mesh::Packet* packet,
|
||||
bool& fast_track) {
|
||||
fast_track = false;
|
||||
if (packet == NULL || !packet->isRouteFlood()) return false;
|
||||
|
||||
bool incoming_region_allowed = false;
|
||||
RegionEntry* incoming_region = NULL;
|
||||
if (packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
incoming_region = region_map.findMatch(packet, REGION_DENY_FLOOD);
|
||||
incoming_region_allowed = incoming_region != NULL;
|
||||
} else if (packet->getRouteType() == ROUTE_TYPE_FLOOD) {
|
||||
incoming_region_allowed =
|
||||
(region_map.getWildcard().flags & REGION_DENY_FLOOD) == 0;
|
||||
}
|
||||
|
||||
mesh::Packet candidate = *packet;
|
||||
uint32_t match_mask = flood_rules.evaluate(
|
||||
packet, isTempRadioActive(), incoming_region_allowed, incoming_region);
|
||||
bool scope_set = false;
|
||||
return flood_rules.applyScope(&candidate, match_mask, scope_set,
|
||||
fast_track, false);
|
||||
}
|
||||
|
||||
static const uint8_t ROOM_MAX_LOOP_MINIMAL[] = { 0, 4, 2, 1 };
|
||||
static const uint8_t ROOM_MAX_LOOP_MODERATE[] = { 0, 2, 1, 1 };
|
||||
static const uint8_t ROOM_MAX_LOOP_STRICT[] = { 0, 1, 1, 1 };
|
||||
|
||||
bool MyMesh::isLooped(const mesh::Packet* packet,
|
||||
const uint8_t max_counters[]) const {
|
||||
uint8_t hash_size = packet->getPathHashSize();
|
||||
uint8_t hash_count = packet->getPathHashCount();
|
||||
uint8_t occurrences = 0;
|
||||
const uint8_t* path = packet->path;
|
||||
while (hash_count > 0) {
|
||||
if (self_id.isHashMatch(path, hash_size)) occurrences++;
|
||||
hash_count--;
|
||||
path += hash_size;
|
||||
}
|
||||
return occurrences >= max_counters[hash_size];
|
||||
}
|
||||
|
||||
int MyMesh::calcRxDelayForPacket(const mesh::Packet* packet, float score,
|
||||
uint32_t air_time) {
|
||||
bool fast_track = false;
|
||||
if (!evaluateFloodRuleTiming(packet, fast_track)) {
|
||||
return calcRxDelay(score, air_time);
|
||||
}
|
||||
if (fast_track) return 0;
|
||||
float slow_base =
|
||||
FloodFilterPolicy::slowScopeRxDelayBase(_prefs.rx_delay_base);
|
||||
return (int)((powf(slow_base, 0.85f - score) - 1.0f) * air_time);
|
||||
}
|
||||
|
||||
uint32_t MyMesh::getSlowFloodRuleRetransmitDelay(
|
||||
const mesh::Packet* packet) {
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_delay = FloodFilterPolicy::slowScopeMaxDelay(airtime);
|
||||
return getRNG()->nextInt(0, max_delay + 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *MyMesh::getLogDateTime() {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
@@ -409,11 +475,62 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
|
||||
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
if (flood_rules.shouldBlock(packet, recv_pkt_rule_match_mask,
|
||||
_ms->getMillis())) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
if (packet->isRouteFlood()) {
|
||||
if (recv_pkt_region == NULL && !recv_pkt_regionless_scope_set) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
|
||||
return false;
|
||||
}
|
||||
if (_prefs.loop_detect != LOOP_DETECT_OFF) {
|
||||
const uint8_t* maximums = _prefs.loop_detect == LOOP_DETECT_MINIMAL
|
||||
? ROOM_MAX_LOOP_MINIMAL
|
||||
: (_prefs.loop_detect == LOOP_DETECT_MODERATE
|
||||
? ROOM_MAX_LOOP_MODERATE : ROOM_MAX_LOOP_STRICT);
|
||||
if (isLooped(packet, maximums)) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"allowPacketForward: FLOOD packet loop detected!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
flood_rules.commitRates(packet, recv_pkt_rule_match_mask,
|
||||
_ms->getMillis());
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) {
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
bool scope_changed = false;
|
||||
bool fast_track_scope_change = false;
|
||||
recv_pkt_regionless_scope_set = false;
|
||||
recv_pkt_rule_match_mask = 0;
|
||||
if (pkt->isRouteFlood()) {
|
||||
bool incoming_region_allowed = false;
|
||||
RegionEntry* incoming_region = NULL;
|
||||
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
incoming_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
||||
incoming_region_allowed = incoming_region != NULL;
|
||||
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
|
||||
incoming_region_allowed =
|
||||
(region_map.getWildcard().flags & REGION_DENY_FLOOD) == 0;
|
||||
}
|
||||
recv_pkt_rule_match_mask = flood_rules.evaluate(
|
||||
pkt, isTempRadioActive(), incoming_region_allowed, incoming_region);
|
||||
bool scope_set = false;
|
||||
scope_changed = flood_rules.applyScope(
|
||||
pkt, recv_pkt_rule_match_mask, scope_set, fast_track_scope_change);
|
||||
recv_pkt_regionless_scope_set = scope_set;
|
||||
}
|
||||
#endif
|
||||
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
||||
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
|
||||
@@ -425,7 +542,20 @@ mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) {
|
||||
} else {
|
||||
recv_pkt_region = NULL;
|
||||
}
|
||||
return Mesh::onRecvPacket(pkt);
|
||||
mesh::DispatcherAction action = Mesh::onRecvPacket(pkt);
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
if (scope_changed && action != ACTION_RELEASE
|
||||
&& action != ACTION_MANUAL_HOLD) {
|
||||
if (fast_track_scope_change) {
|
||||
action = ACTION_RETRANSMIT(0);
|
||||
} else {
|
||||
uint8_t priority = (action >> 24) - 1;
|
||||
action = ACTION_RETRANSMIT_DELAYED(
|
||||
priority, getSlowFloodRuleRetransmitDelay(pkt));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return action;
|
||||
}
|
||||
|
||||
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
|
||||
@@ -602,96 +732,118 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
client->last_timestamp = sender_timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
client->last_activity = now;
|
||||
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
const char* text = (const char*)&data[5];
|
||||
const size_t text_len = strlen(text);
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
uint8_t temp[166];
|
||||
temp[5] = 0;
|
||||
bool send_ack = false;
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to
|
||||
// sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key,
|
||||
PUB_KEY_SIZE);
|
||||
|
||||
uint8_t temp[166];
|
||||
bool send_ack;
|
||||
if (flags == TXT_TYPE_CLI_DATA) {
|
||||
if (client->isAdmin()) {
|
||||
if (is_retry) {
|
||||
temp[5] = 0; // no reply
|
||||
} else {
|
||||
handleCommand(sender_timestamp, (char *)&data[5], (char *)&temp[5],
|
||||
i, packet->getPathHashSize());
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
|
||||
}
|
||||
send_ack = false;
|
||||
} else {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // and no ACK... user shoudn't be sending these
|
||||
}
|
||||
} else { // TXT_TYPE_PLAIN
|
||||
if ((client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // no ACK
|
||||
} else {
|
||||
if (!is_retry) {
|
||||
addPost(client, (const char *)&data[5]);
|
||||
}
|
||||
temp[5] = 0; // no reply (ACK is enough)
|
||||
send_ack = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t delay_millis;
|
||||
if (send_ack) {
|
||||
if (client->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
mesh::Packet *ack = createAck(ack_hash);
|
||||
if (ack) sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize());
|
||||
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet *a1 = createMultiAck(ack_hash, 1);
|
||||
if (a1) sendDirect(a1, client->out_path, client->out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet *a2 = createAck(ack_hash);
|
||||
if (a2) sendDirect(a2, client->out_path, client->out_path_len, d);
|
||||
delay_millis = d + REPLY_DELAY_MILLIS;
|
||||
if (flags == TXT_TYPE_PLAIN) {
|
||||
const bool is_guest =
|
||||
(client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST;
|
||||
if (is_guest) {
|
||||
if (sender_timestamp < client->extra.room.last_post_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: stale room post detected");
|
||||
return;
|
||||
}
|
||||
client->extra.room.last_post_timestamp = sender_timestamp;
|
||||
} else {
|
||||
delay_millis = 0;
|
||||
uint8_t message_fingerprint[MAX_HASH_SIZE];
|
||||
mesh::Utils::sha256(message_fingerprint, sizeof(message_fingerprint),
|
||||
client->id.pub_key, PUB_KEY_SIZE,
|
||||
(const uint8_t*)text, text_len);
|
||||
const auto replay_decision = recent_room_posts.classifyAndRemember(
|
||||
message_fingerprint, sender_timestamp,
|
||||
client->extra.room.last_post_timestamp);
|
||||
|
||||
using ReplayDecision =
|
||||
mesh::LogicalMessageCache<ROOM_MESSAGE_CACHE_SIZE>::ReplayDecision;
|
||||
if (replay_decision == ReplayDecision::StaleOrMismatched) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: stale or mismatched room post detected");
|
||||
return;
|
||||
}
|
||||
if (replay_decision == ReplayDecision::NewMessage) {
|
||||
addPost(client, text);
|
||||
}
|
||||
// Exact retries are ACKed again regardless of whether newer posts have
|
||||
// advanced this client's room-post replay timestamp.
|
||||
send_ack = true;
|
||||
}
|
||||
} else { // TXT_TYPE_CLI_DATA
|
||||
if (sender_timestamp < client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
return;
|
||||
}
|
||||
|
||||
int text_len = strlen((char *)&temp[5]);
|
||||
if (text_len > 0) {
|
||||
if (now == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
now++;
|
||||
}
|
||||
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
|
||||
const bool is_retry = sender_timestamp == client->last_timestamp;
|
||||
client->last_timestamp = sender_timestamp;
|
||||
if (client->isAdmin() && !is_retry) {
|
||||
handleCommand(sender_timestamp, (char*)text, (char*)&temp[5],
|
||||
i, packet->getPathHashSize());
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
|
||||
}
|
||||
// CLI_DATA replies are the result signal; no separate ACK is expected.
|
||||
}
|
||||
|
||||
// calc expected ACK reply
|
||||
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
|
||||
// PUB_KEY_SIZE);
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
client->last_activity = now;
|
||||
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
uint32_t delay_millis;
|
||||
if (send_ack) {
|
||||
uint32_t ack_hash; // prove receipt of this timestamp, attempt, text, and sender
|
||||
mesh::Utils::sha256((uint8_t*)&ack_hash, 4, data, 5 + text_len,
|
||||
client->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
if (client->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
mesh::Packet *ack = createAck(ack_hash);
|
||||
if (ack) sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize());
|
||||
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet *a1 = createMultiAck(ack_hash, 1);
|
||||
if (a1) sendDirect(a1, client->out_path, client->out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet *a2 = createAck(ack_hash);
|
||||
if (a2) sendDirect(a2, client->out_path, client->out_path_len, d);
|
||||
delay_millis = d + REPLY_DELAY_MILLIS;
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
delay_millis = 0;
|
||||
}
|
||||
|
||||
int reply_text_len = strlen((char*)&temp[5]);
|
||||
if (reply_text_len > 0) {
|
||||
if (now == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
now++;
|
||||
}
|
||||
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
|
||||
|
||||
// calc expected ACK reply
|
||||
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + reply_text_len, self_id.pub_key,
|
||||
// PUB_KEY_SIZE);
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret,
|
||||
temp, 5 + reply_text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY,
|
||||
packet->getPathHashSize());
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len,
|
||||
delay_millis + SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_REQ && len >= 5) {
|
||||
uint32_t sender_timestamp;
|
||||
@@ -909,6 +1061,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
radio_apply_retry_at = 0;
|
||||
radio_apply_failures = 0;
|
||||
recv_pkt_region = NULL;
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
recv_pkt_rule_match_mask = 0;
|
||||
recv_pkt_regionless_scope_set = false;
|
||||
#endif
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
@@ -998,6 +1154,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
|
||||
acl.load(_fs, self_id);
|
||||
region_map.load(_fs);
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
flood_rules.begin(_fs, ®ion_map);
|
||||
#endif
|
||||
|
||||
// establish default-scope
|
||||
{
|
||||
@@ -1818,7 +1977,13 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
addSystemPost(msg);
|
||||
snprintf(reply, MAX_POST_TEXT_LEN, "OK");
|
||||
}
|
||||
} else{
|
||||
}
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
else if (flood_rules.handleCommand(command, reply)) {
|
||||
// handled by the FULL-profile persistent flood rule engine
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#endif
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/LogicalMessageCache.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include "FloodRuleEngine.h"
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
@@ -87,6 +89,10 @@
|
||||
#define MAX_UNSYNCED_POSTS 32
|
||||
#endif
|
||||
|
||||
#ifndef ROOM_MESSAGE_CACHE_SIZE
|
||||
#define ROOM_MESSAGE_CACHE_SIZE 32
|
||||
#endif
|
||||
|
||||
#ifndef SERVER_RESPONSE_DELAY
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
#endif
|
||||
@@ -128,7 +134,13 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
NodePrefs _prefs;
|
||||
TransportKeyStore key_store;
|
||||
RegionMap region_map, temp_map;
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
FloodRuleEngine flood_rules;
|
||||
uint32_t recv_pkt_rule_match_mask;
|
||||
bool recv_pkt_regionless_scope_set;
|
||||
#endif
|
||||
ClientACL acl;
|
||||
mesh::LogicalMessageCache<ROOM_MESSAGE_CACHE_SIZE> recent_room_posts;
|
||||
CommonCLI _cli;
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
UserGpioReplyTracker _gpio_reply_tracker;
|
||||
@@ -218,6 +230,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
mesh::Packet* createSelfAdvert();
|
||||
File openAppend(const char* fname);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
bool evaluateFloodRuleTiming(const mesh::Packet* packet,
|
||||
bool& fast_track);
|
||||
bool isLooped(const mesh::Packet* packet,
|
||||
const uint8_t max_counters[]) const;
|
||||
uint32_t getSlowFloodRuleRetransmitDelay(
|
||||
const mesh::Packet* packet);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool isTempRadioActive() const override {
|
||||
@@ -233,6 +253,10 @@ protected:
|
||||
void logTxFail(mesh::Packet* pkt, int len) override;
|
||||
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
||||
int calcRxDelayForPacket(const mesh::Packet* packet, float score,
|
||||
uint32_t air_time) override;
|
||||
#endif
|
||||
const char* getLogDateTime() override;
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
||||
|
||||
@@ -28,6 +28,7 @@ struct ClientInfo {
|
||||
union {
|
||||
struct {
|
||||
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
|
||||
uint32_t last_post_timestamp; // sender timestamp for room posts only (transient)
|
||||
uint32_t pending_ack;
|
||||
uint32_t push_post_timestamp;
|
||||
unsigned long ack_timeout;
|
||||
|
||||
+21
-255
@@ -497,92 +497,20 @@ static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockHops(const char* value, uint8_t& max_hops) {
|
||||
static bool parseFloodChannelHops(const char* value, uint8_t& max_hops) {
|
||||
if (value == NULL) {
|
||||
return false;
|
||||
}
|
||||
value = skipSpacesConst(value);
|
||||
if (strcmp(value, "all") == 0) {
|
||||
max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
return true;
|
||||
}
|
||||
return parseUint8Strict(value, 1, 7, max_hops);
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockRowHops(const char* value, uint8_t& max_hops) {
|
||||
if (value == NULL) {
|
||||
return false;
|
||||
}
|
||||
value = skipSpacesConst(value);
|
||||
if (strcmp(value, "default") == 0 || strcmp(value, "def") == 0 || strcmp(value, "inherit") == 0) {
|
||||
max_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT;
|
||||
return true;
|
||||
}
|
||||
return parseFloodChannelBlockHops(value, max_hops);
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockHopAssignment(const char* text, bool allow_bare, uint8_t& max_hops) {
|
||||
char token[16];
|
||||
text = skipSpacesConst(text);
|
||||
if (text == NULL || *text == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
while (text[len] && text[len] != ' ' && len + 1 < sizeof(token)) {
|
||||
token[len] = text[len];
|
||||
len++;
|
||||
}
|
||||
token[len] = 0;
|
||||
|
||||
const char* value = NULL;
|
||||
if (strncmp(token, "h=", 2) == 0) {
|
||||
value = token + 2;
|
||||
} else if (strncmp(token, "hops=", 5) == 0) {
|
||||
value = token + 5;
|
||||
} else if (allow_bare) {
|
||||
value = token;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return parseFloodChannelBlockRowHops(value, max_hops);
|
||||
}
|
||||
|
||||
static bool looksFloodChannelBlockHopAssignment(const char* text) {
|
||||
text = skipSpacesConst(text);
|
||||
if (text == NULL || *text == 0) {
|
||||
return false;
|
||||
}
|
||||
return (*text >= '0' && *text <= '9')
|
||||
|| strncmp(text, "h=", 2) == 0
|
||||
|| strncmp(text, "hops=", 5) == 0
|
||||
|| strncmp(text, "all", 3) == 0
|
||||
|| strncmp(text, "def", 3) == 0
|
||||
|| strncmp(text, "default", 7) == 0
|
||||
|| strncmp(text, "inherit", 7) == 0;
|
||||
}
|
||||
|
||||
static bool trimFloodChannelBlockHopSuffix(char* name, uint8_t& max_hops) {
|
||||
size_t len = strlen(name);
|
||||
while (len > 0 && name[len - 1] == ' ') {
|
||||
name[--len] = 0;
|
||||
}
|
||||
char* token = strrchr(name, ' ');
|
||||
if (token == NULL) {
|
||||
return true;
|
||||
}
|
||||
if (strncmp(token + 1, "h=", 2) != 0 && strncmp(token + 1, "hops=", 5) != 0) {
|
||||
return true;
|
||||
}
|
||||
if (!parseFloodChannelBlockHopAssignment(token + 1, false, max_hops)) {
|
||||
return false;
|
||||
}
|
||||
*token = 0;
|
||||
return strlen(name) > 0;
|
||||
}
|
||||
|
||||
static void formatFloodChannelBlockHops(char* dest, uint8_t max_hops) {
|
||||
if (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL) {
|
||||
static void formatFloodChannelHops(char* dest, uint8_t max_hops) {
|
||||
if (max_hops == FLOOD_CHANNEL_HOPS_ALL) {
|
||||
strcpy(dest, "h=all");
|
||||
} else {
|
||||
sprintf(dest, "h>%u", (unsigned int)max_hops);
|
||||
@@ -699,69 +627,6 @@ static bool formatFloodRetryBucketCollisionWarning(char* reply, const NodePrefs*
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockKey(const char* text, uint8_t secret[PUB_KEY_SIZE], uint8_t& key_len) {
|
||||
if (text == NULL || text[0] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(secret, 0, PUB_KEY_SIZE);
|
||||
if (text[0] == '#') {
|
||||
if (!isValidName(text)) {
|
||||
return false;
|
||||
}
|
||||
mesh::Utils::sha256(secret, CIPHER_KEY_SIZE, (const uint8_t*)text, strlen(text));
|
||||
key_len = CIPHER_KEY_SIZE;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t hex_len = strlen(text);
|
||||
if (!(hex_len == CIPHER_KEY_SIZE * 2 || hex_len == PUB_KEY_SIZE * 2)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < hex_len; i++) {
|
||||
if (!mesh::Utils::isHexChar(text[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
key_len = (uint8_t)(hex_len / 2);
|
||||
return mesh::Utils::fromHex(secret, key_len, text);
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockDotIndex(const char*& cursor, int& index) {
|
||||
if (*cursor != '.') {
|
||||
index = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor++;
|
||||
if (*cursor < '0' || *cursor > '9') {
|
||||
return false;
|
||||
}
|
||||
int value = 0;
|
||||
while (*cursor >= '0' && *cursor <= '9') {
|
||||
value = (value * 10) + (*cursor - '0');
|
||||
if (value > FLOOD_CHANNEL_BLOCK_SLOTS) {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
if (value < 1) {
|
||||
return false;
|
||||
}
|
||||
index = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void copyTrimmedFloodChannelBlockName(char* dest, size_t dest_len, const char* src) {
|
||||
src = skipSpacesConst(src);
|
||||
StrHelper::strncpy(dest, src, dest_len);
|
||||
size_t len = strlen(dest);
|
||||
while (len > 0 && dest[len - 1] == ' ') {
|
||||
dest[--len] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
prefs->retry_preset = preset;
|
||||
if (preset == RETRY_PRESET_INFRA) {
|
||||
@@ -1208,8 +1073,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
memset(_prefs->battery_alert_region, 0, sizeof(_prefs->battery_alert_region));
|
||||
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
||||
_prefs->flood_channel_data_enabled = 1;
|
||||
_prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
_prefs->legacy_flood_channel_block_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
has_flood_retry_prefs = file.available() >= 2;
|
||||
if (has_flood_retry_prefs) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
||||
@@ -1244,8 +1109,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_channel_block_max_hops)) {
|
||||
file.read((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops));
|
||||
if (file.available() >= (int)sizeof(_prefs->legacy_flood_channel_block_max_hops)) {
|
||||
file.read((uint8_t *)&_prefs->legacy_flood_channel_block_max_hops,
|
||||
sizeof(_prefs->legacy_flood_channel_block_max_hops));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_max_hops)) {
|
||||
file.read((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
|
||||
@@ -1431,13 +1297,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
|
||||
_prefs->flood_channel_data_enabled = constrain(_prefs->flood_channel_data_enabled, 0, 1);
|
||||
_prefs->telemetry_access = constrain(_prefs->telemetry_access, 0, 1);
|
||||
if (_prefs->flood_channel_block_max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL
|
||||
&& (_prefs->flood_channel_block_max_hops < 1 || _prefs->flood_channel_block_max_hops > 7)) {
|
||||
_prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
if (_prefs->legacy_flood_channel_block_max_hops != FLOOD_CHANNEL_HOPS_ALL
|
||||
&& (_prefs->legacy_flood_channel_block_max_hops < 1
|
||||
|| _prefs->legacy_flood_channel_block_max_hops > 7)) {
|
||||
_prefs->legacy_flood_channel_block_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
}
|
||||
if (_prefs->flood_channel_data_max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL
|
||||
if (_prefs->flood_channel_data_max_hops != FLOOD_CHANNEL_HOPS_ALL
|
||||
&& (_prefs->flood_channel_data_max_hops < 1 || _prefs->flood_channel_data_max_hops > 7)) {
|
||||
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
|
||||
_prefs->flood_channel_data_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
}
|
||||
if (_prefs->battery_alert_low_percent < 1
|
||||
|| _prefs->battery_alert_low_percent > 100
|
||||
@@ -1564,7 +1431,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->battery_alert_critical_percent);
|
||||
WRITE_COMMON_PREFS(&prefs->direct_retry_recent_enabled);
|
||||
WRITE_COMMON_PREFS(&prefs->flood_channel_data_enabled);
|
||||
WRITE_COMMON_PREFS(&prefs->flood_channel_block_max_hops);
|
||||
WRITE_COMMON_PREFS(&prefs->legacy_flood_channel_block_max_hops);
|
||||
WRITE_COMMON_PREFS(&prefs->flood_channel_data_max_hops);
|
||||
WRITE_COMMON_PREFS(&prefs->telemetry_access); // 674
|
||||
#if defined(ENABLE_OTA)
|
||||
@@ -1708,7 +1575,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
||||
file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
||||
file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
||||
file.write((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops));
|
||||
file.write((uint8_t *)&_prefs->legacy_flood_channel_block_max_hops,
|
||||
sizeof(_prefs->legacy_flood_channel_block_max_hops));
|
||||
file.write((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
|
||||
file.write((uint8_t *)&_prefs->telemetry_access, sizeof(_prefs->telemetry_access)); // 674
|
||||
#if defined(ENABLE_OTA)
|
||||
@@ -3945,7 +3813,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
} else if (memcmp(config, "flood.channel.data.hops ", 24) == 0) {
|
||||
uint8_t max_hops;
|
||||
if (parseFloodChannelBlockHops(&config[24], max_hops)) {
|
||||
if (parseFloodChannelHops(&config[24], max_hops)) {
|
||||
_prefs->flood_channel_data_max_hops = max_hops;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
@@ -3964,62 +3832,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "flood.channel.block.hops ", 25) == 0) {
|
||||
uint8_t max_hops;
|
||||
if (parseFloodChannelBlockHops(&config[25], max_hops)) {
|
||||
_prefs->flood_channel_block_max_hops = max_hops;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be all or 1-7");
|
||||
}
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
|
||||
const char* key_start = cursor;
|
||||
while (*cursor && *cursor != ' ') cursor++;
|
||||
size_t key_len_text = cursor - key_start;
|
||||
char key_text[PUB_KEY_SIZE * 2 + 1];
|
||||
if (key_len_text == 0 || key_len_text >= sizeof(key_text)) {
|
||||
strcpy(reply, "Error, use: set flood.channel.block[.n] <hex-key> <name>|#channel");
|
||||
return;
|
||||
}
|
||||
memcpy(key_text, key_start, key_len_text);
|
||||
key_text[key_len_text] = 0;
|
||||
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
uint8_t block_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT;
|
||||
if (key_text[0] == '#') {
|
||||
StrHelper::strncpy(name, key_text, sizeof(name));
|
||||
const char* extra = skipSpacesConst(cursor);
|
||||
if (*extra && looksFloodChannelBlockHopAssignment(extra)
|
||||
&& !parseFloodChannelBlockHopAssignment(extra, true, block_hops)) {
|
||||
strcpy(reply, "Error, hops must be all, default, or 1-7");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
copyTrimmedFloodChannelBlockName(name, sizeof(name), cursor);
|
||||
if (!trimFloodChannelBlockHopSuffix(name, block_hops)) {
|
||||
strcpy(reply, "Error, bad name or hops");
|
||||
return;
|
||||
}
|
||||
}
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
uint8_t decoded_key_len = 0;
|
||||
if (!parseFloodChannelBlockKey(key_text, secret, decoded_key_len)) {
|
||||
strcpy(reply, "Error, key must be 128/256-bit hex or #channel");
|
||||
} else if (name[0] == 0 || !isValidName(name)) {
|
||||
strcpy(reply, "Error, bad name");
|
||||
} else {
|
||||
_callbacks->setFloodChannelBlock(index, secret, decoded_key_len, name, block_hops, reply);
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.count ", 18) == 0) {
|
||||
int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
||||
if (attempts >= 0 && attempts <= 15) {
|
||||
@@ -4570,36 +4382,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
||||
} else if (strcmp(config, "flood.channel.data.hops") == 0) {
|
||||
char hops[8];
|
||||
formatFloodChannelBlockHops(hops, _prefs->flood_channel_data_max_hops);
|
||||
formatFloodChannelHops(hops, _prefs->flood_channel_data_max_hops);
|
||||
sprintf(reply, "> %s", hops);
|
||||
} else if (strcmp(config, "flood.channel.data") == 0) {
|
||||
char hops[8];
|
||||
formatFloodChannelBlockHops(hops, _prefs->flood_channel_data_max_hops);
|
||||
formatFloodChannelHops(hops, _prefs->flood_channel_data_max_hops);
|
||||
sprintf(reply, "> %s %s", _prefs->flood_channel_data_enabled ? "on" : "off", hops);
|
||||
} else if (memcmp(config, "flood.channel.block.hops", 24) == 0) {
|
||||
char hops[8];
|
||||
formatFloodChannelBlockHops(hops, _prefs->flood_channel_block_max_hops);
|
||||
sprintf(reply, "> %s", hops);
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == 0 || config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
char selector[8];
|
||||
if (index > 0 && *cursor != 0) {
|
||||
strcpy(reply, "Error, use index or selector");
|
||||
return;
|
||||
}
|
||||
if (index > 0) {
|
||||
snprintf(selector, sizeof(selector), "%d", index);
|
||||
_callbacks->formatFloodChannelBlocks(selector, reply);
|
||||
} else {
|
||||
_callbacks->formatFloodChannelBlocks(cursor, reply);
|
||||
}
|
||||
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
||||
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
||||
@@ -4799,28 +4587,6 @@ void CommonCLI::handleDelCmd(char* command, char* reply) {
|
||||
_callbacks->deleteScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
||||
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
||||
_callbacks->deleteScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
char selector[8];
|
||||
if (index > 0 && *cursor != 0) {
|
||||
strcpy(reply, "Error, use index or selector");
|
||||
return;
|
||||
}
|
||||
if (index > 0) {
|
||||
snprintf(selector, sizeof(selector), "%d", index);
|
||||
_callbacks->deleteFloodChannelBlock(selector, reply);
|
||||
} else if (*cursor != 0) {
|
||||
_callbacks->deleteFloodChannelBlock(cursor, reply);
|
||||
} else {
|
||||
strcpy(reply, "Error, use: del flood.channel.block <index|name|prefix>");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "unknown del: ");
|
||||
StrHelper::strncpy(&reply[13], config, 160 - 14);
|
||||
|
||||
+6
-29
@@ -90,17 +90,10 @@
|
||||
#define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4))
|
||||
#endif
|
||||
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_SLOTS
|
||||
#define FLOOD_CHANNEL_BLOCK_SLOTS 15
|
||||
#ifndef FLOOD_CHANNEL_KEY_PREFIX_LEN
|
||||
#define FLOOD_CHANNEL_KEY_PREFIX_LEN 4
|
||||
#endif
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_NAME_LEN
|
||||
#define FLOOD_CHANNEL_BLOCK_NAME_LEN 32
|
||||
#endif
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_PREFIX_LEN
|
||||
#define FLOOD_CHANNEL_BLOCK_PREFIX_LEN 4
|
||||
#endif
|
||||
#define FLOOD_CHANNEL_BLOCK_HOPS_ALL 0xFF
|
||||
#define FLOOD_CHANNEL_BLOCK_HOPS_INHERIT 0xFE
|
||||
#define FLOOD_CHANNEL_HOPS_ALL 0xFF
|
||||
|
||||
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
|
||||
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
|
||||
@@ -191,7 +184,9 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t battery_alert_critical_percent;
|
||||
uint8_t direct_retry_recent_enabled;
|
||||
uint8_t flood_channel_data_enabled;
|
||||
uint8_t flood_channel_block_max_hops;
|
||||
// Retains the removed flood.channel.block byte at its established file
|
||||
// offset so every following preference remains upgrade-compatible.
|
||||
uint8_t legacy_flood_channel_block_max_hops;
|
||||
uint8_t flood_channel_data_max_hops;
|
||||
uint8_t telemetry_access;
|
||||
|
||||
@@ -334,24 +329,6 @@ public:
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
|
||||
const char* name, uint8_t max_hops, char* reply) {
|
||||
(void)index;
|
||||
(void)secret;
|
||||
(void)key_len;
|
||||
(void)name;
|
||||
(void)max_hops;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void formatFloodChannelBlocks(const char* selector, char* reply) {
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void deleteFloodChannelBlock(const char* selector, char* reply) {
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
|
||||
virtual void startRegionsLoad() {
|
||||
// no op by default
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
inline bool isCompanionPushFrame(const uint8_t* frame, size_t len) {
|
||||
return frame != NULL && len > 0 && (frame[0] & 0x80) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a frame to a companion transport's contiguous outbound queue.
|
||||
*
|
||||
* Protocol responses use codes below 0x80 and complete an app command that is
|
||||
* waiting for them. Push frames use codes at or above 0x80 and are
|
||||
* asynchronous. Keep one slot available for a response, and if an older queue
|
||||
* is already full of mixed traffic, let a response replace the newest push.
|
||||
* Responses are inserted before pushes so packet-log traffic cannot delay a
|
||||
* command indefinitely.
|
||||
*/
|
||||
template <typename Frame, typename QueueLength>
|
||||
bool enqueueCompanionFrame(Frame queue[], QueueLength& queue_len, size_t capacity,
|
||||
const uint8_t* src, size_t len) {
|
||||
if (queue == NULL || src == NULL || len == 0 || capacity == 0) return false;
|
||||
|
||||
size_t count = static_cast<size_t>(queue_len);
|
||||
if (count > capacity) return false;
|
||||
|
||||
const bool push = isCompanionPushFrame(src, len);
|
||||
if (push && count >= capacity - 1) {
|
||||
return false; // preserve one slot for the reply to an app command
|
||||
}
|
||||
|
||||
if (count == capacity) {
|
||||
// A response may displace best-effort asynchronous traffic, but never an
|
||||
// earlier response that another command is already waiting for.
|
||||
size_t evict = count;
|
||||
while (evict > 0) {
|
||||
--evict;
|
||||
if (isCompanionPushFrame(queue[evict].buf, queue[evict].len)) break;
|
||||
}
|
||||
if (!isCompanionPushFrame(queue[evict].buf, queue[evict].len)) return false;
|
||||
|
||||
for (size_t i = evict; i + 1 < count; ++i) {
|
||||
queue[i] = queue[i + 1];
|
||||
}
|
||||
--count;
|
||||
}
|
||||
|
||||
size_t insert_at = count;
|
||||
if (!push) {
|
||||
// Keep responses FIFO with respect to one another, ahead of asynchronous
|
||||
// pushes such as raw-packet logs and message-waiting notifications.
|
||||
// The stable partition also repairs a queue populated by older admission
|
||||
// behavior before this policy gets a chance to add the next response.
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
if (isCompanionPushFrame(queue[i].buf, queue[i].len)) continue;
|
||||
|
||||
Frame response = queue[i];
|
||||
size_t j = i;
|
||||
while (j > 0
|
||||
&& isCompanionPushFrame(queue[j - 1].buf, queue[j - 1].len)) {
|
||||
queue[j] = queue[j - 1];
|
||||
--j;
|
||||
}
|
||||
queue[j] = response;
|
||||
}
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (isCompanionPushFrame(queue[i].buf, queue[i].len)) {
|
||||
insert_at = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = count; i > insert_at; --i) {
|
||||
queue[i] = queue[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
queue[insert_at].len = len;
|
||||
memcpy(queue[insert_at].buf, src, len);
|
||||
queue_len = static_cast<QueueLength>(count + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
@@ -23,6 +23,16 @@ static constexpr uint8_t SCOPE_PATH_BRIDGE_BUCKET_BASE = 2;
|
||||
static constexpr uint8_t SCOPE_PATH_BRIDGE_BUCKET_COUNT = 6;
|
||||
static constexpr uint8_t SCOPE_PATH_INVALID_BUCKET = 0xFF;
|
||||
|
||||
enum RuleIncomingScope : uint8_t {
|
||||
RULE_IN_ANY = 0,
|
||||
RULE_IN_NONE = 1,
|
||||
RULE_IN_SCOPED = 2,
|
||||
RULE_IN_ALLOWED = 3,
|
||||
RULE_IN_UNKNOWN = 4,
|
||||
RULE_IN_SCOPE = 5,
|
||||
RULE_IN_REGION = 6,
|
||||
};
|
||||
|
||||
enum ChannelScopeGate {
|
||||
CHANNEL_SCOPE_USE_GLOBAL,
|
||||
CHANNEL_SCOPE_BYPASS,
|
||||
@@ -66,6 +76,49 @@ inline uint8_t scopeSelectorValue(uint8_t stored_selector) {
|
||||
& (uint8_t)~(SLOW_SCOPE_FLAG | SCOPE_PATH_MASK);
|
||||
}
|
||||
|
||||
// Regionless channel-scope rows use alternate base selector values so the
|
||||
// existing 16-bit target field can hold a one-based direct-scope table index.
|
||||
// Region-backed rows retain their original on-disk selector values.
|
||||
static constexpr uint8_t CHANNEL_SCOPE_DIRECT_KEY_128 = 17;
|
||||
static constexpr uint8_t CHANNEL_SCOPE_DIRECT_KEY_256 = 18;
|
||||
static constexpr uint8_t CHANNEL_SCOPE_DIRECT_TXT_ANY = 33;
|
||||
static constexpr uint8_t CHANNEL_SCOPE_DIRECT_LOGIN_ANY = 34;
|
||||
static constexpr uint8_t CHANNEL_SCOPE_DIRECT_OTHER_ANY = 35;
|
||||
static constexpr uint8_t CHANNEL_SCOPE_INVALID_SELECTOR = 0xFF;
|
||||
|
||||
inline uint8_t encodeChannelScopeTargetSelector(uint8_t match_selector,
|
||||
bool direct_target) {
|
||||
if (!direct_target) return match_selector;
|
||||
if (match_selector <= 2) {
|
||||
return (uint8_t)(CHANNEL_SCOPE_DIRECT_TXT_ANY + match_selector);
|
||||
}
|
||||
if (match_selector == 16 || match_selector == 32) {
|
||||
return (uint8_t)(16 + match_selector / 16);
|
||||
}
|
||||
return CHANNEL_SCOPE_INVALID_SELECTOR;
|
||||
}
|
||||
|
||||
inline bool channelScopeUsesDirectTarget(uint8_t stored_selector) {
|
||||
uint8_t selector = scopeSelectorValue(stored_selector);
|
||||
return (selector >= CHANNEL_SCOPE_DIRECT_KEY_128
|
||||
&& selector <= CHANNEL_SCOPE_DIRECT_KEY_256)
|
||||
|| (selector >= CHANNEL_SCOPE_DIRECT_TXT_ANY
|
||||
&& selector <= CHANNEL_SCOPE_DIRECT_OTHER_ANY);
|
||||
}
|
||||
|
||||
inline uint8_t channelScopeMatchSelectorValue(uint8_t stored_selector) {
|
||||
uint8_t selector = scopeSelectorValue(stored_selector);
|
||||
if (selector >= CHANNEL_SCOPE_DIRECT_KEY_128
|
||||
&& selector <= CHANNEL_SCOPE_DIRECT_KEY_256) {
|
||||
return (uint8_t)((selector - 16) * 16);
|
||||
}
|
||||
if (selector >= CHANNEL_SCOPE_DIRECT_TXT_ANY
|
||||
&& selector <= CHANNEL_SCOPE_DIRECT_OTHER_ANY) {
|
||||
return (uint8_t)(selector - CHANNEL_SCOPE_DIRECT_TXT_ANY);
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
inline bool scopeUsesSlowTiming(uint8_t stored_selector) {
|
||||
return (stored_selector & SLOW_SCOPE_FLAG) != 0;
|
||||
}
|
||||
@@ -176,6 +229,92 @@ inline bool pathMatchesConfiguredIds(const mesh::Packet* packet,
|
||||
packet, ids, configuredIdCount(ids, maximum_count));
|
||||
}
|
||||
|
||||
inline bool pathStartsWith(const mesh::Packet* packet,
|
||||
uint8_t hash_size,
|
||||
uint8_t prefix_hops,
|
||||
const uint8_t* prefix) {
|
||||
if (prefix_hops == 0) return true;
|
||||
if (packet == NULL || prefix == NULL || hash_size < 1 || hash_size > 3
|
||||
|| packet->getPathHashSize() != hash_size
|
||||
|| packet->getPathHashCount() < prefix_hops) {
|
||||
return false;
|
||||
}
|
||||
return memcmp(packet->path, prefix, hash_size * prefix_hops) == 0;
|
||||
}
|
||||
|
||||
inline bool ruleIncomingScopeMatches(uint8_t kind,
|
||||
bool incoming_is_scoped,
|
||||
uint16_t incoming_transport_code,
|
||||
bool incoming_region_allowed,
|
||||
uint16_t wanted_transport_code) {
|
||||
switch (kind) {
|
||||
case RULE_IN_ANY:
|
||||
return true;
|
||||
case RULE_IN_NONE:
|
||||
return !incoming_is_scoped;
|
||||
case RULE_IN_SCOPED:
|
||||
return incoming_is_scoped;
|
||||
case RULE_IN_ALLOWED:
|
||||
return incoming_region_allowed;
|
||||
case RULE_IN_UNKNOWN:
|
||||
return incoming_is_scoped && !incoming_region_allowed;
|
||||
case RULE_IN_SCOPE:
|
||||
return incoming_is_scoped
|
||||
&& incoming_transport_code == wanted_transport_code;
|
||||
case RULE_IN_REGION:
|
||||
// Region rules are name-bound and must be evaluated by the caller with
|
||||
// RegionNameUtils. Numeric region IDs are deliberately not rule identity.
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool rateLimitReached(bool window_active, uint32_t now,
|
||||
uint32_t window_started, uint16_t window_count,
|
||||
uint16_t rate_per_minute) {
|
||||
uint16_t effective_count = (!window_active
|
||||
|| now - window_started >= 60000UL)
|
||||
? 0 : window_count;
|
||||
return effective_count >= rate_per_minute;
|
||||
}
|
||||
|
||||
inline bool sameChannelKey(uint8_t left_len, const uint8_t left[],
|
||||
uint8_t right_len, const uint8_t right[]) {
|
||||
return left != NULL && right != NULL && left_len != 0
|
||||
&& left_len == right_len && memcmp(left, right, left_len) == 0;
|
||||
}
|
||||
|
||||
inline int nextOrderedRule(uint32_t match_mask, uint32_t visited_mask,
|
||||
const uint8_t priorities[], uint8_t count) {
|
||||
if (priorities == NULL || count > 32) return -1;
|
||||
int best = -1;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
uint32_t bit = (uint32_t)1U << i;
|
||||
if ((match_mask & bit) == 0 || (visited_mask & bit) != 0) continue;
|
||||
if (best < 0 || priorities[i] > priorities[best]) best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
inline uint32_t truncateRulesAtStop(uint32_t match_mask,
|
||||
const uint8_t priorities[],
|
||||
const uint8_t stop_flags[],
|
||||
uint8_t count) {
|
||||
if (stop_flags == NULL) return match_mask;
|
||||
uint32_t effective = 0;
|
||||
uint32_t visited = 0;
|
||||
while (true) {
|
||||
int index = nextOrderedRule(match_mask, visited, priorities, count);
|
||||
if (index < 0) break;
|
||||
uint32_t bit = (uint32_t)1U << index;
|
||||
visited |= bit;
|
||||
effective |= bit;
|
||||
if (stop_flags[index] != 0) break;
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
|
||||
inline bool scopeRuleAllowed(bool requires_region_match,
|
||||
bool incoming_region_allowed) {
|
||||
return !requires_region_match || incoming_region_allowed;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Remembers a bounded set of logical messages independently of their on-air
|
||||
// attempt byte. The fingerprint supplied by the caller must already identify
|
||||
// the peer and message text.
|
||||
template <size_t ENTRY_COUNT>
|
||||
class LogicalMessageCache {
|
||||
public:
|
||||
enum class ReplayDecision : uint8_t {
|
||||
NewMessage,
|
||||
ExactRetry,
|
||||
StaleOrMismatched
|
||||
};
|
||||
|
||||
LogicalMessageCache() { clear(); }
|
||||
|
||||
bool find(const uint8_t fingerprint[MAX_HASH_SIZE], uint32_t source_timestamp,
|
||||
uint32_t* mapped_timestamp = NULL) const {
|
||||
if (fingerprint == NULL) return false;
|
||||
|
||||
for (size_t i = 0; i < ENTRY_COUNT; i++) {
|
||||
const Entry& entry = entries_[i];
|
||||
if (entry.valid && entry.source_timestamp == source_timestamp
|
||||
&& memcmp(entry.fingerprint, fingerprint, MAX_HASH_SIZE) == 0) {
|
||||
if (mapped_timestamp != NULL) {
|
||||
*mapped_timestamp = entry.mapped_timestamp;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool remember(const uint8_t fingerprint[MAX_HASH_SIZE], uint32_t source_timestamp,
|
||||
uint32_t mapped_timestamp = 0) {
|
||||
if (fingerprint == NULL) return false;
|
||||
|
||||
for (size_t i = 0; i < ENTRY_COUNT; i++) {
|
||||
Entry& entry = entries_[i];
|
||||
if (entry.valid && entry.source_timestamp == source_timestamp
|
||||
&& memcmp(entry.fingerprint, fingerprint, MAX_HASH_SIZE) == 0) {
|
||||
entry.mapped_timestamp = mapped_timestamp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Entry& entry = entries_[next_entry_];
|
||||
memcpy(entry.fingerprint, fingerprint, MAX_HASH_SIZE);
|
||||
entry.source_timestamp = source_timestamp;
|
||||
entry.mapped_timestamp = mapped_timestamp;
|
||||
entry.valid = true;
|
||||
next_entry_ = (next_entry_ + 1) % ENTRY_COUNT;
|
||||
return true;
|
||||
}
|
||||
|
||||
ReplayDecision classifyAndRemember(
|
||||
const uint8_t fingerprint[MAX_HASH_SIZE], uint32_t source_timestamp,
|
||||
uint32_t& latest_source_timestamp) {
|
||||
if (fingerprint == NULL) return ReplayDecision::StaleOrMismatched;
|
||||
if (find(fingerprint, source_timestamp)) {
|
||||
return ReplayDecision::ExactRetry;
|
||||
}
|
||||
if (source_timestamp <= latest_source_timestamp) {
|
||||
return ReplayDecision::StaleOrMismatched;
|
||||
}
|
||||
|
||||
latest_source_timestamp = source_timestamp;
|
||||
remember(fingerprint, source_timestamp);
|
||||
return ReplayDecision::NewMessage;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
memset(entries_, 0, sizeof(entries_));
|
||||
next_entry_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static_assert(ENTRY_COUNT > 0, "LogicalMessageCache needs at least one entry");
|
||||
|
||||
struct Entry {
|
||||
uint8_t fingerprint[MAX_HASH_SIZE];
|
||||
uint32_t source_timestamp;
|
||||
uint32_t mapped_timestamp;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
Entry entries_[ENTRY_COUNT];
|
||||
size_t next_entry_;
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Keeps the most recently completed remote CLI reply so an exact request
|
||||
// retry can recover a lost response without executing the command twice.
|
||||
// This is deliberately a single bounded entry: remote CLI commands are
|
||||
// serialized, and repeater builds on small MCUs cannot afford one reply-sized
|
||||
// buffer for every ACL client.
|
||||
class RemoteCliReplyCache {
|
||||
public:
|
||||
static constexpr size_t MAX_REPLY_TEXT =
|
||||
MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1) - 5;
|
||||
|
||||
RemoteCliReplyCache() { clear(); }
|
||||
|
||||
static uint32_t fingerprint(const char* command, size_t command_len) {
|
||||
if (command == NULL) return 0;
|
||||
|
||||
// FNV-1a is only a compact request-mismatch guard. Authentication and the
|
||||
// sender/timestamp tuple remain the replay-protection boundary.
|
||||
uint32_t value = 2166136261UL;
|
||||
for (size_t i = 0; i < command_len; i++) {
|
||||
value ^= (uint8_t)command[i];
|
||||
value *= 16777619UL;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool remember(const uint8_t* sender_pub_key, uint32_t request_timestamp,
|
||||
uint32_t command_fingerprint, const char* response) {
|
||||
if (sender_pub_key == NULL || response == NULL) return false;
|
||||
|
||||
memcpy(sender_pub_key_, sender_pub_key, sizeof(sender_pub_key_));
|
||||
request_timestamp_ = request_timestamp;
|
||||
command_fingerprint_ = command_fingerprint;
|
||||
|
||||
size_t response_len = 0;
|
||||
while (response_len < MAX_REPLY_TEXT && response[response_len] != 0) {
|
||||
response_len++;
|
||||
}
|
||||
memcpy(response_, response, response_len);
|
||||
response_[response_len] = 0;
|
||||
valid_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool matches(const uint8_t* sender_pub_key, uint32_t request_timestamp,
|
||||
uint32_t command_fingerprint) const {
|
||||
return valid_ && sender_pub_key != NULL
|
||||
&& request_timestamp_ == request_timestamp
|
||||
&& command_fingerprint_ == command_fingerprint
|
||||
&& memcmp(sender_pub_key_, sender_pub_key, sizeof(sender_pub_key_)) == 0;
|
||||
}
|
||||
|
||||
const char* response() const { return response_; }
|
||||
bool hasResponse() const { return valid_ && response_[0] != 0; }
|
||||
bool isValid() const { return valid_; }
|
||||
|
||||
void clear() {
|
||||
valid_ = false;
|
||||
memset(sender_pub_key_, 0, sizeof(sender_pub_key_));
|
||||
request_timestamp_ = 0;
|
||||
command_fingerprint_ = 0;
|
||||
memset(response_, 0, sizeof(response_));
|
||||
}
|
||||
|
||||
private:
|
||||
bool valid_;
|
||||
uint8_t sender_pub_key_[PUB_KEY_SIZE];
|
||||
uint32_t request_timestamp_;
|
||||
uint32_t command_fingerprint_;
|
||||
char response_[MAX_REPLY_TEXT + 1];
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SerialBLEInterface.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
@@ -211,15 +212,11 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
}
|
||||
|
||||
if (deviceConnected && len > 0) {
|
||||
if (send_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
|
||||
src, len)) {
|
||||
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
send_queue[send_queue_len].len = len; // add to send queue
|
||||
memcpy(send_queue[send_queue_len].buf, src, len);
|
||||
send_queue_len++;
|
||||
|
||||
return len;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SerialWifiInterface.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
void SerialWifiInterface::begin(int port) {
|
||||
@@ -25,15 +26,11 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
}
|
||||
|
||||
if (deviceConnected && len > 0) {
|
||||
if (send_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
|
||||
src, len)) {
|
||||
WIFI_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
send_queue[send_queue_len].len = len; // add to send queue
|
||||
memcpy(send_queue[send_queue_len].buf, src, len);
|
||||
send_queue_len++;
|
||||
|
||||
return len;
|
||||
}
|
||||
return 0;
|
||||
@@ -173,4 +170,4 @@ size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
|
||||
bool SerialWifiInterface::isConnected() const {
|
||||
return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SerialEthernetInterface.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
|
||||
#define RECV_STATE_IDLE 0
|
||||
#define RECV_STATE_HDR_FOUND 1
|
||||
@@ -27,15 +28,11 @@ size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
}
|
||||
|
||||
if (isConnected() && len > 0) {
|
||||
if (send_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
|
||||
src, len)) {
|
||||
ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
send_queue[send_queue_len].len = len; // add to send queue
|
||||
memcpy(send_queue[send_queue_len].buf, src, len);
|
||||
send_queue_len++;
|
||||
|
||||
return len;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SerialBLEInterface.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "ble_gap.h"
|
||||
@@ -353,15 +354,11 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
|
||||
bool connected = isConnected();
|
||||
if (connected && len > 0) {
|
||||
if (send_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
|
||||
src, len)) {
|
||||
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
send_queue[send_queue_len].len = len;
|
||||
memcpy(send_queue[send_queue_len].buf, src, len);
|
||||
send_queue_len++;
|
||||
|
||||
return len;
|
||||
}
|
||||
return 0;
|
||||
|
||||
+4
-1
@@ -39,7 +39,10 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_prefs_save_routing` | `src/helpers/PrefsSaveRouting.h` | runtime common/observer setters write only their owning preference image; mixed-owner setters and migrations can deliberately write both |
|
||||
| `test_mqtt_payload_builder` | `src/helpers/MQTTPayloadBuilder.cpp` | status/packet/raw JSON contracts; optional fields; escaping; RX metrics and path; score handling; exact buffer bounds; maximum representative payloads |
|
||||
| `test_telemetry_history` | `src/helpers/TelemetryHistory.h` | 30-minute rings; seven-day temperature/voltage and dynamically sized GPS retention; exact 1 C temperature/status encoding; separate Base64 series payloads; 14-bit GPS differentials; resize preservation, heap budgets, and 1-based paging bounds |
|
||||
| `test_flood_filter_policy` | `src/helpers/FloodFilterPolicy.h` | unordered 3-byte and 2-byte-prefix path matching; match thresholds; repeated path-entry semantics; blacklist and bridge-bucket channel-scope selection; `require=region` and per-channel scope-gate truth tables; fast/slow scope timing; adding, replacing, and preserving packet scope |
|
||||
| `test_flood_filter_policy` | `src/helpers/FloodFilterPolicy.h` | unordered blacklist matching; ordered 1/2/3-byte pbyte rule prefixes; original incoming scope classes and canonical region-name identity; channel-authentication cache key comparison; priority ordering and terminal stop masks; bridge-bucket and regionless channel-target selector encoding; `require=region` and per-channel scope-gate truth tables; fast/slow timing; adding, replacing, and preserving packet scope |
|
||||
| `test_logical_message_cache` | `src/helpers/LogicalMessageCache.h` | bounded logical-message mapping; stable retry timestamps; exact older retries after newer messages; stale and same-timestamp mismatch rejection |
|
||||
| `test_remote_cli_reply_cache` | `src/helpers/RemoteCliReplyCache.h` | authenticated sender/timestamp/command matching; owned response storage; empty-response completion; on-air truncation; replacement and clearing |
|
||||
| `test_companion_frame_queue` | `src/helpers/CompanionFrameQueue.h` | reserved reply capacity; response priority; push eviction under saturation; response preservation |
|
||||
| `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) |
|
||||
|
||||
## Conventions (and how to add a suite)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/CompanionFrameQueue.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct TestFrame {
|
||||
size_t len;
|
||||
uint8_t buf[8];
|
||||
};
|
||||
|
||||
static bool enqueue(TestFrame queue[], size_t& count, size_t capacity,
|
||||
uint8_t code) {
|
||||
return mesh::enqueueCompanionFrame(queue, count, capacity, &code, 1);
|
||||
}
|
||||
|
||||
TEST(CompanionFrameQueue, PushTrafficLeavesOneResponseSlot) {
|
||||
TestFrame queue[4] = {};
|
||||
size_t count = 0;
|
||||
|
||||
EXPECT_TRUE(enqueue(queue, count, 4, 0x88));
|
||||
EXPECT_TRUE(enqueue(queue, count, 4, 0x84));
|
||||
EXPECT_TRUE(enqueue(queue, count, 4, 0x81));
|
||||
EXPECT_FALSE(enqueue(queue, count, 4, 0x8A));
|
||||
ASSERT_EQ(3U, count);
|
||||
|
||||
EXPECT_TRUE(enqueue(queue, count, 4, 0x00));
|
||||
ASSERT_EQ(4U, count);
|
||||
EXPECT_EQ(0x00, queue[0].buf[0]);
|
||||
}
|
||||
|
||||
TEST(CompanionFrameQueue, ResponsesRunBeforeQueuedPushesAndRemainFifo) {
|
||||
TestFrame queue[5] = {};
|
||||
size_t count = 0;
|
||||
|
||||
ASSERT_TRUE(enqueue(queue, count, 5, 0x88));
|
||||
ASSERT_TRUE(enqueue(queue, count, 5, 0x06));
|
||||
ASSERT_TRUE(enqueue(queue, count, 5, 0x84));
|
||||
ASSERT_TRUE(enqueue(queue, count, 5, 0x00));
|
||||
|
||||
ASSERT_EQ(4U, count);
|
||||
EXPECT_EQ(0x06, queue[0].buf[0]);
|
||||
EXPECT_EQ(0x00, queue[1].buf[0]);
|
||||
EXPECT_EQ(0x88, queue[2].buf[0]);
|
||||
EXPECT_EQ(0x84, queue[3].buf[0]);
|
||||
}
|
||||
|
||||
TEST(CompanionFrameQueue, ResponseEvictsNewestPushFromAnAlreadyFullQueue) {
|
||||
TestFrame queue[4] = {
|
||||
{1, {0x06}}, {1, {0x88}}, {1, {0x00}}, {1, {0x84}}};
|
||||
size_t count = 4;
|
||||
|
||||
ASSERT_TRUE(enqueue(queue, count, 4, 0x01));
|
||||
ASSERT_EQ(4U, count);
|
||||
EXPECT_EQ(0x06, queue[0].buf[0]);
|
||||
EXPECT_EQ(0x00, queue[1].buf[0]);
|
||||
EXPECT_EQ(0x01, queue[2].buf[0]);
|
||||
EXPECT_EQ(0x88, queue[3].buf[0]);
|
||||
}
|
||||
|
||||
TEST(CompanionFrameQueue, ResponseDoesNotEvictAnotherResponse) {
|
||||
TestFrame queue[3] = {
|
||||
{1, {0x00}}, {1, {0x01}}, {1, {0x06}}};
|
||||
size_t count = 3;
|
||||
|
||||
EXPECT_FALSE(enqueue(queue, count, 3, 0x02));
|
||||
EXPECT_EQ(3U, count);
|
||||
EXPECT_EQ(0x00, queue[0].buf[0]);
|
||||
EXPECT_EQ(0x01, queue[1].buf[0]);
|
||||
EXPECT_EQ(0x06, queue[2].buf[0]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/FloodFilterPolicy.h>
|
||||
#include <helpers/RegionNameUtils.h>
|
||||
|
||||
static mesh::Packet makeFloodPacket(uint8_t hash_size,
|
||||
const uint8_t* path,
|
||||
@@ -127,6 +128,161 @@ TEST(FloodFilterBlacklist, ConfiguredBucketStopsAtEmptyTrailingIds) {
|
||||
&packet, &bucket[0][0], 17));
|
||||
}
|
||||
|
||||
TEST(FloodRulePrefix, SupportsOneTwoAndThreeBytePathPrefixes) {
|
||||
const uint8_t one_byte_path[] = {0x11, 0x22, 0x33};
|
||||
const uint8_t two_byte_path[] = {0x11, 0x22, 0x33, 0x44};
|
||||
const uint8_t three_byte_path[] = {
|
||||
0x11, 0x22, 0x33,
|
||||
0x44, 0x55, 0x66,
|
||||
};
|
||||
mesh::Packet one = makeFloodPacket(1, one_byte_path, 3);
|
||||
mesh::Packet two = makeFloodPacket(2, two_byte_path, 2);
|
||||
mesh::Packet three = makeFloodPacket(3, three_byte_path, 2);
|
||||
|
||||
EXPECT_TRUE(FloodFilterPolicy::pathStartsWith(
|
||||
&one, 1, 2, one_byte_path));
|
||||
EXPECT_TRUE(FloodFilterPolicy::pathStartsWith(
|
||||
&two, 2, 2, two_byte_path));
|
||||
EXPECT_TRUE(FloodFilterPolicy::pathStartsWith(
|
||||
&three, 3, 2, three_byte_path));
|
||||
}
|
||||
|
||||
TEST(FloodRulePrefix, RequiresTheExactPathWidthAndStartingSequence) {
|
||||
const uint8_t path[] = {
|
||||
0x11, 0x22,
|
||||
0x33, 0x44,
|
||||
};
|
||||
const uint8_t wrong_start[] = {0x33, 0x44};
|
||||
mesh::Packet packet = makeFloodPacket(2, path, 2);
|
||||
|
||||
EXPECT_FALSE(FloodFilterPolicy::pathStartsWith(
|
||||
&packet, 1, 1, path));
|
||||
EXPECT_FALSE(FloodFilterPolicy::pathStartsWith(
|
||||
&packet, 2, 1, wrong_start));
|
||||
EXPECT_FALSE(FloodFilterPolicy::pathStartsWith(
|
||||
&packet, 2, 3, path));
|
||||
}
|
||||
|
||||
TEST(FloodRuleIncomingScope, MatchesOriginalScopeClasses) {
|
||||
using namespace FloodFilterPolicy;
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_ANY, false, 0, false, 0));
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_NONE, false, 0, true, 0));
|
||||
EXPECT_FALSE(ruleIncomingScopeMatches(
|
||||
RULE_IN_NONE, true, 0x1234, true, 0));
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_SCOPED, true, 0x1234, false, 0));
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_ALLOWED, true, 0x1234, true, 0));
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_UNKNOWN, true, 0x1234, false, 0));
|
||||
}
|
||||
|
||||
TEST(FloodRuleIncomingScope, ExactScopeDoesNotCrossMatch) {
|
||||
using namespace FloodFilterPolicy;
|
||||
EXPECT_TRUE(ruleIncomingScopeMatches(
|
||||
RULE_IN_SCOPE, true, 0x1234, true, 0x1234));
|
||||
EXPECT_FALSE(ruleIncomingScopeMatches(
|
||||
RULE_IN_SCOPE, true, 0x1235, true, 0x1234));
|
||||
EXPECT_FALSE(ruleIncomingScopeMatches(
|
||||
RULE_IN_REGION, true, 0x1234, true, 0));
|
||||
}
|
||||
|
||||
TEST(FloodRuleIncomingScope, RegionIdentityUsesCanonicalNames) {
|
||||
EXPECT_TRUE(RegionNameUtils::equivalent("usa", "#usa"));
|
||||
EXPECT_TRUE(RegionNameUtils::equivalent("BlackHole86", "BlackHole86"));
|
||||
EXPECT_FALSE(RegionNameUtils::equivalent("usa", "BlackHole86"));
|
||||
}
|
||||
|
||||
TEST(FloodRuleRate, BlocksAtTheConfiguredPerMinuteLimit) {
|
||||
EXPECT_FALSE(FloodFilterPolicy::rateLimitReached(
|
||||
false, 1000, 0, 500, 2));
|
||||
EXPECT_FALSE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 2000, 1000, 1, 2));
|
||||
EXPECT_TRUE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 2000, 1000, 2, 2));
|
||||
EXPECT_TRUE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 2000, 1000, 0, 0));
|
||||
}
|
||||
|
||||
TEST(FloodRuleRate, StartsANewWindowAfterSixtySeconds) {
|
||||
EXPECT_FALSE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 61000, 1000, 2, 2));
|
||||
EXPECT_TRUE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 999, 0xFFFFFF00U, 2, 2));
|
||||
EXPECT_FALSE(FloodFilterPolicy::rateLimitReached(
|
||||
true, 60000, 0xFFFFFF00U, 2, 2));
|
||||
}
|
||||
|
||||
TEST(FloodRuleAuthentication, CacheKeysCompareOnlyTheirActiveBytes) {
|
||||
uint8_t first[32] = {0};
|
||||
uint8_t same[32] = {0};
|
||||
uint8_t different[32] = {0};
|
||||
first[0] = same[0] = different[0] = 0xA5;
|
||||
first[15] = same[15] = different[15] = 0x5A;
|
||||
first[31] = 0x11;
|
||||
same[31] = 0x22;
|
||||
different[15] = 0x5B;
|
||||
|
||||
EXPECT_TRUE(FloodFilterPolicy::sameChannelKey(16, first, 16, same));
|
||||
EXPECT_FALSE(FloodFilterPolicy::sameChannelKey(
|
||||
16, first, 16, different));
|
||||
EXPECT_FALSE(FloodFilterPolicy::sameChannelKey(16, first, 32, first));
|
||||
EXPECT_FALSE(FloodFilterPolicy::sameChannelKey(0, first, 0, same));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, HighestPriorityWinsAndSlotBreaksTies) {
|
||||
const uint8_t priorities[] = {10, 30, 30, 5};
|
||||
uint32_t matches = 0x0F;
|
||||
uint32_t visited = 0;
|
||||
|
||||
int first = FloodFilterPolicy::nextOrderedRule(
|
||||
matches, visited, priorities, 4);
|
||||
ASSERT_EQ(1, first);
|
||||
visited |= (uint32_t)1U << first;
|
||||
EXPECT_EQ(2, FloodFilterPolicy::nextOrderedRule(
|
||||
matches, visited, priorities, 4));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, StopRemovesOnlyLowerOrderedMatches) {
|
||||
const uint8_t priorities[] = {10, 30, 20, 5};
|
||||
const uint8_t stop_flags[] = {0, 0, 1, 0};
|
||||
|
||||
EXPECT_EQ(0x06U, FloodFilterPolicy::truncateRulesAtStop(
|
||||
0x0F, priorities, stop_flags, 4));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, StopAtEqualPriorityUsesLowerSlotFirst) {
|
||||
const uint8_t priorities[] = {40, 40, 50};
|
||||
const uint8_t stop_flags[] = {1, 0, 0};
|
||||
|
||||
EXPECT_EQ(0x05U, FloodFilterPolicy::truncateRulesAtStop(
|
||||
0x07, priorities, stop_flags, 3));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, NoStopPreservesEveryMatch) {
|
||||
const uint8_t priorities[] = {1, 200, 3, 99};
|
||||
const uint8_t stop_flags[] = {0, 0, 0, 0};
|
||||
|
||||
EXPECT_EQ(0x0BU, FloodFilterPolicy::truncateRulesAtStop(
|
||||
0x0B, priorities, stop_flags, 4));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, ThirtyOneSlotTableIncludesTheLastSlot) {
|
||||
uint8_t priorities[31] = {0};
|
||||
uint8_t stop_flags[31] = {0};
|
||||
priorities[30] = 200;
|
||||
stop_flags[30] = 1;
|
||||
const uint32_t matches = ((uint32_t)1U << 30) | 1U;
|
||||
|
||||
EXPECT_EQ(30, FloodFilterPolicy::nextOrderedRule(
|
||||
matches, 0, priorities, 31));
|
||||
EXPECT_EQ((uint32_t)1U << 30,
|
||||
FloodFilterPolicy::truncateRulesAtStop(
|
||||
matches, priorities, stop_flags, 31));
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, RegionRequirementHasTheExpectedTruthTable) {
|
||||
EXPECT_TRUE(FloodFilterPolicy::scopeRuleAllowed(false, false));
|
||||
EXPECT_TRUE(FloodFilterPolicy::scopeRuleAllowed(false, true));
|
||||
@@ -193,6 +349,42 @@ TEST(FloodFilterScope, EveryBridgeBucketRoundTripsWithoutChangingSelector) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, DirectChannelTargetsRoundTripEveryMatcherClass) {
|
||||
const uint8_t selectors[] = {0, 1, 2, 16, 32};
|
||||
for (uint8_t selector : selectors) {
|
||||
uint8_t direct = FloodFilterPolicy::encodeChannelScopeTargetSelector(
|
||||
selector, true);
|
||||
uint8_t stored = FloodFilterPolicy::encodeScopeSelector(
|
||||
direct, true, FloodFilterPolicy::SCOPE_PATH_BRIDGE_BUCKET_BASE + 5);
|
||||
|
||||
EXPECT_NE(FloodFilterPolicy::CHANNEL_SCOPE_INVALID_SELECTOR, direct);
|
||||
EXPECT_TRUE(FloodFilterPolicy::channelScopeUsesDirectTarget(stored));
|
||||
EXPECT_EQ(selector,
|
||||
FloodFilterPolicy::channelScopeMatchSelectorValue(stored));
|
||||
EXPECT_TRUE(FloodFilterPolicy::scopeUsesSlowTiming(stored));
|
||||
EXPECT_EQ(FloodFilterPolicy::SCOPE_PATH_BRIDGE_BUCKET_BASE + 5,
|
||||
FloodFilterPolicy::scopePathSelectorValue(stored));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, RegionChannelTargetsKeepLegacySelectorValues) {
|
||||
const uint8_t selectors[] = {0, 1, 2, 16, 32};
|
||||
for (uint8_t selector : selectors) {
|
||||
uint8_t region = FloodFilterPolicy::encodeChannelScopeTargetSelector(
|
||||
selector, false);
|
||||
|
||||
EXPECT_EQ(selector, region);
|
||||
EXPECT_FALSE(FloodFilterPolicy::channelScopeUsesDirectTarget(region));
|
||||
EXPECT_EQ(selector,
|
||||
FloodFilterPolicy::channelScopeMatchSelectorValue(region));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, DirectChannelTargetRejectsUnknownMatcher) {
|
||||
EXPECT_EQ(FloodFilterPolicy::CHANNEL_SCOPE_INVALID_SELECTOR,
|
||||
FloodFilterPolicy::encodeChannelScopeTargetSelector(7, true));
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, OnlyChangedFastRulesReceiveFastTrackTreatment) {
|
||||
EXPECT_TRUE(FloodFilterPolicy::fastTrackScopeChange(true, false));
|
||||
EXPECT_FALSE(FloodFilterPolicy::fastTrackScopeChange(true, true));
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/LogicalMessageCache.h>
|
||||
|
||||
static void makeFingerprint(uint8_t fingerprint[MAX_HASH_SIZE], uint8_t value) {
|
||||
memset(fingerprint, value, MAX_HASH_SIZE);
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, MatchesFingerprintAndSourceTimestamp) {
|
||||
mesh::LogicalMessageCache<4> cache;
|
||||
uint8_t first[MAX_HASH_SIZE];
|
||||
uint8_t second[MAX_HASH_SIZE];
|
||||
makeFingerprint(first, 0x11);
|
||||
makeFingerprint(second, 0x22);
|
||||
|
||||
ASSERT_TRUE(cache.remember(first, 100U, 500U));
|
||||
|
||||
uint32_t mapped = 0;
|
||||
EXPECT_TRUE(cache.find(first, 100U, &mapped));
|
||||
EXPECT_EQ(500U, mapped);
|
||||
EXPECT_FALSE(cache.find(first, 101U));
|
||||
EXPECT_FALSE(cache.find(second, 100U));
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, RepeatedRememberUpdatesMappedTimestamp) {
|
||||
mesh::LogicalMessageCache<2> cache;
|
||||
uint8_t fingerprint[MAX_HASH_SIZE];
|
||||
makeFingerprint(fingerprint, 0x33);
|
||||
|
||||
ASSERT_TRUE(cache.remember(fingerprint, 7U, 70U));
|
||||
ASSERT_TRUE(cache.remember(fingerprint, 7U, 71U));
|
||||
|
||||
uint32_t mapped = 0;
|
||||
EXPECT_TRUE(cache.find(fingerprint, 7U, &mapped));
|
||||
EXPECT_EQ(71U, mapped);
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, RoundRobinReplacementIsBounded) {
|
||||
mesh::LogicalMessageCache<2> cache;
|
||||
uint8_t first[MAX_HASH_SIZE];
|
||||
uint8_t second[MAX_HASH_SIZE];
|
||||
uint8_t third[MAX_HASH_SIZE];
|
||||
makeFingerprint(first, 0x41);
|
||||
makeFingerprint(second, 0x42);
|
||||
makeFingerprint(third, 0x43);
|
||||
|
||||
ASSERT_TRUE(cache.remember(first, 1U));
|
||||
ASSERT_TRUE(cache.remember(second, 2U));
|
||||
ASSERT_TRUE(cache.remember(third, 3U));
|
||||
|
||||
EXPECT_FALSE(cache.find(first, 1U));
|
||||
EXPECT_TRUE(cache.find(second, 2U));
|
||||
EXPECT_TRUE(cache.find(third, 3U));
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, ClearForgetsAllEntries) {
|
||||
mesh::LogicalMessageCache<2> cache;
|
||||
uint8_t fingerprint[MAX_HASH_SIZE];
|
||||
makeFingerprint(fingerprint, 0x55);
|
||||
ASSERT_TRUE(cache.remember(fingerprint, 9U, 90U));
|
||||
|
||||
cache.clear();
|
||||
|
||||
EXPECT_FALSE(cache.find(fingerprint, 9U));
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, ExactOlderRetrySurvivesNewerMessages) {
|
||||
mesh::LogicalMessageCache<4> cache;
|
||||
uint8_t first[MAX_HASH_SIZE];
|
||||
uint8_t second[MAX_HASH_SIZE];
|
||||
makeFingerprint(first, 0x61);
|
||||
makeFingerprint(second, 0x62);
|
||||
uint32_t latest = 0;
|
||||
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::NewMessage,
|
||||
cache.classifyAndRemember(first, 100U, latest));
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::NewMessage,
|
||||
cache.classifyAndRemember(second, 101U, latest));
|
||||
EXPECT_EQ(101U, latest);
|
||||
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::ExactRetry,
|
||||
cache.classifyAndRemember(first, 100U, latest));
|
||||
EXPECT_EQ(101U, latest);
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, RejectsOldUnseenAndSameTimestampMismatches) {
|
||||
mesh::LogicalMessageCache<4> cache;
|
||||
uint8_t accepted[MAX_HASH_SIZE];
|
||||
uint8_t mismatched[MAX_HASH_SIZE];
|
||||
makeFingerprint(accepted, 0x71);
|
||||
makeFingerprint(mismatched, 0x72);
|
||||
uint32_t latest = 200U;
|
||||
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::StaleOrMismatched,
|
||||
cache.classifyAndRemember(mismatched, 199U, latest));
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::StaleOrMismatched,
|
||||
cache.classifyAndRemember(mismatched, 200U, latest));
|
||||
EXPECT_EQ(mesh::LogicalMessageCache<4>::ReplayDecision::NewMessage,
|
||||
cache.classifyAndRemember(accepted, 201U, latest));
|
||||
}
|
||||
|
||||
TEST(LogicalMessageCache, RejectsNullFingerprint) {
|
||||
mesh::LogicalMessageCache<2> cache;
|
||||
EXPECT_FALSE(cache.remember(NULL, 1U, 2U));
|
||||
EXPECT_FALSE(cache.find(NULL, 1U));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/RemoteCliReplyCache.h>
|
||||
|
||||
TEST(RemoteCliReplyCache, ReplaysOnlyTheSameAuthenticatedRequest) {
|
||||
mesh::RemoteCliReplyCache cache;
|
||||
uint8_t alice[PUB_KEY_SIZE] = {};
|
||||
uint8_t bob[PUB_KEY_SIZE] = {};
|
||||
alice[0] = 0xA1;
|
||||
bob[0] = 0xB2;
|
||||
const char command[] = "set repeat off";
|
||||
const uint32_t fingerprint =
|
||||
mesh::RemoteCliReplyCache::fingerprint(command, strlen(command));
|
||||
|
||||
EXPECT_FALSE(cache.matches(alice, 123U, fingerprint));
|
||||
ASSERT_TRUE(cache.remember(alice, 123U, fingerprint, "OK"));
|
||||
EXPECT_TRUE(cache.matches(alice, 123U, fingerprint));
|
||||
EXPECT_STREQ("OK", cache.response());
|
||||
EXPECT_TRUE(cache.hasResponse());
|
||||
|
||||
EXPECT_FALSE(cache.matches(bob, 123U, fingerprint));
|
||||
EXPECT_FALSE(cache.matches(alice, 124U, fingerprint));
|
||||
EXPECT_FALSE(cache.matches(
|
||||
alice, 123U,
|
||||
mesh::RemoteCliReplyCache::fingerprint("set repeat on", 13)));
|
||||
}
|
||||
|
||||
TEST(RemoteCliReplyCache, OwnsAndReplacesTheRememberedResponse) {
|
||||
mesh::RemoteCliReplyCache cache;
|
||||
uint8_t first_sender[PUB_KEY_SIZE] = {};
|
||||
uint8_t sender[PUB_KEY_SIZE] = {};
|
||||
first_sender[0] = 0x11;
|
||||
memcpy(sender, first_sender, sizeof(sender));
|
||||
char response[] = "first";
|
||||
|
||||
ASSERT_TRUE(cache.remember(sender, 1U, 10U, response));
|
||||
response[0] = 'X';
|
||||
sender[0] = 0x22;
|
||||
EXPECT_STREQ("first", cache.response());
|
||||
|
||||
ASSERT_TRUE(cache.remember(sender, 2U, 20U, "second"));
|
||||
EXPECT_FALSE(cache.matches(first_sender, 1U, 10U));
|
||||
EXPECT_TRUE(cache.matches(sender, 2U, 20U));
|
||||
EXPECT_STREQ("second", cache.response());
|
||||
}
|
||||
|
||||
TEST(RemoteCliReplyCache, EmptyResponseStillMarksRequestComplete) {
|
||||
mesh::RemoteCliReplyCache cache;
|
||||
uint8_t sender[PUB_KEY_SIZE] = {};
|
||||
|
||||
ASSERT_TRUE(cache.remember(sender, 9U, 99U, ""));
|
||||
EXPECT_TRUE(cache.matches(sender, 9U, 99U));
|
||||
EXPECT_TRUE(cache.isValid());
|
||||
EXPECT_FALSE(cache.hasResponse());
|
||||
EXPECT_STREQ("", cache.response());
|
||||
}
|
||||
|
||||
TEST(RemoteCliReplyCache, TruncatesToTheOnAirReplyLimit) {
|
||||
mesh::RemoteCliReplyCache cache;
|
||||
uint8_t sender[PUB_KEY_SIZE] = {};
|
||||
char response[mesh::RemoteCliReplyCache::MAX_REPLY_TEXT + 20];
|
||||
memset(response, 'x', sizeof(response));
|
||||
response[sizeof(response) - 1] = 0;
|
||||
|
||||
ASSERT_TRUE(cache.remember(sender, 7U, 77U, response));
|
||||
EXPECT_EQ(mesh::RemoteCliReplyCache::MAX_REPLY_TEXT,
|
||||
strlen(cache.response()));
|
||||
}
|
||||
|
||||
TEST(RemoteCliReplyCache, ClearForgetsTheRequestAndResponse) {
|
||||
mesh::RemoteCliReplyCache cache;
|
||||
uint8_t sender[PUB_KEY_SIZE] = {};
|
||||
ASSERT_TRUE(cache.remember(sender, 1U, 2U, "OK"));
|
||||
|
||||
cache.clear();
|
||||
|
||||
EXPECT_FALSE(cache.isValid());
|
||||
EXPECT_FALSE(cache.matches(sender, 1U, 2U));
|
||||
EXPECT_STREQ("", cache.response());
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -17,10 +17,13 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
[env:RAK_3x72_repeater]
|
||||
extends = rak3x72
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${rak3x72.build_flags}
|
||||
-Oz ; strongest size profile preserves the deployed 16 KiB filesystem boundary
|
||||
-flto
|
||||
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
|
||||
@@ -18,10 +18,13 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
[env:Tiny_Relay_repeater]
|
||||
extends = Tiny_Relay
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${Tiny_Relay.build_flags}
|
||||
-Oz ; strongest size profile preserves the deployed 16 KiB filesystem boundary
|
||||
-flto ; retain headroom for current repeater safety features
|
||||
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
|
||||
@@ -18,10 +18,13 @@ build_src_filter = ${stm32_base.build_src_filter}
|
||||
[env:wio-e5_repeater]
|
||||
extends = lora_e5
|
||||
board_upload.maximum_size = 245760 ; 240 KiB app, 16 KiB LittleFS
|
||||
build_unflags = -Os
|
||||
build_flags = ${lora_e5.build_flags}
|
||||
-Oz ; strongest size profile preserves the deployed 16 KiB filesystem boundary
|
||||
-flto
|
||||
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
|
||||
@@ -26,6 +26,7 @@ build_flags = ${lora_e5_mini.build_flags}
|
||||
-flto
|
||||
-fno-inline-small-functions ; reduce flash without moving the deployed filesystem boundary
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
|
||||
Reference in new issue
Block a user