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>
43 lines
3.2 KiB
JavaScript
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);
|
|
}
|