// Trace: follow packets through every table's rules, and see what // decided their fate. import { h, mount, input, select, field, errorBox, badge } from "../ui.js"; import { get, api, can } from "../api.js"; import { onLeave } from "../app.js"; export async function render(el, params, query) { const address = input({ placeholder: "192.168.1.50, or 2001:db8:1::/64", class: "mono", value: (query && query.get && query.get("address")) || "" }); const port = input({ placeholder: "443", class: "short", type: "number", min: 1, max: 65535 }); const protocol = select([["", "any"], "tcp", "udp", "sctp", "icmp", "icmpv6"], "", { class: "inline" }); const seconds = input({ value: "60", class: "short", type: "number", min: 5, max: 600 }); const err = h("div"); const status = h("div"); const packets = h("div"); const write = can("config", true); const startBtn = h("button", { class: "primary", onclick: start }, "Start tracing"); const stopBtn = h("button", { class: "hidden", onclick: stop }, "Stop"); let timer = null; async function start() { mount(err); try { await api("POST", "/trace", { body: { address: address.value.trim(), port: parseInt(port.value || "0", 10), protocol: protocol.value, seconds: parseInt(seconds.value || "60", 10) } }); poll(); } catch (e) { mount(err, errorBox(e)); } } async function stop() { try { show(await api("DELETE", "/trace")); } catch (e) { mount(err, errorBox(e)); } } async function poll() { clearTimeout(timer); let st; try { st = await get("/trace"); } catch (e) { mount(err, errorBox(e)); return; } show(st); if (st.running) timer = setTimeout(poll, 1000); } function show(st) { startBtn.classList.toggle("hidden", st.running || !write); stopBtn.classList.toggle("hidden", !st.running || !write); const what = [st.input.address, st.input.protocol, st.input.port ? "port " + st.input.port : ""].filter(Boolean).join(" "); mount(status, st.started && st.started !== "0001-01-01T00:00:00Z" ? h("div", { class: "msg " + (st.error ? "err" : st.running ? "info" : "ok") }, st.running ? ["Tracing ", h("strong", null, what), " until " + new Date(st.ends).toLocaleTimeString() + ". Make the traffic happen now."] : ["Traced ", h("strong", null, what), ": " + (st.packets.length === 1 ? "1 packet" : st.packets.length + " packets") + "."], st.error ? h("div", null, st.error) : null) : null); mount(packets, st.packets.length ? st.packets.slice().reverse().map(packet) : st.running ? h("div", { class: "empty" }, "No packets yet.") : null); } const hostPort = (a, p) => !a ? "" : !p ? a : a.includes(":") ? "[" + a + "]:" + p : a + ":" + p; function packet(p) { const head = [p.protocol, p.tcp_flags, p.icmp_type].filter(Boolean).join(" "); const decided = p.decided_by.label || [p.decided_by.table, p.decided_by.chain, p.decided_by.kind].filter(Boolean).join(" "); return h("div", { class: "panel trace-packet" }, h("div", { class: "trace-head" }, badge(p.verdict, p.verdict === "dropped" ? "err" : "ok"), " ", h("span", { class: "mono" }, hostPort(p.source, p.source_port), " → ", hostPort(p.destination, p.destination_port)), " ", h("span", { class: "muted" }, head, p.in ? " · in " + p.in : "", p.out ? " · out " + p.out : ""), h("span", { class: "muted small trace-time" }, new Date(p.time).toLocaleTimeString())), decided ? h("div", { class: "small" }, p.verdict === "dropped" ? "Dropped by " : "Accepted by ", h("strong", null, decided)) : null, p.would_drop ? h("div", { class: "small" }, badge("shadow mode", "warn"), " Housecarl would have dropped it: ", h("strong", null, p.would_drop.label || [p.would_drop.chain, p.would_drop.mark].filter(Boolean).join(" "))) : null, h("table", { class: "trace-steps" }, h("tbody", null, p.steps.map((s) => h("tr", { class: /^(drop|reject)/.test(s.verdict) || /\[would (drop|reject)\]/.test(s.rule || "") ? "step-drop" : s.verdict === "accept" ? "step-accept" : "" }, h("td", { class: "muted small" }, s.table + " " + s.chain), h("td", null, s.label ? (s.mark && s.mark.startsWith("r") ? h("a", { href: "#/rules?chain=" + s.chain }, s.label) : s.label) : s.kind === "policy" ? s.chain + " policy" : h("span", { class: "mono small" }, s.rule)), h("td", { class: "small" }, s.verdict)))))); } mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Trace"), h("div", { class: "muted" }, "Follows packets through the whole ruleset, Housecarl's rules and every other table (Docker, fail2ban…): each rule and policy they meet, and what accepted or dropped them. For “why doesn't this get through?”"))), write ? h("div", { class: "panel" }, h("div", { class: "form-grid" }, field("Address", address, "Packets from or to it (either way). An address or a prefix."), field("Port", h("div", null, port, " ", protocol), "Packets from or to it. Empty: any."), field("For", h("div", null, seconds, " seconds"), "At most 600; it stops by itself after 200 packets.")), h("div", { class: "actions form-actions" }, startBtn, stopBtn), err) : null, status, packets); await poll(); onLeave(() => clearTimeout(timer)); }