Rule dialog: only the zone fields the chain can use

Input and early have no outgoing interface, output no incoming one, so
the dialog hides those fields (they could only be refused) and a zone
left in a hidden field isn't sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-22 23:51:54 +01:00
1 parent 0e958289de
commit e841986a86
3 files changed
+19 -4

No files matched your search

+4
View File
@@ -115,6 +115,10 @@ All notable changes to Housecarl are documented here. The format follows
ruleset, for entries made before this version too: the configuration
before and after it is got back from the current one and rendered by
this version.
- The rule dialog shows only the zone fields its chain can use: no
"Going out on" for input and early, no "Coming in on" for output
(they could only be refused). A zone left in a field the chain hides
isn't sent.
- History lists a changed setting by its own fields
(`builtins.drop_invalid: false → true`), on the web page and in `hc
history show`, instead of the whole `builtins` before and after.
@@ -260,6 +260,9 @@ export async function ruleDialog(r, chain) {
r = r || { chain, action: "accept" };
const ch = select(["input", "forward", "output", "early"], r.chain || chain);
const match = matchEditor(r, n);
const chainZones = () => match.zones(ch.value !== "output", ch.value === "forward" || ch.value === "output");
ch.addEventListener("change", chainZones);
chainZones();
const action = select([["accept", "accept"], ["drop", "drop"], ["reject", "reject"], ["log", "log only (and go on)"]], r.action);
const rejectWith = select([["", "with an ICMP error"], ["tcp-reset", "with a TCP reset (TCP rules only)"]], r.reject_with || "");
const every = checkbox("every packet: also stop connections already open, not just new ones", r.every_packet);
@@ -371,6 +374,7 @@ export async function ruleDialog(r, chain) {
const addersLabel = h("label");
sections.raw.onShow = (on) => {
for (const n of [...matchNodes, ...actionField, ...logField]) n.classList.toggle("hidden", on);
if (!on) chainZones();
for (const [k, sec] of Object.entries(sections)) {
if (k === "raw") continue;
if (on && sec.open) for (const n of sec.nodes) n.classList.add("hidden");
+11 -4
View File
@@ -77,9 +77,11 @@ export function matchEditor(m, n, opts = {}) {
const notSrc = checkbox("any source but these", m.not_sources), notDst = checkbox("any destination but these", m.not_destinations);
const svc = listEditor(m.services, { suggestions: n.services, placeholder: "ssh, https, tcp/8080, udp/1000-2000, icmp/echo-request", addLabel: "Add service" });
const fam = select([["", "IPv4 and IPv6"], ["ipv4", "IPv4 only"], ["ipv6", "IPv6 only"]], m.family || "");
const inField = opts.in !== false ? field("Coming in on", h("div", null, inZ, notIn)) : null;
const outField = opts.out !== false ? field("Going out on", h("div", null, outZ, notOut)) : null;
const fields = [
opts.in !== false ? field("Coming in on", h("div", null, inZ, notIn)) : null,
opts.out !== false ? field("Going out on", h("div", null, outZ, notOut)) : null,
inField,
outField,
field("From", h("div", null, src, notSrc), "Sets, addresses, prefixes or ranges, IPv4 and IPv6 mixed. Empty: any."),
field("To", h("div", null, dst, notDst)),
field("Services", svc, "Named services (built in, or yours) or entries such as tcp/8080. Empty: any traffic."),
@@ -88,10 +90,15 @@ export function matchEditor(m, n, opts = {}) {
fields.value = () => {
const v = { sources: src.value(), not_sources: notSrc.input.checked, destinations: dst.value(), not_destinations: notDst.input.checked,
services: svc.value(), family: fam.value };
if (opts.in !== false) Object.assign(v, { in_zones: inZ.value(), not_in: notIn.input.checked });
if (opts.out !== false) Object.assign(v, { out_zones: outZ.value(), not_out: notOut.input.checked });
// A hidden zone field (the chain has no such interface) matches nothing.
if (inField) Object.assign(v, inField[1].classList.contains("hidden") ? { in_zones: [], not_in: false } : { in_zones: inZ.value(), not_in: notIn.input.checked });
if (outField) Object.assign(v, outField[1].classList.contains("hidden") ? { out_zones: [], not_out: false } : { out_zones: outZ.value(), not_out: notOut.input.checked });
return v;
};
// zones shows the zone fields a chain can use: input and early have no
// outgoing interface, output no incoming one.
const show = (f, on) => f && f.filter(Boolean).forEach((e) => e.classList.toggle("hidden", !on));
fields.zones = (inOn, outOn) => { show(inField, inOn); show(outField, outOn); };
return fields;
}