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
This commit is contained in:
2026-03-18 09:39:40 +01:00
parent d1940e6f28
commit ab7275059f
5 changed files with 71 additions and 5 deletions

View File

@@ -2,6 +2,7 @@ package device
import ( import (
"context" "context"
"os"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
@@ -62,7 +63,7 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ
if len(destinations) == 0 { if len(destinations) == 0 {
destinations = []string{"172.16.10.0/24"} destinations = []string{"172.16.10.0/24"}
} }
profileAllowedIPs := mergeProfileAllowedIPs(destinations, selectedGateway.DNSServers) profileAllowedIPs := mergeProfileAllowedIPs(destinations, selectedGateway.DNSServers, alwaysAllowWebProxyTargets())
enrollment.Peer = PeerView{ enrollment.Peer = PeerView{
AssignedIP: assignedIP, AssignedIP: assignedIP,
@@ -184,13 +185,13 @@ func (s *Service) applyCurrentPolicy(ctx context.Context, enrollment EnrollmentR
Label: destination, Label: destination,
}) })
} }
enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(destinations, enrollment.Peer.DNSServers) enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(destinations, enrollment.Peer.DNSServers, alwaysAllowWebProxyTargets())
return withDebugProfile(enrollment), nil return withDebugProfile(enrollment), nil
} }
func mergeProfileAllowedIPs(destinations []string, dnsServers []string) []string { func mergeProfileAllowedIPs(destinations []string, dnsServers []string, webProxyTargets []string) []string {
seen := make(map[string]struct{}, len(destinations)+len(dnsServers)) seen := make(map[string]struct{}, len(destinations)+len(dnsServers)+len(webProxyTargets))
merged := make([]string, 0, len(destinations)+len(dnsServers)) merged := make([]string, 0, len(destinations)+len(dnsServers)+len(webProxyTargets))
for _, destination := range destinations { for _, destination := range destinations {
destination = strings.TrimSpace(destination) destination = strings.TrimSpace(destination)
@@ -216,6 +217,18 @@ func mergeProfileAllowedIPs(destinations []string, dnsServers []string) []string
merged = append(merged, route) 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 return merged
} }
@@ -229,3 +242,25 @@ func dnsServerRoute(value string) string {
} }
return value + "/32" 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
}

View File

@@ -4,6 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net/netip" "net/netip"
"os"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
@@ -118,12 +120,35 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
return wireguard.GatewayBundle{}, err return wireguard.GatewayBundle{}, err
} }
peer.DeviceID = deviceID.String() peer.DeviceID = deviceID.String()
peer.WebProxyTargets = alwaysAllowWebProxyTargets()
bundle.Peers = append(bundle.Peers, peer) bundle.Peers = append(bundle.Peers, peer)
} }
return bundle, rows.Err() 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) { func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) {
row := r.db.QueryRow(ctx, ` row := r.db.QueryRow(ctx, `
update gateways update gateways

View File

@@ -6,6 +6,7 @@ type Peer struct {
AssignedIP string `json:"assigned_ip"` AssignedIP string `json:"assigned_ip"`
AllowedDestinations []string `json:"allowed_destinations"` AllowedDestinations []string `json:"allowed_destinations"`
DNSServers []string `json:"dns_servers"` DNSServers []string `json:"dns_servers"`
WebProxyTargets []string `json:"web_proxy_targets"`
} }
type GatewayBundle struct { type GatewayBundle struct {

View File

@@ -24,3 +24,4 @@ NEXAVPN_GATEWAY_INTERFACE=wg0
NEXAVPN_UPLINK_INTERFACE=eth0 NEXAVPN_UPLINK_INTERFACE=eth0
NEXAVPN_ENABLE_MASQUERADE=true NEXAVPN_ENABLE_MASQUERADE=true
NEXAVPN_BACKEND_HOST=127.0.0.1 NEXAVPN_BACKEND_HOST=127.0.0.1
NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS=172.16.0.109

View File

@@ -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} udp dport 53 accept"
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} tcp dport 53 accept" echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} tcp dport 53 accept"
done 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 printf '%s' "${peer}" | jq -r '.allowed_destinations[]?' | while read -r destination; do
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${destination} accept" echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${destination} accept"
done done