From 784971f11169152dfbc812736f792b3dc29e9281 Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 20 Mar 2026 08:30:35 +0100 Subject: [PATCH] feat: add environment-based DNS server override support with service-aware fallback logic Add dnsServersForProfile and dnsServersForPeer helpers with conditional DNS server selection based on service presence. Use NEXAVPN_CLIENT_DNS_SERVERS override when services are configured, otherwise fall back to DEFAULT_DNS_SERVERS or gateway base DNS servers. Replace direct gateway DNS server usage in Enroll and applyCurrentPolicy with profileDNSServers variable. Update BuildSyncBundle to scan gateway DNS servers separately --- backend/internal/device/service.go | 47 ++++++++++++++++++++++++-- backend/internal/gateway/repository.go | 43 ++++++++++++++++++++++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/backend/internal/device/service.go b/backend/internal/device/service.go index e54cc86..bc99004 100644 --- a/backend/internal/device/service.go +++ b/backend/internal/device/service.go @@ -72,14 +72,15 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ selectedDestinations = destinations } selectedServices := servicesForSelectedProfile(availableProfiles, selectedProfileID) + profileDNSServers := dnsServersForProfile(selectedGateway.DNSServers, selectedServices) profileAllowedIPs := mergeProfileAllowedIPs( append(selectedDestinations, proxyRoutesForServices(selectedServices)...), - selectedGateway.DNSServers, + profileDNSServers, ) enrollment.Peer = PeerView{ AssignedIP: assignedIP, - DNSServers: selectedGateway.DNSServers, + DNSServers: profileDNSServers, AllowedIPs: profileAllowedIPs, Gateway: GatewayView{ ID: selectedGateway.ID, @@ -98,7 +99,7 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ Content: profile.BuildWireGuardConfig(profile.BuildInput{ PrivateKey: privateKeyPlaceholder, Address: assignedIP, - DNSServers: selectedGateway.DNSServers, + DNSServers: profileDNSServers, ServerPublicKey: selectedGateway.PublicKey, ServerEndpoint: selectedGateway.Endpoint, AllowedIPs: profileAllowedIPs, @@ -223,6 +224,7 @@ func (s *Service) applyCurrentPolicy(ctx context.Context, enrollment EnrollmentR if len(selectedDestinations) == 0 && len(selectedServices) == 0 { selectedDestinations = []string{"172.16.10.0/24"} } + enrollment.Peer.DNSServers = dnsServersForProfile(enrollment.Peer.DNSServers, selectedServices) enrollment.Resources = resourcesFromProfile(selectedDestinations, selectedServices) enrollment.AvailableProfiles = availableProfiles @@ -394,3 +396,42 @@ func dnsServerRoute(value string) string { } return value + "/32" } + +func dnsServersForProfile(base []string, services []AccessService) []string { + if len(services) > 0 { + if override := parseEnvList("NEXAVPN_CLIENT_DNS_SERVERS"); len(override) > 0 { + return override + } + return dedupeList(base) + } + + if override := parseEnvList("DEFAULT_DNS_SERVERS"); len(override) > 0 { + return override + } + return dedupeList(base) +} + +func parseEnvList(key string) []string { + return parseCommaList(os.Getenv(key)) +} + +func parseCommaList(raw string) []string { + seen := make(map[string]struct{}) + values := make([]string, 0) + for _, part := range strings.Split(raw, ",") { + value := strings.TrimSpace(part) + if value == "" { + continue + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + values = append(values, value) + } + return values +} + +func dedupeList(values []string) []string { + return parseCommaList(strings.Join(values, ",")) +} diff --git a/backend/internal/gateway/repository.go b/backend/internal/gateway/repository.go index 56e4840..2f1a594 100644 --- a/backend/internal/gateway/repository.go +++ b/backend/internal/gateway/repository.go @@ -128,7 +128,8 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) var peer wireguard.Peer var deviceID uuid.UUID var selectedProfileID *string - if err := rows.Scan(&deviceID, &peer.PublicKey, &peer.AssignedIP, &peer.AllowedDestinations, &peer.DNSServers, &selectedProfileID); err != nil { + var gatewayDNSServers []string + if err := rows.Scan(&deviceID, &peer.PublicKey, &peer.AssignedIP, &peer.AllowedDestinations, &gatewayDNSServers, &selectedProfileID); err != nil { return wireguard.GatewayBundle{}, err } peer.DeviceID = deviceID.String() @@ -136,6 +137,7 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) if err != nil { return wireguard.GatewayBundle{}, err } + peer.DNSServers = dnsServersForPeer(gatewayDNSServers, services) peer.AllowedServices = services bundle.Peers = append(bundle.Peers, peer) } @@ -220,6 +222,45 @@ func (r *PGRepository) ListServiceDNSRecords(ctx context.Context) ([]ServiceDNSR return items, rows.Err() } +func dnsServersForPeer(base []string, services []wireguard.AllowedService) []string { + if len(services) > 0 { + if override := parseEnvList("NEXAVPN_CLIENT_DNS_SERVERS"); len(override) > 0 { + return override + } + return dedupeList(base) + } + + if override := parseEnvList("DEFAULT_DNS_SERVERS"); len(override) > 0 { + return override + } + return dedupeList(base) +} + +func parseEnvList(key string) []string { + return parseCommaList(os.Getenv(key)) +} + +func parseCommaList(raw string) []string { + seen := make(map[string]struct{}) + values := make([]string, 0) + for _, part := range strings.Split(raw, ",") { + value := strings.TrimSpace(part) + if value == "" { + continue + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + values = append(values, value) + } + return values +} + +func dedupeList(values []string) []string { + return parseCommaList(strings.Join(values, ",")) +} + func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) { row := r.db.QueryRow(ctx, ` update gateways