internal/stats samples every minute what the kernel already counts: the
interfaces' counters (per zone too), Housecarl's table (hits per rule,
built-in and policy; drops by chain; quotas; per-device sets), the
connection tracking table, and housecarld's and the host's memory, CPU
and load. Logged drops feed top lists of sources and what they tried.
Aggregates go to a database of their own (stats.db; per minute 7 days,
per hour 90 days, per day 2 years), with the store and queries from
Jarl's.
Traffic per inside device is a setting (stats_per_device): counting sets
stats_up/down_v4/v6 and rules at the top of the forward chain, IPv6 per
/64, carried across reloads. Set names starting with meter_, conn_,
quota_ or stats_ are reserved (set_name_reserved).
GET /stats/{status,summary,timeline,top,hits,heatmap}, hc stats, a
Statistics page with Jarl's charts, two charts on the dashboard, and a
conntrack_full warning. e2e: 188 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
4.6 KiB
JavaScript
68 lines
4.6 KiB
JavaScript
// Settings: policies, built-ins, antispoofing, flowtable, probation.
|
|
import { h, mount, field, select, checkbox, input, errorBox, readOnly } from "../ui.js";
|
|
import { get, change, can } from "../api.js";
|
|
import { rerender } from "../app.js";
|
|
import { listEditor, fmtSeconds, parseSeconds } from "../widgets.js";
|
|
|
|
const builtinText = [
|
|
["loopback", "Accept everything on lo"],
|
|
["established", "Accept established and related connections (input and forward)"],
|
|
["drop_invalid", "Drop invalid packets (input and forward)"],
|
|
["icmpv6", "Accept the ICMPv6 IPv6 needs: errors, echo, neighbour discovery, MLD (RFC 4890)"],
|
|
["ping_internal", "Accept ping to the firewall from internal zones"],
|
|
["dhcpv6_client", "Accept DHCPv6 replies on external zones (your ISP's prefix delegation)"],
|
|
["antispoof", "Drop impossible sources arriving on external zones (below)"],
|
|
["mss_clamp", "Clamp TCP MSS to the route MTU on external zones (needed with PPPoE)"],
|
|
];
|
|
|
|
export async function render(el) {
|
|
const [s, ifs] = await Promise.all([get("/settings"), get("/interfaces")]);
|
|
const write = can("config", true);
|
|
const pol = (v) => select([["drop", "drop"], ["accept", "accept"]], v);
|
|
const input_ = pol(s.input_policy), forward = pol(s.forward_policy), output = pol(s.output_policy);
|
|
const logDrops = checkbox("log what the input and forward policies drop (rate-limited; the Log page)", s.log_policy_drops);
|
|
const bis = builtinText.map(([k, text]) => [k, checkbox(text, s.builtins[k])]);
|
|
const own = listEditor(s.own_prefixes, { placeholder: "2001:db8::/48", addLabel: "Add prefix" });
|
|
const v4 = listEditor(s.antispoof_v4, { addLabel: "Add" });
|
|
const v6 = listEditor(s.antispoof_v6, { addLabel: "Add" });
|
|
const flow = listEditor(s.flowtable_devices, { suggestions: ifs.map((i) => i.name), placeholder: "wan", addLabel: "Add device" });
|
|
const probation = input({ value: fmtSeconds(s.probation_seconds || 120), class: "short" });
|
|
const perDevice = checkbox("count traffic per inside device (IPv6 per /64), for the Statistics page's top devices: a counter for each address talking to an external zone", s.stats_per_device);
|
|
const manual = checkbox("a person confirms each change (otherwise the browser or hc that made it confirms it by itself, once it reaches Housecarl again over a new connection)", s.manual_confirm);
|
|
const err = h("div");
|
|
const save = async (dry) => {
|
|
mount(err);
|
|
const p = parseSeconds(probation.value);
|
|
if (p === null) { mount(err, h("div", { class: "msg err" }, "Write the probation as 2m or 90s.")); return; }
|
|
const builtins = {};
|
|
for (const [k, c] of bis) builtins[k] = c.input.checked;
|
|
try {
|
|
const r = await change("PATCH", "/settings", { input_policy: input_.value, forward_policy: forward.value, output_policy: output.value,
|
|
log_policy_drops: logDrops.input.checked, builtins, own_prefixes: own.value(), antispoof_v4: v4.value(), antispoof_v6: v6.value(),
|
|
flowtable_devices: flow.value(), probation_seconds: p === 120 ? 0 : p, manual_confirm: manual.input.checked, stats_per_device: perDevice.input.checked }, { query: dry ? { dry_run: 1 } : undefined });
|
|
if (!dry && (r.applied || r.staged)) rerender();
|
|
} catch (e) { mount(err, errorBox(e)); }
|
|
};
|
|
mount(el,
|
|
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Settings and built-ins"),
|
|
h("div", { class: "muted" }, "What happens to packets no rule decided, and the rules Housecarl adds before yours."))),
|
|
h("div", { class: "panel" }, h("div", { class: "form-grid" },
|
|
field("Input policy", input_, "Traffic to the firewall itself."),
|
|
field("Forward policy", forward, "Traffic through it."),
|
|
field("Output policy", output, "Traffic from it."),
|
|
h("label"), logDrops,
|
|
field("Built-ins", h("div", { class: "checks" }, bis.map(([, c]) => c))),
|
|
field("Your prefixes", own, "Your public prefixes (your delegated IPv6 /48): they can't come from outside."),
|
|
field("Antispoof IPv4", v4),
|
|
field("Antispoof IPv6", v6),
|
|
field("Flowtable", flow, "Forwarded TCP and UDP between these devices skip the rules once established (fast path). Exact names; none: off."),
|
|
field("Probation", probation, "How long a change made over the network has to be confirmed before it is rolled back."),
|
|
h("label"), manual,
|
|
field("Statistics", perDevice)),
|
|
err,
|
|
write ? h("div", { class: "actions form-actions" },
|
|
h("button", { class: "primary", onclick: () => save(false) }, "Save"),
|
|
h("button", { onclick: () => save(true), title: "Check without saving" }, "Check")) : null));
|
|
if (!write) readOnly(el.querySelector(".panel"));
|
|
}
|