From 31d3485a2f33307d118a966f1319c9ed86c368fc Mon Sep 17 00:00:00 2001 From: Debian Date: Fri, 29 May 2026 12:52:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(dns):=20Validierung=20f=C3=BCr=20DNS-Setti?= =?UTF-8?q?ngs=20=E2=80=94=20upstream=20IPs,=20ACL-CIDRs,=20TTL-Kreuzcheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: validateSettings() prüft vor dem Reload-Trigger ob upstream_forwards gültige IPs (inkl. @port), access_acl gültige IPs/CIDRs und listen_addresses gültige IPs sind; cache_max_ttl ≥ cache_min_ttl. Frontend: Pattern-Validatoren auf upstream_forwards + access_acl; TTL- Kreuzvalidierung mit dependencies. Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- internal/handlers/dns.go | 52 ++++++++++++++++++ management-ui/src/i18n/locales/de/common.json | 5 +- management-ui/src/i18n/locales/en/common.json | 5 +- management-ui/src/pages/DNS/index.tsx | 55 +++++++++++++++++-- 8 files changed, 113 insertions(+), 12 deletions(-) diff --git a/VERSION b/VERSION index 997fdeb..032f5c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.144 +1.1.145 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 7a4dd4c..4c29bed 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.144" +var version = "1.1.145" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 45149ab..00f09e8 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.144" +var version = "1.1.145" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 5daff93..f737b1f 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -41,7 +41,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.144" +var version = "1.1.145" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/dns.go b/internal/handlers/dns.go index c2e2e62..7032f1c 100644 --- a/internal/handlers/dns.go +++ b/internal/handlers/dns.go @@ -3,7 +3,9 @@ package handlers import ( "context" "errors" + "fmt" "log/slog" + "net" "os/exec" "strconv" "strings" @@ -286,6 +288,10 @@ func (h *DNSHandler) UpdateSettings(c *gin.Context) { response.BadRequest(c, err) return } + if err := validateSettings(&req); err != nil { + response.BadRequest(c, err) + return + } out, err := h.Repo.UpdateSettings(c.Request.Context(), req) if err != nil { response.Internal(c, err) @@ -312,6 +318,52 @@ func (h *DNSHandler) FlushCache(c *gin.Context) { // ── Validation ───────────────────────────────────────────────── +// validateSettings checks user-supplied DNS global settings before they +// reach unbound. A malformed upstream IP or CIDR would cause unbound to +// fail on the next reload without any visible error. +func validateSettings(s *models.DNSSettings) error { + if s.ListenPort < 1 || s.ListenPort > 65535 { + return fmt.Errorf("listen_port %d out of range (1-65535)", s.ListenPort) + } + if s.CacheMaxTTL < s.CacheMinTTL { + return fmt.Errorf("cache_max_ttl (%d) must be ≥ cache_min_ttl (%d)", s.CacheMaxTTL, s.CacheMinTTL) + } + for _, raw := range strings.Split(s.UpstreamForwards, ",") { + entry := strings.TrimSpace(raw) + if entry == "" { + continue + } + // strip optional @port suffix (e.g. 1.1.1.1@853) + host, _, _ := strings.Cut(entry, "@") + if net.ParseIP(host) == nil { + return fmt.Errorf("invalid upstream forwarder IP: %q", host) + } + } + for _, raw := range strings.Split(s.AccessACL, ",") { + entry := strings.TrimSpace(raw) + if entry == "" { + continue + } + if strings.Contains(entry, "/") { + if _, _, err := net.ParseCIDR(entry); err != nil { + return fmt.Errorf("invalid access ACL CIDR: %q", entry) + } + } else if net.ParseIP(entry) == nil { + return fmt.Errorf("invalid access ACL IP: %q", entry) + } + } + for _, raw := range strings.Split(s.ListenAddresses, ",") { + addr := strings.TrimSpace(raw) + if addr == "" { + continue + } + if net.ParseIP(addr) == nil { + return fmt.Errorf("invalid listen address: %q", addr) + } + } + return nil +} + func validateZone(z *models.DNSZone) error { if z.Name == "" { return errors.New("name required") diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 729a6a2..b4f45df 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -990,7 +990,10 @@ "flushCacheBtn": "DNS-Cache leeren", "flushCacheTooltip": "Alle gecachten Einträge verwerfen (unbound-control flush_zone .). Verwenden wenn DNS-Änderungen sofort greifen sollen.", "flushCacheOk": "DNS-Cache geleert", - "flushCacheFailed": "Cache-Flush fehlgeschlagen" + "flushCacheFailed": "Cache-Flush fehlgeschlagen", + "upstreamForwardsInvalid": "Jeder Forwarder muss eine gültige IP sein (z.B. 1.1.1.1 oder 9.9.9.9)", + "accessACLInvalid": "Jeder Eintrag muss eine gültige IP oder CIDR sein (z.B. 10.0.0.0/8)", + "cacheTTLError": "Cache-Max-TTL muss ≥ Cache-Min-TTL sein" } }, "fwd": { diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index c8f118e..29efae7 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -990,7 +990,10 @@ "flushCacheBtn": "Flush DNS cache", "flushCacheTooltip": "Discard all cached records (unbound-control flush_zone .). Use after DNS changes have propagated.", "flushCacheOk": "DNS cache flushed", - "flushCacheFailed": "Flush failed" + "flushCacheFailed": "Flush failed", + "upstreamForwardsInvalid": "Each forwarder must be a valid IP (e.g. 1.1.1.1 or 9.9.9.9)", + "accessACLInvalid": "Each entry must be a valid IP or CIDR (e.g. 10.0.0.0/8 or 192.168.1.0/24)", + "cacheTTLError": "Cache max-TTL must be ≥ cache min-TTL" } }, "fwd": { diff --git a/management-ui/src/pages/DNS/index.tsx b/management-ui/src/pages/DNS/index.tsx index 4e50601..408e7dd 100644 --- a/management-ui/src/pages/DNS/index.tsx +++ b/management-ui/src/pages/DNS/index.tsx @@ -524,12 +524,40 @@ function SettingsTab() { - + s.trim()).filter(Boolean).find(s => !IP.test(s)) + return bad ? Promise.reject(new Error(t('dns.settings.upstreamForwardsInvalid'))) : Promise.resolve() + }, + }, + ]} + > - + s.trim()).filter(Boolean).find(s => !IPCIDR.test(s)) + return bad ? Promise.reject(new Error(t('dns.settings.accessACLInvalid'))) : Promise.resolve() + }, + }, + ]} + > @@ -539,10 +567,25 @@ function SettingsTab() { - + - + ({ + validator(_, val) { + const minTTL = getFieldValue('cache_min_ttl') as number | undefined + if (val != null && minTTL != null && val < minTTL) { + return Promise.reject(new Error(t('dns.settings.cacheTTLError'))) + } + return Promise.resolve() + }, + }), + ]} + >