feat(dns): Validierung für DNS-Settings — upstream IPs, ACL-CIDRs, TTL-Kreuzcheck

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 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 12:52:28 +02:00
parent 0cbc781d4f
commit 31d3485a2f
8 changed files with 113 additions and 12 deletions

View File

@@ -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")