- 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>
179 lines
4.7 KiB
Go
179 lines
4.7 KiB
Go
// Package squid renders /etc/edgeguard/squid/squid.conf from
|
|
// forward_proxy_acls and reloads squid.service. Cache directory
|
|
// + in-memory cache are hard-coded sensible defaults; the operator
|
|
// scope is just "what can pass through the proxy".
|
|
package squid
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/forwardproxy"
|
|
)
|
|
|
|
const (
|
|
confPath = "/etc/edgeguard/squid/squid.conf"
|
|
defaultListenPort = 3128
|
|
)
|
|
|
|
//go:embed squid.cfg.tpl
|
|
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 {
|
|
ListenAddrs []ListenAddr
|
|
ACLs []models.ForwardProxyACL
|
|
CacheMemMB int
|
|
CacheDirMB int
|
|
MaxObjSizeMB int
|
|
ConnectTimeout int
|
|
ReadTimeout int
|
|
RequestTimeout int
|
|
}
|
|
|
|
type Generator struct {
|
|
Pool *pgxpool.Pool
|
|
Repo *forwardproxy.Repo
|
|
SkipReload bool
|
|
}
|
|
|
|
func New(pool *pgxpool.Pool) *Generator {
|
|
return &Generator{Pool: pool, Repo: forwardproxy.New(pool)}
|
|
}
|
|
|
|
func (g *Generator) Name() string { return "squid" }
|
|
|
|
func (g *Generator) renderBuf(ctx context.Context) (bytes.Buffer, error) {
|
|
acls, err := g.Repo.List(ctx)
|
|
if err != nil {
|
|
return bytes.Buffer{}, fmt.Errorf("list acls: %w", err)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
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 {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (g *Generator) Render(ctx context.Context) error {
|
|
body, err := g.renderBuf(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(confPath), 0o755); err != nil {
|
|
return fmt.Errorf("mkdir: %w", err)
|
|
}
|
|
tmp := confPath + ".tmp"
|
|
if err := os.WriteFile(tmp, body.Bytes(), 0o644); err != nil {
|
|
return fmt.Errorf("write %s: %w", tmp, err)
|
|
}
|
|
if err := os.Rename(tmp, confPath); err != nil {
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
if err := ensureDistroSymlink(); err != nil {
|
|
return fmt.Errorf("symlink: %w", err)
|
|
}
|
|
if g.SkipReload {
|
|
return nil
|
|
}
|
|
return configgen.ReloadService("squid")
|
|
}
|
|
|
|
// ensureDistroSymlink prüft ob /etc/squid/squid.conf auf unsere
|
|
// managed conf zeigt. Setup ist Postinst-Verantwortung (Renderer
|
|
// hat als edgeguard-User kein Schreibrecht in /etc/squid). Wenn
|
|
// Symlink fehlt → Warnung, aber kein Fehler — squid liest dann
|
|
// noch die Distro-Default und der Operator merkt's beim nächsten
|
|
// reload.
|
|
func ensureDistroSymlink() error {
|
|
const link = "/etc/squid/squid.conf"
|
|
if cur, err := os.Readlink(link); err == nil && cur == confPath {
|
|
return nil
|
|
}
|
|
// Versuch zu setzen — bei permission-denied (= edgeguard-User
|
|
// hat keinen Schreibrecht in /etc/squid) warnen + ok melden.
|
|
if _, err := os.Stat(link); err == nil {
|
|
_ = os.Rename(link, link+".distro-bak")
|
|
}
|
|
if err := os.Symlink(confPath, link); err != nil {
|
|
// Postinst hat den Symlink schon angelegt oder soll's beim
|
|
// Upgrade nachholen. Renderer sollte hier nicht failen.
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|