// Connections: what the kernel tracks, and closing them so the rules // check them again. import { h, mount, input, debounce, toast, confirmDialog, badge } from "../ui.js"; import { get, api, can } from "../api.js"; import { onLeave } from "../app.js"; import { fmtSeconds, lookupNames, named } from "../widgets.js"; export async function render(el, params, query) { const search = input({ type: "search", placeholder: "address, :port, protocol, state…", value: query.get("q") || "" }); const out = h("div"); const count = h("span", { class: "muted small" }); const write = can("config", true); let shown = []; const hostPort = (a, p) => !p ? a : a.includes(":") ? "[" + a + "]:" + p : a + ":" + p; const close = async (list, what) => { if (!await confirmDialog("Close connections", "Close " + what + "? Their next packet is checked by the rules as they are now: what they block is stopped, what they allow carries on.", "Close", "danger")) return; try { const r = await api("POST", "/connections/close", { body: list ? { connections: list } : { search: search.value.trim() } }); toast("Closed " + r.closed + (r.closed === 1 ? " connection" : " connections")); draw(); } catch (e) { toast(e.message, "err"); } }; const draw = async () => { shown = await get("/connections", { q: search.value.trim(), limit: 500 }); const names = await lookupNames(shown.flatMap((c) => [c.source, c.destination, c.dnat_to])); count.textContent = shown.length === 1 ? "1 connection" : shown.length + " connections"; mount(out, shown.length ? h("div", { class: "table-wrap" }, h("table", null, h("thead", null, h("tr", null, ["Protocol", "From", "To", "NAT", "State", "Expires", ""].map((t) => h("th", null, t)))), h("tbody", null, shown.map((c) => h("tr", null, h("td", null, c.protocol), h("td", { class: "mono" }, named(names, c.source, hostPort(c.source, c.source_port))), h("td", { class: "mono" }, named(names, c.destination, hostPort(c.destination, c.destination_port))), h("td", { class: "mono small" }, c.dnat_to ? ["to ", named(names, c.dnat_to, hostPort(c.dnat_to, c.dnat_port || c.destination_port))] : "", c.snat_to ? (c.dnat_to ? ", " : "") + "as " + c.snat_to : ""), h("td", null, c.state || "", c.assured ? null : [" ", badge("one way", "")]), h("td", { class: "small" }, fmtSeconds(c.timeout)), h("td", { class: "actions-cell" }, write ? h("button", { class: "small reveal", onclick: () => close([c], "this connection") }, "Close") : null)))))) : h("div", { class: "empty" }, search.value.trim() ? "No connection matches." : "No connections tracked.")); }; search.addEventListener("input", debounce(draw, 300)); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Connections"), h("div", { class: "muted" }, "The connections the firewall tracks. Their packets are let through by the built-in for established connections, without the rules being checked again, so a rule added later doesn't stop them. Close them, and their next packet is checked by the rules as they are now.")), write ? h("div", { class: "actions" }, h("button", { class: "danger", onclick: () => close(null, search.value.trim() ? "every connection matching “" + search.value.trim() + "”" : "every connection") }, "Close all shown")) : null), h("div", { class: "filter-row" }, search, count), h("div", { class: "panel" }, out)); await draw(); const t = setInterval(() => { if (!document.hidden && document.activeElement !== search) draw().catch(() => {}); }, 5000); onLeave(() => clearInterval(t)); }