Files
housecarl/internal/web/static/assets/pages/import.js
T
abrown38andClaude Opus 5.5 e4efdf9c78 Confirm changes automatically from the client that made them
A change made over the network answers with a one-time confirm_token
and closes its connection (HTTP/2: a GOAWAY). The web UI and hc send the
token straight back to /confirm, which accepts it only for the newest
change, from the same user and API token, the same address, and over a
new connection: proof that the client can still reach the API through
the new ruleset (an established connection would get through anyway). A
change that cuts its client off can't be confirmed so, and rolls back by
itself. The setting manual_confirm (hc settings set --auto-confirm off)
and hc --no-confirm leave confirming to a person. Changes through the
local socket still need no confirmation. e2e: 136 checks, among them a
lockout change rolled back without anyone confirming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:23:28 +01:00

43 lines
3.2 KiB
JavaScript

// Import: an existing nftables ruleset as Housecarl's configuration.
import { h, mount, field, textarea, input, checkbox, errorBox, copyBox } from "../ui.js";
import { api } from "../api.js";
import { refreshProbation, autoConfirm } from "../app.js";
export async function render(el) {
const json = textarea("", { rows: 6, class: "mono", placeholder: "the output of: sudo nft -j list ruleset" });
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 replace = checkbox("replace Housecarl's configuration", false);
const out = h("div");
const run = async (dry) => {
mount(out);
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 },
query: dry ? { dry_run: 1 } : undefined });
const st = r.state;
mount(out, h("div", { class: "msg " + (dry ? "info" : "ok") },
dry ? "Would import: " : "Imported: ",
`${st.zones.length} zones, ${st.sets.length} sets, ${st.services.length} services, ${st.rules.length} rules, ${st.forwards.length} port forwards, ${st.snats.length} source NAT rules, ${st.redirects.length} redirects.`,
r.confirm_by && !r.confirm_token ? " It is on probation: confirm it in the banner." : ""),
r.notes.length ? h("div", { class: "panel" }, h("h2", null, "Notes"), h("ul", null, r.notes.map((n) => h("li", null, n)))) : null,
r.raw.length ? h("div", { class: "panel" }, h("h2", null, "Kept as raw rules"), h("ul", { class: "mono small" }, r.raw.map((n) => h("li", null, n)))) : null,
!dry ? h("p", null, "Check the ", h("a", { href: "#/rules" }, "rules"), " and ", h("a", { href: "#/nat" }, "NAT"),
"; both rulesets apply until the old table goes: ", copyBox("sudo hc takeover \"inet main\"")) : null);
if (!dry && r.confirm_token) autoConfirm(r.confirm_token);
else if (!dry) refreshProbation();
} catch (e) { mount(out, errorBox(e)); }
};
mount(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),
h("div", { class: "actions form-actions" },
h("button", { onclick: () => run(true) }, "Preview"),
h("button", { class: "primary", onclick: () => run(false) }, "Import"))),
out);
}