- SystemHandler.ExtraReloaders: neues Feld für nftables, wireguard,
squid, unbound, chrony Reloader
- WithAllReloaders(): Wire-Methode; main.go bindet alle Reloader nach
Initialisierung ein
- RenderConfigs: läuft alle Reloader durch, sammelt Fehler, antwortet
mit {ok, rendered[], errors{}} — partiell OK wird als warning gemeldet
- UI: Settings-Button-Label "Re-render all configs"; partial-error
Toast zeigt welche Dienste fehlschlugen
- Timeout: 30s statt 10s (alle Dienste brauchen mehr Zeit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /wireguard/importable listet alle .conf-Dateien in /etc/wireguard/
mit already_in-Flag (bereits in DB)
- POST /wireguard/import nimmt optionale Names-Liste; ohne Namen → alles
- edgeguard-ctl wg-import [iface…] importiert nur die genannten Interfaces
- UI: Checkbox-Modal mit "Alle neuen auswählen" + Einzelauswahl;
bereits importierte Interfaces disabled + grüner Tag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: POST /dns/flush-cache (unbound-control flush_zone .)
POST /ntp/force-sync (chronyc makestep)
Beide werden im Audit-Log festgehalten.
UI: Schaltflächen in DNS-Settings und NTP-Settings neben Save,
mit Tooltip-Beschreibung + i18n (de+en).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ActionButtons: Edit-Button wird für Viewer wie Delete gesperrt (Tooltip zeigt Reason)
- Domains/Detail: isViewer-Flag an alle Sub-Panels weitergegeben; Save-, TLS-Cert-,
Routing-Rules- und Headers-Buttons für Viewer disabled
- Backends/Detail: Save-Button + ServerPanel Add-Button für Viewer disabled
- i18n: domains.backendUp/backendDown Keys (waren noch hardkodiert)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wireguard.go: UpdatePeer zerstörte den gespeicherten encPriv wenn der Client
denselben public_key im Update-Body mitschickte (encPriv = nil auf Zeile 532).
Fix: encPriv wird nur geleert wenn der public_key sich tatsächlich ÄNDERT.
Andernfalls (gleicher Public Key = Benutzer ändert z.B. nur Allowed-IPs)
bleibt das verschlüsselte Private Key erhalten und QR/conf-Download
bleiben verfügbar.
domains.go + domainheaders.go: UpdateHeader() ignorierte die domainID aus der
URL. Fix: Update-SQL prüft jetzt WHERE id=$4 AND domain_id=$5 — falsch
adressierte Requests kriegen 404 statt blind zu updaten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RemoteTargets: alle 20+ hardkodierten Form-Labels und deutschen Hilfetexte
(S3: Endpoint/Region/Bucket/AccessKey/SecretKey/PathPrefix/HTTPS;
SFTP: Host/Port/Username/Password/PrivateKey/RemoteDir/HostKeyFp)
sind jetzt in t()-Aufrufen mit remotes.s3.*/remotes.sftp.*-Keys.
NTP: Listen-Address-Dropdown-Labels (alle IPv4/IPv6-Interfaces, Loopback)
verwenden jetzt dns.settings.allIPv4/allIPv6/loopback (identisch zu DNS-Settings).
NTP-Pool-Adresse-Placeholder → t('ntp.pool.addressPlaceholder').
en/de: alle neuen Keys ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
haproxy: http_to_https=false Domains bekommen jetzt eigene use_backend-Regeln
im public_http-Frontend statt dem globalen HTTPS-Redirect. Das Feld war bisher
in DB + UI vorhanden aber vom Config-Generator komplett ignoriert.
unbound: Forward-Zones ohne forward-addr (ForwardTo=nil oder leer) werden jetzt
übersprungen. Unbound lehnt solche Blöcke beim Start ab.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wenn beim Anlegen eines neuen Peers ein Schlüsselpaar server-seitig
generiert wurde (has_private_key=true), öffnet sich das QR-Modal
direkt nach dem Speichern — kein manuelles Klicken nötig.
Außerdem: version-Fallback-Strings in allen drei Binaries auf 1.1.80 synct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- haproxy_stats.go: FRONTEND-Rows aus show-stat CSV auslesen und als
frontends[] zurückgeben (Name, Sessions, MaxSess, Bytes, ReqTot/Rate)
- Dashboard: Listener-Sektion über Backend-Liste; query jetzt unified
haproxyStats mit backends/frontends alias für Abwärtskompatibilität
- common.relTime.*-Keys (en+de) eingeführt; relativeTime/relativeFromIso
in Dashboard + relTime in WireGuard Servers/Clients auf t-Parameter
umgestellt statt hardcodierter Strings
- CertExpiry in Cluster-Seite nutzt jetzt certDaysRemaining/certExpiredDaysAgo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relativeTime() in Dashboard, relTime() in WireGuard Clients+Servers und
CertExpiry() im Cluster-Page haben bisher hardcodiertes Deutsch
(„vor 5s", „abgelaufen vor X Tagen") gerendert, unabhängig von der
gewählten UI-Sprache.
Neu: gemeinsame i18n-Keys common.relTime.{Xs,Xm,Xh,Xd,never} in EN+DE,
sowie cluster.{certDaysRemaining,certExpiredDaysAgo}. Alle Helper-
Funktionen nehmen jetzt t() als Parameter und sind damit sprachunabhängig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/services/users: Repo mit CRUD, bcrypt (cost 12), Upsert für
setup-store-Admin-Migration, RecordLogin
- internal/handlers/users: GET/POST/PUT /users, POST /users/:id/password,
DELETE /users/:id; Schutz gegen Selbst-Löschung
- auth.go Login: DB-Nutzer first, Fallback auf setup-store-Admin;
bei erfolgreichem Fallback wird der Admin per Upsert in die DB
migriert (kein manueller Eingriff nötig)
- management-ui: /users-Seite mit Tabelle, Anlegen-, Bearbeiten-,
Passwort-setzen- und Löschen-Modals; "You"-Badge für eigenen Account
- Sidebar + Route + i18n (de/en) ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: force_http1 (bool, default false) — zwingt HAProxy auf der
Backend-Verbindung zu HTTP/1.1 statt h2,http/1.1 zu verhandeln.
Nötig für Legacy-Apps die kein h2 sprechen.
Domain: disable_h3 (bool, default false) — unterdrückt Alt-Svc-
Response-Header für diese Domain. Browser erhalten keinen h3/QUIC-
Hinweis und bleiben auf h2/http1.1.
Migration 0027, Model+Service+HAProxy-Template+UI+i18n.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WireGuard-Peer-Änderungen landeten nicht im laufenden Interface:
/etc/wireguard/ ist root:root 700, os.Readlink schlug fehl →
ensureWGQuickSymlink fiel immer in den Error-Pfad. Fix: Symlink
via sudo /bin/ln -sf (sudoers-Entry in postinst ergänzt).
- Site-to-Site-Masquerade: Roadwarrior-Clients (z. B. 192.168.99.3)
konnten LANs hinter anderen Peers nicht erreichen, weil das remote
Gateway die VPN-Client-IP nicht als Tunnel-Route kannte. Fix: auto
masquerade in nftables postrouting_nat pro WireGuard-Server-Interface
(oifname "wg7" ip saddr 192.168.99.0/24 masquerade).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neues Feld 'client_routes' auf wireguard_interfaces: der Operator
trägt dort kommagetrennte Netzwerke ein (z. B. 10.0.10.0/24 für ein
LAN hinter der Box). Der Peer-Config-Download fügt diese automatisch
als zusätzliche AllowedIPs in den [Peer]-Block der Client-Config ein.
Bisher wurde nur das Server-Tunnel-Subnetz (ifc.address_cidr) als
AllowedIPs exportiert — Peers konnten so keine anderen Netze über
den Tunnel erreichen ohne die Config manuell anzupassen.
Migration: 0026_wg_client_routes.sql
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zwei unabhängige Fixes für das WireGuard peer-to-peer Problem:
1. Startup-nftables-Render: edgeguard-api rendert beim Start die
nftables-Konfiguration neu. Damit werden Template-Änderungen aus
einem Update (z.B. 1.1.51 WG-forward-Rule) sofort aktiv, ohne
dass der Operator manuell eine Firewall-Mutation triggern müsste.
2. UI-Hint: Peer-AllowedIPs-Feld erklärt explizit warum /32 nötig
ist und was /24 kaputtmacht (Server routet ganzen Subnet-Block zu
einem Peer → andere Peers nicht mehr erreichbar).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Die forward-chain hatte policy=drop ohne Ausnahme für WireGuard-
Peer-to-Peer-Traffic. Pakete von Peer A nach Peer B (beide am
selben wg-Interface) müssen durch die Box forwarded werden — das
war bisher stiller Drop.
Lösung: loadView liest alle aktiven server-mode WG-Interfaces und
trägt sie in View.WGServerIfaces ein. Das Template emittiert pro
Interface iifname "<wg>" accept in der forward-chain. Return-Pakete
gehen bereits via ct state established durch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Statt aller 15+ Felder in einem Modal gibt es jetzt eine eigene Seite
unter /domains/:id. Der Create-Flow bleibt im Modal (Name, Backend, Active,
HTTP→HTTPS), ein Hinweistext führt nach dem Speichern auf die Detailseite
weiter.
Die Detailseite enthält links das vollständige Settings-Formular (HSTS,
Rate-Limit, Maintenance, Max-Body, Notes, WWW-Redirect) und rechts das
HeadersPanel zum Verwalten custom HTTP-Response-Header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ruleset.nft.tpl: operator-rules bekommen `counter <action> comment "egid:<id>"`
→ nft zählt Packets + Bytes per Regel ab dem letzten Ruleset-Apply
- handlers/firewall_counters.go: GET /firewall/counters parst
`sudo nft list table inet edgeguard` per Regex, liefert [{rule_id,packets,bytes}]
- handlers/firewall_counters_test.go: unit-tests für parseNFTCounters
- Firewall/Rules.tsx: neue "Hits"-Spalte (Packet-Count, Tooltip mit Bytes),
10s-Polling via TanStack Query ['fw','counters']
- i18n de+en: fw.rule.hits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /haproxy/stats liefert jetzt req_tot (kumulierte Requests seit
HAProxy-Start) und req_rate (Requests/s im letzten Messfenster).
Backend-Detail zeigt in der Live-Spalte 'N sess · X/s' wenn Traffic
fließt; Dashboard-HAProxy-Card zeigt req/s ebenfalls an.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Domain-Attachments werden beim Speichern korrekt sync't
(syncDomainAttachments war in Detail.tsx vergessen worden)
- Server-Tabelle in der Detail-Seite zeigt jetzt eine 'Live'-Spalte
mit HAProxy-Status (UP/DOWN), aktive Sessions und Bytes per Hover-
Tooltip; Daten kommen aus dem gemeinsamen ['haproxy','stats']-Query
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/backends/:id zeigt Pool-Einstellungen (links) und Server-Liste
(rechts) auf einer eigenen Seite. Der Edit-Button in der Liste
navigiert dorthin; Anlegen bleibt ein schlankes Modal ohne Server-
Panel. Außerdem: Backend-Detail lädt den Pool per GET /backends/:id
und speichert alle Felder direkt, keine Sync-Logik nötig.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /system/interfaces liefert jetzt rx_bytes/tx_bytes/rx_packets/
tx_packets/rx_drop/tx_drop aus /proc/net/dev. Die System-Interfaces-
Card in Netzwerk → Interfaces zeigt die Werte als Mini-Tabelle mit
10s-Refetch; Hover auf den Bytes-Werten zeigt Paketzähler + Drops.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backends- und Routing-Rules-Seite zeigen jetzt beim Hover auf den
UP/DOWN-Chip die aggregierten HAProxy-Stats des jeweiligen Backends:
aktive Sessions + kumulierte Bytes rein/raus seit letztem HAProxy-Start.
Daten kommen aus dem bereits vorhandenen /haproxy/stats-Endpunkt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-Frage: „Werden via haproxy die echten IPs durchgereicht?". Antwort:
X-Forwarded-For ja (option forwardfor), aber Apps wie WordPress/Mailcow
brauchen zusätzlich X-Forwarded-Proto=https um Redirect-Loops zu
vermeiden, und X-Real-IP ist die bequeme single-value-Variante die viele
Tools out-of-the-box lesen (ohne die XFF-Chain parsen zu müssen).
Beide Frontends (public_https + mgmt_https) emittieren jetzt:
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
Was Backends sehen:
X-Forwarded-For: <client-ip> (defaults: option forwardfor)
X-Forwarded-Proto: https (NEW)
X-Real-IP: <client-ip> (NEW, single value)
PROXY-Protocol-Toggle pro Backend kommt nicht in diesem Release — der
Operator hat „nur Header-Variante" gewählt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Peers als „offline"
Bug-Report: Unify-Home-Tunnel ist aktiv (handshake live, 2.8 GB rx),
UI zeigte „Letzter Handshake: nie / Traffic: — / Offline".
Root cause: handlers/wireguard.go Status() parsed `wg show all dump`
mit `PeerPublicKey: fields[2]` — fields[2] ist aber der pre-shared
key (PSK), NICHT der peer_pubkey. Format mit "all"-prefix ist:
iface lines (5 cols): iface priv_key pubkey listen_port fwmark
peer lines (9 cols): iface peer_pubkey psk endpoint allowed_ips
last_hs rx tx keepalive
Damit matched die UI gegen den PSK → DB-Peer-Row (gespeichert per
pubkey) traf nie zu, Status blieb „nie/—" auch bei aktivem Tunnel.
Fix: fields[1] statt fields[2]. Endpoint/AllowedIPs/Handshake/RX/TX
waren bereits korrekt verschoben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator hat Admin-Passwort vergessen aber SSH-Zugang zur Box →
schneller Reset ohne SMTP/Email-Setup.
Flow:
1. `sudo edgeguard-ctl reset-password` auf der Box → 32-hex-Token
+ ISO-Expiry werden nach /var/lib/edgeguard/.reset-token (mode
0600 edgeguard:edgeguard) geschrieben, Token kommt auf stdout.
TTL: 30 min.
2. Login-Seite hat „Passwort vergessen?"-Link → /reset-password.
3. Reset-Page: Token + neues Passwort (min. 12). POST /auth/reset-
password validiert Token (constant-time compare), prüft Expiry,
löscht das File (single-use), hash't das Passwort + speichert
in setup.json.
internal/services/setup/:
- SetAdminPassword() — bcrypt-hash + save, fehler wenn setup nicht
completed
- GenerateResetToken() / ConsumeResetToken() — File-basiert,
Format: "<token>|<RFC3339-expiry>"
internal/handlers/auth.go: POST /api/v1/auth/reset-password.
cmd/edgeguard-ctl/main.go: `reset-password` command.
UI: /reset-password Page mit Info-Alert für CLI-Snippet
(„sudo edgeguard-ctl reset-password" im dunklen Code-Block); Login-
Seite bekommt den „Passwort vergessen?"-Link.
Verifiziert auf 1.0.76: CLI druckt Token + schreibt File mit 0600
edgeguard:edgeguard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schutz gegen Box-Total-Loss — lokale Backups in /var/backups/edgeguard
helfen nicht, wenn die Disk stirbt oder die Box brennt. Nach jedem
erfolgreichen lokalen Backup wird die tar.gz an alle aktiven
Off-Site-Ziele hochgeladen.
Migration 0022: backup_remotes (kind=s3|sftp, target_url, settings
JSONB, active, last_upload_at, last_error) + backups.remote_uploads
JSONB (per-Target-Result).
internal/services/backup/remote/:
- UploadAll() — pro aktivem Target ein Upload, Failures non-fatal
- S3 via minio-go/v7 — funktioniert mit AWS, MinIO, Backblaze B2,
Cloudflare R2, Hetzner Object Storage (alle S3-API-kompatibel)
- SFTP via golang.org/x/crypto/ssh + pkg/sftp. Password + Private-
Key (OpenSSH, base64-encoded) als Auth. Optional host_key_
fingerprint-Pinning (SHA256:...); leer = TOFU (unsicher vs MitM,
OK für initial setup).
- Test() lädt eine 1KB-Probe + löscht sie wieder — Operator-UI hat
einen „Verbindung testen"-Button.
backup.Service.RemoteUploader-Interface: nach erfolgreichem
recordSuccess() läuft UploadAll, Results landen in backups.remote_
uploads JSONB. last_upload_at/last_error in backup_remotes pro Target
gepflegt. API + Scheduler injizieren beide den Adapter.
internal/handlers/backup_remotes.go: CRUD + POST /:id/test. Sensitive
Felder (secret_key, password, private_key) werden in GET-Responses
durch ***SET*** maskiert; UpdateChannel merged das zurück damit der
Operator bei Edit ohne Re-Eingabe speichern kann.
UI: Backups-Page jetzt mit Tabs "Sicherungen" + "Off-Site-Ziele".
Tab 2 hat CRUD-Tabelle mit kind-konditionalem Form (S3-Felder oder
SFTP-Felder), Test-Button pro Row, last_upload-Status mit FAIL-Tag
bei Errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent-Activity-Karte zeigt neue audit_log-Events jetzt sofort statt
in 15s-Polls.
internal/services/audit/audit.go:
- Repo bekommt Subscribe()-Methode mit fan-out-channel (Buffer 32,
non-blocking-send — langsame Clients droppen Events statt die
Pipeline zu blockieren).
- Log() macht jetzt INSERT … RETURNING id, created_at und broadcastet
den fertigen Entry an alle Subscribers. Broadcast nur nach
erfolgreichem INSERT — failed inserts erscheinen nicht.
internal/handlers/audit.go:
- Neuer GET /api/v1/audit/live (WebSocket): sendet beim Connect die
letzten 50 Einträge (oldest→newest), danach Live-Stream aus
Subscribe-Channel. 30s-Ping gegen HAProxy-Tunnel-Timeout.
- Recent (Poll-Endpoint) bleibt für Fallbacks erhalten.
UI Dashboard:
- useAuditLive(keep=15)-Hook ersetzt das 15s-useQuery-Poll.
- WebSocket auf wss://<host>/api/v1/audit/live; Auto-Reconnect alle
2s nach Drop.
- dedupe per id (Snapshot + erste live-Events können sich kurz
überschneiden während des Subscribe-Race).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setzt die Foundation aus 1.0.70 fort — bisher war ha_nodes.config_hash
noch NULL und das UI konnte keinen Drift erkennen.
internal/cluster/confighash.go:
- ComputeConfigHash() berechnet SHA-256 (truncated auf 16 hex chars)
über alle replizierbaren Tabellen. Pattern 1:1 aus mail-gateway/
internal/handlers/cluster_status.go (driftHashSpec).
- Pro Tabelle: md5((to_jsonb(t) - id - updated_at - created_at -
excludes)::text) per row, dann string_agg ORDER BY rh.
- Singleton-Tabellen (dns_settings, ntp_settings, mail_config-Stil)
hashen direkt ohne agg.
- 23 Tabellen: domains, backends, backend_servers, routing_rules,
network_interfaces, ip_addresses, tls_certs (mit ExtraExclude
last_renewed_at + last_error damit cert-renewal keinen drift
erzeugt), firewall_zones+address_objects+address_groups+services+
service_groups+rules+nat_rules, wireguard_interfaces+peers,
forward_proxy_acls, dns_zones+records+settings, ntp_pools+settings,
static_routes.
- RefreshLocalHash() schreibt den Hash in die eigene ha_nodes-Row.
Scheduler:
- 5-min-Tick ruft RefreshLocalHash. Pro-Mutation-Refresh wäre zu
teuer (jede UI-Action triggert sonst 23 jsonb-Queries).
- Initial-Refresh beim Scheduler-Boot damit /cluster/status nicht
5 min auf den ersten Wert wartet.
handlers/cluster.go:
- Status() ruft RefreshLocalHash mit 2s-Timeout on-demand. Damit
sieht das UI auch zwischen den Scheduler-Ticks immer frische
Werte; bei Timeout fallback auf den DB-Wert (eventuell stale).
Verifiziert auf 1.0.71: ha_nodes-Row hat config_hash=728834dce5ca4e48,
scheduler-log "config-hash refresh enabled tick=5m0s".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>