Repository: openkruise/kruise-game

Issue:
E2E Test Failure: GameServer lifecycle(DeleteGameServerReclaimPolicy) timing out in workflow
Description

The E2E test suite is failing in CI due to a timeout in the GameServer lifecycle(DeleteGameServerReclaimPolicy) test.

Logs :

```
 Failure [193.034 seconds]
Run kruise game manager e2e tests
/home/runner/work/kruise-game/kruise-game/test/e2e/e2e_test.go:20
  kruise game controllers
  /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:14
    GameServer lifecycle(DeleteGameServerReclaimPolicy) [It]
    /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:100

    Expected
        <context.deadlineExceededError>: 
        context deadline exceeded
        {}
    to be nil

    /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:116
------------------------------
Run kruise game manager e2e tests kruise game controllers 
  GameServer lifecycle(CascadeGameServerReclaimPolicy)
  /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:125

[SLOW TEST:18.050 seconds]
Run kruise game manager e2e tests
/home/runner/work/kruise-game/kruise-game/test/e2e/e2e_test.go:20
  kruise game controllers
  /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:14
    GameServer lifecycle(CascadeGameServerReclaimPolicy)
    /home/runner/work/kruise-game/kruise-game/test/e2e/testcase/testcase.go:125
------------------------------
```

Summary

Test suite: Run kruise game manager e2e tests

Failing test: GameServer lifecycle(DeleteGameServerReclaimPolicy)

Error: context deadline exceeded

Other tests: Passed (4/5 successful)



Hints from issue comments:
@ballista01: Solved by https://github.com/openkruise/kruise-game/pull/283.

Solution patch:
diff --git a/apis/v1alpha1/gameserver_types.go b/apis/v1alpha1/gameserver_types.go
index 6e73cfc..142988d 100644
--- a/apis/v1alpha1/gameserver_types.go
+++ b/apis/v1alpha1/gameserver_types.go
@@ -42,7 +42,7 @@ type GameServerSpec struct {
 	OpsState         OpsState            `json:"opsState,omitempty"`
 	UpdatePriority   *intstr.IntOrString `json:"updatePriority,omitempty"`
 	DeletionPriority *intstr.IntOrString `json:"deletionPriority,omitempty"`
-	NetworkDisabled  bool                `json:"networkDisabled,omitempty"`
+	NetworkDisabled  *bool               `json:"networkDisabled,omitempty"`
 	// Containers can be used to make the corresponding GameServer container fields
 	// different from the fields defined by GameServerTemplate in GameServerSetSpec.
 	Containers []GameServerContainer `json:"containers,omitempty"`
diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go
index 2bb2e4e..54c06c3 100644
--- a/apis/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/v1alpha1/zz_generated.deepcopy.go
@@ -272,6 +272,11 @@ func (in *GameServerSpec) DeepCopyInto(out *GameServerSpec) {
 		*out = new(intstr.IntOrString)
 		**out = **in
 	}
+	if in.NetworkDisabled != nil {
+		in, out := &in.NetworkDisabled, &out.NetworkDisabled
+		*out = new(bool)
+		**out = **in
+	}
 	if in.Containers != nil {
 		in, out := &in.Containers, &out.Containers
 		*out = make([]GameServerContainer, len(*in))
diff --git a/cloudprovider/alibabacloud/slb.go b/cloudprovider/alibabacloud/slb.go
index dcef3d6..021b2cf 100644
--- a/cloudprovider/alibabacloud/slb.go
+++ b/cloudprovider/alibabacloud/slb.go
@@ -582,14 +582,14 @@ func (s *SlbPlugin) consSvc(sc *slbConfig, pod *corev1.Pod, c client.Client, ctx
 	for i := 0; i < len(sc.targetPorts); i++ {
 		if sc.protocols[i] == ProtocolTCPUDP {
 			svcPorts = append(svcPorts, corev1.ServicePort{
-				Name:       fmt.Sprintf("%s-%s", strconv.Itoa(sc.targetPorts[i]), corev1.ProtocolTCP),
+				Name:       strconv.Itoa(sc.targetPorts[i]) + "-" + strings.ToLower(string(corev1.ProtocolTCP)),
 				Port:       ports[i],
 				Protocol:   corev1.ProtocolTCP,
 				TargetPort: intstr.FromInt(sc.targetPorts[i]),
 			})
 
 			svcPorts = append(svcPorts, corev1.ServicePort{
-				Name:       fmt.Sprintf("%s-%s", strconv.Itoa(sc.targetPorts[i]), corev1.ProtocolUDP),
+				Name:       strconv.Itoa(sc.targetPorts[i]) + "-" + strings.ToLower(string(corev1.ProtocolUDP)),
 				Port:       ports[i],
 				Protocol:   corev1.ProtocolUDP,
 				TargetPort: intstr.FromInt(sc.targetPorts[i]),
@@ -597,7 +597,7 @@ func (s *SlbPlugin) consSvc(sc *slbConfig, pod *corev1.Pod, c client.Client, ctx
 
 		} else {
 			svcPorts = append(svcPorts, corev1.ServicePort{
-				Name:       fmt.Sprintf("%s-%s", strconv.Itoa(sc.targetPorts[i]), sc.protocols[i]),
+				Name:       strconv.Itoa(sc.targetPorts[i]) + "-" + strings.ToLower(string(sc.protocols[i])),
 				Port:       ports[i],
 				Protocol:   sc.protocols[i],
 				TargetPort: intstr.FromInt(sc.targetPorts[i]),
diff --git a/docs/en/user_manuals/CRD_field_description.md b/docs/en/user_manuals/CRD_field_description.md
index 8f27c45..54f3e30 100644
--- a/docs/en/user_manuals/CRD_field_description.md
+++ b/docs/en/user_manuals/CRD_field_description.md
@@ -256,7 +256,8 @@ type GameServerSpec struct {
 
    // Whether to perform network isolation and cut off the access layer network
    // Default is false
-   NetworkDisabled  bool                `json:"networkDisabled,omitempty"`
+   // Optional override; when nil, inherit from GameServer template (defaults to false).
+   NetworkDisabled  *bool               `json:"networkDisabled,omitempty"`
    
    // Containers can be used to make the corresponding GameServer container fields
    // different from the fields defined by GameServerTemplate in GameServerSetSpec.
@@ -307,4 +308,3 @@ type GameServerStatus struct {
     LastTransitionTime metav1.Time         `json:"lastTransitionTime,omitempty"`
 }
 ```
-
diff --git "a/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md" "b/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md"
index 1c6d5e5..d64ab2d 100644
--- "a/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md"
+++ "b/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md"
@@ -217,7 +217,8 @@ type GameServerSpec struct {
    DeletionPriority *intstr.IntOrString `json:"deletionPriority,omitempty"`
 
    // 是否进行网络隔离、切断接入层网络，默认为false
-   NetworkDisabled  bool                `json:"networkDisabled,omitempty"`
+   // 可选布尔值；当为 nil 时沿用 GameServer 模板（默认为 false）。
+   NetworkDisabled  *bool               `json:"networkDisabled,omitempty"`
    
    // 使对应的GameServer Containers字段与GameServerSetSpec中GameServerTemplate定义的字段不同，意味着该GameServer可以拥有独立的参数配置。
    // 当前支持更改 Image 与 Resources
@@ -267,4 +268,4 @@ type GameServerStatus struct {
     // 上次变更时间
     LastTransitionTime metav1.Time         `json:"lastTransitionTime,omitempty"`
 }
-```
\ No newline at end of file
+```
diff --git a/pkg/controllers/gameserver/gameserver_manager.go b/pkg/controllers/gameserver/gameserver_manager.go
index 1410e4a..49d76be 100644
--- a/pkg/controllers/gameserver/gameserver_manager.go
+++ b/pkg/controllers/gameserver/gameserver_manager.go
@@ -18,6 +18,11 @@ package gameserver
 
 import (
 	"context"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
 	kruisePub "github.com/openkruise/kruise-api/apps/pub"
 	gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1"
 	"github.com/openkruise/kruise-game/cloudprovider/utils"
@@ -30,11 +35,8 @@ import (
 	"k8s.io/apimachinery/pkg/util/json"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
-	"reflect"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"strconv"
-	"strings"
-	"time"
 )
 
 var (
@@ -97,16 +99,24 @@ func (manager GameServerManager) SyncGsToPod() error {
 
 	newLabels := make(map[string]string)
 	newAnnotations := make(map[string]string)
-	if gs.Spec.DeletionPriority.String() != podDeletePriority {
-		newLabels[gameKruiseV1alpha1.GameServerDeletePriorityKey] = gs.Spec.DeletionPriority.String()
+	// tolerate nil pointers in spec priorities
+	var gsDpStr, gsUpStr string
+	if gs.Spec.DeletionPriority != nil {
+		gsDpStr = gs.Spec.DeletionPriority.String()
+	}
+	if gs.Spec.UpdatePriority != nil {
+		gsUpStr = gs.Spec.UpdatePriority.String()
+	}
+	if gsDpStr != podDeletePriority {
+		newLabels[gameKruiseV1alpha1.GameServerDeletePriorityKey] = gsDpStr
 		if podDeletePriority != "" {
-			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "DeletionPriority turn from %s to %s ", podDeletePriority, gs.Spec.DeletionPriority.String())
+			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "DeletionPriority turn from %s to %s ", podDeletePriority, gsDpStr)
 		}
 	}
-	if gs.Spec.UpdatePriority.String() != podUpdatePriority {
-		newLabels[gameKruiseV1alpha1.GameServerUpdatePriorityKey] = gs.Spec.UpdatePriority.String()
+	if gsUpStr != podUpdatePriority {
+		newLabels[gameKruiseV1alpha1.GameServerUpdatePriorityKey] = gsUpStr
 		if podUpdatePriority != "" {
-			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "UpdatePriority turn from %s to %s ", podUpdatePriority, gs.Spec.UpdatePriority.String())
+			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "UpdatePriority turn from %s to %s ", podUpdatePriority, gsUpStr)
 		}
 	}
 	if string(gs.Spec.OpsState) != podGsOpsState {
@@ -120,10 +130,11 @@ func (manager GameServerManager) SyncGsToPod() error {
 			manager.eventRecorder.Eventf(gs, eventType, StateReason, "OpsState turn from %s to %s ", podGsOpsState, string(gs.Spec.OpsState))
 		}
 	}
-	if podNetworkDisabled != strconv.FormatBool(gs.Spec.NetworkDisabled) {
-		newLabels[gameKruiseV1alpha1.GameServerNetworkDisabled] = strconv.FormatBool(gs.Spec.NetworkDisabled)
+	currentNetworkDisabled := strconv.FormatBool(ptr.Deref(gs.Spec.NetworkDisabled, false))
+	if podNetworkDisabled != currentNetworkDisabled {
+		newLabels[gameKruiseV1alpha1.GameServerNetworkDisabled] = currentNetworkDisabled
 		if podNetworkDisabled != "" {
-			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "NetworkDisabled turn from %s to %s ", podNetworkDisabled, strconv.FormatBool(gs.Spec.NetworkDisabled))
+			manager.eventRecorder.Eventf(gs, corev1.EventTypeNormal, StateReason, "NetworkDisabled turn from %s to %s ", podNetworkDisabled, currentNetworkDisabled)
 		}
 	}
 
@@ -267,16 +278,56 @@ func (manager GameServerManager) SyncPodToGs(gss *gameKruiseV1alpha1.GameServerS
 	}
 
 	if !reflect.DeepEqual(oldGsSpec, gs.Spec) || !reflect.DeepEqual(oldGsLabels, gs.GetLabels()) || !reflect.DeepEqual(oldGsAnnotations, gs.GetAnnotations()) {
-		// patch gs spec & metadata
-		patchSpec := map[string]interface{}{"spec": gs.Spec, "metadata": map[string]interface{}{"labels": gs.GetLabels(), "annotations": gs.GetAnnotations()}}
-		jsonPatchSpec, err := json.Marshal(patchSpec)
-		if err != nil {
-			return err
+		// Build a minimal patch to avoid clobbering fields updated concurrently by users/tests.
+		// Only include fields we actually changed and that the controller owns.
+		patch := make(map[string]interface{})
+
+		// Spec subfields: restrict to opsState, updatePriority, deletionPriority, networkDisabled
+		if !reflect.DeepEqual(oldGsSpec, gs.Spec) {
+			specPatch := make(map[string]interface{})
+			// opsState
+			if oldGsSpec.OpsState != gs.Spec.OpsState {
+				specPatch["opsState"] = gs.Spec.OpsState
+			}
+			// updatePriority
+			if (oldGsSpec.UpdatePriority == nil) != (gs.Spec.UpdatePriority == nil) ||
+				(oldGsSpec.UpdatePriority != nil && gs.Spec.UpdatePriority != nil && *oldGsSpec.UpdatePriority != *gs.Spec.UpdatePriority) {
+				specPatch["updatePriority"] = gs.Spec.UpdatePriority
+			}
+			// deletionPriority
+			if (oldGsSpec.DeletionPriority == nil) != (gs.Spec.DeletionPriority == nil) ||
+				(oldGsSpec.DeletionPriority != nil && gs.Spec.DeletionPriority != nil && *oldGsSpec.DeletionPriority != *gs.Spec.DeletionPriority) {
+				specPatch["deletionPriority"] = gs.Spec.DeletionPriority
+			}
+			// networkDisabled
+			oldNetworkDisabled := ptr.Deref(oldGsSpec.NetworkDisabled, false)
+			newNetworkDisabled := ptr.Deref(gs.Spec.NetworkDisabled, false)
+			if (oldGsSpec.NetworkDisabled == nil) != (gs.Spec.NetworkDisabled == nil) || oldNetworkDisabled != newNetworkDisabled {
+				specPatch["networkDisabled"] = gs.Spec.NetworkDisabled
+			}
+			if len(specPatch) > 0 {
+				patch["spec"] = specPatch
+			}
 		}
-		err = manager.client.Patch(context.TODO(), gs, client.RawPatch(types.MergePatchType, jsonPatchSpec))
-		if err != nil && !errors.IsNotFound(err) {
-			klog.Errorf("failed to patch GameServer spec %s in %s,because of %s.", gs.GetName(), gs.GetNamespace(), err.Error())
-			return err
+
+		// Metadata changes (labels/annotations) are safe to include fully
+		if !reflect.DeepEqual(oldGsLabels, gs.GetLabels()) || !reflect.DeepEqual(oldGsAnnotations, gs.GetAnnotations()) {
+			patch["metadata"] = map[string]interface{}{
+				"labels":      gs.GetLabels(),
+				"annotations": gs.GetAnnotations(),
+			}
+		}
+
+		if len(patch) > 0 {
+			jsonPatchSpec, err := json.Marshal(patch)
+			if err != nil {
+				return err
+			}
+			err = manager.client.Patch(context.TODO(), gs, client.RawPatch(types.MergePatchType, jsonPatchSpec))
+			if err != nil && !errors.IsNotFound(err) {
+				klog.Errorf("failed to patch GameServer spec/metadata %s in %s,because of %s.", gs.GetName(), gs.GetNamespace(), err.Error())
+				return err
+			}
 		}
 	}
 
@@ -396,14 +447,22 @@ func syncServiceQualities(serviceQualities []gameKruiseV1alpha1.ServiceQuality,
 			var lastActionTransitionTime metav1.Time
 			sqCondition, exist := sqConditionsMap[sq.Name]
 			if !exist || ((sqCondition.Status != string(podCondition.Status) || (sqCondition.Result != podConditionMessage)) && (sqCondition.LastActionTransitionTime.IsZero() || !sq.Permanent)) {
-				// exec action
+				// exec action (only apply fields explicitly set in action)
 				for _, action := range sq.ServiceQualityAction {
 					state, err := strconv.ParseBool(string(podCondition.Status))
 					if err == nil && state == action.State && (action.Result == "" || podConditionMessage == action.Result) {
-						gs.Spec.DeletionPriority = action.DeletionPriority
-						gs.Spec.UpdatePriority = action.UpdatePriority
-						gs.Spec.OpsState = action.OpsState
-						gs.Spec.NetworkDisabled = action.NetworkDisabled
+						if action.DeletionPriority != nil {
+							gs.Spec.DeletionPriority = action.DeletionPriority
+						}
+						if action.UpdatePriority != nil {
+							gs.Spec.UpdatePriority = action.UpdatePriority
+						}
+						if action.OpsState != "" {
+							gs.Spec.OpsState = action.OpsState
+						}
+						if action.NetworkDisabled != nil {
+							gs.Spec.NetworkDisabled = ptr.To(ptr.Deref(action.NetworkDisabled, false))
+						}
 						gs.SetLabels(util.MergeMapString(gs.GetLabels(), action.Labels))
 						gs.SetAnnotations(util.MergeMapString(gs.GetAnnotations(), action.Annotations))
 						lastActionTransitionTime = timeNow
diff --git a/pkg/util/client/no_deepcopy_lister.go b/pkg/util/client/no_deepcopy_lister.go
index 3272f57..79de654 100644
--- a/pkg/util/client/no_deepcopy_lister.go
+++ b/pkg/util/client/no_deepcopy_lister.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"fmt"
 	"reflect"
+	"sort"
 	"strings"
 	"time"
 
@@ -85,11 +86,6 @@ func (r *noDeepCopyLister) List(ctx context.Context, out client.ObjectList, opts
 
 	runtimeObjs := make([]runtime.Object, 0, len(objs))
 	for _, item := range objs {
-		// if the Limit option is set and the number of items
-		// listed exceeds this limit, then stop reading.
-		if limitSet && int64(len(runtimeObjs)) >= listOpts.Limit {
-			break
-		}
 		obj, isObj := item.(runtime.Object)
 		if !isObj {
 			return fmt.Errorf("cache contained %T, which is not an Object", obj)
@@ -106,6 +102,20 @@ func (r *noDeepCopyLister) List(ctx context.Context, out client.ObjectList, opts
 		}
 		runtimeObjs = append(runtimeObjs, obj)
 	}
+	sort.SliceStable(runtimeObjs, func(i, j int) bool {
+		iMeta, _ := apimeta.Accessor(runtimeObjs[i])
+		jMeta, _ := apimeta.Accessor(runtimeObjs[j])
+		if iMeta.GetNamespace() == jMeta.GetNamespace() {
+			return iMeta.GetName() < jMeta.GetName()
+		}
+		return iMeta.GetNamespace() < jMeta.GetNamespace()
+	})
+	if limitSet && int64(len(runtimeObjs)) > listOpts.Limit {
+		limit := int(listOpts.Limit)
+		if limit < len(runtimeObjs) {
+			runtimeObjs = runtimeObjs[:limit]
+		}
+	}
 	defer func() {
 		klog.V(6).Infof("Listed %v %v objects %v without DeepCopy, cost %v", gvk.GroupVersion(), gvk.Kind, len(runtimeObjs), time.Since(startTime))
 	}()
diff --git a/pkg/util/gameserver.go b/pkg/util/gameserver.go
index 7109337..f508e11 100644
--- a/pkg/util/gameserver.go
+++ b/pkg/util/gameserver.go
@@ -290,7 +290,7 @@ func InitGameServer(gss *gameKruiseV1alpha1.GameServerSet, name string) *gameKru
 	gs.SetAnnotations(gsAnnotations)
 
 	// set NetWork
-	gs.Spec.NetworkDisabled = false
+	gs.Spec.NetworkDisabled = ptr.To(false)
 
 	// set OpsState
 	gs.Spec.OpsState = gameKruiseV1alpha1.None



Test patch:
diff --git a/cloudprovider/alibabacloud/slb_test.go b/cloudprovider/alibabacloud/slb_test.go
index 4d44a57..0b3a25c 100644
--- a/cloudprovider/alibabacloud/slb_test.go
+++ b/cloudprovider/alibabacloud/slb_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package alibabacloud
 
 import (
+	"context"
 	"reflect"
 	"sync"
 	"testing"
@@ -289,3 +290,208 @@ func TestInitLbCache(t *testing.T) {
 		t.Errorf("podAllocate expect %v, but actully got %v", test.podAllocate, actualPodAllocate)
 	}
 }
+
+func TestConsSvc(t *testing.T) {
+	tests := []struct {
+		name        string
+		sc          *slbConfig
+		pod         *corev1.Pod
+		setup       func(*SlbPlugin)
+		expectErr   bool
+		validateSvc func(*testing.T, *corev1.Service)
+	}{
+		{
+			name: "basic service construction",
+			sc: &slbConfig{
+				lbIds:                     []string{"lb-123"},
+				targetPorts:               []int{80},
+				protocols:                 []corev1.Protocol{corev1.ProtocolTCP},
+				isFixed:                   false,
+				externalTrafficPolicyType: corev1.ServiceExternalTrafficPolicyTypeCluster,
+				lBHealthCheckSwitch:       "on",
+				lBHealthCheckType:         "tcp",
+				lBHealthCheckFlag:         "off",
+			},
+			pod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pod",
+					Namespace: "default",
+				},
+			},
+			setup: func(s *SlbPlugin) {
+				s.minPort = 10000
+				s.maxPort = 11000
+				s.cache = make(map[string]portAllocated)
+				s.podAllocate = make(map[string]string)
+			},
+			expectErr: false,
+			validateSvc: func(t *testing.T, svc *corev1.Service) {
+				if svc.Name != "test-pod" {
+					t.Errorf("expected service name 'test-pod', got '%s'", svc.Name)
+				}
+				if len(svc.Spec.Ports) != 1 {
+					t.Errorf("expected 1 port, got %d", len(svc.Spec.Ports))
+				}
+				if svc.Spec.Ports[0].Port < 10000 || svc.Spec.Ports[0].Port > 11000 {
+					t.Errorf("port %d is out of range", svc.Spec.Ports[0].Port)
+				}
+				if svc.Spec.Ports[0].TargetPort.IntValue() != 80 {
+					t.Errorf("expected target port 80, got %d", svc.Spec.Ports[0].TargetPort.IntValue())
+				}
+				if svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
+					t.Errorf("expected service type LoadBalancer, got %s", svc.Spec.Type)
+				}
+				if svc.Annotations[SlbIdAnnotationKey] == "" {
+					t.Error("expected SlbIdAnnotationKey to be set")
+				}
+			},
+		},
+		{
+			name: "service construction with existing allocation",
+			sc: &slbConfig{
+				lbIds:                     []string{"lb-123"},
+				targetPorts:               []int{8080},
+				protocols:                 []corev1.Protocol{corev1.ProtocolTCP},
+				isFixed:                   false,
+				externalTrafficPolicyType: corev1.ServiceExternalTrafficPolicyTypeLocal,
+				lBHealthCheckSwitch:       "off",
+				lBHealthCheckFlag:         "on",
+			},
+			pod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pod-2",
+					Namespace: "default",
+				},
+			},
+			setup: func(s *SlbPlugin) {
+				s.minPort = 10000
+				s.maxPort = 11000
+				s.cache = map[string]portAllocated{
+					"lb-123": {
+						10240: true,
+						10241: false,
+					},
+				}
+				s.podAllocate = map[string]string{
+					"default/test-pod-2": "lb-123:10240",
+				}
+			},
+			expectErr: false,
+			validateSvc: func(t *testing.T, svc *corev1.Service) {
+				if svc.Spec.ExternalTrafficPolicy != corev1.ServiceExternalTrafficPolicyTypeLocal {
+					t.Errorf("expected ExternalTrafficPolicy Local, got %s", svc.Spec.ExternalTrafficPolicy)
+				}
+				if svc.Annotations[LBHealthCheckFlagAnnotationKey] != "on" {
+					t.Errorf("expected health check flag 'on', got '%s'", svc.Annotations[LBHealthCheckFlagAnnotationKey])
+				}
+				// Should use the already allocated port
+				if len(svc.Spec.Ports) > 0 && svc.Spec.Ports[0].Port != 10240 {
+					t.Errorf("expected port 10240 (pre-allocated), got %d", svc.Spec.Ports[0].Port)
+				}
+			},
+		},
+		{
+			name: "TCP/UDP protocol service",
+			sc: &slbConfig{
+				lbIds:                     []string{"lb-456"},
+				targetPorts:               []int{53},
+				protocols:                 []corev1.Protocol{ProtocolTCPUDP},
+				isFixed:                   false,
+				externalTrafficPolicyType: corev1.ServiceExternalTrafficPolicyTypeCluster,
+				lBHealthCheckSwitch:       "on",
+				lBHealthCheckType:         "http",
+				lBHealthCheckFlag:         "off",
+			},
+			pod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "dns-pod",
+					Namespace: "kube-system",
+				},
+			},
+			setup: func(s *SlbPlugin) {
+				s.minPort = 30000
+				s.maxPort = 32767
+				s.cache = make(map[string]portAllocated)
+				s.podAllocate = make(map[string]string)
+			},
+			expectErr: false,
+			validateSvc: func(t *testing.T, svc *corev1.Service) {
+				// Should create both TCP and UDP ports
+				if len(svc.Spec.Ports) != 2 {
+					t.Errorf("expected 2 ports for TCPUDP protocol, got %d", len(svc.Spec.Ports))
+				}
+				if len(svc.Spec.Ports) >= 2 {
+					if svc.Spec.Ports[0].Protocol != corev1.ProtocolTCP {
+						t.Errorf("first port should be TCP, got %s", svc.Spec.Ports[0].Protocol)
+					}
+					if svc.Spec.Ports[1].Protocol != corev1.ProtocolUDP {
+						t.Errorf("second port should be UDP, got %s", svc.Spec.Ports[1].Protocol)
+					}
+					if svc.Spec.Ports[0].Port != svc.Spec.Ports[1].Port {
+						t.Error("TCP and UDP ports should have the same port number")
+					}
+				}
+			},
+		},
+		{
+			name: "no available ports",
+			sc: &slbConfig{
+				lbIds:                     []string{"lb-789"},
+				targetPorts:               []int{80},
+				protocols:                 []corev1.Protocol{corev1.ProtocolTCP},
+				isFixed:                   false,
+				externalTrafficPolicyType: corev1.ServiceExternalTrafficPolicyTypeCluster,
+				lBHealthCheckSwitch:       "on",
+				lBHealthCheckType:         "tcp",
+				lBHealthCheckFlag:         "off",
+			},
+			pod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pod-3",
+					Namespace: "default",
+				},
+			},
+			setup: func(s *SlbPlugin) {
+				s.minPort = 100
+				s.maxPort = 100
+				s.cache = map[string]portAllocated{
+					"lb-789": {
+						100: true, // Only available port is taken
+					},
+				}
+				s.podAllocate = make(map[string]string)
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			slb := &SlbPlugin{
+				mutex: sync.RWMutex{},
+			}
+
+			if tt.setup != nil {
+				tt.setup(slb)
+			}
+
+			svc, err := slb.consSvc(tt.sc, tt.pod, nil, context.Background())
+
+			if tt.expectErr {
+				if err == nil {
+					t.Error("expected error but got none")
+				}
+				return
+			}
+
+			if err != nil {
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+
+			if tt.validateSvc != nil {
+				tt.validateSvc(t, svc)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/gameserver/gameserver_controller_test.go b/pkg/controllers/gameserver/gameserver_controller_test.go
index 7ae5079..900e39e 100644
--- a/pkg/controllers/gameserver/gameserver_controller_test.go
+++ b/pkg/controllers/gameserver/gameserver_controller_test.go
@@ -129,7 +129,7 @@ func TestGameServerReconcile(t *testing.T) {
 					DeletionPriority: &deletionPriority,
 					UpdatePriority:   &updatePriority,
 					OpsState:         gameKruiseV1alpha1.None,
-					NetworkDisabled:  false,
+					NetworkDisabled:  ptr.To(false),
 				}
 				return gs
 			},
diff --git a/pkg/controllers/gameserver/gameserver_manager_test.go b/pkg/controllers/gameserver/gameserver_manager_test.go
index 215724d..16f825e 100644
--- a/pkg/controllers/gameserver/gameserver_manager_test.go
+++ b/pkg/controllers/gameserver/gameserver_manager_test.go
@@ -17,6 +17,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
@@ -585,8 +586,9 @@ func TestSyncGsToPod(t *testing.T) {
 			t.Errorf("expect DeletionPriority is %s ,but actually is %s", test.gs.Spec.DeletionPriority.String(), pod.Labels[gameKruiseV1alpha1.GameServerDeletePriorityKey])
 		}
 
-		if pod.Labels[gameKruiseV1alpha1.GameServerNetworkDisabled] != strconv.FormatBool(test.gs.Spec.NetworkDisabled) {
-			t.Errorf("expect NetworkDisabled is %s ,but actually is %s", strconv.FormatBool(test.gs.Spec.NetworkDisabled), pod.Labels[gameKruiseV1alpha1.GameServerNetworkDisabled])
+		expectNetworkDisabled := strconv.FormatBool(ptr.Deref(test.gs.Spec.NetworkDisabled, false))
+		if pod.Labels[gameKruiseV1alpha1.GameServerNetworkDisabled] != expectNetworkDisabled {
+			t.Errorf("expect NetworkDisabled is %s ,but actually is %s", expectNetworkDisabled, pod.Labels[gameKruiseV1alpha1.GameServerNetworkDisabled])
 		}
 
 		for gsKey, gsValue := range test.gs.GetAnnotations() {
diff --git a/pkg/util/gameserver_test.go b/pkg/util/gameserver_test.go
index ac5584a..7ac9d06 100644
--- a/pkg/util/gameserver_test.go
+++ b/pkg/util/gameserver_test.go
@@ -425,7 +425,7 @@ func TestInitGameServer(t *testing.T) {
 					},
 				},
 				Spec: gameKruiseV1alpha1.GameServerSpec{
-					NetworkDisabled:  false,
+					NetworkDisabled:  ptr.To(false),
 					OpsState:         gameKruiseV1alpha1.None,
 					UpdatePriority:   &updatePriority,
 					DeletionPriority: &deletionPriority,
diff --git a/test/e2e/client/client.go b/test/e2e/client/client.go
index 714758d..b120638 100644
--- a/test/e2e/client/client.go
+++ b/test/e2e/client/client.go
@@ -145,3 +145,7 @@ func (client *Client) GetPod(podName string) (*corev1.Pod, error) {
 func (client *Client) DeletePod(podName string) error {
 	return client.kubeClint.CoreV1().Pods(Namespace).Delete(context.TODO(), podName, metav1.DeleteOptions{})
 }
+
+func (client *Client) GetService(name string) (*corev1.Service, error) {
+	return client.kubeClint.CoreV1().Services(Namespace).Get(context.TODO(), name, metav1.GetOptions{})
+}
diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go
index 3d00fae..885c3bc 100644
--- a/test/e2e/framework/framework.go
+++ b/test/e2e/framework/framework.go
@@ -20,6 +20,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/wait"
 	clientset "k8s.io/client-go/kubernetes"
 	restclient "k8s.io/client-go/rest"
+	"k8s.io/utils/ptr"
 )
 
 type Framework struct {
@@ -82,6 +83,22 @@ func (f *Framework) DeployGameServerSet() (*gamekruiseiov1alpha1.GameServerSet,
 	return f.client.CreateGameServerSet(gss)
 }
 
+func (f *Framework) DeployGameServerSetWithNetwork(networkType string, conf []gamekruiseiov1alpha1.NetworkConfParams, ports []corev1.ContainerPort) (*gamekruiseiov1alpha1.GameServerSet, error) {
+	gss := f.client.DefaultGameServerSet()
+	gss.Spec.Network = &gamekruiseiov1alpha1.Network{
+		NetworkType: networkType,
+		NetworkConf: conf,
+	}
+	if len(ports) > 0 && len(gss.Spec.GameServerTemplate.Spec.Containers) > 0 {
+		gss.Spec.GameServerTemplate.Spec.Containers[0].Ports = append(gss.Spec.GameServerTemplate.Spec.Containers[0].Ports, ports...)
+	}
+	return f.client.CreateGameServerSet(gss)
+}
+
+func (f *Framework) GetGameServer(name string) (*gamekruiseiov1alpha1.GameServer, error) {
+	return f.client.GetGameServer(name)
+}
+
 func (f *Framework) DeployGameServerSetWithReclaimPolicy(reclaimPolicy gamekruiseiov1alpha1.GameServerReclaimPolicy) (*gamekruiseiov1alpha1.GameServerSet, error) {
 	gss := f.client.DefaultGameServerSet()
 	gss.Spec.GameServerTemplate.ReclaimPolicy = reclaimPolicy
@@ -139,6 +156,35 @@ func (f *Framework) GameServerScale(gss *gamekruiseiov1alpha1.GameServerSet, des
 	return f.client.PatchGameServerSet(data)
 }
 
+// PatchGssSpec applies a generic spec-only merge patch using a map of fields.
+func (f *Framework) PatchGssSpec(specFields map[string]interface{}) (*gamekruiseiov1alpha1.GameServerSet, error) {
+	patch := map[string]interface{}{"spec": specFields}
+	data, err := json.Marshal(patch)
+	if err != nil {
+		return nil, err
+	}
+	return f.client.PatchGameServerSet(data)
+}
+
+// PatchGss applies a generic merge patch to the GameServerSet resource.
+func (f *Framework) PatchGss(patch map[string]interface{}) (*gamekruiseiov1alpha1.GameServerSet, error) {
+	data, err := json.Marshal(patch)
+	if err != nil {
+		return nil, err
+	}
+	return f.client.PatchGameServerSet(data)
+}
+
+// PatchGameServerSpec applies a merge patch to GameServer.spec using provided fields.
+func (f *Framework) PatchGameServerSpec(gsName string, specFields map[string]interface{}) (*gamekruiseiov1alpha1.GameServer, error) {
+	patch := map[string]interface{}{"spec": specFields}
+	data, err := json.Marshal(patch)
+	if err != nil {
+		return nil, err
+	}
+	return f.client.PatchGameServer(gsName, data)
+}
+
 func (f *Framework) ImageUpdate(gss *gamekruiseiov1alpha1.GameServerSet, name, image string) (*gamekruiseiov1alpha1.GameServerSet, error) {
 	var newContainers []corev1.Container
 	for _, c := range gss.Spec.GameServerTemplate.Spec.Containers {
@@ -368,3 +414,85 @@ func (f *Framework) WaitForGsUpdatePriorityUpdated(gsName string, updatePriority
 			return false, nil
 		})
 }
+
+func (f *Framework) WaitForGsDesiredNetworkState(gsName string, desired gamekruiseiov1alpha1.NetworkState) error {
+	return wait.PollUntilContextTimeout(context.TODO(), 5*time.Second, 3*time.Minute, true,
+		func(ctx context.Context) (done bool, err error) {
+			gs, err := f.client.GetGameServer(gsName)
+			if err != nil {
+				return false, err
+			}
+			if gs.Status.NetworkStatus.DesiredNetworkState == desired {
+				return true, nil
+			}
+			return false, nil
+		})
+}
+
+func (f *Framework) WaitForNodePortServiceSelector(gsName string, disabled bool) error {
+	const (
+		activeKey   = "statefulset.kubernetes.io/pod-name"
+		disabledKey = "game.kruise.io/svc-selector-disabled"
+	)
+	return wait.PollUntilContextTimeout(context.TODO(), 5*time.Second, 3*time.Minute, true,
+		func(ctx context.Context) (done bool, err error) {
+			svc, err := f.client.GetService(gsName)
+			if err != nil {
+				if apierrors.IsNotFound(err) {
+					return false, nil
+				}
+				return false, err
+			}
+			selector := svc.Spec.Selector
+			_, hasActive := selector[activeKey]
+			disabledVal, hasDisabled := selector[disabledKey]
+			if disabled {
+				if !hasActive && hasDisabled && disabledVal == gsName {
+					return true, nil
+				}
+				return false, nil
+			}
+			if hasActive && selector[activeKey] == gsName && !hasDisabled {
+				return true, nil
+			}
+			return false, nil
+		})
+}
+
+func (f *Framework) WaitForGsNetworkDisabled(gsName string, want bool) error {
+	var lastPodValue, lastSpecValue string
+	var lastSpecNil bool
+	var lastPodErr, lastGsErr error
+	wantStr := strconv.FormatBool(want)
+
+	err := wait.PollUntilContextTimeout(context.TODO(), 5*time.Second, 3*time.Minute, true,
+		func(ctx context.Context) (done bool, err error) {
+			specMatched := false
+			if gs, err0 := f.client.GetGameServer(gsName); err0 == nil {
+				lastSpecValue = strconv.FormatBool(ptr.Deref(gs.Spec.NetworkDisabled, false))
+				lastSpecNil = gs.Spec.NetworkDisabled == nil
+				lastGsErr = nil
+				if lastSpecValue == wantStr {
+					specMatched = true
+				}
+			} else {
+				lastGsErr = err0
+			}
+
+			if pod, err1 := f.client.GetPod(gsName); err1 == nil {
+				lastPodValue = pod.GetLabels()[gamekruiseiov1alpha1.GameServerNetworkDisabled]
+				lastPodErr = nil
+				if specMatched && lastPodValue == wantStr {
+					return true, nil
+				}
+			} else {
+				lastPodErr = err1
+			}
+			return false, nil
+		})
+	if err != nil {
+		return fmt.Errorf("WaitForGsNetworkDisabled timeout: want=%s lastPod=%s lastSpec=%s lastSpecNil=%t podErr=%v gsErr=%v",
+			wantStr, lastPodValue, lastSpecValue, lastSpecNil, lastPodErr, lastGsErr)
+	}
+	return nil
+}
diff --git a/test/e2e/testcase/testcase.go b/test/e2e/testcase/testcase.go
index e0748fc..58e0aea 100644
--- a/test/e2e/testcase/testcase.go
+++ b/test/e2e/testcase/testcase.go
@@ -1,6 +1,7 @@
 package testcase
 
 import (
+	"fmt"
 	"time"
 
 	"github.com/onsi/ginkgo"
@@ -8,6 +9,7 @@ import (
 	gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1"
 	"github.com/openkruise/kruise-game/test/e2e/client"
 	"github.com/openkruise/kruise-game/test/e2e/framework"
+	corev1 "k8s.io/api/core/v1"
 )
 
 func RunTestCases(f *framework.Framework) {
@@ -97,6 +99,42 @@ func RunTestCases(f *framework.Framework) {
 			gomega.Expect(err).To(gomega.BeNil())
 		})
 
+		ginkgo.Describe("network control", func() {
+			ginkgo.It("disables NodePort traffic when networkDisabled is true", func() {
+				networkConf := []gameKruiseV1alpha1.NetworkConfParams{
+					{Name: "PortProtocols", Value: "8080/TCP"},
+				}
+				ports := []corev1.ContainerPort{{ContainerPort: 8080}}
+				gss, err := f.DeployGameServerSetWithNetwork("Kubernetes-NodePort", networkConf, ports)
+				gomega.Expect(err).To(gomega.BeNil())
+
+				err = f.ExpectGssCorrect(gss, []int{0, 1, 2})
+				gomega.Expect(err).To(gomega.BeNil())
+
+				target := fmt.Sprintf("%s-0", client.GameServerSet)
+
+				gomega.Expect(f.WaitForNodePortServiceSelector(target, false)).To(gomega.BeNil())
+				gomega.Expect(f.WaitForGsDesiredNetworkState(target, gameKruiseV1alpha1.NetworkReady)).To(gomega.BeNil())
+
+				_, err = f.PatchGameServerSpec(target, map[string]interface{}{"networkDisabled": true})
+				gomega.Expect(err).To(gomega.BeNil())
+				gomega.Expect(f.WaitForGsNetworkDisabled(target, true)).To(gomega.BeNil())
+				gomega.Expect(f.WaitForGsDesiredNetworkState(target, gameKruiseV1alpha1.NetworkNotReady)).To(gomega.BeNil())
+				gomega.Expect(f.WaitForNodePortServiceSelector(target, true)).To(gomega.BeNil())
+
+				_, err = f.PatchGameServerSpec(target, map[string]interface{}{"networkDisabled": false})
+				gomega.Expect(err).To(gomega.BeNil())
+				gomega.Expect(f.WaitForGsNetworkDisabled(target, false)).To(gomega.BeNil())
+				gomega.Expect(f.WaitForGsDesiredNetworkState(target, gameKruiseV1alpha1.NetworkReady)).To(gomega.BeNil())
+				gomega.Expect(f.WaitForNodePortServiceSelector(target, false)).To(gomega.BeNil())
+
+				gs, err := f.GetGameServer(target)
+				gomega.Expect(err).To(gomega.BeNil())
+				gomega.Expect(gs.Spec.NetworkDisabled).NotTo(gomega.BeNil())
+				gomega.Expect(*gs.Spec.NetworkDisabled).To(gomega.BeFalse())
+			})
+		})
+
 		ginkgo.It("GameServer lifecycle(DeleteGameServerReclaimPolicy)", func() {
 
 			// Deploy a gss, and the ReclaimPolicy is Delete