Repository: kubernetes/kubernetes
Pull request #130833: Add Unit Tests for Windows DSR and Overlay Support
Author: login: rzlink
Labels: name: sig/network
description: Categorizes an issue or PR as relevant to SIG Network.
name: kind/cleanup
description: Categorizes issue or PR as related to cleaning up code, process, or technical debt.
name: area/kube-proxy
name: lgtm
description: "Looks good to me", indicates that a PR is ready to be merged.
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: sig/windows
description: Categorizes an issue or PR as relevant to SIG Windows.
name: cncf-cla: yes
description: Indicates the PR's author has signed the CNCF CLA.
name: ok-to-test
description: Indicates a non-member PR verified by an org member that is safe to test.
name: needs-priority
description: Indicates a PR lacks a `priority/foo` label and requires one.
name: needs-triage
description: Indicates an issue or PR lacks a `triage/foo` label and requires one.
Created: 2025-03-15T00:28:55Z | Merged: 2025-03-18T19:45:56Z

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?
This PR adds unit tests to validate that the `WinDSR` feature gate is set correctly and ensures the correct flags are passed to HNS calls.

/kind cleanup
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/sig windows 

#### What this PR does / why we need it:
This PR introduces unit tests for `WinDSR` on Windows nodes, which is required to progress the feature from Alpha to Beta.

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
NONE
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:
<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs
- [KEP]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-windows/5100-windows-dsr-and-overlay-support#beta
```



Diff:
diff --git a/pkg/proxy/winkernel/hcnutils.go b/pkg/proxy/winkernel/hcnutils.go
index afb513b3fed65..5c86a113bc09b 100644
--- a/pkg/proxy/winkernel/hcnutils.go
+++ b/pkg/proxy/winkernel/hcnutils.go
@@ -49,6 +49,7 @@ type HcnService interface {
 	DsrSupported() error
 	// Policy functions
 	DeleteAllHnsLoadBalancerPolicy()
+	RemoteSubnetSupported() error
 }
 
 type hcnImpl struct{}
@@ -139,3 +140,7 @@ func (hcnObj hcnImpl) DeleteAllHnsLoadBalancerPolicy() {
 		}
 	}
 }
+
+func (hcnObj hcnImpl) RemoteSubnetSupported() error {
+	return hcn.RemoteSubnetSupported()
+}
diff --git a/pkg/proxy/winkernel/proxier.go b/pkg/proxy/winkernel/proxier.go
index 57c4a908b6f9f..6279a92c0cbd5 100644
--- a/pkg/proxy/winkernel/proxier.go
+++ b/pkg/proxy/winkernel/proxier.go
@@ -59,6 +59,10 @@ type KernelCompatTester interface {
 	IsCompatible() error
 }
 
+type HostMacProvider interface {
+	GetHostMac(nodeIP net.IP) string
+}
+
 // CanUseWinKernelProxier returns true if we should use the Kernel Proxier
 // instead of the "classic" userspace Proxier.  This is determined by checking
 // the windows kernel version and for the existence of kernel features.
@@ -723,6 +727,41 @@ func NewProxier(
 	}
 
 	hcnImpl := newHcnImpl()
+	proxier, err := newProxierInternal(
+		ipFamily,
+		hostname,
+		nodeIP,
+		serviceHealthServer,
+		healthzServer,
+		healthzPort,
+		hcnImpl,
+		&localHostMacProvider{},
+		config,
+		true, // waitForHNSOverlay
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	burstSyncs := 2
+	klog.V(3).InfoS("Record sync param", "minSyncPeriod", minSyncPeriod, "syncPeriod", syncPeriod, "burstSyncs", burstSyncs)
+	proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
+	return proxier, nil
+}
+
+// allow internal testing of proxier
+func newProxierInternal(
+	ipFamily v1.IPFamily,
+	hostname string,
+	nodeIP net.IP,
+	serviceHealthServer healthcheck.ServiceHealthServer,
+	healthzServer *healthcheck.ProxyHealthServer,
+	healthzPort int,
+	hcnImpl HcnService,
+	hostMacProvider HostMacProvider,
+	config config.KubeProxyWinkernelConfiguration,
+	waitForHNSOverlay bool,
+) (*Proxier, error) {
 	hns, supportedFeatures := newHostNetworkService(hcnImpl)
 	hnsNetworkName, err := getNetworkName(config.NetworkName)
 	if err != nil {
@@ -741,7 +780,10 @@ func NewProxier(
 	// Network could have been detected before Remote Subnet Routes are applied or ManagementIP is updated
 	// Sleep and update the network to include new information
 	if isOverlay(hnsNetworkInfo) {
-		time.Sleep(10 * time.Second)
+		if waitForHNSOverlay {
+			time.Sleep(10 * time.Second)
+		}
+
 		hnsNetworkInfo, err = hns.getNetworkByName(hnsNetworkName)
 		if err != nil {
 			return nil, fmt.Errorf("could not find HNS network %s", hnsNetworkName)
@@ -765,7 +807,7 @@ func NewProxier(
 		if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.WinOverlay) {
 			return nil, fmt.Errorf("WinOverlay feature gate not enabled")
 		}
-		err = hcn.RemoteSubnetSupported()
+		err = hcnImpl.RemoteSubnetSupported()
 		if err != nil {
 			return nil, err
 		}
@@ -783,17 +825,8 @@ func NewProxier(
 			}
 		}
 
-		interfaces, _ := net.Interfaces() //TODO create interfaces
-		for _, inter := range interfaces {
-			addresses, _ := inter.Addrs()
-			for _, addr := range addresses {
-				addrIP, _, _ := netutils.ParseCIDRSloppy(addr.String())
-				if addrIP.String() == nodeIP.String() {
-					klog.V(2).InfoS("Record Host MAC address", "addr", inter.HardwareAddr)
-					hostMac = inter.HardwareAddr.String()
-				}
-			}
-		}
+		hostMac = hostMacProvider.GetHostMac(nodeIP)
+
 		if len(hostMac) == 0 {
 			return nil, fmt.Errorf("could not find host mac address for %s", nodeIP)
 		}
@@ -827,9 +860,6 @@ func NewProxier(
 	proxier.endpointsChanges = endPointChangeTracker
 	proxier.serviceChanges = serviceChanges
 
-	burstSyncs := 2
-	klog.V(3).InfoS("Record sync param", "minSyncPeriod", minSyncPeriod, "syncPeriod", syncPeriod, "burstSyncs", burstSyncs)
-	proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
 	return proxier, nil
 }
 
@@ -1785,3 +1815,21 @@ func (proxier *Proxier) deleteLoadBalancer(hns HostNetworkService, lbHnsID *stri
 	*lbHnsID = ""
 	return true
 }
+
+type localHostMacProvider struct{}
+
+func (r *localHostMacProvider) GetHostMac(nodeIP net.IP) string {
+	var hostMac string
+	interfaces, _ := net.Interfaces()
+	for _, inter := range interfaces {
+		addresses, _ := inter.Addrs()
+		for _, addr := range addresses {
+			addrIP, _, _ := netutils.ParseCIDRSloppy(addr.String())
+			if addrIP.String() == nodeIP.String() {
+				klog.V(2).InfoS("Record Host MAC address", "addr", inter.HardwareAddr)
+				hostMac = inter.HardwareAddr.String()
+			}
+		}
+	}
+	return hostMac
+}
diff --git a/pkg/proxy/winkernel/proxier_test.go b/pkg/proxy/winkernel/proxier_test.go
index e78a800368650..dcfcc0cd49d25 100644
--- a/pkg/proxy/winkernel/proxier_test.go
+++ b/pkg/proxy/winkernel/proxier_test.go
@@ -25,7 +25,6 @@ import (
 	"net"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/Microsoft/hnslib/hcn"
 
@@ -34,7 +33,11 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
+	kubefeatures "k8s.io/kubernetes/pkg/features"
 	"k8s.io/kubernetes/pkg/proxy"
+	"k8s.io/kubernetes/pkg/proxy/apis/config"
 	"k8s.io/kubernetes/pkg/proxy/healthcheck"
 	fakehcn "k8s.io/kubernetes/pkg/proxy/winkernel/testing"
 	netutils "k8s.io/utils/net"
@@ -84,8 +87,38 @@ func newHnsNetwork(networkInfo *hnsNetworkInfo) *hcn.HostComputeNetwork {
 	return network
 }
 
-func NewFakeProxier(syncPeriod time.Duration, minSyncPeriod time.Duration, hostname string, nodeIP net.IP, networkType string) *Proxier {
+func NewFakeProxier(t *testing.T, hostname string, nodeIP net.IP, networkType string, enableDSR bool) *Proxier {
 	sourceVip := "192.168.1.2"
+
+	// enable `WinDSR` feature gate
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.WinDSR, true)
+
+	config := config.KubeProxyWinkernelConfiguration{
+		SourceVip:             sourceVip,
+		EnableDSR:             enableDSR,
+		NetworkName:           testNetwork,
+		ForwardHealthCheckVip: true,
+	}
+
+	hcnMock := getHcnMock(networkType)
+
+	proxier, _ := newProxierInternal(
+		v1.IPv4Protocol,
+		hostname,
+		nodeIP,
+		healthcheck.NewFakeServiceHealthServer(),
+		nil,
+		0,
+		hcnMock,
+		&testHostMacProvider{macAddress: macAddress},
+		config,
+		false,
+	)
+
+	return proxier
+}
+
+func getHcnMock(networkType string) *fakehcn.HcnMock {
 	var remoteSubnets []*remoteSubnetInfo
 	rs := &remoteSubnetInfo{
 		destinationPrefix: destinationPrefix,
@@ -102,37 +135,11 @@ func NewFakeProxier(syncPeriod time.Duration, minSyncPeriod time.Duration, hostn
 	}
 	hnsNetwork := newHnsNetwork(hnsNetworkInfo)
 	hcnMock := fakehcn.NewHcnMock(hnsNetwork)
-	proxier := &Proxier{
-		svcPortMap:          make(proxy.ServicePortMap),
-		endpointsMap:        make(proxy.EndpointsMap),
-		hostname:            testHostName,
-		nodeIP:              nodeIP,
-		serviceHealthServer: healthcheck.NewFakeServiceHealthServer(),
-		network:             *hnsNetworkInfo,
-		sourceVip:           sourceVip,
-		hostMac:             macAddress,
-		isDSR:               false,
-		hns: &hns{
-			hcn: hcnMock,
-		},
-		hcn:                   hcnMock,
-		endPointsRefCount:     make(endPointsReferenceCountMap),
-		forwardHealthCheckVip: true,
-		mapStaleLoadbalancers: make(map[string]bool),
-		terminatedEndpoints:   make(map[string]bool),
-	}
-
-	serviceChanges := proxy.NewServiceChangeTracker(v1.IPv4Protocol, proxier.newServiceInfo, proxier.serviceMapChange)
-	endpointChangeTracker := proxy.NewEndpointsChangeTracker(v1.IPv4Protocol, hostname, proxier.newEndpointInfo, proxier.endpointsMapChange)
-	proxier.endpointsChanges = endpointChangeTracker
-	proxier.serviceChanges = serviceChanges
-
-	return proxier
+	return hcnMock
 }
 
 func TestCreateServiceVip(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -185,8 +192,7 @@ func TestCreateServiceVip(t *testing.T) {
 }
 
 func TestCreateRemoteEndpointOverlay(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, false)
 	if proxier == nil {
 		t.Error()
 	}
@@ -250,8 +256,7 @@ func TestCreateRemoteEndpointOverlay(t *testing.T) {
 }
 
 func TestCreateRemoteEndpointL2Bridge(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge")
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge", false)
 	if proxier == nil {
 		t.Error()
 	}
@@ -311,9 +316,104 @@ func TestCreateRemoteEndpointL2Bridge(t *testing.T) {
 		t.Errorf("Global refCount: %v does not match endpoint refCount: %v", *proxier.endPointsRefCount[endpointGuid1], *epInfo.refCount)
 	}
 }
+
+func TestDsrEndpointsAreCreatedCorrectly(t *testing.T) {
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
+	if proxier == nil {
+		t.Fatal("Failed to create proxier")
+	}
+
+	svcIP := "10.20.30.41"
+	svcPort := 80
+	svcPortName := proxy.ServicePortName{
+		NamespacedName: makeNSN("ns1", "svc1"),
+		Port:           "p80",
+		Protocol:       v1.ProtocolTCP,
+	}
+
+	makeServiceMap(proxier,
+		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
+			svc.Spec.Type = "ClusterIP"
+			svc.Spec.ClusterIP = svcIP
+			svc.Spec.Ports = []v1.ServicePort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: v1.ProtocolTCP,
+			}}
+		}),
+	)
+
+	populateEndpointSlices(proxier,
+		makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, 1, func(eps *discovery.EndpointSlice) {
+			eps.AddressType = discovery.AddressTypeIPv4
+			eps.Endpoints = []discovery.Endpoint{{
+				Addresses: []string{epIpAddressRemote},
+			}}
+			eps.Ports = []discovery.EndpointPort{{
+				Name:     ptr.To(svcPortName.Port),
+				Port:     ptr.To(int32(svcPort)),
+				Protocol: ptr.To(v1.ProtocolTCP),
+			}}
+		}),
+	)
+
+	proxier.setInitialized(true)
+	proxier.syncProxyRules()
+
+	ep := proxier.endpointsMap[svcPortName][0]
+	epInfo, ok := ep.(*endpointInfo)
+	if !ok {
+		t.Errorf("Failed to cast endpointInfo %q", svcPortName.String())
+	}
+
+	if epInfo.hnsID == "" {
+		t.Errorf("Expected HNS ID to be set for endpoint %s, but got empty value", epIpAddressRemote)
+	}
+}
+
+func TestDsrNotAppliedToClusterTrafficPolicy(t *testing.T) {
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
+	if proxier == nil {
+		t.Fatal("Failed to create proxier")
+	}
+
+	svcIP := "10.20.30.41"
+	svcPort := 80
+	svcPortName := proxy.ServicePortName{
+		NamespacedName: makeNSN("ns1", "svc1"),
+		Port:           "p80",
+		Protocol:       v1.ProtocolTCP,
+	}
+
+	makeServiceMap(proxier,
+		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
+			svc.Spec.Type = "ClusterIP"
+			svc.Spec.ClusterIP = svcIP
+			svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyCluster
+			svc.Spec.Ports = []v1.ServicePort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: v1.ProtocolTCP,
+			}}
+		}),
+	)
+
+	proxier.setInitialized(true)
+	proxier.syncProxyRules()
+
+	svc := proxier.svcPortMap[svcPortName]
+	svcInfo, ok := svc.(*serviceInfo)
+	if !ok {
+		t.Errorf("Failed to cast serviceInfo %q", svcPortName.String())
+	}
+
+	if svcInfo.localTrafficDSR {
+		t.Errorf("Expected localTrafficDSR to be false for ExternalTrafficPolicy=Cluster, but got true")
+	}
+}
+
 func TestSharedRemoteEndpointDelete(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge")
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge", true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -454,8 +554,7 @@ func TestSharedRemoteEndpointDelete(t *testing.T) {
 	}
 }
 func TestSharedRemoteEndpointUpdate(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge")
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), "L2Bridge", true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -627,9 +726,9 @@ func TestSharedRemoteEndpointUpdate(t *testing.T) {
 		t.Errorf("Global refCount: %v does not match endpoint refCount: %v", *proxier.endPointsRefCount[endpointGuid1], *epInfo.refCount)
 	}
 }
-func TestCreateLoadBalancer(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+
+func TestCreateLoadBalancerWithoutDSR(t *testing.T) {
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, false)
 	if proxier == nil {
 		t.Error()
 	}
@@ -677,16 +776,97 @@ func TestCreateLoadBalancer(t *testing.T) {
 	if !ok {
 		t.Errorf("Failed to cast serviceInfo %q", svcPortName.String())
 
-	} else {
-		if svcInfo.hnsID != loadbalancerGuid1 {
-			t.Errorf("%v does not match %v", svcInfo.hnsID, loadbalancerGuid1)
-		}
+	}
+
+	if svcInfo.hnsID != loadbalancerGuid1 {
+		t.Errorf("%v does not match %v", svcInfo.hnsID, loadbalancerGuid1)
+	}
+
+	lb, err := proxier.hcn.GetLoadBalancerByID(svcInfo.hnsID)
+	if err != nil {
+		t.Errorf("Failed to fetch loadbalancer: %v", err)
+	}
+
+	if lb == nil {
+		t.Errorf("Failed to fetch loadbalancer: %v", err)
+	}
+
+	if lb.Flags != hcn.LoadBalancerFlagsNone {
+		t.Errorf("Incorrect loadbalancer flags. Current value: %v", lb.Flags)
+	}
+}
+
+func TestCreateLoadBalancerWithDSR(t *testing.T) {
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
+	if proxier == nil {
+		t.Error()
+	}
+
+	svcIP := "10.20.30.41"
+	svcPort := 80
+	svcNodePort := 3001
+	svcPortName := proxy.ServicePortName{
+		NamespacedName: makeNSN("ns1", "svc1"),
+		Port:           "p80",
+		Protocol:       v1.ProtocolTCP,
+	}
+
+	makeServiceMap(proxier,
+		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
+			svc.Spec.Type = "NodePort"
+			svc.Spec.ClusterIP = svcIP
+			svc.Spec.Ports = []v1.ServicePort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: v1.ProtocolTCP,
+				NodePort: int32(svcNodePort),
+			}}
+		}),
+	)
+	populateEndpointSlices(proxier,
+		makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, 1, func(eps *discovery.EndpointSlice) {
+			eps.AddressType = discovery.AddressTypeIPv4
+			eps.Endpoints = []discovery.Endpoint{{
+				Addresses: []string{epIpAddressRemote},
+			}}
+			eps.Ports = []discovery.EndpointPort{{
+				Name:     ptr.To(svcPortName.Port),
+				Port:     ptr.To(int32(svcPort)),
+				Protocol: ptr.To(v1.ProtocolTCP),
+			}}
+		}),
+	)
+
+	proxier.setInitialized(true)
+	proxier.syncProxyRules()
+
+	svc := proxier.svcPortMap[svcPortName]
+	svcInfo, ok := svc.(*serviceInfo)
+	if !ok {
+		t.Errorf("Failed to cast serviceInfo %q", svcPortName.String())
+
+	}
+
+	if svcInfo.hnsID != loadbalancerGuid1 {
+		t.Errorf("%v does not match %v", svcInfo.hnsID, loadbalancerGuid1)
+	}
+
+	lb, err := proxier.hcn.GetLoadBalancerByID(svcInfo.hnsID)
+	if err != nil {
+		t.Errorf("Failed to fetch loadbalancer: %v", err)
+	}
+
+	if lb == nil {
+		t.Errorf("Failed to fetch loadbalancer: %v", err)
+	}
+
+	if lb.Flags != hcn.LoadBalancerFlagsDSR {
+		t.Errorf("Incorrect loadbalancer flags. Current value: %v", lb.Flags)
 	}
 }
 
 func TestUpdateLoadBalancerWhenSupported(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -827,8 +1007,7 @@ func TestUpdateLoadBalancerWhenSupported(t *testing.T) {
 }
 
 func TestUpdateLoadBalancerWhenUnsupported(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -970,8 +1149,7 @@ func TestUpdateLoadBalancerWhenUnsupported(t *testing.T) {
 }
 
 func TestCreateDsrLoadBalancer(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -1038,7 +1216,8 @@ func TestCreateDsrLoadBalancer(t *testing.T) {
 		}
 		if len(svcInfo.loadBalancerIngressIPs) == 0 {
 			t.Errorf("svcInfo does not have any loadBalancerIngressIPs, %+v", svcInfo)
-		} else if svcInfo.loadBalancerIngressIPs[0].healthCheckHnsID != "LBID-4" {
+		}
+		if svcInfo.loadBalancerIngressIPs[0].healthCheckHnsID != "LBID-4" {
 			t.Errorf("The Hns Loadbalancer HealthCheck Id %v does not match %v. ServicePortName %q", svcInfo.loadBalancerIngressIPs[0].healthCheckHnsID, loadbalancerGuid1, svcPortName.String())
 		}
 	}
@@ -1048,8 +1227,7 @@ func TestCreateDsrLoadBalancer(t *testing.T) {
 // syncproxyrules only creates ClusterIP Loadbalancer and no NodePort, External IP or IngressIP
 // loadbalancers will be created.
 func TestClusterIPLBInCreateDsrLoadBalancer(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, false)
 
 	if proxier == nil {
 		t.Error()
@@ -1129,8 +1307,7 @@ func TestClusterIPLBInCreateDsrLoadBalancer(t *testing.T) {
 }
 
 func TestEndpointSlice(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -1210,8 +1387,7 @@ func TestNoopEndpointSlice(t *testing.T) {
 }
 
 func TestFindRemoteSubnetProviderAddress(t *testing.T) {
-	syncPeriod := 30 * time.Second
-	proxier := NewFakeProxier(syncPeriod, syncPeriod, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY)
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
 	if proxier == nil {
 		t.Error()
 	}
@@ -1236,6 +1412,117 @@ func TestFindRemoteSubnetProviderAddress(t *testing.T) {
 	}
 }
 
+func TestWinDSRWithOverlayEnabled(t *testing.T) {
+	proxier := NewFakeProxier(t, "testhost", netutils.ParseIPSloppy("10.0.0.1"), NETWORK_TYPE_OVERLAY, true)
+	if proxier == nil {
+		t.Error("Failed to create proxier")
+	}
+
+	svcIP := "10.20.30.41"
+	svcPort := 80
+	svcNodePort := 3001
+	svcPortName := proxy.ServicePortName{
+		NamespacedName: makeNSN("ns1", "svc1"),
+		Port:           "p80",
+		Protocol:       v1.ProtocolTCP,
+	}
+
+	makeServiceMap(proxier,
+		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
+			svc.Spec.Type = "ClusterIP"
+			svc.Spec.ClusterIP = svcIP
+			svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyLocal
+			svc.Spec.Ports = []v1.ServicePort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: v1.ProtocolTCP,
+				NodePort: int32(svcNodePort),
+			}}
+		}),
+	)
+
+	proxier.setInitialized(true)
+	proxier.syncProxyRules()
+
+	svc := proxier.svcPortMap[svcPortName]
+	svcInfo, ok := svc.(*serviceInfo)
+	if !ok {
+		t.Errorf("Failed to cast serviceInfo %q", svcPortName.String())
+	}
+
+	if !svcInfo.localTrafficDSR {
+		t.Errorf("Expected localTrafficDSR to be enabled but got false")
+	}
+}
+
+func TestDSRFeatureGateValidation(t *testing.T) {
+	testCases := []struct {
+		name          string
+		enableDSR     bool
+		featureGate   bool
+		expectFailure bool
+	}{
+		{
+			name:          "DSR enabled but feature gate disabled",
+			enableDSR:     true,
+			featureGate:   false,
+			expectFailure: true,
+		},
+		{
+			name:          "DSR enabled and feature gate enabled",
+			enableDSR:     true,
+			featureGate:   true,
+			expectFailure: false,
+		},
+		{
+			name:          "DSR disabled, feature gate does not matter",
+			enableDSR:     false,
+			featureGate:   false,
+			expectFailure: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Mock feature gate
+			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.WinDSR, tc.featureGate)
+
+			config := config.KubeProxyWinkernelConfiguration{
+				EnableDSR:   tc.enableDSR,
+				NetworkName: testNetwork,
+				SourceVip:   serviceVip,
+			}
+
+			hostMacProvider := &testHostMacProvider{macAddress: macAddress}
+
+			hcnMock := getHcnMock(NETWORK_TYPE_OVERLAY)
+
+			_, err := newProxierInternal(
+				v1.IPv4Protocol,                       // ipFamily
+				testHostName,                          // hostname
+				netutils.ParseIPSloppy("192.168.1.1"), // nodeIP
+				nil,                                   // serviceHealthServer (not needed in this unit test)
+				nil,                                   // healthzServer (not needed in this unit test)
+				0,                                     // healthzPort
+				hcnMock,                               // hcnImpl
+				hostMacProvider,                       // hostMacProvider
+				config,                                // kube-proxy config
+				false,                                 // waitForHNSOverlay
+			)
+
+			if tc.expectFailure {
+				if err == nil {
+					t.Errorf("Expected failure for case %q, but got success", tc.name)
+				}
+			} else {
+				if err != nil {
+					t.Errorf("Expected success for case %q, but got error: %v", tc.name, err)
+				}
+			}
+		})
+	}
+}
+
 func makeNSN(namespace, name string) types.NamespacedName {
 	return types.NamespacedName{Namespace: namespace, Name: name}
 }
@@ -1300,3 +1587,11 @@ func makeTestEndpointSlice(namespace, name string, sliceNum int, epsFunc func(*d
 	epsFunc(eps)
 	return eps
 }
+
+type testHostMacProvider struct {
+	macAddress string
+}
+
+func (r *testHostMacProvider) GetHostMac(nodeIP net.IP) string {
+	return r.macAddress
+}
diff --git a/pkg/proxy/winkernel/testing/hcnutils_mock.go b/pkg/proxy/winkernel/testing/hcnutils_mock.go
index 4b1b8e7cb9e7f..26843f2f0042a 100644
--- a/pkg/proxy/winkernel/testing/hcnutils_mock.go
+++ b/pkg/proxy/winkernel/testing/hcnutils_mock.go
@@ -62,6 +62,7 @@ func NewHcnMock(hnsNetwork *hcn.HostComputeNetwork) *HcnMock {
 				V2: true,
 			},
 			DSR:           true,
+			RemoteSubnet:  true,
 			IPv6DualStack: true,
 		},
 		network: hnsNetwork,
@@ -223,3 +224,11 @@ func (hcnObj HcnMock) DeleteAllHnsLoadBalancerPolicy() {
 		delete(loadbalancerMap, k)
 	}
 }
+
+func (hcnObj HcnMock) RemoteSubnetSupported() error {
+	if hcnObj.supportedFeatures.RemoteSubnet {
+		return nil
+	}
+
+	return errors.New("remote Subnet Not Supported")
+}


Reviews:
user: login: marosset
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:09:00Z
user: login: rzlink
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T21:39:38Z

Review comments:
diff_hunk: @@ -58,6 +58,9 @@ import (
 type KernelCompatTester interface {
 	IsCompatible() error
 }
path: pkg/proxy/winkernel/proxier.go
user: login: marosset
body: ```suggestion
}

```
created_at: 2025-03-17T17:09:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,9 @@ import (
 type KernelCompatTester interface {
 	IsCompatible() error
 }
path: pkg/proxy/winkernel/proxier.go
user: login: rzlink
body: fixed.
created_at: 2025-03-17T21:39:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: linux-foundation-easycla[bot]
created_at: 2025-03-15T00:28:59Z
body: <a href="https://easycla.lfx.linuxfoundation.org/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-signed.svg" alt="CLA Signed" align="left" height="28" width="328" ></a><br/><br />The committers listed above are authorized under a signed CLA.<ul><li>:white_check_mark: login: rzlink / name: Dawei Wei  (61863035803cc105619925a0fa23a779a9578bd4)</li></ul><!-- Date Modified: 2025-03-17 21:36:49.157618 -->
author_association: NONE
user: login: k8s-ci-robot
created_at: 2025-03-15T00:29:03Z
body: This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-15T00:29:04Z
body: Welcome @rzlink! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes/kubernetes'>kubernetes/kubernetes</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes/kubernetes has [its own contribution guidelines](https://github.com/kubernetes/kubernetes/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-15T00:29:05Z
body: Hi @rzlink. Thanks for your PR.

I'm waiting for a [kubernetes](https://github.com/orgs/kubernetes/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkubernetes).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T17:05:31Z
body: /ok-to-test
author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T17:05:41Z
body: /milestone v1.33
author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T17:20:05Z
body: /test pull-kubernetes-unit-windows-master
author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T21:36:39Z
body: /check-cla
author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T21:40:42Z
body: /test pull-kubernetes-unit-windows-master
author_association: CONTRIBUTOR
user: login: marosset
created_at: 2025-03-17T22:40:08Z
body: /assign @sbangari @daschott 

author_association: CONTRIBUTOR
user: login: sbangari
created_at: 2025-03-18T18:37:29Z
body: /lgtm
/approve
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-18T18:37:35Z
body: LGTM label has been added.  <details>Git tree hash: b68ac48c78596ff550d1d0367f45e6210ff3a2b4</details>
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-18T18:37:44Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kubernetes/pull/130833#" title="Author self-approved">rzlink</a>*, *<a href="https://github.com/kubernetes/kubernetes/pull/130833#issuecomment-2734352923" title="Approved">sbangari</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkubernetes).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[pkg/proxy/winkernel/OWNERS](https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/winkernel/OWNERS)~~ [sbangari]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR