diff --git a/CHANGELOG.md b/CHANGELOG.md index b004649..db708d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/internal/web/static/assets/pages/rules.js b/internal/web/static/assets/pages/rules.js index 6062f00..6563f73 100644 --- a/internal/web/static/assets/pages/rules.js +++ b/internal/web/static/assets/pages/rules.js @@ -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"); diff --git a/internal/web/static/assets/widgets.js b/internal/web/static/assets/widgets.js index d30c087..d06c6ca 100644 --- a/internal/web/static/assets/widgets.js +++ b/internal/web/static/assets/widgets.js @@ -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; }