Address ranges in address match lists (IPv4 and IPv6)

ACLs, views' match lists, options with address match lists and blocking
client groups take ranges such as 10.0.10.101-10.0.10.199 or
2001:db8::100-2001:db8::1ff (negated, or with spaces around the dash, too).
Jarl stores the range as written and writes the fewest prefixes that cover
exactly that range to named.conf, with the range as a comment. New package
internal/iprange; namedconf gets an AMRange element (a range used to parse
as an ACL name), ExpandRanges and statement comments. Mistakes are refused
when entered, validation sees what ranges cover, and IPv6 ranges in the
IPv4-mapped space get a warning. The web UI shows what a range becomes
while typing (GET /catalog/ranges).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-11 23:17:14 +01:00
1 parent 49b1c872c9
commit e2e5007f94
32 files changed
+905 -31

No files matched your search

+7 -5
View File
@@ -3,6 +3,7 @@ import { h, mount, table, field, input, select, checkbox, toast, errorBox, badge
import { get, change, describeResult, draftQuery, can, api } from "../api.js";
import { rerender } from "../app.js";
import { pendingBadge } from "./zones.js";
import { rangeHints } from "../aml.js";
// Well-known lists in Adblock format (they say which entries cover
// subdomains). Sizes change; these are rough guides.
@@ -355,7 +356,7 @@ async function del(path, what) {
function devicesPanel(groups, steps, views, autoViews, writable) {
const live = steps.filter((s) => s.pending !== "delete");
const name = input({ placeholder: "kids", class: "mono", style: { width: "9em" } });
const members = input({ placeholder: "10.10.0.50 10.10.0.51 or an ACL", class: "mono" });
const members = input({ placeholder: "10.10.0.50 10.10.0.20-10.10.0.29 or an ACL", class: "mono" });
const step = stepSelect(steps, live.length ? live[0].name : "off", true);
const vp = viewPicker(views, autoViews);
const err = h("div");
@@ -368,7 +369,7 @@ function devicesPanel(groups, steps, views, autoViews, writable) {
} catch (e) { mount(err, errorBox(e)); }
} },
h("div", { class: "f" }, h("label", null, "Group"), name),
h("div", { class: "f grow" }, h("label", null, "Devices"), members),
h("div", { class: "f grow" }, h("label", null, "Devices"), members, rangeHints(members)),
h("div", { class: "f" }, h("label", null, "Step"), step),
vp.el,
h("div", { class: "f" }, h("label", null, " "), h("button", { class: "primary", type: "submit" }, "Add group")));
@@ -389,9 +390,10 @@ function devicesPanel(groups, steps, views, autoViews, writable) {
].filter(Boolean), groups, { empty: "No groups: every device is at normal.", rowClass: (g) => g.pending ? "pending-" + g.pending : "" }));
}
// splitMembers turns "a, b c" into a list.
// splitMembers turns "a, b c" into a list; a range may have spaces
// around its dash.
function splitMembers(s) {
return s.split(/[\s,;]+/).map((x) => x.trim()).filter(Boolean);
return s.replace(/\s*-\s*/g, "-").split(/[\s,;]+/).map((x) => x.trim()).filter(Boolean);
}
// stepsLine shows the steps in order, with controls for the ones above
@@ -463,7 +465,7 @@ function editGroup(g, steps, views, autoViews) {
const vp = viewPicker(views, autoViews, ownViews(g));
editDialog("Edit client group " + g.name, "/blocking/groups/" + g.id, [
["Name", name],
["Devices", members, "Addresses, networks (10.10.0.0/24) or ACLs, separated by spaces. A device can be in one group; a more specific address in another group wins."],
["Devices", h("div", null, members, rangeHints(members)), "Addresses, networks (10.10.0.0/24), ranges (10.10.0.20-10.10.0.49) or ACLs, separated by spaces. A device can be in one group; a more specific address in another group wins."],
["Step", step],
vp.picker ? ["Views", vp.picker, "None ticked: every view with blocking."] : null,
["Comment", comment],