- Connections: the kernel's tracked connections, read and closed over ctnetlink (internal/conntrack, raw netlink like the drop log), with their NAT and TCP state; closing one makes the rules check its next packet again (GET /connections, POST /connections/close; hc connections; Traffic → Connections). - Rules can check every packet (every_packet, --every-packet): before established connections are accepted, so a drop or reject also stops connections already open. They can't accept. - A packet tracer (internal/trace): a table of its own, inet housecarl-trace, marks the packets a filter picks, `nft monitor trace` reports every rule and policy they meet in every table, and the tracer names what accepted or dropped them, Housecarl's rules by name (POST, GET, DELETE /trace; hc trace; Traffic → Trace). One trace at a time, at most 200 packets and 20 a second; the table goes when it ends. e2e: 157 checks, among them an upload a normal block doesn't stop and an every-packet one does, and a trace naming the blocking rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
3.4 KiB
JavaScript
50 lines
3.4 KiB
JavaScript
// 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 } 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 });
|
|
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" }, hostPort(c.source, c.source_port)),
|
|
h("td", { class: "mono" }, hostPort(c.destination, c.destination_port)),
|
|
h("td", { class: "mono small" }, c.dnat_to ? "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));
|
|
}
|