Add per-profile TX routing and bridge filtering

Persist Companion TX preferences per contact and channel. Default repeater,
room, and sensor replies to both active TX profiles, with a reply-only force
option for an RX-only secondary profile. Track both reply copies before a
temporary-radio handoff and account for OTA copies under queue backpressure.

Add bridge/crossover filter modes and the third built-in wardriving filter,
plus compact filter CLI syntax, documentation, and CI coverage.

Fix quoted target names being interpreted as slot/key selectors, partial
recovery of invalid saved reply settings, filter suspension during tempradio2,
and unnecessary packet allocation while OTA traffic is throttled.

Validation: 1,477 native tests, 52 Python integration tests, 29 filter UI tests,
and five firmware builds covering ESP32, nRF52, and STM32 passed.
This commit is contained in:
mikecarper committed 2026-09-15 01:58:43 -07:00
1 parent 0e0aa8e014
commit 95aca52b4e
60 files changed
+3188 -362

No files matched your search

+17
View File
@@ -214,6 +214,7 @@
pathPrefix: "",
sender: "",
tempRadio: "any",
via: "radio",
verdict: "continue",
scopeGate: "unchanged",
targetKind: "scope",
@@ -257,6 +258,8 @@
pathKind: enumValue(input.pathKind, PATH_KINDS, "Path matcher", "none"),
pathPrefix: "",
sender: clean(input.sender),
via: enumValue(input.via === "cross,bridge" ? "bridge,cross" : input.via,
["radio", "bridge", "cross", "bridge,cross"], "Traffic path", "radio"),
tempRadio: enumValue(input.tempRadio, ["any", "active", "inactive"], "Temporary-radio matcher", "any"),
verdict: enumValue(input.verdict, ["continue", "drop"], "Forwarding verdict", "continue"),
scopeGate: enumValue(input.scopeGate, ["unchanged", "require_allowed", "bypass_global"], "Scope-gate action", "unchanged"),
@@ -356,6 +359,7 @@
if (rule.pathKind === "prefix") matches.push(`path=prefix:${rule.pathPrefix}`);
else if (rule.pathKind !== "none") matches.push(`path=${rule.pathKind}`);
if (rule.sender) matches.push(`sender=${quoteDsl(rule.sender)}`);
if (rule.via !== "radio") matches.push(`via=${rule.via}`);
if (rule.tempRadio !== "any") matches.push(`tempradio=${rule.tempRadio}`);
const actions = [];
if (rule.verdict === "drop") actions.push("drop");
@@ -464,6 +468,7 @@
rule.pathPrefix = value.slice(7);
} else rule.pathKind = value;
} else if (name === "sender") rule.sender = value;
else if (name === "via") rule.via = value;
else if (name === "tempradio") rule.tempRadio = value;
else throw new FilterToolError(`Unsupported receive-time matcher: ${token}`);
});
@@ -564,6 +569,7 @@
else if (rule.pathKind.startsWith("bucket:")) conditions.push(`a path matching ${rule.pathKind}`);
else if (rule.pathKind.startsWith("loop:")) conditions.push(`the ${rule.pathKind.slice(5)} own-ID loop threshold`);
if (rule.sender) conditions.push(`decrypted sender "${rule.sender}"`);
conditions.push(`traffic path ${rule.via}`);
if (rule.tempRadio !== "any") conditions.push(`temporary radio ${rule.tempRadio}`);
const mode = rule.mode === "shadow"
? "In shadow mode, report that it would "
@@ -970,6 +976,7 @@
regionName: clean(input.regionName),
sender,
tempRadio: Boolean(input.tempRadio),
via: enumValue(input.via, ["radio", "bridge", "cross"], "Packet traffic path", "radio"),
blacklist: Boolean(input.blacklist),
buckets: Array.from(new Set(buckets)),
loopLevel: requiredInteger(input.loopLevel == null ? 0 : input.loopLevel, 0, 3, "Loop result"),
@@ -1002,6 +1009,7 @@
const rule = normalizeRule(inputRule);
const packet = normalizePacket(inputPacket);
const misses = [];
if (!rule.via.split(",").includes(packet.via)) misses.push(`traffic path is ${packet.via}`);
if (rule.route === "flood" && !["unscoped_flood", "scoped_flood"].includes(packet.route)) misses.push("route is not flood");
else if (rule.route !== "flood" && rule.route !== packet.route) misses.push(`route is ${packet.route}`);
if (!typeMatches(rule.type, packet.type)) misses.push(`payload type ${packet.type} is outside ${rule.type}`);
@@ -1228,6 +1236,12 @@
owner: "system",
channel: "#wardriving",
},
{
...documentedDropRule("wardriving-bridge-cross", "any", "all", 220),
owner: "system",
channel: "#wardriving",
via: "bridge,cross",
},
]),
});
@@ -1302,6 +1316,7 @@
pathPrefix: field("path-prefix").value,
sender: field("sender").value,
tempRadio: field("temp-radio").value,
via: field("via").value,
verdict: field("verdict").value,
scopeGate: field("scope-gate").value,
targetKind: field("target-kind").value,
@@ -1335,6 +1350,7 @@
"path-prefix": rule.pathPrefix,
sender: rule.sender,
"temp-radio": rule.tempRadio,
via: rule.via,
verdict: rule.verdict,
"scope-gate": rule.scopeGate,
"target-kind": rule.targetKind,
@@ -1564,6 +1580,7 @@
regionName: packetField("region-name").value,
sender: packetField("sender").value,
tempRadio: packetField("temp-radio").checked,
via: packetField("via").value,
blacklist: packetField("blacklist").value === "yes",
buckets: packetField("buckets").value,
loopLevel: packetField("loop-level").value,
+166 -24
View File
@@ -743,6 +743,54 @@ set flag bit 0.
---
## Set Companion TX routing per contact or channel
```text
set tx.user "Alice Jones" radio2
get tx.user "Alice Jones"
set tx.channel 0 both
get tx.channel 0
set tx.channel #wardriving radio
```
Both command families accept `auto`, `radio`, `radio2`, `both`, or `off`.
`auto` uses the global crossing rules. Explicit choices override
`radio2.cross`, but radio2 must still be enabled in `rxtx` to transmit there.
`radio2` never falls back to the primary profile; `both` uses whichever of its
selected profiles can transmit. Set a target back to `auto` to remove its
override.
Use an exact contact/channel name, a zero-based channel index, or
`key:<12-64 hex digits>` for a unique contact public-key prefix. Duplicate
names/prefixes are rejected. `get tx.user` and `get tx.channel` show override
counts; queries with a target show its saved choice and current effective
profile selection. Choices are saved with the contact/channel and work for
phone-app and terminal messages. A contact's `off` setting also blocks its
ACKs and addressed responses.
See [Companion routing details and examples](radio_profiles.md#companion-tx-routing-by-contact-or-channel)
for temporary profiles, persistence, and the traffic covered by each setting.
## Set repeater, room-server, and sensor reply profiles
```text
get tx.reply
set tx.reply both
set tx.reply both force
set tx.reply radio
set tx.reply auto
```
Persistent choices: `auto`, `radio`, `radio2`, `both` (default), and `off`.
Explicit choices override `radio2.cross` for local replies, including CLI,
ACKs, telemetry, room subscription posts, and served LoRa OTA responses.
Append `force` to `both` or `radio2` to permit reply TX on an active RX-only
second profile. It never enables a disabled profile or ordinary forwarding.
Omitting `force` removes that override. `off` suppresses remote replies too.
See [reply routing and OTA examples](radio_profiles.md#repeater-room-server-and-sensor-replies)
for temporary profiles, receive-only exceptions, and queued reply handling.
## Set Companion display rotation
SSD1306 Full Companion builds support a persisted runtime orientation:
@@ -2428,6 +2476,42 @@ setting.
---
#### Bridge and radio crossover filters
`mode=` selects where a generalized `flood.rule` / `flood.filter` row applies:
| Mode | Where the rule applies |
| --- | --- |
| `radio` | Ordinary LoRa flood forwarding. This is the default when `mode=` is omitted. |
| `bridge` | Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. |
| `cross` | Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. |
| `bridge,cross` | Both transport paths using one rule. `cross,bridge` is equivalent. |
The third built-in repeater rule blocks authenticated `#wardriving` text and
data at all hops on bridge and crossover paths, including temporary-radio
sessions. It leaves same-profile traffic subject to the separate four-hop
rule. `set radio2.cross on` does not override this filter.
```text
# Factory slot: inspect before replacing an existing rule.
get flood.rule.3
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
# On an existing device, add it without overwriting an occupied slot.
set flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop
```
Saved tables are preserved on upgrade; new defaults are seeded only when the
table is first initialized. Bridge/cross modes accept drop, rate, priority and
stop actions; scope rewrites, retry actions and TX timing are rejected.
These modes cover direct as well as flood packets, in both crossing directions.
The default authenticates the channel key, avoiding false drops from short
channel-hash collisions. FULL room servers support explicit transport rules;
companions and compact FPF6 profiles do not expose this table.
See [filtering documentation](flood_filtering.md#filter-bridge-and-radio-crossover-traffic)
for persistence, queue/rate semantics, exceptions, and how to remove one or
both blocks.
#### Block selected flood channels with FPF7
The separate `flood.channel.block` command and 15-row table have been retired.
@@ -2714,9 +2798,13 @@ For setup guidance, interactions with the existing forwarding controls, and
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|retry|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]`
- `get fr[.<n>]` (numbered form returns a complete short setter for copying)
- `set fr[.<n>] <type> [h=<range>] [m=r|b|c|bc] [c=<channel>] <d|s=<scope>|r=<region>|q=<N>|r|s> [...]`
- `del fr.<n>` or `del fr all`
- `set flood.rule[.<n>] type=<type> [mode=radio|bridge|cross|bridge,cross] [hops=<range>] [channel=<channel>] [prefix=<path-prefix>] [in=<input-scope>] <drop|scope=<name>|region=<name>|rate=<N>/min|retry|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]`
- `del flood.rule.<n>`
- `del flood.rule all`
- `get flood.filter`
@@ -2733,16 +2821,21 @@ worked moderation examples, see [Flood Filtering and Moderation](flood_filtering
- `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
on FULL-profile ESP32 room servers. A FULL room server exposes
`fr`, `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`: Forward-rule slot in the build's compiled table (`1-63` on generalized
repeaters and `1-31` on FULL room servers; compact profiles may use fewer).
- `mode`: Optional; omitted means `radio`. `bridge` filters bridge admission,
`cross` filters copies to the other radio profile, and `bridge,cross` covers
both with one row. `cross,bridge` is an alias. See the
[mode matrix](#bridge-and-radio-crossover-filters) for scope and supported actions.
- `type`: Payload type name, full `PAYLOAD_TYPE_*` name, decimal value `0-15`,
hexadecimal value `0x00-0x0F`, or `any`.
- `hops`: Optional; omitted means `all`.
@@ -2806,8 +2899,8 @@ compile this table.
wildcard, or has no usable transport key, both the rewrite and its `stop`
are inert so lower-order safety rows still run. A direct `scope=` target does
not depend on region configuration.
- `suspend=tempradio`: Optional. Skip this row only while the temporary radio
is actually active.
- `suspend=tempradio`: Optional. Skip this row while either `tempradio` or
`tempradio2` is active, including scheduled temporary sessions.
- `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
@@ -2847,14 +2940,17 @@ The payload names follow the [MeshCore packet-format allocation](https://docs.me
| `0x0E` | `14` | reserved |
| `0x0F` | `raw_custom` | `PAYLOAD_TYPE_RAW_CUSTOM` |
**Route scope:** Rules are evaluated only for the two flood route values:
**Route scope:** `mode=radio` rules are evaluated only for the two flood route values:
`ROUTE_TYPE_TRANSPORT_FLOOD` (`0x00`, flood plus transport codes) and
`ROUTE_TYPE_FLOOD` (`0x01`, unscoped flood). Direct routes `0x02` and `0x03`
are never affected.
are unaffected by radio rules. Bridge/cross rules also cover direct routes at
their respective transport boundaries.
**Behavior:** Match fields within one row are ANDed. Every FPF7 row is matched
**Behavior:** Match fields within one row are ANDed. Radio rows are matched
against the same immutable receive-time packet, before any rule changes its
scope. Matching rows are processed in descending `priority`. At equal numeric
scope. Bridge/cross rows match the packet at their admission boundary; a TX
packet can already have an appended hop or rewritten scope. Each mode evaluates
only its applicable rows, processed in descending `priority`. At equal numeric
priority, authenticated channel matches precede raw hashes, which precede an
unrestricted channel matcher; lower slot wins after that. The first matching
`stop` row is included and all lower-order FPF7 matches are discarded. A stop
@@ -2903,10 +2999,11 @@ 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).
While either `tempradio` or `tempradio2` is active, only rows explicitly marked
`suspend=tempradio` are skipped. Temporary radio is a radio state, not an OTA
mode; normal payload types can also use the temporary channel. Other rows
remain in force, and suspended rows resume after both temporary sessions end.
A malformed persisted table fails open (no general rules are applied).
Within one receive evaluation, rows that use the same channel key share one
authentication result. The cache is discarded after that packet and stores
@@ -2951,10 +3048,10 @@ Deleting or replacing the last active `retry` row restores the legacy global
retry eligibility. Firmware that predates the `retry`/`hash:XX` FPF7 extension
cannot preserve tables containing those rows; remove them before downgrading.
**Default row:** Repeater firmware and FULL ESP32 room-server firmware seed a
**Default rows:** 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
floods at every received hop unless either temporary radio profile is active. The OTA
core independently refuses OTA receive, relay, and transmit outside temporary
radio. The row is editable and deletable; once the table is saved, deletion is
persistent. Restore the exact seeded row with:
@@ -2966,6 +3063,18 @@ 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.
Generalized repeaters also seed slot 2 with the authenticated `#wardriving`
radio hop limit and slot 3 with the bridge/crossover block:
```text
set flood.rule.2 type=any channel=#wardriving hops=5+ drop
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
```
These numbered commands replace those slots. Saved tables are preserved on
upgrade; use the unnumbered third command to add the new rule without replacing
an occupied slot. See [restoring the three defaults](flood_filtering.md#restore-the-factory-seeded-rows).
**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
@@ -2980,14 +3089,47 @@ or action.
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=str`
combines slow timing (`s`), temporary-radio suspension (`t`), and retry
allowance (`r`). Packet type is shown numerically
in that fallback. Normal-sized rows keep the descriptive spelling above.
`fr` is a shorter alias for `flood.rule`, with the same explicit-action
requirement. Short and long field names can be mixed in either command:
```text
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
set fr.3 any c=#wardriving m=bc d
get fr.3
```
The two setters above save the same row. `get fr.3` returns the complete
short setter; `get fr` lists the table. Unnumbered `set fr ...` reuses an
identical row or adds one to the first empty slot. `del fr.N` and `del fr all`
have the same behavior as their long forms.
| Full token | Short token |
| --- | --- |
| `type=...` | Positional type or `t=...`; `*` means `any` |
| `hops=...` | Positional range or `h=...`; `*` means `all` |
| `mode=radio/bridge/cross/bridge,cross` | `m=r/b/c/bc` (choose one value) |
| `channel=...`, `prefix=...` | `c=...`, `p=...` |
| `path=blacklist` | `p=bl` (repeater only) |
| `in=any/none/scoped/allowed/unknown` | `i=*/n/s/a/u` (choose one value) |
| `in=scope:name`, `in=region:name` | `i=s:name`, `i=r:name` |
| `drop`, `stop`, `retry` | `d`, `s`, `r` |
| `scope=name`, `region=name` | `s=name`, `r=name` |
| `rate=N/min`, `priority=N` | `q=N`, `pri=N` |
| `tx=slow`, `suspend=tempradio`, `retry` | `f=s`, `f=t`, `f=r`; combine as `f=str` |
All existing validation and permissions apply. `m=cb`, `m=b,c`, and `m=c,b`
also mean bridge and crossover. Short aliases for `flood.channel.scope`,
`flood.moderation`, and blacklist management are not added.
The numbered long getters use this complete short setter when descriptive
fields would exceed a reply. Defaults are omitted and specific payload types
are printed numerically. If it still cannot fit, the reply is
`Err - compact command exceeds reply size`, never a partial setter.
Private channel keys are shown as `key:XXXXXXXX`; that reference can be
reused while an active rule on the same device holds the key. Copying it to
another device requires the original key, unless that device already has a
matching rule. Unknown or ambiguous references are rejected. See the
[shorthand guide](flood_filtering.md#shorthand-for-copying-rules) for examples.
On generalized repeaters, filter rows, scope-rewrite rows, the shared
blacklist, and `flood.channel.data` compatibility state are committed in one
@@ -3135,7 +3277,7 @@ 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.filter*`, `flood.rule*`, and
`flood.filter*`, `flood.rule*`, `fr[.N]`, 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`
+26
View File
@@ -131,3 +131,29 @@ For every command's full details, see the [bridge CLI reference](cli_commands.md
For a broader comparison with a Linux-based bridge, see the
[MeshCore Nexus bridging guide](https://meshcore.nexus/guides/lora-bridging#method-2-meshcore-esp-now-bridge).
## Keep wardriving traffic off the bridge
New generalized repeater filter tables include a third built-in rule that
blocks authenticated `#wardriving` text and data in both bridge directions,
at every hop count. The same row blocks crossover between radio profiles:
```text
get flood.rule.3
set flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop
```
The same rule can be added with the shorter command:
```text
set fr any c=#wardriving m=bc d
```
Both forms use the same table. `m=b` means bridge only, `m=c` means crossover
only, and `get fr.N` returns a short command for copying a numbered rule.
The unnumbered command adds the rule safely on existing devices whose saved
tables predate this default. It works with either bridge packet source and
both ESP-NOW formats. Nearby same-profile LoRa forwarding retains the separate
four-hop default. Use `mode=bridge` for a bridge-only block, or `mode=cross` for
a crossover-only block. See the [mode matrix and examples](flood_filtering.md#filter-bridge-and-radio-crossover-traffic).
+38 -4
View File
@@ -13,7 +13,7 @@ drafts are not uploaded anywhere.
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
is configured with <code>set flood.*</code> or <code>set fr</code> commands and cannot install a
bundle from this page.
</p>
</div>
@@ -111,8 +111,8 @@ in the simulator below. The examples draw from
<code>when type=any hops=all path=blacklist do drop</code>
</button>
<button type="button" data-example="factory">
<span>Drop OTA outside temporary-radio mode and distant #wardriving</span>
<code>when type=ota hops=all tempradio=inactive do drop<br>when type=any channel=#wardriving hops=5+ do drop</code>
<span>Three defaults: OTA, distant #wardriving, and bridge/crossover #wardriving</span>
<code>when type=ota hops=all tempradio=inactive do drop<br>when type=any channel=#wardriving hops=5+ do drop<br>when type=any channel=#wardriving hops=all via=bridge,cross do drop</code>
</button>
<button type="button" data-example="wildcards">
<span>Set #BlackHole86 scope on login and bucket-matched other traffic</span>
@@ -131,6 +131,12 @@ in the simulator below. The examples draw from
<button type="button" data-role="reset-form">Reset form</button>
</div>
<p class="filter-field-help">The factory example contains all three repeater defaults.
The playground's <code>via=</code> traffic-path matcher corresponds to firmware
<code>mode=radio|bridge|cross|bridge,cross</code>; the advanced Active/Shadow/Disabled
mode is a separate playground setting. See the
<a href="../flood_filtering/#filter-bridge-and-radio-crossover-traffic">firmware mode guide</a>.
This simulator models flood packets; firmware bridge/cross rules also cover direct routes.</p>
<h3>Common match settings</h3>
<div class="filter-form-grid">
<label>
@@ -204,6 +210,15 @@ in the simulator below. The examples draw from
Decrypted sender (optional)
<input data-field="sender" maxlength="31" placeholder="Noisy User">
</label>
<label>
Traffic path (firmware mode)
<select data-field="via">
<option value="radio">Normal radio forwarding</option>
<option value="bridge">Bridge entry/exit</option>
<option value="cross">Radio profile crossover</option>
<option value="bridge,cross">Bridge and crossover</option>
</select>
</label>
<label>
Temporary-radio state
<select data-field="temp-radio">
@@ -439,6 +454,14 @@ in the simulator below. The examples draw from
</div>
<div class="filter-form-grid filter-simulator-facts">
<label>
Packet traffic path
<select data-packet="via">
<option value="radio">Normal radio forwarding</option>
<option value="bridge">Bridge entry/exit</option>
<option value="cross">Radio profile crossover</option>
</select>
</label>
<label>
Received route
<select data-packet="route">
<option value="unscoped_flood">Unscoped flood</option>
@@ -609,7 +632,7 @@ The class column shows which broader selector also matches it.
| Firmware command | FPF7 role |
| --- | --- |
| `flood.rule` / `flood.filter` | Forward-phase match and action rows |
| `fr` / `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 |
@@ -620,6 +643,17 @@ 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.
The firmware accepts both full and short CLI forms for the same forward row:
```text
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
set fr.3 any c=#wardriving m=bc d
```
Use `get fr.3` on the device to obtain a short setter ready to copy. This CLI
syntax is separate from the playground's proposed policy/bundle format.
See the [shorthand reference](flood_filtering.md#shorthand-for-copying-rules).
## Proposed evaluation contract
The simulator uses these rules:
+188 -24
View File
@@ -158,8 +158,9 @@ 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
authenticated `#wardriving hops=5+` radio drop in slot 2, and an authenticated
`#wardriving hops=all mode=bridge,cross` drop in slot 3. FULL room servers seed
only the OTA row; their generalized engine also accepts transport-mode rules. `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
@@ -400,14 +401,17 @@ scope arbitration.
## Runtime flood rules
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
servers, `fr`, `flood.rule`, and `flood.filter` name 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.
Older firmware cannot preserve an FPF7 table containing the packed `retry`
action or one-byte `hash:XX` matcher; remove those rows before downgrading.
action, one-byte `hash:XX` matcher, or `mode=bridge`/`cross` flags; remove those
rows before downgrading. Old firmware rejects such a table and fails open,
which disables its other filter rows too. New firmware loads older rows as
`mode=radio` without changing their behavior.
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
@@ -434,7 +438,8 @@ reactivates the rule.
The extended form is:
```text
set flood.rule[.<slot>] type=<type> [hops=<range>] [channel=<channel>]
set flood.rule[.<slot>] type=<type> [mode=radio|bridge|cross|bridge,cross]
[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
@@ -444,13 +449,18 @@ 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
Radio rows are matched against the same immutable packet state captured on
receive, before any rule rewrites its scope. Bridge/cross rows match at their
transport boundary. Only rows for the current mode participate in ordering
and stop processing. Matching rows are then processed
by descending numeric `priority`. At an equal numeric priority, authenticated
channel matches run before raw `hash:XX` matches, raw hashes run before
`channel=*`, and lower slot number breaks the remaining tie. Priority defaults
to `0`; an explicitly higher numeric priority overrides specificity.
Numbered examples replace the selected slots, including factory defaults.
Omit the slot number to add a rule without replacing an occupied slot.
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
@@ -464,6 +474,8 @@ Match fields:
`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+`.
- `mode=radio|bridge|cross|bridge,cross` selects the admission phase; omitted
means `radio`. See [bridge and crossover filtering](#filter-bridge-and-radio-crossover-traffic).
- `channel=*|public|#name|hash:XX|128-bit-key|256-bit-key` optionally narrows by
channel. `channel=*` is an unconstrained wildcard: it performs no channel
authentication and matches every payload selected by `type=`. Thus
@@ -636,13 +648,77 @@ 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`).
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=str` combines slow timing (`s`), temporary-radio
suspension (`t`), and retry allowance (`r`). The fallback prints packet type
numerically.
Numbered `get flood.rule.N` and `get flood.filter.N` normally use descriptive
field names. On generalized builds, a reply that would be too long uses the
complete `set fr.N ...` spelling below. `get fr.N` always requests that spelling.
If even the short command exceeds one reply, it returns
`Err - compact command exceeds reply size` instead of a partial command.
Saving such a rule still succeeds and reports that its details exceed the
reply size.
### Shorthand for copying rules
On generalized repeaters and FULL ESP32 room servers, `fr` is a shorter name
for `flood.rule`. Both forms use the same parser, validation, permissions, and
saved rows. Short and full field names can be mixed in either command.
`fr` requires an explicit action, just like `flood.rule`.
These commands set exactly the same rule:
```text
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
set fr.3 any c=#wardriving m=bc d
```
| Full form | Short form | Meaning |
| --- | --- | --- |
| `flood.rule[.N]` | `fr[.N]` | Same rule table for `get`, `set`, and `del`. |
| `type=any` | `any` or `*` | First token: any payload type. Named/numeric types also work, with optional `t=`. |
| `hops=N`, `N+`, `N-M`, or `all` | `h=N`, `h=N+`, `h=N-M`, or `h=*` | Hop condition; a bare range also works. Omitted means all hops. |
| `mode=radio`, `bridge`, `cross`, `bridge,cross` | `m=r`, `m=b`, `m=c`, `m=bc` | Select the admission path. Omitted means radio; `cb`, `b,c`, and `c,b` also mean both transport paths. |
| `channel=...` | `c=...` | Same authenticated name/key or explicit `hash:XX` matcher. |
| `prefix=...` | `p=...` | Ordered path prefix. |
| `path=blacklist` | `p=bl` | Shared unordered blacklist; repeater only. |
| `in=any`, `none`, `scoped`, `allowed`, `unknown` | `i=*`, `i=n`, `i=s`, `i=a`, `i=u` | Incoming scope condition. |
| `in=scope:name`, `in=region:name` | `i=s:name`, `i=r:name` | Match a specific incoming scope or region. |
| `drop` | `d` | Drop matching traffic. |
| `scope=name`, `region=name` | `s=name`, `r=name` | Rewrite to a public scope or configured region. |
| `rate=N/min` | `q=N` | Per-row forwarding allowance. |
| `priority=N` | `pri=N` | Higher values run first. |
| `stop`, `retry` | `s`, `r` | Stop later rules or allow the configured flood retry. |
| `tx=slow`, `suspend=tempradio`, `retry` | `f=s`, `f=t`, `f=r` | Flags can combine, for example `f=str`. |
The `=` distinguishes `s=name` (scope) from `s` (stop), and `r=name`
(region) from `r` (retry). Short forms keep the same action restrictions;
for example, `m=bc r` is rejected because transport rules cannot enable retries.
```text
get fr.3
```
Returns a complete command ready to copy:
```text
set fr.3 any m=bc c=#wardriving d
```
The compact reply omits default values and prints specific payload types as
numbers. `get fr` lists the table. `set fr ...` without a slot reuses an
identical rule or adds it to the first empty slot. `del fr.3` removes slot 3;
`del fr all` clears the forward rules.
Private keys are still hidden in replies. A displayed `c=key:XXXXXXXX` is a
local reference to the exact key in an active rule on that device, so it can
be pasted to edit or copy a rule there. On another device, or after deleting
the last rule holding that key, supply the original key instead. An unknown
or ambiguous reference is rejected; it never becomes an unauthenticated
channel-hash match.
The shorthand does not rename `flood.channel.scope`, `flood.moderation`, or
the blacklist-management commands. Companions and compact FPF6 profiles do
not expose `fr`.
### Scope actions and legacy rules
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
@@ -695,8 +771,9 @@ This is a normal editable row. After the table has been saved, deleting it
remains persistent across reboot; the firmware does not recreate it. Run the
same command to restore the exact seeded row, or omit `.1` to preserve existing
slot assignments and use the first empty slot. Operators may add
`suspend=tempradio` to any other row that should be skipped while the radio is
on a temporary channel.
`suspend=tempradio` to any other row that should be skipped while either
`tempradio` or `tempradio2` is active. This includes scheduled temporary
profiles. The row resumes once both temporary sessions have ended.
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
@@ -705,7 +782,9 @@ 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
Standard traceroute uses direct routing and bypasses `mode=radio` filters.
Explicit bridge/cross filters can also match direct traffic at those transport
boundaries; the built-in channel rule does not match TRACE packets. For a
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.
@@ -788,13 +867,15 @@ get flood.filter
| `path 9+` | 9 or more path entries |
| `control 1+` | 1 or more path entries |
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
On a new generalized repeater table, the three factory rules occupy slots
1 through 3, so these unnumbered commands normally fill slots 4 through 9.
FULL room servers and compact tables seed only OTA and use slots 2 through 7.
Existing tables may choose different
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
are direct zero-hop packets and bypass these `mode=radio` rules. These rules affect
only retransmission by the repeater; local reception and logging remain
unchanged.
@@ -922,7 +1003,8 @@ other words, the controls combine as deny rules:
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
can still appear in local logs or MQTT output, unless a separate
`mode=bridge` rule blocks MQTT packet publication. Moderation runs last because its
rate counters are charged only for packets that pass every other forwarding
control and will actually be retransmitted.
@@ -936,7 +1018,7 @@ 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.filter*`, `flood.rule*`, and `flood.moderation*`. Delegated `get` access uses an
`flood.filter*`, `flood.rule*`, `fr[.N]`, 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
@@ -944,7 +1026,7 @@ 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
`fr`, `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
@@ -965,6 +1047,79 @@ Use username and path rules as traffic moderation, not as an authorization
boundary. For a strict network boundary, combine these tools with region ACLs,
private transport/channel keys, and controlled device access.
## Filter bridge and radio crossover traffic
The `mode=` matcher selects a separate admission phase within the same
`flood.rule` / `flood.filter` table:
| Mode | Where the rule applies |
| --- | --- |
| `radio` | Ordinary LoRa flood forwarding. This is the default when `mode=` is omitted. |
| `bridge` | Packets entering or leaving an RS232 or ESP-NOW bridge, and MeshCore RX/TX packet publication through MQTT. |
| `cross` | Transmissions copied to the other radio profile, including a copy's retries. The same-profile transmission remains eligible. |
| `bridge,cross` | Both transport paths using one rule. `cross,bridge` is equivalent. |
The third built-in repeater filter is:
```text
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
get flood.rule.3
```
The equivalent short command is `set fr.3 any c=#wardriving m=bc d`.
Use `m=b` for bridge only or `m=c` for crossover only.
It authenticates both `GRP_TXT` and `GRP_DATA` against the public `#wardriving`
channel key. A different channel with the same one-byte hash is not blocked.
It covers every hop count, including zero-hop/local messages, flood routes and
direct routes. It remains active during temporary-radio sessions and with
`radio2.cross on`: enabling crossover makes a copy eligible, but does not
override the filter. The second built-in rule still permits ordinary
`#wardriving` forwarding through four received hops; the third rule keeps
those messages on their originating radio profile.
Bridge filtering runs in both directions before bridge deduplication, inbound
queueing, serial writes, ESP-NOW queueing, or MQTT packet publication. It
applies with either `bridge.source rx` or `bridge.source tx` and with wrapped or raw
ESP-NOW framing. MQTT here is the observer packet output; this does not add
MQTT-to-LoRa injection or filter MQTT status/telemetry. Local reception and
local packet logs remain available. Switching one radio between profiles is
covered by `cross`; the bridge's WiFi channel is a separate setting.
Transport modes support `drop`, `rate=X/min`, `priority=`, and `stop`, with the
existing type, channel, hops, prefix, blacklist, incoming-scope and
`suspend=tempradio` matchers. Scope/region rewrites, retry actions, and TX
timing actions are rejected in these modes. Ordering and `stop` apply only to
rules in the current mode: a radio exception cannot bypass a bridge/cross
drop. A combined rule shares one rate counter across bridge and cross
admission attempts; retries count too. A rejected queue insertion can consume
an admission allowance. Queued packets already admitted are not recalled by
editing a rule. Same-profile transmissions use the ordinary radio policy.
`get flood.rule.N` shows `mode=`; the overview marks each row with its mode.
Modes are persistent, case-insensitive, and apply immediately to new admission
attempts. `radio` cannot be combined with `bridge` or `cross` in a single row.
For transport rules, hop and scope matchers see the packet at that boundary:
a TX/crossover packet can already contain an appended hop or rewritten scope.
The built-in rule uses all hops and any scope, so these changes cannot bypass it.
On FULL room servers, add a transport rule explicitly; their factory table
still has only the OTA row. Companions and compact FPF6 builds do not expose
this generalized filter table.
Saved tables, occupied slots, and deleted defaults are preserved on upgrade.
To add the new default to an existing table without replacing another rule,
omit the slot number (repeating the identical command reuses its row):
```text
set flood.rule type=any channel=#wardriving hops=all mode=bridge,cross drop
get flood.rule
```
To allow only bridge traffic while retaining the crossover block, replace the
third rule with `mode=cross`. Use `mode=bridge` to retain only the bridge block.
Use `del flood.rule.3` to remove both, after checking that slot 3 is still the
built-in rule. Removing it does not change the separate LoRa hop-limit rule.
## Restore the factory-seeded rows
The repeater's factory-seeded forwarding rows can be restored through the CLI:
@@ -972,9 +1127,18 @@ The repeater's factory-seeded forwarding rows can be restored through the CLI:
```text
set flood.rule.1 type=ota hops=all drop suspend=tempradio
set flood.rule.2 type=any channel=#wardriving hops=5+ drop
set flood.rule.3 type=any channel=#wardriving hops=all mode=bridge,cross drop
```
These commands explicitly replace the two seeded generalized-repeater slots.
Or use the equivalent short forms:
```text
set fr.1 ota d f=t
set fr.2 any 5+ c=#wardriving d
set fr.3 any c=#wardriving m=bc d
```
These commands explicitly replace the three 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.
+9 -1
View File
@@ -87,7 +87,7 @@ set flood.retry.ignore none
| `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. Generalized repeaters have 63 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.filter` | Persistent flood-route rules selected by payload type and optional hop range. Generalized repeaters have 63 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` slot 2 with an authenticated `#wardriving hops=5+` radio drop, and slot 3 with `#wardriving hops=all mode=bridge,cross drop`. `mode=radio` is the default; `bridge`, `cross`, and `bridge,cross` select separate transport admission paths, including direct packets. Local receive/logging is unchanged. See the [mode guide](flood_filtering.md#filter-bridge-and-radio-crossover-traffic). | `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 match, 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, allow flood retry, and/or stop lower-priority FPF7 rules. `channel=*` means no channel condition; `public`, `#name`, and keys authenticate; `hash:XX` is an unauthenticated one-byte fallback with collision/spoof risk. With no `retry` rows, global retry behavior is unchanged; once any exists, matching `retry` rows allow-list received floods while the global retry gates and `flood.retry.bridge` algorithm still apply. `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 numeric `priority` runs first; equal values automatically order authenticated channels before raw hashes before `channel=*`, then use lower slot as the final tie-break. 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|retry|stop> [priority=0-255]`, `del flood.rule.<n>|all` | `set flood.rule.2 type=any channel=#hamradio retry` |
| `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 all repeater, sensor, and room-server 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` |
@@ -98,6 +98,14 @@ set flood.retry.ignore none
| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath <hops>`, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` |
| `altpath` | Adds a secondary direct route for repeater replies to the current remote client. | `get altpath`, `set altpath <hops>`, `set altpath direct`, `set altpath clear`, `set altpath flood` | `set altpath 71CE82,BA09F0` |
The generalized filter table also accepts `fr` as a short alias for
`flood.rule`: `set fr.3 any c=#wardriving m=bc d` is the full third built-in
rule. Modes can be `m=r` (radio), `m=b` (bridge), `m=c` (crossover), or `m=bc`
(both transport paths). `get fr.N` returns a short setter ready to copy;
`del fr.N` deletes that row. Full names and short tokens can be mixed, with
the same validation and permissions. See the
[shorthand reference](flood_filtering.md#shorthand-for-copying-rules).
## Other Keymind Commands
| Command | What it does | How to use | Example |
+148 -5
View File
@@ -29,7 +29,7 @@ denominator, mode, optional preamble in symbols**. `5` means coding rate 4/5.
| Mode | Receive on profile 2 | Transmit on profile 2 |
| --- | --- | --- |
| `rx` | Yes | No |
| `rx` | Yes | Only infrastructure replies explicitly configured with `tx.reply ... force` |
| `rxtx` (also `rx&tx`) | Yes | Yes |
| `off` | No | No |
@@ -53,7 +53,8 @@ get tempradio2
```
This enables the second profile for 120 minutes (2 hours). Locally generated
LoRa OTA packets use the temporary second profile. Ordinary local traffic stays
LoRa OTA requests use the temporary second profile. Infrastructure OTA responses
follow `tx.reply` below (both TX-capable profiles by default). Ordinary local traffic stays
on the primary profile by default. Use an OTA-capable build and the usual OTA
setup on every participating node; a temporary profile does not add OTA support
to firmware built without it.
@@ -62,6 +63,67 @@ New settings take effect after a short reply allowance. Temporary periods and
schedules live in RAM and disappear on reboot. Timer expiry also uses monotonic
time, so setting the clock backwards cannot extend a temporary session.
## Repeater, room-server, and sensor replies
These roles default to **`tx.reply both`**: locally generated replies are queued
on `radio` and on an active `radio2` that permits TX. This includes replies to
remote CLI/login/telemetry requests, ACKs, returned paths, delayed GPIO command
completion, room subscription posts, and locally served LoRa OTA catalog,
manifest, data, proof, and leaves responses. Each copy has its own retry state.
Forwarded packets, periodic adverts, sensor pushes, and OTA requests continue
to use the normal crossing rules.
```text
get tx.reply
set tx.reply both
set tx.reply both force
set tx.reply radio
set tx.reply auto
```
| Choice | Replies transmit on |
| --- | --- |
| `both` | Primary plus an active `rxtx` second profile. Default. |
| `radio` | Primary only. |
| `radio2` | Second only; no primary fallback. |
| `auto` | Normal `radio2.cross` behavior, including permanent/temporary isolation. |
| `off` | Neither profile. This also suppresses remote CLI replies and ACKs. |
Append **`force`** to `both` or `radio2` to allow these replies on an active
second profile configured as `rx`. Without `force`, `rx` remains receive-only.
Sending the command again without `force` removes the override. It never turns
on `radio2 off`, changes the saved `rx` mode, or enables ordinary forwarding/TX
there. Existing crossover packet filters still apply to the extra copy.
The explicit choices override `radio2.cross`, including `off` and mixed
permanent/temporary profiles. `radio` means the active primary profile,
including `tempradio`; `radio2` also includes an active `tempradio2`.
For example, an OTA source can retain its normal channel while allowing OTA
and management responses on a temporary receive profile:
```text
set tempradio2 910.5,500,8,5,rx,120
set tx.reply both force
```
The source must have OTA support and an active temporary-radio session as
usual. A node fetching an update still needs `rxtx` for its OTA requests.
Forcing replies does not change OTA's receive gates, scan timing, or preambles.
This setting survives reboot; older saved profiles adopt `both` without force.
The getter reports when the chosen second profile cannot transmit. Already
queued replies retain their choice; disabling/changing a profile invalidates
stale queued packets. A parameterized repeater `tempradio` command waits for
both admitted reply copies to drain before changing the primary settings;
at least one must transmit successfully. Those tracked replies omit alternate
paths and transport retries so no late success copy can outlive the handoff.
Queue capacity and the shared transceiver's airtime budget still limit delivery.
OTA data/proof admission counts both copies against its queue credit and
reserves receive capacity before allowing the response.
Companions use the per-contact/channel choices below; `tx.reply` is an
infrastructure setting.
## Companion messages on both profiles
To send ordinary Companion messages on both `radio` and an active `tempradio2`,
@@ -83,6 +145,83 @@ ends or the node reboots. Run `set radio2.cross auto` to restore the default
isolation between permanent and temporary profiles. The firmware picker's
Companion tempradio2 instructions include both choices and the status commands.
## Companion TX routing by contact or channel
Companions can save a transmit choice for each contact and each configured
channel. The commands apply to messages sent from the phone app, USB/BLE/TCP
Companion clients, and the text terminal:
```text
set tx.user "Alice Jones" radio2
get tx.user "Alice Jones"
set tx.channel 0 both
get tx.channel 0
set tx.channel #wardriving radio
get tx.channel #wardriving
```
Channel indices match the app and the terminal's `channels` list; Public is
normally slot `0`. Names must match exactly, including capitalization and any
leading `#`. Quotes always select a literal name, so `"1"` is a channel named
`1`, while unquoted `1` selects slot 1. Quote contact names beginning with
`key:` to distinguish them from public-key selectors. Duplicate names are
rejected: use the channel index or a contact's public key instead.
| Choice | Locally addressed traffic transmits on |
| --- | --- |
| `auto` | Existing behavior, including `radio2.cross` and temporary-profile isolation. This is the default. |
| `radio` | The primary profile only. |
| `radio2` | The second profile only; sending fails if it is off or RX-only. |
| `both` | The primary profile and the second profile whenever the second is enabled for TX. |
| `off` | Neither profile for this contact/channel. Reception stays enabled. |
Explicit choices override `radio2.cross`, including `off` and the isolation
between a permanent and temporary profile. They do not turn on radio2 or
change its `rx`/`rxtx` mode. For example, leave `radio2.cross auto` and use
`set tx.channel 0 both` to send Public messages on both `radio` and an active
`tempradio2`, while other channels keep their default routing.
`radio` means the currently active primary profile, whether permanent or
`tempradio`; `radio2` similarly includes `tempradio2`. There are still at most
two active profiles. Getters show the saved choice and `active=` for newly
initiated messages with the current profile configuration. A temporarily
unavailable second profile is reported explicitly.
Contact selection uses the full stored public key after resolving a unique
name or key prefix. It never routes by the short on-air destination hash.
For key selection, prefix 12–64 hexadecimal digits (whole bytes) with `key:`:
```text
set tx.user key:0123456789AB radio2
get tx.user key:0123456789AB
```
Replace the example prefix with your contact's key; use more digits if it is
ambiguous. `get tx.user` and `get tx.channel` report override counts and query
syntax. To return a target to the global behavior:
```text
set tx.user "Alice Jones" auto
set tx.channel 0 auto
```
A user's setting covers addressed texts, login/CLI/telemetry requests, replies,
ACKs, and returned paths to that contact. Thus `off` also prevents ACKs to
that contact. Channel settings cover group text and group data, including
an explicitly routed group datagram. Contact choices do not filter other
people's messages inside a group channel. Adverts, raw traces, raw packets,
and OTA packets without a contact/channel destination retain global routing.
Packets and their retries already queued retain the choice used when created;
changing or disabling the radio profile still invalidates stale queued work.
Message timeout estimates use the selected profiles' airtime, independently
of the scanner's current receive profile.
Choices survive reboot and normal phone-app updates to the same contact or
channel key. Replacing a channel's key or deleting/replacing a contact clears
its choice. They use reserved bytes in existing saved records, so record
sizes and the phone protocol are unchanged. Older firmware ignores these
bytes and may reset the choices when it saves the records.
## Choose whether traffic crosses between profiles
```text
@@ -99,12 +238,16 @@ set radio2.cross off
| `radio` | `tempradio2` | Isolated | Cross | Isolated |
| `tempradio` | `radio2` | Isolated | Cross | Isolated |
Crossing always respects `rx`: nothing transmits on an RX-only second profile.
These are the global rules used by Companion targets set to `auto`; an explicit
contact/channel choice above overrides crossing for its own outbound packets.
Ordinary crossing respects `rx`. The infrastructure-only `tx.reply ... force`
override above permits locally generated replies on that profile.
With crossing allowed, a packet received there may still be forwarded on the
primary profile. Existing routing, forwarding and packet-filter settings apply.
An RX-only `tempradio2` also blocks locally generated OTA transmissions under
An RX-only `tempradio2` also blocks locally generated OTA requests under
`auto` or `off`; they do not fall back to the normal primary channel. Use `rxtx`
for an update session that needs to exchange requests and data.
for a fetching node that needs to exchange requests and data. Infrastructure
OTA responses follow `tx.reply` independently of this crossing table.
## Preamble
+20
View File
@@ -85,6 +85,26 @@ The browser CLI requires station/LAN mode and `wifi.cli on`. Infrastructure
uses its admin login; Companion uses the trusted LAN. Configuration forms keep
password fields masked; explicit local CLI getters return their values.
## Companion contact and channel TX profiles
Choose where the Companion transmits to a contact or group channel:
```text
set tx.user "Alice Jones" radio2
get tx.user "Alice Jones"
set tx.channel 0 both
set tx.channel #wardriving radio
get tx.channel 0
```
Values are `auto`, `radio`, `radio2`, `both`, and `off`. `auto` follows
`radio2.cross`; explicit choices override it. Radio2 must still be in `rxtx`
mode, and a `radio2`-only send fails when it cannot transmit. These settings
persist and also apply to messages sent by the phone app. Use exact names,
a channel index, or `key:<unique public-key prefix>` for a contact.
See the [routing guide](radio_profiles.md#companion-tx-routing-by-contact-or-channel)
for which requests/replies are covered and how to reset a choice.
## Companion USB mode
Updated 1.17.1.6 USB Companion builds, including ordinary and Full builds, start