From 33cfc1a90dea29165a81ac73b933827cac703511 Mon Sep 17 00:00:00 2001 From: noroot Date: Fri, 11 Sep 2026 09:32:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(fwlog):=20Live-Log=20war=20seit=20Tagen=20s?= =?UTF-8?q?till=20tot=20+=20Seitengr=C3=B6=C3=9Fe=20wirkungslos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei unabhängige Bugs hinter "Live-Log zeigt Einträge, aber nichts Neues": 1) ulogd2 stirbt beim nächtlichen Logrotate. Der postinst nahm an, ulogd laufe als root — die Debian-Unit startet aber `ulogd --daemon --uid ulog`. Beim Start öffnet ulogd die JSONL noch als root und schreibt danach über den offenen fd weiter, egal wem sie gehört. Nachts schickt das Distro- Profil /etc/logrotate.d/ulogd2 ein SIGHUP; das Reopen läuft dann als `ulog` und scheitert an root:edgeguard 0640 ("can't open JSON log file: Permission denied"). ulogd wertet das als fatal und beendet sich mit Exit-Code 0 — Restart=on-failure hätte also nicht gegriffen, und ohne Restart= blieb der Dienst tot (auf utm-1 5 Tage unbemerkt). Die API servierte derweil weiter ihren In-Memory-Ring von vor der Rotation, deshalb sah die UI Einträge, aber nie neue. Fix: Owner ulog (Schreiber) : edgeguard (Leser), logrotate `create` passend, plus Drop-in Restart=always als Selbstheilung. 2) Seitengröße liess sich nicht umstellen. Die Tabellen übergaben ein literales `pagination={{ pageSize: N }}`. antd merged via extendsObject(innerPagination, paginationObj) — der Prop überschreibt bei jedem Render den State, den der Size-Changer gerade gesetzt hat. Bei einem Live-Log rendert das im Sekundentakt, der Klick auf 20/100 war also sofort wieder weg. Fix: defaultPageSize (unkontrolliert). Betraf ausser dem Live-Log auch Logs, Backups-History, Routes, Alerts und CrowdSec. Ausserdem: `t` aus den WS-Effect-Deps genommen. i18n wechselt dessen Identität bei Store-Updates, was den Effect neu laufen liess — inklusive setEntries([]), d.h. der Live-Puffer leerte sich ohne erkennbaren Grund. Co-Authored-By: Claude Opus 5 --- management-ui/src/pages/Alerts/index.tsx | 2 +- management-ui/src/pages/Backups/History.tsx | 2 +- management-ui/src/pages/CrowdSec/index.tsx | 10 ++--- .../src/pages/FirewallLive/index.tsx | 12 ++++-- management-ui/src/pages/Logs/index.tsx | 2 +- management-ui/src/pages/Networks/Routes.tsx | 2 +- .../debian/edgeguard-api/DEBIAN/postinst | 38 +++++++++++++++++-- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/management-ui/src/pages/Alerts/index.tsx b/management-ui/src/pages/Alerts/index.tsx index d82232c..bf57949 100644 --- a/management-ui/src/pages/Alerts/index.tsx +++ b/management-ui/src/pages/Alerts/index.tsx @@ -438,7 +438,7 @@ export default function AlertsPage() { onChange: (keys) => setSelectedIds(keys as number[]), }} rowClassName={(r) => (r.acknowledged_at ? 'eg-row-muted' : '')} - pagination={{ pageSize: 25 }} + pagination={{ defaultPageSize: 25 }} locale={{ emptyText: ( } diff --git a/management-ui/src/pages/Backups/History.tsx b/management-ui/src/pages/Backups/History.tsx index 0396bbb..9942f43 100644 --- a/management-ui/src/pages/Backups/History.tsx +++ b/management-ui/src/pages/Backups/History.tsx @@ -350,7 +350,7 @@ export default function HistoryTab() { loading={list.isFetching} dataSource={list.data ?? []} columns={columns} - pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }} + pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }} locale={{ emptyText: t('backups.empty') }} /> diff --git a/management-ui/src/pages/CrowdSec/index.tsx b/management-ui/src/pages/CrowdSec/index.tsx index 6775483..3042f2e 100644 --- a/management-ui/src/pages/CrowdSec/index.tsx +++ b/management-ui/src/pages/CrowdSec/index.tsx @@ -225,7 +225,7 @@ function DecisionsTab() { loading={isLoading} dataSource={decisions ?? []} columns={columns} - pagination={{ pageSize: 20 }} + pagination={{ defaultPageSize: 20 }} /> ) @@ -379,7 +379,7 @@ function BouncersTab() { loading={isLoading} dataSource={bouncers ?? []} columns={columns} - pagination={{ pageSize: 20 }} + pagination={{ defaultPageSize: 20 }} /> ) @@ -436,7 +436,7 @@ function MachinesTab() { loading={isLoading} dataSource={machines ?? []} columns={columns} - pagination={{ pageSize: 20 }} + pagination={{ defaultPageSize: 20 }} /> ) @@ -515,7 +515,7 @@ function CollectionsTab() { loading={isLoading} dataSource={collections ?? []} columns={columns} - pagination={{ pageSize: 50 }} + pagination={{ defaultPageSize: 50 }} /> ) diff --git a/management-ui/src/pages/FirewallLive/index.tsx b/management-ui/src/pages/FirewallLive/index.tsx index 46083f4..4a86e6a 100644 --- a/management-ui/src/pages/FirewallLive/index.tsx +++ b/management-ui/src/pages/FirewallLive/index.tsx @@ -129,6 +129,12 @@ export default function FirewallLivePage() { pausedRef.current = paused const pendingDuringPauseRef = useRef([]) const wsRef = useRef(null) + // tRef statt `t` in den WS-Effect-Deps: `t` wechselt bei jedem + // i18n-Store-Update die Identität. Stünde es in den Deps, würde der + // Effect neu laufen — inkl. `setEntries([])` und WS-Reconnect, d. h. + // der Live-Puffer wäre ohne erkennbaren Grund plötzlich leer. + const tRef = useRef(t) + useEffect(() => { tRef.current = t }, [t]) useEffect(() => { const t = setTimeout(() => setAppliedFilters(filters), 300) @@ -187,7 +193,7 @@ export default function FirewallLivePage() { if (!cancelled && active) scheduleReconnect() } ws.onerror = () => { - setError(t('fwlog.connError')) + setError(tRef.current('fwlog.connError')) } } @@ -204,7 +210,7 @@ export default function FirewallLivePage() { if (reconnectTimer) clearTimeout(reconnectTimer) if (wsRef.current) { wsRef.current.close(); wsRef.current = null } } - }, [active, query, t]) + }, [active, query]) // Resume: gebufferte Events in die Tabelle mergen. useEffect(() => { @@ -412,7 +418,7 @@ export default function FirewallLivePage() { size="small" dataSource={[...entries].reverse()} columns={columns} - pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }} + pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }} locale={{ emptyText: connected ? t('fwlog.empty') : t('fwlog.connecting') }} /> diff --git a/management-ui/src/pages/Logs/index.tsx b/management-ui/src/pages/Logs/index.tsx index 82a0fac..19fb0d9 100644 --- a/management-ui/src/pages/Logs/index.tsx +++ b/management-ui/src/pages/Logs/index.tsx @@ -293,7 +293,7 @@ export default function LogsPage() { loading={logsQuery.isFetching} dataSource={entries} columns={columns} - pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }} + pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }} /> {entries.length === 0 && !logsQuery.isFetching && ( diff --git a/management-ui/src/pages/Networks/Routes.tsx b/management-ui/src/pages/Networks/Routes.tsx index 5a028a4..b1959bf 100644 --- a/management-ui/src/pages/Networks/Routes.tsx +++ b/management-ui/src/pages/Networks/Routes.tsx @@ -228,7 +228,7 @@ export default function RoutesTab() { size="small" dataSource={live.data ?? []} columns={liveColumns} - pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }} + pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }} locale={{ emptyText: t('routes.liveEmpty') }} style={{ marginTop: 12 }} /> diff --git a/packaging/debian/edgeguard-api/DEBIAN/postinst b/packaging/debian/edgeguard-api/DEBIAN/postinst index 1b88419..dabef4a 100755 --- a/packaging/debian/edgeguard-api/DEBIAN/postinst +++ b/packaging/debian/edgeguard-api/DEBIAN/postinst @@ -410,9 +410,25 @@ SYSCTL if [ ! -f /var/log/edgeguard/firewall.jsonl ]; then : > /var/log/edgeguard/firewall.jsonl fi - # ulogd2 läuft als root (eigener Daemon); File muss von ihm - # schreibbar UND von edgeguard-API lesbar sein. - chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl + # WICHTIG (Incident 2026-09-06): ulogd2 läuft NICHT als root — + # die Debian-Unit startet `ulogd --daemon --uid ulog`, der Daemon + # dropped also nach dem Öffnen seiner Files auf den User `ulog`. + # Solange er läuft schreibt er über den offenen fd weiter, egal + # wem die Datei gehört. Aber: das Distro-Profil + # /etc/logrotate.d/ulogd2 schickt nachts SIGHUP an ulogd, damit + # es seine Logfiles neu öffnet — und DAS passiert als `ulog`. + # Gehörte die Datei root:edgeguard 0640, scheitert das Reopen mit + # "can't open JSON log file: Permission denied", ulogd wertet das + # als fatal und BEENDET sich. Ohne Restart= blieb der Dienst dann + # tagelang still tot (5 Tage unbemerkt) → Firewall-Live-Log zeigte + # nur noch den alten In-Memory-Ring der API, nie neue Events. + # Daher: Owner = ulog (Schreiber), Gruppe = edgeguard (Leser). + if getent passwd ulog >/dev/null 2>&1; then + chown ulog:"$EG_USER" /var/log/edgeguard/firewall.jsonl + else + # ulogd2 (noch) nicht installiert — Fallback wie bisher. + chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl + fi chmod 0640 /var/log/edgeguard/firewall.jsonl cat > /etc/ulogd.conf <<'ULOGD' @@ -460,7 +476,9 @@ ULOGD compress delaycompress copytruncate - create 0640 root edgeguard + # Owner muss ulog sein — siehe Kommentar oben: ulogd re-öffnet die + # Datei bei SIGHUP als `ulog` und stirbt sonst an EACCES. + create 0640 ulog edgeguard } LOGROTATE chmod 0644 /etc/logrotate.d/edgeguard-firewall @@ -469,6 +487,18 @@ LOGROTATE # da ist (Dependency-Konflikt o.ä.), nur warnen — die Firewall # läuft auch ohne Logger. if systemctl list-unit-files ulogd2.service >/dev/null 2>&1; then + # Restart=always als Selbstheilung: ulogd beendet sich bei + # einem fehlgeschlagenen Reopen mit Exit-Code 0 (also KEIN + # Restart=on-failure — das würde nicht greifen). Ohne das + # bleibt Firewall-Logging nach einem Rotate-Hiccup still tot. + install -d -m 0755 /etc/systemd/system/ulogd2.service.d + cat > /etc/systemd/system/ulogd2.service.d/edgeguard-restart.conf <<'ULOGDUNIT' +# Managed by edgeguard — re-installation overwrites this file. +[Service] +Restart=always +RestartSec=5s +ULOGDUNIT + systemctl daemon-reload systemctl enable ulogd2.service >/dev/null 2>&1 || true systemctl restart ulogd2.service || \ echo "postinst: ulogd2.service restart failed (firewall logs disabled until fixed)" >&2