Repository: kmesh-net/kmesh

Issue:
[OSPP 2025] Verifying and Complementing Kmesh Feature
Kmesh is a high performance, low overhead service mesh data plane based on eBPF and programmable kernels. Last year, the donation of CNCF was completed, and the functionality of traffic management has been basically improved. However, there is a lack of validation tests and documentation for the traffic management features in Kmesh dual-engine mode. Therefore, we hope to complete the validation tests and documentation for the following features.
- locality load balance
- circuit breaker
- rate limit
- Ingress
- engress

Among them about Ingress and egress after testing, lack of DNS resolution logic, need to add development.

Discussion:
@LiZhenCheng9527: ## OUTPUT:
1. egress feature for serverEntry development
1.1. Additions to the DNS resolution module logic
1.2. Additions to STATIC and NONE processing logic for serverEntry
1.3. Unit test
1.4. e2e Test
2.  Verification documentation for other features
2.1. locality load balance
2.2. circuit breaker
2.3. rate limit
2.4. Ingress@Flying-Tom: /assign@yp969803: @Flying-Tom this issue is for ospp mentorship@yp969803: you can apply for the project from https://summer-ospp.ac.cn/@yp969803: we cant assign you this now, please unassign@Flying-Tom: /unassign@Flying-Tom: > you can apply for the project from https://summer-ospp.ac.cn/

My apology, I'm trying to follow up this project, application for OSPP will be prepared.

Solution patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5446c364..bf55cb09 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -14,7 +14,7 @@ on:
 
 jobs:
   build:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     strategy:
       matrix:
         go-version: [ '1.23' ]
diff --git a/.markdownlint.yml b/.markdownlint.yml
index fd23fe85..64bc85e3 100644
--- a/.markdownlint.yml
+++ b/.markdownlint.yml
@@ -6,3 +6,6 @@ ol-prefix: false
 no-duplicate-heading: false
 single-h1: false
 no-emphasis-as-heading: false
+no-hard-tabs:
+  code_blocks: true
+  spaces_per_tab: 2
\ No newline at end of file
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 239648ae..f3834742 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -9,5 +9,6 @@
 | Zhonghu Xu           | [hzxuzhonghu](https://github.com/hzxuzhonghu)           | Huawei |
 | Songyang Xie         | [supercharge-xsy](https://github.com/supercharge-xsy)   | Huawei |
 | Xin Liu              | [bitcoffeeiux](https://github.com/bitcoffeeiux)         | Huawei |
+| Zhencheng Lee        | [LiZhencheng9527](https://github.com/LiZhencheng9527)]  | Huawei |
 
 ## Emeritus
diff --git a/Makefile b/Makefile
index 61a4409d..fdd046eb 100644
--- a/Makefile
+++ b/Makefile
@@ -97,6 +97,8 @@ kmesh-bpf:
 	$(QUIET) make -C bpf/deserialization_to_bpf_map
 	
 	$(QUIET) $(GO) generate bpf/kmesh/bpf2go/bpf2go.go
+
+	$(QUIET) $(GO) run hack/gen_bpf_specs.go
 kmesh-ko:
 	$(QUIET) find $(ROOT_DIR)/mk -name "*.pc" | xargs sed -i "s#^prefix=.*#prefix=${ROOT_DIR}#g"
 	$(call printlog, BUILD, "kernel")
diff --git a/bpf/include/inner_map_defs.h b/bpf/include/inner_map_defs.h
index 028f59ca..f2bd28c8 100644
--- a/bpf/include/inner_map_defs.h
+++ b/bpf/include/inner_map_defs.h
@@ -10,8 +10,8 @@
 typedef enum { MAP_TYPE_64, MAP_TYPE_192, MAP_TYPE_296, MAP_TYPE_1600, MAP_TYPE_MAX } map_in_map_type;
 
 #define MAP_GET_TYPE(idx)                (__u8)((__u32)(idx) >> 24)
-#define MAP_GET_INDEX(idx)               (__u32)((__u32)(idx)&0xFFFFFF)
-#define MAP_GEN_OUTER_KEY(map_type, pos) ((__u32)((((__u8)(map_type)&0xFF) << 24) + ((__u32)(pos)&0xFFFFFF)))
+#define MAP_GET_INDEX(idx)               (__u32)((__u32)(idx) & 0xFFFFFF)
+#define MAP_GEN_OUTER_KEY(map_type, pos) ((__u32)((((__u8)(map_type) & 0xFF) << 24) + ((__u32)(pos) & 0xFFFFFF)))
 
 #define MAP_VAL_SIZE_64   64
 #define MAP_VAL_SIZE_192  192
diff --git a/bpf/kmesh/ads/include/kmesh_common.h b/bpf/kmesh/ads/include/kmesh_common.h
index b1a35d91..d59f02d0 100644
--- a/bpf/kmesh/ads/include/kmesh_common.h
+++ b/bpf/kmesh/ads/include/kmesh_common.h
@@ -84,7 +84,7 @@ enum kmesh_l7_msg_type { MSG_UNKNOW = 0, MSG_REQUEST, MSG_MID_REPONSE, MSG_FINAL
 enum kmesh_strncmp_type { STRNCMP_FAILED = 0, STRNCMP_PREFIX, STRNCMP_EXACT };
 
 #define KMESH_PROTO_TYPE_WIDTH (8)
-#define GET_RET_PROTO_TYPE(n)  ((n)&0xff)
+#define GET_RET_PROTO_TYPE(n)  ((n) & 0xff)
 #define GET_RET_MSG_TYPE(n)    (((n) >> KMESH_PROTO_TYPE_WIDTH) & 0xff)
 
 #define CHECK_MODULE_NAME_NULL(ret) ((ret) == -EINVAL)
diff --git a/deploy/charts/kmesh-helm/templates/kmesh-rbac.yaml b/deploy/charts/kmesh-helm/templates/kmesh-rbac.yaml
index dbc57537..1ebee642 100644
--- a/deploy/charts/kmesh-helm/templates/kmesh-rbac.yaml
+++ b/deploy/charts/kmesh-helm/templates/kmesh-rbac.yaml
@@ -26,6 +26,9 @@ rules:
   - daemonsets
   verbs:
   - get
+- apiGroups: ["kmesh.net"]
+  resources: ["kmeshnodeinfos"]
+  verbs: ["get", "create", "update", "delete", "list", "watch"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/deploy/charts/kmesh-helm/templates/kmesh.net_kmeshnodeinfos.yaml b/deploy/charts/kmesh-helm/templates/kmesh.net_kmeshnodeinfos.yaml
new file mode 100644
index 00000000..a52b07dc
--- /dev/null
+++ b/deploy/charts/kmesh-helm/templates/kmesh.net_kmeshnodeinfos.yaml
@@ -0,0 +1,78 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.4
+  name: kmeshnodeinfos.kmesh.net
+spec:
+  group: kmesh.net
+  names:
+    kind: KmeshNodeInfo
+    listKind: KmeshNodeInfoList
+    plural: kmeshnodeinfos
+    singular: kmeshnodeinfo
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: KmeshNode is the Schema for the kmeshnodes API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              addresses:
+                description: |-
+                  Addresses is used to store the internal ip address information on the
+                  host. The IP address information is used to generate the IPsec state
+                  information. IPsec uses this information to determine which network
+                  adapter is used to encrypt and send data.
+                items:
+                  type: string
+                type: array
+              bootID:
+                description: |-
+                  bootid is used to generate the ipsec key. After the node is restarted,
+                  the key needs to be updated.
+                type: string
+              podCIDRS:
+                description: |-
+                  PodCIDRs used in IPsec checks the destination of the data to
+                  determine which IPsec state is used for encryption.
+                items:
+                  type: string
+                type: array
+              spi:
+                description: |-
+                  The SPI is used to identify the version number of the current key.
+                  The communication can be normal only when both communication parties
+                  have spis and the spi keys are the same.
+                type: integer
+            required:
+            - addresses
+            - bootID
+            - podCIDRS
+            - spi
+            type: object
+          status:
+            type: object
+        type: object
+    served: true
+    storage: true
diff --git a/docs/proposal/dual_engine_dns.md b/docs/proposal/dual_engine_dns.md
new file mode 100644
index 00000000..44ba05ac
--- /dev/null
+++ b/docs/proposal/dual_engine_dns.md
@@ -0,0 +1,242 @@
+---
+title: Support DNS Resolution in Dual Engine Mode
+authors:
+- "@Flying-Tom"
+reviewers:
+- 
+approvers:
+- 
+creation-date: 2025-07-09
+---
+
+## Support DNS Resolution in Dual Engine Mode
+
+<!--
+This is the title of your KEP. Keep it short, simple, and descriptive. A good
+title can help communicate what the KEP is and should be considered as part of
+any review.
+-->
+
+### Summary
+
+<!--
+This section is incredibly important for producing high-quality, user-focused
+documentation such as release notes or a development roadmap.
+A good summary is probably at least a paragraph in length.
+-->
+
+This proposal adds DNS resolution support for Dual-Engine mode workloads, enabling seamless Istio migration by supporting ServiceEntry resources with DNS-based endpoints. Workloads dynamically resolve hostnames to IP addresses without static configuration.
+
+### Motivation
+
+<!--
+This section is for explicitly listing the motivation, goals, and non-goals of
+this KEP.  Describe why the change is important and the benefits to users.
+-->
+
+In Istio, [ExternalName services](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) and DNS-typed [ServiceEntry](https://istio.io/latest/docs/reference/config/networking/service-entry/#ServiceEntry-Resolution) rely on client-side DNS resolution. When istiod processes these configurations, it generates workloads without pre-resolved IP addresses.
+
+Example ServiceEntry:
+
+```yaml
+apiVersion: networking.istio.io/v1
+kind: ServiceEntry
+metadata:
+  name: external-svc-google
+  namespace: default
+spec:
+  hosts:
+  - news.google.com
+  ports:
+  - number: 80
+    name: http
+    protocol: HTTP
+  resolution: DNS
+```
+
+In Release 1.1, Kmesh refactored the DNS module, extracting DNS logic from the kernel-native Ads controller into a standalone `AdsDnsController`. This decoupling enables reuse in Dual-Engine mode.
+
+![](./pics/dns-evolution.png)
+
+#### Goals
+
+<!--
+List the specific goals of the KEP. What is it trying to achieve? How will we
+know that this has succeeded?
+-->
+
+- Support DNS resolution for workloads generated from ServiceEntry resources in Dual-Engine mode
+- Implement asynchronous DNS resolution to avoid blocking workload processing
+- Provide automatic cleanup when workloads with DNS hostnames are removed
+- Support both IPv4 and IPv6 address resolution
+
+#### Non-Goals
+
+<!--
+What is out of scope for this KEP? Listing non-goals helps to focus discussion
+and make progress.
+-->
+
+This KEP does not aim to implement or provide a DNS proxy or DNS server functionality. Specifically, we do not support resolving DNS names on behalf of client workloads. As a result, if a `ServiceEntry` uses a non-resolvable or fake DNS domain, client workloads may fail to resolve and access the intended service. Handling such DNS resolution scenarios is explicitly out of scope for this proposal.
+
+### Proposal
+
+<!--
+This is where we get down to the specifics of what the proposal actually is.
+This should have enough detail that reviewers can understand exactly what
+you're proposing, but should not include things like API designs or
+implementation. What is the desired outcome and how do we measure success?.
+The "Design Details" section below is for the real
+nitty-gritty.
+-->
+
+Thanks to the `DNSResolver` in `pkg/dns` which extracted independent DNS resolution logic, we can reuse the DNS capabilities in Dual-engine mode. Inspired by the `AdsDnsController` in kernel-native mode, we implement a similar `WorkloadDnsController` to handle DNS resolution for workloads generated by `ServiceEntry` without address information.
+
+The controller receives workloads needing DNS resolution from the Processor via a channel, handles the DNS resolution asynchronously, and sends the resolved results back to the Processor through per-workload result channels. This design ensures that DNS resolution does not block the workload processing pipeline.
+
+### Design Details
+
+<!--
+This section should contain enough information that the specifics of your
+change are understandable. This may include API specs (though not always
+required) or even code snippets. If there's any ambiguity about HOW your
+proposal will be implemented, this is the place to discuss them.
+-->
+
+#### Architecture Overview
+
+The DNS resolution flow in Dual-Engine mode follows this pattern:
+
+```txt
+Processor → WorkloadDnsController → DNSResolver → Upstream DNS
+    ↑                                      ↓
+    └────── Resolved Workload ────────────┘
+```
+
+![](./pics/dual-engine-dns.png)
+
+#### Key Components
+
+##### WorkloadDnsController
+
+The WorkloadDnsController manages asynchronous DNS resolution for ServiceEntry-generated workloads. Key data structures:
+
+- **Resolution Queue**: Buffered channel receiving workloads from Processor for non-blocking submission
+- **Result Channel Registry**: Per-workload result channels (indexed by UID) for independent resolution tracking
+- **Domain Resolution Cache**: Bidirectional index between hostnames and pending workloads for batch resolution
+
+The controller operates through three concurrent workers:
+
+1. **Domain Processor**: Consumes workloads, groups by domain, delegates to DNS Resolver
+2. **Refresh Worker**: Receives resolved addresses, constructs workload objects, delivers via result channels
+3. **DNS Resolver**: Executes DNS queries (A/AAAA), maintains TTL-based cache (reused from `pkg/dns`)
+
+##### Resolution Flow
+
+The DNS resolution mechanism follows a producer-consumer pattern with timeout protection:
+
+**Workload Submission**: When the Processor encounters a workload without addresses, it registers a result channel and enqueues the workload for resolution. The Processor blocks on the result channel with a 3-second timeout to prevent pipeline blocking.
+
+**Domain Aggregation**: The Domain Processor maintains a hostname-indexed cache to aggregate workloads requiring the same domain, reducing redundant DNS queries. It checks for cached resolutions before initiating new queries.
+
+**Address Resolution**: The DNS Resolver performs parallel IPv4 (A) and IPv6 (AAAA) queries, respecting DNS TTL values. Resolved addresses are stored in a protocol-agnostic format.
+
+**Result Distribution**: The Refresh Worker reconstructs workload objects with resolved addresses and delivers them via result channels. Channel operations include a 100ms send timeout to prevent deadlocks. After delivery, the controller removes the result channel registration to prevent memory leaks.
+
+##### Cleanup Mechanism
+
+When a workload is deleted, the controller:
+
+- Removes the workload from pending hostname tracking
+- Removes the workload from the hostname's pending domain cache
+- If no more workloads depend on the hostname, unwatches the domain from DNS resolver
+
+This ensures no memory leaks and prevents unnecessary DNS queries.
+
+##### Design Rationale
+
+The WorkloadDnsController design diverges from AdsDnsController in several key aspects to better accommodate workload-level resolution requirements:
+
+| Design Decision | Approach | Rationale |
+|----------------|----------|-----------|
+| **Result Delivery** | Dedicated per-workload channels | Eliminates result filtering overhead and spurious wake-ups; enables direct workload-specific blocking without cross-workload interference |
+| **Timeout Strategy** | Two-tier mechanism (3s processor + 100ms channel) | Processor-level timeout prevents indefinite pipeline blocking; channel-level timeout prevents deadlocks from abandoned consumers |
+| **Address Format** | `netip.ParseAddr().AsSlice()` byte representation | Provides protocol-agnostic representation supporting both IPv4 and IPv6 without conditional logic |
+| **Refresh Interval** | Fixed 200ms rate | Simplifies implementation while maintaining adequate freshness for typical ServiceEntry use cases; trades configurability for consistency |
+
+#### Integration Points
+
+The WorkloadDnsController integrates into the Kmesh control plane through two primary integration points:
+
+| Component | Integration Method | Lifecycle |
+|-----------|-------------------|-----------|
+| **WorkloadController** | Instantiated during `NewController()` | Started via `Run()` context; shutdown via context cancellation |
+| **Processor** | Reference-based invocation | Synchronous blocking on resolution for address-less workloads; ensures data consistency before processing |
+
+**Initialization Sequence**:
+
+1. WorkloadController creates WorkloadDnsController instance
+2. DnsController reference stored in Processor
+3. DnsController goroutines started when WorkloadController.Run() is invoked
+4. Lifecycle bound to WorkloadController's context
+
+**Runtime Interaction**:
+
+1. Processor detects workload without addresses (ServiceEntry-originated)
+2. Processor registers result channel in DnsController
+3. Processor submits workload to resolution queue
+4. Processor blocks on result channel with timeout
+5. DnsController delivers resolved workload or timeout expires
+6. Processor continues with workload processing
+
+#### Comparison with AdsDnsController
+
+| Aspect | AdsDnsController | WorkloadDnsController |
+|--------|------------------|----------------------|
+| **Input** | Clusters with DNS endpoints | Workloads without addresses |
+| **Processing Unit** | Cluster | Workload |
+| **Result Delivery** | Shared channel | Per-workload channels |
+| **Timeout** | No processor timeout | 3s processor + 100ms channel |
+| **Refresh Rate** | From cluster config | Fixed 200ms |
+| **Cleanup** | On cluster removal | On workload removal |
+
+#### Test Plan
+
+<!--
+**Note:** *Not required until targeted at a release.*
+Consider the following in developing a test plan for this enhancement:
+- Will there be e2e and integration tests, in addition to unit tests?
+- How will it be tested in isolation vs with other components?
+No need to outline all test cases, just the general strategy. Anything
+that would count as tricky in the implementation, and anything particularly
+challenging to test, should be called out.
+-->
+
+**Unit Tests**
+
+DNS controller unit tests cover IPv4, IPv6, and dual-stack resolution scenarios, workload address overwriting logic, cleanup on workload deletion, and concurrent resolution of multiple workloads.
+
+**E2E Tests**
+
+E2E test validates the end-to-end ServiceEntry DNS resolution flow by creating a ServiceEntry with DNS resolution pointing to a fake hostname, creating a VirtualService routing the fake hostname to a real service, and verifying traffic flows successfully.
+
+Note: DNS proxy is disabled in IPv6-only environments, so tests skip in that configuration.
+
+### Alternatives
+
+<!--
+What other approaches did you consider, and why did you rule them out? These do
+not need to be as detailed as the proposal, but should include enough
+information to express the idea and why it was not acceptable.
+-->
+
+**Synchronous Resolution in Processor**: Embedding DNS resolution directly within the Processor's main workload handling loop would introduce blocking behavior, eliminating the possibility of batching concurrent resolutions for identical domains and complicating timeout implementation. This approach violates the separation of concerns principle by coupling network I/O with workload state management.
+
+**Shared Result Channel**: Reusing AdsDnsController's single shared channel pattern would necessitate complex result filtering logic to match responses with their corresponding workload requests. The additional synchronization overhead and potential for spurious wake-ups make this approach less suitable for workload-level granularity.
+
+**Kernel-Space DNS Resolution**: Implementing DNS resolution within eBPF programs would require reimplementing the DNS protocol stack in a constrained execution environment with strict complexity limits. This approach would duplicate existing userspace functionality, significantly increase maintenance burden, and provide minimal performance benefits given the infrequent nature of DNS lookups.
+
+<!--
+Note: This is a simplified version of kubernetes enhancement proposal template.
+https://github.com/kubernetes/enhancements/tree/3317d4cb548c396a430d1c1ac6625226018adf6a/keps/NNNN-kep-template
+-->
\ No newline at end of file
diff --git a/docs/proposal/ipsec_authz_e2e_test.md b/docs/proposal/ipsec_authz_e2e_test.md
new file mode 100644
index 00000000..2e27149b
--- /dev/null
+++ b/docs/proposal/ipsec_authz_e2e_test.md
@@ -0,0 +1,302 @@
+---
+title: IPSec & Offload Authorization E2E Test
+authors:
+- "@xiaojiangao123" # Authors' GitHub accounts here.
+reviewers:
+- "@robot"
+- TBD
+approvers:
+- "@robot"
+- TBD
+
+creation-date: 2025-5-12
+
+---
+
+## IPSec & Offload Authorization E2E Test Proposal
+
+### Overview
+
+This proposal aims to design end-to-end (E2E) test cases for the IPSec feature and Offload Authorization of Kmesh. The IPSec E2E tests focus on basic functionality, security, and reliability, verifying proper execution of IPSec tunnel setup, encryption/decryption, key management, and fault recovery. The Offload Authorization E2E tests include IP authorization, Port authorization, Header authorization, namespace authorization, and hosts authorization.
+
+### Motivation
+
+Kmesh’s IPSec functionality is a key component for secure communication within the service mesh. Its stability and reliability directly affect the security of the entire mesh. The Offload Authorization feature is a unique Kmesh capability that offloads IP and Port authorization to the Linux kernel via XDP. Without comprehensive E2E testing, version upgrades and releases may pose potential risks.
+
+#### Goals
+
+1. Develop complete E2E test cases  
+2. Cover all functional scenarios of Kmesh IPSec  
+3. Cover all types of Offload Authorization mechanisms
+
+### Proposal
+
+For the IPSec feature, we designed IPSec E2E test cases covering three core scenarios: basic connectivity, key rotation, and fault recovery.
+
+1. The basic connectivity test verifies IPSec tunnel establishment and the correctness of encrypted communication using tcpdump to inspect ESP packets.
+
+2. The key rotation test ensures the reliability of the PSK update mechanism and validates service continuity during key changes.
+
+For the Offload Authorization feature, E2E test cases are designed for IP, Port, Header, Namespace, and Hosts authorization.
+
+The test environment requires at least a 2-nodes Kubernetes cluster using httpbin and sleep or fortio as test applications.
+
+### Design Details
+
+#### 1. Test Environment Preparation
+
+##### Requirements
+
+- At least a 2-nodes Kubernetes cluster with Kmesh installed  
+- Tools: tcpdump  
+- Applications: httpbin, sleep, fortio
+
+#### 2. IPSec Test Scenarios
+
+##### 2.1 Basic Connectivity E2E Test
+
+###### Test Steps
+
+- Deploy httpbin and sleep applications on different nodes  
+- Verify connectivity between applications  
+- Check IPSec state and policy rules:
+
+   ```bash
+   ip xfrm state show
+   ```
+
+   **Expected Output:**
+
+   ```plaintext
+   src {{SRC_IP}} dst {{DST_IP}}
+       proto esp spi 0x{{SPI}} reqid 1 mode tunnel
+       replay-window 0 
+       output-mark 0xd0/0xffffffff
+       aead rfc4106(gcm(aes)) {{KEY}} 128
+       anti-replay context: seq 0x0, oseq 0x0, bitmap 0x00000000
+       sel src ::/0 dst ::/0 
+   ```
+
+   ```bash
+   ip xfrm policy show
+   ```
+
+   **Expected Output:**
+
+   ```plaintext
+   src ::/0 dst {{DST_SUBNET}} 
+       dir out priority 0 
+       mark 0xe0/0xffffffff 
+       tmpl src {{SRC_IP}} dst {{DST_IP}}
+           proto esp spi 0x{{SPI}} reqid 1 mode tunnel
+   ```
+
+- Verify encryption using tcpdump; analyze ESP headers and confirm payload is encrypted:
+
+   ```bash
+   tcpdump -i any esp
+   ```
+
+   **Expected Output:** ESP packets should be visible during communication.
+
+##### 2.2 Key Rotation E2E Test
+
+###### Test Steps
+
+- Deploy httpbin and sleep applications on different nodes
+- Record initial SPI and pre-shared key:
+
+   ```bash
+   ip xfrm state show
+   ```
+
+   **Expected Output:**
+
+   ```plaintext
+   src {{SRC_IP}} dst {{DST_IP}}
+       proto esp spi 0x{{INITIAL_SPI}} reqid 1 mode tunnel
+       aead rfc4106(gcm(aes)) {{INITIAL_KEY}} 128
+   ```
+
+- Send continuous traffic between applications
+- Update the pre-shared key
+
+   ```bash
+   kubectl create secret
+   ```
+
+- Check if SPI and key are updated in xfrm rules:
+
+   ```bash
+   ip xfrm state show
+   ```
+
+   **Expected Output:**
+
+   ```plaintext
+   src {{SRC_IP}} dst {{DST_IP}}
+       proto esp spi 0x{{NEW_SPI}} reqid 1 mode tunnel
+       aead rfc4106(gcm(aes)) {{NEW_KEY}} 128
+   ```
+
+- Verify communication continuity, encryption status
+
+#### 3. Offload Authorization Test Scenarios
+
+Apply the corresponding security policies (ALLOW/DENY) and routing strategies, and verify connectivity, including traffic that matches the rules and will be allowed and traffic that will be denied by the rules.
+
+Here are some AuthorizationPolicy samples.
+
+##### 3.1  IP Authorization Test
+
+- Example security policy YAML:
+
+```yaml
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: ip-allow-policy
+  namespace: test-ns1
+spec:
+  action: ALLOW
+  rules:
+  - from:
+    - source:
+        ipBlocks:
+        - "{{.Ip}}"
+---
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: ip-deny-policy
+  namespace: test-ns1
+spec:
+  action: DENY
+  rules:
+  - from:
+    - source:
+        ipBlocks:
+        - "{{.Ip}}"
+```
+
+##### 3.2 Port Authorization Test
+
+- Example security policy YAML:
+
+```yaml
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: port-allow-policy
+  namespace: test-ns1
+spec:
+  action: ALLOW
+  rules:
+  - to:
+    - operation:
+        ports: {{.Ports}}
+---
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: port-deny-policy
+  namespace: test-ns1
+spec:
+  action: DENY
+  rules:
+  - to:
+    - operation:
+        ports: {{.Ports}}
+```
+
+##### 3.3 Namespace Authorization Test
+
+- Example security policy YAML:
+
+```yaml
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: namespace-allow-policy
+  namespace: test-ns1
+spec:
+  action: ALLOW
+  rules:
+  - from:
+    - source:
+        namespaces:
+        - "{{.SourceNamespace}}"
+---
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: namespace-deny-policy
+  namespace: test-ns1
+spec:
+  action: DENY
+  rules:
+  - from:
+    - source:
+        namespaces:
+        - "{{.SourceNamespace}}"
+```
+
+##### 3.4 Header Authorization Test
+
+- Example security policy YAML:
+
+```yaml
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: header-allow-policy
+  namespace: test-ns1
+spec:
+  action: ALLOW
+  rules:
+  - when:
+    - key: request.headers[{{.HeaderName}}]
+      values: ["{{.HeaderValue}}"]
+---
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: header-deny-policy
+  namespace: test-ns1
+spec:
+  action: DENY
+  rules:
+  - when:
+    - key: request.headers[{{.HeaderName}}]
+      values: ["{{.HeaderValue}}"]
+```
+
+##### 3.5 Hosts Authorization Test
+
+- Example security policy YAML:
+
+```yaml
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: host-allow-policy
+  namespace: test-ns1
+spec:
+  action: ALLOW
+  rules:
+  - to:
+    - operation:
+        hosts: ["{{.TargetHost}}"]
+---
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: host-deny-policy
+  namespace: test-ns1
+spec:
+  action: DENY
+  rules:
+  - to:
+    - operation:
+        hosts: ["{{.TargetHost}}"]
+```
diff --git a/docs/proposal/pics/dns-evolution.png b/docs/proposal/pics/dns-evolution.png
new file mode 100644
index 00000000..e402e119
Binary files /dev/null and b/docs/proposal/pics/dns-evolution.png differ
diff --git a/docs/proposal/pics/dual-engine-dns.png b/docs/proposal/pics/dual-engine-dns.png
new file mode 100644
index 00000000..666d000f
Binary files /dev/null and b/docs/proposal/pics/dual-engine-dns.png differ
diff --git a/docs/proposal/ut_test.md b/docs/proposal/ut_test.md
new file mode 100644
index 00000000..80f9f06e
--- /dev/null
+++ b/docs/proposal/ut_test.md
@@ -0,0 +1,576 @@
+---
+title: Add Unit Testing Support for eBPF Programs in Kmesh
+authors:
+  - "@wxnzb"
+reviewers:
+  - "@lizhencheng"
+
+approvers:
+  - "@lizhencheng"
+
+creation-date: 2025-07-15
+---
+
+### Summary
+
+<!--
+This section is incredibly important for producing high-quality, user-focused
+documentation such as release notes or a development roadmap.
+
+A good summary is probably at least a paragraph in length.
+-->
+
+### Motivation
+
+<!--
+This section is for explicitly listing the motivation, goals, and non-goals of
+this KEP.  Describe why the change is important and the benefits to users.
+-->
+
+When developing eBPF programs in Kmesh, verifying their functionality requires compiling and performing black-box testing. Following Cilium's approach, we introduced a dedicated testing framework for eBPF programs. The framework is now fully set up, and we need contributors to help complete the test cases for each eBPF program.
+
+#### Goals
+
+<!--
+List the specific goals of the KEP. What is it trying to achieve? How will we
+know that this has succeeded?
+-->
+
+- Successfully running unit test code for Kmesh's eBPF sendMsg program.
+
+- Successfully running unit test code for Kmesh's eBPF cgroup program.
+
+- Documentation written in English for the tests of both sendMsg and cgroup programs.
+
+#### Non-Goals
+
+<!--
+What is out of scope for this KEP? Listing non-goals helps to focus discussion
+and make progress.
+-->
+
+### Proposal
+
+<!--
+This is where we get down to the specifics of what the proposal actually is.
+This should have enough detail that reviewers can understand exactly what
+you're proposing, but should not include things like API designs or
+implementation. What is the desired outcome and how do we measure success?.
+The "Design Details" section below is for the real
+nitty-gritty.
+-->
+
+- Test Framework: sendmsg.c and cgroup_sock.c will use the unitTests_BUILD_CONTEXT framework,cgroup_skb.c use the unitTests_BPF_PROG_TEST_RUN framework
+
+- Mock Functions: For each \_test.c file, include the necessary mocked BPF helper functions required during testing.
+
+- Testing Methods:
+  - For branches that write to BPF maps, use coll.Maps["..."] on the Go testing side to verify whether the map contents are correct.
+
+### Design Details
+
+<!--
+This section should contain enough information that the specifics of your
+change are understandable. This may include API specs (though not always
+required) or even code snippets. If there's any ambiguity about HOW your
+proposal will be implemented, this is the place to discuss them.
+-->
+
+### sendmsg.c
+
+### mount and set up
+
+#### mount
+
+- include the sockhash map in workload_sendmsg.c
+
+```c
+struct {
+    __uint(type, BPF_MAP_TYPE_SOCKHASH);
+    __type(key, struct bpf_sock_tuple);
+    __type(value, __u32);
+    __uint(max_entries, MAP_SIZE_OF_MANAGER);
+    __uint(map_flags, 0);
+} map_of_kmesh_socket SEC(".maps");
+```
+
+- in workload_test.go
+
+- load the eBPF program into the kernel
+
+```go
+   //load the eBPF program
+    spec := loadAndPrepSpec(t, path.Join(*testPath, objFilePath))
+    var (
+            coll *ebpf.Collection
+            err  error
+                        )
+    t.Log(path.Join(*testPath, objFilePath))
+    // Load the eBPF collection into the kernel
+    coll, err = ebpf.NewCollection(spec)
+```
+
+- Lookup the sockhash map and attach the sk_msg eBPF program to the map
+
+```go
+    sockMap := coll.Maps["km_socket"]
+    t.Log(sockMap.Type())
+    t.Log(ebpf.SockHash)
+    prog := coll.Programs["sendmsg_prog"]
+    err = link.RawAttachProgram(link.RawAttachProgramOptions{
+                            Attach: ebpf.AttachSkMsgVerdict,
+                            Target: sockMap.FD(),
+                            Program: prog,
+                        })
+```
+
+#### set up
+
+- Establish a network connection, obtain the file descriptor (fd), insert the fd into the map, and sending messages through this fd will trigger the eBPF program
+
+```go
+    localIP := get_local_ipv4(t)
+    clientPort := 12345
+    serverPort := 54321
+    serverSocket := localIP + ":" + strconv.Itoa(serverPort)
+    listener, err := net.Listen("tcp4", serverSocket)
+    if err != nil {
+        t.Fatalf("Failed to start TCP server: %v", err)
+    }
+    defer listener.Close()
+
+    // try to connect
+    conn, err := (&net.Dialer{
+        LocalAddr: &net.TCPAddr{
+            IP:   net.ParseIP(localIP),
+            Port: clientPort,
+        },
+        Timeout: 2 * time.Second,
+    }).Dial("tcp4", serverSocket)
+    defer conn.Close()
+    // get fd
+    fd, err := getSysFd(conn)
+    if err != nil {
+        t.Fatal(err)
+    }
+  // Construct the key-value pair and then insert it.
+    type bpfSockTuple4 struct {
+        Saddr [4]byte
+        Daddr [4]byte
+        Sport uint16
+        Dport uint16
+        _     [24]byte // padding
+    }
+    var tupleKey bpfSockTuple4
+    copy(tupleKey.Saddr[:], net.ParseIP(localIP).To4())
+    copy(tupleKey.Daddr[:], net.ParseIP(localIP).To4())
+    tupleKey.Sport = uint16(htons(uint16(clientPort)))
+    tupleKey.Dport = uint16(htons(uint16(serverPort)))
+    // insert fd
+    fd32 := uint32(fd)
+    err = sockMap.Update(&tupleKey, &fd32, ebpf.UpdateAny)
+    if err != nil {
+        t.Fatalf("failed to update sockhash: %v", err)
+    } else {
+        t.Logf("Update successful for key: %+v, fd: %d", tupleKey, fd32)
+    }
+```
+
+### test
+
+- For the sendmsg program, there are key steps:
+- get_origin_dst(msg, &dst_ip, &dst_port) internally calls
+- storage = bpf_sk_storage_get(&map_of_sock_storage, sk, 0, 0); — this may need to be mocked in workload_sendmsg_test.c.
+- alloc_dst_length(msg, tlv_size + TLV_END_SIZE) internally calls
+- ret = bpf_msg_push_data(msg, 0, length, 0); — in tests, we currently need to observe whether this should be mocked.
+- I observed that in sockops.c, auth_ip_tuple(skops); internally calls
+- struct ringbuf_msg_type *msg = bpf_ringbuf_reserve(&map_of_auth_req, sizeof(*msg), 0); and it works successfully.
+- SK_MSG_WRITE_BUF(msg, off, &type, TLV_TYPE_SIZE); performs the "TLV" write.
+- The current plan is to write a test that directly verifies whether TLV is written into the message header.
+- Validation method: Check whether TLV is correctly written into the message header.
+- Validation method
+
+```go
+    buf := make([]byte, 64)
+    n, _ := ln.Accept().Read(buf)
+    t.Logf("Received data: %x", buf[:n])
+```
+
+### cgroup_sock.c
+
+### mount and set up
+
+#### mount
+
+- in workload_test.go
+
+```go
+   // mount cgroup2
+    mount_cgroup2(t, cgroupPath)
+    defer syscall.Unmount(cgroupPath, 0)
+    //load the eBPF program
+    coll, lk := load_bpf_prog_to_cgroup(t, objFilePath, "cgroup_connect4_prog", cgroupPath)
+    defer coll.Close()
+    defer lk.Close()
+```
+
+#### set up
+
+```go
+conn, err := net.Dial("tcp4", "...")
+if err != nil {
+    t.Fatalf("Dial failed: %v", err)
+}
+defer conn.Close()
+```
+
+### test
+
+- Currently, connect4 and connect6 each have 5 test points.
+
+- 1
+
+- handle_kmesh_manage_process(&kmesh_ctx) internally calls bpf_map_update_elem(&map_of_manager, &key, &value, BPF_ANY); or err = bpf_map_delete_elem(&map_of_manager, &key); for verification.
+
+- When the destination address is CONTROL_CMD_IP: ENABLE_KMESH_PORT, it adds its netns_cookie to the map; when the destination address is CONTROL_CMD_IP: DISABLE_KMESH_PORT, it deletes its netns_cookie from the map.
+
+- Validation method:
+
+- Verify the addition when inserting.
+
+```go
+    kmManageMap := coll.Maps["km_manage"]
+    if kmManageMap == nil {
+        t.Fatal("Failed to get km_manage map from collection")
+    }
+    var (
+        key   [16]byte
+        value uint32
+    )
+
+    iter := kmManageMap.Iterate()
+    count := 0
+
+    for iter.Next(&key, &value) {
+        netnsCookie := binary.LittleEndian.Uint64(key[:8])
+        t.Logf("Entry %d: netns_cookie=%d, value=%d", count+1, netnsCookie, value)
+        count++
+    }
+    if err := iter.Err(); err != nil {
+        t.Fatalf("Iterate error: %v", err)
+    }
+
+    if count != 1 {
+        t.Fatalf("Expected 1 entry in km_manage map, but got %d", count)
+    }
+```
+
+- Validation when deleting
+
+```go
+    iter = kmManageMap.Iterate()
+    count = 0
+    for iter.Next(&key, &value) {
+        count++
+    }
+
+    if err := iter.Err(); err != nil {
+        t.Fatalf("Iterate error: %v", err)
+    }
+    if count != 0 {
+        t.Fatalf("Expected 0 entry in km_manage map, but got %d", count)
+    }
+```
+
+- Notes
+
+- Here it may be necessary to mock storage = bpf_sk_storage_get(&map_of_sock_storage, sk, 0, BPF_LOCAL_STORAGE_GET_F_CREATE); inside workload_cgroup_sock_test.c.
+
+- 2
+
+- The function sock_traffic_control(&kmesh_ctx) is critical and internally includes
+
+- frontend_v = map_lookup_frontend(&frontend_k); Consider how to return frontend_v; this must return a value.
+
+- By constructing a key-value pair so that the map contains this k-v, it can be found; construct the frontend map.
+
+```go
+    //frontend map
+    type ip_addr struct {
+        Raw [16]byte
+    }
+
+    type frontend_key struct {
+        Addr ip_addr
+    }
+
+    type frontend_value struct {
+        UpstreamID uint32
+    }
+
+    FrontendMap := coll.Maps["km_frontend"]
+    var f_key frontend_key
+    ip4 := net.ParseIP(localIP).To4()
+    if ip4 == nil {
+        t.Fatalf("invalid IPv4 address")
+    }
+    copy(f_key.Addr.Raw[0:4], ip4)
+    // Build the value
+    f_val := frontend_value{
+        UpstreamID: 1,
+    }
+    if err := FrontendMap.Update(&f_key, &f_val, ebpf.UpdateAny); err != nil {
+        log.Fatalf("Update failed: %v", err)
+    }
+```
+
+- frontend_manager(kmesh_ctx, frontend_v); internally includes
+- kmesh_map_lookup_elem(&map_of_service, key)
+- 2.1: can find:
+- 2.1.1:
+- Test point: waypoint == true in service_value
+
+```go
+type service_value struct {
+    PrioEndpointCount [7]uint32
+    LbPolicy          uint32
+    ServicePort       [10]uint32
+    TargetPort        [10]uint32
+    WpAddr            ip_addr
+    WaypointPort      uint32
+}
+wpIP := net.ParseIP("localIP").To4()
+// Build the value
+s_val := service_value{
+    WpAddr:       ip_addr{Raw: [16]byte{}},
+    WaypointPort: 55555,                    //Build
+}
+```
+
+- At this point, you need to monitor the forwarded address so that the forwarded connection will not be rejected
+
+### Alternatives
+
+```go
+test:=localIp+":"+strconv.Itoa(htons(55555))
+-  _, err = net.Listen("tcp4", "10.30.0.124:985")
+```
+
+- 2.2.2:
+
+- 2.2: If not found, perform kmesh_map_lookup_elem(&map_of_backend, key); this must return a value
+
+- 2.2.1:
+
+- Test point: waypoint == true in backend_value
+
+- Construction:
+
+```go
+type backend_value struct {
+    Addr         ip_addr
+    ServiceCount uint32
+    Service      [MAX_SERVICE_COUNT]uint32
+    WpAddr       ip_addr
+    WaypointPort uint32
+}
+    wpIP := net.ParseIP(localIP).To4()
+// Build the value
+b_val := backend_value{
+    Addr:         ip_addr{Raw: [16]byte{}},
+    ServiceCount: 0,
+    Service:      [MAX_SERVICE_COUNT]uint32{},
+    WpAddr:       ip_addr{Raw: [16]byte{}},
+    WaypointPort: uint32(testPort),
+}
+// map WpAddr
+copy(b_val.WpAddr.Raw[0:4], wpIP)
+if err := BackendMap.Update(&b_key, &b_val, ebpf.UpdateAny); err != nil {
+    log.Fatalf("Update failed: %v", err)
+}
+```
+
+- This traffic is also forwarded, so the forwarded address must be listened to in advance.
+
+```go
+testIpPort := localIP + ":" + strconv.Itoa(htons(testPort))
+_, err = net.Listen("tcp4", testIpPort)
+```
+
+- 2.2.2
+
+- Test point: waypoint == false in backend_value
+
+- Construction:
+
+```go
+type backend_value struct {
+    Addr         ip_addr
+    ServiceCount uint32
+    Service      [MAX_SERVICE_COUNT]uint32
+    WpAddr       ip_addr
+    WaypointPort uint32
+}
+b_val := backend_value{
+    Addr:         ip_addr{Raw: [16]byte{}},
+    ServiceCount: 0,
+    Service:      [MAX_SERVICE_COUNT]uint32{},
+    WpAddr:       ip_addr{Raw: [16]byte{}},
+    WaypointPort: 0,
+}
+if err := BackendMap.Update(&b_key, &b_val, ebpf.UpdateAny); err != nil {
+    log.Fatalf("Update failed: %v", err)
+}
+```
+
+- Note: The UpstreamID in the constructed value must match the key length used later when populating km_backend or the key length used in km_service. For example:
+
+```go
+type backend_key struct {
+    BackendUID uint32
+}
+b_key := backend_key{
+    BackendUID: 1,
+}
+```
+
+```go
+type service_key struct {
+    ServiceID uint32
+}
+s_key := service_key{
+    ServiceID: 1,
+}
+```
+
+- Validation method:
+
+```go
+expectedIP := localIP
+    expectedPort := strconv.Itoa(htons(testPort))
+
+    if host != expectedIP || port != expectedPort {
+        t.Fatalf("Expected redirected to %s:%s, but got %s:%s", expectedIP, expectedPort, host, port)
+    }
+```
+
+### cgroup_skb.c
+
+- It is a type supported by bpf_prog_run and can be tested using the first framework.
+- Since it uses the same \_\_sk_buff parameters as tc triggers, it can be written by following that example.
+
+```c
+#include "ut_common.h"
+#include <linux/bpf.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <netinet/tcp.h>
+#include "workload/cgroup_skb.c"
+struct tcp_probe_info mock_info;
+bool mock_ringbuf_called = false;
+static __always_inline void mock_clear()
+{
+    __builtin_memset(&mock_info, 0, sizeof(mock_info));
+    mock_ringbuf_called = false;
+}
+void *bpf_ringbuf_reserve(void *ringbuf, __u64 size, __u64 flags)
+{
+    mock_ringbuf_called = true;
+    return &mock_info; // return mock_info address
+}
+// Tail call map for jump
+struct {
+    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+    __uint(key_size, sizeof(__u32));
+    __uint(max_entries, 2);
+    __array(values, int());
+} entry_call_map SEC(".maps") = {
+    .values =
+        {
+            [0] = &cgroup_skb_ingress_prog, // 0 号 slot into ingress
+            [1] = &cgroup_skb_egress_prog,  // 1 号 slot into egress
+        },
+};
+
+// 模拟 IP + TCP 头，构造简单 TCP SYN 报文
+#define SRC_IP    0x0F00000A
+#define SRC_PORT  23445
+#define DEST_IP   0x0F00010A
+#define DEST_PORT 80
+
+const struct ethhdr l2 = {
+    .h_source = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
+    .h_dest = {0x12, 0x23, 0x34, 0x45, 0x56, 0x67},
+    .h_proto = bpf_htons(ETH_P_IP),
+};
+
+const struct iphdr l3 = {
+    .version = 4,
+    .ihl = 5,
+    .tot_len = 40,
+    .id = 0x5438,
+    .frag_off = bpf_htons(IP_DF),
+    .ttl = 64,
+    .protocol = IPPROTO_TCP,
+    .saddr = SRC_IP,
+    .daddr = DEST_IP,
+};
+
+const struct tcphdr l4 = {
+    .source = bpf_htons(SRC_PORT),
+    .dest = bpf_htons(DEST_PORT),
+    .seq = 2922048129,
+    .doff = 0,
+    .syn = 1,
+    .window = 64240,
+};
+
+const char payload[20] = "Cgroup SKB Test!!";
+
+// ---------------------- PKTGEN ----------------------
+/// 构造测试用数据包
+PKTGEN("cgroup_skb", "cgroup_skb_ingress")
+int test_ingress_pktgen(struct __sk_buff *ctx)
+{
+    return build_skb_packet(ctx, &l2, &l3, &l4, payload, sizeof(payload));
+}
+
+/// ---------------------- JUMP ----------------------
+/// 通过 tail call 跳转执行 ingress 程序
+JUMP("cgroup_skb", "cgroup_skb_ingress")
+int test_ingress_jump(struct __sk_buff *ctx)
+{
+    // 触发 tail call
+    bpf_tail_call(ctx, &entry_call_map, 0); // index 0 = ingress
+    return TEST_ERROR;
+}
+
+/// ---------------------- CHECK ----------------------
+/// 校验结果
+CHECK("cgroup_skb", "cgroup_skb_ingress")
+int test_ingress_check(struct __sk_buff *ctx)
+{
+    const __u32 expected_status = SK_PASS;
+
+    test_init();
+    mock_clear();
+    // 校验基本信息（头部和内容）
+    check_skb_packet(ctx, &expected_status, &l2, &l3, &l4, payload, sizeof(payload));
+    if (!mock_ringbuf_called) {
+        test_fatal("Expected bpf_ringbuf_submit to be called, but it wasn't");
+    }
+
+    test_finish();
+}
+```
+
+<!--
+What other approaches did you consider, and why did you rule them out? These do
+not need to be as detailed as the proposal, but should include enough
+information to express the idea and why it was not acceptable.
+-->
+
+<!--
+Note: This is a simplified version of kubernetes enhancement proposal template.
+https://github.com/kubernetes/enhancements/tree/3317d4cb548c396a430d1c1ac6625226018adf6a/keps/NNNN-kep-template
+-->
diff --git a/hack/gen_bpf2go_exec.sh b/hack/gen_bpf2go_exec.sh
index a2dfd779..90af0e58 100755
--- a/hack/gen_bpf2go_exec.sh
+++ b/hack/gen_bpf2go_exec.sh
@@ -8,5 +8,6 @@ kmesh_exec() {
 	set_enhanced_kernel_env
 	prepare
 	go generate bpf/kmesh/bpf2go/bpf2go.go
+	go run hack/gen_bpf_specs.go
 }
 kmesh_exec
diff --git a/hack/gen_bpf_specs.go b/hack/gen_bpf_specs.go
new file mode 100644
index 00000000..ff7a8404
--- /dev/null
+++ b/hack/gen_bpf_specs.go
@@ -0,0 +1,485 @@
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"go/format"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strings"
+	"text/template"
+)
+
+type entry struct {
+	OutputDir string // e.g. kernelnative/normal or kernelnative/enhanced or dualengine or general
+	GoPkg     string // go package name passed in --go-package
+	Symbol    string // the symbol token (KmeshCgroupSock etc)
+}
+
+type pkgInfo struct {
+	Alias      string
+	ImportPath string
+	OutputDir  string
+	Entries    []entry
+}
+
+type symbolGen struct {
+	BaseName       string // e.g. KmeshSockops
+	NormalPkgAlias string // alias that provides normal symbol (may be "")
+	NormalSymbol   string // e.g. KmeshSockops
+	CompatPkgAlias string // alias that provides compat symbol (may be "")
+	CompatSymbol   string // e.g. KmeshSockopsCompat
+}
+
+func main() {
+	root, err := repoRoot()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed detect repo root: %v\n", err)
+		os.Exit(2)
+	}
+
+	files := collectGoFiles(root)
+
+	// regexes
+	genRe := regexp.MustCompile(`//\s*go:generate\s+(?:.*\b)bpf2go\b(.*)`)
+	outDirRe := regexp.MustCompile(`--output-dir\s+([^\s]+)`)
+	goPkgRe := regexp.MustCompile(`--go-package\s+([^\s]+)`)
+	symbolRe := regexp.MustCompile(`\b([A-Za-z0-9_]+)\s+[^\s]+\.(?:c|C)\b`)
+
+	var entries []entry
+	for _, f := range files {
+		b, _ := os.ReadFile(f)
+		for _, line := range strings.Split(string(b), "\n") {
+			m := genRe.FindStringSubmatch(line)
+			if m == nil {
+				continue
+			}
+			rest := m[1]
+			out := ""
+			if mm := outDirRe.FindStringSubmatch(rest); mm != nil {
+				out = mm[1]
+			}
+			pkg := ""
+			if mm := goPkgRe.FindStringSubmatch(rest); mm != nil {
+				pkg = mm[1]
+			}
+			if mm := symbolRe.FindStringSubmatch(rest); mm != nil {
+				sym := mm[1]
+				entries = append(entries, entry{OutputDir: out, GoPkg: pkg, Symbol: sym})
+			}
+		}
+	}
+
+	if len(entries) == 0 {
+		fmt.Println("no bpf2go generate lines found, nothing to do")
+		return
+	}
+
+	byDir := map[string][]entry{}
+	for _, e := range entries {
+		dir := strings.Trim(e.OutputDir, `"'`)
+		byDir[dir] = append(byDir[dir], e)
+	}
+
+	keys := make([]string, 0, len(byDir))
+	for k := range byDir {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	modulePrefix := detectModulePath(root)
+	if modulePrefix == "" {
+		modulePrefix = "kmesh.net/kmesh"
+	}
+
+	sanitizeRe := regexp.MustCompile(`[^A-Za-z0-9_]`)
+	sanitize := func(s string) string {
+		out := sanitizeRe.ReplaceAllString(s, "_")
+		out = strings.Trim(out, "_")
+		if out == "" {
+			out = "pkg"
+		}
+		if out[0] >= '0' && out[0] <= '9' {
+			out = "pkg_" + out
+		}
+		return out
+	}
+
+	baseBpf2go := filepath.ToSlash(filepath.Join(root, "bpf", "kmesh", "bpf2go"))
+
+	var pkgsDefault []pkgInfo
+	var pkgsEnhanced []pkgInfo
+
+	for _, k := range keys {
+		list := byDir[k]
+		pattern := strings.Trim(k, `"'`)
+
+		var realDirs []string
+		if strings.ContainsAny(pattern, "*?[]") {
+			globFull := filepath.ToSlash(filepath.Join(baseBpf2go, pattern))
+			matches, _ := filepath.Glob(globFull)
+			for _, m := range matches {
+				if fi, err := os.Stat(m); err == nil && fi.IsDir() {
+					rel, err := filepath.Rel(baseBpf2go, m)
+					if err == nil {
+						realDirs = append(realDirs, filepath.ToSlash(rel))
+					}
+				}
+			}
+			if len(realDirs) == 0 {
+				realDirs = append(realDirs, pattern)
+			}
+		} else {
+			realDirs = append(realDirs, pattern)
+		}
+
+		for _, real := range realDirs {
+			if strings.Contains(real, "$ENHANCED_KERNEL") {
+				rdDefault := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "normal")
+				aliasDef := sanitize(rdDefault)
+				importDef := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdDefault))
+				pkgsDefault = append(pkgsDefault, pkgInfo{Alias: aliasDef, ImportPath: importDef, OutputDir: rdDefault, Entries: list})
+
+				rdEnh := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "enhanced")
+				aliasEnh := sanitize(rdEnh)
+				importEnh := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdEnh))
+				pkgsEnhanced = append(pkgsEnhanced, pkgInfo{Alias: aliasEnh, ImportPath: importEnh, OutputDir: rdEnh, Entries: list})
+			} else {
+				alias := sanitize(real)
+				importPath := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", real))
+				pi := pkgInfo{Alias: alias, ImportPath: importPath, OutputDir: real, Entries: list}
+				pkgsDefault = append(pkgsDefault, pi)
+				pkgsEnhanced = append(pkgsEnhanced, pi)
+			}
+		}
+	}
+
+	sort.Slice(pkgsDefault, func(i, j int) bool { return pkgsDefault[i].ImportPath < pkgsDefault[j].ImportPath })
+	sort.Slice(pkgsEnhanced, func(i, j int) bool { return pkgsEnhanced[i].ImportPath < pkgsEnhanced[j].ImportPath })
+
+	// build per-bucket symbol gens
+	knDefault := filterPkgsByPrefix(pkgsDefault, "kernelnative")
+	deDefault := filterPkgsByPrefix(pkgsDefault, "dualengine")
+	genDefault := filterPkgsByPrefix(pkgsDefault, "general")
+
+	knEnhanced := filterPkgsByPrefix(pkgsEnhanced, "kernelnative")
+
+	symsKnDefault := buildSymbolGen(knDefault)
+	symsDeDefault := buildSymbolGen(deDefault)
+	symsGenDefault := buildSymbolGen(genDefault)
+
+	symsKnEnhanced := buildSymbolGen(knEnhanced)
+
+	// prepare output dir
+	outDir := filepath.Join(root, "pkg", "bpf", "restart")
+	if err := os.MkdirAll(outDir, 0755); err != nil {
+		fmt.Fprintf(os.Stderr, "mkdir target dir: %v\n", err)
+		os.Exit(2)
+	}
+
+	// template
+	tplText := `/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+{{- range .Pkgs }}
+	{{ .Alias }} "{{ .ImportPath }}"
+{{- end }}
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+{{- range .SymsKn }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} has both normal and compat variants.
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	// Symbol {{ .BaseName }} only normal
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} only compat
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+
+{{- end }}
+	} else if config.DualEngineEnabled() {
+{{- range .SymsDe }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} has both normal and compat variants (dualengine).
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	// Symbol {{ .BaseName }} only normal (dualengine)
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} only compat (dualengine)
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+{{- end }}
+	}
+
+{{- range .SymsGen }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// General Symbol {{ .BaseName }} has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load General {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load General Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+{{- end }}
+
+	return specs, nil
+}
+`
+
+	funcMap := template.FuncMap{}
+	tpl, err := template.New("out").Funcs(funcMap).Parse(tplText)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "parse tpl: %v\n", err)
+		os.Exit(2)
+	}
+
+	// render default (non-enhanced) file
+	if err := renderVariant(tpl, pkgsDefault, symsKnDefault, symsDeDefault, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader.go"), "!enhanced"); err != nil {
+		fmt.Fprintf(os.Stderr, "write default: %v\n", err)
+		os.Exit(2)
+	}
+
+	// render enhanced file
+	if err := renderVariant(tpl, pkgsEnhanced, symsKnEnhanced, []symbolGen{}, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader_enhanced.go"), "enhanced"); err != nil {
+		fmt.Fprintf(os.Stderr, "write enhanced: %v\n", err)
+		os.Exit(2)
+	}
+}
+
+func collectGoFiles(root string) []string {
+	files := []string{}
+	_ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if fi.IsDir() {
+			if fi.Name() == "vendor" || strings.HasPrefix(path, filepath.Join(root, ".git")) {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+		if strings.HasSuffix(path, ".go") {
+			files = append(files, path)
+		}
+		return nil
+	})
+	return files
+}
+
+func detectModulePath(root string) string {
+	modf := filepath.Join(root, "go.mod")
+	b, err := os.ReadFile(modf)
+	if err != nil {
+		return ""
+	}
+	for _, line := range strings.Split(string(b), "\n") {
+		line = strings.TrimSpace(line)
+		if strings.HasPrefix(line, "module ") {
+			return strings.TrimSpace(strings.TrimPrefix(line, "module "))
+		}
+	}
+	return ""
+}
+
+func repoRoot() (string, error) {
+	dir, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	return dir, nil
+}
+
+func filterPkgsByPrefix(pkgs []pkgInfo, prefix string) []pkgInfo {
+	out := []pkgInfo{}
+	for _, p := range pkgs {
+		trim := strings.TrimPrefix(strings.TrimSpace(p.OutputDir), "/")
+		if strings.HasPrefix(trim, prefix) {
+			out = append(out, p)
+		}
+	}
+	sort.Slice(out, func(i, j int) bool { return out[i].ImportPath < out[j].ImportPath })
+	return out
+}
+
+func buildSymbolGen(pkgs []pkgInfo) []symbolGen {
+	m := map[string]*symbolGen{}
+	for _, p := range pkgs {
+		for _, e := range p.Entries {
+			sym := e.Symbol
+			base := sym
+			isCompat := false
+			if strings.HasSuffix(sym, "Compat") {
+				base = strings.TrimSuffix(sym, "Compat")
+				isCompat = true
+			}
+			if _, ok := m[base]; !ok {
+				m[base] = &symbolGen{BaseName: base}
+			}
+			sg := m[base]
+			if isCompat {
+				sg.CompatPkgAlias = p.Alias
+				sg.CompatSymbol = sym
+			} else {
+				sg.NormalPkgAlias = p.Alias
+				sg.NormalSymbol = sym
+			}
+		}
+	}
+	out := make([]symbolGen, 0, len(m))
+	for _, v := range m {
+		out = append(out, *v)
+	}
+	sort.Slice(out, func(i, j int) bool { return out[i].BaseName < out[j].BaseName })
+	return out
+}
+
+func renderVariant(tpl *template.Template, pkgs []pkgInfo, symsKn, symsDe, symsGen []symbolGen, outPath, buildTag string) error {
+	// combine pkgs into template data (unique by import path)
+	uniq := map[string]pkgInfo{}
+	for _, p := range pkgs {
+		uniq[p.ImportPath] = p
+	}
+	list := make([]pkgInfo, 0, len(uniq))
+	for _, v := range uniq {
+		list = append(list, v)
+	}
+	sort.Slice(list, func(i, j int) bool { return list[i].ImportPath < list[j].ImportPath })
+
+	buf := &bytes.Buffer{}
+	err := tpl.Execute(buf, map[string]interface{}{
+		"Pkgs":    list,
+		"SymsKn":  symsKn,
+		"SymsDe":  symsDe,
+		"SymsGen": symsGen,
+	})
+	if err != nil {
+		return fmt.Errorf("execute tpl: %w", err)
+	}
+
+	var out []byte
+	if buildTag != "" {
+		header := fmt.Sprintf("//go:build %s\n// +build %s\n\n", buildTag, buildTag)
+		out = append([]byte(header), buf.Bytes()...)
+	} else {
+		out = buf.Bytes()
+	}
+
+	src, err := format.Source(out)
+	if err != nil {
+		return fmt.Errorf("gofmt failed: %w\nraw output:\n%s\n", err, string(out))
+	}
+	if err := os.WriteFile(outPath, src, 0644); err != nil {
+		return fmt.Errorf("write out: %w", err)
+	}
+	return nil
+}
diff --git a/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h b/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
index 20c8c86b..f6569a51 100644
--- a/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
+++ b/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
@@ -27,10 +27,10 @@ enum kmesh_strncmp_type { STRNCMP_FAILED = 0, STRNCMP_PREFIX, STRNCMP_EXACT };
 
 #define KMESH_PROTO_TYPE_WIDTH (8)
 
-#define SET_RET_PROTO_TYPE(n, type) (n) = (((n)&0xff00) | ((u32)(type)&0xff))
-#define GET_RET_PROTO_TYPE(n)       ((n)&0xff)
+#define SET_RET_PROTO_TYPE(n, type) (n) = (((n) & 0xff00) | ((u32)(type) & 0xff))
+#define GET_RET_PROTO_TYPE(n)       ((n) & 0xff)
 
-#define SET_RET_MSG_TYPE(n, type) (n) = (((n)&0xff) | (((u32)(type)&0xff) << KMESH_PROTO_TYPE_WIDTH))
+#define SET_RET_MSG_TYPE(n, type) (n) = (((n) & 0xff) | (((u32)(type) & 0xff) << KMESH_PROTO_TYPE_WIDTH))
 #define GET_RET_MSG_TYPE(n)       (((n) >> KMESH_PROTO_TYPE_WIDTH) & 0xff)
 
 #define LOG(level, fmt, ...) printk(level "Kmesh_module: " fmt, ##__VA_ARGS__)
diff --git a/pkg/bpf/ads/sock_connection.go b/pkg/bpf/ads/sock_connection.go
index 2f72f6ba..abae9067 100644
--- a/pkg/bpf/ads/sock_connection.go
+++ b/pkg/bpf/ads/sock_connection.go
@@ -140,7 +140,7 @@ func (sc *BpfSockConn) Attach() error {
 	}
 
 	progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link)
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/ads/sock_ops.go b/pkg/bpf/ads/sock_ops.go
index 579638f5..d6fa974b 100644
--- a/pkg/bpf/ads/sock_ops.go
+++ b/pkg/bpf/ads/sock_ops.go
@@ -95,7 +95,7 @@ func (sc *BpfSockOps) Attach() error {
 
 	// pin bpf_link
 	progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link)
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/bpf.go b/pkg/bpf/bpf.go
index 76d8b2e3..b8ee93ac 100644
--- a/pkg/bpf/bpf.go
+++ b/pkg/bpf/bpf.go
@@ -141,7 +141,7 @@ func StopMda() error {
 func (l *BpfLoader) Stop() {
 	var err error
 	C.deserial_uninit()
-	if restart.GetExitType() == restart.Restart {
+	if restart.GetExitType() == restart.Restart || restart.GetExitType() == restart.Update {
 		return
 	}
 
@@ -194,8 +194,9 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map {
 	case restart.Restart:
 		return versionMap
 	case restart.Update:
-		// TODO : update mode has not been fully developed and is currently consistent with normal mode
-		log.Warnf("Update mode support is under development, Will be started in Normal mode.")
+		if updateVersionMap := restart.UpdateMapHandler(versionMap, versionPath, config); updateVersionMap != nil {
+			return updateVersionMap
+		}
 	default:
 	}
 
@@ -231,9 +232,19 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map {
 		return nil
 	}
 
+	mapspecs, err := restart.LoadCompileTimeSpecs(config)
+	if err != nil {
+		log.Errorf("failed to load compile time specs: %v", err)
+		return nil
+	}
+
 	storeVersionInfo(m)
 	log.Infof("kmesh start with Normal")
 	restart.SetStartType(restart.Normal)
+	if err := restart.SnapshotSpecsbyProg(mapspecs); err != nil {
+		log.Errorf("failed to store compile time specs: %v", err)
+		return nil
+	}
 	return m
 }
 
diff --git a/pkg/bpf/restart/bpf_update.go b/pkg/bpf/restart/bpf_update.go
new file mode 100644
index 00000000..19323fb4
--- /dev/null
+++ b/pkg/bpf/restart/bpf_update.go
@@ -0,0 +1,500 @@
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package restart
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/btf"
+
+	"kmesh.net/kmesh/daemon/options"
+	"kmesh.net/kmesh/pkg/version"
+)
+
+type StructFieldChanges struct {
+	FieldRemoved        bool // A field was present in the old struct (A) but is now missing in the new struct (B).
+	FieldAdded          bool // A new field was added to the struct.
+	FieldTypeChanged    bool // A field with the same name has a different type.
+	FieldOffsetChanged  bool // The memory offset of a field with the same name has changed.
+	NestedLayoutChanged bool // The layout of a nested struct has changed (e.g., its fields were added, removed, or changed).
+}
+
+type PersistedMemberLayout struct {
+	Name         string                 `json:"name"`
+	TypeName     string                 `json:"typeName"`
+	Offset       uint32                 `json:"offset"`
+	BitfieldSize uint32                 `json:"bitfieldsize"` // only have value when the type is bitfield
+	Nested       *PersistedStructLayout `json:"nested,omitempty"`
+}
+
+type PersistedStructLayout struct {
+	Name    string                  `json:"name"`
+	Members []PersistedMemberLayout `json:"members"`
+}
+
+type PersistedMapSpec struct {
+	Name            string                `json:"name"`
+	Type            string                `json:"type"` // MapType.String()
+	KeySize         uint32                `json:"keySize"`
+	ValueSize       uint32                `json:"valueSize"`
+	MaxEntries      uint32                `json:"maxEntries"`
+	Flags           uint32                `json:"flags"`
+	KeyStructInfo   PersistedStructLayout `json:"keyInfo"` // get from btf.Struct
+	ValueStructInfo PersistedStructLayout `json:"valueInfo"`
+}
+
+type PersistedSnapshot struct {
+	Maps map[string]map[string]PersistedMapSpec `json:"maps"`
+}
+
+const (
+	MapSpecDir      = "/mnt/kmesh_mapspecs"
+	MapSpecFilename = "mapspecs_by_prog.json"
+)
+
+// UpdateMapHandler handles the “Update” case in NewVersionMap.
+// It will migrate any BPF maps whose on‑disk pin already exists but whose
+// compiled MapSpec has changed
+func UpdateMapHandler(versionMap *ebpf.Map, kmBpfPath string, config *options.BpfConfig) *ebpf.Map {
+	persistedSpecs, err := LoadPersistedSnapshot()
+	if err != nil {
+		log.Errorf("load persisted map spec failed")
+		return nil
+	}
+	if persistedSpecs == nil {
+		log.Errorf("persisted map spec is nil")
+		return nil
+	}
+	specsbyProg, err := LoadCompileTimeSpecs(config)
+	if err != nil {
+		log.Errorf("load oldSpecsbyProg failed")
+		return nil
+	}
+	progNames := unionKeys(specsbyProg, persistedSpecs.Maps)
+	for _, progName := range progNames {
+		newMaps := specsbyProg[progName]
+		oldMaps := persistedSpecs.Maps[progName]
+		if newMaps == nil || oldMaps == nil {
+			continue
+		}
+
+		mapNames := unionKeys(newMaps, oldMaps)
+		for _, mapName := range mapNames {
+			newSpec, hasNew := newMaps[mapName]
+			oldSpec, hasOld := oldMaps[mapName]
+
+			pinPath := filepath.Join(kmBpfPath, mapName)
+
+			switch {
+			case !hasNew && hasOld: // clean up
+				oldMap, _ := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{})
+				if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) {
+					log.Warnf("failed to unpin old map: %v (continuing)", err)
+				}
+				if err := oldMap.Close(); err != nil {
+					log.Warnf("failed to close old map FD: %v (continuing)", err)
+				}
+				if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+					log.Warnf("failed to remove old map pinpath: %v (continuing)", err)
+				}
+			case hasNew && !hasOld:
+				if _, err := createEmptyMap(newSpec, pinPath, mapName, nil); err != nil {
+					log.Errorf("create new map %s/%s failed: %v", progName, mapName, err)
+				}
+			case hasNew && hasOld:
+				if _, err := migrateMap(&oldSpec, newSpec, progName, mapName, pinPath); err != nil {
+					log.Errorf("migrate map %s/%s failed: %v", progName, mapName, err)
+				}
+			}
+		}
+	}
+
+	log.Infof("kmesh start with Update")
+	updateVersionInfo(versionMap)
+	if err := SnapshotSpecsbyProg(specsbyProg); err != nil {
+		return versionMap
+	}
+	return versionMap
+}
+
+func updateVersionInfo(versionMap *ebpf.Map) {
+	key := uint32(0)
+	var value uint32
+	hash.Reset()
+	hash.Write([]byte(version.Get().GitVersion))
+	value = hash.Sum32()
+	if err := versionMap.Put(&key, &value); err != nil {
+		log.Errorf("update Version Map failed, err is %v", err)
+	}
+}
+
+func MigrateMap(
+	oldMapSpec *PersistedMapSpec,
+	newMapSpec *ebpf.MapSpec,
+	progName, mapName, pinPath string,
+) (*ebpf.Map, error) {
+	return migrateMap(oldMapSpec, newMapSpec, progName, mapName, pinPath)
+}
+
+func migrateMap(
+	oldMapSpec *PersistedMapSpec,
+	newMapSpec *ebpf.MapSpec,
+	progName, mapName, pinPath string,
+) (*ebpf.Map, error) {
+	if oldMapSpec == nil {
+		return createEmptyMap(newMapSpec, pinPath, mapName, nil)
+	}
+	if oldMapSpec.Type != newMapSpec.Type.String() ||
+		oldMapSpec.KeySize != newMapSpec.KeySize ||
+		oldMapSpec.ValueSize != newMapSpec.ValueSize ||
+		oldMapSpec.MaxEntries > newMapSpec.MaxEntries {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+
+	if needsRecreate(oldMapSpec.KeyStructInfo, newMapSpec.Key) {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+
+	if needsRecreate(oldMapSpec.ValueStructInfo, newMapSpec.Value) {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+	return nil, nil
+}
+
+func needsRecreate(oldStruct PersistedStructLayout, newType btf.Type) bool {
+	if newType == nil {
+		if oldStruct.Name != "" || len(oldStruct.Members) != 0 {
+			return true
+		}
+		return false
+	}
+
+	if newStruct, ok := newType.(*btf.Struct); ok {
+		diff := diffStructInfoAgainstBTF(oldStruct, newStruct, make(map[string]bool))
+		if diff.FieldAdded || diff.FieldRemoved || diff.FieldTypeChanged ||
+			diff.FieldOffsetChanged || diff.NestedLayoutChanged {
+			return true
+		}
+		return false
+	}
+
+	newTypeName := newType.TypeName()
+	if len(oldStruct.Members) != 0 {
+		return true
+	}
+	if oldStruct.Name == "" {
+		return true
+	}
+	if oldStruct.Name != newTypeName {
+		return true
+	}
+	return false
+}
+
+func createEmptyMapWithPinnedMap(spec *ebpf.MapSpec, pinPath, mapName string) (*ebpf.Map, error) {
+	oldMap, err := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{})
+	if err != nil {
+		return createEmptyMap(spec, pinPath, mapName, nil)
+	}
+	return createEmptyMap(spec, pinPath, mapName, oldMap)
+}
+
+func DiffStructInfoAgainstBTF(
+	a PersistedStructLayout,
+	b *btf.Struct,
+	visited map[string]bool,
+) StructFieldChanges {
+	return diffStructInfoAgainstBTF(a, b, visited)
+}
+
+func diffStructInfoAgainstBTF(
+	a PersistedStructLayout,
+	b *btf.Struct,
+	visited map[string]bool,
+) StructFieldChanges {
+	diff := StructFieldChanges{}
+
+	oldMap := make(map[string]PersistedMemberLayout, len(a.Members))
+	for _, m := range a.Members {
+		oldMap[m.Name] = m
+	}
+	newMap := make(map[string]btf.Member, len(b.Members))
+	for _, m := range b.Members {
+		newMap[m.Name] = m
+	}
+
+	// check added fields (present in new but not in old)
+	for name := range newMap {
+		if _, ok := oldMap[name]; !ok {
+			diff.FieldAdded = true
+			break
+		}
+	}
+
+	// check removed / type / offset / nested changes
+	for name, map_old := range oldMap {
+		map_new, exists := newMap[name]
+		if !exists {
+			diff.FieldRemoved = true
+			break
+		}
+
+		if map_old.Offset != uint32(map_new.Offset) || map_old.BitfieldSize != uint32(map_new.BitfieldSize) {
+			diff.FieldOffsetChanged = true
+			break
+		}
+
+		if map_old.TypeName == map_new.Type.TypeName() {
+			if mbStruct, ok := map_new.Type.(*btf.Struct); ok {
+				if map_old.Nested != nil {
+					if map_old.Nested.Name != "" {
+						if !visited[map_old.Nested.Name] {
+							visited[map_old.Nested.Name] = true
+							nestedDiff := diffStructInfoAgainstBTF(*map_old.Nested, mbStruct, visited)
+							if nestedDiff.FieldAdded || nestedDiff.FieldRemoved || nestedDiff.FieldTypeChanged ||
+								nestedDiff.FieldOffsetChanged || nestedDiff.NestedLayoutChanged {
+								diff.NestedLayoutChanged = true
+								break
+							}
+						}
+						continue
+					}
+				}
+				diff.NestedLayoutChanged = true
+				break
+			} else { // if new side is not struct
+				log.Info(map_old.Nested != nil)
+				if map_old.TypeName != map_new.Type.TypeName() || map_old.Nested != nil {
+					diff.FieldTypeChanged = true
+					break
+				}
+			}
+		} else {
+			diff.FieldTypeChanged = true
+			break
+		}
+	}
+	return diff
+}
+
+func createEmptyMap(spec *ebpf.MapSpec, pinPath string, mapName string, oldMap *ebpf.Map) (*ebpf.Map, error) {
+	if oldMap == nil {
+		if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+			return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err)
+		}
+		m, err := ebpf.NewMap(spec)
+		if err != nil {
+			return nil, fmt.Errorf("new map %s: %w", mapName, err)
+		}
+		if err := os.MkdirAll(filepath.Dir(pinPath), syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR|syscall.S_IRGRP|syscall.S_IXGRP); err != nil && !os.IsExist(err) {
+			return nil, fmt.Errorf("mkdir %s: %w", filepath.Dir(pinPath), err)
+		}
+		if err := m.Pin(pinPath); err != nil {
+			return nil, fmt.Errorf("pin empty map %s: %w", pinPath, err)
+		}
+		return m, nil
+	}
+
+	tmpPinPath := pinPath + ".tmp"
+	m, err := ebpf.NewMap(spec)
+	if err != nil {
+		return nil, fmt.Errorf("new map %s: %w", mapName, err)
+	}
+	if err := os.MkdirAll(filepath.Dir(tmpPinPath), 0755); err != nil && !os.IsExist(err) {
+		m.Close()
+		return nil, fmt.Errorf("mkdir %s: %w", tmpPinPath, err)
+	}
+	if err := m.Pin(tmpPinPath); err != nil {
+		m.Close()
+		return nil, fmt.Errorf("pin tmp map %s: %w", tmpPinPath, err)
+	}
+	if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) {
+		log.Warnf("failed to unpin old map %s: %v (continuing)", pinPath, err)
+	}
+	if err := oldMap.Close(); err != nil {
+		log.Warnf("failed to close old map FD: %v (continuing)", err)
+	}
+	if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+		m.Close()
+		return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err)
+	}
+	if err := os.Rename(tmpPinPath, pinPath); err != nil {
+		m.Close()
+		return nil, fmt.Errorf("rename tmp %s to %s failed: %w", tmpPinPath, pinPath, err)
+	}
+	return m, nil
+}
+
+// This function correctly handles two maps with different value types.
+func unionKeys[V1, V2 any](map1 map[string]V1, map2 map[string]V2) []string {
+	// Pre-allocate the set with a reasonable capacity.
+	set := make(map[string]struct{}, len(map1))
+
+	for k := range map1 {
+		set[k] = struct{}{}
+	}
+
+	for k := range map2 {
+		set[k] = struct{}{}
+	}
+
+	keys := make([]string, 0, len(set))
+	for k := range set {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+func buildStructInfoRecursive(t btf.Type, registry map[string]PersistedStructLayout, visited map[btf.Type]bool) PersistedStructLayout {
+	if t == nil {
+		return PersistedStructLayout{}
+	}
+
+	st, ok := t.(*btf.Struct)
+	if !ok {
+		return PersistedStructLayout{Name: t.TypeName()}
+	}
+
+	if existing, ok := registry[st.Name]; ok {
+		return existing
+	}
+
+	if visited[t] {
+		return PersistedStructLayout{Name: st.Name}
+	}
+
+	visited[t] = true
+	si := PersistedStructLayout{
+		Name:    st.Name,
+		Members: make([]PersistedMemberLayout, 0, len(st.Members)),
+	}
+
+	for _, m := range st.Members {
+		offBytes := uint32(m.Offset)
+		sizeBytes := uint32(m.BitfieldSize)
+		log.Info("sizeBytes", sizeBytes)
+		mi := PersistedMemberLayout{
+			Name:         m.Name,
+			TypeName:     m.Type.TypeName(),
+			Offset:       offBytes,
+			BitfieldSize: sizeBytes,
+			Nested:       nil,
+		}
+
+		if nested, ok := m.Type.(*btf.Struct); ok {
+			nestedInfo := buildStructInfoRecursive(nested, registry, visited)
+			mi.Nested = &nestedInfo
+			registry[nestedInfo.Name] = nestedInfo
+		}
+
+		si.Members = append(si.Members, mi)
+	}
+	registry[si.Name] = si
+	return si
+}
+
+// SnapshotSpecsbyProg takes a nested map of BPF map specifications and persists them to a file.
+// The structure of the input map `specsbyProg` is:
+//
+//		map[programName] -> map[mapName] -> *ebpf.MapSpec
+//
+//	  - The first key (programName) is the name of the BPF program collection,
+//	    e.g., "KmeshCgroupSock".
+//	  - The second key (mapName) is the name of a specific BPF map within that program,
+//	    e.g., "kmesh_endpoints".
+func SnapshotSpecsbyProg(specsbyProg map[string]map[string]*ebpf.MapSpec) error {
+	wrapper := make(map[string]map[string]PersistedMapSpec, len(specsbyProg))
+	registry := make(map[string]PersistedStructLayout)
+	visited := make(map[btf.Type]bool)
+
+	for prog, maps := range specsbyProg {
+		wrapper[prog] = make(map[string]PersistedMapSpec, len(maps))
+		for name, ms := range maps {
+			if ms == nil {
+				continue
+			}
+
+			var keyInfo PersistedStructLayout
+			if ms.Key != nil {
+				keyInfo = buildStructInfoRecursive(ms.Key, registry, visited)
+			} else {
+				keyInfo = PersistedStructLayout{Name: ""}
+			}
+
+			var valueInfo PersistedStructLayout
+			if ms.Value != nil {
+				valueInfo = buildStructInfoRecursive(ms.Value, registry, visited)
+			} else {
+				valueInfo = PersistedStructLayout{Name: ""}
+			}
+
+			pms := PersistedMapSpec{
+				Name:            ms.Name,
+				Type:            ms.Type.String(),
+				KeySize:         ms.KeySize,
+				ValueSize:       ms.ValueSize,
+				MaxEntries:      ms.MaxEntries,
+				Flags:           ms.Flags,
+				KeyStructInfo:   keyInfo,
+				ValueStructInfo: valueInfo,
+			}
+			wrapper[prog][name] = pms
+		}
+	}
+
+	snapshot := PersistedSnapshot{
+		Maps: wrapper,
+	}
+
+	if err := os.MkdirAll(MapSpecDir, 0755); err != nil {
+		return fmt.Errorf("mkdir specDir: %w", err)
+	}
+	data, err := json.MarshalIndent(snapshot, "", "  ")
+	if err != nil {
+		return fmt.Errorf("marshal snapshot: %w", err)
+	}
+	tmp := filepath.Join(MapSpecDir, MapSpecFilename+".tmp")
+	target := filepath.Join(MapSpecDir, MapSpecFilename)
+	if err := os.WriteFile(tmp, data, 0644); err != nil {
+		return fmt.Errorf("write tmp snapshot: %w", err)
+	}
+	if err := os.Rename(tmp, target); err != nil {
+		return fmt.Errorf("rename wrapper: %w", err)
+	}
+	return nil
+}
+
+func LoadPersistedSnapshot() (*PersistedSnapshot, error) {
+	path := filepath.Join(MapSpecDir, MapSpecFilename)
+	buf, err := os.ReadFile(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("read snapshot file: %w", err)
+	}
+	var snap PersistedSnapshot
+	if err := json.Unmarshal(buf, &snap); err != nil {
+		return nil, fmt.Errorf("unmarshal snapshot: %w", err)
+	}
+	return &snap, nil
+}
diff --git a/pkg/bpf/restart/new_version_mapspec_loader.go b/pkg/bpf/restart/new_version_mapspec_loader.go
new file mode 100644
index 00000000..7c9cb6f1
--- /dev/null
+++ b/pkg/bpf/restart/new_version_mapspec_loader.go
@@ -0,0 +1,171 @@
+//go:build !enhanced
+// +build !enhanced
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine"
+	general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general"
+	kernelnative_normal "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/normal"
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+		// Symbol KmeshCgroupSock has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_normal.LoadKmeshCgroupSockCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_normal.LoadKmeshCgroupSock(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err)
+			} else {
+				specs["KmeshCgroupSock"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockops has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_normal.LoadKmeshSockopsCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_normal.LoadKmeshSockops(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockops spec: %w", err)
+			} else {
+				specs["KmeshSockops"] = coll.Maps
+			}
+		}
+	} else if config.DualEngineEnabled() {
+		// Symbol KmeshCgroupSkb has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshCgroupSkbCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSkbCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSkbCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshCgroupSkb(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSkb spec: %w", err)
+			} else {
+				specs["KmeshCgroupSkb"] = coll.Maps
+			}
+		}
+		// Symbol KmeshCgroupSockWorkload has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshCgroupSockWorkloadCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockWorkloadCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockWorkloadCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshCgroupSockWorkload(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSockWorkload spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockWorkload"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSendmsg has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshSendmsgCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSendmsgCompat spec: %w", err)
+			} else {
+				specs["KmeshSendmsgCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshSendmsg(); err != nil {
+				return nil, fmt.Errorf("load KmeshSendmsg spec: %w", err)
+			} else {
+				specs["KmeshSendmsg"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockopsWorkload has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshSockopsWorkloadCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsWorkloadCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsWorkloadCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshSockopsWorkload(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockopsWorkload spec: %w", err)
+			} else {
+				specs["KmeshSockopsWorkload"] = coll.Maps
+			}
+		}
+		// Symbol KmeshXDPAuth has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshXDPAuthCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshXDPAuthCompat spec: %w", err)
+			} else {
+				specs["KmeshXDPAuthCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshXDPAuth(); err != nil {
+				return nil, fmt.Errorf("load KmeshXDPAuth spec: %w", err)
+			} else {
+				specs["KmeshXDPAuth"] = coll.Maps
+			}
+		}
+	}
+	// General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecrypt"] = coll.Maps
+		}
+	}
+	// General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncrypt"] = coll.Maps
+		}
+	}
+
+	return specs, nil
+}
diff --git a/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go
new file mode 100644
index 00000000..c0c1a4fd
--- /dev/null
+++ b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go
@@ -0,0 +1,101 @@
+//go:build enhanced
+// +build enhanced
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine"
+	general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general"
+	kernelnative_enhanced "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/enhanced"
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+		// Symbol KmeshCgroupSock has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_enhanced.LoadKmeshCgroupSockCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_enhanced.LoadKmeshCgroupSock(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err)
+			} else {
+				specs["KmeshCgroupSock"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockops has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_enhanced.LoadKmeshSockopsCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_enhanced.LoadKmeshSockops(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockops spec: %w", err)
+			} else {
+				specs["KmeshSockops"] = coll.Maps
+			}
+		}
+	} else if config.DualEngineEnabled() {
+	}
+	// General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecrypt"] = coll.Maps
+		}
+	}
+	// General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncrypt"] = coll.Maps
+		}
+	}
+
+	return specs, nil
+}
diff --git a/pkg/bpf/workload/skb.go b/pkg/bpf/workload/skb.go
index 88e1b180..3db97981 100644
--- a/pkg/bpf/workload/skb.go
+++ b/pkg/bpf/workload/skb.go
@@ -122,7 +122,7 @@ func (cs *BpfCroupSkbWorkload) Attach() error {
 	}
 	pinPathIn := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_ingress_prog")
 	pinPathEg := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_egress_prog")
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if cs.Link, err = utils.BpfProgUpdate(pinPathIn, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/workload/sock_connection.go b/pkg/bpf/workload/sock_connection.go
index ea254c73..fdbe316b 100644
--- a/pkg/bpf/workload/sock_connection.go
+++ b/pkg/bpf/workload/sock_connection.go
@@ -169,7 +169,7 @@ func (sc *SockConnWorkload) Attach() error {
 	pinPathDns4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_prog")
 	pinPathDnsRecv4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_recv_prog")
 
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(pinPath4, cgopt4); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/workload/sock_ops.go b/pkg/bpf/workload/sock_ops.go
index b6211b72..02ff479d 100644
--- a/pkg/bpf/workload/sock_ops.go
+++ b/pkg/bpf/workload/sock_ops.go
@@ -112,7 +112,7 @@ func (so *BpfSockOpsWorkload) Attach() error {
 	}
 	pinPath := filepath.Join(so.Info.BpfFsPath, "cgroup_sockops_prog")
 
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if so.Link, err = utils.BpfProgUpdate(pinPath, cgopt); err != nil {
 			return err
 		}



Test patch:
diff --git a/pkg/bpf/bpf_test.go b/pkg/bpf/bpf_test.go
index eb46e4f6..0f78fe15 100644
--- a/pkg/bpf/bpf_test.go
+++ b/pkg/bpf/bpf_test.go
@@ -27,6 +27,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/btf"
+
 	"kmesh.net/kmesh/daemon/options"
 	"kmesh.net/kmesh/pkg/bpf/factory"
 	"kmesh.net/kmesh/pkg/bpf/restart"
@@ -246,3 +249,295 @@ func Test_getNodeIPAddress(t *testing.T) {
 		})
 	}
 }
+
+// helper: build a simple btf.Int without relying on encoding constants
+func intType(name string, sizeBytes int) *btf.Int {
+	return &btf.Int{
+		Name: name,
+		Size: uint32(sizeBytes * 8),
+	}
+}
+
+// Test diffStructInfoAgainstBTF basic cases: FieldAdded / FieldRemoved / Offset / Nested
+func TestDiffStructInfoAgainstBTF_Basics(t *testing.T) {
+	// old StructInfo: one member "a"
+	old := restart.PersistedStructLayout{
+		Name: "S_old",
+		Members: []restart.PersistedMemberLayout{
+			{
+				Name:         "a",
+				TypeName:     "uint32",
+				Offset:       0, // we'll match against btf.Member.Offset below (no /8 used in current diff impl)
+				BitfieldSize: 0,
+			},
+		},
+	}
+
+	// New BTF struct: has "a" and new field "b" => FieldAdded == true
+	newWithAdded := &btf.Struct{
+		Name: "S_new_added",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(32),
+			},
+			{
+				Name:         "b",
+				Type:         intType("uint8", 1),
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	d := restart.DiffStructInfoAgainstBTF(old, newWithAdded, make(map[string]bool))
+	if !d.FieldAdded {
+		t.Fatalf("expected FieldAdded==true, got %+v", d)
+	}
+
+	// New BTF struct: missing "a" => FieldRemoved true
+	newRemoved := &btf.Struct{
+		Name: "S_new_removed",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	d = restart.DiffStructInfoAgainstBTF(old, newRemoved, make(map[string]bool))
+	if !d.FieldRemoved {
+		t.Fatalf("expected FieldRemoved==true, got %+v", d)
+	}
+
+	// Offset change: new has "a" but with different Offset
+	newOff := &btf.Struct{
+		Name: "S_new_off",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(8), // note: current diff code compares uint32(member.Offset) vs saved Offset
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	// To make the offset comparison hit, set old.Members[0].Offset to uint32(member.Offset)
+	oldOffsetMatch := old
+	oldOffsetMatch.Members[0].Offset = uint32(newOff.Members[0].Offset) // direct match -> no offset diff
+	d = restart.DiffStructInfoAgainstBTF(oldOffsetMatch, newOff, make(map[string]bool))
+	if d.FieldOffsetChanged {
+		t.Fatalf("did not expect FieldOffsetChanged when offsets match (got %+v)", d)
+	}
+	// Now set old offset to different value -> expect FieldOffsetChanged
+	oldOffsetMismatch := old
+	oldOffsetMismatch.Members[0].Offset = uint32(0)
+	d = restart.DiffStructInfoAgainstBTF(oldOffsetMismatch, newOff, make(map[string]bool))
+	if !d.FieldOffsetChanged {
+		t.Fatalf("expected FieldOffsetChanged true, got %+v", d)
+	}
+}
+
+func TestDiffStructInfoAgainstBTF_NestedIncompatible(t *testing.T) {
+	// old: inner { a: uint32 }, outer { x: inner, y: uint64 }
+	innerInt := intType("uint32", 4)
+	oldInner := &btf.Struct{
+		Name: "inner",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         innerInt,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	outerUint := intType("__u64", 8)
+	oldOuter := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         oldInner,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// persisted registry stores old definitions (StructInfo with Nested expanded)
+	registry := map[string]restart.PersistedStructLayout{
+		"inner": {
+			Name: "inner",
+			Members: []restart.PersistedMemberLayout{
+				{
+					Name:         "a",
+					TypeName:     "uint32",
+					Offset:       uint32(oldInner.Members[0].Offset), // use raw bit value as in your diff impl
+					BitfieldSize: 0,
+				},
+			},
+		},
+		"outer": {
+			Name: "outer",
+			Members: []restart.PersistedMemberLayout{
+				{
+					Name:         "x",
+					TypeName:     "inner",
+					Offset:       uint32(oldOuter.Members[0].Offset),
+					BitfieldSize: 0,
+					Nested: &restart.PersistedStructLayout{
+						Name: "inner",
+						Members: []restart.PersistedMemberLayout{
+							{Name: "a", TypeName: "uint32", Offset: uint32(oldInner.Members[0].Offset), BitfieldSize: 0},
+						},
+					},
+				},
+				{
+					Name:         "y",
+					TypeName:     "__u64",
+					Offset:       uint32(oldOuter.Members[1].Offset),
+					BitfieldSize: 0,
+				},
+			},
+		},
+	}
+
+	// new: inner_changed { b: uint32 }  <-- note: field name changed (a -> b)
+	newInnerChanged := &btf.Struct{
+		Name: "inner_changed",
+		Members: []btf.Member{
+			{
+				Name:         "b", // different name -> incompatible
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	// new outer uses this new inner_changed type
+	newOuter := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         newInnerChanged,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// Compare persisted outer (StructInfo) against newOuter (btf.Struct).
+	diff := restart.DiffStructInfoAgainstBTF(registry["outer"], newOuter, make(map[string]bool))
+
+	// Expect nested change (because inner's member name changed a->b)
+	if !diff.NestedLayoutChanged && !diff.FieldTypeChanged && !diff.FieldRemoved && !diff.FieldAdded {
+		t.Fatalf("expected incompatibility detected (NestedLayoutChanged/FieldTypeChanged/FieldAdded/FieldRemoved), got %#v", diff)
+	}
+}
+
+// Test nested struct comparisons and compatibility path in migrateMap
+func TestDiffStructInfoAgainstBTF_NestedAndMigrateMap_Compatible(t *testing.T) {
+	// Build nested btf structs: inner { a:uint32 }, outer { x:inner, y:uint64 }
+	innerInt := intType("uint32", 4)
+	inner := &btf.Struct{
+		Name: "inner",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         innerInt,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	outerUint := intType("__u64", 8)
+	outer := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         inner,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// persisted StructInfo registry: includes both inner and outer
+	registry := map[string]restart.PersistedStructLayout{
+		"inner": {
+			Name: "inner",
+			Members: []restart.PersistedMemberLayout{
+				{Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0},
+			},
+		},
+		"outer": {
+			Name: "outer",
+			Members: []restart.PersistedMemberLayout{
+				{Name: "x", TypeName: "inner", Offset: uint32(outer.Members[0].Offset), BitfieldSize: 0, Nested: &restart.PersistedStructLayout{
+					Name: "inner",
+					Members: []restart.PersistedMemberLayout{
+						{Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0},
+					},
+				}},
+				{Name: "y", TypeName: "__u64", Offset: uint32(outer.Members[1].Offset), BitfieldSize: 0},
+			},
+		},
+	}
+
+	// persisted map spec using outer
+	oldMapSpec := restart.PersistedMapSpec{
+		Name:            "km_nested_map",
+		Type:            ebpf.Hash.String(),
+		KeySize:         4,
+		ValueSize:       16,
+		MaxEntries:      128,
+		KeyStructInfo:   restart.PersistedStructLayout{Name: "int", Members: nil},
+		ValueStructInfo: registry["outer"],
+	}
+
+	// new compiled MapSpec that uses the same outer struct
+	newMapSpec := &ebpf.MapSpec{
+		Name:       "km_nested_map",
+		Type:       ebpf.Hash,
+		KeySize:    4,
+		ValueSize:  16,
+		MaxEntries: 128,
+		Key:        intType("int", 4), // key type non-struct in this test
+		Value:      outer,
+	}
+
+	// Call migrateMap: because the persisted value layout matches newMapSpec.Value,
+	// migrateMap should consider them compatible and return (nil, nil) (no creation).
+	m, err := restart.MigrateMap(&oldMapSpec, newMapSpec, "pkg", "mapNested", filepath.Join(t.TempDir(), "mapping"))
+	if err != nil {
+		t.Fatalf("migrateMap returned unexpected error: %v", err)
+	}
+	if m != nil {
+		t.Fatalf("expected nil map (reuse existing), got non-nil: %v", m)
+	}
+}
diff --git a/test/e2e/authorization_test.go b/test/e2e/authorization_test.go
new file mode 100644
index 00000000..e03eb640
--- /dev/null
+++ b/test/e2e/authorization_test.go
@@ -0,0 +1,647 @@
+//go:build integ
+// +build integ
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// NOTE: THE CODE IN THIS FILE IS MAINLY REFERENCED FROM ISTIO INTEGRATION
+// FRAMEWORK(https://github.com/istio/istio/tree/master/tests/integration)
+// AND ADAPTED FOR KMESH.
+
+package kmesh
+
+import (
+	"fmt"
+	"istio.io/istio/pkg/test/echo/common/scheme"
+	"istio.io/istio/pkg/test/framework"
+	"os/exec"
+	"testing"
+	"time"
+
+	"istio.io/istio/pkg/test/framework/components/echo"
+	"istio.io/istio/pkg/test/framework/components/echo/check"
+)
+
+// Wait for all the XDP programs of dst to load
+func waitForXDPOnDstWorkloads(t framework.TestContext, dst echo.Instances) {
+	count := 0
+	workloads := dst.WorkloadsOrFail(t)
+	namespace := apps.Namespace.Name()
+	for _, client := range workloads {
+		if count == len(workloads) {
+			break
+		}
+		podName := client.PodName()
+		timeout := time.After(5 * time.Second)
+		ticker := time.NewTicker(500 * time.Millisecond)
+		defer ticker.Stop()
+	InnerLoop:
+		for {
+			select {
+			case <-timeout:
+				t.Fatalf("Timeout: XDP eBPF program not found on pod %s", podName)
+			case <-ticker.C:
+				cmd := exec.Command("kubectl", "exec", "-n", namespace, podName, "--", "sh", "-c", "ip a | grep xdp")
+				output, err := cmd.CombinedOutput()
+				if err == nil && len(output) > 0 {
+					t.Logf("XDP program is loaded on pod %s", podName)
+					count++
+					break InnerLoop
+				}
+				t.Logf("Waiting for XDP program to load on pod %s: %v", podName, err)
+			}
+		}
+	}
+}
+
+func TestIPAuthorization(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("IP Authorization").Run(func(t framework.TestContext) {
+
+			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
+				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
+			}
+			src := apps.ServiceWithWaypointAtServiceGranularity[0]
+
+			clients := src.WorkloadsOrFail(t)
+			dst := apps.EnrolledToKmesh
+
+			addresses := clients.Addresses()
+			if len(addresses) < 2 {
+				t.Fatal(fmt.Errorf("need at least 2 clients"))
+			}
+			selectedAddress := addresses[0]
+
+			authzCases := []struct {
+				name string
+				spec string
+			}{
+				{
+					name: "allow",
+					spec: `
+  action: ALLOW
+`,
+				},
+				{
+					name: "deny",
+					spec: `
+  action: DENY
+`,
+				},
+			}
+
+			chooseChecker := func(action string, ip string) echo.Checker {
+				switch action {
+				case "allow":
+					if ip != selectedAddress {
+						return check.NotOK()
+					} else {
+						return check.OK()
+					}
+				case "deny":
+					if ip != selectedAddress {
+						return check.OK()
+					} else {
+						return check.NotOK()
+					}
+				default:
+					t.Fatal("invalid action")
+				}
+
+				return check.OK()
+			}
+
+			waitForXDPOnDstWorkloads(t, dst)
+
+			for _, tc := range authzCases {
+				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+					"Destination": dst.Config().Service,
+					"Ip":          selectedAddress,
+				}, `apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: ip-policy
+spec:
+  selector:
+    matchLabels:
+      app: "{{.Destination}}"
+`+tc.spec+`
+  rules:
+  - from:
+    - source:
+        ipBlocks:
+        - "{{.Ip}}"
+`).ApplyOrFail(t)
+
+				for _, client := range clients {
+					opt := echo.CallOptions{
+						To:                      dst,
+						Port:                    echo.Port{Name: "tcp"},
+						Scheme:                  scheme.TCP,
+						NewConnectionPerRequest: true,
+						// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
+						Timeout: time.Minute * 2,
+					}
+
+					var name string
+					if client.Address() != selectedAddress {
+						name = tc.name + ", not selected address"
+					} else {
+						name = tc.name + ", selected address"
+					}
+
+					opt.Check = chooseChecker(tc.name, client.Address())
+
+					t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
+						src.WithWorkloads(client).CallOrFail(t, opt)
+					})
+				}
+			}
+		})
+	})
+}
+
+func TestPortAuthorization(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("Port Authorization").Run(func(t framework.TestContext) {
+
+			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
+				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
+			}
+			src := apps.ServiceWithWaypointAtServiceGranularity[0]
+
+			clients := src.WorkloadsOrFail(t)
+			client := clients[0]
+			dst := apps.EnrolledToKmesh
+
+			// Define the test port
+			selectedPodPort := 19090
+			selectedServicePort := 9090
+			notSelectedPodPort := 16060
+			notSelectedServicePort := 9091
+
+			// Echo Pod healthy port
+			readyPort := 8080
+			livenessPort := 3333
+
+			authzCases := []struct {
+				name  string
+				spec  string
+				ports string
+			}{
+				{
+					name:  "allow",
+					spec:  `  action: ALLOW`,
+					ports: fmt.Sprintf(`["%d", "%d", "%d"]`, selectedPodPort, readyPort, livenessPort),
+				},
+				{
+					name:  "deny",
+					spec:  `  action: DENY`,
+					ports: fmt.Sprintf(`["%d"]`, selectedPodPort),
+				},
+			}
+
+			chooseChecker := func(action string, servicePort int) echo.Checker {
+				switch action {
+				case "allow":
+					if servicePort == selectedServicePort {
+						return check.OK()
+					} else {
+						return check.NotOK()
+					}
+				case "deny":
+					if servicePort == selectedServicePort {
+						return check.NotOK()
+					} else {
+						return check.OK()
+					}
+				default:
+					t.Fatal("invalid action")
+				}
+
+				return check.OK()
+			}
+
+			portTestCases := []struct {
+				servicePort int
+				podPort     int
+				description string
+			}{
+				{
+					servicePort: selectedServicePort,
+					podPort:     selectedPodPort,
+					description: fmt.Sprintf("service port %d (pod port %d)", selectedServicePort, selectedPodPort),
+				},
+				{
+					servicePort: notSelectedServicePort,
+					podPort:     notSelectedPodPort,
+					description: fmt.Sprintf("service port %d (pod port %d)", notSelectedServicePort, notSelectedPodPort),
+				},
+			}
+
+			waitForXDPOnDstWorkloads(t, dst)
+
+			for _, tc := range authzCases {
+				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+					"Destination": dst.Config().Service,
+					"Ports":       tc.ports,
+				}, `apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: port-policy
+spec:
+  selector:
+    matchLabels:
+      app: "{{.Destination}}"
+`+tc.spec+`
+  rules:
+  - to:
+    - operation:
+        ports: {{.Ports}}
+`).ApplyOrFail(t)
+
+				for _, portTest := range portTestCases {
+					opt := echo.CallOptions{
+						To:                      dst,
+						Port:                    echo.Port{Name: "tcp", ServicePort: portTest.servicePort},
+						Scheme:                  scheme.TCP,
+						NewConnectionPerRequest: true,
+						// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
+						Timeout: time.Minute * 2,
+					}
+
+					var name string
+					name = tc.name + ", " + portTest.description
+
+					opt.Check = chooseChecker(tc.name, portTest.servicePort)
+
+					t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
+						src.WithWorkloads(client).CallOrFail(t, opt)
+					})
+				}
+			}
+		})
+	})
+}
+
+func TestNamespaceAuthorization(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("Namespace Authorization").Run(func(t framework.TestContext) {
+
+			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
+				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
+			}
+			src := apps.ServiceWithWaypointAtServiceGranularity[0]
+
+			clients := src.WorkloadsOrFail(t)
+			client := clients[0]
+			dst := apps.EnrolledToKmesh
+
+			// get the namespace of the apps
+			selectedNamespace := apps.Namespace.Name()
+
+			authzCases := []struct {
+				name string
+				spec string
+			}{
+				{
+					name: "allow",
+					spec: `
+  action: ALLOW
+`,
+				},
+				{
+					name: "deny",
+					spec: `
+  action: DENY
+`,
+				},
+			}
+
+			chooseChecker := func(action string) echo.Checker {
+				switch action {
+				case "allow":
+					return check.OK()
+				case "deny":
+					return check.NotOK()
+				default:
+					t.Fatal("invalid action")
+				}
+
+				return check.OK()
+			}
+
+			waitForXDPOnDstWorkloads(t, dst)
+
+			for _, tc := range authzCases {
+				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+					"Destination":     dst.Config().Service,
+					"SourceNamespace": selectedNamespace,
+				}, `apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: namespace-policy
+spec:
+  selector:
+    matchLabels:
+      app: "{{.Destination}}"
+`+tc.spec+`
+  rules:
+  - from:
+    - source:
+        namespaces:
+        - "{{.SourceNamespace}}"
+`).ApplyOrFail(t)
+
+				opt := echo.CallOptions{
+					To:                      dst,
+					Port:                    echo.Port{Name: "tcp"},
+					Scheme:                  scheme.TCP,
+					NewConnectionPerRequest: true,
+					// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
+					Timeout: time.Minute * 2,
+				}
+
+				var name string
+				name = tc.name + ", namespace " + selectedNamespace
+
+				opt.Check = chooseChecker(tc.name)
+
+				t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
+					src.WithWorkloads(client).CallOrFail(t, opt)
+				})
+
+			}
+		})
+	})
+}
+
+func TestHeaderAuthorization(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("Header Authorization").Run(func(t framework.TestContext) {
+
+			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
+				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
+			}
+			src := apps.EnrolledToKmesh[0]
+
+			clients := src.WorkloadsOrFail(t)
+			client := clients[0]
+			dst := apps.ServiceWithWaypointAtServiceGranularity
+
+			// Define the test header and ports - use pod port
+			selectedHeaderName := "x-api-key"
+			selectedHeaderValue := "secret-token"
+			notSelectedHeaderValue := "wrong-token"
+			targetHttpServicePort := 80 // Target HTTP Service port
+
+			authzCases := []struct {
+				name string
+				spec string
+			}{
+				{
+					name: "allow",
+					spec: `
+  action: ALLOW
+`,
+				},
+				{
+					name: "deny",
+					spec: `
+  action: DENY
+`,
+				},
+			}
+
+			chooseChecker := func(action string, headerMatches bool) echo.Checker {
+				switch action {
+				case "allow":
+					if !headerMatches {
+						return check.NotOK()
+					} else {
+						return check.OK()
+					}
+				case "deny":
+					if !headerMatches {
+						return check.OK()
+					} else {
+						return check.NotOK()
+					}
+				default:
+					t.Fatal("invalid action")
+				}
+
+				return check.OK()
+			}
+
+			headerTestCases := []struct {
+				headerValue string
+				matches     bool
+				description string
+			}{
+				{
+					headerValue: selectedHeaderValue,
+					matches:     true,
+					description: "matching header",
+				},
+				{
+					headerValue: notSelectedHeaderValue,
+					matches:     false,
+					description: "non-matching header",
+				},
+			}
+
+			waitForXDPOnDstWorkloads(t, dst)
+
+			for _, tc := range authzCases {
+
+				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+					"Destination": dst.Config().Service,
+					"HeaderName":  selectedHeaderName,
+					"HeaderValue": selectedHeaderValue,
+				}, `apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: header-policy
+spec:
+  targetRefs:
+  - kind: Service
+    group: ""
+    name: "{{.Destination}}"
+`+tc.spec+`
+  rules:
+  - when:
+    - key: request.headers[{{.HeaderName}}]
+      values: ["{{.HeaderValue}}"]
+`).ApplyOrFail(t)
+
+				for _, headerTest := range headerTestCases {
+					opt := echo.CallOptions{
+						To:     dst,
+						Port:   echo.Port{Name: "http", ServicePort: targetHttpServicePort},
+						Scheme: scheme.HTTP,
+						HTTP: echo.HTTP{
+							Path: "/api/test",
+							Headers: map[string][]string{
+								selectedHeaderName: {headerTest.headerValue},
+							},
+						},
+						NewConnectionPerRequest: true,
+						// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
+						Timeout: time.Minute * 2,
+					}
+
+					var name string
+					name = fmt.Sprintf("%s, %s on service port %d", tc.name, headerTest.description, targetHttpServicePort)
+
+					opt.Check = chooseChecker(tc.name, headerTest.matches)
+
+					t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
+						src.WithWorkloads(client).CallOrFail(t, opt)
+					})
+				}
+			}
+		})
+	})
+}
+
+func TestHostAuthorization(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("Host Authorization").Run(func(t framework.TestContext) {
+
+			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
+				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
+			}
+			src := apps.EnrolledToKmesh[0]
+
+			clients := src.WorkloadsOrFail(t)
+			client := clients[0]
+			dst := apps.ServiceWithWaypointAtServiceGranularity
+
+			// Define variables
+			selectedHost := "example.com"
+			notSelectedHost := "wrong.example.com"
+			targetHttpServicePort := 80
+
+			authzCases := []struct {
+				name string
+				spec string
+			}{
+				{
+					name: "allow",
+					spec: `
+  action: ALLOW
+`,
+				},
+				{
+					name: "deny",
+					spec: `
+  action: DENY
+`,
+				},
+			}
+
+			hostTestCases := []struct {
+				hostValue   string
+				matches     bool
+				description string
+			}{
+				{
+					hostValue:   selectedHost,
+					matches:     true,
+					description: "matching host",
+				},
+				{
+					hostValue:   notSelectedHost,
+					matches:     false,
+					description: "non-matching host",
+				},
+			}
+
+			chooseChecker := func(action string, hostMatches bool) echo.Checker {
+				switch action {
+				case "allow":
+					if !hostMatches {
+						return check.NotOK()
+					} else {
+						return check.OK()
+					}
+				case "deny":
+					if !hostMatches {
+						return check.OK()
+					} else {
+						return check.NotOK()
+					}
+				default:
+					t.Fatal("invalid action")
+				}
+
+				return check.OK()
+			}
+
+			waitForXDPOnDstWorkloads(t, dst)
+
+			for _, tc := range authzCases {
+
+				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+					"Destination": dst.Config().Service,
+					"TargetHost":  selectedHost,
+				}, `apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: host-policy
+spec:
+  targetRefs:
+  - kind: Service
+    group: ""
+    name: "{{.Destination}}"
+`+tc.spec+`
+  rules:
+  - to:
+    - operation:
+        hosts: ["{{.TargetHost}}"]
+`).ApplyOrFail(t)
+
+				for _, hostTest := range hostTestCases {
+					opt := echo.CallOptions{
+						To:     dst,
+						Port:   echo.Port{Name: "http", ServicePort: targetHttpServicePort},
+						Scheme: scheme.HTTP,
+						HTTP: echo.HTTP{
+							Path: "/api/test",
+							Headers: map[string][]string{
+								"Host": {hostTest.hostValue},
+							},
+						},
+						NewConnectionPerRequest: true,
+						// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
+						Timeout: time.Minute * 2,
+					}
+
+					var name string
+					name = tc.name + ", " + hostTest.description
+
+					opt.Check = chooseChecker(tc.name, hostTest.matches)
+
+					t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
+						src.WithWorkloads(client).CallOrFail(t, opt)
+					})
+				}
+
+			}
+		})
+	})
+}
diff --git a/test/e2e/baseline_test.go b/test/e2e/baseline_test.go
index ad5580b1..fd2e09f2 100644
--- a/test/e2e/baseline_test.go
+++ b/test/e2e/baseline_test.go
@@ -29,7 +29,6 @@ import (
 	"net"
 	"net/http"
 	"net/netip"
-	"os/exec"
 	"sort"
 	"strings"
 	"testing"
@@ -751,141 +750,6 @@ func TestMixNsAndServiceWaypoint(t *testing.T) {
 	})
 }
 
-func TestAuthorizationL4(t *testing.T) {
-	framework.NewTest(t).Run(func(t framework.TestContext) {
-		t.NewSubTest("L4 Authorization").Run(func(t framework.TestContext) {
-			// Enable authorizaiton offload to xdp.
-
-			if len(apps.ServiceWithWaypointAtServiceGranularity) == 0 {
-				t.Fatal(fmt.Errorf("need at least 1 instance of apps.ServiceWithWaypointAtServiceGranularity"))
-			}
-			src := apps.ServiceWithWaypointAtServiceGranularity[0]
-
-			clients := src.WorkloadsOrFail(t)
-			dst := apps.EnrolledToKmesh
-
-			addresses := clients.Addresses()
-			if len(addresses) < 2 {
-				t.Fatal(fmt.Errorf("need at least 2 clients"))
-			}
-			selectedAddress := addresses[0]
-
-			authzCases := []struct {
-				name string
-				spec string
-			}{
-				{
-					name: "allow",
-					spec: `
-  action: ALLOW
-`,
-				},
-				{
-					name: "deny",
-					spec: `
-  action: DENY
-`,
-				},
-			}
-
-			chooseChecker := func(action string, ip string) echo.Checker {
-				switch action {
-				case "allow":
-					if ip != selectedAddress {
-						return check.NotOK()
-					} else {
-						return check.OK()
-					}
-				case "deny":
-					if ip != selectedAddress {
-						return check.OK()
-					} else {
-						return check.NotOK()
-					}
-				default:
-					t.Fatal("invalid action")
-				}
-
-				return check.OK()
-			}
-
-			count := 0
-			workloads := dst.WorkloadsOrFail(t)
-			for _, client := range workloads {
-				if count == len(workloads) {
-					break
-				}
-				podName := client.PodName()
-				namespace := apps.Namespace.Name()
-				timeout := time.After(5 * time.Second)
-				ticker := time.NewTicker(500 * time.Millisecond)
-				defer ticker.Stop()
-			InnerLoop:
-				for {
-					select {
-					case <-timeout:
-						t.Fatalf("Timeout: XDP eBPF program not found on pod %s", podName)
-					case <-ticker.C:
-						cmd := exec.Command("kubectl", "exec", "-n", namespace, podName, "--", "sh", "-c", "ip a | grep xdp")
-						output, err := cmd.CombinedOutput()
-						if err == nil && len(output) > 0 {
-							t.Logf("XDP program is loaded on pod %s", podName)
-							count++
-							break InnerLoop
-						}
-						t.Logf("Waiting for XDP program to load on pod %s: %v", podName, err)
-					}
-				}
-			}
-
-			for _, tc := range authzCases {
-				t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
-					"Destination": dst.Config().Service,
-					"Ip":          selectedAddress,
-				}, `apiVersion: security.istio.io/v1beta1
-kind: AuthorizationPolicy
-metadata:
-  name: policy
-spec:
-  selector:
-    matchLabels:
-      app: "{{.Destination}}"
-`+tc.spec+`
-  rules:
-  - from:
-    - source:
-        ipBlocks:
-        - "{{.Ip}}"
-`).ApplyOrFail(t)
-
-				for _, client := range clients {
-					opt := echo.CallOptions{
-						To:                      dst,
-						Port:                    echo.Port{Name: "tcp"},
-						Scheme:                  scheme.TCP,
-						NewConnectionPerRequest: true,
-						// Due to the mechanism of Kmesh L4 authorization, we need to set the timeout slightly longer.
-						Timeout: time.Minute * 2,
-					}
-
-					var name string
-					if client.Address() != selectedAddress {
-						name = tc.name + ", not selected address"
-					} else {
-						name = tc.name + ", selected address"
-					}
-
-					opt.Check = chooseChecker(tc.name, client.Address())
-
-					t.NewSubTestf("%v", name).Run(func(t framework.TestContext) {
-						src.WithWorkloads(client).CallOrFail(t, opt)
-					})
-				}
-			}
-		})
-	})
-}
-
 func TestBookinfo(t *testing.T) {
 	framework.NewTest(t).Run(func(t framework.TestContext) {
 		namespace := apps.Namespace.Name()
diff --git a/test/e2e/ipsec_test.go b/test/e2e/ipsec_test.go
new file mode 100644
index 00000000..2121e43e
--- /dev/null
+++ b/test/e2e/ipsec_test.go
@@ -0,0 +1,549 @@
+//go:build integ
+// +build integ
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// NOTE: THE CODE IN THIS FILE IS MAINLY REFERENCED FROM ISTIO INTEGRATION
+// FRAMEWORK(https://github.com/istio/istio/tree/master/tests/integration)
+// AND ADAPTED FOR KMESH.
+package kmesh
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"encoding/hex"
+	"fmt"
+	"istio.io/istio/pkg/test/framework"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"os/exec"
+	"strings"
+	"testing"
+	"text/template"
+	"time"
+)
+
+type DeployParams struct {
+	Name          string
+	AppLabel      string
+	Image         string
+	NodeName      string
+	SvcPort       int
+	TargetPort    int
+	ContainerPort int
+}
+
+var sleepTmpl = `
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.Name}}
+  labels:
+    app: {{.AppLabel}}
+    service: {{.AppLabel}}
+spec:
+  ports:
+  - port: 80
+    name: http
+  selector:
+    app: {{.AppLabel}}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{.Name}}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{.AppLabel}}
+  template:
+    metadata:
+      labels:
+        app: {{.AppLabel}}
+    spec:
+      nodeName: {{.NodeName}}
+      containers:
+      - name: {{.Name}}
+        image: {{.Image}}
+        command: ["/bin/sleep", "infinity"]
+        imagePullPolicy: IfNotPresent
+`
+
+var echoTmpl = `
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.Name}}
+  namespace: default
+spec:
+  ports:
+  - name: http
+    port: {{.SvcPort}}
+    targetPort: {{.TargetPort}}
+  selector:
+    app: {{.AppLabel}}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{.Name}}
+  namespace: default
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{.AppLabel}}
+  template:
+    metadata:
+      labels:
+        app: {{.AppLabel}}
+    spec:
+      containers:
+      - name: {{.Name}}
+        image: {{.Image}}
+        imagePullPolicy: IfNotPresent
+        args:
+        - --port={{.ContainerPort}}
+        ports:
+        - containerPort: {{.ContainerPort}}
+`
+
+func labelNamespace(t framework.TestContext, namespace, key, value string) error {
+	var cmd *exec.Cmd
+	if value == "" {
+		cmd = exec.Command("kubectl", "label", "namespace", namespace, key+"-")
+	} else {
+		cmd = exec.Command("kubectl", "label", "namespace", namespace, key+"="+value, "--overwrite")
+	}
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("kubectl label namespace failed: %v\n%s", err, string(out))
+		return err
+	}
+	t.Logf("namespace %s label set: %s=%s", namespace, key, value)
+	return nil
+}
+
+func deployService(t framework.TestContext, tmplText string, params DeployParams) error {
+	tmpl, err := template.New("yaml").Parse(tmplText)
+	if err != nil {
+		t.Fatalf("failed to parse template: %v", err)
+		return err
+	}
+	var buf bytes.Buffer
+	if err := tmpl.Execute(&buf, params); err != nil {
+		t.Fatalf("failed to execute template: %v", err)
+		return err
+	}
+	cmd := exec.Command("kubectl", "apply", "-f", "-")
+	cmd.Stdin = &buf
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("kubectl apply failed: %v\n%s", err, string(out))
+		return err
+	}
+	t.Logf("applied resources for %s:\n%s", params.Name, string(out))
+	return nil
+}
+
+func deleteService(t framework.TestContext, name, namespace string) error {
+	t.Logf("Deleting deployment and service %s in namespace %s...", name, namespace)
+	cmd := exec.Command("kubectl", "delete", "deployment,service", name, "-n", namespace)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Logf("Failed to delete %s: %v\n%s", name, err, string(out))
+		return err
+	}
+	t.Logf("Deleted %s: %s", name, string(out))
+	return nil
+}
+
+func getPodNameAndIP(t framework.TestContext, namespace string, appLabel string) (string, string, error) {
+	client := t.Clusters().Default().Kube().CoreV1().Pods(namespace)
+	selector := "app=" + appLabel
+
+	for {
+		podList, err := client.List(context.TODO(), metav1.ListOptions{LabelSelector: selector})
+		if err != nil {
+			t.Logf("list pods failed: %v, retrying...", err)
+			time.Sleep(3 * time.Second)
+			continue
+		}
+		if len(podList.Items) == 0 {
+			t.Logf("no pods found for app=%s, retrying...", appLabel)
+			time.Sleep(3 * time.Second)
+			continue
+		}
+		for _, p := range podList.Items {
+			if p.Status.Phase == corev1.PodRunning && p.Status.PodIP != "" && p.Status.PodIP != "<none>" {
+				return p.Name, p.Status.PodIP, nil
+			}
+			t.Logf("pod %s status=%s ip=%s, waiting...", p.Name, p.Status.Phase, p.Status.PodIP)
+		}
+		time.Sleep(3 * time.Second)
+	}
+}
+
+func downloadTcpdump(t framework.TestContext, containerName string) error {
+	checkCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "which tcpdump")
+	out, err := checkCmd.CombinedOutput()
+	if err == nil && strings.TrimSpace(string(out)) != "" {
+		t.Logf("tcpdump is already installed in container: %s", containerName)
+		return nil
+	}
+	t.Logf("tcpdump not found, installing in container: %s", containerName)
+
+	updateCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "apt update")
+	out, err = updateCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("apt update failed: %v\n%s", err, string(out))
+		return err
+	}
+	t.Logf("apt update success")
+
+	installCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "apt install tcpdump -y")
+	out, err = installCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("apt install tcpdump failed: %v\n%s", err, string(out))
+		return err
+	}
+	t.Logf("apt install tcpdump success")
+	return nil
+}
+
+func checkIPSecRules(t framework.TestContext, containerName string) error {
+	stateCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "ip xfrm state")
+	stateOut, err := stateCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("failed to run ip xfrm state: %v\n%s", err, string(stateOut))
+		return err
+	}
+	t.Logf("ip xfrm state output:\n%s", string(stateOut))
+
+	policyCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "ip xfrm policy")
+	policyOut, err := policyCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("failed to run ip xfrm policy: %v\n%s", err, string(policyOut))
+		return err
+	}
+	t.Logf("ip xfrm policy output:\n%s", string(policyOut))
+
+	stateStr := string(stateOut)
+	policyStr := string(policyOut)
+
+	if !strings.Contains(stateStr, "proto esp spi") || !strings.Contains(stateStr, "mode tunnel") {
+		t.Fatalf("ip xfrm state output does not contain expected ESP tunnel rules")
+		return fmt.Errorf("ip xfrm state output does not contain expected ESP tunnel rules")
+	}
+	if !strings.Contains(stateStr, "aead rfc4106(gcm(aes))") {
+		t.Fatalf("ip xfrm state output does not contain expected AEAD algorithm")
+		return fmt.Errorf("ip xfrm state output does not contain expected AEAD algorithm")
+	}
+
+	if !strings.Contains(policyStr, "tmpl src") || !strings.Contains(policyStr, "proto esp") || !strings.Contains(policyStr, "mode tunnel") {
+		t.Fatalf("ip xfrm policy output does not contain expected tunnel template")
+		return fmt.Errorf("ip xfrm policy output does not contain expected tunnel template")
+	}
+	if !strings.Contains(policyStr, "dir out") || !strings.Contains(policyStr, "dir in") || !strings.Contains(policyStr, "dir fwd") {
+		t.Fatalf("ip xfrm policy output does not contain expected directions (out/in/fwd)")
+		return fmt.Errorf("ip xfrm policy output does not contain expected directions (out/in/fwd)")
+	}
+	if !strings.Contains(policyStr, "mark") {
+		t.Fatalf("ip xfrm policy output does not contain expected mark field")
+		return fmt.Errorf("ip xfrm policy output does not contain expected mark field")
+	}
+
+	t.Logf("IPSec xfrm state and policy outputs match expected format.")
+	return nil
+}
+
+func runTcpdumpESP(t framework.TestContext, containerName string, duration int) (string, error) {
+	cmd := exec.Command("docker", "exec", containerName, "bash", "-c",
+		fmt.Sprintf("timeout %d tcpdump -i any | grep ESP", duration))
+	out, err := cmd.CombinedOutput()
+	if err != nil && !strings.Contains(err.Error(), "exit status 124") {
+		t.Fatalf("tcpdump failed: %v\n%s", err, string(out))
+		return "", err
+	}
+	return string(out), nil
+}
+
+func curlFromSleepToEcho(t framework.TestContext, sleepPod, echoIP string, echoPort int) (string, error) {
+	t.Logf("Curling from sleep pod %s to echo pod IP %s...", sleepPod, echoIP)
+	var url string
+	if strings.Contains(echoIP, ":") {
+		// IPv6 address
+		url = fmt.Sprintf("http://[%s]:%d", echoIP, echoPort)
+	} else {
+		// IPv4 address
+		url = fmt.Sprintf("http://%s:%d", echoIP, echoPort)
+	}
+	cmd := exec.Command("kubectl", "exec", sleepPod, "--", "curl", url)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("curl failed: %v\n%s", err, string(out))
+		return "", err
+	}
+	return string(out), nil
+}
+
+func genRandomKeyHex() string {
+	b := make([]byte, 36)
+	_, err := rand.Read(b)
+	if err != nil {
+		panic("failed to generate random key")
+	}
+	return hex.EncodeToString(b)
+}
+
+func rotateIPSecKeyAndWait(t framework.TestContext, containerName string, intervalSec int) error {
+	t.Logf("Rotating IPSec key with kmeshctl secret ...")
+	keyHex := genRandomKeyHex()
+	rotateCmd := exec.Command("kmeshctl", "secret", "create", "--key="+keyHex)
+	out, err := rotateCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("Failed to rotate IPSec key: %v\n%s", err, string(out))
+		return err
+	}
+	t.Logf("kmeshctl secret update success")
+
+	t.Logf("Waiting for SPI 0x00000002 to appear in both state and policy (interval %ds)...", intervalSec)
+	for {
+		time.Sleep(time.Duration(intervalSec) * time.Second)
+		stateCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "ip xfrm state")
+		stateOut, err := stateCmd.CombinedOutput()
+		if err != nil {
+			t.Logf("ip xfrm state failed: %v\n%s", err, string(stateOut))
+			continue
+		}
+		policyCmd := exec.Command("docker", "exec", containerName, "bash", "-c", "ip xfrm policy")
+		policyOut, err := policyCmd.CombinedOutput()
+		if err != nil {
+			t.Logf("ip xfrm policy failed: %v\n%s", err, string(policyOut))
+			continue
+		}
+		stateHasSPI := strings.Contains(string(stateOut), "spi 0x00000002")
+		policyHasSPI := strings.Contains(string(policyOut), "spi 0x00000002")
+		if stateHasSPI && policyHasSPI {
+			t.Logf("Found new SPI 0x00000002 in both ip xfrm state and policy!")
+			return nil
+		}
+		t.Logf("SPI 0x00000002 not found in both state and policy, waiting...")
+	}
+}
+
+func TestIPsec(t *testing.T) {
+	secretCmd := exec.Command("kmeshctl", "secret", "create")
+	out, err := secretCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("Failed to rotate IPSec key: %v\n%s", err, string(out))
+		return
+	}
+
+	// wait until both ip xfrm state and policy are non-empty before continuing
+	for {
+		stateCmd := exec.Command("docker", "exec", "kmesh-testing-control-plane", "bash", "-c", "ip xfrm state")
+		stateOut, _ := stateCmd.CombinedOutput()
+
+		policyCmd := exec.Command("docker", "exec", "kmesh-testing-control-plane", "bash", "-c", "ip xfrm policy")
+		policyOut, _ := policyCmd.CombinedOutput()
+
+		if strings.TrimSpace(string(stateOut)) != "" && strings.TrimSpace(string(policyOut)) != "" {
+			t.Logf("ip xfrm state and policy populated")
+			break
+		}
+
+		t.Logf("waiting for ip xfrm state/policy to be populated (stateLen=%d policyLen=%d)...", len(stateOut), len(policyOut))
+		time.Sleep(3 * time.Second)
+	}
+
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("IPsec Connectivity").Run(func(t framework.TestContext) {
+
+			// prepare params
+			if err := labelNamespace(t, "default", "istio.io/dataplane-mode", "Kmesh"); err != nil {
+				return
+			}
+			sleepParams := DeployParams{
+				Name:     "sleep-1",
+				AppLabel: "sleep-1",
+				Image:    "curlimages/curl",
+				NodeName: "kmesh-testing-control-plane",
+			}
+			echoParams := DeployParams{
+				Name:          "echo-1",
+				AppLabel:      "echo-1",
+				Image:         "gcr.io/istio-testing/app:latest",
+				SvcPort:       80,
+				TargetPort:    8080,
+				ContainerPort: 8080,
+			}
+
+			// deploy
+			if err := deployService(t, sleepTmpl, sleepParams); err != nil {
+				return
+			}
+			if err := deployService(t, echoTmpl, echoParams); err != nil {
+				_ = deleteService(t, sleepParams.Name, "default")
+				return
+			}
+
+			// cleanup
+			t.Cleanup(func() {
+				_ = deleteService(t, sleepParams.Name, "default")
+				_ = deleteService(t, echoParams.Name, "default")
+				_ = labelNamespace(t, "default", "istio.io/dataplane-mode", "")
+			})
+
+			t.Logf("Resources applied. Waiting for pods...")
+
+			sleepPodName, sleepPodIP, err := getPodNameAndIP(t, "default", sleepParams.AppLabel)
+			if err != nil {
+				return
+			}
+			t.Logf("sleep pod name: %s, ip: %s", sleepPodName, sleepPodIP)
+
+			echoPodName, echoPodIP, err := getPodNameAndIP(t, "default", echoParams.AppLabel)
+			if err != nil {
+				return
+			}
+			t.Logf("echo pod name: %s, ip: %s", echoPodName, echoPodIP)
+
+			if err = downloadTcpdump(t, "kmesh-testing-control-plane"); err != nil {
+				return
+			}
+			if err = checkIPSecRules(t, "kmesh-testing-control-plane"); err != nil {
+				return
+			}
+
+			t.Logf("Starting tcpdump in kmesh-testing-control-plane container...")
+			tcpdumpCh := make(chan string)
+			go func() {
+				out, _ := runTcpdumpESP(t, "kmesh-testing-control-plane", 10)
+				tcpdumpCh <- out
+			}()
+			time.Sleep(2 * time.Second)
+
+			t.Logf("Curling from sleep pod to echo pod...")
+			_, err = curlFromSleepToEcho(t, sleepPodName, echoPodIP, echoParams.ContainerPort)
+			if err != nil {
+				<-tcpdumpCh
+				return
+			}
+			t.Logf("curl success")
+			tcpdumpOut := <-tcpdumpCh
+
+			if strings.Contains(tcpdumpOut, "ESP") {
+				t.Logf("Connectivity test success: ESP packets detected during curl")
+			} else {
+				t.Fatalf("Connectivity test failed: No ESP packets detected.")
+			}
+		})
+	})
+
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		t.NewSubTest("IPsec Key Rotation").Run(func(t framework.TestContext) {
+
+			// prepare params
+			if err := labelNamespace(t, "default", "istio.io/dataplane-mode", "Kmesh"); err != nil {
+				return
+			}
+			sleepParams := DeployParams{
+				Name:     "sleep-2",
+				AppLabel: "sleep-2",
+				Image:    "curlimages/curl",
+				NodeName: "kmesh-testing-control-plane",
+			}
+			echoParams := DeployParams{
+				Name:          "echo-2",
+				AppLabel:      "echo-2",
+				Image:         "gcr.io/istio-testing/app:latest",
+				SvcPort:       80,
+				TargetPort:    8080,
+				ContainerPort: 8080,
+			}
+
+			// deploy
+			if err := deployService(t, sleepTmpl, sleepParams); err != nil {
+				return
+			}
+			if err := deployService(t, echoTmpl, echoParams); err != nil {
+				_ = deleteService(t, sleepParams.Name, "default")
+				return
+			}
+
+			// cleanup
+			t.Cleanup(func() {
+				_ = deleteService(t, sleepParams.Name, "default")
+				_ = deleteService(t, echoParams.Name, "default")
+				_ = labelNamespace(t, "default", "istio.io/dataplane-mode", "")
+			})
+
+			t.Logf("Resources applied. Waiting for pods...")
+
+			sleepPodName, sleepPodIP, err := getPodNameAndIP(t, "default", sleepParams.AppLabel)
+			if err != nil {
+				return
+			}
+			t.Logf("sleep pod name: %s, ip: %s", sleepPodName, sleepPodIP)
+
+			echoPodName, echoPodIP, err := getPodNameAndIP(t, "default", echoParams.AppLabel)
+			if err != nil {
+				return
+			}
+			t.Logf("echo pod name: %s, ip: %s", echoPodName, echoPodIP)
+
+			if err = downloadTcpdump(t, "kmesh-testing-control-plane"); err != nil {
+				return
+			}
+			if err = checkIPSecRules(t, "kmesh-testing-control-plane"); err != nil {
+				return
+			}
+
+			// rotate key and wait for new SPI in both state and policy
+			if err := rotateIPSecKeyAndWait(t, "kmesh-testing-control-plane", 5); err != nil {
+				return
+			}
+
+			t.Logf("Starting tcpdump in kmesh-testing-control-plane container after key rotation...")
+			tcpdumpCh := make(chan string)
+			go func() {
+				out, _ := runTcpdumpESP(t, "kmesh-testing-control-plane", 10)
+				tcpdumpCh <- out
+			}()
+			time.Sleep(2 * time.Second)
+
+			t.Logf("Curling from sleep pod to echo pod after key rotation...")
+			_, err = curlFromSleepToEcho(t, sleepPodName, echoPodIP, echoParams.ContainerPort)
+			if err != nil {
+				<-tcpdumpCh
+				return
+			}
+			t.Logf("curl after key rotation success")
+			tcpdumpOut := <-tcpdumpCh
+			t.Logf("tcpdump output after key rotation:\n%s", tcpdumpOut)
+
+			if strings.Contains(tcpdumpOut, "ESP") && strings.Contains(tcpdumpOut, "spi=0x00000002") {
+				t.Logf("Key rotation test success: ESP packets with new SPI detected after key rotation")
+			} else {
+				t.Fatalf("Key rotation test failed: No ESP packets with new SPI detected after key rotation.")
+			}
+		})
+	})
+}
diff --git a/test/e2e/run_test.sh b/test/e2e/run_test.sh
index 74fa51bb..0a670b7d 100755
--- a/test/e2e/run_test.sh
+++ b/test/e2e/run_test.sh
@@ -117,7 +117,7 @@ function setup_kmesh() {
 	# skip dns proxy for ipv6
 	[[ -n ${IPV6:-} ]] && extra_args="--set features.dnsProxy.enabled=false"
 	helm install kmesh $ROOT_DIR/deploy/charts/kmesh-helm -n kmesh-system --create-namespace --set deploy.kmesh.image.repository=localhost:5000/kmesh \
-		--set deploy.kmesh.containers.kmeshDaemonArgs="--mode=dual-engine --enable-bypass=false --monitoring=true" \
+		--set deploy.kmesh.containers.kmeshDaemonArgs="--mode=dual-engine --enable-bypass=false --monitoring=true --enable-ipsec=true" \
 		$extra_args
 
 	# Wait for all Kmesh pods to be ready.
@@ -176,6 +176,55 @@ function setup_kmesh() {
 	done
 }
 
+function set_daemonupgarde_testcase_image() {
+	local TMP_BUILD
+	TMP_BUILD="$(mktemp -d)"
+	echo "Building in temp dir: $TMP_BUILD"
+
+	git clone --depth 1 . "$TMP_BUILD" || {
+		echo "git clone failed"
+		rm -rf "$TMP_BUILD"
+		return 1
+	}
+
+	pushd "$TMP_BUILD" >/dev/null
+
+	BPF_HEADER_FILE="./bpf/include/bpf_common.h"
+	echo "Modifying BPF header file: ${BPF_HEADER_FILE}"
+
+	sed -i \
+		'/} kmesh_map64 SEC(".maps");/a\
+		\
+struct {\
+	__uint(type, BPF_MAP_TYPE_HASH);\
+	__uint(key_size, sizeof(__u32));\
+	__uint(value_size, MAP_VAL_SIZE_64);\
+	__uint(max_entries, MAP_MAX_ENTRIES);\
+	__uint(map_flags, BPF_F_NO_PREALLOC);\
+} kmesh_map64_bak_for_test SEC(".maps");' \
+		"${BPF_HEADER_FILE}"
+
+	local HUB="localhost:5000"
+	local TARGET="kmesh"
+	local TAG="test-upgrade-map-change"
+	local IMAGE="${HUB}/${TARGET}:${TAG}"
+
+	echo "Running 'make docker.push' with custom HUB and TAG in $TMP_BUILD"
+	if ! HUB=${HUB} TARGET=${TARGET} TAG=${TAG} make docker.push; then
+		echo "make docker.push failed"
+		popd >/dev/null
+		rm -rf "$TMP_BUILD"
+		return 1
+	fi
+
+	export KMESH_UPGRADE_IMAGE="${IMAGE}"
+	echo "Built and pushed image: ${IMAGE}"
+
+	popd >/dev/null
+	# rm -rf "$TMP_BUILD"
+	return 0
+}
+
 function setup_kmesh_log() {
 	# Set log of each Kmesh pods.
 	PODS=$(kubectl get pods -n kmesh-system -l app=kmesh -o jsonpath='{.items[*].metadata.name}')
@@ -346,6 +395,10 @@ while (("$#")); do
 		PARAMS+=("-istio.test.nocleanup")
 		shift
 		;;
+	--skip-build-daemonupgarde-image)
+		SKIP_BUILD_UPGARDE=true
+		shift
+		;;
 	*)
 		PARAMS+=("$1")
 		shift
@@ -367,6 +420,9 @@ if [[ -z ${SKIP_BUILD:-} ]]; then
 	setup_kind_registry
 	build_and_push_images
 	install_kmeshctl
+	if [[ -z ${SKIP_BUILD_UPGARDE:-} ]]; then
+		set_daemonupgarde_testcase_image
+	fi
 fi
 
 kubectl config use-context "kind-$NAME"
@@ -403,6 +459,6 @@ if [[ -n ${CLEANUP_REGISTRY} ]]; then
 	cleanup_docker_registry
 fi
 
-rm -rf "${TMP}"
+#rm -rf "${TMP}"
 
 exit $EXIT_CODE
diff --git a/test/e2e/upgarde_test.go b/test/e2e/upgarde_test.go
new file mode 100644
index 00000000..58d59b03
--- /dev/null
+++ b/test/e2e/upgarde_test.go
@@ -0,0 +1,137 @@
+//go:build integ
+// +build integ
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// NOTE: THE CODE IN THIS FILE IS MAINLY REFERENCED FROM ISTIO INTEGRATION
+// FRAMEWORK(https://github.com/istio/istio/tree/master/tests/integration)
+// AND ADAPTED FOR KMESH.
+
+package kmesh
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"istio.io/istio/pkg/test/framework"
+	"istio.io/istio/pkg/test/framework/components/echo"
+	"istio.io/istio/pkg/test/framework/components/echo/util/traffic"
+	kubetest "istio.io/istio/pkg/test/kube"
+	"istio.io/istio/pkg/test/util/retry"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+// TestKmeshUpgrade performs a rolling upgrade of the kmesh daemonset image
+// while continuous traffic is flowing, and asserts there is no traffic disruption.
+func TestKmeshUpgrade(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		configureDNSProxy(t, false)
+
+		src := apps.EnrolledToKmesh[0]
+		dst := apps.ServiceWithWaypointAtServiceGranularity
+		options := echo.CallOptions{
+			To:    dst,
+			Count: 1,
+			Check: httpValidator,
+			Port: echo.Port{
+				Name: "http",
+			},
+			Retry: echo.Retry{NoRetry: true},
+		}
+
+		g := traffic.NewGenerator(t, traffic.Config{
+			Source:   src,
+			Options:  options,
+			Interval: 50 * time.Millisecond,
+		}).Start()
+
+		upgradeKmesh(t)
+
+		g.Stop().CheckSuccessRate(t, 1)
+
+		configureDNSProxy(t, true)
+	})
+}
+
+// upgradeKmesh patches the daemonset image to the value of KMESH_UPGRADE_IMAGE and waits for rollout.
+func upgradeKmesh(t framework.TestContext) {
+	newImage := os.Getenv("KMESH_UPGRADE_IMAGE")
+	if newImage == "" {
+		newImage = "localhost:5000/kmesh"
+	}
+
+	patchData := fmt.Sprintf(`{
+		"spec": {
+			"template": {
+				"metadata": {
+					"annotations": {
+						"kmesh-upgrade-at": %q
+					}
+				},
+				"spec": {
+					"containers": [
+						{
+							"name": "kmesh",
+							"image": "%s"
+						}
+					]
+				}
+			}
+		}
+	}`, time.Now().Format(time.RFC3339), newImage)
+
+	patchKmesh_upgrade(t, patchData)
+}
+
+// patchKmesh applies a strategic merge patch to the Kmesh DaemonSet and waits for rollout completion.
+func patchKmesh_upgrade(t framework.TestContext, patchData string) {
+	patchOpts := metav1.PatchOptions{}
+	ds := t.Clusters().Default().Kube().AppsV1().DaemonSets(KmeshNamespace)
+	_, err := ds.Patch(context.Background(), KmeshDaemonsetName, types.StrategicMergePatchType, []byte(patchData), patchOpts)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := retry.UntilSuccess(func() error {
+		d, err := ds.Get(context.Background(), KmeshDaemonsetName, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+		if !daemonsetsetComplete_upgrade(d) {
+			return fmt.Errorf("rollout is not yet done")
+		}
+		return nil
+	}, retry.Timeout(120*time.Second), retry.Delay(2*time.Second)); err != nil {
+		t.Fatalf("failed to wait for Kmesh rollout status: %v", err)
+	}
+
+	if _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(t.AllClusters()[0], KmeshNamespace, "app=kmesh")); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// daemonsetsetComplete returns true when DaemonSet rollout appears complete.
+func daemonsetsetComplete_upgrade(ds *appsv1.DaemonSet) bool {
+	return ds.Status.UpdatedNumberScheduled == ds.Status.DesiredNumberScheduled &&
+		ds.Status.NumberReady == ds.Status.DesiredNumberScheduled &&
+		ds.Status.ObservedGeneration >= ds.Generation
+}