Reverse lookups (internal/names, POST /names) put a name beside each inside address in connections, the log, quota usage, the dashboard's quotas and the statistics' devices and blocked sources, in the web UI and hc (--no-names to leave them out). Only inside addresses are looked up: an inside zone's networks, and private ones outside the external zones'. Names are cached (names.cache, 10m) and never stored; names.resolver picks the DNS server. Also: hc stats top devices listed each device twice; the dashboard's quotas named the rule's ID twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
2.8 KiB
JavaScript
41 lines
2.8 KiB
JavaScript
// The log: packets rules (with logging on) and the policies logged,
|
|
// refreshed every few seconds.
|
|
import { h, mount, input, debounce, badge } from "../ui.js";
|
|
import { get, state } from "../api.js";
|
|
import { onLeave } from "../app.js";
|
|
import { lookupNames, named } from "../widgets.js";
|
|
|
|
export async function render(el, params, query) {
|
|
const search = input({ type: "search", placeholder: "address, interface, rule…", value: query.get("q") || "" });
|
|
const out = h("div");
|
|
let live = true;
|
|
const pause = h("button", { onclick: () => { live = !live; pause.textContent = live ? "Pause" : "Resume"; } }, "Pause");
|
|
const addr = (a, port) => port ? (a.includes(":") ? "[" + a + "]:" + port : a + ":" + port) : a;
|
|
const draw = async () => {
|
|
const es = await get("/log", { limit: 300, q: search.value.trim() });
|
|
const names = await lookupNames(es.flatMap((e) => [e.source, e.destination]));
|
|
mount(out, es.length ? h("div", { class: "table-wrap" }, h("table", { class: "activity" },
|
|
h("thead", null, h("tr", null, ["Time", "Why", "In", "Out", "From", "To", "Protocol"].map((t) => h("th", null, t)))),
|
|
h("tbody", null, es.map((e) => h("tr", null,
|
|
h("td", { class: "mono small" }, new Date(e.time).toLocaleTimeString(undefined, { hourCycle: "h23" })),
|
|
h("td", null, e.mark && e.mark.startsWith("r") ? h("a", { href: "#/rules?rule=" + e.mark.slice(1), title: "rule #" + e.mark.slice(1) }, e.prefix) : e.prefix,
|
|
e.shadow && /^(drop|reject)$/.test(e.verdict) ? [" ", badge("would " + e.verdict, "warn")] : null),
|
|
h("td", { class: "mono" }, e.in || ""), h("td", { class: "mono" }, e.out || ""),
|
|
h("td", { class: "mono" }, named(names, e.source, addr(e.source, e.source_port))),
|
|
h("td", { class: "mono" }, named(names, e.destination, addr(e.destination, e.destination_port))),
|
|
h("td", null, e.protocol, e.icmp_type ? " " + e.icmp_type : "", e.tcp_flags ? h("span", { class: "muted small" }, " " + e.tcp_flags) : null)))))) :
|
|
h("div", { class: "empty" }, state.me.mode === "shadow"
|
|
? "Nothing logged yet. In shadow mode, what the policies would drop is logged; turn logging on for a rule with Edit → Log."
|
|
: "Nothing logged yet. Turn logging on for a rule (Edit → Log), or for what the policies drop (Settings)."));
|
|
};
|
|
search.addEventListener("input", debounce(draw, 300));
|
|
mount(el,
|
|
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Log"),
|
|
h("div", { class: "muted" }, "Packets logged by rules and policies, newest first (the last 2000 are kept)."))),
|
|
h("div", { class: "filter-row" }, search, pause),
|
|
h("div", { class: "panel" }, out));
|
|
await draw();
|
|
const t = setInterval(() => { if (live && !document.hidden) draw().catch(() => {}); }, 3000);
|
|
onLeave(() => clearInterval(t));
|
|
}
|