mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-19 14:39:58 +00:00
Support 2+ path-hash width filters
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -2910,9 +2910,9 @@ worked moderation examples, see [Flood Filtering and Moderation](flood_filtering
|
||||
- `get flood.rule`
|
||||
- `get flood.rule.<n>`
|
||||
- `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>] [pb=any|1|2|3] <d|s=<scope>|r=<region>|q=<N>|r|s> [...]`
|
||||
- `set fr[.<n>] <type> [h=<range>] [m=r|b|c|bc] [c=<channel>] [pb=any|1|2|2+|3] <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>] [hashbytes=any|1|2|3] [prefix=<path-prefix>] [in=<input-scope>] <drop|scope=<name>|region=<name>|rate=<N>/min|retry|stop> [priority=<0-255>] [tx=slow] [suspend=tempradio]`
|
||||
- `set flood.rule[.<n>] type=<type> [mode=radio|bridge|cross|bridge,cross] [hops=<range>] [channel=<channel>] [hashbytes=any|1|2|2+|3] [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`
|
||||
@@ -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=<ID[,ID...]>`: 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` |
|
||||
|
||||
+11
-1
@@ -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.
|
||||
|
||||
<div class="filter-tool" data-filter-tool>
|
||||
<div class="filter-management-safety" role="note">
|
||||
<strong>Remote login and direct routes</strong>
|
||||
@@ -77,7 +82,7 @@ installable commands.
|
||||
<tr><td><code>channel=</code></td><td><code>*</code> means no channel condition; a name or key authenticates one group channel; <code>hash:XX</code> is an unauthenticated one-byte fallback</td></tr>
|
||||
<tr><td><code>rx.scope=</code></td><td>Original incoming transport scope</td></tr>
|
||||
<tr><td><code>path=</code></td><td>Path prefix, blacklist, bucket, or loop match</td></tr>
|
||||
<tr><td><code>hashbytes=</code></td><td>Encoded path-hash width: 1, 2, or 3 bytes, including zero-hop packets</td></tr>
|
||||
<tr><td><code>hashbytes=</code></td><td>Encoded path-hash width: 1, 2, 3, or 2+ (2 and 3 bytes), including zero-hop packets</td></tr>
|
||||
<tr><td><code>tempradio=</code></td><td>Temporary-radio state</td></tr>
|
||||
<tr><td><code>do drop</code></td><td>Do not retransmit</td></tr>
|
||||
<tr><td><code>do scope=</code></td><td>Set the outgoing transport scope</td></tr>
|
||||
@@ -96,6 +101,10 @@ installable commands.
|
||||
<span>Drop all floods with 1-byte path hashes (including zero hops)</span>
|
||||
<code>when type=any hops=all hashbytes=1 do drop</code>
|
||||
</button>
|
||||
<button type="button" data-example="hash_width_two_plus">
|
||||
<span>Drop all floods with 2- or 3-byte path hashes (including zero hops)</span>
|
||||
<code>when type=any hops=all hashbytes=2+ do drop</code>
|
||||
</button>
|
||||
<button type="button" data-example="channel_scope">
|
||||
<span>Set #BlackHole86 scope on all #rgdata group traffic</span>
|
||||
<code>when type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast</code>
|
||||
@@ -205,6 +214,7 @@ installable commands.
|
||||
<option value="0">Any width</option>
|
||||
<option value="1">1 byte</option>
|
||||
<option value="2">2 bytes</option>
|
||||
<option value="2+">2+ bytes (2 or 3)</option>
|
||||
<option value="3">3 bytes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
+10
-7
@@ -445,7 +445,7 @@ The extended form is:
|
||||
|
||||
```text
|
||||
set flood.rule[.<slot>] type=<type> [mode=radio|bridge|cross|bridge,cross]
|
||||
[hops=<range>] [channel=<channel>] [hashbytes=any|1|2|3]
|
||||
[hops=<range>] [channel=<channel>] [hashbytes=any|1|2|2+|3]
|
||||
[prefix=<ID[,ID...]>] [in=<input-scope>] <action> [rate=<N>/min]
|
||||
[priority=<0-255>] [stop] [tx=fast|slow] [suspend=tempradio]
|
||||
get flood.rule
|
||||
@@ -495,12 +495,14 @@ Match fields:
|
||||
collision, while `public` verifies and decrypts with the Public key. This is
|
||||
channel authentication, not sender authentication: the group MAC is two
|
||||
bytes and the Public key is shared.
|
||||
- `hashbytes=any|1|2|3` matches the encoded width of each path hash, independent
|
||||
- `hashbytes=any|1|2|2+|3` matches the encoded width of each path hash, independent
|
||||
of its value or the hop count. Omitted or `any` means any width; `*` is an
|
||||
alias for `any`. It includes zero-hop packets carrying the selected width.
|
||||
Four-byte path hashes are reserved and unsupported. Combine this with other
|
||||
match fields as needed; a `prefix=` must have the same width when both are
|
||||
specified. The compact alias is `pb=`.
|
||||
`2+` matches the supported 2- and 3-byte widths in one rule; a rate limit is
|
||||
shared across both widths. Four-byte path hashes are reserved and unsupported.
|
||||
Combine this with other match fields as needed; a `prefix=` must satisfy the
|
||||
width selector. With `2+`, a 2- or 3-byte prefix narrows the rule to that exact
|
||||
width, and saved output shows the prefix. The compact alias is `pb=`.
|
||||
- `prefix=` is a source-path prefix containing one to three comma-separated
|
||||
pbyte IDs. Every ID must use the packet's pbyte width: 2, 4, or 6 hex
|
||||
characters for 1-, 2-, or 3-byte paths. Order matters and matching begins at
|
||||
@@ -690,7 +692,7 @@ set fr.3 any c=#wardriving m=bc d
|
||||
| `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. |
|
||||
| `hashbytes=any`, `1`, `2`, or `3` | `pb=*`, `1`, `2`, or `3` | Encoded path-hash width, even with zero hops. |
|
||||
| `hashbytes=any`, `1`, `2`, `2+`, or `3` | `pb=*`, `1`, `2`, `2+`, or `3` | Encoded path-hash width, even with zero hops; `2+` matches 2 and 3. |
|
||||
| `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. |
|
||||
@@ -716,7 +718,8 @@ slot without replacing the default rules. It stops this node forwarding all
|
||||
1-byte flood packets, including advertisements and relayed login/admin traffic;
|
||||
direct routes and local delivery are unchanged. Use a narrower `type=` or
|
||||
`channel=` if those floods must remain reachable. Use `hashbytes=2` or `3` for
|
||||
the other supported widths, and `get fr.<slot>` to copy the saved command.
|
||||
an exact width, or `set fr any pb=2+ d` to drop both larger supported widths with
|
||||
one rule. Use `get fr.<slot>` to copy the saved command.
|
||||
|
||||
Width-only rules require updated firmware. Existing rules remain unchanged and
|
||||
the table uses no additional RAM or storage. Before downgrading to firmware
|
||||
|
||||
@@ -7426,8 +7426,8 @@ void MyMesh::formatFloodPacketFilterDetail(int index, char* reply, size_t reply_
|
||||
char retry[10];
|
||||
char hashbytes[16] = {};
|
||||
if (entry.path_hops == 0 && entry.path_hash_size != 0)
|
||||
snprintf(hashbytes, sizeof(hashbytes), " hashbytes=%u",
|
||||
(unsigned)entry.path_hash_size);
|
||||
snprintf(hashbytes, sizeof(hashbytes), " hashbytes=%s",
|
||||
FloodFilterPolicy::pathHashBytesName(entry.path_hash_size));
|
||||
formatFloodFilterHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
||||
if (entry.match_blacklisted_path) {
|
||||
strcpy(prefix, "blacklist");
|
||||
@@ -7945,7 +7945,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
if (!FloodFilterPolicy::parsePathHashBytes(
|
||||
strchr(tokens[i], '=') + 1, hashbytes)) {
|
||||
strcpy(reply, "Err - hashbytes must be any, 1, 2, or 3");
|
||||
strcpy(reply, "Err - hashbytes must be any, 1, 2, 2+, or 3");
|
||||
return;
|
||||
}
|
||||
hashbytes_set = true;
|
||||
@@ -7984,11 +7984,13 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
}
|
||||
if (hashbytes != 0) {
|
||||
if (path_hash_size != 0 && path_hash_size != hashbytes) {
|
||||
if (path_hash_size != 0
|
||||
&& !FloodFilterPolicy::pathHashBytesMatch(hashbytes, path_hash_size)) {
|
||||
strcpy(reply, "Err - hashbytes conflicts with prefix width");
|
||||
return;
|
||||
}
|
||||
path_hash_size = hashbytes;
|
||||
// An explicit prefix already narrows 2+ to its exact 2- or 3-byte width.
|
||||
if (path_hash_size == 0) path_hash_size = hashbytes;
|
||||
}
|
||||
if (scope_timing_set && !target_set) {
|
||||
strcpy(reply, "Err - tx timing requires scope= or region=");
|
||||
|
||||
@@ -1108,8 +1108,8 @@ void FloodRuleEngine::formatDetail(int index, char* reply,
|
||||
char retry[10];
|
||||
char hashbytes[16] = {};
|
||||
if (entry.path_hops == 0 && entry.path_hash_size != 0)
|
||||
snprintf(hashbytes, sizeof(hashbytes), " hashbytes=%u",
|
||||
(unsigned)entry.path_hash_size);
|
||||
snprintf(hashbytes, sizeof(hashbytes), " hashbytes=%s",
|
||||
FloodFilterPolicy::pathHashBytesName(entry.path_hash_size));
|
||||
formatHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
||||
if (entry.match_blacklisted_path) copyString(prefix, "blacklist", sizeof(prefix));
|
||||
else formatPathPrefix(prefix, sizeof(prefix), entry.path_hash_size,
|
||||
@@ -1616,7 +1616,7 @@ void FloodRuleEngine::set(const char* args, char* reply,
|
||||
}
|
||||
if (!FloodFilterPolicy::parsePathHashBytes(
|
||||
strchr(tokens[i], '=') + 1, hashbytes)) {
|
||||
copyString(reply, "Err - hashbytes must be any, 1, 2, or 3", 160);
|
||||
copyString(reply, "Err - hashbytes must be any, 1, 2, 2+, or 3", 160);
|
||||
return;
|
||||
}
|
||||
hashbytes_set = true;
|
||||
@@ -1654,11 +1654,13 @@ void FloodRuleEngine::set(const char* args, char* reply,
|
||||
}
|
||||
|
||||
if (hashbytes != 0) {
|
||||
if (path_hash_size != 0 && path_hash_size != hashbytes) {
|
||||
if (path_hash_size != 0
|
||||
&& !FloodFilterPolicy::pathHashBytesMatch(hashbytes, path_hash_size)) {
|
||||
copyString(reply, "Err - hashbytes conflicts with prefix width", 160);
|
||||
return;
|
||||
}
|
||||
path_hash_size = hashbytes;
|
||||
// An explicit prefix already narrows 2+ to its exact 2- or 3-byte width.
|
||||
if (path_hash_size == 0) path_hash_size = hashbytes;
|
||||
}
|
||||
if (scope_timing_set && !target_set) {
|
||||
copyString(reply, "Err - tx timing requires scope= or region=", 160);
|
||||
|
||||
+1
-1
@@ -122,5 +122,5 @@ extra_javascript:
|
||||
- _javascript/firmware_picker.js?v=20260929-4
|
||||
- _javascript/telemetry_decoder.js
|
||||
- _javascript/management_decoder.js
|
||||
- _javascript/filter_tool.js?v=20261002-hashbytes
|
||||
- _javascript/filter_tool.js?v=20261002-hashbytes-2plus
|
||||
- _javascript/preset_test.js?v=20260923-2
|
||||
|
||||
@@ -432,4 +432,33 @@ test("wildcard width preserves existing behavior and explicit conflicting widths
|
||||
hashBytes: decoded.pathHashBytes })).hashBytes, 3);
|
||||
});
|
||||
|
||||
test("2+ matches both supported larger widths, including zero hops, and round trips", () => {
|
||||
const rule = tool.EXAMPLES.hash_width_two_plus[0];
|
||||
const definition = tool.buildDefinition(rule);
|
||||
assert.ok(definition.includes("hashbytes=2+"));
|
||||
assert.strictEqual(tool.parseDefinition(definition).hashBytes, "2+");
|
||||
assert.strictEqual(tool.decodeBundle(tool.encodeBundle([rule]))[0].hashBytes, "2+");
|
||||
assert.match(tool.explainRule(rule), /2- or 3-byte/);
|
||||
for (const actual of [1, 2, 3]) {
|
||||
for (const path of ["", "12".repeat(actual)]) {
|
||||
const facts = packet({ path, hops: path ? 1 : 0, hashBytes: actual });
|
||||
assert.strictEqual(tool.matchRule(rule, facts).matched, actual >= 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("2+ works with 2- and 3-byte prefixes but rejects incompatible or invalid ranges", () => {
|
||||
const base = tool.EXAMPLES.hash_width_two_plus[0];
|
||||
for (const prefix of ["1234", "123456"]) {
|
||||
const rule = tool.normalizeRule({ ...base, pathKind: "prefix", pathPrefix: prefix });
|
||||
assert.strictEqual(tool.parseDefinition(tool.buildDefinition(rule)).hashBytes, "2+");
|
||||
assert.strictEqual(tool.matchRule(rule, packet({ path: prefix })).matched, true);
|
||||
assert.strictEqual(tool.matchRule(rule, packet({ path: "", hops: 0, hashBytes: 2 })).matched, false);
|
||||
}
|
||||
assertToolError(() => tool.normalizeRule({ ...base, pathKind: "prefix", pathPrefix: "12" }), /conflicts with prefix width/);
|
||||
for (const hashBytes of ["1+", "3+", "4+", "02+", "2++"])
|
||||
assertToolError(() => tool.normalizeRule({ ...base, hashBytes }), /Path hash bytes/);
|
||||
assertToolError(() => tool.normalizePacket(packet({ path: "", hashBytes: "2+" })), /Packet path hash bytes/);
|
||||
});
|
||||
|
||||
process.stdout.write(`# ${passed} filter-policy playground tests passed\n`);
|
||||
|
||||
@@ -407,6 +407,25 @@ inline bool pathMatchesConfiguredIds(const mesh::Packet* packet,
|
||||
|
||||
// A nonzero width with no prefix hops is a width-only matcher. Reuse the
|
||||
// existing FPF7 fields so runtime RAM and serialized row sizes stay fixed.
|
||||
// The high-bit selector is a rule-storage value, never a packet hash width.
|
||||
static constexpr uint8_t RULE_HASH_BYTES_TWO_PLUS = 0x82;
|
||||
|
||||
inline const char* pathHashBytesName(uint8_t width) {
|
||||
switch (width) {
|
||||
case 1: return "1";
|
||||
case 2: return "2";
|
||||
case 3: return "3";
|
||||
case RULE_HASH_BYTES_TWO_PLUS: return "2+";
|
||||
default: return "any";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool pathHashBytesMatch(uint8_t selector, uint8_t actual_width) {
|
||||
return actual_width >= 1 && actual_width <= 3
|
||||
&& (selector == 0 || selector == actual_width
|
||||
|| (selector == RULE_HASH_BYTES_TWO_PLUS && actual_width >= 2));
|
||||
}
|
||||
|
||||
inline bool parsePathHashBytes(const char* text, uint8_t& width) {
|
||||
if (text == NULL) return false;
|
||||
if (strcmp(text, "*") == 0
|
||||
@@ -414,6 +433,10 @@ inline bool parsePathHashBytes(const char* text, uint8_t& width) {
|
||||
width = 0;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(text, "2+") == 0) {
|
||||
width = RULE_HASH_BYTES_TWO_PLUS;
|
||||
return true;
|
||||
}
|
||||
if (text[0] >= '1' && text[0] <= '3' && text[1] == 0) {
|
||||
width = (uint8_t)(text[0] - '0');
|
||||
return true;
|
||||
@@ -424,6 +447,7 @@ inline bool parsePathHashBytes(const char* text, uint8_t& width) {
|
||||
inline bool pathMatcherValid(uint8_t width, uint8_t prefix_hops,
|
||||
bool blacklist) {
|
||||
return (width == 0 && prefix_hops == 0)
|
||||
|| (width == RULE_HASH_BYTES_TWO_PLUS && prefix_hops == 0)
|
||||
|| (width >= 1 && width <= 3 && prefix_hops <= 3
|
||||
&& (prefix_hops == 0 || !blacklist));
|
||||
}
|
||||
@@ -433,8 +457,9 @@ inline bool pathStartsWith(const mesh::Packet* packet,
|
||||
uint8_t prefix_hops,
|
||||
const uint8_t* prefix) {
|
||||
if (hash_size == 0 && prefix_hops == 0) return true;
|
||||
if (packet == NULL || hash_size < 1 || hash_size > 3
|
||||
|| packet->getPathHashSize() != hash_size
|
||||
if (packet == NULL
|
||||
|| (prefix_hops != 0 && (hash_size < 1 || hash_size > 3))
|
||||
|| !pathHashBytesMatch(hash_size, packet->getPathHashSize())
|
||||
|| packet->getPathHashCount() < prefix_hops) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ bool formatCompact(char* out, size_t capacity, unsigned slot, const Entry& entry
|
||||
}
|
||||
if (entry.channel_key_len != 0) text.append(" c=%s", entry.channel_name);
|
||||
if (entry.path_hops == 0 && entry.path_hash_size != 0)
|
||||
text.append(" pb=%u", (unsigned)entry.path_hash_size);
|
||||
text.append(" pb=%s", pathHashBytesName(entry.path_hash_size));
|
||||
if (entry.match_blacklisted_path) text.append(" p=bl");
|
||||
else if (entry.path_hops) {
|
||||
text.append(" p=");
|
||||
|
||||
@@ -310,7 +310,16 @@ TEST(FloodRuleHashBytes, StrictSelectorAndStoredMatcherValidation) {
|
||||
EXPECT_TRUE(pathMatcherValid(width, 3, false));
|
||||
EXPECT_FALSE(pathMatcherValid(width, 1, true));
|
||||
}
|
||||
for (const char* text : {"", "0", "4", "-1", "1x", "01", "anymore"})
|
||||
ASSERT_TRUE(parsePathHashBytes("2+", width));
|
||||
EXPECT_EQ(RULE_HASH_BYTES_TWO_PLUS, width);
|
||||
EXPECT_STREQ("2+", pathHashBytesName(width));
|
||||
EXPECT_TRUE(pathMatcherValid(width, 0, false));
|
||||
EXPECT_TRUE(pathMatcherValid(width, 0, true));
|
||||
EXPECT_FALSE(pathMatcherValid(width, 1, false));
|
||||
for (uint8_t actual = 0; actual <= 4; actual++)
|
||||
EXPECT_EQ(actual == 2 || actual == 3, pathHashBytesMatch(width, actual));
|
||||
for (const char* text : {"", "0", "4", "-1", "1x", "01", "anymore",
|
||||
"1+", "3+", "4+", "02+", "2++", "130"})
|
||||
EXPECT_FALSE(parsePathHashBytes(text, width));
|
||||
EXPECT_FALSE(parsePathHashBytes(nullptr, width));
|
||||
EXPECT_TRUE(pathMatcherValid(0, 0, false));
|
||||
@@ -325,8 +334,10 @@ TEST(FloodRuleHashBytes, WidthOnlyMatchesEmptyAndNonemptyPaths) {
|
||||
for (uint8_t hops : {0, 1, 3}) {
|
||||
auto packet = makeFloodPacket(actual, path, hops);
|
||||
EXPECT_TRUE(FloodFilterPolicy::pathStartsWith(&packet, 0, 0, nullptr));
|
||||
for (uint8_t wanted = 1; wanted <= 3; wanted++)
|
||||
EXPECT_EQ(actual == wanted,
|
||||
for (uint8_t wanted : {uint8_t(1), uint8_t(2), uint8_t(3),
|
||||
FloodFilterPolicy::RULE_HASH_BYTES_TWO_PLUS})
|
||||
EXPECT_EQ(actual == wanted
|
||||
|| (wanted == FloodFilterPolicy::RULE_HASH_BYTES_TWO_PLUS && actual >= 2),
|
||||
FloodFilterPolicy::pathStartsWith(&packet, wanted, 0, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,10 @@ TEST_F(TransportFilter, ShorthandAndLongCommandsSaveIdenticalRules) {
|
||||
"set fr.3 any c=hash:A7 p=1234,5678 i=s q=3 r s"},
|
||||
Pair{"set flood.rule.3 type=any hashbytes=1 drop", "set fr.3 any pb=1 d"},
|
||||
Pair{"set flood.rule.3 type=any mode=bridge,cross hashbytes=3 rate=5/min",
|
||||
"set fr.3 any m=bc pb=3 q=5"}}) {
|
||||
"set fr.3 any m=bc pb=3 q=5"},
|
||||
Pair{"set flood.rule.3 type=any hashbytes=2+ drop", "set fr.3 any pb=2+ d"},
|
||||
Pair{"set flood.rule.3 type=any mode=bridge,cross hashbytes=2+ rate=5/min",
|
||||
"set fr.3 any m=bc pb=2+ q=5"}}) {
|
||||
command(pair.full); ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
const auto full = fs.files["/flood_filter"];
|
||||
command(pair.short_form); ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
@@ -284,6 +287,74 @@ TEST_F(TransportFilter, HashBytesCombinesWithTransportChannelAndHops) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TransportFilter, TwoPlusMatchesBothWidthsAndPersistsAsOneRule) {
|
||||
command("del fr all");
|
||||
for (const char* mode : {"radio", "bridge,cross"}) {
|
||||
command((std::string("set flood.rule.3 type=any hashbytes=2+ mode=") + mode + " drop").c_str());
|
||||
ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
const auto saved = fs.files["/flood_filter"];
|
||||
rules.begin(&fs, nullptr);
|
||||
command("get flood.rule.3");
|
||||
ASSERT_NE(nullptr, strstr(reply, "hashbytes=2+")) << reply;
|
||||
command("get fr.3");
|
||||
ASSERT_NE(nullptr, strstr(reply, "pb=2+")) << reply;
|
||||
const std::string copy = reply;
|
||||
command(copy.c_str()); // Compact setter survives its own parser and storage.
|
||||
ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
EXPECT_EQ(saved, fs.files["/flood_filter"]);
|
||||
for (uint8_t actual = 1; actual <= 3; actual++)
|
||||
for (uint8_t hops : {0, 1, 3})
|
||||
for (uint8_t type = 0; type < 16; type++)
|
||||
for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD,
|
||||
ROUTE_TYPE_DIRECT, ROUTE_TYPE_TRANSPORT_DIRECT}) {
|
||||
auto p = packet("#wardriving", type, route);
|
||||
p.setPathHashSizeAndCount(actual, hops);
|
||||
memset(p.path, 0x12, actual * hops);
|
||||
const bool radio = strcmp(mode, "radio") == 0;
|
||||
const bool flood = route == ROUTE_TYPE_FLOOD || route == ROUTE_TYPE_TRANSPORT_FLOOD;
|
||||
EXPECT_EQ(radio && flood && actual >= 2, blocked(p, RULE_MODE_RADIO));
|
||||
EXPECT_EQ(!radio && actual >= 2, blocked(p, RULE_MODE_BRIDGE));
|
||||
EXPECT_EQ(!radio && actual >= 2, blocked(p, RULE_MODE_CROSS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TransportFilter, TwoPlusPrefixNarrowsToItsExactWidthInEitherOrder) {
|
||||
command("del fr all");
|
||||
for (const char* tail : {"pb=2+ prefix=1234", "prefix=1234 hashbytes=2+",
|
||||
"hashbytes=2+ prefix=123456", "prefix=123456 pb=2+"}) {
|
||||
command((std::string("set fr.3 any ") + tail + " d").c_str());
|
||||
ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
rules.begin(&fs, nullptr);
|
||||
const uint8_t wanted = strstr(tail, "123456") ? 3 : 2;
|
||||
for (uint8_t actual = 1; actual <= 3; actual++) {
|
||||
auto p = packet();
|
||||
p.setPathHashSizeAndCount(actual, 1);
|
||||
p.path[0] = 0x12; p.path[1] = 0x34; p.path[2] = 0x56;
|
||||
EXPECT_EQ(actual == wanted, blocked(p, RULE_MODE_RADIO));
|
||||
p.setPathHashSizeAndCount(actual, 0);
|
||||
EXPECT_FALSE(blocked(p, RULE_MODE_RADIO)); // Prefix still requires a hop.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TransportFilter, TwoPlusSharesOneRateAllowanceAcrossBothWidths) {
|
||||
command("del fr all");
|
||||
command("set fr.3 any pb=2+ q=1");
|
||||
ASSERT_EQ(0, strncmp(reply, "OK", 2)) << reply;
|
||||
auto two = packet(), three = packet(), one = packet();
|
||||
two.setPathHashSizeAndCount(2, 0);
|
||||
three.setPathHashSizeAndCount(3, 0);
|
||||
auto mask = rules.evaluate(&two, false, true, nullptr, RULE_MODE_RADIO);
|
||||
EXPECT_FALSE(rules.shouldBlock(&two, mask, 100));
|
||||
rules.commitRates(&two, mask, 100);
|
||||
EXPECT_TRUE(blocked(two, RULE_MODE_RADIO));
|
||||
EXPECT_TRUE(blocked(three, RULE_MODE_RADIO));
|
||||
EXPECT_FALSE(blocked(one, RULE_MODE_RADIO));
|
||||
mask = rules.evaluate(&three, false, true, nullptr, RULE_MODE_RADIO);
|
||||
EXPECT_FALSE(rules.shouldBlock(&three, mask, 60100));
|
||||
}
|
||||
|
||||
TEST_F(TransportFilter, HashBytesWildcardAndPrefixAreOrderIndependent) {
|
||||
command("del fr all");
|
||||
for (const char* tail : {"hashbytes=any", "pb=*", "prefix=* hashbytes=1", "pb=1 p=*",
|
||||
@@ -307,7 +378,9 @@ TEST_F(TransportFilter, InvalidHashBytesNeverReplacesSavedRules) {
|
||||
const auto saved = fs.files;
|
||||
for (const char* tail : {"hashbytes=0", "hashbytes=4", "pb=-1", "pb=01", "pb=1x", "pb=",
|
||||
"pb=1 hashbytes=1", "hashbytes=any pb=2",
|
||||
"hashbytes=1 prefix=1234", "prefix=1234 pb=1"}) {
|
||||
"hashbytes=1 prefix=1234", "prefix=1234 pb=1",
|
||||
"pb=1+", "pb=3+", "pb=4+", "pb=02+", "pb=2++",
|
||||
"pb=2+ prefix=12", "prefix=12 hashbytes=2+", "pb=2+ hashbytes=2+"}) {
|
||||
command((std::string("set fr.3 any ") + tail + " d").c_str());
|
||||
EXPECT_EQ(0, strncmp(reply, "Err", 3)) << tail << ": " << reply;
|
||||
EXPECT_EQ(saved, fs.files);
|
||||
|
||||
Reference in New Issue
Block a user