From ea3b5e16ea5c773eefbe31d8a7ca2ed87432287b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 6 Aug 2026 23:31:25 -0700 Subject: [PATCH] Refine filter policy playground --- docs/_javascript/filter_tool.js | 333 ++++++++++++++---------------- docs/_stylesheets/filter_tool.css | 77 +++++++ docs/filter_tool.md | 227 ++++++++++++-------- scripts/test_filter_tool.js | 44 +++- 4 files changed, 417 insertions(+), 264 deletions(-) diff --git a/docs/_javascript/filter_tool.js b/docs/_javascript/filter_tool.js index 0d39132a..96c2fe6d 100644 --- a/docs/_javascript/filter_tool.js +++ b/docs/_javascript/filter_tool.js @@ -22,7 +22,7 @@ }); const MODES = Object.freeze(["active", "shadow", "disabled"]); const STOPS = Object.freeze(["none", "phase", "policy"]); - const ROUTES = Object.freeze(["flood", "unscoped_flood", "scoped_flood", "direct", "any"]); + const ROUTES = Object.freeze(["flood", "unscoped_flood", "scoped_flood"]); const TYPE_NAMES = Object.freeze([ "req", "response", "txt_msg", "ack", "advert", "grp_txt", "grp_data", "anon_req", "path", "trace", "multipart", "control", "ota", "13", "14", "raw_custom", @@ -82,16 +82,6 @@ return parsed; } - function nullableNumber(value, minimum, maximum, label) { - const text = clean(value); - if (!text) return null; - const parsed = Number(text); - if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) { - throw new FilterToolError(`${label} must be ${minimum}-${maximum}.`); - } - return Math.round(parsed * 100) / 100; - } - function enumValue(value, allowed, label, fallback) { const normalized = clean(value).toLowerCase() || fallback; if (!allowed.includes(normalized)) throw new FilterToolError(`${label} is invalid.`); @@ -215,8 +205,6 @@ pathPrefix: "", sender: "", tempRadio: "any", - snrMin: null, - snrMax: null, verdict: "continue", scopeGate: "unchanged", targetKind: "scope", @@ -261,8 +249,6 @@ pathPrefix: "", sender: clean(input.sender), tempRadio: enumValue(input.tempRadio, ["any", "active", "inactive"], "Temporary-radio matcher", "any"), - snrMin: nullableNumber(input.snrMin, -30, 30, "Minimum SNR"), - snrMax: nullableNumber(input.snrMax, -30, 30, "Maximum SNR"), verdict: enumValue(input.verdict, ["continue", "drop"], "Forwarding verdict", "continue"), scopeGate: enumValue(input.scopeGate, ["unchanged", "require_allowed", "bypass_global"], "Scope-gate action", "unchanged"), targetKind: enumValue(input.targetKind, ["none", "scope", "region"], "Scope target", "none"), @@ -289,10 +275,6 @@ 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."); } - if (rule.snrMin != null && rule.snrMax != null && rule.snrMin > rule.snrMax) { - throw new FilterToolError("Minimum SNR cannot exceed maximum SNR."); - } - if (rule.targetKind === "scope") rule.target = normalizeScopeName(input.target); if (rule.targetKind === "region") rule.target = normalizeRegionName(input.target); if (rule.rate != null) { @@ -366,9 +348,6 @@ else if (rule.pathKind !== "none") matches.push(`path=${rule.pathKind}`); if (rule.sender) matches.push(`sender=${quoteDsl(rule.sender)}`); if (rule.tempRadio !== "any") matches.push(`tempradio=${rule.tempRadio}`); - if (rule.snrMin != null || rule.snrMax != null) { - matches.push(`snr=${rule.snrMin == null ? "" : rule.snrMin}..${rule.snrMax == null ? "" : rule.snrMax}`); - } const actions = []; if (rule.verdict === "drop") actions.push("drop"); if (rule.scopeGate === "require_allowed") actions.push("scope-gate=require"); @@ -442,8 +421,6 @@ rule.pathPrefix = ""; rule.sender = ""; rule.tempRadio = "any"; - rule.snrMin = null; - rule.snrMax = null; rule.verdict = "continue"; rule.scopeGate = "unchanged"; rule.targetKind = "none"; @@ -479,12 +456,7 @@ } else rule.pathKind = value; } else if (name === "sender") rule.sender = value; else if (name === "tempradio") rule.tempRadio = value; - else if (name === "snr") { - const match = value.match(/^(-?\d+(?:\.\d+)?)?\.\.(-?\d+(?:\.\d+)?)?$/); - if (!match || (!match[1] && !match[2])) throw new FilterToolError("SNR matcher must use min..max, min.., or ..max."); - rule.snrMin = match[1] || null; - rule.snrMax = match[2] || null; - } else throw new FilterToolError(`Unsupported receive-time matcher: ${token}`); + else throw new FilterToolError(`Unsupported receive-time matcher: ${token}`); }); tokens.slice(doIndex + 1).forEach((token) => { const [name, value] = splitOption(token); @@ -534,8 +506,6 @@ flood: "either flood route", unscoped_flood: "an unscoped flood", scoped_flood: "a transport-scoped flood", - direct: "a direct route", - any: "any route", }[route]; } @@ -585,11 +555,6 @@ 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}"`); if (rule.tempRadio !== "any") conditions.push(`temporary radio ${rule.tempRadio}`); - if (rule.snrMin != null || rule.snrMax != null) { - if (rule.snrMin != null && rule.snrMax != null) conditions.push(`SNR ${rule.snrMin} through ${rule.snrMax} dB`); - else if (rule.snrMin != null) conditions.push(`SNR at least ${rule.snrMin} dB`); - else conditions.push(`SNR at most ${rule.snrMax} dB`); - } const mode = rule.mode === "shadow" ? "In shadow mode, report that it would " : rule.mode === "disabled" @@ -598,23 +563,36 @@ return `${PHASE_LABELS[rule.phase]} rule ${rule.id}, owned by ${OWNER_LABELS[rule.owner]}, matches ${conditions.join(", ")}. ${mode}${actionPhrases(rule).join(" and ")}. Priority ${rule.priority}; every condition reads immutable receive-time facts.`; } + function limitsRemoteManagement(rule) { + const matchesLoginType = rule.type === "any" + || rule.type === "class:login" + || LOGIN_TYPES.includes(rule.type); + const canRestrict = rule.verdict === "drop" + || rule.rate != null + || rule.scopeGate === "require_allowed"; + return !rule.channel && matchesLoginType && canRestrict; + } + function ruleWarnings(input) { const rule = normalizeRule(input); const warnings = []; const [minimumHops] = hopBounds(rule.hops); - if (rule.verdict === "drop" && rule.route === "any" && rule.type === "any" && rule.hops === "all") { - warnings.push("This is a global drop rule across flood and direct traffic."); + if (rule.verdict === "drop" && rule.route === "flood" && rule.type === "any" + && rule.hops === "all" && !rule.channel && rule.incoming === "any" + && rule.pathKind === "none" && rule.tempRadio === "any") { + warnings.push("This is a global drop rule across both flood route types."); } - if (rule.verdict === "drop" && minimumHops === 0 - && ["any", "class:login", "req", "response", "txt_msg", "anon_req", "path"].includes(rule.type)) { - warnings.push("This rule can block remote administration traffic at hop 0. Keep a serial recovery path."); + if (limitsRemoteManagement(rule)) { + const zeroHop = minimumHops === 0 && rule.pathKind === "none"; + warnings.push(zeroHop + ? "Remote-management relay risk: this rule can restrict zero-hop login/admin floods. Direct routes and local delivery stay outside this policy, but multi-hop login reach can still be lost." + : "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.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."); - if (rule.snrMin != null || rule.snrMax != null) warnings.push("Signal-based policy can produce different decisions at neighboring nodes hearing the same packet."); if (rule.stop === "policy") warnings.push("stop=policy skips every later configurable phase when this rule matches, but not mandatory protocol/radio safety."); if (rule.mode === "shadow") warnings.push("Shadow mode records the match but applies no action and does not stop processing."); if (rule.targetKind === "region") warnings.push("The configured region must exist, allow flooding, and have a usable transport key at evaluation time."); @@ -631,7 +609,6 @@ else if (rule.pathKind !== "none") bytes += 3; if (rule.sender) bytes += 3 + rule.sender.length; if (rule.tempRadio !== "any") bytes += 3; - if (rule.snrMin != null || rule.snrMax != null) bytes += 6; if (rule.verdict === "drop") bytes += 2; if (rule.scopeGate !== "unchanged") bytes += 3; if (rule.targetKind !== "none") bytes += 3 + rule.target.length; @@ -673,6 +650,10 @@ }); }); const estimated = rules.reduce((total, rule) => total + estimateRuleBytes(rule), 16); + const managementRules = rules.filter((rule) => rule.mode === "active" && limitsRemoteManagement(rule)); + if (managementRules.length) { + warnings.push(`${managementRules.length} active rule${managementRules.length === 1 ? "" : "s"} can limit relayed remote-login traffic. Direct routes and local packet delivery remain outside the policy, but end-to-end flood login reach is not guaranteed.`); + } const budget = PROFILE_BUDGETS[profile] || PROFILE_BUDGETS.nrf52; if (estimated > budget) warnings.push(`Approximate packed size ${estimated} bytes exceeds the selected ${budget}-byte target budget.`); if (rules.length > 255) warnings.push("The draft exceeds the proposed 255 stable rule-ID limit."); @@ -780,7 +761,7 @@ ? clean(input.buckets).split(",").map((value) => requiredInteger(value, 1, 6, "Path bucket")) : []; return { - route: enumValue(input.route, ["unscoped_flood", "scoped_flood", "direct"], "Packet route", "unscoped_flood"), + route: enumValue(input.route, ["unscoped_flood", "scoped_flood"], "Packet route", "unscoped_flood"), type, hops: requiredInteger(input.hops, 0, 63, "Packet hops"), channel, @@ -790,7 +771,6 @@ regionName: clean(input.regionName), sender, tempRadio: Boolean(input.tempRadio), - snr: nullableNumber(input.snr, -30, 30, "Packet SNR"), blacklist: Boolean(input.blacklist), buckets: Array.from(new Set(buckets)), loopLevel: requiredInteger(input.loopLevel == null ? 0 : input.loopLevel, 0, 3, "Loop result"), @@ -824,7 +804,7 @@ const packet = normalizePacket(inputPacket); const misses = []; if (rule.route === "flood" && !["unscoped_flood", "scoped_flood"].includes(packet.route)) misses.push("route is not flood"); - else if (rule.route !== "any" && rule.route !== "flood" && rule.route !== packet.route) misses.push(`route is ${packet.route}`); + 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}`); const [minimum, maximum] = hopBounds(rule.hops); if (packet.hops < minimum || packet.hops > maximum) misses.push(`hop ${packet.hops} is outside ${rule.hops}`); @@ -844,8 +824,6 @@ if (rule.sender && rule.sender.toLowerCase() !== packet.sender.toLowerCase()) misses.push("decrypted sender differs or is unavailable"); if (rule.tempRadio === "active" && !packet.tempRadio) misses.push("temporary radio is inactive"); if (rule.tempRadio === "inactive" && packet.tempRadio) misses.push("temporary radio is active"); - if (rule.snrMin != null && (packet.snr == null || packet.snr < rule.snrMin)) misses.push(`SNR is below ${rule.snrMin} dB`); - if (rule.snrMax != null && (packet.snr == null || packet.snr > rule.snrMax)) misses.push(`SNR is above ${rule.snrMax} dB`); return { matched: misses.length === 0, misses }; } @@ -936,40 +914,78 @@ return { packet, decision, trace }; } + function documentedDropRule(id, type, hops, priority) { + return { + ...defaultRule(id), + phase: "forward", + owner: "filter", + priority: priority == null ? 100 : priority, + type, + hops, + channel: "", + incoming: "any", + targetKind: "none", + target: "", + timing: "inherit", + verdict: "drop", + }; + } + const EXAMPLES = Object.freeze({ + channel_scope: Object.freeze([ + { + ...defaultRule("rgdata-scope"), + type: "class:group", + channel: "#rgdata", + incoming: "any", + priority: 100, + }, + ]), blackhole: Object.freeze([ { - ...defaultRule("blackhole-rewrite"), - priority: 160, + ...defaultRule("blackhole-after-hop-3"), type: "grp_data", hops: "4+", channel: "#rgdata", incoming: "none", - pathKind: "prefix", - pathPrefix: "860C", - stop: "phase", + priority: 100, }, ]), - wildcards: Object.freeze([ + scope_rewrite: Object.freeze([ { - ...defaultRule("login-scope"), - type: "class:login", - channel: "", - incoming: "any", - target: "BlackHole86", - priority: 140, + ...defaultRule("usa-to-blackhole"), + type: "grp_data", + channel: "#rgdata", + incoming: "scope:usa", + priority: 100, }, + ]), + prefix_rate: Object.freeze([ { - ...defaultRule("other-scope-bucket2"), - type: "class:other", - channel: "", - incoming: "any", - pathKind: "bucket:2", - target: "BlackHole86", - timing: "slow", - priority: 130, + ...documentedDropRule("prefix-860c-rate", "any", "all"), + pathKind: "prefix", + pathPrefix: "860C", + verdict: "continue", + rate: 10, + 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+"), + documentedDropRule("limit-group-data", "grp_data", "3+"), + documentedDropRule("limit-anon-request", "anon_req", "9+"), + documentedDropRule("limit-path", "path", "9+"), + documentedDropRule("limit-control", "control", "1+"), + ]), moderation: Object.freeze([ { ...defaultRule("public-noisy-user"), @@ -984,123 +1000,55 @@ timing: "inherit", rate: 5, burst: 5, - tag: "public-rate", - }, - { - ...defaultRule("local-bot-path"), - phase: "content", - owner: "filter", - type: "grp_txt", - channel: "#local", - incoming: "any", - pathKind: "prefix", - pathPrefix: "A1B2C3,D4E5F6", - sender: "bot", - targetKind: "none", - target: "", - timing: "inherit", - verdict: "drop", - priority: 180, }, ]), - system: Object.freeze([ + blacklist: Object.freeze([ { - ...defaultRule("system-flood-max"), - phase: "forward", - owner: "system", - type: "any", + ...documentedDropRule("drop-blacklisted-path", "any", "all"), + pathKind: "blacklist", + }, + ]), + wildcards: Object.freeze([ + { + ...defaultRule("login-scope"), + type: "class:login", channel: "", incoming: "any", - hops: "9+", - targetKind: "none", - target: "", - timing: "inherit", - verdict: "drop", - priority: 220, + priority: 140, }, { - ...defaultRule("system-loop-minimal"), - phase: "forward", - owner: "system", - type: "any", + ...defaultRule("other-scope-bucket2"), + type: "class:other", channel: "", incoming: "any", - pathKind: "loop:minimal", - targetKind: "none", - target: "", - timing: "inherit", - verdict: "drop", - priority: 230, + pathKind: "bucket:2", + timing: "slow", + priority: 130, }, + ]), + factory: Object.freeze([ { - ...defaultRule("ota-outside-temp-radio"), - phase: "forward", + ...documentedDropRule("ota-outside-temp-radio", "ota", "all", 250), owner: "system", - type: "ota", - channel: "", - incoming: "any", tempRadio: "inactive", - targetKind: "none", - target: "", - timing: "inherit", - verdict: "drop", - priority: 250, + }, + { + ...documentedDropRule("wardriving-after-hop-4", "any", "5+", 220), + owner: "system", + channel: "#wardriving", }, ]), }); const COMPLETE_EXAMPLES = Object.freeze({ - blackhole: EXAMPLES.blackhole, - wildcards: EXAMPLES.wildcards, - moderation: EXAMPLES.moderation, - system: EXAMPLES.system, + ...EXAMPLES, + system: EXAMPLES.factory, mixed: Object.freeze([ - { - ...defaultRule("group-gate-default"), - phase: "scope_gate", - owner: "scope", - type: "class:group", - channel: "", - incoming: "any", - targetKind: "none", - target: "", - timing: "inherit", - scopeGate: "bypass_global", - priority: 1, - }, - { - ...defaultRule("bot-requires-region"), - phase: "scope_gate", - owner: "scope", - type: "class:group", - channel: "#bot", - incoming: "any", - targetKind: "none", - target: "", - timing: "inherit", - scopeGate: "require_allowed", - stop: "phase", - priority: 200, - }, - ...clone(EXAMPLES.blackhole), + ...clone(EXAMPLES.channel_scope), + ...clone(EXAMPLES.prefix_rate), ...clone(EXAMPLES.moderation), - ...clone(EXAMPLES.system), - { - ...defaultRule("weak-signal-slow-queue"), - phase: "schedule", - owner: "filter", - type: "any", - channel: "", - incoming: "any", - snrMax: -8, - targetKind: "none", - target: "", - timing: "slow", - queue: "low", - retryBucket: "bucket:3", - retryAttempts: 2, - priority: 80, - }, + ...clone(EXAMPLES.high_traffic), + ...clone(EXAMPLES.factory), ]), }); @@ -1160,8 +1108,6 @@ pathPrefix: field("path-prefix").value, sender: field("sender").value, tempRadio: field("temp-radio").value, - snrMin: field("snr-min").value, - snrMax: field("snr-max").value, verdict: field("verdict").value, scopeGate: field("scope-gate").value, targetKind: field("target-kind").value, @@ -1195,8 +1141,6 @@ "path-prefix": rule.pathPrefix, sender: rule.sender, "temp-radio": rule.tempRadio, - "snr-min": rule.snrMin, - "snr-max": rule.snrMax, verdict: rule.verdict, "scope-gate": rule.scopeGate, "target-kind": rule.targetKind, @@ -1225,6 +1169,43 @@ field("retry-attempts").disabled = field("retry-bucket").value === "none"; } + function suggestCompatibleExecution(element) { + const name = element.getAttribute("data-field"); + if (name === "scope-gate" && field("scope-gate").value !== "unchanged") { + setValue("phase", "scope_gate"); + setValue("owner", "scope"); + setValue("target-kind", "none"); + setValue("retry-bucket", "none"); + setValue("sender", ""); + } else if (name === "target-kind" && field("target-kind").value !== "none") { + setValue("phase", "rewrite"); + setValue("scope-gate", "unchanged"); + setValue("retry-bucket", "none"); + setValue("sender", ""); + if (field("target-kind").value === "region" && field("owner").value === "filter") { + setValue("owner", "scope"); + } + } else if (name === "retry-bucket" && field("retry-bucket").value !== "none") { + setValue("phase", "schedule"); + setValue("owner", "filter"); + setValue("scope-gate", "unchanged"); + setValue("target-kind", "none"); + setValue("sender", ""); + } else if (name === "sender" && clean(field("sender").value)) { + setValue("phase", "content"); + setValue("owner", "filter"); + setValue("scope-gate", "unchanged"); + setValue("target-kind", "none"); + setValue("retry-bucket", "none"); + } else if ((name === "verdict" && field("verdict").value === "drop") + || (name === "rate" && clean(field("rate").value))) { + if (field("target-kind").value === "none") setValue("phase", "forward"); + if (field("owner").value === "scope") { + setValue("owner", field("target-kind").value === "region" ? "admin" : "filter"); + } + } + } + function renderWarnings(container, warnings) { container.replaceChildren(); warnings.forEach((warning) => { @@ -1389,7 +1370,6 @@ regionName: packetField("region-name").value, sender: packetField("sender").value, tempRadio: packetField("temp-radio").checked, - snr: packetField("snr").value, blacklist: packetField("blacklist").value === "yes", buckets: packetField("buckets").value, loopLevel: packetField("loop-level").value, @@ -1442,8 +1422,14 @@ } root.querySelectorAll("[data-field]").forEach((element) => { - element.addEventListener("input", updateLive); - element.addEventListener("change", updateLive); + element.addEventListener("input", () => { + suggestCompatibleExecution(element); + updateLive(); + }); + element.addEventListener("change", () => { + suggestCompatibleExecution(element); + updateLive(); + }); }); root.querySelector("[data-role='reset-form']").addEventListener("click", resetForm); saveButton.addEventListener("click", () => { @@ -1512,7 +1498,6 @@ packetField("scope-name").value = ""; packetField("region-name").value = ""; packetField("sender").value = ""; - packetField("snr").value = "6"; packetField("blacklist").value = "no"; packetField("buckets").value = ""; packetField("loop-level").value = "0"; diff --git a/docs/_stylesheets/filter_tool.css b/docs/_stylesheets/filter_tool.css index bf05f6df..29dd7ed8 100644 --- a/docs/_stylesheets/filter_tool.css +++ b/docs/_stylesheets/filter_tool.css @@ -31,6 +31,25 @@ display: none !important; } +.filter-management-safety { + margin-bottom: 1rem; + padding: 0.75rem 0.9rem; + border-left: 0.25rem solid var(--md-primary-fg-color); + border-radius: 0.25rem; + background: var(--filter-accent); +} + +.filter-management-safety strong { + display: block; + margin-bottom: 0.2rem; +} + +.filter-management-safety p { + margin: 0; + font-size: 0.78rem; + line-height: 1.5; +} + .filter-tool-toolbar { display: flex; flex-wrap: wrap; @@ -45,6 +64,46 @@ var(--md-default-bg-color); } +.filter-example-heading { + display: grid; + flex: 1 0 100%; + gap: 0.15rem; +} + +.filter-example-heading span { + color: color-mix(in srgb, var(--md-default-fg-color) 64%, transparent); + font-size: 0.72rem; +} + +.filter-example-grid { + display: grid; + flex: 1 0 100%; + grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); + gap: 0.55rem; +} + +.filter-tool .filter-example-grid button { + display: grid; + gap: 0.3rem; + align-content: start; + min-height: 4.25rem; + text-align: left; +} + +.filter-example-grid button span { + line-height: 1.3; +} + +.filter-example-grid button code { + color: color-mix(in srgb, var(--md-default-fg-color) 65%, transparent); + background: transparent; + font-size: 0.61rem; + font-weight: 500; + line-height: 1.35; + overflow-wrap: anywhere; + white-space: normal; +} + .filter-tool-grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(18rem, 0.9fr); @@ -229,6 +288,24 @@ line-height: 1.48; } +.filter-advanced { + margin-top: 1rem; + padding: 0.7rem 0.8rem; + border: 1px solid var(--filter-border); + border-radius: 0.48rem; + background: var(--filter-softer); +} + +.filter-advanced summary { + font-size: 0.78rem; + font-weight: 800; + cursor: pointer; +} + +.filter-advanced[open] summary { + margin-bottom: 0.65rem; +} + .filter-live-preview { display: grid; gap: 0.6rem; diff --git a/docs/filter_tool.md b/docs/filter_tool.md index 808e5f1c..22d04dd8 100644 --- a/docs/filter_tool.md +++ b/docs/filter_tool.md @@ -20,14 +20,70 @@ 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. +
+
+ Remote login and direct routes +

+ 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. +

+
- Load an example: - - - - - +
+ Drop-in replacements for documented settings + Choose one to load its proposed-engine equivalent. +
+
+ + + + + + + + + + +
@@ -40,65 +96,8 @@ drafts are not uploaded anywhere.
-

Identity and execution

+

Common match settings

- - - - - - -
- -

Match immutable receive-time facts

-
- - -
-

Accumulate forwarding actions

+

Common actions

+

+ Choosing a phase-specific action automatically selects a compatible + phase and ACL owner. Open the advanced section to inspect or override + those choices. +

+
+ Advanced execution, ownership, and flood route +

+ Direct packets already carry a supplied path and are intentionally + outside this engine. The route condition below only distinguishes + unscoped floods from transport-scoped floods. +

+
+ + + + + + + +
+
+
Readable policy definition @@ -326,7 +384,6 @@ drafts are not uploaded anywhere. -