diff --git a/docs/_javascript/filter_tool.js b/docs/_javascript/filter_tool.js index a71726423..c3739d368 100644 --- a/docs/_javascript/filter_tool.js +++ b/docs/_javascript/filter_tool.js @@ -257,7 +257,8 @@ channel: normalizeChannel(input.channel, true), incoming: normalizeIncoming(input.incoming), hashBytes: ["", "0", "any", "*"].includes(clean(input.hashBytes).toLowerCase()) - ? 0 : requiredInteger(input.hashBytes, 1, 3, "Path hash bytes"), + ? 0 : clean(input.hashBytes) === "2+" ? "2+" + : requiredInteger(input.hashBytes, 1, 3, "Path hash bytes"), pathKind: enumValue(input.pathKind, PATH_KINDS, "Path matcher", "none"), pathPrefix: "", sender: clean(input.sender), @@ -280,7 +281,8 @@ if (rule.pathKind === "prefix") rule.pathPrefix = normalizePathPrefix(input.pathPrefix, 3); if (rule.pathKind === "prefix" && !rule.pathPrefix) throw new FilterToolError("Prefix path matcher requires at least one pbyte ID."); if (rule.pathKind === "prefix" && rule.hashBytes - && rule.pathPrefix.split(",")[0].length / 2 !== rule.hashBytes) { + && (rule.hashBytes === "2+" ? rule.pathPrefix.split(",")[0].length / 2 < 2 + : rule.pathPrefix.split(",")[0].length / 2 !== rule.hashBytes)) { throw new FilterToolError("Path hash bytes conflicts with prefix width."); } if (rule.sender) { @@ -577,7 +579,9 @@ else if (rule.pathKind === "blacklist") conditions.push("a path matching the passive blacklist"); 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.hashBytes) conditions.push(`${rule.hashBytes}-byte encoded path hashes, including zero hops`); + if (rule.hashBytes) conditions.push(rule.hashBytes === "2+" + ? "2- or 3-byte encoded path hashes, including zero hops" + : `${rule.hashBytes}-byte encoded path hashes, including zero hops`); 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}`); @@ -607,7 +611,7 @@ && rule.hops === "all" && !rule.channel && rule.incoming === "any" && rule.pathKind === "none" && rule.tempRadio === "any") { warnings.push(rule.hashBytes - ? `This drops all ${rule.hashBytes}-byte path-hash packets across both flood route types.` + ? `This drops all ${rule.hashBytes === "2+" ? "2- or 3" : rule.hashBytes}-byte path-hash packets across both flood route types.` : "This is a global drop rule across both flood route types."); } if (limitsRemoteManagement(rule)) { @@ -1038,7 +1042,8 @@ if (packet.hops < minimum || packet.hops > maximum) misses.push(`hop ${packet.hops} is outside ${rule.hops}`); if (rule.channel && rule.channel !== packet.channel) misses.push("channel differs or is unavailable"); if (!incomingMatches(rule.incoming, packet)) misses.push(`original scope does not satisfy ${rule.incoming}`); - if (rule.hashBytes && rule.hashBytes !== packet.hashBytes) misses.push(`path hash width is ${packet.hashBytes} bytes`); + if (rule.hashBytes && (rule.hashBytes === "2+" ? packet.hashBytes < 2 + : rule.hashBytes !== packet.hashBytes)) misses.push(`path hash width is ${packet.hashBytes} bytes`); if (rule.pathKind === "prefix") { const wanted = rule.pathPrefix.split(","); const actual = packet.path ? packet.path.split(",") : []; @@ -1164,6 +1169,9 @@ hash_width: Object.freeze([ { ...documentedDropRule("drop-one-byte-paths", "any", "all"), hashBytes: 1 }, ]), + hash_width_two_plus: Object.freeze([ + { ...documentedDropRule("drop-two-plus-byte-paths", "any", "all"), hashBytes: "2+" }, + ]), channel_scope: Object.freeze([ { ...defaultRule("rgdata-scope"), diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 421d21b3c..80c909e1e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -2910,9 +2910,9 @@ worked moderation examples, see [Flood Filtering and Moderation](flood_filtering - `get flood.rule` - `get flood.rule.` - `get fr[.]` (numbered form returns a complete short setter for copying) -- `set fr[.] [h=] [m=r|b|c|bc] [c=] [pb=any|1|2|3] |r=|q=|r|s> [...]` +- `set fr[.] [h=] [m=r|b|c|bc] [c=] [pb=any|1|2|2+|3] |r=|q=|r|s> [...]` - `del fr.` or `del fr all` -- `set flood.rule[.] type= [mode=radio|bridge|cross|bridge,cross] [hops=] [channel=] [hashbytes=any|1|2|3] [prefix=] [in=] |region=|rate=/min|retry|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]` +- `set flood.rule[.] type= [mode=radio|bridge|cross|bridge,cross] [hops=] [channel=] [hashbytes=any|1|2|2+|3] [prefix=] [in=] |region=|rate=/min|retry|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]` - `del flood.rule.` - `del flood.rule all` - `get flood.filter` @@ -2968,10 +2968,12 @@ compile this table. MAC/decrypt check with the Public channel key. That distinguishes an ordinary `0x11` collision, but it is channel authentication rather than sender authentication: the group MAC is two bytes and the Public key is shared. -- `hashbytes=any|1|2|3`: Optional encoded path-hash width, independent of hop +- `hashbytes=any|1|2|2+|3`: Optional encoded path-hash width, independent of hop count and hash value. Omitted, `any`, or `*` matches every supported width. Width-specific rules also match zero-hop packets. Four-byte hashes are - reserved and unsupported. A simultaneous `prefix=` must use the same width. + reserved and unsupported. `2+` selects both 2- and 3-byte paths in one row, + sharing any rate limit across both. A simultaneous `prefix=` must satisfy the + selector; a 2- or 3-byte prefix narrows `2+` to its own exact width. Example: `set fr any pb=1 d` drops all 1-byte floods from forwarding, including relayed login/admin traffic, but not direct packets or local delivery. - `prefix=`: Optional ordered source-path prefix of one to three @@ -3223,7 +3225,7 @@ have the same behavior as their long forms. | `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=...` | -| `hashbytes=any/1/2/3` | `pb=*/1/2/3` (choose one value) | +| `hashbytes=any/1/2/2+/3` | `pb=*/1/2/2+/3` (choose one value) | | `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` | diff --git a/docs/filter_tool.md b/docs/filter_tool.md index ec594a056..f6ec31910 100644 --- a/docs/filter_tool.md +++ b/docs/filter_tool.md @@ -33,6 +33,11 @@ and local delivery remain unaffected. Width-only rules require updated firmware. The simulator's policy definitions remain design-preview text, not installable commands. +Select **2+ bytes (2 or 3)** to match both larger widths in one rule. The +firmware command is `set fr any pb=2+ d`. A rate-limit action on this rule uses +one allowance shared by both widths, rather than a separate limit for each. +Four-byte paths remain unsupported. +
Remote login and direct routes @@ -77,7 +82,7 @@ installable commands. channel=* means no channel condition; a name or key authenticates one group channel; hash:XX is an unauthenticated one-byte fallback rx.scope=Original incoming transport scope path=Path prefix, blacklist, bucket, or loop match - hashbytes=Encoded path-hash width: 1, 2, or 3 bytes, including zero-hop packets + hashbytes=Encoded path-hash width: 1, 2, 3, or 2+ (2 and 3 bytes), including zero-hop packets tempradio=Temporary-radio state do dropDo not retransmit do scope=Set the outgoing transport scope @@ -96,6 +101,10 @@ installable commands. Drop all floods with 1-byte path hashes (including zero hops) when type=any hops=all hashbytes=1 do drop +