177 lines
5.0 KiB
HTML
177 lines
5.0 KiB
HTML
{% extends "base.html" %} {% block title %}Firewall — Intrusion Detection{%
|
|
endblock %} {% block content %}
|
|
<h1>Firewall</h1>
|
|
<p class="muted">
|
|
Current MikroTik state (from the latest <code>data/mikrotik</code> snapshot).
|
|
Sync previews or applies the blocklist diff.
|
|
</p>
|
|
|
|
<div class="panel">
|
|
<div class="panel-actions" style="margin-top: 0">
|
|
<button class="btn" id="preview-btn">Preview sync (dry-run)</button>
|
|
<button class="btn danger" id="apply-btn">Apply sync</button>
|
|
<span class="muted" id="fw-result"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-head"><h2>Address lists</h2></div>
|
|
<div id="address-lists"></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-head"><h2>Firewall rules</h2></div>
|
|
<div id="firewall-rules"></div>
|
|
</div>
|
|
{% endblock %} {% block scripts %}
|
|
<script>
|
|
async function loadState() {
|
|
try {
|
|
const s = await api("/api/firewall/state");
|
|
renderAddressLists(s.address_lists || {});
|
|
renderRules("firewall-rules", s.firewall_rules || {});
|
|
} catch (e) {
|
|
toast(e.message, "error");
|
|
}
|
|
}
|
|
|
|
function renderAddressLists(addr) {
|
|
const box = document.getElementById("address-lists");
|
|
box.innerHTML = "";
|
|
const byList = {};
|
|
for (const version of ["ipv4", "ipv6"]) {
|
|
for (const e of addr[version] || []) {
|
|
const name = e.list || "?";
|
|
(byList[name] = byList[name] || { ipv4: [], ipv6: [] })[version].push(
|
|
e.address,
|
|
);
|
|
}
|
|
}
|
|
const names = Object.keys(byList);
|
|
if (!names.length) {
|
|
box.innerHTML =
|
|
'<p class="muted">No address-list snapshot available. Run <code>app.py --update-firewall</code> first.</p>';
|
|
return;
|
|
}
|
|
for (const name of names) {
|
|
const rows = [];
|
|
for (const version of ["ipv4", "ipv6"]) {
|
|
const entries = (byList[name][version] || []).sort();
|
|
rows.push(
|
|
el(
|
|
"tr",
|
|
el("td", { text: version }),
|
|
el("td", { class: "mono", text: entries.join(", ") || "—" }),
|
|
),
|
|
);
|
|
}
|
|
box.append(
|
|
el(
|
|
"div",
|
|
{ style: "margin-bottom:1rem" },
|
|
el("div", {
|
|
style: "font-weight:600;margin-bottom:.4rem",
|
|
text: name,
|
|
}),
|
|
el(
|
|
"table",
|
|
{ class: "table" },
|
|
el(
|
|
"thead",
|
|
{},
|
|
el(
|
|
"tr",
|
|
{},
|
|
el("th", { text: "Version" }),
|
|
el("th", { text: "Addresses" }),
|
|
),
|
|
),
|
|
el("tbody", {}, ...rows),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function renderRules(boxId, rules) {
|
|
const box = document.getElementById(boxId);
|
|
box.innerHTML = "";
|
|
for (const version of ["ipv4", "ipv6"]) {
|
|
const list = rules[version] || [];
|
|
const rows = list.map((r) =>
|
|
el(
|
|
"tr",
|
|
el("td", {
|
|
class: "mono",
|
|
text: r.address ? r.address : r["src-address-list"] || "",
|
|
}),
|
|
el("td", { text: r.chain || "—" }),
|
|
el("td", { text: r.action || "—" }),
|
|
el("td", { text: r.comment || "" }),
|
|
),
|
|
);
|
|
box.append(
|
|
el(
|
|
"div",
|
|
{ style: "margin-bottom:1rem" },
|
|
el("div", {
|
|
style: "font-weight:600;margin-bottom:.4rem",
|
|
text: `IPv${version} (${list.length})`,
|
|
}),
|
|
rows.length
|
|
? el(
|
|
"table",
|
|
{ class: "table" },
|
|
el(
|
|
"thead",
|
|
{},
|
|
el(
|
|
"tr",
|
|
{},
|
|
el("th", { text: "Address / List" }),
|
|
el("th", { text: "Chain" }),
|
|
el("th", { text: "Action" }),
|
|
el("th", { text: "Comment" }),
|
|
),
|
|
),
|
|
el("tbody", {}, ...rows),
|
|
)
|
|
: el("p", { class: "muted", text: "No rules in snapshot." }),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
async function sync(dryRun) {
|
|
const out = document.getElementById("fw-result");
|
|
out.textContent = "Working…";
|
|
try {
|
|
const changes = await api(`/api/firewall/${dryRun ? "sync" : "apply"}`, {
|
|
method: "POST",
|
|
body: {},
|
|
});
|
|
const verb = dryRun ? "Would" : "";
|
|
out.textContent =
|
|
`${verb} add ${(changes.added_ipv4 || []).length} IPv4 + ${(changes.added_ipv6 || []).length} IPv6; ` +
|
|
`remove ${(changes.removed_ipv4 || []).length} IPv4 + ${(changes.removed_ipv6 || []).length} IPv6.`;
|
|
toast(
|
|
(dryRun ? "Preview" : "Applied") + " firewall sync",
|
|
dryRun ? "warn" : "ok",
|
|
);
|
|
if (!dryRun) loadState();
|
|
} catch (e) {
|
|
out.textContent = "";
|
|
toast(e.message, "error");
|
|
}
|
|
}
|
|
|
|
document
|
|
.getElementById("preview-btn")
|
|
.addEventListener("click", () => sync(true));
|
|
document
|
|
.getElementById("apply-btn")
|
|
.addEventListener("click", () => sync(false));
|
|
loadState();
|
|
</script>
|
|
{% endblock %}
|