From ab7275059f2194163c1b51452a2f410ce13d5357 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 18 Mar 2026 09:39:40 +0100 Subject: [PATCH] feat: add web proxy target allowlist support via NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS environment variable Add alwaysAllowWebProxyTargets function to parse comma-separated IPs from NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS environment variable with deduplication. Update mergeProfileAllowedIPs to accept webProxyTargets parameter and merge them into profile allowed IPs using /32 routes. Add WebProxyTargets field to wireguard.Peer struct and populate it in BuildSyncBundle and device enrollment/policy application --- backend/internal/device/service.go | 45 +++++++++++++++++++++++--- backend/internal/gateway/repository.go | 25 ++++++++++++++ backend/internal/wireguard/types.go | 1 + deploy/.env.example | 1 + deploy/scripts/gateway-entrypoint.sh | 4 +++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/backend/internal/device/service.go b/backend/internal/device/service.go index 5818032..6871062 100644 --- a/backend/internal/device/service.go +++ b/backend/internal/device/service.go @@ -2,6 +2,7 @@ package device import ( "context" + "os" "strings" "github.com/google/uuid" @@ -62,7 +63,7 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ if len(destinations) == 0 { destinations = []string{"172.16.10.0/24"} } - profileAllowedIPs := mergeProfileAllowedIPs(destinations, selectedGateway.DNSServers) + profileAllowedIPs := mergeProfileAllowedIPs(destinations, selectedGateway.DNSServers, alwaysAllowWebProxyTargets()) enrollment.Peer = PeerView{ AssignedIP: assignedIP, @@ -184,13 +185,13 @@ func (s *Service) applyCurrentPolicy(ctx context.Context, enrollment EnrollmentR Label: destination, }) } - enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(destinations, enrollment.Peer.DNSServers) + enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(destinations, enrollment.Peer.DNSServers, alwaysAllowWebProxyTargets()) return withDebugProfile(enrollment), nil } -func mergeProfileAllowedIPs(destinations []string, dnsServers []string) []string { - seen := make(map[string]struct{}, len(destinations)+len(dnsServers)) - merged := make([]string, 0, len(destinations)+len(dnsServers)) +func mergeProfileAllowedIPs(destinations []string, dnsServers []string, webProxyTargets []string) []string { + seen := make(map[string]struct{}, len(destinations)+len(dnsServers)+len(webProxyTargets)) + merged := make([]string, 0, len(destinations)+len(dnsServers)+len(webProxyTargets)) for _, destination := range destinations { destination = strings.TrimSpace(destination) @@ -216,6 +217,18 @@ func mergeProfileAllowedIPs(destinations []string, dnsServers []string) []string merged = append(merged, route) } + for _, target := range webProxyTargets { + route := dnsServerRoute(target) + if route == "" { + continue + } + if _, exists := seen[route]; exists { + continue + } + seen[route] = struct{}{} + merged = append(merged, route) + } + return merged } @@ -229,3 +242,25 @@ func dnsServerRoute(value string) string { } return value + "/32" } + +func alwaysAllowWebProxyTargets() []string { + raw := os.Getenv("NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS") + if strings.TrimSpace(raw) == "" { + return nil + } + + seen := make(map[string]struct{}) + targets := 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{}{} + targets = append(targets, value) + } + return targets +} diff --git a/backend/internal/gateway/repository.go b/backend/internal/gateway/repository.go index 2b4c33a..45096c8 100644 --- a/backend/internal/gateway/repository.go +++ b/backend/internal/gateway/repository.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "net/netip" + "os" + "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -118,12 +120,35 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) return wireguard.GatewayBundle{}, err } peer.DeviceID = deviceID.String() + peer.WebProxyTargets = alwaysAllowWebProxyTargets() bundle.Peers = append(bundle.Peers, peer) } return bundle, rows.Err() } +func alwaysAllowWebProxyTargets() []string { + raw := os.Getenv("NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS") + if strings.TrimSpace(raw) == "" { + return nil + } + + seen := make(map[string]struct{}) + targets := 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{}{} + targets = append(targets, value) + } + return targets +} + func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) { row := r.db.QueryRow(ctx, ` update gateways diff --git a/backend/internal/wireguard/types.go b/backend/internal/wireguard/types.go index 7160d99..73068cc 100644 --- a/backend/internal/wireguard/types.go +++ b/backend/internal/wireguard/types.go @@ -6,6 +6,7 @@ type Peer struct { AssignedIP string `json:"assigned_ip"` AllowedDestinations []string `json:"allowed_destinations"` DNSServers []string `json:"dns_servers"` + WebProxyTargets []string `json:"web_proxy_targets"` } type GatewayBundle struct { diff --git a/deploy/.env.example b/deploy/.env.example index 03e016f..4f73323 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -24,3 +24,4 @@ NEXAVPN_GATEWAY_INTERFACE=wg0 NEXAVPN_UPLINK_INTERFACE=eth0 NEXAVPN_ENABLE_MASQUERADE=true NEXAVPN_BACKEND_HOST=127.0.0.1 +NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS=172.16.0.109 diff --git a/deploy/scripts/gateway-entrypoint.sh b/deploy/scripts/gateway-entrypoint.sh index 4c0ff37..f1357e2 100644 --- a/deploy/scripts/gateway-entrypoint.sh +++ b/deploy/scripts/gateway-entrypoint.sh @@ -118,6 +118,10 @@ EOF echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} udp dport 53 accept" echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} tcp dport 53 accept" done + printf '%s' "${peer}" | jq -r '.web_proxy_targets[]?' | while read -r proxy_target; do + echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${proxy_target} tcp dport 80 accept" + echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${proxy_target} tcp dport 443 accept" + done printf '%s' "${peer}" | jq -r '.allowed_destinations[]?' | while read -r destination; do echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${destination} accept" done