From cfb0e9ed01a6abb19579bef3fd6be5411c8ba3df Mon Sep 17 00:00:00 2001 From: Debian Date: Mon, 25 May 2026 06:28:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(config-preview):=20chrony=20+=20wireguard?= =?UTF-8?q?=20in=20Config-Preview=20verf=C3=BCgbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chrony.RenderToString(): rendert /etc/chrony/conf.d/edgeguard.conf ohne Datei-Write oder Service-Reload - wireguard.RenderToString(): kombiniert alle aktiven Interface-Configs; PrivateKey + PresharedKey werden als ausgegeben (sicher für UI-Anzeige) - main.go: beide in WithConfigPreviewers eingetragen - Settings UI: chrony + wireguard im Generator-Dropdown Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 12 ++-- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- internal/chrony/chrony.go | 24 ++++++++ internal/wireguard/wireguard.go | 72 ++++++++++++++++++++++ management-ui/src/pages/Settings/index.tsx | 2 + 7 files changed, 108 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 748d107..219fe46 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.96 +1.1.97 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 31f6d0d..ad37a53 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.96" +var version = "1.1.97" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") @@ -268,10 +268,12 @@ func main() { systemHdl.WithAudit(auditRepo, nodeID) systemHdl.WithDB(pool) systemHdl.WithConfigPreviewers(map[string]func(context.Context) (string, error){ - "haproxy": haproxy.New(pool).RenderToString, - "nftables": firewallrender.New(pool).RenderToString, - "squid": squidrender.New(pool).RenderToString, - "unbound": unboundrender.New(pool).RenderToString, + "haproxy": haproxy.New(pool).RenderToString, + "nftables": firewallrender.New(pool).RenderToString, + "squid": squidrender.New(pool).RenderToString, + "unbound": unboundrender.New(pool).RenderToString, + "chrony": chronyrender.New(pool).RenderToString, + "wireguard": wgrender.New(pool, secretsBox).RenderToString, }) setupHdl.WithAudit(auditRepo, nodeID) usersRepo := usersvc.New(pool) diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index a29c847..82b5e76 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.96" +var version = "1.1.97" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index b49393c..30ec136 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -35,7 +35,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.96" +var version = "1.1.97" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/chrony/chrony.go b/internal/chrony/chrony.go index 14b5eca..a409ee2 100644 --- a/internal/chrony/chrony.go +++ b/internal/chrony/chrony.go @@ -49,6 +49,30 @@ func New(pool *pgxpool.Pool) *Generator { func (g *Generator) Name() string { return "chrony" } +// RenderToString renders the chrony config to a string without writing +// to disk or reloading the service. Used by the config-preview endpoint. +func (g *Generator) RenderToString(ctx context.Context) (string, error) { + settings, err := g.Repo.GetSettings(ctx) + if err != nil { + return "", fmt.Errorf("settings: %w", err) + } + pools, err := g.Repo.ListPools(ctx) + if err != nil { + return "", fmt.Errorf("pools: %w", err) + } + view := View{ + Settings: settings, + Pools: pools, + ListenAddresses: filterNonLoopback(splitCSV(settings.ListenAddresses)), + AllowACLs: splitCSV(settings.AllowACL), + } + var body bytes.Buffer + if err := tpl.Execute(&body, view); err != nil { + return "", fmt.Errorf("template: %w", err) + } + return body.String(), nil +} + func (g *Generator) Render(ctx context.Context) error { settings, err := g.Repo.GetSettings(ctx) if err != nil { diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index 4e86e6b..c3f7e4d 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -44,6 +44,78 @@ func New(pool *pgxpool.Pool, box *secrets.Box) *Generator { func (g *Generator) Name() string { return "wireguard" } +// RenderToString renders all active interface configs to a combined +// string for the config-preview endpoint. Private keys are redacted +// so the output is safe to display in the management UI. +func (g *Generator) RenderToString(ctx context.Context) (string, error) { + ifs, err := g.Ifaces.List(ctx) + if err != nil { + return "", fmt.Errorf("list ifaces: %w", err) + } + var combined strings.Builder + for _, ifc := range ifs { + if !ifc.Active { + continue + } + fmt.Fprintf(&combined, "# ── %s (%s) ────────────────────────────────\n", ifc.Name, ifc.Mode) + combined.WriteString("[Interface]\n") + fmt.Fprintf(&combined, "Address = %s\n", ifc.AddressCIDR) + combined.WriteString("PrivateKey = \n") + if ifc.ListenPort != nil { + fmt.Fprintf(&combined, "ListenPort = %d\n", *ifc.ListenPort) + } + if ifc.MTU != nil { + fmt.Fprintf(&combined, "MTU = %d\n", *ifc.MTU) + } + combined.WriteString("\n") + switch ifc.Mode { + case "client": + if ifc.PeerPublicKey != nil && ifc.PeerEndpoint != nil { + combined.WriteString("[Peer]\n") + fmt.Fprintf(&combined, "PublicKey = %s\n", *ifc.PeerPublicKey) + fmt.Fprintf(&combined, "Endpoint = %s\n", *ifc.PeerEndpoint) + if ifc.AllowedIPs != nil && *ifc.AllowedIPs != "" { + fmt.Fprintf(&combined, "AllowedIPs = %s\n", *ifc.AllowedIPs) + } else { + combined.WriteString("AllowedIPs = 0.0.0.0/0,::/0\n") + } + if ifc.PersistentKeepalive != nil { + fmt.Fprintf(&combined, "PersistentKeepalive = %d\n", *ifc.PersistentKeepalive) + } + if len(ifc.PeerPSKEnc) > 0 { + combined.WriteString("PresharedKey = \n") + } + } + case "server": + peers, err := g.Peers.ListForInterface(ctx, ifc.ID) + if err == nil { + sort.Slice(peers, func(i, j int) bool { return peers[i].Name < peers[j].Name }) + for _, p := range peers { + if !p.Enabled { + continue + } + combined.WriteString("[Peer]\n") + fmt.Fprintf(&combined, "# %s\n", p.Name) + fmt.Fprintf(&combined, "PublicKey = %s\n", p.PublicKey) + fmt.Fprintf(&combined, "AllowedIPs = %s\n", p.AllowedIPs) + if p.Keepalive != nil { + fmt.Fprintf(&combined, "PersistentKeepalive = %d\n", *p.Keepalive) + } + if len(p.PSKEnc) > 0 { + combined.WriteString("PresharedKey = \n") + } + combined.WriteString("\n") + } + } + } + combined.WriteString("\n") + } + if combined.Len() == 0 { + return "# No active WireGuard interfaces configured.\n", nil + } + return combined.String(), nil +} + func (g *Generator) Render(ctx context.Context) error { if err := os.MkdirAll(ConfDir, 0o700); err != nil { return fmt.Errorf("mkdir %s: %w", ConfDir, err) diff --git a/management-ui/src/pages/Settings/index.tsx b/management-ui/src/pages/Settings/index.tsx index b5aa0c8..d9b4cde 100644 --- a/management-ui/src/pages/Settings/index.tsx +++ b/management-ui/src/pages/Settings/index.tsx @@ -706,6 +706,8 @@ export default function SettingsPage() { { value: 'nftables', label: 'nftables' }, { value: 'squid', label: 'squid' }, { value: 'unbound', label: 'unbound' }, + { value: 'chrony', label: 'chrony' }, + { value: 'wireguard', label: 'wireguard' }, ]} />