Files
jarl/internal/web/static/assets/pages/acls.js
T
adamandClaude Opus 5.5 e2e5007f94 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>
2026-10-11 23:17:14 +01:00

57 lines
3.5 KiB
JavaScript

import { h, mount, table, field, input, textarea, toast, errorBox, confirmDialog, modal, lines } from "../ui.js";
import { amlContext, suggestions } from "../aml.js";
import { get, change, describeResult } from "../api.js";
import { rerender } from "../app.js";
export async function render(el) {
const acls = await get("/acls");
mount(el,
h("div", { class: "page-head" },
h("div", { class: "grow" }, h("h1", null, "ACLs"),
h("div", { class: "muted" }, "Named address match lists, used in options such as allow-query, allow-recursion, allow-transfer and in views' match-clients.")),
h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => aclDialog(null) }, "New ACL"))),
h("div", { class: "msg info small" }, "Built-in ACLs: any, none, localhost (this server's addresses), localnets (networks this server is directly attached to). Elements are checked in order and the first match wins, so put exceptions (!addr) before broader prefixes."),
h("div", { class: "panel" }, table([
{ label: "Name", render: (a) => h("strong", { class: "mono" }, a.name) },
{ label: "Elements", render: (a) => a.elements.join("; ") || h("span", { class: "muted" }, "(empty: matches nothing)"), tdClass: "data" },
{ label: "Used by", render: (a) => a.used_by.length ? a.used_by.join(", ") : h("span", { class: "muted" }, "unused"), tdClass: "small" },
{ label: "Comment", render: (a) => h("span", { class: "muted" }, a.comment || "") },
{ label: "", render: (a) => h("span", null,
h("button", { class: "small", onclick: () => aclDialog(a) }, "Edit"), " ",
h("button", { class: "small danger", disabled: a.used_by.length > 0, title: a.used_by.length ? "Still in use" : "", onclick: () => del(a) }, "Delete")), tdClass: "actions-cell" },
], acls, { empty: "No ACLs yet." })));
}
async function aclDialog(a) {
const err = h("div");
const ctx = await amlContext();
const name = input({ value: a ? a.name : "", placeholder: "homelab", class: "mono" });
const elems = textarea(a ? a.elements.join("\n") : "", { class: "mono", rows: 8, placeholder: "192.168.1.0/24\n10.0.0.0/8\n!10.0.0.66\nkey xfer-key\nlocalhost" });
const comment = input({ value: a ? a.comment : "" });
await modal(a ? "Edit ACL " + a.name : "New ACL", h("div", null, h("div", { class: "form-grid" },
field("Name", name, a ? "Renaming updates every reference automatically." : ""),
field("Elements", h("div", null, elems, suggestions(elems, ctx, { exclude: a ? a.name : "" })), "One per line: address, prefix (host bits must be zero), range (10.0.10.101-10.0.10.199), !negated, key NAME, another ACL's name, any/none/localhost/localnets."),
field("Comment", comment)), err), [
{ label: "Cancel", value: null },
{ label: a ? "Save" : "Create ACL", kind: "primary", onclick: async () => {
const data = { name: name.value.trim(), elements: lines(elems.value), comment: comment.value };
try {
const r = a ? await change("PATCH", "/acls/" + a.id, data) : await change("POST", "/acls", data);
toast(describeResult(r));
(r.warnings || []).forEach((w) => toast(w, "warn", 8000));
rerender();
return true;
} catch (e) { mount(err, errorBox(e)); return false; }
} },
], { sticky: true });
}
async function del(a) {
if (!(await confirmDialog("Delete ACL", "Delete ACL " + a.name + "?", "Delete", "danger"))) return;
try {
const r = await change("DELETE", "/acls/" + a.id);
toast(describeResult(r));
rerender();
} catch (e) { toast(e.message, "err", 8000); }
}