Clarify filter policy playground

This commit is contained in:
mikecarper
2026-08-07 10:19:32 -07:00
parent e2df34e19c
commit a30e1e03db
4 changed files with 95 additions and 68 deletions
+11 -19
View File
@@ -139,7 +139,7 @@
const channel = clean(value);
if (!channel) {
if (optional) return "";
throw new FilterToolError("Authenticated channel is required.");
throw new FilterToolError("Channel is required.");
}
if (channel.toLowerCase() === "public") return "public";
if (channel[0] === "#") {
@@ -279,7 +279,7 @@
}
}
if (rule.channel && !["any", "class:group", "grp_txt", "grp_data"].includes(rule.type)) {
throw new FilterToolError("Authenticated channel matching requires a group-capable payload type or class.");
throw new FilterToolError("Channel matching requires a group-capable payload type or class.");
}
if (rule.targetKind === "scope") rule.target = normalizeScopeName(input.target);
if (rule.targetKind === "region") rule.target = normalizeRegionName(input.target);
@@ -523,9 +523,9 @@
}
function channelDescription(channel) {
if (channel === "public") return "authenticated Public channel";
if (channel.startsWith("#")) return `authenticated ${channel}`;
return `authenticated key ${channel.slice(0, 8)}...`;
if (channel === "public") return "the Public channel";
if (channel.startsWith("#")) return `the ${channel} channel`;
return `channel key ${channel.slice(0, 8)}...`;
}
function actionPhrases(rule) {
@@ -595,7 +595,7 @@
: "Remote-management reach warning: this rule intentionally limits relayed login/admin floods at its hop or path condition. Direct routes and local delivery stay outside this policy.");
}
if (rule.type === "class:other") warnings.push("class:other intentionally includes current and future types outside group and login classes, including OTA.");
if (rule.channel && rule.type === "any") warnings.push("A channel condition narrows type=any to authenticated group text/data packets.");
if (rule.channel && rule.type === "any") warnings.push("A channel condition narrows type=any to group text/data packets on that channel.");
if (rule.sender) warnings.push("Displayed sender names are spoofable and are moderation signals, not identities.");
if (rule.pathKind !== "none") warnings.push("Pbyte and path-table matches use truncated routing hints, not authenticated identities.");
if (rule.pathKind.startsWith("bucket:")) warnings.push("The selected bucket must exist on the target node; the policy stores a reference, not its IDs.");
@@ -892,7 +892,7 @@
notes.push("Transport codes are on the wire, but a local region/scope table is required to resolve their names and allow status.");
}
if (GROUP_TYPES.includes(type)) {
notes.push("The raw channel hash does not authenticate a channel; the matching channel key is required.");
notes.push("A raw channel hash alone is insufficient to identify a channel; the matching channel key is required.");
}
if (["req", "response", "txt_msg", "path", "grp_txt", "grp_data", "anon_req"].includes(type)) {
notes.push("Encrypted content, including a displayed sender, cannot be recovered without the appropriate key.");
@@ -934,7 +934,7 @@
const channel = clean(input.channel) ? normalizeChannel(input.channel, false) : "";
const sender = clean(input.sender);
if (channel && !GROUP_TYPES.includes(type)) {
throw new FilterToolError("Only group text/data packet facts can include an authenticated channel.");
throw new FilterToolError("Only group text/data packet facts can include a channel.");
}
if (sender && type !== "grp_txt") {
throw new FilterToolError("Only a decrypted group-text packet can include a displayed sender.");
@@ -990,7 +990,7 @@
if (!typeMatches(rule.type, packet.type)) misses.push(`payload type ${packet.type} is outside ${rule.type}`);
const [minimum, maximum] = hopBounds(rule.hops);
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("authenticated channel differs or is unavailable");
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.pathKind === "prefix") {
const wanted = rule.pathPrefix.split(",");
@@ -1125,9 +1125,9 @@
]),
blackhole: Object.freeze([
{
...defaultRule("blackhole-after-hop-3"),
...defaultRule("blackhole-unscoped-rgdata"),
type: "grp_data",
hops: "4+",
hops: "all",
channel: "#rgdata",
incoming: "none",
priority: 100,
@@ -1152,14 +1152,6 @@
burst: 10,
},
]),
channel_stop: Object.freeze([
{
...documentedDropRule("rgdata-short-hop-stop", "grp_data", "0-2", 200),
channel: "#rgdata",
verdict: "continue",
stop: "policy",
},
]),
high_traffic: Object.freeze([
documentedDropRule("limit-req", "req", "3+"),
documentedDropRule("limit-response", "response", "9+"),
+18
View File
@@ -75,6 +75,24 @@
font-size: 0.72rem;
}
.filter-example-primer {
flex: 1 0 100%;
padding: 0.6rem 0.7rem;
border-radius: 0.4rem;
background: var(--filter-soft);
}
.filter-example-primer p {
margin: 0;
color: color-mix(in srgb, var(--md-default-fg-color) 76%, transparent);
font-size: 0.72rem;
line-height: 1.45;
}
.filter-example-primer p + p {
margin-top: 0.35rem;
}
.filter-example-grid {
display: grid;
flex: 1 0 100%;
+60 -46
View File
@@ -1,9 +1,8 @@
# Filter policy playground
Design and test policies for the proposed ground-up MeshCore forwarding engine.
The playground models phased evaluation, immutable receive-time matches,
explicit priority and stop behavior, ACL ownership, compact typed conditions,
and accumulated forwarding decisions.
Build a forwarding policy, see its readable definition, and simulate how a
repeater handles a packet. Rules match received packet facts, then apply actions
such as dropping, scoping, rate-limiting, or retrying a flood.
Everything runs locally in this browser. Channel keys, packet facts, and policy
drafts are not uploaded anywhere.
@@ -20,68 +19,83 @@ drafts are not uploaded anywhere.
## Build a policy
The presets below reproduce common examples from
[Flood Filtering and Moderation](flood_filtering.md) in the proposed rule model.
Start with an example or build a rule, then test the draft against packet facts
in the simulator below. The examples draw from
[Flood Filtering and Moderation](flood_filtering.md).
<div class="filter-tool" data-filter-tool>
<div class="filter-management-safety" role="note">
<strong>Remote login and direct routes</strong>
<p>
This policy controls flood retransmission only. Direct packets carry a
supplied route and stay outside the filter, matching today's firmware.
Local packet delivery also happens independently of the relay decision.
Rules that can limit relayed REQ, RESPONSE, TXT_MSG, ANON_REQ, or PATH
traffic receive a prominent warning because they can still reduce
multi-hop remote-login reach. The analyzer warns instead of silently
exempting those floods, because an exemption would make the documented
high-traffic rules behave differently.
This policy only decides whether a relay retransmits a flood. Direct
packets and local delivery are unaffected. Rules matching the login/admin
family can still reduce multi-hop remote-login reach, so the analyzer
flags them.
</p>
</div>
<div class="filter-tool-toolbar" aria-label="Policy examples">
<div class="filter-example-heading">
<strong>Drop-in replacements for documented settings</strong>
<span>Choose one to load its proposed-engine equivalent.</span>
<strong>Example policies</strong>
<span>Choose one to load its full rules into the builder and draft.</span>
</div>
<div class="filter-example-primer" role="note">
<p>
Read each summary as <code>when</code> all conditions match,
<code>do</code> the actions. A match alone does not stop forwarding.
<code>hops=3+</code> means the rule applies at a received hop count of
three or more.
</p>
<p>
<code>type=grp_data</code> matches one payload type.
<code>type=any</code> matches every payload.
<code>type=class:group</code> matches group text and data;
<code>type=class:login</code> matches REQ, RESPONSE, TXT_MSG, ANON_REQ,
and PATH; <code>type=class:other</code> matches everything else.
</p>
<p>
<code>channel=</code>, <code>rx.scope=</code>, <code>path=</code>, and
<code>tempradio=</code> further narrow a match. After <code>do</code>,
<code>drop</code> prevents retransmission, <code>scope=</code> sets the
outgoing transport scope, <code>rate=</code> limits matches per minute,
and <code>timing=</code> selects the schedule.
</p>
</div>
<div class="filter-example-grid">
<button type="button" data-example="channel_scope">
<span>Scope all authenticated #rgdata</span>
<code>set flood.channel.scope #rgdata scope=BlackHole86</code>
<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>
</button>
<button type="button" data-example="blackhole">
<span>Scope unscoped #rgdata after hop 3</span>
<code>type=grp_data hops=4+ channel=#rgdata in=none scope=BlackHole86</code>
<span>Add #BlackHole86 scope to unscoped #rgdata data</span>
<code>when type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast</code>
</button>
<button type="button" data-example="scope_rewrite">
<span>Rewrite #usa to #BlackHole86</span>
<code>channel=#rgdata in=scope:usa scope=BlackHole86</code>
<span>Replace #usa with #BlackHole86 on #rgdata data</span>
<code>when type=grp_data hops=all channel=#rgdata rx.scope=scope:usa do scope=#BlackHole86 timing=fast</code>
</button>
<button type="button" data-example="prefix_rate">
<span>Rate-limit source path 860C</span>
<code>type=any prefix=860C rate=10/min</code>
</button>
<button type="button" data-example="channel_stop">
<span>Stop lower rules for short-hop #rgdata</span>
<code>hops=0-2 channel=#rgdata priority=200 stop</code>
<span>Rate-limit packets whose path starts with 860C</span>
<code>when type=any hops=all path=prefix:860C do rate=10/min burst=10</code>
</button>
<button type="button" data-example="high_traffic">
<span>Six-rule high-traffic mesh preset</span>
<code>req 3+ | response 9+ | grp_data 3+ | login paths 9+</code>
<span>Drop selected flood types at their hop limits</span>
<code>when type=control hops=1+ do drop<br>when type=req hops=3+ do drop; same for type=grp_data<br>when type=response hops=9+ do drop; same for type=anon_req and type=path</code>
</button>
<button type="button" data-example="moderation">
<span>Limit a Public display name</span>
<code>public "Noisy User" rate=5/min</code>
<span>Rate-limit Public messages from “Noisy User”</span>
<code>when type=grp_txt hops=all channel=public sender="Noisy User" do rate=5/min burst=5</code>
</button>
<button type="button" data-example="blacklist">
<span>Drop a passive-blacklist path</span>
<code>set flood.filter any all path=blacklist</code>
<span>Drop packets whose path matches the passive blacklist</span>
<code>when type=any hops=all path=blacklist do drop</code>
</button>
<button type="button" data-example="factory">
<span>Factory OTA and #wardriving rows</span>
<code>ota suspend=tempradio | #wardriving hops=5+</code>
<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>
</button>
<button type="button" data-example="wildcards">
<span>Login and other wildcard scopes</span>
<code>login:* | other:* path=bucket:2</code>
<span>Set #BlackHole86 scope on login and bucket-matched other traffic</span>
<code>when type=class:login hops=all do scope=#BlackHole86 timing=fast<br>when type=class:other hops=all path=bucket:2 do scope=#BlackHole86 timing=slow</code>
</button>
</div>
</div>
@@ -128,7 +142,7 @@ The presets below reproduce common examples from
<input data-field="hops" value="all" placeholder="all, 5+, 2-6, or 3">
</label>
<label>
Authenticated channel (optional)
Channel (optional)
<input data-field="channel" placeholder="#rgdata, public, or key">
</label>
<label>
@@ -305,11 +319,11 @@ The presets below reproduce common examples from
</select>
</label>
<label>
Stop behavior after a match
Rule processing after a match
<select data-field="stop">
<option value="none">Continue processing</option>
<option value="phase">Stop this phase</option>
<option value="policy">Stop later policy phases</option>
<option value="none">Continue to later rules</option>
<option value="phase">Skip later rules in this phase</option>
<option value="policy">Skip all later policy rules</option>
</select>
</label>
<label>
@@ -435,7 +449,7 @@ The presets below reproduce common examples from
<input data-packet="hops" type="number" min="0" max="63" inputmode="numeric" value="4">
</label>
<label>
Authenticated channel
Channel
<input data-packet="channel" value="#rgdata" placeholder="blank if none">
</label>
<label>
@@ -505,7 +519,7 @@ The presets below reproduce common examples from
Accepts one-line <code>policy set ... when ... do ...</code> definitions,
playground JSON, or a playground Base64 bundle.
</p>
<textarea data-role="import-input" spellcheck="false" placeholder="policy set blackhole phase=rewrite owner=scope priority=150 when route=flood type=grp_data hops=4+ channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast stop=phase"></textarea>
<textarea data-role="import-input" spellcheck="false" placeholder="policy set rgdata-scope phase=rewrite owner=scope priority=150 when route=flood type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast"></textarea>
<div class="filter-builder-actions">
<button class="filter-primary-action" type="button" data-role="explain-input">Explain input</button>
<button type="button" data-role="load-input">Load into builder</button>
@@ -561,7 +575,7 @@ The simulator uses these rules:
configurable rules, but never mandatory packet validation or radio safety.
8. Shadow rules report what they would do without changing the decision or
stopping other rules.
9. Expensive facts such as channel authentication, decryption, and path-table
9. Expensive facts such as channel-key matching, decryption, and path-table
lookup are resolved once per packet and reused by every matching rule.
The byte-budget display is deliberately approximate until the packed firmware
+6 -3
View File
@@ -167,16 +167,19 @@ test("imports JSON and multiple readable definitions", () => {
assert.strictEqual(tool.parsePolicyInput(readable).length, 2);
});
test("loads proposed replacements for the documented commands", () => {
test("loads the playground policy examples", () => {
Object.values(tool.EXAMPLES).forEach((rules) => rules.forEach(tool.normalizeRule));
assert.strictEqual(
tool.buildDefinition(tool.EXAMPLES.channel_scope[0]),
"policy set rgdata-scope phase=rewrite owner=scope priority=100 when route=flood type=class:group hops=all channel=#rgdata do scope=#BlackHole86 timing=fast"
);
assert.match(tool.explainRule(tool.EXAMPLES.channel_scope[0]), /the #rgdata channel/);
assert.doesNotMatch(tool.explainRule(tool.EXAMPLES.channel_scope[0]), /authenticated/i);
assert.strictEqual(
tool.buildDefinition(tool.EXAMPLES.blackhole[0]),
"policy set blackhole-after-hop-3 phase=rewrite owner=scope priority=100 when route=flood type=grp_data hops=4+ channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast"
"policy set blackhole-unscoped-rgdata phase=rewrite owner=scope priority=100 when route=flood type=grp_data hops=all channel=#rgdata rx.scope=none do scope=#BlackHole86 timing=fast"
);
assert.strictEqual(tool.EXAMPLES.channel_stop, undefined);
assert.strictEqual(
tool.buildDefinition(tool.EXAMPLES.prefix_rate[0]),
"policy set prefix-860c-rate phase=forward owner=filter priority=100 when route=flood type=any hops=all path=prefix:860C do rate=10/min burst=10"
@@ -272,7 +275,7 @@ test("matches login and future-safe other payload classes", () => {
assert.strictEqual(tool.matchRule(other, packet({ type: "ota", channel: "" })).matched, true);
assert.strictEqual(tool.matchRule(other, packet({ type: "grp_data" })).matched, false);
assertToolError(() => tool.normalizePacket(packet({ type: "class:group", channel: "" })), /exact known type/);
assertToolError(() => tool.normalizePacket(packet({ type: "req" })), /authenticated channel/);
assertToolError(() => tool.normalizePacket(packet({ type: "req" })), /include a channel/);
});
test("keeps direct routes and SNR outside the flood policy schema", () => {