Repository: kmesh-net/kmesh

Issue:
Some issues encountered in writing Ipsec E2E tests
I followed the steps below to deploy kmesh with ipsec enabled：
1.`kmeshctl secret --key=$(dd if=/dev/urandom count=36 bs=1 2>/dev/null | xxd -p -c 64)`
2.`kubectl apply -f ./kmesh/deploy/yaml/crd/kmesh.net_kmeshnodeinfos.yaml`
3.`kubectl apply -f ./kmesh/deploy/yaml/`
` [
  "./start_kmesh.sh --mode=dual-engine --enable-bypass=false --enable-ipsec=true",
]`
4.`kubectl label namespace default istio.io/dataplane-mode=Kmesh`
`kubectl apply -f ./kmesh/samples/httpbin/httpbin.yaml` with     nodeName: kmesh-testing-worker
`kubectl apply -f ./kmesh/samples/sleep/sleep.yaml`    with  nodeName: kmesh-testing-control-plane

In the sleep pod，curl  ip + port of the httpbin service would fail, while kmesh reported an error:
`time="2025-08-12T11:00:52Z" level=error msg="grpc reconnect failed, create workload stream failed, DeltaAggregatedResources failed, rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.95.184:15012: i/o timeout\"" subsys=controller`

curl the ip + port of the httpbin pod would succeed. The ESP package can be seen in the docker container of the kind cluster.

Then I run E2E test script with ipsec enabled. The modifications I made are as follows:
1.add volume and volumount
`  - mountPath: /kmesh/kmesh-ipsec
    name: kmesh-ipsec`

`  - name: kmesh-ipsec
    secret:
    secretName: kmesh-ipsec
    optional: true` in daemonset.yaml
2.add clusterrole apigroups
`- apiGroups: ["kmesh.net"]
  resources: ["kmeshnodeinfos"]
  verbs: ["get", "create", "update", "delete", "list", "watch"]`  in kmesh-rbac.yaml
3.then run
`	kmeshctl secret --key=$(dd if=/dev/urandom count=36 bs=1 2>/dev/null | xxd -p -c 64)`
`	kubectl apply -f deploy/yaml/crd/kmesh.net_kmeshnodeinfos.yaml`
`	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 --enable-ipsec=true"`

Then the waypoint failed to start and kmesh log as：
`2025-08-12T10:52:18.598497Z     error   citadelclient   failed to sign CSR: create certificate: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp: lookup istiod.istio-system.svc: i/o timeout"`
`2025-08-12T10:52:18.598874Z     info    citadelclient   recreated connection`
`2025-08-12T10:52:18.598907Z     error   cache   resource:default failed to sign: create certificate: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp: lookup istiod.istio-system.svc: i/o timeout"`
`2025-08-12T10:52:18.598913Z     warn    sds     failed to warm certificate: failed to generate workload certificate: create certificate: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp: lookup istiod.istio-system.svc: i/o timeout"`


Discussion:
@zrggw: A possible cause of this situation is the current ipsec configuration will include all pods on the node in xfrm policy rules, not just those in specific namespaces which managed by kmesh. This can lead to issues if istio sends back data packets that are unencrypted because they lack encryption modules, when these data packets arriving local network interface card and matching with xfrm policies, due to their unencrypted status mismatching kernel requirements for ipsec, these packages may be discarded by kernel and causing timeouts.

A possible solution is adjusting the scope of the xfrm policy so it only includes pod IPs under kmesh management rather than affecting every pod on node.@zrggw: After testing with `curl -IsS "http://10.244.1.5:80/status/200"` (httpbin pod ip), I found that the communication issues occur when crossing nodes and namespaces(default namespace managed by kmesh).

<img width="4960" height="2769" alt="Image" src="https://github.com/user-attachments/assets/cc5afa75-8dd2-4b6b-a3de-24ebe41c4be0" />

And, use `cat /proc/net/xfrm_stat`, I found `XfrmInTmplMismatch` is a large number, according to cilium's [documentation](https://docs.cilium.io/en/stable/security/network/encryption-ipsec/#encryption-ipsec),  `XfrmInTmplMismatch` is described as `a bug in the XFRM configuration for  decryption`.

Solution patch:
diff --git a/bpf/kmesh/general/include/tc.h b/bpf/kmesh/general/include/tc.h
index 18b178e3..e6d71c91 100644
--- a/bpf/kmesh/general/include/tc.h
+++ b/bpf/kmesh/general/include/tc.h
@@ -24,8 +24,9 @@ struct tc_info {
     };
 };
 
-#define PARSER_FAILED 1
-#define PARSER_SUCC   0
+#define PARSER_FAILED        1
+#define PARSER_SUCC          0
+#define IPSEC_DECRYPTED_MARK 0x00d0
 
 static inline bool is_ipv4(struct tc_info *info)
 {
diff --git a/bpf/kmesh/general/tc_mark_decrypt.c b/bpf/kmesh/general/tc_mark_decrypt.c
index 04e4d3a6..08803643 100644
--- a/bpf/kmesh/general/tc_mark_decrypt.c
+++ b/bpf/kmesh/general/tc_mark_decrypt.c
@@ -12,16 +12,34 @@ int tc_mark_decrypt(struct __sk_buff *ctx)
 {
     struct nodeinfo *nodeinfo;
     struct tc_info info = {0};
+    __u8 protocol = 0;
+    bool decrypted = false;
+    __u32 mark = 0;
 
     if (parser_tc_info(ctx, &info)) {
         return TC_ACT_OK;
     }
-    nodeinfo = check_remote_manage_by_kmesh(ctx, &info, info.iph->saddr, info.ip6h->saddr.s6_addr32);
-    if (!nodeinfo) {
+    if (is_ipv4(&info)) {
+        protocol = info.iph->protocol;
+    } else if (is_ipv6(&info)) {
+        protocol = info.ip6h->nexthdr;
+    } else {
         return TC_ACT_OK;
     }
-    // 0x00d0 mean need decryption in ipsec
-    ctx->mark = 0x00d0;
+
+    if (protocol == IPPROTO_ESP) {
+        return TC_ACT_OK;
+    }
+
+    mark = ctx->mark;
+    decrypted = (mark == IPSEC_DECRYPTED_MARK); // IPSEC_DECRYPTED_MARK is same with xfmr state output-mark, which means
+                                                // packet was decrypted and then back to ingress
+
+    if (decrypted) {
+        return TC_ACT_OK;
+    }
+
+    ctx->mark = 0;
     return TC_ACT_OK;
 }
 
diff --git a/ctl/secret/secret.go b/ctl/secret/secret.go
index 74157292..ab4491e6 100644
--- a/ctl/secret/secret.go
+++ b/ctl/secret/secret.go
@@ -19,6 +19,7 @@ package secret
 import (
 	"context"
 	"crypto/rand"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -30,10 +31,12 @@ import (
 
 	"kmesh.net/kmesh/ctl/utils"
 	"kmesh.net/kmesh/pkg/controller/encryption/ipsec"
+	"kmesh.net/kmesh/pkg/kube"
 	"kmesh.net/kmesh/pkg/logger"
 )
 
 var log = logger.NewLoggerScope("kmeshctl/secret")
+var clientset kube.CLIClient
 
 const (
 	SecretName        = "kmesh-ipsec"
@@ -43,67 +46,109 @@ const (
 )
 
 func NewCmd() *cobra.Command {
+	clientset = createKubeClientOrExit()
+
 	cmd := &cobra.Command{
 		Use:   "secret",
-		Short: "Manage IPsec secrets for Kmesh",
-		Long:  "Generate and manage IPsec encryption secrets for secure communication between nodes",
+		Short: "Use secrets to manage secret configuration data for IPsec",
+		Example: `# Use kmeshctl secret to manage secret configuration data for IPsec:
+kmeshctl secret create or kmeshctl secret create --key=$(echo -n "{36-character user-defined key here}" | xxd -p -c 64)
+kmeshctl secret get
+kmeshctl secret delete
+`,
+		Args: cobra.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+		},
 	}
 
-	cmd.AddCommand(newCreateCmd())
-
-	return cmd
-}
-
-func newCreateCmd() *cobra.Command {
-	cmd := &cobra.Command{
+	// create cmd
+	createCmd := &cobra.Command{
 		Use:   "create",
-		Short: "Create a new IPsec secret with automatically generated key",
-		Long: `Create a new IPsec secret with automatically generated encryption key.
-The key is generated using cryptographically secure random bytes and formatted
-for use with the rfc4106(gcm(aes)) AEAD algorithm.`,
-		Example: `# Create a new IPsec secret with automatically generated key:
+		Short: "Generate IPsec key and configuration by kmeshctl",
+		Example: `# Generate IPsec configuration with random IPsec key:
 kmeshctl secret create
+# Generate IPsec configuration with user-defined key:
+kmeshctl secret create --key=$(echo -n "{36-character user-defined key here}" | xxd -p -c 64)`,
+		Args: cobra.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			CreateOrUpdateSecret(cmd, args)
+		},
+	}
+
+	createCmd.Flags().StringP("key", "k", "", "key of the encryption") // user defined key
 
-# This will generate a 36-byte key (32-byte key + 4-byte salt) and create
-# the 'kmesh-ipsec' secret in the kmesh-system namespace.`,
+	// get cmd
+	getCmd := &cobra.Command{
+		Use:   "get",
+		Short: "Get IPsec key and configuration by kmeshctl",
+		Example: `# Get IPsec key and configuration by kmeshctl. The results will be displayed in JSON format.
+kmeshctl secret get`,
 		Args: cobra.NoArgs,
 		Run: func(cmd *cobra.Command, args []string) {
-			createSecretWithRandomKey()
+			GetSecret()
 		},
 	}
-	return cmd
-}
 
-// generateRandomKey generates a cryptographically secure random key for IPsec
-func generateRandomKey() ([]byte, error) {
-	key := make([]byte, AeadKeyLength)
-	_, err := rand.Read(key)
-	if err != nil {
-		return nil, fmt.Errorf("failed to generate random key: %v", err)
+	// delete cmd
+	deleteCmd := &cobra.Command{
+		Use:     "delete",
+		Short:   "Delete IPsec key and configuration by kmeshctl",
+		Example: `kmeshctl secret delete`,
+		Args:    cobra.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			DeleteSecret()
+		},
 	}
-	return key, nil
-}
 
-func createSecretWithRandomKey() {
-	var ipSecKey, ipSecKeyOld ipsec.IpSecKey
-	var err error
+	// add sub-command
+	cmd.AddCommand(createCmd)
+	cmd.AddCommand(getCmd)
+	cmd.AddCommand(deleteCmd)
 
+	return cmd
+}
+
+func createKubeClientOrExit() kube.CLIClient {
 	clientset, err := utils.CreateKubeClient()
 	if err != nil {
 		log.Errorf("failed to connect k8s client, %v", err)
 		os.Exit(1)
 	}
+	return clientset
+}
+
+func CreateOrUpdateSecret(cmd *cobra.Command, args []string) {
+	var ipSecKey, ipSecKeyOld ipsec.IpSecKey
+	var err error
 
 	ipSecKey.AeadKeyName = AeadAlgoName
 
-	// Generate random key automatically
-	aeadKey, err := generateRandomKey()
-	if err != nil {
-		log.Errorf("failed to generate random key: %v", err)
+	aeadKeyArg, _ := cmd.Flags().GetString("key")
+
+	var aeadKey []byte
+
+	if !cmd.Flags().Changed("key") {
+		aeadKey = make([]byte, AeadKeyLength)
+		_, err := rand.Read(aeadKey)
+		if err != nil {
+			log.Errorf("failed to generate random key: %v", err)
+			os.Exit(1)
+		}
+	} else {
+		aeadKey, err = hex.DecodeString(aeadKeyArg)
+		if err != nil {
+			log.Errorf("failed to decode hex string: %v, input: %v", err, aeadKeyArg)
+			os.Exit(1)
+		}
+	}
+
+	if len(aeadKey) != AeadKeyLength {
+		log.Errorf("invalid key length: expected %d bytes, got %d bytes (key must be 256-bit + 32-bit salt)", AeadKeyLength, len(aeadKey))
 		os.Exit(1)
 	}
 
 	ipSecKey.AeadKey = aeadKey
+
 	ipSecKey.Length = AeadAlgoICVLength
 
 	secretOld, err := clientset.Kube().CoreV1().Secrets(utils.KmeshNamespace).Get(context.TODO(), SecretName, metav1.GetOptions{})
@@ -151,5 +196,65 @@ func createSecretWithRandomKey() {
 			os.Exit(1)
 		}
 	}
-	log.Infof("IPsec secret created successfully")
+}
+
+func GetSecret() {
+	secret, err := clientset.Kube().CoreV1().Secrets(utils.KmeshNamespace).Get(context.TODO(), SecretName, metav1.GetOptions{})
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			log.Errorf("secret %s not found", SecretName)
+			os.Exit(1)
+		}
+		log.Errorf("failed to get secret: %v", err)
+		os.Exit(1)
+	}
+
+	if secret.Data == nil || secret.Data["ipSec"] == nil {
+		log.Errorf("invalid secret data: missing ipSec field")
+		os.Exit(1)
+	}
+
+	// Parse the IPsec data
+	var ipSecKey ipsec.IpSecKey
+	if err := json.Unmarshal(secret.Data["ipSec"], &ipSecKey); err != nil {
+		log.Errorf("failed to unmarshal secret data: %v", err)
+		os.Exit(1)
+	}
+
+	// Create a display structure with hex string key
+	displayKey := struct {
+		Spi         int    `json:"spi"`
+		AeadKeyName string `json:"aeadKeyName"`
+		AeadKey     string `json:"aeadKey"`
+		Length      int    `json:"length"`
+	}{
+		Spi:         ipSecKey.Spi,
+		AeadKeyName: ipSecKey.AeadKeyName,
+		AeadKey:     hex.EncodeToString(ipSecKey.AeadKey),
+		Length:      ipSecKey.Length,
+	}
+
+	displayData, err := json.MarshalIndent(displayKey, "", "  ")
+	if err != nil {
+		log.Errorf("failed to marshal display data: %v", err)
+		os.Exit(1)
+	}
+
+	fmt.Printf("Secret name: %s\n", SecretName)
+	fmt.Printf("Namespace: %s\n", utils.KmeshNamespace)
+	fmt.Printf("Created: %s\n", secret.CreationTimestamp.Format("2006-01-02 15:04:05"))
+	fmt.Println("IPsec Configuration:")
+	fmt.Println(string(displayData))
+}
+
+func DeleteSecret() {
+	err := clientset.Kube().CoreV1().Secrets(utils.KmeshNamespace).Delete(context.TODO(), SecretName, metav1.DeleteOptions{})
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			log.Errorf("secret %s not found", SecretName)
+			os.Exit(1)
+		}
+		log.Errorf("failed to delete secret: %v", err)
+		os.Exit(1)
+	}
 }
diff --git a/docs/ctl/kmeshctl.md b/docs/ctl/kmeshctl.md
index d53156f2..2a0c1ead 100644
--- a/docs/ctl/kmeshctl.md
+++ b/docs/ctl/kmeshctl.md
@@ -14,6 +14,6 @@ Kmesh command line tools to operate and debug Kmesh
 * [kmeshctl dump](kmeshctl_dump.md) - Dump config of kernel-native or dual-engine mode
 * [kmeshctl log](kmeshctl_log.md) - Get or set kmesh-daemon's logger level
 * [kmeshctl monitoring](kmeshctl_monitoring.md) - Control Kmesh's monitoring to be turned on as needed
-* [kmeshctl secret](kmeshctl_secret.md) - Manage IPsec secrets for Kmesh
+* [kmeshctl secret](kmeshctl_secret.md) - Use secrets to manage secret configuration data for IPsec
 * [kmeshctl version](kmeshctl_version.md) - Prints out build version info
 * [kmeshctl waypoint](kmeshctl_waypoint.md) - Manage waypoint configuration
diff --git a/docs/ctl/kmeshctl_secret.md b/docs/ctl/kmeshctl_secret.md
index 4176ca6e..01cdbdce 100644
--- a/docs/ctl/kmeshctl_secret.md
+++ b/docs/ctl/kmeshctl_secret.md
@@ -1,10 +1,20 @@
 ## kmeshctl secret
 
-Manage IPsec secrets for Kmesh
+Use secrets to manage secret configuration data for IPsec
 
-### Synopsis
+```bash
+kmeshctl secret [flags]
+```
+
+### Examples
 
-Generate and manage IPsec encryption secrets for secure communication between nodes
+```bash
+# Use kmeshctl secret to manage secret configuration data for IPsec:
+kmeshctl secret create or kmeshctl secret create --key=$(echo -n "{36-character user-defined key here}" | xxd -p -c 64)
+kmeshctl secret get
+kmeshctl secret delete
+
+```
 
 ### Options
 
@@ -15,4 +25,6 @@ Generate and manage IPsec encryption secrets for secure communication between no
 ### SEE ALSO
 
 * [kmeshctl](kmeshctl.md) - Kmesh command line tools to operate and debug Kmesh
-* [kmeshctl secret create](kmeshctl_secret_create.md) - Create a new IPsec secret with automatically generated key
+* [kmeshctl secret create](kmeshctl_secret_create.md) - Generate IPsec key and configuration by kmeshctl
+* [kmeshctl secret delete](kmeshctl_secret_delete.md) - Delete IPsec key and configuration by kmeshctl
+* [kmeshctl secret get](kmeshctl_secret_get.md) - Get IPsec key and configuration by kmeshctl
diff --git a/docs/ctl/kmeshctl_secret_create.md b/docs/ctl/kmeshctl_secret_create.md
index 6552cb46..63d82d95 100644
--- a/docs/ctl/kmeshctl_secret_create.md
+++ b/docs/ctl/kmeshctl_secret_create.md
@@ -1,12 +1,6 @@
 ## kmeshctl secret create
 
-Create a new IPsec secret with automatically generated key
-
-### Synopsis
-
-Create a new IPsec secret with automatically generated encryption key.
-The key is generated using cryptographically secure random bytes and formatted
-for use with the rfc4106(gcm(aes)) AEAD algorithm.
+Generate IPsec key and configuration by kmeshctl
 
 ```bash
 kmeshctl secret create [flags]
@@ -15,19 +9,19 @@ kmeshctl secret create [flags]
 ### Examples
 
 ```bash
-# Create a new IPsec secret with automatically generated key:
+# Generate IPsec configuration with random IPsec key:
 kmeshctl secret create
-
-# This will generate a 36-byte key (32-byte key + 4-byte salt) and create
-# the 'kmesh-ipsec' secret in the kmesh-system namespace.
+# Generate IPsec configuration with user-defined key:
+kmeshctl secret create --key=$(echo -n "{36-character user-defined key here}" | xxd -p -c 64)
 ```
 
 ### Options
 
 ```bash
-  -h, --help   help for create
+  -h, --help         help for create
+  -k, --key string   key of the encryption
 ```
 
 ### SEE ALSO
 
-* [kmeshctl secret](kmeshctl_secret.md) - Manage IPsec secrets for Kmesh
+* [kmeshctl secret](kmeshctl_secret.md) - Use secrets to manage secret configuration data for IPsec
diff --git a/docs/ctl/kmeshctl_secret_delete.md b/docs/ctl/kmeshctl_secret_delete.md
new file mode 100644
index 00000000..90790edd
--- /dev/null
+++ b/docs/ctl/kmeshctl_secret_delete.md
@@ -0,0 +1,23 @@
+## kmeshctl secret delete
+
+Delete IPsec key and configuration by kmeshctl
+
+```bash
+kmeshctl secret delete [flags]
+```
+
+### Examples
+
+```bash
+kmeshctl secret delete
+```
+
+### Options
+
+```bash
+  -h, --help   help for delete
+```
+
+### SEE ALSO
+
+* [kmeshctl secret](kmeshctl_secret.md) - Use secrets to manage secret configuration data for IPsec
diff --git a/docs/ctl/kmeshctl_secret_get.md b/docs/ctl/kmeshctl_secret_get.md
new file mode 100644
index 00000000..5c792c9c
--- /dev/null
+++ b/docs/ctl/kmeshctl_secret_get.md
@@ -0,0 +1,24 @@
+## kmeshctl secret get
+
+Get IPsec key and configuration by kmeshctl
+
+```bash
+kmeshctl secret get [flags]
+```
+
+### Examples
+
+```bash
+# Get IPsec key and configuration by kmeshctl. The results will be displayed in JSON format.
+kmeshctl secret get
+```
+
+### Options
+
+```bash
+  -h, --help   help for get
+```
+
+### SEE ALSO
+
+* [kmeshctl secret](kmeshctl_secret.md) - Use secrets to manage secret configuration data for IPsec
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 2e1681ee..f44fec61 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -31,8 +31,11 @@ const (
 	ENABLED       = uint32(1)
 	DISABLED      = uint32(0)
 
-	TC_MARK_DECRYPT = "tc_mark_decrypt"
-	TC_MARK_ENCRYPT = "tc_mark_encrypt"
+	TC_MARK_DECRYPT   = "tc_mark_decrypt"
+	TC_MARK_ENCRYPT   = "tc_mark_encrypt"
+	XfrmDecryptedMark = 0x00d0
+	XfrmEncryptMark   = 0x00e0
+	XfrmMarkMask      = 0xffffffff
 
 	TC_ATTACH = 0
 	TC_DETACH = 1
diff --git a/pkg/controller/encryption/ipsec/ipsec_handler.go b/pkg/controller/encryption/ipsec/ipsec_handler.go
index e9d44155..42806c60 100644
--- a/pkg/controller/encryption/ipsec/ipsec_handler.go
+++ b/pkg/controller/encryption/ipsec/ipsec_handler.go
@@ -280,6 +280,10 @@ func (is *IpSecHandler) createStateRule(src net.IP, dst net.IP, key []byte, ipse
 			Key:    key,
 			ICVLen: ipsecKey.Length,
 		},
+		OutputMark: &netlink.XfrmMark{
+			Value: constants.XfrmDecryptedMark,
+			Mask:  constants.XfrmMarkMask,
+		},
 	}
 	err := netlink.XfrmStateAdd(state)
 	if err != nil && !os.IsExist(err) {
@@ -302,12 +306,12 @@ func (is *IpSecHandler) createPolicyRule(srcCIDR, dstCIDR *net.IPNet, src, dst n
 			},
 		},
 		Mark: &netlink.XfrmMark{
-			Mask: 0xffffffff,
+			Mask: constants.XfrmMarkMask,
 		},
 	}
 	if out {
 		// ingress
-		mark := uint32(0xd0)
+		mark := uint32(constants.XfrmDecryptedMark)
 		policy.Mark.Value = mark
 
 		policy.Dir = netlink.XFRM_DIR_IN
@@ -321,7 +325,7 @@ func (is *IpSecHandler) createPolicyRule(srcCIDR, dstCIDR *net.IPNet, src, dst n
 		}
 	} else {
 		// egress, update SPI
-		mark := uint32(0xe0)
+		mark := uint32(constants.XfrmEncryptMark)
 
 		policy.Mark.Value = uint32(mark)
 		policy.Tmpls[0].Spi = int(spi)



Test patch:
diff --git a/test/bpf_ut/bpftest/general_test.go b/test/bpf_ut/bpftest/general_test.go
index 9f96fe1a..2649b333 100644
--- a/test/bpf_ut/bpftest/general_test.go
+++ b/test/bpf_ut/bpftest/general_test.go
@@ -46,7 +46,23 @@ func testGeneralTC(t *testing.T) {
 			objFilename: "tc_mark_decrypt_test.o",
 			uts: []unitTest_BPF_PROG_TEST_RUN{
 				{
-					name: "tc_mark_decrypt",
+					name: "tc_mark_decrypt_case1",
+					setupInUserSpace: func(t *testing.T, coll *ebpf.Collection) {
+						setBpfConfig(t, coll, &factory.GlobalBpfConfig{
+							BpfLogLevel: constants.BPF_LOG_DEBUG,
+						})
+					},
+				},
+				{
+					name: "tc_mark_decrypt_case2",
+					setupInUserSpace: func(t *testing.T, coll *ebpf.Collection) {
+						setBpfConfig(t, coll, &factory.GlobalBpfConfig{
+							BpfLogLevel: constants.BPF_LOG_DEBUG,
+						})
+					},
+				},
+				{
+					name: "tc_mark_decrypt_case3",
 					setupInUserSpace: func(t *testing.T, coll *ebpf.Collection) {
 						setBpfConfig(t, coll, &factory.GlobalBpfConfig{
 							BpfLogLevel: constants.BPF_LOG_DEBUG,
diff --git a/test/bpf_ut/tc_mark_decrypt_test.c b/test/bpf_ut/tc_mark_decrypt_test.c
index 365e4f1d..13cc6d31 100644
--- a/test/bpf_ut/tc_mark_decrypt_test.c
+++ b/test/bpf_ut/tc_mark_decrypt_test.c
@@ -52,27 +52,24 @@ const struct tcphdr l4 = {
 };
 const char body[20] = "Should not change!!";
 
-PKTGEN("tc", "tc_mark_decrypt")
+// Test Case 1: TCP protocol, mark 0x00a0, expect mark to be 0x0
+PKTGEN("tc", "tc_mark_decrypt_case1")
 int test1_pktgen(struct __sk_buff *ctx)
 {
     return build_tc_packet(ctx, &l2, &l3, &l4, body, (uint)sizeof(body));
 }
 
-JUMP("tc", "tc_mark_decrypt")
+JUMP("tc", "tc_mark_decrypt_case1")
 int test1_jump(struct __sk_buff *ctx)
 {
-    // build context
-    struct lpm_key key = {0};
-    key.trie_key.prefixlen = 32;
-    key.ip.ip4 = SRC_IP;
-    __u32 value = 1;
-    bpf_map_update_elem(&map_of_nodeinfo, &key, &value, BPF_ANY);
+    // Set initial mark to 0x00a0
+    ctx->mark = 0x00a0;
 
     bpf_tail_call(ctx, &entry_call_map, 0);
     return TEST_ERROR;
 }
 
-CHECK("tc", "tc_mark_decrypt")
+CHECK("tc", "tc_mark_decrypt_case1")
 int test1_check(struct __sk_buff *ctx)
 {
     const __u32 expected_status_code = TC_ACT_OK;
@@ -80,6 +77,95 @@ int test1_check(struct __sk_buff *ctx)
     test_init();
 
     check_tc_packet(ctx, &expected_status_code, &l2, &l3, &l4, body, (uint)sizeof(body));
+    if (ctx->mark != 0x0)
+        test_fatal("ctx->mark mismatch, expected 0x0, got %u", ctx->mark);
+
+    test_finish();
+}
+
+// Test Case 2: TCP protocol, mark 0x00d0, expect mark to remain 0x00d0
+PKTGEN("tc", "tc_mark_decrypt_case2")
+int test2_pktgen(struct __sk_buff *ctx)
+{
+    return build_tc_packet(ctx, &l2, &l3, &l4, body, (uint)sizeof(body));
+}
+
+JUMP("tc", "tc_mark_decrypt_case2")
+int test2_jump(struct __sk_buff *ctx)
+{
+    // Set initial mark to 0x00d0 (already decrypted)
+    ctx->mark = 0x00d0;
+
+    bpf_tail_call(ctx, &entry_call_map, 0);
+    return TEST_ERROR;
+}
+
+CHECK("tc", "tc_mark_decrypt_case2")
+int test2_check(struct __sk_buff *ctx)
+{
+    const __u32 expected_status_code = TC_ACT_OK;
+
+    test_init();
+
+    check_tc_packet(ctx, &expected_status_code, &l2, &l3, &l4, body, (uint)sizeof(body));
+    if (ctx->mark != 0x00d0)
+        test_fatal("ctx->mark mismatch, expected 0x00d0, got %u", ctx->mark);
+
+    test_finish();
+}
+
+// Test Case 3: ESP protocol, mark 0x00d0, expect mark to remain 0x00d0
+PKTGEN("tc", "tc_mark_decrypt_case3")
+int test3_pktgen(struct __sk_buff *ctx)
+{
+    // Use ESP protocol header for this test
+    struct iphdr l3_esp = {
+        .version = 4,
+        .ihl = 5,
+        .tot_len = 40,
+        .id = 0x5438,
+        .frag_off = bpf_htons(IP_DF),
+        .ttl = 64,
+        .protocol = IPPROTO_ESP,
+        .saddr = SRC_IP,
+        .daddr = DEST_IP,
+    };
+
+    return build_tc_packet(ctx, &l2, &l3_esp, &l4, body, (uint)sizeof(body));
+}
+
+JUMP("tc", "tc_mark_decrypt_case3")
+int test3_jump(struct __sk_buff *ctx)
+{
+    // Set initial mark to 0x00d0
+    ctx->mark = 0x00d0;
+
+    // build context - no need for nodeinfo map for ESP packets
+    bpf_tail_call(ctx, &entry_call_map, 0);
+    return TEST_ERROR;
+}
+
+CHECK("tc", "tc_mark_decrypt_case3")
+int test3_check(struct __sk_buff *ctx)
+{
+    const __u32 expected_status_code = TC_ACT_OK;
+
+    test_init();
+
+    // Use ESP protocol header for verification
+    struct iphdr l3_esp = {
+        .version = 4,
+        .ihl = 5,
+        .tot_len = 40,
+        .id = 0x5438,
+        .frag_off = bpf_htons(IP_DF),
+        .ttl = 64,
+        .protocol = IPPROTO_ESP,
+        .saddr = SRC_IP,
+        .daddr = DEST_IP,
+    };
+
+    check_tc_packet(ctx, &expected_status_code, &l2, &l3_esp, &l4, body, (uint)sizeof(body));
     if (ctx->mark != 0x00d0)
         test_fatal("ctx->mark mismatch, expected 0x00d0, got %u", ctx->mark);