mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-29 06:08:18 +00:00
Unify repeater flood policy persistence
This commit is contained in:
+21
-8
@@ -1334,8 +1334,13 @@ 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 FPF7 and applies to flood `GRP_DATA` packets regardless
|
||||
of channel key. Flood group text (`GRP_TXT`) is unaffected by this setting.
|
||||
On generalized repeaters these commands manage an ordinary visible FPF7
|
||||
`type=grp_data` drop row. `off` with `all` maps to `hops=all`; `off` with `N`
|
||||
maps to `hops=N+1+`. The 240 KB compact FPF6 profiles retain the legacy hard
|
||||
gate. Because it is an ordinary FPF7 row, a matching higher-priority `stop`
|
||||
rule can exempt traffic from it. The compact `get flood.filter` list marks the
|
||||
managed row with `~data`. 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`.
|
||||
|
||||
@@ -1344,7 +1349,7 @@ of channel key. Flood group text (`GRP_TXT`) is unaffected by this setting.
|
||||
#### Block selected flood channels with FPF7
|
||||
|
||||
The separate `flood.channel.block` command and 15-row table have been retired.
|
||||
Generalized repeaters use the 31-row FPF7 table for authenticated channel
|
||||
Generalized repeaters use the 32-row FPF7 forward phase for authenticated channel
|
||||
blocks:
|
||||
|
||||
```text
|
||||
@@ -1654,8 +1659,8 @@ ordered `prefix=` match on a room server. Standard room-server profiles do not
|
||||
compile this table.
|
||||
|
||||
**Parameters:**
|
||||
- `n`: Rule slot in the build's compiled table (normally `1-31`; some
|
||||
constrained profiles use fewer slots).
|
||||
- `n`: Forward-rule slot in the build's compiled table (`1-32` on generalized
|
||||
repeaters and `1-31` on FULL room servers; compact profiles may use fewer).
|
||||
- `type`: Payload type name, full `PAYLOAD_TYPE_*` name, decimal value `0-15`,
|
||||
hexadecimal value `0x00-0x0F`, or `any`.
|
||||
- `hops`: Optional; omitted means `all`.
|
||||
@@ -1749,7 +1754,7 @@ 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
|
||||
bypass hard forwarding gates or the other policy phases. 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
|
||||
@@ -1833,12 +1838,20 @@ 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
|
||||
On generalized repeaters, filter rows, scope-rewrite rows, the shared
|
||||
blacklist, and `flood.channel.data` compatibility state are committed in one
|
||||
atomic FPF7 image. Compact FPF6 profiles retain separate files. Replacing or deleting
|
||||
the blacklist does not delete rows containing `path=blacklist`; such rows
|
||||
remain dormant while the list is empty. Path hashes are truncated routing
|
||||
identifiers, not authenticated identities, so this is a forwarding signal
|
||||
rather than proof that a particular repeater handled a packet.
|
||||
|
||||
A common use is containment of bulk internet-to-mesh dumping: list the path
|
||||
IDs associated with the offending gateways, then add a broad
|
||||
`type=any hops=all path=blacklist drop` row. This prevents this repeater from
|
||||
retransmitting matching floods; it does not delete them from local logs or
|
||||
prove who originated them.
|
||||
|
||||
The unnumbered blacklist `set` replaces the whole list and accepts up to 18 IDs
|
||||
so it fits every CLI transport. Numbered `set` writes up to 18 consecutive
|
||||
entries beginning at an existing slot or exactly the next slot, allowing an
|
||||
@@ -1883,7 +1896,7 @@ 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.
|
||||
FPF7 forward rows; hard gates and the rewrite/moderation phases still apply.
|
||||
|
||||
The fixed 240 KB STM32WL profiles leave
|
||||
`MESH_ENABLE_FLOOD_RULE_ENGINE=0` and retain the compact, persistent FPF6
|
||||
|
||||
+21
-6
@@ -8,12 +8,13 @@ Everything runs locally in this browser. Channel keys, packet facts, and policy
|
||||
drafts are not uploaded anywhere.
|
||||
|
||||
<div class="filter-design-warning" role="note">
|
||||
<strong>Engine design preview</strong>
|
||||
<strong>Policy design preview</strong>
|
||||
<p>
|
||||
This page targets the new policy-engine idea, not today's FPF7 file or
|
||||
existing <code>set flood.*</code> commands. Its readable policy language,
|
||||
JSON, and Base64 bundle are a prototype for design testing. Current
|
||||
firmware cannot install these policies yet.
|
||||
The phases and core conditions model current FPF7 behavior, including its
|
||||
forward rows, scope rewrites, and shared blacklist. The readable policy
|
||||
language, JSON, and Base64 bundle are still a prototype: current firmware
|
||||
is configured with <code>set flood.*</code> commands and cannot install a
|
||||
bundle from this page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +107,7 @@ in the simulator below. The examples draw from
|
||||
<code>when type=grp_txt hops=all channel=public sender="Noisy User" do rate=5/min burst=5</code>
|
||||
</button>
|
||||
<button type="button" data-example="blacklist">
|
||||
<span>Drop packets whose path matches the passive blacklist</span>
|
||||
<span>Stop forwarding traffic from blacklisted internet gateways</span>
|
||||
<code>when type=any hops=all path=blacklist do drop</code>
|
||||
</button>
|
||||
<button type="button" data-example="factory">
|
||||
@@ -603,6 +604,20 @@ The class column shows which broader selector also matches it.
|
||||
| `14` | Reserved payload type 14 | `class:other` |
|
||||
| `raw_custom` | Application-defined raw data | `class:other` |
|
||||
|
||||
## Current FPF7 command mapping
|
||||
|
||||
| Firmware command | FPF7 role |
|
||||
| --- | --- |
|
||||
| `flood.rule` / `flood.filter` | Forward-phase match and action rows |
|
||||
| `flood.channel.data` | Compatibility view over one visible `type=grp_data` forward drop row |
|
||||
| `flood.channel.scope` | Scope-rewrite phase rows |
|
||||
| `flood.filter.blacklist` | One shared unordered path-ID set referenced by `path=blacklist` rows |
|
||||
|
||||
Generalized repeaters commit those sections together. The blacklist is useful
|
||||
for refusing to retransmit floods associated with internet gateways dumping
|
||||
bulk traffic, but a path ID is truncated and unauthenticated; it identifies a
|
||||
routing pattern, not a person.
|
||||
|
||||
## Proposed evaluation contract
|
||||
|
||||
The simulator uses these rules:
|
||||
|
||||
+51
-28
@@ -3,8 +3,8 @@
|
||||
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
|
||||
its `flood.filter` alias) with 31 forward-rule slots, but not the repeater's
|
||||
scope-rewrite, passive-blacklist, or text-moderation phases. 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.
|
||||
@@ -35,12 +35,15 @@ get flood.channel.scope
|
||||
get flood.channel.scope.require
|
||||
get flood.filter
|
||||
get flood.rule
|
||||
get flood.filter.blacklist
|
||||
get flood.moderation
|
||||
```
|
||||
|
||||
`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
|
||||
`flood.rule` is an alias for `flood.filter`, not another table. Generalized
|
||||
repeater FPF7 has 32 forward-rule slots plus scope-rewrite and shared-blacklist
|
||||
sections in the same atomic policy file. FULL room servers have 31 forward
|
||||
slots and empty repeater-only sections. Compact target profiles retain their
|
||||
separate FPF6-era controls. `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
|
||||
@@ -177,15 +180,14 @@ replaced.
|
||||
|
||||
Capacity is selected at build time:
|
||||
|
||||
- Roomy ESP32 builds: 255 rule slots and 32 regionless-target slots, 10,204
|
||||
bytes RAM, 10,210-byte file.
|
||||
- Roomy ESP32 builds: 255 rewrite slots and 32 regionless-target slots, 10,204
|
||||
bytes RAM.
|
||||
- DRAM-tight classic ESP32 LoRa-OTA repeaters, nRF52, and other normal
|
||||
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
|
||||
constrained builds: 31 rewrite and regionless-target slots, 2,108 bytes RAM.
|
||||
- Very-tight STM32WL builds: 15 rewrite slots and one reusable regionless-target
|
||||
slot, 572 bytes RAM.
|
||||
- The no-PSRAM LilyGo T-LoRa V2.1 repeater/observer: 4 rewrite and
|
||||
regionless-target slots, 272 bytes RAM. This minimum holds the
|
||||
three wildcard classes and one exact channel mapping.
|
||||
|
||||
Each rule retains its 36-byte record. A separate table holds 32-byte normalized
|
||||
@@ -194,6 +196,10 @@ 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.
|
||||
|
||||
On generalized builds these records are the FPF7 rewrite phase, and the file
|
||||
stores only through the highest occupied slot. Compact FPF6 builds retain the
|
||||
standalone FCS5 file and the file sizes described by their build profile.
|
||||
|
||||
## Require valid incoming scopes only on selected channels
|
||||
|
||||
`flood.channel.scope.require` changes region enforcement for received flood
|
||||
@@ -284,9 +290,18 @@ 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.
|
||||
becomes `type=any channel=#wardriving hops=5+ drop`. The 32nd forward slot
|
||||
guarantees room to migrate the old global `flood.channel.data` gate even when
|
||||
all 31 former general/channel slots were occupied. Compact STM32WL FPF6 builds
|
||||
cannot match authenticated channels and retain the older separate gate.
|
||||
|
||||
On generalized repeaters, `flood.channel.data*` is a compatibility view over
|
||||
one ordinary visible FPF7 `type=grp_data ... drop` row. Turning it off creates
|
||||
or updates that row; turning it on removes the row. Its hop setting maps to
|
||||
`hops=all` or `hops=N+1+`. There is no hidden GRP_DATA forwarding check ahead
|
||||
of FPF7. Normal ordering applies, so a matching higher-priority `stop` row can
|
||||
exempt selected traffic. The compact rule list marks the managed row with
|
||||
`~data`.
|
||||
|
||||
FPF7 binds `in=region:<name>` and `region=<name>` to canonical region names,
|
||||
not numeric region IDs. Removing, reordering, or reusing a region ID cannot
|
||||
@@ -312,11 +327,11 @@ 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
|
||||
The first matching `stop` row ends the FPF7 forward phase 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
|
||||
higher-priority drop and it does not bypass hard forwarding gates or the
|
||||
scope-rewrite and moderation phases. Without a stop row, matching drop and rate rows remain
|
||||
independent and the highest-order matching scope or region rewrite wins.
|
||||
|
||||
Match fields:
|
||||
@@ -425,6 +440,11 @@ set flood.filter.blacklist.4 445566
|
||||
set flood.filter any all path=blacklist
|
||||
```
|
||||
|
||||
This is intended for abuse containment, such as refusing to retransmit floods
|
||||
that repeatedly enter the mesh through known internet gateways dumping bulk
|
||||
traffic. The list is shared by every FPF7 row and scope-rewrite row that uses
|
||||
`path=blacklist`; it is not copied into each rule.
|
||||
|
||||
An unnumbered `set` replaces the list with up to 18 IDs, the largest command
|
||||
that fits every CLI transport. A numbered `set` writes a batch of up to 18 IDs
|
||||
beginning at an existing slot or the next consecutive slot. This is how an
|
||||
@@ -480,13 +500,15 @@ 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.
|
||||
|
||||
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 generalized repeaters, forward rules, `flood.channel.scope` rewrite rows,
|
||||
the shared blacklist, and the `flood.channel.data` compatibility state are one
|
||||
atomic FPF7 policy image. Existing `/flood_ch_scope`, `/flood_filter_bl`, FPF6,
|
||||
and FCB2 data is imported once; the old files are removed only after the new
|
||||
image verifies and commits. Compact FPF6 repeaters retain separate files.
|
||||
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—or a particular
|
||||
person—handled a packet. FULL room servers reject blacklist commands.
|
||||
|
||||
On first initialization, flood-filter slot 1 is seeded with:
|
||||
|
||||
@@ -711,8 +733,9 @@ other words, the controls combine as deny rules:
|
||||
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. Remaining `flood.filter`/`flood.rule` drop and rate rows use that saved
|
||||
4. `repeat` and `flood.max*` are checked.
|
||||
5. The FPF7 forward phase, including any row managed through
|
||||
`flood.channel.data*`, applies drop and rate decisions using 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
|
||||
|
||||
@@ -80,11 +80,11 @@ 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 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.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.data` | Compatibility view for a visible FPF7 `type=grp_data` drop row on generalized repeaters. `off` creates the row and `on` removes it; compact FPF6 builds retain the legacy gate. Use an authenticated `channel=` row 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` | Sets the compatibility row's hop range: `all` maps to `hops=all`; `1`-`7` repeats through that hop count and maps to `hops=N+1+`. | `get flood.channel.data.hops`, `set flood.channel.data.hops <all|1-7>` | `set flood.channel.data.hops 7` |
|
||||
| `flood.channel.scope` | FPF7 rewrite-phase rows that add a transport scope to received unscoped floods or replace 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. Generalized builds commit these rows with the forward phase and blacklist; compact FPF6 builds retain separate storage. 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 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.filter` | Persistent flood-route rules selected by payload type and optional hop range. Generalized repeaters have 32 FPF7 forward slots; FULL ESP32 room servers have 31. Repeaters store their scope-rewrite phase, shared unordered blacklist, and channel-data compatibility state in the same atomic FPF7 image. `path=blacklist` is intended for forwarding abuse containment, including bulk internet-to-mesh dumping, but truncated path IDs are not authenticated identities. Fixed 240 KB STM32WL repeaters retain compact FPF6 filtering and separate blacklist storage. 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, optional channel authentication, 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. `channel=*` means no channel condition and `type=any channel=*` matches every flood payload type. `scope=BlackHole86` directly derives a regionless sink scope; `region=BlackHole86` would require a configured flood-allowed region. 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 makes its `region=` rewrite and paired `stop` inert until the name returns, allowing lower safety rows to run. 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` |
|
||||
|
||||
@@ -115,6 +115,7 @@ extern "C" caddr_t _sbrk(int increment);
|
||||
#define FLOOD_PACKET_FILTER_BLACKLIST_FILE "/flood_filter_bl"
|
||||
#define FLOOD_CHANNEL_SCOPE_FILE "/flood_ch_scope"
|
||||
#define FLOOD_CHANNEL_SCOPE_TEMP_FILE "/flood_ch_scope.tmp"
|
||||
#define FLOOD_POLICY_SECTION_MAGIC "FPS1"
|
||||
#define FLOOD_CHANNEL_SCOPE_REQUIRE_FILE "/flood_ch_req"
|
||||
#define FLOOD_CHANNEL_SCOPE_REQUIRE_TEMP_FILE "/flood_ch_req.tmp"
|
||||
#define FLOOD_GROUP_MODERATION_FILE "/flood_grp_mod"
|
||||
@@ -927,7 +928,7 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
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(PORTABLE_MQTT_OBSERVER)
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER) && !MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
if (!_prefs.flood_channel_data_enabled
|
||||
&& packet->getPayloadType() == PAYLOAD_TYPE_GRP_DATA
|
||||
&& floodChannelDataHopApplies(packet)) {
|
||||
@@ -935,6 +936,8 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
packet->getPathHashCount());
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER)
|
||||
if (shouldBlockFloodPacketForward(packet)) return false;
|
||||
#endif
|
||||
}
|
||||
@@ -2915,6 +2918,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
memset(flood_channel_scope_requirements, 0,
|
||||
sizeof(flood_channel_scope_requirements));
|
||||
memset(flood_group_moderation, 0, sizeof(flood_group_moderation));
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
flood_policy_has_embedded_sections = false;
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
#endif
|
||||
memset(clock_sync_samples, 0, sizeof(clock_sync_samples));
|
||||
clock_sync_mesh_enabled = CLOCK_SYNC_MESH_DEFAULT_ENABLED != 0;
|
||||
clock_sync_mesh_edge_enabled = CLOCK_SYNC_MESH_EDGE_DEFAULT_ENABLED != 0;
|
||||
@@ -3071,14 +3079,15 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
// TODO: key_store.begin();
|
||||
region_map.load(_fs);
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER)
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
bool flood_filters_loaded = loadFloodPacketFilters();
|
||||
if (flood_filters_loaded) importLegacyFloodPolicySections();
|
||||
#else
|
||||
loadFloodPacketFilterBlacklist();
|
||||
bool flood_filters_loaded = loadFloodPacketFilters();
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
if (flood_filters_loaded) migrateLegacyFloodChannelBlocks();
|
||||
#else
|
||||
(void)flood_filters_loaded;
|
||||
#endif
|
||||
loadFloodChannelScopes();
|
||||
#endif
|
||||
loadFloodChannelScopeRequirements();
|
||||
loadFloodGroupModeration();
|
||||
loadClockSyncPrefs();
|
||||
@@ -4531,13 +4540,13 @@ static bool parseFloodPacketFilterBlacklist(
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
void MyMesh::loadFloodPacketFilterBlacklist() {
|
||||
bool MyMesh::loadFloodPacketFilterBlacklist() {
|
||||
flood_packet_filter_blacklist_count = 0;
|
||||
memset(flood_packet_filter_blacklist, 0, sizeof(flood_packet_filter_blacklist));
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_PACKET_FILTER_BLACKLIST_FILE)) return;
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_PACKET_FILTER_BLACKLIST_FILE)) return true;
|
||||
|
||||
File file = openFloodSettingsRead(_fs, FLOOD_PACKET_FILTER_BLACKLIST_FILE);
|
||||
if (!file) return;
|
||||
if (!file) return false;
|
||||
|
||||
uint8_t magic[4];
|
||||
uint8_t count = 0;
|
||||
@@ -4563,9 +4572,13 @@ void MyMesh::loadFloodPacketFilterBlacklist() {
|
||||
flood_packet_filter_blacklist_count = count;
|
||||
memcpy(flood_packet_filter_blacklist, loaded, sizeof(loaded));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool MyMesh::saveFloodPacketFilterBlacklist() {
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
return saveFloodPacketFilters();
|
||||
#else
|
||||
if (_fs == NULL) return false;
|
||||
File file = openFloodSettingsWrite(_fs, FLOOD_PACKET_FILTER_BLACKLIST_FILE);
|
||||
if (!file) return false;
|
||||
@@ -4580,6 +4593,7 @@ bool MyMesh::saveFloodPacketFilterBlacklist() {
|
||||
== flood_packet_filter_blacklist_count * FLOOD_PACKET_FILTER_PATH_ID_SIZE;
|
||||
file.close();
|
||||
return success;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::formatFloodPacketFilterBlacklist(const char* args, char* reply) const {
|
||||
@@ -4828,6 +4842,15 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
enum class FileState : uint8_t { Missing, Valid, Invalid, Unreadable };
|
||||
auto loadFile = [this](const char* filename) -> FileState {
|
||||
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
|
||||
memset(flood_channel_direct_scopes, 0,
|
||||
sizeof(flood_channel_direct_scopes));
|
||||
flood_packet_filter_blacklist_count = 0;
|
||||
memset(flood_packet_filter_blacklist, 0,
|
||||
sizeof(flood_packet_filter_blacklist));
|
||||
flood_policy_has_embedded_sections = false;
|
||||
if (!_fs->exists(filename)) return FileState::Missing;
|
||||
|
||||
File file = openFloodSettingsRead(_fs, filename);
|
||||
@@ -5033,9 +5056,140 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// FPF7 keeps the forwarding rows byte-compatible with the room-server
|
||||
// engine. Repeater-only policy phases follow in a tagged extension so an
|
||||
// older FPF7 image can still be upgraded in place.
|
||||
if (success && version_7 && file.available() > 0) {
|
||||
static_assert(sizeof(FloodChannelScopeEntry) == 36,
|
||||
"channel scope persistence requires 36-byte entries");
|
||||
uint8_t section_magic[4];
|
||||
uint8_t compatibility_slot = 0xFF;
|
||||
uint8_t compatibility_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
uint8_t scope_count = 0;
|
||||
success = file.read(section_magic, sizeof(section_magic))
|
||||
== sizeof(section_magic)
|
||||
&& memcmp(section_magic, FLOOD_POLICY_SECTION_MAGIC,
|
||||
sizeof(section_magic)) == 0
|
||||
&& file.read(&compatibility_slot, sizeof(compatibility_slot))
|
||||
== sizeof(compatibility_slot)
|
||||
&& file.read(&compatibility_max_hops,
|
||||
sizeof(compatibility_max_hops))
|
||||
== sizeof(compatibility_max_hops)
|
||||
&& (compatibility_max_hops == FLOOD_CHANNEL_HOPS_ALL
|
||||
|| (compatibility_max_hops >= 1
|
||||
&& compatibility_max_hops <= 7))
|
||||
&& file.read(&scope_count, sizeof(scope_count))
|
||||
== sizeof(scope_count);
|
||||
|
||||
uint8_t retained_scope = scope_count < FLOOD_CHANNEL_SCOPE_SLOTS
|
||||
? scope_count : FLOOD_CHANNEL_SCOPE_SLOTS;
|
||||
size_t retained_scope_bytes = (size_t)retained_scope
|
||||
* sizeof(FloodChannelScopeEntry);
|
||||
success = success
|
||||
&& file.read((uint8_t*)flood_channel_scopes, retained_scope_bytes)
|
||||
== retained_scope_bytes;
|
||||
FloodChannelScopeEntry discarded_scope;
|
||||
for (uint16_t i = retained_scope; success && i < scope_count; i++) {
|
||||
success = file.read((uint8_t*)&discarded_scope,
|
||||
sizeof(discarded_scope))
|
||||
== sizeof(discarded_scope);
|
||||
}
|
||||
|
||||
uint8_t direct_count = 0;
|
||||
success = success
|
||||
&& file.read(&direct_count, sizeof(direct_count))
|
||||
== sizeof(direct_count);
|
||||
uint8_t retained_direct = direct_count < FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS
|
||||
? direct_count : FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS;
|
||||
for (uint16_t i = 0; success && i < direct_count; i++) {
|
||||
char direct_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
success = file.read((uint8_t*)direct_name, sizeof(direct_name))
|
||||
== sizeof(direct_name);
|
||||
if (success && direct_name[0] != 0
|
||||
&& !isValidStoredFloodFilterScopeName(direct_name)) {
|
||||
success = false;
|
||||
}
|
||||
if (success && i < retained_direct) {
|
||||
memcpy(flood_channel_direct_scopes[i], direct_name,
|
||||
sizeof(direct_name));
|
||||
}
|
||||
}
|
||||
|
||||
for (uint16_t i = 0; success && i < retained_scope; i++) {
|
||||
auto& entry = flood_channel_scopes[i];
|
||||
uint8_t selector =
|
||||
FloodFilterPolicy::channelScopeMatchSelectorValue(entry.selector);
|
||||
bool direct_target =
|
||||
FloodFilterPolicy::channelScopeUsesDirectTarget(entry.selector);
|
||||
if (entry.target_id == 0) {
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
} else if (direct_target
|
||||
&& (entry.target_id > retained_direct
|
||||
|| flood_channel_direct_scopes[entry.target_id - 1][0] == 0)) {
|
||||
success = false;
|
||||
} else if (selector <= FLOOD_CHANNEL_SCOPE_OTHER_ANY) {
|
||||
entry.channel_hash = 0;
|
||||
memset(entry.secret, 0, sizeof(entry.secret));
|
||||
} else if (selector == CIPHER_KEY_SIZE || selector == PUB_KEY_SIZE) {
|
||||
mesh::Utils::sha256(&entry.channel_hash,
|
||||
sizeof(entry.channel_hash), entry.secret,
|
||||
selector);
|
||||
if (selector == CIPHER_KEY_SIZE) {
|
||||
memset(&entry.secret[CIPHER_KEY_SIZE], 0,
|
||||
PUB_KEY_SIZE - CIPHER_KEY_SIZE);
|
||||
}
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t blacklist_count = 0;
|
||||
success = success
|
||||
&& file.read(&blacklist_count, sizeof(blacklist_count))
|
||||
== sizeof(blacklist_count)
|
||||
&& blacklist_count <= FLOOD_PACKET_FILTER_BLACKLIST_MAX
|
||||
&& file.read((uint8_t*)flood_packet_filter_blacklist,
|
||||
blacklist_count * FLOOD_PACKET_FILTER_PATH_ID_SIZE)
|
||||
== blacklist_count * FLOOD_PACKET_FILTER_PATH_ID_SIZE;
|
||||
for (uint16_t i = 0; success && i < blacklist_count; i++) {
|
||||
for (uint16_t j = 0; j < i; j++) {
|
||||
if (memcmp(flood_packet_filter_blacklist[i],
|
||||
flood_packet_filter_blacklist[j],
|
||||
FLOOD_PACKET_FILTER_PATH_ID_SIZE) == 0) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (success && file.available() != 0) success = false;
|
||||
if (success && compatibility_slot != 0xFF) {
|
||||
success = compatibility_slot < count
|
||||
&& isFloodChannelDataRule(loaded[compatibility_slot])
|
||||
&& loaded[compatibility_slot].min_hops
|
||||
== (compatibility_max_hops == FLOOD_CHANNEL_HOPS_ALL
|
||||
? 0 : compatibility_max_hops + 1);
|
||||
}
|
||||
if (success) {
|
||||
flood_packet_filter_blacklist_count = blacklist_count;
|
||||
flood_channel_data_rule_slot = compatibility_slot;
|
||||
flood_channel_data_rule_max_hops = compatibility_max_hops;
|
||||
flood_policy_has_embedded_sections = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
if (!success) memset(flood_packet_filters, 0,
|
||||
sizeof(flood_packet_filters));
|
||||
if (!success) {
|
||||
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
|
||||
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
|
||||
memset(flood_channel_direct_scopes, 0,
|
||||
sizeof(flood_channel_direct_scopes));
|
||||
flood_packet_filter_blacklist_count = 0;
|
||||
memset(flood_packet_filter_blacklist, 0,
|
||||
sizeof(flood_packet_filter_blacklist));
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
flood_policy_has_embedded_sections = false;
|
||||
}
|
||||
return success ? FileState::Valid : FileState::Invalid;
|
||||
};
|
||||
|
||||
@@ -5098,14 +5252,230 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
|| backup == FileState::Unreadable) {
|
||||
return false;
|
||||
}
|
||||
return saveFloodPacketFilters();
|
||||
// Defer the first FPF7 write until legacy scope, blacklist, channel-data,
|
||||
// and channel-block settings have been imported into the same transaction.
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
if (_fs == NULL || !_fs->exists(LEGACY_FLOOD_CHANNEL_BLOCK_FILE)) {
|
||||
bool MyMesh::isFloodChannelDataRule(
|
||||
const FloodPacketFilterEntry& entry) const {
|
||||
bool valid_hops = entry.min_hops == 0
|
||||
|| (entry.min_hops >= 2 && entry.min_hops <= 8);
|
||||
return entry.active
|
||||
&& entry.payload_type == PAYLOAD_TYPE_GRP_DATA
|
||||
&& valid_hops
|
||||
&& entry.max_hops == FLOOD_PACKET_FILTER_MAX_HOPS
|
||||
&& !entry.suspend_on_temp_radio
|
||||
&& entry.scope_name[0] == 0
|
||||
&& !entry.match_blacklisted_path
|
||||
&& !entry.scope_uses_slow_timing
|
||||
&& entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_ANY
|
||||
&& entry.incoming_scope_name[0] == 0
|
||||
&& entry.channel_key_len == 0
|
||||
&& entry.channel_name[0] == 0
|
||||
&& entry.path_hash_size == 0
|
||||
&& entry.path_hops == 0
|
||||
&& entry.target_region_name[0] == 0
|
||||
&& entry.drop_on_match
|
||||
&& !entry.rate_limit_enabled
|
||||
&& entry.priority == 0
|
||||
&& !entry.stop_on_match;
|
||||
}
|
||||
|
||||
int MyMesh::findFloodChannelDataRule() const {
|
||||
if (flood_channel_data_rule_slot >= FLOOD_PACKET_FILTER_SLOTS) return -1;
|
||||
return isFloodChannelDataRule(
|
||||
flood_packet_filters[flood_channel_data_rule_slot])
|
||||
? flood_channel_data_rule_slot : -1;
|
||||
}
|
||||
|
||||
static bool parseFloodChannelDataHopsValue(const char* value,
|
||||
uint8_t& max_hops) {
|
||||
value = skipFloodFilterSpaces(value);
|
||||
if (strcmp(value, "all") == 0) {
|
||||
max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
return true;
|
||||
}
|
||||
uint8_t parsed = 0;
|
||||
if (!parseFloodFilterUnsigned(value, 7, parsed)
|
||||
|| parsed < 1 || parsed > 7) {
|
||||
return false;
|
||||
}
|
||||
max_hops = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::formatFloodChannelDataHops(char* reply) const {
|
||||
if (flood_channel_data_rule_max_hops == FLOOD_CHANNEL_HOPS_ALL) {
|
||||
strcpy(reply, "> h=all");
|
||||
} else {
|
||||
snprintf(reply, 160, "> h>%u",
|
||||
(uint32_t)flood_channel_data_rule_max_hops);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::formatFloodChannelData(char* reply) const {
|
||||
const char* state = findFloodChannelDataRule() >= 0 ? "off" : "on";
|
||||
if (flood_channel_data_rule_max_hops == FLOOD_CHANNEL_HOPS_ALL) {
|
||||
snprintf(reply, 160, "> %s h=all", state);
|
||||
} else {
|
||||
snprintf(reply, 160, "> %s h>%u", state,
|
||||
(uint32_t)flood_channel_data_rule_max_hops);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::setFloodChannelData(const char* value, char* reply) {
|
||||
value = skipFloodFilterSpaces(value);
|
||||
bool enable = strcmp(value, "on") == 0;
|
||||
if (!enable && strcmp(value, "off") != 0) {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t previous_slot = flood_channel_data_rule_slot;
|
||||
int current = findFloodChannelDataRule();
|
||||
int slot = current;
|
||||
if (!enable && slot < 0) {
|
||||
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
|
||||
if (!flood_packet_filters[i].active) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
strcpy(reply, "Err - filter table full");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FloodPacketFilterEntry previous;
|
||||
memset(&previous, 0, sizeof(previous));
|
||||
if (slot >= 0) previous = flood_packet_filters[slot];
|
||||
if (enable) {
|
||||
if (current >= 0) {
|
||||
memset(&flood_packet_filters[current], 0,
|
||||
sizeof(flood_packet_filters[current]));
|
||||
}
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
} else {
|
||||
auto& entry = flood_packet_filters[slot];
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
entry.active = true;
|
||||
entry.payload_type = PAYLOAD_TYPE_GRP_DATA;
|
||||
entry.min_hops = flood_channel_data_rule_max_hops
|
||||
== FLOOD_CHANNEL_HOPS_ALL
|
||||
? 0 : flood_channel_data_rule_max_hops + 1;
|
||||
entry.max_hops = FLOOD_PACKET_FILTER_MAX_HOPS;
|
||||
entry.incoming_scope_kind = FloodFilterPolicy::RULE_IN_ANY;
|
||||
entry.drop_on_match = true;
|
||||
flood_channel_data_rule_slot = (uint8_t)slot;
|
||||
}
|
||||
|
||||
if (!saveFloodPacketFilters()) {
|
||||
flood_channel_data_rule_slot = previous_slot;
|
||||
if (slot >= 0) flood_packet_filters[slot] = previous;
|
||||
strcpy(reply, "Err - unable to save flood.channel.data rule");
|
||||
return;
|
||||
}
|
||||
_prefs.flood_channel_data_enabled = enable ? 1 : 0;
|
||||
_prefs.flood_channel_data_max_hops = flood_channel_data_rule_max_hops;
|
||||
_cli.savePrefs(_fs, PrefsSaveRouting::Scope::Common);
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
|
||||
void MyMesh::setFloodChannelDataHops(const char* value, char* reply) {
|
||||
uint8_t max_hops = FLOOD_CHANNEL_HOPS_ALL;
|
||||
if (!parseFloodChannelDataHopsValue(value, max_hops)) {
|
||||
strcpy(reply, "Error, must be all or 1-7");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t previous_max_hops = flood_channel_data_rule_max_hops;
|
||||
int slot = findFloodChannelDataRule();
|
||||
uint8_t previous_min_hops = slot >= 0
|
||||
? flood_packet_filters[slot].min_hops : 0;
|
||||
flood_channel_data_rule_max_hops = max_hops;
|
||||
if (slot >= 0) {
|
||||
flood_packet_filters[slot].min_hops = max_hops
|
||||
== FLOOD_CHANNEL_HOPS_ALL
|
||||
? 0 : max_hops + 1;
|
||||
}
|
||||
if (!saveFloodPacketFilters()) {
|
||||
flood_channel_data_rule_max_hops = previous_max_hops;
|
||||
if (slot >= 0) flood_packet_filters[slot].min_hops = previous_min_hops;
|
||||
strcpy(reply, "Err - unable to save flood.channel.data rule");
|
||||
return;
|
||||
}
|
||||
_prefs.flood_channel_data_max_hops = max_hops;
|
||||
_cli.savePrefs(_fs, PrefsSaveRouting::Scope::Common);
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
|
||||
bool MyMesh::migrateLegacyFloodChannelData() {
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
flood_channel_data_rule_max_hops = _prefs.flood_channel_data_max_hops;
|
||||
if (_prefs.flood_channel_data_enabled) return true;
|
||||
|
||||
int slot = -1;
|
||||
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
|
||||
if (!flood_packet_filters[i].active) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"legacy flood.channel.data migration skipped: FPF7 table full");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& entry = flood_packet_filters[slot];
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
entry.active = true;
|
||||
entry.payload_type = PAYLOAD_TYPE_GRP_DATA;
|
||||
entry.min_hops = flood_channel_data_rule_max_hops
|
||||
== FLOOD_CHANNEL_HOPS_ALL
|
||||
? 0 : flood_channel_data_rule_max_hops + 1;
|
||||
entry.max_hops = FLOOD_PACKET_FILTER_MAX_HOPS;
|
||||
entry.incoming_scope_kind = FloodFilterPolicy::RULE_IN_ANY;
|
||||
entry.drop_on_match = true;
|
||||
flood_channel_data_rule_slot = (uint8_t)slot;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::importLegacyFloodPolicySections() {
|
||||
if (flood_policy_has_embedded_sections) return true;
|
||||
if (_fs == NULL
|
||||
|| !loadFloodPacketFilterBlacklist()
|
||||
|| !loadFloodChannelScopes()
|
||||
|| !migrateLegacyFloodChannelData()
|
||||
|| !migrateLegacyFloodChannelBlocks()) {
|
||||
MESH_DEBUG_PRINTLN("FPF7 legacy policy import deferred");
|
||||
return false;
|
||||
}
|
||||
if (!saveFloodPacketFilters()) {
|
||||
MESH_DEBUG_PRINTLN("FPF7 legacy policy import save failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The FPF7 image is now authoritative. Failed removals are harmless: the
|
||||
// embedded-section marker prevents importing stale files again.
|
||||
if (_fs->exists(FLOOD_PACKET_FILTER_BLACKLIST_FILE))
|
||||
_fs->remove(FLOOD_PACKET_FILTER_BLACKLIST_FILE);
|
||||
if (_fs->exists(FLOOD_CHANNEL_SCOPE_FILE))
|
||||
_fs->remove(FLOOD_CHANNEL_SCOPE_FILE);
|
||||
if (_fs->exists(FLOOD_CHANNEL_SCOPE_TEMP_FILE))
|
||||
_fs->remove(FLOOD_CHANNEL_SCOPE_TEMP_FILE);
|
||||
if (_fs->exists(LEGACY_FLOOD_CHANNEL_BLOCK_FILE))
|
||||
_fs->remove(LEGACY_FLOOD_CHANNEL_BLOCK_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
if (_fs == NULL || !_fs->exists(LEGACY_FLOOD_CHANNEL_BLOCK_FILE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
struct LegacyRow {
|
||||
uint8_t active;
|
||||
uint8_t key_len;
|
||||
@@ -5117,7 +5487,7 @@ void MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
memset(rows, 0, sizeof(rows));
|
||||
|
||||
File file = openFloodSettingsRead(_fs, LEGACY_FLOOD_CHANNEL_BLOCK_FILE);
|
||||
if (!file) return;
|
||||
if (!file) return false;
|
||||
|
||||
uint8_t magic[4];
|
||||
uint8_t count = 0;
|
||||
@@ -5155,7 +5525,7 @@ void MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
file.close();
|
||||
if (!success) {
|
||||
MESH_DEBUG_PRINTLN("legacy channel-block migration skipped: invalid file");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t append_slots[LEGACY_FLOOD_CHANNEL_BLOCK_SLOTS];
|
||||
@@ -5204,7 +5574,7 @@ void MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
}
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"legacy channel-block migration skipped: FPF7 table full");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& entry = flood_packet_filters[free_slot];
|
||||
@@ -5228,22 +5598,10 @@ void MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
append_slots[append_count++] = (uint8_t)free_slot;
|
||||
}
|
||||
|
||||
if (append_count != 0 && !saveFloodPacketFilters()) {
|
||||
for (int i = 0; i < append_count; i++) {
|
||||
memset(&flood_packet_filters[append_slots[i]], 0,
|
||||
sizeof(flood_packet_filters[append_slots[i]]));
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("legacy channel-block migration skipped: save failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Once every active legacy row exists in FPF7, the retired file is no
|
||||
// longer needed. A failed remove is harmless because duplicate detection
|
||||
// makes the migration idempotent.
|
||||
_fs->remove(LEGACY_FLOOD_CHANNEL_BLOCK_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::saveFloodPacketFilters() {
|
||||
bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase) {
|
||||
if (_fs == NULL) return false;
|
||||
// Recovery owns transaction remnants; overwriting one could erase the only
|
||||
// complete image after a failed publish boundary.
|
||||
@@ -5312,6 +5670,53 @@ bool MyMesh::saveFloodPacketFilters() {
|
||||
&& writeExact(&entry.priority, sizeof(entry.priority))
|
||||
&& writeExact(&stop_on_match, sizeof(stop_on_match));
|
||||
}
|
||||
static_assert(FLOOD_CHANNEL_SCOPE_SLOTS <= 255,
|
||||
"FPF7 scope phase count is one byte");
|
||||
static_assert(FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS <= 255,
|
||||
"FPF7 direct-scope count is one byte");
|
||||
static_assert(sizeof(FloodChannelScopeEntry) == 36,
|
||||
"channel scope persistence requires 36-byte entries");
|
||||
const uint8_t section_magic[4] = {'F', 'P', 'S', '1'};
|
||||
uint8_t scope_count = 0;
|
||||
uint8_t direct_count = 0;
|
||||
if (!empty_scope_phase) {
|
||||
for (uint16_t i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) {
|
||||
if (flood_channel_scopes[i].target_id != 0) {
|
||||
scope_count = (uint8_t)(i + 1U);
|
||||
}
|
||||
}
|
||||
for (uint16_t i = 0; i < FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS; i++) {
|
||||
if (flood_channel_direct_scopes[i][0] != 0) {
|
||||
direct_count = (uint8_t)(i + 1U);
|
||||
}
|
||||
}
|
||||
}
|
||||
success = success
|
||||
&& writeExact(section_magic, sizeof(section_magic))
|
||||
&& writeExact(&flood_channel_data_rule_slot,
|
||||
sizeof(flood_channel_data_rule_slot))
|
||||
&& writeExact(&flood_channel_data_rule_max_hops,
|
||||
sizeof(flood_channel_data_rule_max_hops))
|
||||
&& writeExact(&scope_count, sizeof(scope_count));
|
||||
if (success && scope_count != 0) {
|
||||
success = writeExact(flood_channel_scopes,
|
||||
scope_count * sizeof(FloodChannelScopeEntry));
|
||||
}
|
||||
success = success && writeExact(&direct_count, sizeof(direct_count));
|
||||
if (success && direct_count != 0) {
|
||||
success = writeExact(flood_channel_direct_scopes,
|
||||
direct_count
|
||||
* FLOOD_PACKET_FILTER_SCOPE_NAME_LEN);
|
||||
}
|
||||
success = success
|
||||
&& writeExact(&flood_packet_filter_blacklist_count,
|
||||
sizeof(flood_packet_filter_blacklist_count));
|
||||
if (success && flood_packet_filter_blacklist_count != 0) {
|
||||
success = writeExact(
|
||||
flood_packet_filter_blacklist,
|
||||
flood_packet_filter_blacklist_count
|
||||
* FLOOD_PACKET_FILTER_PATH_ID_SIZE);
|
||||
}
|
||||
file.close();
|
||||
if (!success || !verifyFloodSettingsWrite(
|
||||
_fs, FLOOD_PACKET_FILTER_TEMP_FILE, bytes_written, write_hash)) {
|
||||
@@ -5332,6 +5737,7 @@ bool MyMesh::saveFloodPacketFilters() {
|
||||
}
|
||||
if (_fs->exists(FLOOD_PACKET_FILTER_BACKUP_FILE))
|
||||
_fs->remove(FLOOD_PACKET_FILTER_BACKUP_FILE);
|
||||
flood_policy_has_embedded_sections = true;
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
@@ -5425,7 +5831,8 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
return success;
|
||||
}
|
||||
|
||||
bool MyMesh::saveFloodPacketFilters() {
|
||||
bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase) {
|
||||
(void)empty_scope_phase;
|
||||
if (_fs == NULL) return false;
|
||||
File file = openFloodSettingsWrite(_fs, FLOOD_PACKET_FILTER_FILE);
|
||||
if (!file) return false;
|
||||
@@ -6019,14 +6426,15 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply) const {
|
||||
snprintf(priority, sizeof(priority), "^%u",
|
||||
(unsigned int)entry.priority);
|
||||
}
|
||||
snprintf(item, sizeof(item), " %d=%s@%s%s%s%s%s%s%s%s",
|
||||
snprintf(item, sizeof(item), " %d=%s@%s%s%s%s%s%s%s%s%s",
|
||||
i + 1, floodFilterPayloadTypeName(entry.payload_type), hops,
|
||||
entry.match_blacklisted_path ? "?blacklist" : "",
|
||||
target, priority,
|
||||
entry.stop_on_match ? "~stop" : "",
|
||||
entry.rate_limit_enabled ? "~rate" : "",
|
||||
entry.scope_uses_slow_timing ? "~slow" : "",
|
||||
entry.suspend_on_temp_radio ? "~tempradio" : "");
|
||||
entry.suspend_on_temp_radio ? "~tempradio" : "",
|
||||
i == findFloodChannelDataRule() ? "~data" : "");
|
||||
size_t item_len = strlen(item);
|
||||
if (used + item_len >= 156) {
|
||||
truncated = true;
|
||||
@@ -6455,6 +6863,10 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
int slot = requested_slot;
|
||||
if (slot < 0) {
|
||||
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
|
||||
// Keep the compatibility-owned row distinct from an ordinary rule with
|
||||
// identical match/action fields. Otherwise a generic, unnumbered set
|
||||
// would silently detach flood.channel.data from its own row.
|
||||
if (i == flood_channel_data_rule_slot) continue;
|
||||
const auto& entry = flood_packet_filters[i];
|
||||
if (entry.active
|
||||
&& memcmp(&entry, &candidate,
|
||||
@@ -6479,9 +6891,14 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
|
||||
FloodPacketFilterEntry previous = flood_packet_filters[slot];
|
||||
uint8_t previous_channel_data_slot = flood_channel_data_rule_slot;
|
||||
if (slot == flood_channel_data_rule_slot) {
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
}
|
||||
flood_packet_filters[slot] = candidate;
|
||||
if (!saveFloodPacketFilters()) {
|
||||
flood_packet_filters[slot] = previous;
|
||||
flood_channel_data_rule_slot = previous_channel_data_slot;
|
||||
strcpy(reply, "Err - unable to save flood filter");
|
||||
return;
|
||||
}
|
||||
@@ -6752,9 +7169,16 @@ void MyMesh::deleteFloodPacketFilter(const char* args, char* reply) {
|
||||
if (floodFilterAsciiEqual(selector, "all")) {
|
||||
FloodPacketFilterEntry previous[FLOOD_PACKET_FILTER_SLOTS];
|
||||
memcpy(previous, flood_packet_filters, sizeof(previous));
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
uint8_t previous_channel_data_slot = flood_channel_data_rule_slot;
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
#endif
|
||||
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
|
||||
if (!saveFloodPacketFilters()) {
|
||||
memcpy(flood_packet_filters, previous, sizeof(flood_packet_filters));
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
flood_channel_data_rule_slot = previous_channel_data_slot;
|
||||
#endif
|
||||
strcpy(reply, "Err - unable to save flood filter");
|
||||
} else {
|
||||
strcpy(reply, "OK - all flood filters removed");
|
||||
@@ -6773,9 +7197,18 @@ void MyMesh::deleteFloodPacketFilter(const char* args, char* reply) {
|
||||
return;
|
||||
}
|
||||
FloodPacketFilterEntry previous = flood_packet_filters[index];
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
uint8_t previous_channel_data_slot = flood_channel_data_rule_slot;
|
||||
if (index == flood_channel_data_rule_slot) {
|
||||
flood_channel_data_rule_slot = 0xFF;
|
||||
}
|
||||
#endif
|
||||
memset(&flood_packet_filters[index], 0, sizeof(flood_packet_filters[index]));
|
||||
if (!saveFloodPacketFilters()) {
|
||||
flood_packet_filters[index] = previous;
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
flood_channel_data_rule_slot = previous_channel_data_slot;
|
||||
#endif
|
||||
strcpy(reply, "Err - unable to save flood filter");
|
||||
} else {
|
||||
strcpy(reply, "OK");
|
||||
@@ -6816,15 +7249,15 @@ static const char* wildcardFloodChannelScopeName(uint8_t selector) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void MyMesh::loadFloodChannelScopes() {
|
||||
bool MyMesh::loadFloodChannelScopes() {
|
||||
static_assert(sizeof(FloodChannelScopeEntry) == 36,
|
||||
"channel scope persistence requires 36-byte entries");
|
||||
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
|
||||
memset(flood_channel_direct_scopes, 0,
|
||||
sizeof(flood_channel_direct_scopes));
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_CHANNEL_SCOPE_FILE)) return;
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_CHANNEL_SCOPE_FILE)) return true;
|
||||
File file = openFloodSettingsRead(_fs, FLOOD_CHANNEL_SCOPE_FILE);
|
||||
if (!file) return;
|
||||
if (!file) return false;
|
||||
|
||||
uint8_t magic[4];
|
||||
uint8_t count = 0;
|
||||
@@ -6909,9 +7342,13 @@ void MyMesh::loadFloodChannelScopes() {
|
||||
memset(flood_channel_direct_scopes, 0,
|
||||
sizeof(flood_channel_direct_scopes));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool MyMesh::saveFloodChannelScopes(bool empty_table) {
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
return saveFloodPacketFilters(empty_table);
|
||||
#else
|
||||
if (_fs == NULL) return false;
|
||||
File file = openFloodSettingsWrite(_fs, FLOOD_CHANNEL_SCOPE_TEMP_FILE);
|
||||
if (!file) return false;
|
||||
@@ -6939,6 +7376,7 @@ bool MyMesh::saveFloodChannelScopes(bool empty_table) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::loadFloodChannelScopeRequirements() {
|
||||
@@ -9678,6 +10116,18 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
}
|
||||
|
||||
// handle ACL related commands
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
if (strcmp(command, "get flood.channel.data.hops") == 0) {
|
||||
formatFloodChannelDataHops(reply);
|
||||
} else if (strcmp(command, "get flood.channel.data") == 0) {
|
||||
formatFloodChannelData(reply);
|
||||
} else if (commandFamilyMatches(command, "set flood.channel.data.hops")) {
|
||||
setFloodChannelDataHops(
|
||||
command + strlen("set flood.channel.data.hops"), reply);
|
||||
} else if (commandFamilyMatches(command, "set flood.channel.data")) {
|
||||
setFloodChannelData(command + strlen("set flood.channel.data"), reply);
|
||||
} else
|
||||
#endif
|
||||
if (commandFamilyMatches(command, "get flood.channel.scope.require")) {
|
||||
formatFloodChannelScopeRequirements(
|
||||
command + strlen("get flood.channel.scope.require"), reply);
|
||||
|
||||
@@ -152,7 +152,9 @@ struct NeighbourInfo {
|
||||
#endif
|
||||
#ifndef FLOOD_PACKET_FILTER_SLOTS
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 31
|
||||
// Keep one extra forward-phase row available when the legacy
|
||||
// flood.channel.data gate is upgraded into an ordinary FPF7 rule.
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 32
|
||||
#else
|
||||
#define FLOOD_PACKET_FILTER_SLOTS 16
|
||||
#endif
|
||||
@@ -394,6 +396,13 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
flood_channel_scope_requirements[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS];
|
||||
FloodGroupModerationEntry flood_group_moderation[FLOOD_GROUP_MODERATION_SLOTS];
|
||||
uint32_t recv_pkt_filter_match_mask;
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
bool flood_policy_has_embedded_sections;
|
||||
// Zero-based forward-row slot owned by the flood.channel.data compatibility
|
||||
// facade. 0xFF means forwarding is enabled and no compatibility row exists.
|
||||
uint8_t flood_channel_data_rule_slot;
|
||||
uint8_t flood_channel_data_rule_max_hops;
|
||||
#endif
|
||||
ClockSyncSample clock_sync_samples[CLOCK_SYNC_SAMPLE_SLOTS];
|
||||
bool clock_sync_mesh_enabled;
|
||||
bool clock_sync_mesh_edge_enabled;
|
||||
@@ -583,11 +592,19 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
bool floodChannelDataHopApplies(const mesh::Packet* packet) const;
|
||||
bool loadFloodPacketFilters();
|
||||
bool saveFloodPacketFilters();
|
||||
bool saveFloodPacketFilters(bool empty_scope_phase = false);
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
void migrateLegacyFloodChannelBlocks();
|
||||
bool migrateLegacyFloodChannelBlocks();
|
||||
bool migrateLegacyFloodChannelData();
|
||||
bool importLegacyFloodPolicySections();
|
||||
int findFloodChannelDataRule() const;
|
||||
bool isFloodChannelDataRule(const FloodPacketFilterEntry& entry) const;
|
||||
void formatFloodChannelData(char* reply) const;
|
||||
void formatFloodChannelDataHops(char* reply) const;
|
||||
void setFloodChannelData(const char* value, char* reply);
|
||||
void setFloodChannelDataHops(const char* value, char* reply);
|
||||
#endif
|
||||
void loadFloodPacketFilterBlacklist();
|
||||
bool loadFloodPacketFilterBlacklist();
|
||||
bool saveFloodPacketFilterBlacklist();
|
||||
void seedDefaultFloodPacketFilters();
|
||||
bool floodPacketFilterBlacklistMatches(const mesh::Packet* packet) const;
|
||||
@@ -622,7 +639,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
void formatFloodPacketFilterBlacklist(const char* args, char* reply) const;
|
||||
void setFloodPacketFilterBlacklist(const char* args, char* reply);
|
||||
void deleteFloodPacketFilterBlacklist(const char* args, char* reply);
|
||||
void loadFloodChannelScopes();
|
||||
bool loadFloodChannelScopes();
|
||||
bool saveFloodChannelScopes(bool empty_table = false);
|
||||
bool applyFloodChannelScopeTarget(mesh::Packet* packet, const FloodChannelScopeEntry& entry,
|
||||
bool& scope_changed, bool& fast_track,
|
||||
|
||||
@@ -589,6 +589,27 @@ void FloodRuleEngine::load() {
|
||||
&& action_valid)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
if (success && version_7 && file.available() > 0) {
|
||||
uint8_t section_magic[4];
|
||||
uint8_t channel_data_slot = 0xFF;
|
||||
uint8_t channel_data_max_hops = 0xFF;
|
||||
uint8_t scope_count = 0;
|
||||
uint8_t direct_count = 0;
|
||||
uint8_t blacklist_count = 0;
|
||||
success = readExact(section_magic, sizeof(section_magic))
|
||||
&& memcmp(section_magic, "FPS1", sizeof(section_magic)) == 0
|
||||
&& readExact(&channel_data_slot, sizeof(channel_data_slot))
|
||||
&& readExact(&channel_data_max_hops,
|
||||
sizeof(channel_data_max_hops))
|
||||
&& readExact(&scope_count, sizeof(scope_count))
|
||||
&& readExact(&direct_count, sizeof(direct_count))
|
||||
&& readExact(&blacklist_count, sizeof(blacklist_count))
|
||||
&& channel_data_slot == 0xFF
|
||||
&& channel_data_max_hops == 0xFF
|
||||
&& scope_count == 0 && direct_count == 0
|
||||
&& blacklist_count == 0
|
||||
&& file.available() == 0;
|
||||
}
|
||||
file.close();
|
||||
if (!success) memset(_entries, 0, sizeof(_entries));
|
||||
@@ -706,6 +727,19 @@ bool FloodRuleEngine::save() {
|
||||
&& writeExact(&entry.priority, sizeof(entry.priority))
|
||||
&& writeExact(&stop_on_match, sizeof(stop_on_match));
|
||||
}
|
||||
const uint8_t section_magic[4] = {'F', 'P', 'S', '1'};
|
||||
const uint8_t no_channel_data_slot = 0xFF;
|
||||
const uint8_t channel_data_hops_all = 0xFF;
|
||||
const uint8_t empty_count = 0;
|
||||
success = success
|
||||
&& writeExact(section_magic, sizeof(section_magic))
|
||||
&& writeExact(&no_channel_data_slot,
|
||||
sizeof(no_channel_data_slot))
|
||||
&& writeExact(&channel_data_hops_all,
|
||||
sizeof(channel_data_hops_all))
|
||||
&& writeExact(&empty_count, sizeof(empty_count))
|
||||
&& writeExact(&empty_count, sizeof(empty_count))
|
||||
&& writeExact(&empty_count, sizeof(empty_count));
|
||||
file.close();
|
||||
if (!success || !verifyWrittenFile(
|
||||
_fs, RULE_TEMP_FILE, bytes_written, write_hash)) {
|
||||
|
||||
@@ -299,6 +299,20 @@ TEST(FloodRuleOrder, ThirtyOneSlotTableIncludesTheLastSlot) {
|
||||
matches, priorities, stop_flags, 31));
|
||||
}
|
||||
|
||||
TEST(FloodRuleOrder, ThirtyTwoSlotTableIncludesTheLastSlot) {
|
||||
uint8_t priorities[32] = {0};
|
||||
uint8_t stop_flags[32] = {0};
|
||||
priorities[31] = 200;
|
||||
stop_flags[31] = 1;
|
||||
const uint32_t matches = ((uint32_t)1U << 31) | 1U;
|
||||
|
||||
EXPECT_EQ(31, FloodFilterPolicy::nextOrderedRule(
|
||||
matches, 0, priorities, 32));
|
||||
EXPECT_EQ((uint32_t)1U << 31,
|
||||
FloodFilterPolicy::truncateRulesAtStop(
|
||||
matches, priorities, stop_flags, 32));
|
||||
}
|
||||
|
||||
TEST(FloodFilterScope, RegionRequirementHasTheExpectedTruthTable) {
|
||||
EXPECT_TRUE(FloodFilterPolicy::scopeRuleAllowed(false, false));
|
||||
EXPECT_TRUE(FloodFilterPolicy::scopeRuleAllowed(false, true));
|
||||
|
||||
Reference in New Issue
Block a user