mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 18:36:55 +00:00
fix(soc): move nft cache into secubox-hub + realtime fallback
The v2.12.16 fix put the nft cache populator in firstboot.sh, which
broke the rule "cron lives next to the module that consumes it" — the
SOC firewall_summary endpoint is owned by secubox-hub, so the timer
that feeds its cache belongs in the same package.
This commit:
* removes the inline cache populator from image/firstboot.sh
* ships /usr/sbin/secubox-nft-cache + secubox-nft-cache.{service,timer}
inside the secubox-hub package, with the timer auto-enabled via the
timers.target.wants/ symlink
* adds a sudoers fragment authorising user secubox to run
`nft list *`, `nft -j list *`, and `systemctl --no-block start
secubox-nft-cache.service` (and only that one unit)
* rewrites the /firewall_summary endpoint so the cache is a speed
optimisation, not the source of truth: if the cache file is missing
or older than 60 s, fall back to a realtime `sudo nft list`,
surface `source: "realtime"` in the JSON, and nudge the cache
populator via systemctl so the next request lands on a hot cache
Per the operator directive: "les cache double buffer ne doivent pas
tomber et permettre juste d'aller plus vite... un echec de cron et de
cache doit forcer le cron et faire du realtime".
This commit is contained in:
parent
27f9cd9db0
commit
40b5837262
|
|
@ -478,52 +478,14 @@ systemctl enable nftables
|
|||
systemctl restart nftables 2>/dev/null || true
|
||||
ok "nftables configuré"
|
||||
|
||||
# ── 12bis. nftables cache populator (powers SOC firewall_summary endpoint) ──
|
||||
# /api/v1/hub/public/firewall_summary reads /var/cache/secubox/nft-*.txt
|
||||
# expecting a cron to populate them. The cron was never created → all
|
||||
# counters showed 0 + status INACTIVE on the SOC dashboard. Ship a tiny
|
||||
# systemd timer that runs `nft list ruleset -j` + `nft list counters`
|
||||
# every 30 s as root and writes the cache files the endpoint expects.
|
||||
mkdir -p /var/cache/secubox
|
||||
cat > /usr/local/bin/secubox-nft-cache <<'NFTCACHE'
|
||||
#!/bin/sh
|
||||
# SecuBox nftables cache populator — fed to /api/v1/hub/public/firewall_summary
|
||||
set -e
|
||||
mkdir -p /var/cache/secubox
|
||||
nft -j list ruleset > /var/cache/secubox/nft-ruleset.json 2>/dev/null || true
|
||||
nft list counters > /var/cache/secubox/nft-counters.txt 2>/dev/null || true
|
||||
# Touch a status sentinel so the endpoint knows the cache is fresh.
|
||||
date -Iseconds > /var/cache/secubox/nft-cache.lastrun
|
||||
NFTCACHE
|
||||
chmod +x /usr/local/bin/secubox-nft-cache
|
||||
|
||||
cat > /etc/systemd/system/secubox-nft-cache.service <<'NFTCACHE_SVC'
|
||||
[Unit]
|
||||
Description=SecuBox nftables cache populator (runs every 30s via timer)
|
||||
Documentation=https://secubox.in/docs/soc
|
||||
After=nftables.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/secubox-nft-cache
|
||||
NFTCACHE_SVC
|
||||
|
||||
cat > /etc/systemd/system/secubox-nft-cache.timer <<'NFTCACHE_TIMER'
|
||||
[Unit]
|
||||
Description=SecuBox nftables cache populator timer (every 30s)
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=30s
|
||||
AccuracySec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
NFTCACHE_TIMER
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now secubox-nft-cache.timer 2>/dev/null || true
|
||||
ok "nftables cache timer installed (powers SOC dashboard)"
|
||||
# The nftables cache timer/service that powers the SOC firewall_summary
|
||||
# widget is now shipped by the secubox-hub package itself (it owns the
|
||||
# consuming endpoint). See:
|
||||
# packages/secubox-hub/debian/secubox-nft-cache.service
|
||||
# packages/secubox-hub/debian/secubox-nft-cache.timer
|
||||
# packages/secubox-hub/sbin/secubox-nft-cache
|
||||
# Each module that needs cached external state ships its own
|
||||
# cache-populator + timer; firstboot stays platform-only.
|
||||
|
||||
# ── Kiosk safety net ────────────────────────────────────────────────
|
||||
# build-live-usb.sh touches /var/lib/secubox/.kiosk-enabled and enables
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from secubox_core.kiosk import (
|
|||
import subprocess
|
||||
import json
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -1863,57 +1864,147 @@ app.include_router(router)
|
|||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Firewall Summary — nftables counters for SOC dashboard
|
||||
# Reads from cache files (/var/cache/secubox/nft-*.txt) updated by cron
|
||||
#
|
||||
# Cache (/var/cache/secubox/nft-*) is populated every 30s by
|
||||
# secubox-nft-cache.timer (shipped by this package). It is a speed
|
||||
# optimisation — the dashboard widget must never go blank because the
|
||||
# cache is missing or stale. If the cache file is absent or older than
|
||||
# NFT_CACHE_MAX_AGE seconds we fall back to a realtime `sudo nft list`
|
||||
# call AND nudge systemd to refresh the cache for the next request.
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
NFT_CACHE_DIR = "/var/cache/secubox"
|
||||
NFT_CACHE_MAX_AGE = 60 # seconds before we consider the cache stale
|
||||
|
||||
|
||||
def _parse_nft_counters(text: str) -> dict:
|
||||
counters: dict = {}
|
||||
current_counter = None
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("counter "):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
current_counter = parts[1]
|
||||
elif "packets" in line and current_counter:
|
||||
parts = line.split()
|
||||
for i, p in enumerate(parts):
|
||||
if p == "packets" and i + 1 < len(parts):
|
||||
try:
|
||||
packets = int(parts[i + 1])
|
||||
except ValueError:
|
||||
break
|
||||
counters[current_counter] = counters.get(current_counter, 0) + packets
|
||||
break
|
||||
return counters
|
||||
|
||||
|
||||
def _parse_nft_ruleset_json(text: str) -> tuple:
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return 0, 0, 0
|
||||
nftables = data.get("nftables", [])
|
||||
tables = sum(1 for x in nftables if "table" in x)
|
||||
chains = sum(1 for x in nftables if "chain" in x)
|
||||
rules = sum(1 for x in nftables if "rule" in x)
|
||||
return tables, chains, rules
|
||||
|
||||
|
||||
def _read_if_fresh(path: str, max_age: int) -> str | None:
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if (time.time() - st.st_mtime) > max_age:
|
||||
return None
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _nft_realtime(args: list) -> str | None:
|
||||
"""Run `sudo /usr/sbin/nft <args>` with a tight timeout. Returns
|
||||
stdout on success, None on any failure. The sudoers fragment
|
||||
shipped by this package whitelists `nft list *` for user secubox."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["sudo", "-n", "/usr/sbin/nft"] + args,
|
||||
capture_output=True, text=True, timeout=3,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _trigger_cache_refresh() -> None:
|
||||
"""Fire-and-forget systemctl start of the cache populator. Used
|
||||
when we just served a realtime response so the next request lands
|
||||
on a hot cache. The hub runs as user `secubox`; the sudoers
|
||||
fragment shipped by this package whitelists the start of this one
|
||||
specific unit, password-less."""
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["sudo", "-n", "/usr/bin/systemctl", "--no-block", "start",
|
||||
"secubox-nft-cache.service"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
@public_router.get("/firewall_summary")
|
||||
async def firewall_summary():
|
||||
"""Get nftables stats for the SOC dashboard.
|
||||
"""nftables stats for the SOC dashboard widget.
|
||||
|
||||
Reads cache files populated every 30 s by secubox-nft-cache.timer.
|
||||
Also surfaces the systemd state of nftables.service so the
|
||||
dashboard widget can show ACTIVE/INACTIVE accurately (operator
|
||||
report: 'Status INACTIVE' while 7 tables / 10 chains / 19 rules
|
||||
were loaded — the kernel had the rules but the endpoint never
|
||||
returned a status field, so the React bundle defaulted to
|
||||
INACTIVE)."""
|
||||
Strategy: cache-first, realtime-fallback.
|
||||
|
||||
1. If the JSON ruleset and counters cache files exist AND are fresh
|
||||
(mtime within NFT_CACHE_MAX_AGE), parse them — fast path, no
|
||||
privileged calls.
|
||||
2. Otherwise call `sudo nft list ruleset -j` / `sudo nft list
|
||||
counters` directly (sudoers fragment authorises this for user
|
||||
secubox), and trigger the cache populator service in the
|
||||
background so the next request is fast again.
|
||||
|
||||
The widget therefore stays populated even when the timer hasn't
|
||||
run yet (fresh boot) or when the cache populator has been
|
||||
masked / failing."""
|
||||
try:
|
||||
counters = {}
|
||||
try:
|
||||
with open("/var/cache/secubox/nft-counters.txt", "r") as f:
|
||||
current_counter = None
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("counter "):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
current_counter = parts[1]
|
||||
elif "packets" in line and current_counter:
|
||||
parts = line.split()
|
||||
for i, p in enumerate(parts):
|
||||
if p == "packets" and i + 1 < len(parts):
|
||||
packets = int(parts[i + 1])
|
||||
counters[current_counter] = counters.get(current_counter, 0) + packets
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
ruleset_path = os.path.join(NFT_CACHE_DIR, "nft-ruleset.json")
|
||||
counters_path = os.path.join(NFT_CACHE_DIR, "nft-counters.txt")
|
||||
lastrun_path = os.path.join(NFT_CACHE_DIR, "nft-cache.lastrun")
|
||||
|
||||
ruleset_text = _read_if_fresh(ruleset_path, NFT_CACHE_MAX_AGE)
|
||||
counters_text = _read_if_fresh(counters_path, NFT_CACHE_MAX_AGE)
|
||||
|
||||
source = "cache"
|
||||
if ruleset_text is None or counters_text is None:
|
||||
source = "realtime"
|
||||
# `-j` is a global option for nft and must come before the
|
||||
# command. The sudoers fragment shipped by this package
|
||||
# whitelists both `nft list *` and `nft -j list *`.
|
||||
rt_ruleset = _nft_realtime(["-j", "list", "ruleset"])
|
||||
rt_counters = _nft_realtime(["list", "counters"])
|
||||
if rt_ruleset is not None:
|
||||
ruleset_text = rt_ruleset
|
||||
if rt_counters is not None:
|
||||
counters_text = rt_counters
|
||||
_trigger_cache_refresh()
|
||||
|
||||
tables = chains = rules = 0
|
||||
if ruleset_text:
|
||||
tables, chains, rules = _parse_nft_ruleset_json(ruleset_text)
|
||||
|
||||
counters = _parse_nft_counters(counters_text) if counters_text else {}
|
||||
processed = counters.get("processed", 0)
|
||||
dropped = sum(v for k, v in counters.items() if "blacklist" in k.lower())
|
||||
|
||||
# Table / chain / rule counts from JSON cache.
|
||||
tables, chains, rules = 0, 0, 0
|
||||
try:
|
||||
with open("/var/cache/secubox/nft-ruleset.json", "r") as f:
|
||||
data = json.loads(f.read())
|
||||
nftables = data.get("nftables", [])
|
||||
tables = sum(1 for x in nftables if "table" in x)
|
||||
chains = sum(1 for x in nftables if "chain" in x)
|
||||
rules = sum(1 for x in nftables if "rule" in x)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Systemd state of nftables.service. `systemctl is-active`
|
||||
# works for unprivileged users.
|
||||
# systemd state of nftables.service.
|
||||
status = "inactive"
|
||||
try:
|
||||
r = subprocess.run(
|
||||
|
|
@ -1924,16 +2015,14 @@ async def firewall_summary():
|
|||
status = "active"
|
||||
except Exception:
|
||||
pass
|
||||
# If rules are loaded in the kernel but systemd reports inactive
|
||||
# (rules came from a manual `nft -f` during firstboot), treat
|
||||
# that as effectively active for dashboard purposes — operators
|
||||
# want to see "the firewall is up" if there are rules in place.
|
||||
# Rules in the kernel but systemd inactive (e.g. firstboot
|
||||
# loaded them with `nft -f`) — surface as active for the widget.
|
||||
if status == "inactive" and rules > 0:
|
||||
status = "active-manual"
|
||||
|
||||
last_cache_run = None
|
||||
try:
|
||||
with open("/var/cache/secubox/nft-cache.lastrun", "r") as f:
|
||||
with open(lastrun_path, "r") as f:
|
||||
last_cache_run = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
|
@ -1948,6 +2037,7 @@ async def firewall_summary():
|
|||
"accepted": processed - dropped if processed > dropped else 0,
|
||||
"counters": counters,
|
||||
"cache_last_run": last_cache_run,
|
||||
"source": source,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "status": "error", "tables": 0, "chains": 0, "rules": 0, "dropped": 0, "accepted": 0, "processed": 0}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,23 @@ override_dh_auto_install:
|
|||
# Nginx snippets (shared CORS config)
|
||||
install -d debian/secubox-hub/etc/nginx/snippets
|
||||
[ -d nginx/snippets ] && cp -r nginx/snippets/. debian/secubox-hub/etc/nginx/snippets/ || true
|
||||
# nftables cache populator + timer (powers SOC firewall_summary widget)
|
||||
install -d debian/secubox-hub/usr/sbin
|
||||
install -m 0755 sbin/secubox-nft-cache debian/secubox-hub/usr/sbin/secubox-nft-cache
|
||||
install -d debian/secubox-hub/lib/systemd/system
|
||||
install -m 0644 debian/secubox-nft-cache.service debian/secubox-hub/lib/systemd/system/
|
||||
install -m 0644 debian/secubox-nft-cache.timer debian/secubox-hub/lib/systemd/system/
|
||||
install -d debian/secubox-hub/etc/systemd/system/timers.target.wants
|
||||
ln -sf /lib/systemd/system/secubox-nft-cache.timer \
|
||||
debian/secubox-hub/etc/systemd/system/timers.target.wants/secubox-nft-cache.timer
|
||||
# sudoers fragment so the hub service (User=secubox) can fall back to
|
||||
# realtime `nft list` when the cache is stale (read-only nft, no risk).
|
||||
# Two patterns because `-j` is a global option for nft and must precede
|
||||
# the command (`nft -j list ruleset`, not `nft list ruleset -j`).
|
||||
install -d debian/secubox-hub/etc/sudoers.d
|
||||
printf '%s\n%s\n%s\n' \
|
||||
'secubox ALL=(root) NOPASSWD: /usr/sbin/nft list *' \
|
||||
'secubox ALL=(root) NOPASSWD: /usr/sbin/nft -j list *' \
|
||||
'secubox ALL=(root) NOPASSWD: /usr/bin/systemctl --no-block start secubox-nft-cache.service' \
|
||||
> debian/secubox-hub/etc/sudoers.d/secubox-hub-nft
|
||||
chmod 0440 debian/secubox-hub/etc/sudoers.d/secubox-hub-nft
|
||||
|
|
|
|||
12
packages/secubox-hub/debian/secubox-nft-cache.service
Normal file
12
packages/secubox-hub/debian/secubox-nft-cache.service
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=SecuBox nftables cache populator (powers SOC firewall_summary)
|
||||
Documentation=https://secubox.in/docs/soc
|
||||
After=nftables.service
|
||||
ConditionPathExists=/usr/sbin/nft
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/sbin/secubox-nft-cache
|
||||
# Runs as root via systemd — nft list requires CAP_NET_ADMIN
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
12
packages/secubox-hub/debian/secubox-nft-cache.timer
Normal file
12
packages/secubox-hub/debian/secubox-nft-cache.timer
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=SecuBox nftables cache populator timer (every 30s)
|
||||
Documentation=https://secubox.in/docs/soc
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=30s
|
||||
AccuracySec=5s
|
||||
Unit=secubox-nft-cache.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
packages/secubox-hub/sbin/secubox-nft-cache
Executable file
14
packages/secubox-hub/sbin/secubox-nft-cache
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
# SecuBox nftables cache populator
|
||||
# Feeds /var/cache/secubox/nft-* files consumed by
|
||||
# /api/v1/hub/public/firewall_summary. The endpoint falls back to
|
||||
# realtime `nft list` (via sudo) when the cache is missing/stale,
|
||||
# so this populator is a SPEED OPTIMISATION — not a source of truth.
|
||||
set -e
|
||||
mkdir -p /var/cache/secubox
|
||||
chmod 755 /var/cache/secubox
|
||||
nft -j list ruleset > /var/cache/secubox/nft-ruleset.json.new 2>/dev/null \
|
||||
&& mv /var/cache/secubox/nft-ruleset.json.new /var/cache/secubox/nft-ruleset.json
|
||||
nft list counters > /var/cache/secubox/nft-counters.txt.new 2>/dev/null \
|
||||
&& mv /var/cache/secubox/nft-counters.txt.new /var/cache/secubox/nft-counters.txt
|
||||
date -Iseconds > /var/cache/secubox/nft-cache.lastrun
|
||||
Loading…
Reference in New Issue
Block a user