feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery

- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:18:31 +02:00
parent 49899e984c
commit 1d06b28064
55 changed files with 3132 additions and 672 deletions

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/jackc/pgx/v5/pgxpool"
@@ -21,8 +22,8 @@ import (
)
const (
confPath = "/etc/edgeguard/squid/squid.conf"
listenPort = 3128
confPath = "/etc/edgeguard/squid/squid.conf"
defaultListenPort = 3128
)
//go:embed squid.cfg.tpl
@@ -30,9 +31,20 @@ var cfgTpl string
var tpl = template.Must(template.New("squid").Parse(cfgTpl))
type ListenAddr struct {
Addr string // empty = all interfaces
Port int
}
type View struct {
ListenPort int
ACLs []models.ForwardProxyACL
ListenAddrs []ListenAddr
ACLs []models.ForwardProxyACL
CacheMemMB int
CacheDirMB int
MaxObjSizeMB int
ConnectTimeout int
ReadTimeout int
RequestTimeout int
}
type Generator struct {
@@ -52,7 +64,45 @@ func (g *Generator) renderBuf(ctx context.Context) (bytes.Buffer, error) {
if err != nil {
return bytes.Buffer{}, fmt.Errorf("list acls: %w", err)
}
view := View{ListenPort: listenPort, ACLs: acls}
// Read all settings — fall back to defaults if table not migrated yet.
s := models.ForwardProxySettings{
ListenPort: defaultListenPort,
CacheMemMB: 64,
CacheDirMB: 100,
MaxObjSizeMB: 4,
ConnectTimeout: 60,
ReadTimeout: 300,
RequestTimeout: 300,
}
_ = g.Pool.QueryRow(ctx, `
SELECT listen_addresses, listen_port,
cache_mem_mb, cache_dir_mb, max_obj_size_mb,
connect_timeout, read_timeout, request_timeout
FROM forward_proxy_settings WHERE id=1`).Scan(
&s.ListenAddresses, &s.ListenPort,
&s.CacheMemMB, &s.CacheDirMB, &s.MaxObjSizeMB,
&s.ConnectTimeout, &s.ReadTimeout, &s.RequestTimeout,
)
var listenAddrs []ListenAddr
for _, raw := range splitCSV(s.ListenAddresses) {
listenAddrs = append(listenAddrs, ListenAddr{Addr: raw, Port: s.ListenPort})
}
if len(listenAddrs) == 0 {
listenAddrs = []ListenAddr{{Addr: "", Port: s.ListenPort}}
}
view := View{
ListenAddrs: listenAddrs,
ACLs: acls,
CacheMemMB: s.CacheMemMB,
CacheDirMB: s.CacheDirMB,
MaxObjSizeMB: s.MaxObjSizeMB,
ConnectTimeout: s.ConnectTimeout,
ReadTimeout: s.ReadTimeout,
RequestTimeout: s.RequestTimeout,
}
var body bytes.Buffer
if err := tpl.Execute(&body, view); err != nil {
return bytes.Buffer{}, fmt.Errorf("template: %w", err)
@@ -60,6 +110,17 @@ func (g *Generator) renderBuf(ctx context.Context) (bytes.Buffer, error) {
return body, nil
}
func splitCSV(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func (g *Generator) RenderToString(ctx context.Context) (string, error) {
buf, err := g.renderBuf(ctx)
if err != nil {