Import: keep what rulesets mean, name zones as asked
Fixes from an audit of the importer, each of which could change what an imported ruleset does: two base chains on one hook imported their rules twice (and hoisted the second's built-ins), now in priority order with the stricter policy; raw rules from a jumped-to chain lost the jump's condition, now put in front; a jump and a rule matching one field are combined only when one's addresses lie within the other's; a second match on a field replaced the first, and != on hop limit and TCP flags read as ==, now kept raw; a forward accept without ct status dnat no longer folds into the port forward, nor an interface-limited IPv6 accept into an open pinhole; raw rules' set references are renamed (NAME_v4/_v6) or written out, so nftables takes them; DNATs with matches a forward can't hold are left out with a note. Sets with flags timeout become dynamic sets; a DNAT of several ports one forward per port. Zone names: --zone IFACE=NAME (and on the Import page), VLAN defines, more suffixes, and a note on where each name came from. testdata/audit.nft covers the cases, checked with nft 1.1.3. make test and make e2e (246 checks) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c17f5cd9f0
commit
47bae07d42
15 files changed
+823
-117
No files matched your search
@@ -8,14 +8,21 @@ export async function render(el) {
|
||||
const text = textarea("", { rows: 4, class: "mono", placeholder: "optional, the output of: sudo nft -a list ruleset (keeps what Housecarl can't express as raw rules)" });
|
||||
const defines = textarea("", { rows: 4, class: "mono", placeholder: "optional, your /etc/nftables.conf: its defines name the sets" });
|
||||
const table = input({ placeholder: "inet main (when there are several)", class: "mono" });
|
||||
const zones = input({ placeholder: "enp2s0=lan, wg0=vpn (optional)", class: "mono" });
|
||||
const replace = checkbox("replace Housecarl's configuration", false);
|
||||
const out = h("div");
|
||||
const run = async (dry) => {
|
||||
mount(out);
|
||||
const zoneNames = {};
|
||||
for (const part of zones.value.split(",").map((s) => s.trim()).filter(Boolean)) {
|
||||
const [iface, name] = part.split("=").map((s) => (s || "").trim());
|
||||
if (!iface || !name) { mount(out, h("div", { class: "msg err" }, "Zone names: write each as interface=name (enp2s0=lan).")); return; }
|
||||
zoneNames[iface] = name;
|
||||
}
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(json.value); } catch { mount(out, h("div", { class: "msg err" }, "The ruleset isn't JSON: paste the output of nft -j list ruleset.")); return; }
|
||||
try {
|
||||
const r = await api("POST", "/import/nft", { body: { json: parsed, text: text.value, defines: defines.value, table: table.value.trim(), replace: replace.input.checked },
|
||||
const r = await api("POST", "/import/nft", { body: { json: parsed, text: text.value, defines: defines.value, table: table.value.trim(), zones: zoneNames, replace: replace.input.checked },
|
||||
query: dry ? { dry_run: 1 } : undefined });
|
||||
const st = r.state;
|
||||
mount(out, h("div", { class: "msg " + (dry ? "info" : "ok") },
|
||||
@@ -34,7 +41,8 @@ export async function render(el) {
|
||||
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Import a ruleset"),
|
||||
h("div", { class: "muted" }, "Turns an nftables ruleset into zones, sets, services, rules, port forwards and NAT. On the firewall, 'sudo hc import --defines /etc/nftables.conf -n' does the same from the command line."))),
|
||||
h("div", { class: "panel" }, h("div", { class: "form-grid" },
|
||||
field("Ruleset (JSON)", json), field("Ruleset (text)", text), field("Ruleset file", defines), field("Table", table), h("label"), replace),
|
||||
field("Ruleset (JSON)", json), field("Ruleset (text)", text), field("Ruleset file", defines), field("Table", table),
|
||||
field("Zone names", zones, "Name zones by what the network is for; interfaces given one name share a zone. Otherwise a define holding the interface names it, or the interface's own name."), h("label"), replace),
|
||||
h("div", { class: "actions form-actions" },
|
||||
h("button", { onclick: () => run(true) }, "Preview"),
|
||||
h("button", { class: "primary", onclick: () => run(false) }, "Import"))),
|
||||
|
||||
Reference in new issue
Block a user