VYPR
High severityNVD Advisory· Published Mar 18, 2026· Updated Mar 18, 2026

Kube-router Proxy Module Blindly Trusts ExternalIPs/LoadBalancer IPs Enabling Cluster-Wide Traffic Hijacking and DNS DoS

CVE-2026-32254

Description

Kube-router is a turnkey solution for Kubernetes networking. Prior to version 2.8.0, Kube-router's proxy module does not validate externalIPs or loadBalancer IPs before programming them into the node's network configuration. Version 2.8.0 contains a patch for the issue. Available workarounds include enabling DenyServiceExternalIPs feature gate, deploying admission policy, restricting service creation RBAC, monitoring service changes, and applying BGP prefix filtering.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Kube-router proxy module before v2.8.0 fails to validate externalIPs or loadBalancer IPs, allowing users with namespace-scoped Service permissions to hijack cluster traffic or cause DNS DoS.

Vulnerability

Description

Kube-router's proxy module, responsible for programming IPVS virtual services and local routes on cluster nodes, blindly copies IPs from Service.spec.externalIPs and status.loadBalancer.ingress without validating them against the --service-external-ip-range parameter [1]. This parameter, intended to restrict allowed external IPs, is only consumed by the network policy module, leaving a gap between administrator expectations and actual enforcement. The buildServicesInfo() function directly applies these IPs to the node's network configuration (kube-dummy-if interface, IPVS, LOCAL routing table) regardless of whether they fall within administrator-defined ranges [1].

Exploitation

Prerequisites

Exploitation primarily affects multi-tenant clusters where untrusted users have namespace-scoped permissions to create or modify Services [1]. An attacker with such permissions can set arbitrary externalIPs or inject IPs into status.loadBalancer.ingress via crafted Service objects, causing those IPs to be programmed on all nodes running kube-router. Kubernetes' DenyServiceExternalIPs feature gate remains disabled by default through v1.31, providing no native admission control [1]. This is a well-known design limitation, previously classified as CVE-2020-8554, and is not unique to kube-router—kube-proxy and other proxies exhibit the same behavior [1].

Impact

A successful attacker can hijack traffic destined for any arbitrary IP by binding that IP as a virtual service on all cluster nodes, enabling cluster-wide traffic interception. Alternatively, they can cause denial of service by assigning an IP already in use by critical cluster services such as kube-dns, breaking internal DNS resolution [1]. The lack of validation means an attacker can effectively undermine network segmentation and privilege boundaries intended by the administrator.

Mitigation

Version 2.8.0 includes a patch where the proxy module now validates externalIPs and loadBalancerIPs against configured CIDR ranges when the new --strict-external-ip-validation flag is enabled [3][4]. Workarounds for unpatched versions include enabling the DenyServiceExternalIPs feature gate (if on a supported Kubernetes version), deploying an admission webhook policy, restricting Service creation RBAC to trusted users, monitoring Service changes for unexpected external IPs, and applying BGP prefix filtering at the network edge [1].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/cloudnativelabs/kube-router/v2Go
< 2.8.02.8.0

Affected products

2

Patches

1
a1f0b2eea3ee

fix: validate external IPs and LB IPs against configured ranges

https://github.com/cloudnativelabs/kube-routerAaron U'RenMar 12, 2026via ghsa
26 files changed · +1570 218
  • docs/dsr.md+8 1 modified
    @@ -19,7 +19,11 @@ Requirements:
     
     * ClusterIP type service has an externalIP set on it or is a LoadBalancer type service
     * kube-router has been started with `--service-external-ip-range` configured at least once. This option can be
    -  specified multiple times for multiple ranges. The external IPs or LoadBalancer IPs must be included in these ranges.
    +  specified multiple times for multiple ranges. The external IPs must be included in these ranges. For LoadBalancer
    +  IPs, `--loadbalancer-ip-range` must also be configured.
    +* With `--strict-external-ip-validation` enabled (the default), these same ranges are used by the proxy module to
    +  validate IPs before programming them into IPVS. If you already have `--service-external-ip-range` and
    +  `--loadbalancer-ip-range` configured for DSR, no additional configuration is needed.
     * kube-router must be run in service proxy mode with `--run-service-proxy` (this option is defaulted to `true` if left
       unspecified)
     * If you are advertising the service outside the cluster `--advertise-external-ip` must be set
    @@ -47,6 +51,9 @@ kubectl annotate service my-service "kube-router.io/service.dsr=tunnel"
     ## Things To Lookout For
     
     * In the current implementation, **DSR will only be available to the external IPs or LoadBalancer IPs**
    +* When `--strict-external-ip-validation` is enabled (the default), externalIPs and loadBalancerIPs must pass validation
    +  against configured CIDR ranges before they can be programmed into IPVS for DSR. Ensure your DSR service IPs are
    +  covered by `--service-external-ip-range` and/or `--loadbalancer-ip-range`.
     * **The current implementation does not support port remapping.** So you need to use same port and target port for the
       service.
     * In order for DSR to work correctly, an `ipip` tunnel to the pod is used. This reduces the
    
  • docs/how-it-works.md+6 1 modified
    @@ -28,10 +28,15 @@ and how they got mapped to the IPVS by kube-router:
     
     kube-router watches the Kubernetes API server to get updates on the
     Services/Endpoints and automatically syncs the IPVS configuration to reflect the
    -desired state of Services. kube-router uses IPVS masquerading mode with round robin scheduling by default
    +desired state of Services.
    +
    +kube-router uses IPVS masquerading mode with round robin scheduling by default
     (other scheduling algorithms such as least connection, source hashing, and maglev hashing are also supported
     via service annotations). Source pod IP is preserved so that appropriate network policies can be applied.
     
    +When `--strict-external-ip-validation` is enabled (the default), externalIPs and loadBalancerIPs are validated
    +against configured CIDR ranges before being programmed into IPVS.
    +
     ## Pod Ingress Firewall
     
     Blog: [Enforcing Kubernetes network policies with iptables](https://cloudnativelabs.github.io/post/2017-05-1-kube-network-policies/)
    
  • docs/ipv6.md+3 1 modified
    @@ -44,7 +44,9 @@ Addresses:
     ```
     
     * Add additional `--service-cluster-ip-range` and `--service-external-ip-range` kube-router parameters for your IPv6
    -  addresses.
    +  addresses. If you use LoadBalancer services with IPv6, also add `--loadbalancer-ip-range` for your IPv6 ranges.
    +  These ranges are used for both routing configuration and proxy-level IP validation when
    +  `--strict-external-ip-validation` is enabled (the default).
     * If you use `--enable-cni=true`, ensure `kube-controller-manager` has been started with both IPv4 and IPv6 cluster
       CIDRs (e.g. `--cluster-cidr=10.242.0.0/16,2001:db8:42:1000::/56`)
     * Ensure `kube-controller-manager` & `kube-apiserver` have been started with both IPv4 and IPv6 service cluster IP
    
  • docs/load-balancer-allocator.md+9 1 modified
    @@ -83,4 +83,12 @@ When running the controller outside a pod, both `POD_NAME` and `POD_NAMESPACE` m
     
     ## Notes
     
    -It's not possible to specify the addresses for the load balancer services. An externalIP service can be used instead.
    +It's not possible to specify the addresses for the load balancer services. An externalIP service can be used instead
    +(note that externalIPs are subject to validation via `--service-external-ip-range` when
    +`--strict-external-ip-validation` is enabled).
    +
    +The `--loadbalancer-ip-range` flag serves a dual purpose: it defines the CIDR ranges used by the load balancer
    +allocator for IPAM **and** is used by the proxy module to validate loadBalancerIPs when
    +`--strict-external-ip-validation` is enabled (the default). If a LoadBalancer IP falls outside the configured ranges
    +(for example, an IP assigned by an external controller like MetalLB), the proxy will reject it unless the range is
    +expanded to include it or strict validation is disabled.
    
  • docs/user-guide.md+76 2 modified
    @@ -135,7 +135,7 @@ Usage of kube-router:
           --ipvs-sync-period duration                     The delay between ipvs config synchronizations (e.g. '5s', '1m', '2h22m'). Must be greater than 0. (default 5m0s)
           --kubeconfig string                             Path to kubeconfig file with authorization information (the master location is set by the master flag).
           --loadbalancer-default-class                    Handle loadbalancer services without a class (default true)
    -      --loadbalancer-ip-range strings                 CIDR values from which loadbalancer services addresses are assigned (can be specified multiple times)
    +      --loadbalancer-ip-range strings                 CIDR values from which loadbalancer services addresses are assigned (can be specified multiple times). Also used by the proxy module to validate loadBalancerIPs when --strict-external-ip-validation is enabled.
           --loadbalancer-sync-period duration             The delay between checking for missed services (e.g. '5s', '1m'). Must be greater than 0. (default 1m0s)
           --masquerade-all                                SNAT all traffic to cluster IP/node port.
           --master string                                 The address of the Kubernetes API server (overrides any value in kubeconfig).
    @@ -162,11 +162,12 @@ Usage of kube-router:
           --run-service-proxy                             Enables Service Proxy -- sets up IPVS for Kubernetes Services. (default true)
           --runtime-endpoint string                       Path to CRI compatible container runtime socket (used for DSR mode). Currently known working with containerd.
           --service-cluster-ip-range strings              CIDR values from which service cluster IPs are assigned (can be specified up to 2 times) (default [10.96.0.0/12])
    -      --service-external-ip-range strings             Specify external IP CIDRs that are used for inter-cluster communication (can be specified multiple times)
    +      --service-external-ip-range strings             Specify external IP CIDRs that are used for inter-cluster communication (can be specified multiple times). Also used by the proxy module to validate externalIPs when --strict-external-ip-validation is enabled.
           --service-node-port-range string                NodePort range specified with either a hyphen or colon (default "30000-32767")
           --service-tcp-timeout duration                  Specify TCP timeout for IPVS services in standard duration syntax (e.g. '5s', '1m'), default 0s preserves default system value (default: 0s)
           --service-tcpfin-timeout duration               Specify TCP FIN timeout for IPVS services in standard duration syntax (e.g. '5s', '1m'), default 0s preserves default system value (default: 0s)
           --service-udp-timeout duration                  Specify UDP timeout for IPVS services in standard duration syntax (e.g. '5s', '1m'), default 0s preserves default system value (default: 0s)
    +      --strict-external-ip-validation                 When enabled, the proxy module validates externalIPs and loadBalancerIPs against configured CIDR ranges (--service-external-ip-range and --loadbalancer-ip-range). When strict mode is enabled and no range is configured, all externalIPs / loadBalancerIPs are rejected (default-deny). Disable this flag to restore previous behavior of accepting all IPs without validation. (default true)
       -v, --v string                                      log level for V logs (default "0")
       -V, --version                                       Print version information.
     ```
    @@ -313,6 +314,79 @@ Advertising LoadBalancer IPs works by inspecting the services `status.loadBalanc
     LoadBalancers like for example MetalLb. This has been successfully tested together with
     [MetalLB](https://github.com/google/metallb) in ARP mode.
     
    +**Note:** When `--strict-external-ip-validation` is enabled (the default), externalIPs and loadBalancerIPs must pass
    +validation against configured CIDR ranges before they are programmed into IPVS. IPs that are rejected by the proxy
    +module will not be advertised. See [External IP and LoadBalancer IP Validation](#external-ip-and-loadbalancer-ip-validation)
    +for details.
    +
    +## External IP and LoadBalancer IP Validation
    +
    +Starting with v2.8.0, the service proxy (Network Services Controller) validates `externalIPs` and
    +`loadBalancerIPs` before programming them into IPVS. This is controlled by the `--strict-external-ip-validation` flag,
    +which defaults to `true`.
    +
    +**This is a breaking change.** Previously, all externalIPs and loadBalancerIPs were accepted unconditionally. Now they
    +are validated against configured CIDR ranges.
    +
    +### How It Works
    +
    +When `--strict-external-ip-validation=true` (the default):
    +
    +- **externalIPs** are validated against `--service-external-ip-range` CIDRs
    +- **loadBalancerIPs** are validated against `--loadbalancer-ip-range` CIDRs
    +- **ClusterIP conflict detection**: externalIPs that fall within `--service-cluster-ip-range` are always rejected to
    +  prevent denial-of-service against cluster services
    +- **Default-deny**: if no range is configured for a given IP type, **all** IPs of that type are rejected
    +
    +When `--strict-external-ip-validation=false`:
    +
    +- All externalIPs and loadBalancerIPs are accepted without validation (previous behavior)
    +
    +### Configuration Examples
    +
    +Allow specific externalIP and loadBalancerIP ranges:
    +
    +```sh
    +kube-router \
    +  --run-service-proxy=true \
    +  --service-external-ip-range=198.51.100.0/24 \
    +  --service-external-ip-range=203.0.113.0/24 \
    +  --loadbalancer-ip-range=10.255.0.0/16
    +```
    +
    +Disable validation to restore previous behavior:
    +
    +```sh
    +kube-router \
    +  --run-service-proxy=true \
    +  --strict-external-ip-validation=false
    +```
    +
    +### Upgrading
    +
    +If you are upgrading from a version without this feature and you use externalIPs or LoadBalancer services, you must
    +either:
    +
    +1. **Configure the appropriate CIDR ranges** before upgrading by adding `--service-external-ip-range` and/or
    +   `--loadbalancer-ip-range` flags to your kube-router arguments
    +2. **Disable strict validation** by adding `--strict-external-ip-validation=false` to your kube-router arguments
    +
    +If you do neither, all externalIPs and loadBalancerIPs will be rejected after the upgrade (default-deny behavior).
    +
    +See [Upgrading kube-router](upgrading.md) for more details.
    +
    +### Shared Flags
    +
    +The `--service-external-ip-range` and `--loadbalancer-ip-range` flags serve multiple purposes:
    +
    +- `--service-external-ip-range` is used by the **network policy controller** for firewall rules and by the
    +  **proxy module** for externalIP validation
    +- `--loadbalancer-ip-range` is used by the **load balancer allocator** for IPAM and by the **proxy module** for
    +  loadBalancerIP validation
    +
    +If you were already using these flags for other controllers, the proxy module will automatically benefit from the same
    +configuration.
    +
     ## Controlling Service Locality or Traffic Policies
     
     Service availability both externally and locally (within the cluster) can be controlled via the Kubernetes standard
    
  • pkg/cmd/kube-router.go+19 4 modified
    @@ -17,6 +17,7 @@ import (
     	"github.com/cloudnativelabs/kube-router/v2/pkg/k8s/indexers"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/metrics"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/version"
     	"k8s.io/klog/v2"
    @@ -182,9 +183,22 @@ func (kr *KubeRouter) Run() error {
     		}
     	}
     
    +	ipValidator, err := svcip.NewValidator(svcip.Config{
    +		ExternalIPCIDRs:   kr.Config.ExternalIPCIDRs,
    +		LoadBalancerCIDRs: kr.Config.LoadBalancerCIDRs,
    +		ClusterIPCIDRs:    kr.Config.ClusterIPCIDRs,
    +		StrictValidation:  kr.Config.StrictExternalIPValidation,
    +		EnableIPv4:        kr.Config.EnableIPv4,
    +		EnableIPv6:        kr.Config.EnableIPv6,
    +	})
    +	if err != nil {
    +		return fmt.Errorf("failed to create service IP validator: %v", err)
    +	}
    +	ipValidator.LogStatus()
    +
     	if kr.Config.RunRouter {
     		nrc, err := routing.NewNetworkRoutingController(kr.Client, kr.Config,
    -			nodeInformer, svcInformer, epSliceInformer, &ipsetMutex)
    +			nodeInformer, svcInformer, epSliceInformer, &ipsetMutex, ipValidator)
     		if err != nil {
     			return fmt.Errorf("failed to create network routing controller: %v", err)
     		}
    @@ -215,7 +229,7 @@ func (kr *KubeRouter) Run() error {
     
     	if kr.Config.RunServiceProxy {
     		nsc, err := proxy.NewNetworkServicesController(kr.Client, kr.Config,
    -			svcInformer, epSliceInformer, podInformer, &ipsetMutex)
    +			svcInformer, epSliceInformer, podInformer, &ipsetMutex, ipValidator)
     		if err != nil {
     			return fmt.Errorf("failed to create network services controller: %v", err)
     		}
    @@ -246,7 +260,8 @@ func (kr *KubeRouter) Run() error {
     			return fmt.Errorf("failed to create iptables handlers: %v", err)
     		}
     		npc, err := netpol.NewNetworkPolicyController(kr.Client,
    -			kr.Config, podInformer, npInformer, nsInformer, &ipsetMutex, nil, iptablesCmdHandlers, ipSetHandlers)
    +			kr.Config, podInformer, npInformer, nsInformer, &ipsetMutex, nil, iptablesCmdHandlers, ipSetHandlers,
    +			ipValidator)
     		if err != nil {
     			return fmt.Errorf("failed to create network policy controller: %v", err)
     		}
    @@ -270,7 +285,7 @@ func (kr *KubeRouter) Run() error {
     
     	if kr.Config.RunLoadBalancer {
     		klog.V(0).Info("running load balancer allocator controller")
    -		lbc, err := lballoc.NewLoadBalancerController(kr.Client, kr.Config, svcInformer)
    +		lbc, err := lballoc.NewLoadBalancerController(kr.Client, kr.Config, svcInformer, ipValidator)
     		if err != nil {
     			return fmt.Errorf("failed to create load balancer allocator: %v", err)
     		}
    
  • pkg/controllers/lballoc/lballoc.go+6 29 modified
    @@ -10,6 +10,8 @@ import (
     
     	"github.com/cloudnativelabs/kube-router/v2/pkg/healthcheck"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	v1core "k8s.io/api/core/v1"
     	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     	"k8s.io/client-go/kubernetes"
    @@ -143,12 +145,7 @@ func (ir *ipRanges) Len() int {
     }
     
     func (ir *ipRanges) Contains(ip net.IP) bool {
    -	for _, in := range ir.ipRanges {
    -		if in.Contains(ip) {
    -			return true
    -		}
    -	}
    -	return false
    +	return utils.IsIPInRanges(ip, ir.ipRanges)
     }
     
     func (lbc *LoadBalancerController) runLeaderElection(ctx context.Context, isLeaderChan chan<- bool) {
    @@ -471,31 +468,11 @@ func (lbc *LoadBalancerController) Run(healthChan chan<- *healthcheck.Controller
     
     func NewLoadBalancerController(clientset kubernetes.Interface,
     	config *options.KubeRouterConfig, svcInformer cache.SharedIndexInformer,
    +	ipRanges svcip.RangeQuerier,
     ) (*LoadBalancerController, error) {
    -	ranges4 := make([]net.IPNet, 0)
    -	ranges6 := make([]net.IPNet, 0)
    -
    -	for _, ir := range config.LoadBalancerCIDRs {
    -		ip, cidr, err := net.ParseCIDR(ir)
    -		if err != nil {
    -			return nil, err
    -		}
    -		if ip.To4() != nil && !config.EnableIPv4 {
    -			return nil, errors.New("IPv4 loadbalancer CIDR specified while IPv4 is disabled")
    -		}
    -		if ip.To4() == nil && !config.EnableIPv6 {
    -			return nil, errors.New("IPv6 loadbalancer CIDR specified while IPv6 is disabled")
    -		}
    -		if ip.To4() != nil {
    -			ranges4 = append(ranges4, *cidr)
    -		} else {
    -			ranges6 = append(ranges6, *cidr)
    -		}
    -	}
    -
     	lbc := &LoadBalancerController{
    -		ipv4Ranges:   newipRanges(ranges4),
    -		ipv6Ranges:   newipRanges(ranges6),
    +		ipv4Ranges:   newipRanges(ipRanges.LoadBalancerIPRanges(v1core.IPv4Protocol)),
    +		ipv6Ranges:   newipRanges(ipRanges.LoadBalancerIPRanges(v1core.IPv6Protocol)),
     		addChan:      make(chan v1core.Service),
     		allocateChan: make(chan v1core.Service),
     		clientset:    clientset,
    
  • pkg/controllers/lballoc/lballoc_test.go+31 12 modified
    @@ -9,6 +9,7 @@ import (
     	"time"
     
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	v1core "k8s.io/api/core/v1"
     	"k8s.io/client-go/kubernetes/fake"
     	"k8s.io/client-go/tools/cache"
    @@ -695,23 +696,41 @@ func TestNewLoadBalancerController(t *testing.T) {
     	}
     	fs := fake.NewSimpleClientset()
     
    -	_, err := NewLoadBalancerController(fs, config, mf)
    +	validator, validatorErr := svcip.NewValidator(svcip.Config{
    +		LoadBalancerCIDRs: config.LoadBalancerCIDRs,
    +		ClusterIPCIDRs:    []string{"10.96.0.0/12", "fd00::/112"},
    +		EnableIPv4:        config.EnableIPv4,
    +		EnableIPv6:        config.EnableIPv6,
    +	})
    +	if validatorErr != nil {
    +		t.Fatalf("expected validator to succeed, got %s", validatorErr)
    +	}
    +
    +	_, err := NewLoadBalancerController(fs, config, mf, validator)
     	if err != nil {
     		t.Fatalf("expected %v, got %s", nil, err)
     	}
     
    -	config.EnableIPv4 = false
    -	_, err = NewLoadBalancerController(fs, config, mf)
    -	errExp := "IPv4 loadbalancer CIDR specified while IPv4 is disabled"
    -	if err.Error() != errExp {
    -		t.Fatalf("expected %s, got %s", errExp, err)
    +	// Family-disabled validation is now handled by the validator at startup.
    +	// Test that the validator correctly rejects IPv4 CIDRs when IPv4 is disabled.
    +	_, validatorErr = svcip.NewValidator(svcip.Config{
    +		LoadBalancerCIDRs: config.LoadBalancerCIDRs,
    +		ClusterIPCIDRs:    []string{"fd00::/112"},
    +		EnableIPv4:        false,
    +		EnableIPv6:        true,
    +	})
    +	if validatorErr == nil {
    +		t.Fatal("expected validator to fail with IPv4 disabled, but it succeeded")
     	}
     
    -	config.EnableIPv4 = true
    -	config.EnableIPv6 = false
    -	_, err = NewLoadBalancerController(fs, config, mf)
    -	errExp = "IPv6 loadbalancer CIDR specified while IPv6 is disabled"
    -	if err.Error() != errExp {
    -		t.Fatalf("expected %s, got %s", errExp, err)
    +	// Test that the validator correctly rejects IPv6 CIDRs when IPv6 is disabled.
    +	_, validatorErr = svcip.NewValidator(svcip.Config{
    +		LoadBalancerCIDRs: config.LoadBalancerCIDRs,
    +		ClusterIPCIDRs:    []string{"10.96.0.0/12"},
    +		EnableIPv4:        true,
    +		EnableIPv6:        false,
    +	})
    +	if validatorErr == nil {
    +		t.Fatal("expected validator to fail with IPv6 disabled, but it succeeded")
     	}
     }
    
  • pkg/controllers/netpol/ipset_fixture_test.go+11 0 modified
    @@ -8,6 +8,7 @@ import (
     
     	"github.com/cloudnativelabs/kube-router/v2/pkg/controllers/testhelpers"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     
     	"github.com/stretchr/testify/require"
    @@ -59,6 +60,15 @@ func TestNetworkPolicyFixtureIPSets(t *testing.T) {
     
     	linkQ := utils.NewFakeLocalLinkQuerier(collectNodeIPs(nodes), nil)
     
    +	validator, validatorErr := svcip.NewValidator(svcip.Config{
    +		ExternalIPCIDRs:   config.ExternalIPCIDRs,
    +		LoadBalancerCIDRs: config.LoadBalancerCIDRs,
    +		ClusterIPCIDRs:    config.ClusterIPCIDRs,
    +		EnableIPv4:        config.EnableIPv4,
    +		EnableIPv6:        config.EnableIPv6,
    +	})
    +	require.NoError(t, validatorErr)
    +
     	controller, err := NewNetworkPolicyController(
     		client,
     		config,
    @@ -75,6 +85,7 @@ func TestNetworkPolicyFixtureIPSets(t *testing.T) {
     			v1.IPv4Protocol: ipv4Handler,
     			v1.IPv6Protocol: ipv6Handler,
     		},
    +		validator,
     	)
     	require.NoError(t, err)
     
    
  • pkg/controllers/netpol/network_policy_controller.go+67 134 modified
    @@ -14,6 +14,7 @@ import (
     	"github.com/cloudnativelabs/kube-router/v2/pkg/healthcheck"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/metrics"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	"github.com/coreos/go-iptables/iptables"
     	"k8s.io/klog/v2"
    @@ -64,17 +65,15 @@ var (
     
     // NetworkPolicyController struct to hold information required by NetworkPolicyController
     type NetworkPolicyController struct {
    -	krNode                      utils.NodeIPAndFamilyAware
    -	serviceClusterIPRanges      []net.IPNet
    -	serviceExternalIPRanges     []net.IPNet
    -	serviceLoadBalancerIPRanges []net.IPNet
    -	serviceNodePortRange        string
    -	mu                          sync.Mutex
    -	syncPeriod                  time.Duration
    -	MetricsEnabled              bool
    -	healthChan                  chan<- *healthcheck.ControllerHeartbeat
    -	fullSyncRequestChan         chan struct{}
    -	ipsetMutex                  *sync.Mutex
    +	krNode               utils.NodeIPAndFamilyAware
    +	ipRanges             svcip.RangeQuerier
    +	serviceNodePortRange string
    +	mu                   sync.Mutex
    +	syncPeriod           time.Duration
    +	MetricsEnabled       bool
    +	healthChan           chan<- *healthcheck.ControllerHeartbeat
    +	fullSyncRequestChan  chan struct{}
    +	ipsetMutex           *sync.Mutex
     
     	iptablesCmdHandlers map[v1core.IPFamily]utils.IPTablesHandler
     	iptablesSaveRestore map[v1core.IPFamily]utils.IPTablesSaveRestorer
    @@ -472,22 +471,18 @@ func (npc *NetworkPolicyController) ensureTopLevelChains() {
     		}
     	}
     
    -	if len(npc.serviceClusterIPRanges) > 0 {
    -		for idx, serviceRange := range npc.serviceClusterIPRanges {
    -			var family v1core.IPFamily
    -			if serviceRange.IP.To4() != nil {
    -				family = v1core.IPv4Protocol
    -			} else {
    -				family = v1core.IPv6Protocol
    -			}
    +	if len(npc.ipRanges.ClusterIPRanges()) == 0 {
    +		klog.Fatalf("Primary service cluster IP range is not configured")
    +	}
    +	for _, family := range []v1core.IPFamily{v1core.IPv4Protocol, v1core.IPv6Protocol} {
    +		for _, serviceRange := range npc.ipRanges.ClusterIPRanges(family) {
     			klog.V(2).Infof("Allow traffic to ingress towards Cluster IP Range: %s for family: %s",
     				serviceRange.String(), family)
    -			npc.allowTrafficToClusterIPRange(rulePosition[family], &npc.serviceClusterIPRanges[idx],
    -				addUUIDForRuleSpec, ensureRuleAtPosition, "allow traffic to primary/secondary cluster IP range")
    +			npc.allowTrafficToClusterIPRange(rulePosition[family], &serviceRange,
    +				addUUIDForRuleSpec, ensureRuleAtPosition,
    +				"allow traffic to primary/secondary cluster IP range")
     			rulePosition[family]++
     		}
    -	} else {
    -		klog.Fatalf("Primary service cluster IP range is not configured")
     	}
     
     	for family, handler := range npc.iptablesCmdHandlers {
    @@ -531,56 +526,40 @@ func (npc *NetworkPolicyController) ensureTopLevelChains() {
     		rulePosition[family]++
     	}
     
    -	for idx, externalIPRange := range npc.serviceExternalIPRanges {
    -		var family v1core.IPFamily
    -		if externalIPRange.IP.To4() != nil {
    -			family = v1core.IPv4Protocol
    -		} else {
    -			family = v1core.IPv6Protocol
    -		}
    -		whitelistServiceVips := []string{"-m", "comment", "--comment",
    -			"allow traffic to external IP range: " + externalIPRange.String(), "-d", externalIPRange.String(),
    -			"-j", "RETURN"}
    -		uuid, err := addUUIDForRuleSpec(kubeInputChainName, &whitelistServiceVips)
    -		if err != nil {
    -			klog.Fatalf("Failed to get uuid for rule: %s", err.Error())
    +	for _, family := range []v1core.IPFamily{v1core.IPv4Protocol, v1core.IPv6Protocol} {
    +		handler := npc.iptablesCmdHandlers[family]
    +		if handler == nil {
    +			continue
     		}
    -		// Access externalIPRange via index to avoid implicit memory aliasing
    -		cidrHandler, err := npc.iptablesCmdHandlerForCIDR(&npc.serviceExternalIPRanges[idx])
    -		if err != nil {
    -			klog.Fatalf("Failed to get iptables handler: %s", err.Error())
    +		for _, externalIPRange := range npc.ipRanges.ExternalIPRanges(family) {
    +			whitelistServiceVips := []string{"-m", "comment", "--comment",
    +				"allow traffic to external IP range: " + externalIPRange.String(),
    +				"-d", externalIPRange.String(), "-j", "RETURN"}
    +			uuid, err := addUUIDForRuleSpec(kubeInputChainName, &whitelistServiceVips)
    +			if err != nil {
    +				klog.Fatalf("Failed to get uuid for rule: %s", err.Error())
    +			}
    +			klog.V(2).Infof("Allow traffic to ingress towards External IP Range: %s for family: %s",
    +				externalIPRange.String(), family)
    +			ensureRuleAtPosition(handler,
    +				kubeInputChainName, whitelistServiceVips, uuid, rulePosition[family])
    +			rulePosition[family]++
     		}
    -		klog.V(2).Infof("Allow traffic to ingress towards External IP Range: %s for family: %s",
    -			externalIPRange.String(), family)
    -		ensureRuleAtPosition(cidrHandler,
    -			kubeInputChainName, whitelistServiceVips, uuid, rulePosition[family])
    -		rulePosition[family]++
    -	}
     
    -	for idx, loadBalancerIPRange := range npc.serviceLoadBalancerIPRanges {
    -		var family v1core.IPFamily
    -		if loadBalancerIPRange.IP.To4() != nil {
    -			family = v1core.IPv4Protocol
    -		} else {
    -			family = v1core.IPv6Protocol
    -		}
    -		whitelistServiceVips := []string{"-m", "comment", "--comment",
    -			"allow traffic to load balancer IP range: " + loadBalancerIPRange.String(), "-d", loadBalancerIPRange.String(),
    -			"-j", "RETURN"}
    -		uuid, err := addUUIDForRuleSpec(kubeInputChainName, &whitelistServiceVips)
    -		if err != nil {
    -			klog.Fatalf("Failed to get uuid for rule: %s", err.Error())
    -		}
    -		// Access loadBalancerIPRange via index to avoid implicit memory aliasing
    -		cidrHandler, err := npc.iptablesCmdHandlerForCIDR(&npc.serviceLoadBalancerIPRanges[idx])
    -		if err != nil {
    -			klog.Fatalf("Failed to get iptables handler: %s", err.Error())
    +		for _, loadBalancerIPRange := range npc.ipRanges.LoadBalancerIPRanges(family) {
    +			whitelistServiceVips := []string{"-m", "comment", "--comment",
    +				"allow traffic to load balancer IP range: " + loadBalancerIPRange.String(),
    +				"-d", loadBalancerIPRange.String(), "-j", "RETURN"}
    +			uuid, err := addUUIDForRuleSpec(kubeInputChainName, &whitelistServiceVips)
    +			if err != nil {
    +				klog.Fatalf("Failed to get uuid for rule: %s", err.Error())
    +			}
    +			klog.V(2).Infof("Allow traffic to ingress towards Load Balancer IP Range: %s "+
    +				"for family: %s", loadBalancerIPRange.String(), family)
    +			ensureRuleAtPosition(handler,
    +				kubeInputChainName, whitelistServiceVips, uuid, rulePosition[family])
    +			rulePosition[family]++
     		}
    -		klog.V(2).Infof("Allow traffic to ingress towards Load Balancer IP Range: %s for family: %s",
    -			loadBalancerIPRange.String(), family)
    -		ensureRuleAtPosition(cidrHandler,
    -			kubeInputChainName, whitelistServiceVips, uuid, rulePosition[family])
    -		rulePosition[family]++
     	}
     }
     
    @@ -905,90 +884,44 @@ func NewNetworkPolicyController(clientset kubernetes.Interface,
     	npInformer cache.SharedIndexInformer, nsInformer cache.SharedIndexInformer,
     	ipsetMutex *sync.Mutex, linkQ utils.LocalLinkQuerier,
     	iptablesCmdHandlers map[v1core.IPFamily]utils.IPTablesHandler,
    -	ipSetHandlers map[v1core.IPFamily]utils.IPSetHandler) (*NetworkPolicyController, error) {
    -	npc := NetworkPolicyController{ipsetMutex: ipsetMutex}
    +	ipSetHandlers map[v1core.IPFamily]utils.IPSetHandler,
    +	ipRanges svcip.RangeQuerier) (*NetworkPolicyController, error) {
    +	npc := NetworkPolicyController{ipsetMutex: ipsetMutex, ipRanges: ipRanges}
     
     	// Creating a single-item buffered channel to ensure that we only keep a single full sync request at a time,
     	// additional requests would be pointless to queue since after the first one was processed the system would already
     	// be up to date with all of the policy changes from any enqueued request after that
     	npc.fullSyncRequestChan = make(chan struct{}, 1)
     
    -	// Validate and parse ClusterIP service range
    -	if len(config.ClusterIPCIDRs) == 0 {
    -		return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter, the list is empty")
    +	// Validate dual-stack ClusterIP configuration using pre-parsed ranges from the validator
    +	clusterIPv4 := ipRanges.ClusterIPRanges(v1core.IPv4Protocol)
    +	clusterIPv6 := ipRanges.ClusterIPRanges(v1core.IPv6Protocol)
    +	if config.EnableIPv4 && !config.EnableIPv6 && len(clusterIPv4) == 0 {
    +		return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: " +
    +			"IPv4 is enabled but only IPv6 address is provided")
     	}
    -
    -	_, primaryIpnet, err := net.ParseCIDR(strings.TrimSpace(config.ClusterIPCIDRs[0]))
    -	if err != nil {
    -		return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: %w", err)
    +	if !config.EnableIPv4 && config.EnableIPv6 && len(clusterIPv6) == 0 {
    +		return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: " +
    +			"IPv6 is enabled but only IPv4 address is provided")
     	}
    -	npc.serviceClusterIPRanges = append(npc.serviceClusterIPRanges, *primaryIpnet)
    -
    -	// Validate that ClusterIP service range type matches the configuration
    -	if config.EnableIPv4 && !config.EnableIPv6 {
    -		if !netutils.IsIPv4CIDR(&npc.serviceClusterIPRanges[0]) {
    +	if config.EnableIPv4 && config.EnableIPv6 && len(config.ClusterIPCIDRs) > 1 {
    +		if len(clusterIPv4) == 0 || len(clusterIPv6) == 0 {
     			return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: " +
    -				"IPv4 is enabled but only IPv6 address is provided")
    +				"dual-stack is enabled, both IPv4 and IPv6 addresses should be provided")
     		}
     	}
    -	if !config.EnableIPv4 && config.EnableIPv6 {
    -		if !netutils.IsIPv6CIDR(&npc.serviceClusterIPRanges[0]) {
    -			return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: " +
    -				"IPv6 is enabled but only IPv4 address is provided")
    -		}
    +	if !config.EnableIPv4 && !config.EnableIPv6 && len(config.ClusterIPCIDRs) > 1 {
    +		return nil, fmt.Errorf("too many CIDRs provided in --service-cluster-ip-range parameter: " +
    +			"dual-stack must be enabled to provide two addresses")
     	}
     
    -	if len(config.ClusterIPCIDRs) > 1 {
    -		if config.EnableIPv4 && config.EnableIPv6 {
    -			_, secondaryIpnet, err := net.ParseCIDR(strings.TrimSpace(config.ClusterIPCIDRs[1]))
    -			if err != nil {
    -				return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: %v", err)
    -			}
    -			npc.serviceClusterIPRanges = append(npc.serviceClusterIPRanges, *secondaryIpnet)
    -
    -			ipv4Provided := netutils.IsIPv4CIDR(&npc.serviceClusterIPRanges[0]) ||
    -				netutils.IsIPv4CIDR(&npc.serviceClusterIPRanges[1])
    -			ipv6Provided := netutils.IsIPv6CIDR(&npc.serviceClusterIPRanges[0]) ||
    -				netutils.IsIPv6CIDR(&npc.serviceClusterIPRanges[1])
    -			if !ipv4Provided || !ipv6Provided {
    -				return nil, fmt.Errorf("failed to get parse --service-cluster-ip-range parameter: " +
    -					"dual-stack is enabled, both IPv4 and IPv6 addresses should be provided")
    -			}
    -		} else {
    -			return nil, fmt.Errorf("too many CIDRs provided in --service-cluster-ip-range parameter: " +
    -				"dual-stack must be enabled to provide two addresses")
    -		}
    -	}
    -	if len(config.ClusterIPCIDRs) > 2 {
    -		return nil, fmt.Errorf("too many CIDRs provided in --service-cluster-ip-range parameter, only two " +
    -			"addresses are allowed at once for dual-stack")
    -	}
    +	var err error
     
     	// Validate and parse NodePort range
     	if npc.serviceNodePortRange, err = validateNodePortRange(config.NodePortRange); err != nil {
     		return nil, err
     	}
     
    -	// Validate and parse ExternalIP service range
    -	for _, externalIPRange := range config.ExternalIPCIDRs {
    -		_, ipnet, err := net.ParseCIDR(externalIPRange)
    -		if err != nil {
    -			return nil, fmt.Errorf("failed to get parse --service-external-ip-range parameter: '%s'. Error: %s",
    -				externalIPRange, err.Error())
    -		}
    -		npc.serviceExternalIPRanges = append(npc.serviceExternalIPRanges, *ipnet)
    -	}
    -
    -	// Validate and parse LoadBalancerIP service range
    -	for _, loadBalancerIPRange := range config.LoadBalancerCIDRs {
    -		_, ipnet, err := net.ParseCIDR(loadBalancerIPRange)
    -		if err != nil {
    -			return nil, fmt.Errorf("failed to get parse --loadbalancer-ip-range parameter: '%s'. Error: %s",
    -				loadBalancerIPRange, err.Error())
    -		}
    -		npc.serviceLoadBalancerIPRanges = append(npc.serviceLoadBalancerIPRanges, *ipnet)
    -	}
    -
     	if config.MetricsEnabled {
     		// Register the metrics for this controller
     		metrics.DefaultRegisterer.MustRegister(metrics.ControllerIptablesSyncTime)
    
  • pkg/controllers/netpol/network_policy_controller_test.go+35 16 modified
    @@ -26,6 +26,7 @@ import (
     	"k8s.io/client-go/kubernetes/fake"
     
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     )
     
    @@ -835,31 +836,31 @@ func TestNetworkPolicyController(t *testing.T) {
     			"Test bad cluster CIDR (not properly formatting ip address)",
     			newMinimalKubeRouterConfig([]string{"10.10.10"}, "", "node", nil, nil, false),
     			true,
    -			"failed to get parse --service-cluster-ip-range parameter: invalid CIDR address: 10.10.10",
    +			"failed to parse --service-cluster-ip-range parameter: '10.10.10': invalid CIDR address: 10.10.10",
     		},
     		{
     			"Test bad cluster CIDR (not using an ip address)",
     			newMinimalKubeRouterConfig([]string{"foo"}, "", "node", nil, nil, false),
     			true,
    -			"failed to get parse --service-cluster-ip-range parameter: invalid CIDR address: foo",
    +			"failed to parse --service-cluster-ip-range parameter: 'foo': invalid CIDR address: foo",
     		},
     		{
     			"Test bad cluster CIDR (using an ip address that is not a CIDR)",
     			newMinimalKubeRouterConfig([]string{"10.10.10.10"}, "", "node", nil, nil, false),
     			true,
    -			"failed to get parse --service-cluster-ip-range parameter: invalid CIDR address: 10.10.10.10",
    +			"failed to parse --service-cluster-ip-range parameter: '10.10.10.10': invalid CIDR address: 10.10.10.10",
     		},
     		{
     			"Test bad cluster CIDRs (using more than 2 ip addresses, including 2 ipv4)",
    -			newMinimalKubeRouterConfig([]string{"10.96.0.0/12", "10.244.0.0/16", "2001:db8:42:1::/112"}, "", "node", nil, nil, false),
    +			newMinimalKubeRouterConfig([]string{"10.96.0.0/12", "10.244.0.0/16", "2001:db8:42:1::/112"}, "", "node", nil, nil, true),
     			true,
    -			"too many CIDRs provided in --service-cluster-ip-range parameter: dual-stack must be enabled to provide two addresses",
    +			"too many CIDRs provided in --service-cluster-ip-range parameter, only two addresses are allowed at once for dual-stack",
     		},
     		{
     			"Test bad cluster CIDRs (using more than 2 ip addresses, including 2 ipv6)",
    -			newMinimalKubeRouterConfig([]string{"10.96.0.0/12", "2001:db8:42:0::/56", "2001:db8:42:1::/112"}, "", "node", nil, nil, false),
    +			newMinimalKubeRouterConfig([]string{"10.96.0.0/12", "2001:db8:42:0::/56", "2001:db8:42:1::/112"}, "", "node", nil, nil, true),
     			true,
    -			"too many CIDRs provided in --service-cluster-ip-range parameter: dual-stack must be enabled to provide two addresses",
    +			"too many CIDRs provided in --service-cluster-ip-range parameter, only two addresses are allowed at once for dual-stack",
     		},
     		{
     			"Test good cluster CIDR (using single IP with a /32)",
    @@ -931,25 +932,25 @@ func TestNetworkPolicyController(t *testing.T) {
     			"Test bad external IP CIDR (not properly formatting ip address)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", []string{"199.10.10"}, nil, false),
     			true,
    -			"failed to get parse --service-external-ip-range parameter: '199.10.10'. Error: invalid CIDR address: 199.10.10",
    +			"failed to parse --service-external-ip-range parameter: '199.10.10': invalid CIDR address: 199.10.10",
     		},
     		{
     			"Test bad external IP CIDR (not using an ip address)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", []string{"foo"}, nil, false),
     			true,
    -			"failed to get parse --service-external-ip-range parameter: 'foo'. Error: invalid CIDR address: foo",
    +			"failed to parse --service-external-ip-range parameter: 'foo': invalid CIDR address: foo",
     		},
     		{
     			"Test bad external IP CIDR (using an ip address that is not a CIDR)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", []string{"199.10.10.10"}, nil, false),
     			true,
    -			"failed to get parse --service-external-ip-range parameter: '199.10.10.10'. Error: invalid CIDR address: 199.10.10.10",
    +			"failed to parse --service-external-ip-range parameter: '199.10.10.10': invalid CIDR address: 199.10.10.10",
     		},
     		{
     			"Test bad external IP CIDR (making sure that it processes all items in the list)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", []string{"199.10.10.10/32", "199.10.10.11"}, nil, false),
     			true,
    -			"failed to get parse --service-external-ip-range parameter: '199.10.10.11'. Error: invalid CIDR address: 199.10.10.11",
    +			"failed to parse --service-external-ip-range parameter: '199.10.10.11': invalid CIDR address: 199.10.10.11",
     		},
     		{
     			"Test good external IP CIDR (using single IP with a /32)",
    @@ -967,25 +968,25 @@ func TestNetworkPolicyController(t *testing.T) {
     			"Test bad load balancer CIDR (not properly formatting ip address)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", nil, []string{"199.10.10"}, false),
     			true,
    -			"failed to get parse --loadbalancer-ip-range parameter: '199.10.10'. Error: invalid CIDR address: 199.10.10",
    +			"failed to parse --loadbalancer-ip-range parameter: '199.10.10': invalid CIDR address: 199.10.10",
     		},
     		{
     			"Test bad load balancer CIDR (not using an ip address)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", nil, []string{"foo"}, false),
     			true,
    -			"failed to get parse --loadbalancer-ip-range parameter: 'foo'. Error: invalid CIDR address: foo",
    +			"failed to parse --loadbalancer-ip-range parameter: 'foo': invalid CIDR address: foo",
     		},
     		{
     			"Test bad load balancer CIDR (using an ip address that is not a CIDR)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", nil, []string{"199.10.10.10"}, false),
     			true,
    -			"failed to get parse --loadbalancer-ip-range parameter: '199.10.10.10'. Error: invalid CIDR address: 199.10.10.10",
    +			"failed to parse --loadbalancer-ip-range parameter: '199.10.10.10': invalid CIDR address: 199.10.10.10",
     		},
     		{
     			"Test bad load balancer CIDR (making sure that it processes all items in the list)",
     			newMinimalKubeRouterConfig([]string{""}, "", "node", nil, []string{"199.10.10.10/32", "199.10.10.11"}, false),
     			true,
    -			"failed to get parse --loadbalancer-ip-range parameter: '199.10.10.11'. Error: invalid CIDR address: 199.10.10.11",
    +			"failed to parse --loadbalancer-ip-range parameter: '199.10.10.11': invalid CIDR address: 199.10.10.11",
     		},
     		{
     			"Test good load balancer CIDR (using single IP with a /32)",
    @@ -1006,13 +1007,31 @@ func TestNetworkPolicyController(t *testing.T) {
     	_, podInformer, nsInformer, netpolInformer := newFakeInformersFromClient(client)
     	for _, test := range testCases {
     		t.Run(test.name, func(t *testing.T) {
    +			// Create validator from config — CIDR parsing errors now come from the validator
    +			validator, validatorErr := svcip.NewValidator(svcip.Config{
    +				ExternalIPCIDRs:   test.config.ExternalIPCIDRs,
    +				LoadBalancerCIDRs: test.config.LoadBalancerCIDRs,
    +				ClusterIPCIDRs:    test.config.ClusterIPCIDRs,
    +				StrictValidation:  test.config.StrictExternalIPValidation,
    +				EnableIPv4:        test.config.EnableIPv4,
    +				EnableIPv6:        test.config.EnableIPv6,
    +			})
    +			if validatorErr != nil {
    +				if !test.expectError {
    +					t.Errorf("Validator creation should have succeeded, but failed: %s", validatorErr)
    +				} else if validatorErr.Error() != test.errorText {
    +					t.Errorf("Expected error: '%s' but instead got: '%s'", test.errorText, validatorErr)
    +				}
    +				return
    +			}
    +
     			// TODO: Handle IPv6
     			iptablesHandlers := make(map[v1.IPFamily]utils.IPTablesHandler, 1)
     			iptablesHandlers[v1.IPv4Protocol] = newFakeIPTables(iptables.ProtocolIPv4)
     			ipSetHandlers := make(map[v1.IPFamily]utils.IPSetHandler, 1)
     			ipSetHandlers[v1.IPv4Protocol] = &fakeIPSet{}
     			_, err := NewNetworkPolicyController(client, test.config, podInformer, netpolInformer, nsInformer,
    -				&sync.Mutex{}, fakeLinkQuerier, iptablesHandlers, ipSetHandlers)
    +				&sync.Mutex{}, fakeLinkQuerier, iptablesHandlers, ipSetHandlers, validator)
     			if err == nil && test.expectError {
     				t.Error("This config should have failed, but it was successful instead")
     			} else if err != nil {
    
  • pkg/controllers/proxy/linux_networking_moq.go+3 2 modified
    @@ -4,11 +4,12 @@
     package proxy
     
     import (
    +	"net"
    +	"sync"
    +
     	"github.com/moby/ipvs"
     	"github.com/vishvananda/netlink"
     	"github.com/vishvananda/netns"
    -	"net"
    -	"sync"
     )
     
     // Ensure, that LinuxNetworkingMock does implement LinuxNetworking.
    
  • pkg/controllers/proxy/network_services_controller.go+13 1 modified
    @@ -20,6 +20,7 @@ import (
     	"github.com/cloudnativelabs/kube-router/v2/pkg/healthcheck"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/metrics"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	"github.com/coreos/go-iptables/iptables"
     	"github.com/moby/ipvs"
    @@ -128,6 +129,7 @@ type NetworkServicesController struct {
     	endpointsMap        endpointSliceInfoMap
     	podCidr             string
     	excludedCidrs       []net.IPNet
    +	ipFilter            svcip.Filter
     	masqueradeAll       bool
     	globalHairpin       bool
     	ipvsPermitAll       bool
    @@ -907,12 +909,20 @@ func (nsc *NetworkServicesController) buildServicesInfo() serviceInfoMap {
     			}
     
     			copy(svcInfo.externalIPs, svc.Spec.ExternalIPs)
    +			if nsc.ipFilter != nil {
    +				svcInfo.externalIPs = nsc.ipFilter.FilterExternalIPs(svcInfo.externalIPs, svc.Name,
    +					svc.Namespace)
    +			}
     			copy(svcInfo.clusterIPs, svc.Spec.ClusterIPs)
     			for _, lbIngress := range svc.Status.LoadBalancer.Ingress {
     				if len(lbIngress.IP) > 0 {
     					svcInfo.loadBalancerIPs = append(svcInfo.loadBalancerIPs, lbIngress.IP)
     				}
     			}
    +			if nsc.ipFilter != nil {
    +				svcInfo.loadBalancerIPs = nsc.ipFilter.FilterLoadBalancerIPs(svcInfo.loadBalancerIPs,
    +					svc.Name, svc.Namespace)
    +			}
     			svcInfo.sessionAffinity = svc.Spec.SessionAffinity == v1.ServiceAffinityClientIP
     
     			if svcInfo.sessionAffinity {
    @@ -2002,7 +2012,7 @@ func (nsc *NetworkServicesController) setupHandlers(node *v1.Node) error {
     func NewNetworkServicesController(clientset kubernetes.Interface,
     	config *options.KubeRouterConfig, svcInformer cache.SharedIndexInformer,
     	epSliceInformer cache.SharedIndexInformer, podInformer cache.SharedIndexInformer,
    -	ipsetMutex *sync.Mutex) (*NetworkServicesController, error) {
    +	ipsetMutex *sync.Mutex, ipFilter svcip.Filter) (*NetworkServicesController, error) {
     
     	var err error
     	ln, err := newLinuxNetworking(config.ServiceTCPTimeout, config.ServiceTCPFinTimeout, config.ServiceUDPTimeout)
    @@ -2069,6 +2079,8 @@ func NewNetworkServicesController(clientset kubernetes.Interface,
     		nsc.excludedCidrs[i] = *ipnet
     	}
     
    +	nsc.ipFilter = ipFilter
    +
     	node, err := utils.GetNodeObject(clientset, config.HostnameOverride)
     	if err != nil {
     		return nil, err
    
  • pkg/controllers/proxy/network_services_controller_test.go+183 0 modified
    @@ -9,6 +9,7 @@ import (
     	"time"
     
     	"github.com/cloudnativelabs/kube-router/v2/internal/testutils"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	"github.com/moby/ipvs"
     	"github.com/stretchr/testify/assert"
    @@ -1905,3 +1906,185 @@ func TestDSR_FWMarkCollisionCase_Issue1045(t *testing.T) {
     		t.SkipNow()
     	}
     }
    +
    +// =============================================================================
    +// ExternalIP / LoadBalancerIP Validation Integration Tests
    +//
    +// Unit tests for FilterExternalIPs and FilterLoadBalancerIPs are in pkg/svcip/validator_test.go.
    +// These integration tests verify that buildServicesInfo correctly uses the svcip.Filter to
    +// filter IPs when building the service map.
    +// =============================================================================
    +
    +func TestBuildServicesInfoWithStrictValidation(t *testing.T) {
    +	intTrafficPolicyCluster := v1core.ServiceInternalTrafficPolicyCluster
    +	extTrafficPolicyCluster := v1core.ServiceExternalTrafficPolicyCluster
    +
    +	tests := []struct {
    +		name                string
    +		strictMode          bool
    +		externalIPRanges    []string
    +		lbIPRanges          []string
    +		clusterIPRanges     []string
    +		service             *v1core.Service
    +		expectedExternalIPs []string
    +		expectedLBIPs       []string
    +	}{
    +		{
    +			name:             "strict mode filters out-of-range externalIPs",
    +			strictMode:       true,
    +			externalIPRanges: []string{"10.243.0.0/24"},
    +			lbIPRanges:       []string{"10.255.0.0/24"},
    +			service: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-strict", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:                  "ClusterIP",
    +					ClusterIP:             "10.0.0.1",
    +					ExternalIPs:           []string{"10.243.0.1", "192.168.100.50"},
    +					InternalTrafficPolicy: &intTrafficPolicyCluster,
    +					ExternalTrafficPolicy: extTrafficPolicyCluster,
    +					Ports: []v1core.ServicePort{
    +						{Name: "http", Port: 80, Protocol: "TCP"},
    +					},
    +				},
    +			},
    +			expectedExternalIPs: []string{"10.243.0.1"},
    +			expectedLBIPs:       []string{},
    +		},
    +		{
    +			name:             "strict mode filters out-of-range loadBalancerIPs",
    +			strictMode:       true,
    +			externalIPRanges: []string{"10.243.0.0/24"},
    +			lbIPRanges:       []string{"10.255.0.0/24"},
    +			service: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-strict-lb", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:                  "LoadBalancer",
    +					ClusterIP:             "10.0.0.2",
    +					InternalTrafficPolicy: &intTrafficPolicyCluster,
    +					ExternalTrafficPolicy: extTrafficPolicyCluster,
    +					Ports: []v1core.ServicePort{
    +						{Name: "http", Port: 80, Protocol: "TCP"},
    +					},
    +				},
    +				Status: v1core.ServiceStatus{
    +					LoadBalancer: v1core.LoadBalancerStatus{
    +						Ingress: []v1core.LoadBalancerIngress{
    +							{IP: "10.255.0.1"},
    +							{IP: "8.8.8.8"},
    +						},
    +					},
    +				},
    +			},
    +			expectedExternalIPs: []string{},
    +			expectedLBIPs:       []string{"10.255.0.1"},
    +		},
    +		{
    +			name:             "strict mode rejects externalIP that conflicts with clusterIP range",
    +			strictMode:       true,
    +			externalIPRanges: []string{"10.0.0.0/8"},
    +			clusterIPRanges:  []string{"10.96.0.0/12"},
    +			service: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-conflict", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:                  "ClusterIP",
    +					ClusterIP:             "10.96.0.1",
    +					ExternalIPs:           []string{"10.96.0.10", "10.243.0.1"},
    +					InternalTrafficPolicy: &intTrafficPolicyCluster,
    +					ExternalTrafficPolicy: extTrafficPolicyCluster,
    +					Ports: []v1core.ServicePort{
    +						{Name: "http", Port: 80, Protocol: "TCP"},
    +					},
    +				},
    +			},
    +			expectedExternalIPs: []string{"10.243.0.1"},
    +			expectedLBIPs:       nil,
    +		},
    +		{
    +			name:       "strict mode off - all IPs pass through",
    +			strictMode: false,
    +			service: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-nostrict", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:                  "LoadBalancer",
    +					ClusterIP:             "10.0.0.3",
    +					ExternalIPs:           []string{"1.1.1.1", "2.2.2.2"},
    +					InternalTrafficPolicy: &intTrafficPolicyCluster,
    +					ExternalTrafficPolicy: extTrafficPolicyCluster,
    +					Ports: []v1core.ServicePort{
    +						{Name: "http", Port: 80, Protocol: "TCP"},
    +					},
    +				},
    +				Status: v1core.ServiceStatus{
    +					LoadBalancer: v1core.LoadBalancerStatus{
    +						Ingress: []v1core.LoadBalancerIngress{
    +							{IP: "10.255.0.1"},
    +						},
    +					},
    +				},
    +			},
    +			expectedExternalIPs: []string{"1.1.1.1", "2.2.2.2"},
    +			expectedLBIPs:       []string{"10.255.0.1"},
    +		},
    +	}
    +
    +	for _, tc := range tests {
    +		t.Run(tc.name, func(t *testing.T) {
    +			clientset := fake.NewSimpleClientset()
    +			_, err := clientset.CoreV1().Services("default").Create(
    +				context.Background(), tc.service, metav1.CreateOptions{})
    +			if err != nil {
    +				t.Fatalf("failed to create service: %v", err)
    +			}
    +
    +			// Build svcip.Config from test case data
    +			clusterIPCIDRs := tc.clusterIPRanges
    +			if len(clusterIPCIDRs) == 0 {
    +				clusterIPCIDRs = []string{"10.96.0.0/12"}
    +			}
    +			validator, validatorErr := svcip.NewValidator(svcip.Config{
    +				ExternalIPCIDRs:   tc.externalIPRanges,
    +				LoadBalancerCIDRs: tc.lbIPRanges,
    +				ClusterIPCIDRs:    clusterIPCIDRs,
    +				StrictValidation:  tc.strictMode,
    +				EnableIPv4:        true,
    +			})
    +			if validatorErr != nil {
    +				t.Fatalf("failed to create validator: %v", validatorErr)
    +			}
    +
    +			krNode := &utils.LocalKRNode{
    +				KRNode: utils.KRNode{
    +					NodeName:  "node-1",
    +					PrimaryIP: net.ParseIP("10.0.0.0"),
    +				},
    +			}
    +			nsc := &NetworkServicesController{
    +				krNode:     krNode,
    +				ipFilter:   validator,
    +				ipsetMutex: &sync.Mutex{},
    +				client:     clientset,
    +				fwMarkMap:  make(map[uint32]string),
    +			}
    +
    +			startInformersForServiceProxy(t, nsc, clientset)
    +			waitForListerWithTimeout(t, nsc.svcLister, time.Second*10)
    +
    +			serviceMap := nsc.buildServicesInfo()
    +
    +			// Find the service info for the first (and only) port
    +			var svcInfo *serviceInfo
    +			for _, si := range serviceMap {
    +				svcInfo = si
    +				break
    +			}
    +			if svcInfo == nil {
    +				t.Fatalf("expected service info to be present in the map")
    +			}
    +
    +			assert.Equal(t, tc.expectedExternalIPs, svcInfo.externalIPs,
    +				"externalIPs should be filtered correctly")
    +			assert.Equal(t, tc.expectedLBIPs, svcInfo.loadBalancerIPs,
    +				"loadBalancerIPs should be filtered correctly")
    +		})
    +	}
    +}
    
  • pkg/controllers/proxy/service_endpoints_sync.go+1 9 modified
    @@ -771,15 +771,7 @@ func (nsc *NetworkServicesController) cleanupStaleIPVSConfig(activeServiceEndpoi
     		// old: if !ok || len(endpointIDs) == 0 {
     		if !ok {
     			klog.V(3).Infof("didn't find key: %s in above map", key)
    -			excluded := false
    -			for _, excludedCidr := range nsc.excludedCidrs {
    -				if excludedCidr.Contains(ipvsSvc.Address) {
    -					excluded = true
    -					break
    -				}
    -			}
    -
    -			if excluded {
    +			if utils.IsIPInRanges(ipvsSvc.Address, nsc.excludedCidrs) {
     				klog.V(1).Infof("Ignoring deletion of an IPVS service %s in an excluded cidr",
     					ipvsServiceString(ipvsSvc))
     				continue
    
  • pkg/controllers/routing/ecmp_vip.go+6 0 modified
    @@ -329,6 +329,9 @@ func (nrc *NetworkRoutingController) getExternalIPs(svc *v1core.Service) []strin
     			externalIPList = append(externalIPList, svc.Spec.ExternalIPs...)
     		}
     	}
    +	if nrc.ipFilter != nil {
    +		externalIPList = nrc.ipFilter.FilterExternalIPs(externalIPList, svc.Name, svc.Namespace)
    +	}
     	return externalIPList
     }
     
    @@ -344,6 +347,9 @@ func (nrc *NetworkRoutingController) getLoadBalancerIPs(svc *v1core.Service) []s
     			}
     		}
     	}
    +	if nrc.ipFilter != nil {
    +		loadBalancerIPList = nrc.ipFilter.FilterLoadBalancerIPs(loadBalancerIPList, svc.Name, svc.Namespace)
    +	}
     	return loadBalancerIPList
     }
     
    
  • pkg/controllers/routing/ecmp_vip_test.go+217 0 modified
    @@ -6,6 +6,7 @@ import (
     	"testing"
     	"time"
     
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	v1core "k8s.io/api/core/v1"
     	discoveryv1 "k8s.io/api/discovery/v1"
    @@ -1071,3 +1072,219 @@ func getNoLocalAddressesEPs() *discoveryv1.EndpointSlice {
     		},
     	}
     }
    +
    +// newTestValidator creates a svcip.Validator for use in NRC filtering tests.
    +// The cluster CIDR is set to 10.0.0.0/24 (a narrow range) so that it doesn't overlap with
    +// the LB test IPs (10.0.255.x) used by getLoadBalancerSvc().
    +func newTestValidator(t *testing.T, externalCIDRs, lbCIDRs []string, strict bool) *svcip.Validator {
    +	t.Helper()
    +	v, err := svcip.NewValidator(svcip.Config{
    +		ExternalIPCIDRs:   externalCIDRs,
    +		LoadBalancerCIDRs: lbCIDRs,
    +		ClusterIPCIDRs:    []string{"10.0.0.0/24"},
    +		StrictValidation:  strict,
    +		EnableIPv4:        true,
    +		EnableIPv6:        true,
    +	})
    +	if err != nil {
    +		t.Fatalf("failed to create test validator: %v", err)
    +	}
    +	return v
    +}
    +
    +func Test_getExternalIPsWithFilter(t *testing.T) {
    +	tests := []struct {
    +		name    string
    +		nrc     *NetworkRoutingController
    +		svc     *v1core.Service
    +		wantIPs []string
    +	}{
    +		{
    +			name: "nil filter passes all IPs through",
    +			nrc:  &NetworkRoutingController{},
    +			svc:  getExternalSvc(),
    +			// ipFilter is nil, so no filtering occurs
    +			wantIPs: []string{"1.1.1.1"},
    +		},
    +		{
    +			name: "strict mode with matching range allows IPs",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, []string{"1.1.1.0/24"}, nil, true),
    +			},
    +			svc:     getExternalSvc(),
    +			wantIPs: []string{"1.1.1.1"},
    +		},
    +		{
    +			name: "strict mode with non-matching range rejects IPs",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, []string{"2.2.2.0/24"}, nil, true),
    +			},
    +			svc:     getExternalSvc(),
    +			wantIPs: []string{},
    +		},
    +		{
    +			name: "strict mode with no external ranges rejects all (default-deny)",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, nil, true),
    +			},
    +			svc:     getExternalSvc(),
    +			wantIPs: nil,
    +		},
    +		{
    +			name: "non-strict mode passes all IPs regardless of ranges",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, []string{"2.2.2.0/24"}, nil, false),
    +			},
    +			svc:     getExternalSvc(),
    +			wantIPs: []string{"1.1.1.1"},
    +		},
    +		{
    +			name: "strict mode rejects external IP in cluster range",
    +			nrc: &NetworkRoutingController{
    +				// External range covers the whole 10.0.0.0/8, but cluster range is 10.0.0.0/24
    +				// so 10.0.0.50 (within the cluster CIDR) should be rejected
    +				ipFilter: newTestValidator(t, []string{"10.0.0.0/8"}, nil, true),
    +			},
    +			svc: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-test", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:        ClusterIPST,
    +					ClusterIP:   "10.0.0.1",
    +					ExternalIPs: []string{"10.0.0.50"},
    +				},
    +			},
    +			// 10.0.0.50 is in cluster range 10.0.0.0/24, so rejected
    +			wantIPs: []string{},
    +		},
    +		{
    +			name: "cluster IP type with no external IPs returns empty",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, []string{"1.1.1.0/24"}, nil, true),
    +			},
    +			svc:     getClusterSvc(),
    +			wantIPs: []string{},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			got := tt.nrc.getExternalIPs(tt.svc)
    +			if !Equal(got, tt.wantIPs) {
    +				t.Errorf("getExternalIPs() = %v, want %v", got, tt.wantIPs)
    +			}
    +		})
    +	}
    +}
    +
    +func Test_getLoadBalancerIPsWithFilter(t *testing.T) {
    +	tests := []struct {
    +		name    string
    +		nrc     *NetworkRoutingController
    +		svc     *v1core.Service
    +		wantIPs []string
    +	}{
    +		{
    +			name: "nil filter passes all LB IPs through",
    +			nrc:  &NetworkRoutingController{},
    +			svc:  getLoadBalancerSvc(),
    +			// ipFilter is nil, so no filtering occurs
    +			wantIPs: []string{"10.0.255.1", "10.0.255.2"},
    +		},
    +		{
    +			name: "strict mode with matching range allows LB IPs",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, []string{"10.0.255.0/24"}, true),
    +			},
    +			svc:     getLoadBalancerSvc(),
    +			wantIPs: []string{"10.0.255.1", "10.0.255.2"},
    +		},
    +		{
    +			name: "strict mode with non-matching range rejects LB IPs",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, []string{"192.168.0.0/16"}, true),
    +			},
    +			svc:     getLoadBalancerSvc(),
    +			wantIPs: []string{},
    +		},
    +		{
    +			name: "strict mode with no LB ranges rejects all (default-deny)",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, nil, true),
    +			},
    +			svc:     getLoadBalancerSvc(),
    +			wantIPs: nil,
    +		},
    +		{
    +			name: "non-strict mode passes all LB IPs regardless of ranges",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, []string{"192.168.0.0/16"}, false),
    +			},
    +			svc:     getLoadBalancerSvc(),
    +			wantIPs: []string{"10.0.255.1", "10.0.255.2"},
    +		},
    +		{
    +			name: "non-LoadBalancer service type returns empty",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, []string{"10.0.255.0/24"}, true),
    +			},
    +			svc:     getExternalSvc(),
    +			wantIPs: []string{},
    +		},
    +		{
    +			name: "strict mode rejects LB IP in cluster range",
    +			nrc: &NetworkRoutingController{
    +				ipFilter: newTestValidator(t, nil, []string{"10.0.0.0/8"}, true),
    +			},
    +			svc: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-lb-test", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:      LoadBalancerST,
    +					ClusterIP: "10.0.0.1",
    +				},
    +				Status: v1core.ServiceStatus{
    +					LoadBalancer: v1core.LoadBalancerStatus{
    +						Ingress: []v1core.LoadBalancerIngress{
    +							{IP: "10.0.0.50"},
    +						},
    +					},
    +				},
    +			},
    +			// 10.0.0.50 is in cluster range 10.0.0.0/24, so rejected
    +			wantIPs: []string{},
    +		},
    +		{
    +			name: "strict mode partial filter - some IPs match some do not",
    +			nrc: &NetworkRoutingController{
    +				// LB range is a narrow /30, only covers 192.168.1.0-192.168.1.3
    +				ipFilter: newTestValidator(t, nil, []string{"192.168.1.0/30"}, true),
    +			},
    +			svc: &v1core.Service{
    +				ObjectMeta: metav1.ObjectMeta{Name: "svc-lb-partial", Namespace: "default"},
    +				Spec: v1core.ServiceSpec{
    +					Type:      LoadBalancerST,
    +					ClusterIP: "10.0.0.1",
    +				},
    +				Status: v1core.ServiceStatus{
    +					LoadBalancer: v1core.LoadBalancerStatus{
    +						Ingress: []v1core.LoadBalancerIngress{
    +							{IP: "192.168.1.1"}, // in /30 (192.168.1.0-192.168.1.3)
    +							{IP: "192.168.1.5"}, // outside /30
    +						},
    +					},
    +				},
    +			},
    +			wantIPs: []string{"192.168.1.1"},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			got := tt.nrc.getLoadBalancerIPs(tt.svc)
    +			if !Equal(got, tt.wantIPs) {
    +				t.Errorf("getLoadBalancerIPs() = %v, want %v", got, tt.wantIPs)
    +			}
    +		})
    +	}
    +}
    
  • pkg/controllers/routing/network_routes_controller.go+4 1 modified
    @@ -21,6 +21,7 @@ import (
     	"github.com/cloudnativelabs/kube-router/v2/pkg/metrics"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/options"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/routes"
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/svcip"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/tunnels"
     	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
     	"github.com/coreos/go-iptables/iptables"
    @@ -157,6 +158,7 @@ type NetworkRoutingController struct {
     	routeSyncer                    RouteSyncer
     	pbr                            PolicyBasedRouter
     	tunneler                       tunnels.Tunneler
    +	ipFilter                       svcip.Filter
     
     	nodeLister    cache.Indexer
     	svcLister     cache.Indexer
    @@ -1239,10 +1241,11 @@ func NewNetworkRoutingController(clientset kubernetes.Interface,
     	kubeRouterConfig *options.KubeRouterConfig,
     	nodeInformer cache.SharedIndexInformer, svcInformer cache.SharedIndexInformer,
     	epSliceInformer cache.SharedIndexInformer, ipsetMutex *sync.Mutex,
    +	ipFilter svcip.Filter,
     ) (*NetworkRoutingController, error) {
     	var err error
     
    -	nrc := NetworkRoutingController{ipsetMutex: ipsetMutex}
    +	nrc := NetworkRoutingController{ipsetMutex: ipsetMutex, ipFilter: ipFilter}
     	if kubeRouterConfig.MetricsEnabled {
     		// Register the metrics for this controller
     		metrics.DefaultRegisterer.MustRegister(metrics.ControllerBGPadvertisementsReceived)
    
  • pkg/options/options.go+12 3 modified
    @@ -85,6 +85,7 @@ type KubeRouterConfig struct {
     	ServiceTCPTimeout              time.Duration
     	ServiceTCPFinTimeout           time.Duration
     	ServiceUDPTimeout              time.Duration
    +	StrictExternalIPValidation     bool
     	Version                        bool
     	VLevel                         string
     	// FullMeshPassword    string
    @@ -192,7 +193,9 @@ func (s *KubeRouterConfig) AddFlags(fs *pflag.FlagSet) {
     	fs.BoolVar(&s.LoadBalancerDefaultClass, "loadbalancer-default-class", true,
     		"Handle loadbalancer services without a class")
     	fs.StringSliceVar(&s.LoadBalancerCIDRs, "loadbalancer-ip-range", s.LoadBalancerCIDRs,
    -		"CIDR values from which loadbalancer services addresses are assigned (can be specified multiple times)")
    +		"CIDR values from which loadbalancer services addresses are assigned (can be specified multiple "+
    +			"times). Also used by the proxy module to validate loadBalancerIPs when "+
    +			"--strict-external-ip-validation is enabled.")
     	fs.DurationVar(&s.LoadBalancerSyncPeriod, "loadbalancer-sync-period", s.LoadBalancerSyncPeriod,
     		"The delay between checking for missed services (e.g. '5s', '1m'). Must be greater than 0.")
     	fs.BoolVar(&s.MasqueradeAll, "masquerade-all", false,
    @@ -251,9 +254,15 @@ func (s *KubeRouterConfig) AddFlags(fs *pflag.FlagSet) {
     			"containerd.")
     	fs.StringSliceVar(&s.ClusterIPCIDRs, "service-cluster-ip-range", s.ClusterIPCIDRs,
     		"CIDR values from which service cluster IPs are assigned (can be specified up to 2 times)")
    +	fs.BoolVar(&s.StrictExternalIPValidation, "strict-external-ip-validation", true,
    +		"When enabled, the proxy module validates externalIPs and loadBalancerIPs against configured CIDR "+
    +			"ranges (--service-external-ip-range and --loadbalancer-ip-range). When strict mode is enabled "+
    +			"and no range is configured, all externalIPs / loadBalancerIPs are rejected (default-deny). "+
    +			"Disable this flag to restore previous behavior of accepting all IPs without validation.")
     	fs.StringSliceVar(&s.ExternalIPCIDRs, "service-external-ip-range", s.ExternalIPCIDRs,
    -		"Specify external IP CIDRs that are used for inter-cluster communication "+
    -			"(can be specified multiple times)")
    +		"Specify external IP CIDRs that are used for inter-cluster communication (can be specified "+
    +			"multiple times). Also used by the proxy module to validate externalIPs when "+
    +			"--strict-external-ip-validation is enabled.")
     	fs.StringVar(&s.NodePortRange, "service-node-port-range", s.NodePortRange,
     		"NodePort range specified with either a hyphen or colon")
     	fs.DurationVar(&s.ServiceTCPTimeout, "service-tcp-timeout", s.ServiceTCPTimeout,
    
  • pkg/svcip/validator.go+278 0 added
    @@ -0,0 +1,278 @@
    +package svcip
    +
    +import (
    +	"fmt"
    +	"net"
    +
    +	v1core "k8s.io/api/core/v1"
    +	"k8s.io/klog/v2"
    +
    +	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
    +)
    +
    +// Config holds the raw CIDR strings and feature flags needed to construct a Validator.
    +type Config struct {
    +	ExternalIPCIDRs   []string
    +	LoadBalancerCIDRs []string
    +	ClusterIPCIDRs    []string
    +	StrictValidation  bool
    +	EnableIPv4        bool
    +	EnableIPv6        bool
    +}
    +
    +// RangeQuerier provides read access to parsed CIDR ranges, optionally filtered by IP family.
    +// Calling with no families returns all ranges. Calling with one or more families returns only
    +// ranges matching those families.
    +type RangeQuerier interface {
    +	ExternalIPRanges(families ...v1core.IPFamily) []net.IPNet
    +	LoadBalancerIPRanges(families ...v1core.IPFamily) []net.IPNet
    +	ClusterIPRanges(families ...v1core.IPFamily) []net.IPNet
    +}
    +
    +// Filter validates individual service IPs against configured CIDR ranges and strict mode settings.
    +type Filter interface {
    +	FilterExternalIPs(ips []string, svcName, svcNamespace string) []string
    +	FilterLoadBalancerIPs(ips []string, svcName, svcNamespace string) []string
    +}
    +
    +// Validator implements both RangeQuerier and Filter. It parses all CIDRs once at construction time,
    +// classifies them by IP family, and validates them against the enabled protocol configuration.
    +type Validator struct {
    +	externalIPv4Ranges     []net.IPNet
    +	externalIPv6Ranges     []net.IPNet
    +	loadBalancerIPv4Ranges []net.IPNet
    +	loadBalancerIPv6Ranges []net.IPNet
    +	clusterIPv4Ranges      []net.IPNet
    +	clusterIPv6Ranges      []net.IPNet
    +	strictValidation       bool
    +}
    +
    +const maxClusterIPCIDRs = 2
    +
    +// NewValidator parses all CIDR strings, classifies them by IP family, and validates that
    +// each CIDR's family matches the enabled protocol configuration. It returns an error if any
    +// CIDR string is invalid, if a CIDR's family conflicts with the enabled protocols, or if
    +// ClusterIP CIDR constraints are violated (must be non-empty, max 2).
    +func NewValidator(cfg Config) (*Validator, error) {
    +	v := &Validator{
    +		strictValidation: cfg.StrictValidation,
    +	}
    +
    +	var err error
    +
    +	v.externalIPv4Ranges, v.externalIPv6Ranges, err = parseCIDRsByFamily(
    +		cfg.ExternalIPCIDRs, cfg.EnableIPv4, cfg.EnableIPv6, "--service-external-ip-range")
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	v.loadBalancerIPv4Ranges, v.loadBalancerIPv6Ranges, err = parseCIDRsByFamily(
    +		cfg.LoadBalancerCIDRs, cfg.EnableIPv4, cfg.EnableIPv6, "--loadbalancer-ip-range")
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	v.clusterIPv4Ranges, v.clusterIPv6Ranges, err = parseCIDRsByFamily(
    +		cfg.ClusterIPCIDRs, cfg.EnableIPv4, cfg.EnableIPv6, "--service-cluster-ip-range")
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	if len(cfg.ClusterIPCIDRs) == 0 {
    +		return nil, fmt.Errorf("failed to parse %s parameter: the list is empty",
    +			"--service-cluster-ip-range")
    +	}
    +	if len(cfg.ClusterIPCIDRs) > maxClusterIPCIDRs {
    +		return nil, fmt.Errorf("too many CIDRs provided in %s parameter, only two "+
    +			"addresses are allowed at once for dual-stack", "--service-cluster-ip-range")
    +	}
    +
    +	return v, nil
    +}
    +
    +// parseCIDRsByFamily parses a list of CIDR strings and classifies them into IPv4 and IPv6 buckets.
    +// It returns an error if any CIDR is invalid or if a CIDR's family conflicts with the enabled
    +// protocol configuration.
    +func parseCIDRsByFamily(cidrs []string, enableIPv4, enableIPv6 bool,
    +	flagName string) (ipv4, ipv6 []net.IPNet, err error) {
    +
    +	for _, cidrStr := range cidrs {
    +		ip, ipnet, parseErr := net.ParseCIDR(cidrStr)
    +		if parseErr != nil {
    +			return nil, nil, fmt.Errorf("failed to parse %s parameter: '%s': %v",
    +				flagName, cidrStr, parseErr)
    +		}
    +
    +		if ip.To4() != nil {
    +			if !enableIPv4 {
    +				return nil, nil, fmt.Errorf("IPv4 CIDR %s specified in %s while IPv4 is disabled",
    +					cidrStr, flagName)
    +			}
    +			ipv4 = append(ipv4, *ipnet)
    +		} else {
    +			if !enableIPv6 {
    +				return nil, nil, fmt.Errorf("IPv6 CIDR %s specified in %s while IPv6 is disabled",
    +					cidrStr, flagName)
    +			}
    +			ipv6 = append(ipv6, *ipnet)
    +		}
    +	}
    +
    +	return ipv4, ipv6, nil
    +}
    +
    +// rangesForFamilies returns the combined ranges for the requested families. If no families are
    +// specified, all ranges are returned.
    +func rangesForFamilies(ipv4, ipv6 []net.IPNet, families []v1core.IPFamily) []net.IPNet {
    +	if len(families) == 0 {
    +		result := make([]net.IPNet, 0, len(ipv4)+len(ipv6))
    +		result = append(result, ipv4...)
    +		result = append(result, ipv6...)
    +		return result
    +	}
    +
    +	result := make([]net.IPNet, 0)
    +	for _, family := range families {
    +		switch family {
    +		case v1core.IPv4Protocol:
    +			result = append(result, ipv4...)
    +		case v1core.IPv6Protocol:
    +			result = append(result, ipv6...)
    +		case v1core.IPFamilyUnknown:
    +			// Unknown family — skip silently
    +		}
    +	}
    +	return result
    +}
    +
    +// ExternalIPRanges returns the parsed ExternalIP CIDR ranges, optionally filtered by IP family.
    +func (v *Validator) ExternalIPRanges(families ...v1core.IPFamily) []net.IPNet {
    +	return rangesForFamilies(v.externalIPv4Ranges, v.externalIPv6Ranges, families)
    +}
    +
    +// LoadBalancerIPRanges returns the parsed LoadBalancerIP CIDR ranges, optionally filtered by family.
    +func (v *Validator) LoadBalancerIPRanges(families ...v1core.IPFamily) []net.IPNet {
    +	return rangesForFamilies(v.loadBalancerIPv4Ranges, v.loadBalancerIPv6Ranges, families)
    +}
    +
    +// ClusterIPRanges returns the parsed ClusterIP CIDR ranges, optionally filtered by IP family.
    +func (v *Validator) ClusterIPRanges(families ...v1core.IPFamily) []net.IPNet {
    +	return rangesForFamilies(v.clusterIPv4Ranges, v.clusterIPv6Ranges, families)
    +}
    +
    +// LogStatus logs the current strict IP validation configuration at startup.
    +func (v *Validator) LogStatus() {
    +	if !v.strictValidation {
    +		klog.Infof("Strict external IP validation is disabled, all externalIPs and " +
    +			"loadBalancerIPs will be accepted")
    +		return
    +	}
    +
    +	klog.Infof("Strict external IP validation is enabled")
    +
    +	externalRanges := v.ExternalIPRanges()
    +	if len(externalRanges) == 0 {
    +		klog.Warningf("No --service-external-ip-range configured: all externalIPs will be " +
    +			"rejected in strict mode")
    +	} else {
    +		for _, cidr := range externalRanges {
    +			klog.Infof("Allowed externalIP range: %s", cidr.String())
    +		}
    +	}
    +
    +	lbRanges := v.LoadBalancerIPRanges()
    +	if len(lbRanges) == 0 {
    +		klog.Warningf("No --loadbalancer-ip-range configured: all loadBalancerIPs will be " +
    +			"rejected in strict mode")
    +	} else {
    +		for _, cidr := range lbRanges {
    +			klog.Infof("Allowed loadBalancerIP range: %s", cidr.String())
    +		}
    +	}
    +}
    +
    +// FilterExternalIPs validates externalIPs against configured CIDR ranges and ClusterIP ranges.
    +// When strict mode is enabled and no ranges are configured, all IPs are rejected (default-deny).
    +// When strict mode is disabled, all IPs pass through unfiltered.
    +func (v *Validator) FilterExternalIPs(ips []string, svcName, svcNamespace string) []string {
    +	if !v.strictValidation {
    +		return ips
    +	}
    +
    +	externalRanges := v.ExternalIPRanges()
    +	if len(externalRanges) == 0 {
    +		if len(ips) > 0 {
    +			klog.Warningf("Service %s/%s: rejecting all %d externalIPs because no "+
    +				"--service-external-ip-range is configured (strict mode default-deny)",
    +				svcNamespace, svcName, len(ips))
    +		}
    +		return nil
    +	}
    +
    +	clusterRanges := v.ClusterIPRanges()
    +	filtered := make([]string, 0, len(ips))
    +	for _, ipStr := range ips {
    +		ip := net.ParseIP(ipStr)
    +		if ip == nil {
    +			klog.Warningf("Service %s/%s: rejecting externalIP %q: not a valid IP address",
    +				svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		if utils.IsIPInRanges(ip, clusterRanges) {
    +			klog.Warningf("Service %s/%s: rejecting externalIP %s: conflicts with "+
    +				"cluster IP range", svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		if !utils.IsIPInRanges(ip, externalRanges) {
    +			klog.Warningf("Service %s/%s: rejecting externalIP %s: not within any "+
    +				"configured --service-external-ip-range",
    +				svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		filtered = append(filtered, ipStr)
    +	}
    +	return filtered
    +}
    +
    +// FilterLoadBalancerIPs validates loadBalancerIPs against configured CIDR ranges and ClusterIP
    +// ranges. When strict mode is enabled and no ranges are configured, all IPs are rejected
    +// (default-deny). When strict mode is disabled, all IPs pass through unfiltered.
    +func (v *Validator) FilterLoadBalancerIPs(ips []string, svcName, svcNamespace string) []string {
    +	if !v.strictValidation {
    +		return ips
    +	}
    +
    +	lbRanges := v.LoadBalancerIPRanges()
    +	if len(lbRanges) == 0 {
    +		if len(ips) > 0 {
    +			klog.Warningf("Service %s/%s: rejecting all %d loadBalancerIPs because no "+
    +				"--loadbalancer-ip-range is configured (strict mode default-deny)",
    +				svcNamespace, svcName, len(ips))
    +		}
    +		return nil
    +	}
    +
    +	clusterRanges := v.ClusterIPRanges()
    +	filtered := make([]string, 0, len(ips))
    +	for _, ipStr := range ips {
    +		ip := net.ParseIP(ipStr)
    +		if ip == nil {
    +			klog.Warningf("Service %s/%s: rejecting loadBalancerIP %q: not a valid "+
    +				"IP address", svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		if utils.IsIPInRanges(ip, clusterRanges) {
    +			klog.Warningf("Service %s/%s: rejecting loadBalancerIP %s: conflicts with "+
    +				"cluster IP range", svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		if !utils.IsIPInRanges(ip, lbRanges) {
    +			klog.Warningf("Service %s/%s: rejecting loadBalancerIP %s: not within any "+
    +				"configured --loadbalancer-ip-range",
    +				svcNamespace, svcName, ipStr)
    +			continue
    +		}
    +		filtered = append(filtered, ipStr)
    +	}
    +	return filtered
    +}
    
  • pkg/svcip/validator_test.go+482 0 added
    @@ -0,0 +1,482 @@
    +package svcip
    +
    +import (
    +	"net"
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	v1core "k8s.io/api/core/v1"
    +)
    +
    +func mustParseCIDR(t *testing.T, cidr string) net.IPNet {
    +	t.Helper()
    +	_, ipnet, err := net.ParseCIDR(cidr)
    +	require.NoError(t, err, "failed to parse CIDR %q", cidr)
    +	return *ipnet
    +}
    +
    +func TestNewValidator(t *testing.T) {
    +	t.Parallel()
    +
    +	tests := []struct {
    +		name      string
    +		cfg       Config
    +		wantErr   bool
    +		errSubstr string
    +	}{
    +		{
    +			name: "valid config with all ranges",
    +			cfg: Config{
    +				ExternalIPCIDRs:   []string{"10.243.0.0/24"},
    +				LoadBalancerCIDRs: []string{"10.255.0.0/24"},
    +				ClusterIPCIDRs:    []string{"10.96.0.0/12"},
    +				EnableIPv4:        true,
    +				EnableIPv6:        false,
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "valid dual-stack config",
    +			cfg: Config{
    +				ExternalIPCIDRs:   []string{"10.243.0.0/24", "fd00::/64"},
    +				LoadBalancerCIDRs: []string{"10.255.0.0/24", "fd01::/64"},
    +				ClusterIPCIDRs:    []string{"10.96.0.0/12", "fd02::/112"},
    +				EnableIPv4:        true,
    +				EnableIPv6:        true,
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "empty external and LB ranges are allowed",
    +			cfg: Config{
    +				ExternalIPCIDRs:   []string{},
    +				LoadBalancerCIDRs: []string{},
    +				ClusterIPCIDRs:    []string{"10.96.0.0/12"},
    +				EnableIPv4:        true,
    +				EnableIPv6:        false,
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "invalid external IP CIDR",
    +			cfg: Config{
    +				ExternalIPCIDRs: []string{"not-a-cidr"},
    +				ClusterIPCIDRs:  []string{"10.96.0.0/12"},
    +				EnableIPv4:      true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "--service-external-ip-range",
    +		},
    +		{
    +			name: "invalid loadbalancer CIDR",
    +			cfg: Config{
    +				LoadBalancerCIDRs: []string{"also-not-valid"},
    +				ClusterIPCIDRs:    []string{"10.96.0.0/12"},
    +				EnableIPv4:        true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "--loadbalancer-ip-range",
    +		},
    +		{
    +			name: "invalid cluster IP CIDR",
    +			cfg: Config{
    +				ClusterIPCIDRs: []string{"bad-cidr"},
    +				EnableIPv4:     true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "--service-cluster-ip-range",
    +		},
    +		{
    +			name: "empty cluster IP CIDRs",
    +			cfg: Config{
    +				ClusterIPCIDRs: []string{},
    +				EnableIPv4:     true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "the list is empty",
    +		},
    +		{
    +			name: "too many cluster IP CIDRs",
    +			cfg: Config{
    +				ClusterIPCIDRs: []string{"10.96.0.0/12", "10.97.0.0/12", "10.98.0.0/12"},
    +				EnableIPv4:     true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "only two",
    +		},
    +		{
    +			name: "IPv4 external CIDR when IPv4 disabled",
    +			cfg: Config{
    +				ExternalIPCIDRs: []string{"10.243.0.0/24"},
    +				ClusterIPCIDRs:  []string{"fd00::/112"},
    +				EnableIPv4:      false,
    +				EnableIPv6:      true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv4 CIDR",
    +		},
    +		{
    +			name: "IPv6 external CIDR when IPv6 disabled",
    +			cfg: Config{
    +				ExternalIPCIDRs: []string{"fd00::/64"},
    +				ClusterIPCIDRs:  []string{"10.96.0.0/12"},
    +				EnableIPv4:      true,
    +				EnableIPv6:      false,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv6 CIDR",
    +		},
    +		{
    +			name: "IPv4 LB CIDR when IPv4 disabled",
    +			cfg: Config{
    +				LoadBalancerCIDRs: []string{"10.255.0.0/24"},
    +				ClusterIPCIDRs:    []string{"fd00::/112"},
    +				EnableIPv4:        false,
    +				EnableIPv6:        true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv4 CIDR",
    +		},
    +		{
    +			name: "IPv6 LB CIDR when IPv6 disabled",
    +			cfg: Config{
    +				LoadBalancerCIDRs: []string{"fd01::/64"},
    +				ClusterIPCIDRs:    []string{"10.96.0.0/12"},
    +				EnableIPv4:        true,
    +				EnableIPv6:        false,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv6 CIDR",
    +		},
    +		{
    +			name: "IPv4 cluster CIDR when IPv4 disabled",
    +			cfg: Config{
    +				ClusterIPCIDRs: []string{"10.96.0.0/12"},
    +				EnableIPv4:     false,
    +				EnableIPv6:     true,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv4 CIDR",
    +		},
    +		{
    +			name: "IPv6 cluster CIDR when IPv6 disabled",
    +			cfg: Config{
    +				ClusterIPCIDRs: []string{"fd00::/112"},
    +				EnableIPv4:     true,
    +				EnableIPv6:     false,
    +			},
    +			wantErr:   true,
    +			errSubstr: "IPv6 CIDR",
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			v, err := NewValidator(tt.cfg)
    +			if tt.wantErr {
    +				require.Error(t, err)
    +				assert.Contains(t, err.Error(), tt.errSubstr)
    +				assert.Nil(t, v)
    +			} else {
    +				require.NoError(t, err)
    +				assert.NotNil(t, v)
    +			}
    +		})
    +	}
    +}
    +
    +func TestRangeQuerier(t *testing.T) {
    +	t.Parallel()
    +
    +	v, err := NewValidator(Config{
    +		ExternalIPCIDRs:   []string{"10.243.0.0/24", "fd00:1::/64"},
    +		LoadBalancerCIDRs: []string{"10.255.0.0/24", "fd00:2::/64"},
    +		ClusterIPCIDRs:    []string{"10.96.0.0/12", "fd00:3::/112"},
    +		EnableIPv4:        true,
    +		EnableIPv6:        true,
    +	})
    +	require.NoError(t, err)
    +
    +	t.Run("ExternalIPRanges no args returns all", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ExternalIPRanges()
    +		assert.Len(t, ranges, 2)
    +		assert.Equal(t, mustParseCIDR(t, "10.243.0.0/24"), ranges[0])
    +		assert.Equal(t, mustParseCIDR(t, "fd00:1::/64"), ranges[1])
    +	})
    +
    +	t.Run("ExternalIPRanges IPv4 only", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ExternalIPRanges(v1core.IPv4Protocol)
    +		assert.Len(t, ranges, 1)
    +		assert.Equal(t, mustParseCIDR(t, "10.243.0.0/24"), ranges[0])
    +	})
    +
    +	t.Run("ExternalIPRanges IPv6 only", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ExternalIPRanges(v1core.IPv6Protocol)
    +		assert.Len(t, ranges, 1)
    +		assert.Equal(t, mustParseCIDR(t, "fd00:1::/64"), ranges[0])
    +	})
    +
    +	t.Run("ExternalIPRanges both families explicit", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ExternalIPRanges(v1core.IPv4Protocol, v1core.IPv6Protocol)
    +		assert.Len(t, ranges, 2)
    +	})
    +
    +	t.Run("LoadBalancerIPRanges no args returns all", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.LoadBalancerIPRanges()
    +		assert.Len(t, ranges, 2)
    +		assert.Equal(t, mustParseCIDR(t, "10.255.0.0/24"), ranges[0])
    +		assert.Equal(t, mustParseCIDR(t, "fd00:2::/64"), ranges[1])
    +	})
    +
    +	t.Run("LoadBalancerIPRanges IPv4 only", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.LoadBalancerIPRanges(v1core.IPv4Protocol)
    +		assert.Len(t, ranges, 1)
    +		assert.Equal(t, mustParseCIDR(t, "10.255.0.0/24"), ranges[0])
    +	})
    +
    +	t.Run("ClusterIPRanges no args returns all", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ClusterIPRanges()
    +		assert.Len(t, ranges, 2)
    +		assert.Equal(t, mustParseCIDR(t, "10.96.0.0/12"), ranges[0])
    +		assert.Equal(t, mustParseCIDR(t, "fd00:3::/112"), ranges[1])
    +	})
    +
    +	t.Run("ClusterIPRanges IPv4 only", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ClusterIPRanges(v1core.IPv4Protocol)
    +		assert.Len(t, ranges, 1)
    +		assert.Equal(t, mustParseCIDR(t, "10.96.0.0/12"), ranges[0])
    +	})
    +
    +	t.Run("ClusterIPRanges IPv6 only", func(t *testing.T) {
    +		t.Parallel()
    +		ranges := v.ClusterIPRanges(v1core.IPv6Protocol)
    +		assert.Len(t, ranges, 1)
    +		assert.Equal(t, mustParseCIDR(t, "fd00:3::/112"), ranges[0])
    +	})
    +}
    +
    +func TestRangeQuerierEmpty(t *testing.T) {
    +	t.Parallel()
    +
    +	v, err := NewValidator(Config{
    +		ClusterIPCIDRs: []string{"10.96.0.0/12"},
    +		EnableIPv4:     true,
    +	})
    +	require.NoError(t, err)
    +
    +	assert.Empty(t, v.ExternalIPRanges())
    +	assert.Empty(t, v.ExternalIPRanges(v1core.IPv4Protocol))
    +	assert.Empty(t, v.ExternalIPRanges(v1core.IPv6Protocol))
    +	assert.Empty(t, v.LoadBalancerIPRanges())
    +	assert.Empty(t, v.LoadBalancerIPRanges(v1core.IPv4Protocol))
    +}
    +
    +func TestFilterExternalIPs(t *testing.T) {
    +	t.Parallel()
    +
    +	tests := []struct {
    +		name              string
    +		strictMode        bool
    +		externalIPRanges  []string
    +		clusterIPRanges   []string
    +		inputIPs          []string
    +		expectedOutputIPs []string
    +	}{
    +		{
    +			name:              "strict mode off - all IPs pass through",
    +			strictMode:        false,
    +			externalIPRanges:  []string{},
    +			inputIPs:          []string{"1.1.1.1", "2.2.2.2", "10.96.0.10"},
    +			expectedOutputIPs: []string{"1.1.1.1", "2.2.2.2", "10.96.0.10"},
    +		},
    +		{
    +			name:              "strict mode on, no ranges - all rejected (default-deny)",
    +			strictMode:        true,
    +			externalIPRanges:  []string{},
    +			inputIPs:          []string{"1.1.1.1", "2.2.2.2"},
    +			expectedOutputIPs: nil,
    +		},
    +		{
    +			name:              "strict mode on, no ranges, empty input - no error",
    +			strictMode:        true,
    +			externalIPRanges:  []string{},
    +			inputIPs:          []string{},
    +			expectedOutputIPs: nil,
    +		},
    +		{
    +			name:              "strict mode on, IP within range - accepted",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"10.243.0.0/24"},
    +			inputIPs:          []string{"10.243.0.1"},
    +			expectedOutputIPs: []string{"10.243.0.1"},
    +		},
    +		{
    +			name:              "strict mode on, IP outside range - rejected",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"10.243.0.0/24"},
    +			inputIPs:          []string{"192.168.1.1"},
    +			expectedOutputIPs: []string{},
    +		},
    +		{
    +			name:              "strict mode on, mixed valid and invalid IPs",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"10.243.0.0/24"},
    +			inputIPs:          []string{"10.243.0.1", "192.168.1.1", "10.243.0.2"},
    +			expectedOutputIPs: []string{"10.243.0.1", "10.243.0.2"},
    +		},
    +		{
    +			name:              "strict mode on, IP conflicts with cluster IP range",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"10.0.0.0/8"},
    +			clusterIPRanges:   []string{"10.96.0.0/12"},
    +			inputIPs:          []string{"10.96.0.10", "10.243.0.1"},
    +			expectedOutputIPs: []string{"10.243.0.1"},
    +		},
    +		{
    +			name:              "strict mode on, multiple ranges",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"10.243.0.0/24", "172.16.0.0/16"},
    +			inputIPs:          []string{"10.243.0.5", "172.16.1.1", "8.8.8.8"},
    +			expectedOutputIPs: []string{"10.243.0.5", "172.16.1.1"},
    +		},
    +		{
    +			name:              "strict mode on, IPv6 IPs with IPv6 ranges",
    +			strictMode:        true,
    +			externalIPRanges:  []string{"fd00::/64"},
    +			inputIPs:          []string{"fd00::1", "fe80::1"},
    +			expectedOutputIPs: []string{"fd00::1"},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			v := &Validator{
    +				strictValidation: tt.strictMode,
    +			}
    +
    +			for _, r := range tt.externalIPRanges {
    +				cidr := mustParseCIDR(t, r)
    +				if cidr.IP.To4() != nil {
    +					v.externalIPv4Ranges = append(v.externalIPv4Ranges, cidr)
    +				} else {
    +					v.externalIPv6Ranges = append(v.externalIPv6Ranges, cidr)
    +				}
    +			}
    +			for _, r := range tt.clusterIPRanges {
    +				cidr := mustParseCIDR(t, r)
    +				if cidr.IP.To4() != nil {
    +					v.clusterIPv4Ranges = append(v.clusterIPv4Ranges, cidr)
    +				} else {
    +					v.clusterIPv6Ranges = append(v.clusterIPv6Ranges, cidr)
    +				}
    +			}
    +
    +			result := v.FilterExternalIPs(tt.inputIPs, "test-svc", "default")
    +			assert.Equal(t, tt.expectedOutputIPs, result)
    +		})
    +	}
    +}
    +
    +func TestFilterLoadBalancerIPs(t *testing.T) {
    +	t.Parallel()
    +
    +	tests := []struct {
    +		name              string
    +		strictMode        bool
    +		lbIPRanges        []string
    +		clusterIPRanges   []string
    +		inputIPs          []string
    +		expectedOutputIPs []string
    +	}{
    +		{
    +			name:              "strict mode off - all IPs pass through",
    +			strictMode:        false,
    +			lbIPRanges:        []string{},
    +			inputIPs:          []string{"10.255.0.1", "10.255.0.2"},
    +			expectedOutputIPs: []string{"10.255.0.1", "10.255.0.2"},
    +		},
    +		{
    +			name:              "strict mode on, no ranges - all rejected (default-deny)",
    +			strictMode:        true,
    +			lbIPRanges:        []string{},
    +			inputIPs:          []string{"10.255.0.1"},
    +			expectedOutputIPs: nil,
    +		},
    +		{
    +			name:              "strict mode on, IP within range - accepted",
    +			strictMode:        true,
    +			lbIPRanges:        []string{"10.255.0.0/24"},
    +			inputIPs:          []string{"10.255.0.1"},
    +			expectedOutputIPs: []string{"10.255.0.1"},
    +		},
    +		{
    +			name:              "strict mode on, IP outside range - rejected",
    +			strictMode:        true,
    +			lbIPRanges:        []string{"10.255.0.0/24"},
    +			inputIPs:          []string{"192.168.1.1"},
    +			expectedOutputIPs: []string{},
    +		},
    +		{
    +			name:              "strict mode on, IP conflicts with cluster IP range",
    +			strictMode:        true,
    +			lbIPRanges:        []string{"10.0.0.0/8"},
    +			clusterIPRanges:   []string{"10.96.0.0/12"},
    +			inputIPs:          []string{"10.96.0.10", "10.255.0.1"},
    +			expectedOutputIPs: []string{"10.255.0.1"},
    +		},
    +		{
    +			name:              "strict mode on, empty input - no error",
    +			strictMode:        true,
    +			lbIPRanges:        []string{"10.255.0.0/24"},
    +			inputIPs:          []string{},
    +			expectedOutputIPs: []string{},
    +		},
    +		{
    +			name:              "strict mode on, mixed valid and invalid IPs",
    +			strictMode:        true,
    +			lbIPRanges:        []string{"10.255.0.0/24"},
    +			inputIPs:          []string{"10.255.0.1", "8.8.8.8", "10.255.0.2"},
    +			expectedOutputIPs: []string{"10.255.0.1", "10.255.0.2"},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			v := &Validator{
    +				strictValidation: tt.strictMode,
    +			}
    +
    +			for _, r := range tt.lbIPRanges {
    +				cidr := mustParseCIDR(t, r)
    +				if cidr.IP.To4() != nil {
    +					v.loadBalancerIPv4Ranges = append(v.loadBalancerIPv4Ranges, cidr)
    +				} else {
    +					v.loadBalancerIPv6Ranges = append(v.loadBalancerIPv6Ranges, cidr)
    +				}
    +			}
    +			for _, r := range tt.clusterIPRanges {
    +				cidr := mustParseCIDR(t, r)
    +				if cidr.IP.To4() != nil {
    +					v.clusterIPv4Ranges = append(v.clusterIPv4Ranges, cidr)
    +				} else {
    +					v.clusterIPv6Ranges = append(v.clusterIPv6Ranges, cidr)
    +				}
    +			}
    +
    +			result := v.FilterLoadBalancerIPs(tt.inputIPs, "test-svc", "default")
    +			assert.Equal(t, tt.expectedOutputIPs, result)
    +		})
    +	}
    +}
    
  • pkg/utils/ip.go+10 0 modified
    @@ -97,6 +97,16 @@ func IPNetEqual(a, b *net.IPNet) bool {
     	return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
     }
     
    +// IsIPInRanges returns true if the given IP is contained within any of the provided CIDR ranges.
    +func IsIPInRanges(ip net.IP, ranges []net.IPNet) bool {
    +	for i := range ranges {
    +		if ranges[i].Contains(ip) {
    +			return true
    +		}
    +	}
    +	return false
    +}
    +
     // IsDefaultRoute checks if a given CIDR is a default route by comparing it to the default routes for IPv4 and IPv6
     func IsDefaultRoute(cidr *net.IPNet) (bool, error) {
     	var defaultPrefixCIDR *net.IPNet
    
  • pkg/utils/iptables_moq.go+2 1 modified
    @@ -4,8 +4,9 @@
     package utils
     
     import (
    -	"github.com/coreos/go-iptables/iptables"
     	"sync"
    +
    +	"github.com/coreos/go-iptables/iptables"
     )
     
     // Ensure, that IPTablesHandlerMock does implement IPTablesHandler.
    
  • pkg/utils/ip_test.go+77 0 modified
    @@ -342,3 +342,80 @@ func TestGetSingleIPNet(t *testing.T) {
     		})
     	}
     }
    +
    +func mustParseCIDR(t *testing.T, cidr string) net.IPNet {
    +	t.Helper()
    +	_, ipnet, err := net.ParseCIDR(cidr)
    +	if err != nil {
    +		t.Fatalf("failed to parse CIDR %q: %v", cidr, err)
    +	}
    +	return *ipnet
    +}
    +
    +func TestIsIPInRanges(t *testing.T) {
    +	tests := []struct {
    +		name   string
    +		ip     string
    +		ranges []string
    +		want   bool
    +	}{
    +		{
    +			name:   "IP within single range",
    +			ip:     "10.0.0.5",
    +			ranges: []string{"10.0.0.0/24"},
    +			want:   true,
    +		},
    +		{
    +			name:   "IP outside single range",
    +			ip:     "192.168.1.1",
    +			ranges: []string{"10.0.0.0/24"},
    +			want:   false,
    +		},
    +		{
    +			name:   "IP within second of two ranges",
    +			ip:     "172.16.0.5",
    +			ranges: []string{"10.0.0.0/24", "172.16.0.0/16"},
    +			want:   true,
    +		},
    +		{
    +			name:   "IP outside all ranges",
    +			ip:     "8.8.8.8",
    +			ranges: []string{"10.0.0.0/8", "172.16.0.0/12"},
    +			want:   false,
    +		},
    +		{
    +			name:   "empty ranges",
    +			ip:     "10.0.0.1",
    +			ranges: []string{},
    +			want:   false,
    +		},
    +		{
    +			name:   "IPv6 within range",
    +			ip:     "fd00::5",
    +			ranges: []string{"fd00::/64"},
    +			want:   true,
    +		},
    +		{
    +			name:   "IPv6 outside range",
    +			ip:     "fe80::1",
    +			ranges: []string{"fd00::/64"},
    +			want:   false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			ip := net.ParseIP(tt.ip)
    +			assert.NotNil(t, ip, "test IP should be valid")
    +
    +			ranges := make([]net.IPNet, len(tt.ranges))
    +			for i, r := range tt.ranges {
    +				ranges[i] = mustParseCIDR(t, r)
    +			}
    +
    +			got := IsIPInRanges(ip, ranges)
    +			assert.Equal(t, tt.want, got)
    +		})
    +	}
    +}
    
  • pkg/version/version.go+9 0 modified
    @@ -28,6 +28,15 @@ var (
     				"recommends that you read the release notes carefully before deploying: " +
     				"https://github.com/cloudnativelabs/kube-router/releases/tag/v2.0.0",
     		},
    +		{
    +			minVersionInclusive: "v2.8.0",
    +			maxVersionExclusive: "v2.10.0",
    +			message: "Version v2.8.0 introduces a default feature of validating external IPs and loadbalancer IPs " +
    +				"on services against the ranges that were specified via kube-router CLI parameters by default. This " +
    +				"is a change from previous versions which only used those parameters for network policy decisions. " +
    +				"kube-router recommends that you read the release notes carefully before deploying: " +
    +				"https://github.com/cloudnativelabs/kube-router/releases/tag/v2.8.0",
    +		},
     	}
     )
     
    
  • README.md+2 0 modified
    @@ -31,6 +31,8 @@ single DaemonSet/Binary. It doesn't get any easier.
     kube-router uses the Linux kernel's LVS/IPVS features to implement its K8s Services Proxy. kube-router fully leverages
     power of LVS/IPVS to provide a rich set of scheduling options and unique features like DSR (Direct Server Return), L3
     load balancing with ECMP for deployments where high throughput, minimal latency and high-availability are crucial.
    +Kube-router also provides built-in validation of externalIPs and loadBalancerIPs against configured CIDR ranges,
    +preventing unauthorized VIP bindings in multi-tenant clusters.
     
     Read more about the advantages of IPVS for container load balancing:
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.