Repository: kmesh-net/kmesh

Issue:
KMesh support for ServiceEntry virtural domain resolution and traffic interception, similar to Sidecar DNS capture
<!-- Please only use this template for submitting enhancement requests -->

**What would you like to be added**:

KMesh should support:
1. Resolving virtual domain names defined in `ServiceEntry` (e.g., `external-service.nacos.test`);
2. Intercepting traffic to those virtual IPs and routing through Waypoint for mesh governance.

This should work similarly to Istio Sidecar DNS capture, but implemented in the KMesh dataplane.

**Why is this needed**:

We are building a hybrid mesh to connect Kubernetes and VM workloads. Without domain resolution and traffic interception for `ServiceEntry`, Kubernetes workloads cannot access VM services using mesh-defined hostnames. This limits service discovery and traffic control across environments.

**Example**:

```yaml
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: external-service
  namespace: vm-services
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts:
  - external-service.nacos.test
  ports:
  - number: 7000
    name: http
    protocol: HTTP
  location: MESH_EXTERNAL
  resolution: STATIC
  endpoints:
  - address: 192.168.5.27
    ports:
      http: 7000
```
Expectation:

curl http://external-service.nacos.test

1. should resolve to a virtual IP (e.g., 240.x.x.x);
2. traffic should be routed through Waypoint for policy enforcement.



Hints from issue comments:
@adity1raut: /assign@adity1raut: /unassign@hzxuzhonghu:  @Kuromesi  is working on this@Kuromesi: /assign

Solution patch:
diff --git a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfeb.go b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfeb.go
index c7166ba4..d197895c 100644
--- a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfeb.go
+++ b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfeb.go
@@ -98,6 +98,8 @@ type KmeshCgroupSockWorkloadSpecs struct {
 //
 // It can be passed ebpf.CollectionSpec.Assign.
 type KmeshCgroupSockWorkloadProgramSpecs struct {
+	BpfRedirectDnsSend *ebpf.ProgramSpec `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.ProgramSpec `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect6_prog"`
 }
@@ -215,12 +217,16 @@ type KmeshCgroupSockWorkloadVariables struct {
 //
 // It can be passed to LoadKmeshCgroupSockWorkloadObjects or ebpf.CollectionSpec.LoadAndAssign.
 type KmeshCgroupSockWorkloadPrograms struct {
+	BpfRedirectDnsSend *ebpf.Program `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.Program `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.Program `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.Program `ebpf:"cgroup_connect6_prog"`
 }
 
 func (p *KmeshCgroupSockWorkloadPrograms) Close() error {
 	return _KmeshCgroupSockWorkloadClose(
+		p.BpfRedirectDnsSend,
+		p.BpfRestoreDnsRecv,
 		p.CgroupConnect4Prog,
 		p.CgroupConnect6Prog,
 	)
diff --git a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfel.go b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfel.go
index 1cb9ba40..3c1f1a81 100644
--- a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfel.go
+++ b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkload_bpfel.go
@@ -98,6 +98,8 @@ type KmeshCgroupSockWorkloadSpecs struct {
 //
 // It can be passed ebpf.CollectionSpec.Assign.
 type KmeshCgroupSockWorkloadProgramSpecs struct {
+	BpfRedirectDnsSend *ebpf.ProgramSpec `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.ProgramSpec `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect6_prog"`
 }
@@ -215,12 +217,16 @@ type KmeshCgroupSockWorkloadVariables struct {
 //
 // It can be passed to LoadKmeshCgroupSockWorkloadObjects or ebpf.CollectionSpec.LoadAndAssign.
 type KmeshCgroupSockWorkloadPrograms struct {
+	BpfRedirectDnsSend *ebpf.Program `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.Program `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.Program `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.Program `ebpf:"cgroup_connect6_prog"`
 }
 
 func (p *KmeshCgroupSockWorkloadPrograms) Close() error {
 	return _KmeshCgroupSockWorkloadClose(
+		p.BpfRedirectDnsSend,
+		p.BpfRestoreDnsRecv,
 		p.CgroupConnect4Prog,
 		p.CgroupConnect6Prog,
 	)
diff --git a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfeb.go b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfeb.go
index 2fcdd500..f6022bee 100644
--- a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfeb.go
+++ b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfeb.go
@@ -98,6 +98,8 @@ type KmeshCgroupSockWorkloadCompatSpecs struct {
 //
 // It can be passed ebpf.CollectionSpec.Assign.
 type KmeshCgroupSockWorkloadCompatProgramSpecs struct {
+	BpfRedirectDnsSend *ebpf.ProgramSpec `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.ProgramSpec `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect6_prog"`
 }
@@ -215,12 +217,16 @@ type KmeshCgroupSockWorkloadCompatVariables struct {
 //
 // It can be passed to LoadKmeshCgroupSockWorkloadCompatObjects or ebpf.CollectionSpec.LoadAndAssign.
 type KmeshCgroupSockWorkloadCompatPrograms struct {
+	BpfRedirectDnsSend *ebpf.Program `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.Program `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.Program `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.Program `ebpf:"cgroup_connect6_prog"`
 }
 
 func (p *KmeshCgroupSockWorkloadCompatPrograms) Close() error {
 	return _KmeshCgroupSockWorkloadCompatClose(
+		p.BpfRedirectDnsSend,
+		p.BpfRestoreDnsRecv,
 		p.CgroupConnect4Prog,
 		p.CgroupConnect6Prog,
 	)
diff --git a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfel.go b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfel.go
index 0c4014ae..57691f00 100644
--- a/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfel.go
+++ b/bpf/kmesh/bpf2go/dualengine/kmeshcgroupsockworkloadcompat_bpfel.go
@@ -98,6 +98,8 @@ type KmeshCgroupSockWorkloadCompatSpecs struct {
 //
 // It can be passed ebpf.CollectionSpec.Assign.
 type KmeshCgroupSockWorkloadCompatProgramSpecs struct {
+	BpfRedirectDnsSend *ebpf.ProgramSpec `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.ProgramSpec `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.ProgramSpec `ebpf:"cgroup_connect6_prog"`
 }
@@ -215,12 +217,16 @@ type KmeshCgroupSockWorkloadCompatVariables struct {
 //
 // It can be passed to LoadKmeshCgroupSockWorkloadCompatObjects or ebpf.CollectionSpec.LoadAndAssign.
 type KmeshCgroupSockWorkloadCompatPrograms struct {
+	BpfRedirectDnsSend *ebpf.Program `ebpf:"bpf_redirect_dns_send"`
+	BpfRestoreDnsRecv  *ebpf.Program `ebpf:"bpf_restore_dns_recv"`
 	CgroupConnect4Prog *ebpf.Program `ebpf:"cgroup_connect4_prog"`
 	CgroupConnect6Prog *ebpf.Program `ebpf:"cgroup_connect6_prog"`
 }
 
 func (p *KmeshCgroupSockWorkloadCompatPrograms) Close() error {
 	return _KmeshCgroupSockWorkloadCompatClose(
+		p.BpfRedirectDnsSend,
+		p.BpfRestoreDnsRecv,
 		p.CgroupConnect4Prog,
 		p.CgroupConnect6Prog,
 	)
diff --git a/bpf/kmesh/workload/cgroup_sock.c b/bpf/kmesh/workload/cgroup_sock.c
index 862cff7a..77443a02 100644
--- a/bpf/kmesh/workload/cgroup_sock.c
+++ b/bpf/kmesh/workload/cgroup_sock.c
@@ -10,6 +10,7 @@
 #include "frontend.h"
 #include "bpf_common.h"
 #include "probe.h"
+#include <bpf/bpf_endian.h>
 
 static inline int sock_traffic_control(struct kmesh_context *kmesh_ctx)
 {
@@ -98,6 +99,26 @@ int cgroup_connect4_prog(struct bpf_sock_addr *ctx)
         return CGROUP_SOCK_OK;
     }
 
+    // TODO: should we always try to hijack 53 dns traffic?
+    if (ctx->user_port == bpf_htons(53)) {
+        backend_key backend_k = {0};
+        backend_value *backend_v = map_lookup_backend(&backend_k);
+
+        // dns proxy is not enabled
+        if (!backend_v) {
+            return CGROUP_SOCK_OK;
+        }
+
+        kmesh_ctx.dnat_ip.ip4 = backend_v->addr.ip4;
+        if (set_original_dst_info(&kmesh_ctx)) {
+            BPF_LOG(ERR, KMESH, "[IPv4]failed to set original destination info");
+            return CGROUP_SOCK_OK;
+        }
+
+        ctx->user_ip4 = backend_v->addr.ip4;
+        return CGROUP_SOCK_OK;
+    }
+
     if (ctx->protocol != IPPROTO_TCP)
         return CGROUP_SOCK_OK;
 
@@ -168,5 +189,77 @@ int cgroup_connect6_prog(struct bpf_sock_addr *ctx)
     return CGROUP_SOCK_OK;
 }
 
+SEC("cgroup/sendmsg4")
+int bpf_redirect_dns_send(struct bpf_sock_addr *ctx)
+{
+    struct kmesh_context kmesh_ctx = {0};
+    kmesh_ctx.ctx = ctx;
+    kmesh_ctx.orig_dst_addr.ip4 = ctx->user_ip4;
+    kmesh_ctx.dnat_ip.ip4 = ctx->user_ip4;
+    kmesh_ctx.dnat_port = ctx->user_port;
+
+    if (handle_kmesh_manage_process(&kmesh_ctx) || !is_kmesh_enabled(ctx)) {
+        return CGROUP_SOCK_OK;
+    }
+
+    if (ctx->user_port != bpf_htons(53)) {
+        return CGROUP_SOCK_OK;
+    }
+
+    backend_key backend_k = {0};
+    backend_value *backend_v = map_lookup_backend(&backend_k);
+
+    // dns proxy is not enabled
+    if (!backend_v) {
+        return CGROUP_SOCK_OK;
+    }
+
+    int ret = set_original_dst_info(&kmesh_ctx);
+    if (ret) {
+        BPF_LOG(ERR, KMESH, "[IPv4]failed to set original destination info, ret is %d\n", ret);
+        return CGROUP_SOCK_OK;
+    }
+
+    ctx->user_ip4 = backend_v->addr.ip4;
+
+    return CGROUP_SOCK_OK;
+}
+
+SEC("cgroup/recvmsg4")
+int bpf_restore_dns_recv(struct bpf_sock_addr *ctx)
+{
+    struct kmesh_context kmesh_ctx = {0};
+    kmesh_ctx.ctx = ctx;
+    kmesh_ctx.orig_dst_addr.ip4 = ctx->user_ip4;
+    kmesh_ctx.dnat_ip.ip4 = ctx->user_ip4;
+    kmesh_ctx.dnat_port = ctx->user_port;
+
+    if (handle_kmesh_manage_process(&kmesh_ctx) || !is_kmesh_enabled(ctx)) {
+        return CGROUP_SOCK_OK;
+    }
+
+    if (ctx->user_port != bpf_htons(53)) {
+        return CGROUP_SOCK_OK;
+    }
+
+    backend_key backend_k = {0};
+    backend_value *backend_v = map_lookup_backend(&backend_k);
+
+    // dns proxy is not enabled
+    if (!backend_v) {
+        return CGROUP_SOCK_OK;
+    }
+
+    struct sock_storage_data *storage = NULL;
+    storage = bpf_sk_storage_get(&map_of_sock_storage, ctx->sk, 0, BPF_LOCAL_STORAGE_GET_F_CREATE);
+    if (!storage) {
+        BPF_LOG(ERR, KMESH, "failed to get storage from map_of_sock_storage");
+        return CGROUP_SOCK_OK;
+    }
+
+    ctx->user_ip4 = storage->sk_tuple.ipv4.daddr;
+    return CGROUP_SOCK_OK;
+}
+
 char _license[] SEC("license") = "Dual BSD/GPL";
 int _version SEC("version") = 1;
diff --git a/deploy/charts/kmesh-helm/templates/daemonset.yaml b/deploy/charts/kmesh-helm/templates/daemonset.yaml
index 80344616..9d08dbde 100644
--- a/deploy/charts/kmesh-helm/templates/daemonset.yaml
+++ b/deploy/charts/kmesh-helm/templates/daemonset.yaml
@@ -51,6 +51,10 @@ spec:
           valueFrom:
             fieldRef:
               fieldPath: spec.nodeName
+        {{ if .Values.features.dnsProxy.enabled }}
+        - name: KMESH_ENABLE_DNS_PROXY
+          value: {{ .Values.features.dnsProxy.enabled | quote }}
+        {{- end }}
         image: {{ .Values.deploy.kmesh.image.repository }}:{{ .Values.deploy.kmesh.image.tag | default .Chart.AppVersion }}
         imagePullPolicy: {{ .Values.deploy.kmesh.imagePullPolicy }}
         name: kmesh
diff --git a/deploy/charts/kmesh-helm/values.yaml b/deploy/charts/kmesh-helm/values.yaml
index 769b8d94..2508c94c 100644
--- a/deploy/charts/kmesh-helm/values.yaml
+++ b/deploy/charts/kmesh-helm/values.yaml
@@ -14,3 +14,6 @@ deploy:
         memory: 800Mi
 kubernetesClusterDomain: cluster.local
 fullnameOverride: kmesh
+features:
+  dnsProxy:
+    enabled: true
\ No newline at end of file
diff --git a/go.mod b/go.mod
index eb701a70..2b264bd8 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 	github.com/envoyproxy/go-control-plane v0.13.2-0.20241125134052-fc612d4a3afa
 	github.com/fsnotify/fsnotify v1.8.0
+	github.com/google/go-cmp v0.7.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/miekg/dns v1.1.66
 	github.com/prometheus/client_golang v1.21.0
@@ -97,7 +98,6 @@ require (
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/cel-go v0.22.1 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
-	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-containerregistry v0.20.2 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
diff --git a/pkg/bpf/workload/sock_connection.go b/pkg/bpf/workload/sock_connection.go
index 53329289..ea254c73 100644
--- a/pkg/bpf/workload/sock_connection.go
+++ b/pkg/bpf/workload/sock_connection.go
@@ -35,10 +35,14 @@ import (
 )
 
 type SockConnWorkload struct {
-	Info  general.BpfInfo
-	Link  link.Link
-	Info6 general.BpfInfo
-	Link6 link.Link
+	Info        general.BpfInfo
+	Link        link.Link
+	Info6       general.BpfInfo
+	Link6       link.Link
+	InfoDns4    general.BpfInfo
+	LinkDns4    link.Link
+	InfoDnsRcv4 general.BpfInfo
+	LinkDnsRcv4 link.Link
 	bpf2go.KmeshCgroupSockWorkloadObjects
 }
 
@@ -47,6 +51,8 @@ func (sc *SockConnWorkload) NewBpf(cfg *options.BpfConfig) error {
 	sc.Info.BpfFsPath = cfg.BpfFsPath + "/bpf_kmesh_workload/sockconn/"
 	sc.Info.Cgroup2Path = cfg.Cgroup2Path
 	sc.Info6 = sc.Info
+	sc.InfoDns4 = sc.Info
+	sc.InfoDnsRcv4 = sc.Info
 
 	if err := os.MkdirAll(sc.Info.MapPath,
 		syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR|
@@ -114,6 +120,14 @@ func (sc *SockConnWorkload) LoadSockConn() error {
 		ebpf.UpdateAny); err != nil {
 		return err
 	}
+
+	prog = spec.Programs["bpf_redirect_dns_send"]
+	sc.InfoDns4.Type = prog.Type
+	sc.InfoDns4.AttachType = prog.AttachType
+
+	prog = spec.Programs["bpf_restore_dns_recv"]
+	sc.InfoDnsRcv4.Type = prog.Type
+	sc.InfoDnsRcv4.AttachType = prog.AttachType
 	return nil
 }
 
@@ -138,8 +152,22 @@ func (sc *SockConnWorkload) Attach() error {
 		Program: sc.KmeshCgroupSockWorkloadObjects.CgroupConnect6Prog,
 	}
 
+	cgoptdns4 := link.CgroupOptions{
+		Path:    sc.InfoDns4.Cgroup2Path,
+		Attach:  sc.InfoDns4.AttachType,
+		Program: sc.KmeshCgroupSockWorkloadObjects.BpfRedirectDnsSend,
+	}
+
+	cgoptdnsrecv4 := link.CgroupOptions{
+		Path:    sc.InfoDnsRcv4.Cgroup2Path,
+		Attach:  sc.InfoDnsRcv4.AttachType,
+		Program: sc.KmeshCgroupSockWorkloadObjects.BpfRestoreDnsRecv,
+	}
+
 	pinPath4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_prog")
 	pinPath6 := filepath.Join(sc.Info.BpfFsPath, "sockconn6_prog")
+	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 sc.Link, err = utils.BpfProgUpdate(pinPath4, cgopt4); err != nil {
@@ -149,6 +177,14 @@ func (sc *SockConnWorkload) Attach() error {
 		if sc.Link6, err = utils.BpfProgUpdate(pinPath6, cgopt6); err != nil {
 			return err
 		}
+
+		if sc.LinkDns4, err = utils.BpfProgUpdate(pinPathDns4, cgoptdns4); err != nil {
+			return err
+		}
+
+		if sc.LinkDnsRcv4, err = utils.BpfProgUpdate(pinPathDnsRecv4, cgoptdnsrecv4); err != nil {
+			return err
+		}
 	} else {
 		sc.Link, err = link.AttachCgroup(cgopt4)
 		if err != nil {
@@ -167,6 +203,23 @@ func (sc *SockConnWorkload) Attach() error {
 		if err := sc.Link6.Pin(pinPath6); err != nil {
 			return err
 		}
+
+		sc.LinkDns4, err = link.AttachCgroup(cgoptdns4)
+		if err != nil {
+			return err
+		}
+
+		if err := sc.LinkDns4.Pin(pinPathDns4); err != nil {
+			return err
+		}
+
+		sc.LinkDnsRcv4, err = link.AttachCgroup(cgoptdnsrecv4)
+		if err != nil {
+			return err
+		}
+		if err := sc.LinkDnsRcv4.Pin(pinPathDnsRecv4); err != nil {
+			return err
+		}
 	}
 
 	return err
diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go
index 9199cdfe..9e2b9086 100644
--- a/pkg/controller/controller.go
+++ b/pkg/controller/controller.go
@@ -19,8 +19,12 @@ package controller
 import (
 	"context"
 	"fmt"
+	"time"
 
 	"github.com/cilium/ebpf"
+	service_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
+	dnsclient "istio.io/istio/pkg/dns/client"
+	"istio.io/pkg/env"
 
 	"kmesh.net/kmesh/daemon/options"
 	"kmesh.net/kmesh/pkg/bpf"
@@ -31,12 +35,21 @@ import (
 	"kmesh.net/kmesh/pkg/controller/encryption/ipsec"
 	manage "kmesh.net/kmesh/pkg/controller/manage"
 	"kmesh.net/kmesh/pkg/controller/security"
+	"kmesh.net/kmesh/pkg/controller/workload"
+	"kmesh.net/kmesh/pkg/dns"
 	"kmesh.net/kmesh/pkg/kolog"
 	"kmesh.net/kmesh/pkg/kube"
 	"kmesh.net/kmesh/pkg/logger"
 	helper "kmesh.net/kmesh/pkg/utils"
 )
 
+var (
+	kmeshNamespace     = env.Register("POD_NAMESPACE", "kmesh-system", "kmesh namespace").Get()
+	clusterDomain      = env.Register("CLUSTER_DOMAIN", "cluster.local", "cluster domain").Get()
+	dnsForwardParallel = env.Register("DNS_FORWARD_PARALLEL", false,
+		"If set to true, kmesh will send parallel DNS queries to all upstream nameservers").Get()
+)
+
 var (
 	ctx, cancel = context.WithCancel(context.Background())
 	log         = logger.NewLoggerScope("controller")
@@ -52,6 +65,7 @@ type Controller struct {
 	enableSecretManager bool
 	bpfConfig           *options.BpfConfig
 	loader              *bpf.BpfLoader
+	dnsServer           *dnsclient.LocalDNSServer
 }
 
 func NewController(opts *options.BootstrapConfigs, bpfLoader *bpf.BpfLoader) *Controller {
@@ -148,11 +162,17 @@ func (c *Controller) Start(stopCh <-chan struct{}) error {
 	c.client = NewXdsClient(c.mode, c.bpfAdsObj, c.bpfWorkloadObj, c.bpfConfig.EnableMonitoring, c.bpfConfig.EnableProfiling)
 
 	if c.client.WorkloadController != nil {
-		c.client.WorkloadController.Run(ctx)
+		if err := c.client.WorkloadController.Run(ctx); err != nil {
+			return fmt.Errorf("failed to start workload controller: %+v", err)
+		}
 	} else {
 		c.client.AdsController.StartDnsController(stopCh)
 	}
 
+	if err := c.setupDNSProxy(); err != nil {
+		return fmt.Errorf("failed to start dns proxy: %+v", err)
+	}
+
 	return c.client.Run(stopCh)
 }
 
@@ -167,8 +187,47 @@ func (c *Controller) Stop() {
 	if c.client != nil {
 		c.client.Close()
 	}
+	if c.dnsServer != nil {
+		c.dnsServer.Close()
+	}
+	if c.client.WorkloadController != nil {
+		c.client.WorkloadController.Close()
+	}
 }
 
 func (c *Controller) GetXdsClient() *XdsClient {
 	return c.client
 }
+
+func (c *Controller) setupDNSProxy() error {
+	if workload.EnableDNSProxy {
+		server, err := dnsclient.NewLocalDNSServer(kmeshNamespace, clusterDomain, ":53", dnsForwardParallel)
+		if err != nil {
+			return fmt.Errorf("failed to start local dns server: %v", err)
+		}
+		ntb := dns.NewNameTableBuilder(c.client.WorkloadController.Processor.ServiceCache, c.client.WorkloadController.Processor.WorkloadCache)
+
+		debounceTime := time.Second
+		timer := time.NewTimer(0)
+		<-timer.C
+		h := func(rsp *service_discovery_v3.DeltaDiscoveryResponse) error {
+			// debounce
+			if timer.Reset(debounceTime) {
+				return nil
+			}
+
+			go func() {
+				<-timer.C
+				log.Debugf("trigger name table update")
+				server.UpdateLookupTable(ntb.BuildNameTable())
+			}()
+
+			return nil
+		}
+
+		c.client.WorkloadController.Processor.WithResourceHandlers(workload.AddressType, h)
+		server.StartDNS()
+		c.dnsServer = server
+	}
+	return nil
+}
diff --git a/pkg/controller/workload/features.go b/pkg/controller/workload/features.go
new file mode 100644
index 00000000..f77e2091
--- /dev/null
+++ b/pkg/controller/workload/features.go
@@ -0,0 +1,24 @@
+/*
+ * 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 workload
+
+import "istio.io/pkg/env"
+
+var (
+	EnableDNSProxy = env.Register("KMESH_ENABLE_DNS_PROXY", false, "When DNS proxy is enabled, a DNS server will be started in kmesh daemon"+
+		"and serve DNS requests. DNS requests of kmesh-managed pods will be redirected to kmesh daemon.").Get()
+)
diff --git a/pkg/controller/workload/workload_controller.go b/pkg/controller/workload/workload_controller.go
index 1eb57ec9..3facbe89 100644
--- a/pkg/controller/workload/workload_controller.go
+++ b/pkg/controller/workload/workload_controller.go
@@ -66,7 +66,11 @@ func NewController(bpfWorkload *bpfwl.BpfWorkload, enableMonitoring, enablePerfM
 	return c
 }
 
-func (c *Controller) Run(ctx context.Context) {
+func (c *Controller) Run(ctx context.Context) error {
+	if err := c.Processor.PrepareDNSProxy(); err != nil {
+		log.Errorf("failed to prepare for dns proxy, err: %+v", err)
+		return err
+	}
 	var wg sync.WaitGroup
 	wg.Add(2)
 	go func() {
@@ -89,6 +93,11 @@ func (c *Controller) Run(ctx context.Context) {
 	if c.OperationMetricController != nil {
 		go c.OperationMetricController.Run(ctx, c.bpfWorkloadObj.SockConn.KmPerfInfo)
 	}
+	return nil
+}
+
+func (c *Controller) Close() error {
+	return c.Processor.Close()
 }
 
 func (c *Controller) WorkloadStreamCreateAndSend(client discoveryv3.AggregatedDiscoveryServiceClient, ctx context.Context) error {
diff --git a/pkg/controller/workload/workload_processor.go b/pkg/controller/workload/workload_processor.go
index c87bece9..e181f7d6 100644
--- a/pkg/controller/workload/workload_processor.go
+++ b/pkg/controller/workload/workload_processor.go
@@ -72,6 +72,8 @@ type Processor struct {
 	authzDone       chan struct{}
 	addressRespOnce sync.Once
 	authzRespOnce   sync.Once
+
+	handlers map[string][]func(resp *service_discovery_v3.DeltaDiscoveryResponse) error
 }
 
 func NewProcessor(workloadMap bpf2go.KmeshCgroupSockWorkloadMaps) *Processor {
@@ -88,7 +90,47 @@ func NewProcessor(workloadMap bpf2go.KmeshCgroupSockWorkloadMaps) *Processor {
 		locality:      bpf.NewLocalityCache(),
 		addressDone:   make(chan struct{}, 1),
 		authzDone:     make(chan struct{}, 1),
+		handlers:      map[string][]func(resp *service_discovery_v3.DeltaDiscoveryResponse) error{},
+	}
+}
+
+func (p *Processor) WithResourceHandlers(typeUrl string, h ...func(resp *service_discovery_v3.DeltaDiscoveryResponse) error) *Processor {
+	p.handlers[typeUrl] = append(p.handlers[typeUrl], h...)
+	return p
+}
+
+func (p *Processor) PrepareDNSProxy() error {
+	bk := &bpf.BackendKey{
+		BackendUid: 0,
+	}
+
+	// when dns proxy is not enabled, we unregister kmesh daemon from bpf map
+	if !EnableDNSProxy {
+		return p.bpf.BackendDelete(bk)
+	}
+
+	podIp := os.Getenv("INSTANCE_IP")
+	if podIp == "" {
+		return fmt.Errorf("ip of kmesh daemon is not set")
+	}
+
+	log.Infof("dns proxy is enabled and will be redirected, ip: %s", podIp)
+
+	bv := &bpf.BackendValue{}
+	nets.CopyIpByteFromSlice(&bv.Ip, netip.MustParseAddr(podIp).AsSlice())
+	if err := p.bpf.BackendUpdate(bk, bv); err != nil {
+		log.Errorf("failed to register kmesh daemon ip, err: %+v", err)
+		return err
+	}
+
+	return nil
+}
+
+func (p *Processor) Close() error {
+	bk := &bpf.BackendKey{
+		BackendUid: 0,
 	}
+	return p.bpf.BackendDelete(bk)
 }
 
 func newDeltaRequest(typeUrl string, names []string, initialResourceVersions map[string]string) *service_discovery_v3.DeltaDiscoveryRequest {
@@ -138,6 +180,14 @@ func (p *Processor) processWorkloadResponse(rsp *service_discovery_v3.DeltaDisco
 	default:
 		err = fmt.Errorf("unsupported type url %s", rsp.GetTypeUrl())
 	}
+
+	for _, h := range p.handlers[rsp.GetTypeUrl()] {
+		err := h(rsp)
+		if err != nil {
+			log.Error(err)
+		}
+	}
+
 	if err != nil {
 		log.Error(err)
 	}
diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go
index 48787a80..3063d0c7 100644
--- a/pkg/dns/dns.go
+++ b/pkg/dns/dns.go
@@ -39,13 +39,16 @@ const (
 	DeRefreshInterval        = 15 * time.Second
 )
 
+type Resolver interface {
+	Resolve(m *dns.Msg) (*dns.Msg, error)
+}
+
 type DNSResolver struct {
-	client            *dns.Client
-	DnsChan           chan string
-	resolvConfServers []string
-	cache             map[string]*DomainCacheEntry
-	refreshQueue      workqueue.TypedDelayingInterface[any]
 	sync.RWMutex
+	DnsChan      chan string
+	cache        map[string]*DomainCacheEntry
+	refreshQueue workqueue.TypedDelayingInterface[any]
+	delegates    []Resolver
 }
 
 type DomainCacheEntry struct {
@@ -59,29 +62,33 @@ type DomainInfo struct {
 
 func NewDNSResolver() (*DNSResolver, error) {
 	r := &DNSResolver{
-		DnsChan: make(chan string, 100),
-		cache:   map[string]*DomainCacheEntry{},
-		client: &dns.Client{
-			DialTimeout:  5 * time.Second,
-			ReadTimeout:  5 * time.Second,
-			WriteTimeout: 5 * time.Second,
-		},
+		DnsChan:      make(chan string, 100),
+		cache:        map[string]*DomainCacheEntry{},
 		refreshQueue: workqueue.NewTypedDelayingQueueWithConfig(workqueue.TypedDelayingQueueConfig[any]{Name: "refreshQueue"}),
+		delegates:    []Resolver{},
 	}
 
 	dnsConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf")
 	if err != nil {
 		return nil, err
 	}
+
 	if dnsConfig != nil {
-		for _, s := range dnsConfig.Servers {
-			r.resolvConfServers = append(r.resolvConfServers, net.JoinHostPort(s, dnsConfig.Port))
+		ur := NewUpstreamResolver()
+		for _, server := range dnsConfig.Servers {
+			ur.WithUpstreams(net.JoinHostPort(server, dnsConfig.Port))
 		}
+		r.WithDelegateResolver(ur)
 	}
 
 	return r, nil
 }
 
+func (r *DNSResolver) WithDelegateResolver(resolvers ...Resolver) *DNSResolver {
+	r.delegates = append(r.delegates, resolvers...)
+	return r
+}
+
 func (r *DNSResolver) StartDnsResolver(stop <-chan struct{}) {
 	for {
 		select {
@@ -196,22 +203,27 @@ func (r *DNSResolver) doResolve(domain string) ([]string, time.Duration, error)
 // Query is copied and adapted from github.com/istio/istio/pilot/pkg/model/network.go.
 func (r *DNSResolver) Query(req *dns.Msg) *dns.Msg {
 	var response *dns.Msg
-	for _, upstream := range r.resolvConfServers {
-		resp, _, err := r.client.Exchange(req, upstream)
-		if err != nil || resp == nil {
+
+	for _, resolver := range r.delegates {
+		res, err := resolver.Resolve(req)
+		if err != nil {
+			log.Errorf("failed to query delegate resolver: %v", err)
 			continue
 		}
 
-		response = resp
-		if resp.Rcode == dns.RcodeSuccess {
+		response = res
+		if res.Rcode == dns.RcodeSuccess {
+			log.Debugf("successfully queried delegate resolver, resp: %+v", res)
 			break
 		}
 	}
+
 	if response == nil {
 		response = new(dns.Msg)
 		response.SetReply(req)
 		response.Rcode = dns.RcodeServerFailure
 	}
+
 	return response
 }
 
diff --git a/pkg/dns/nametable.go b/pkg/dns/nametable.go
new file mode 100644
index 00000000..12b80e8a
--- /dev/null
+++ b/pkg/dns/nametable.go
@@ -0,0 +1,163 @@
+/*
+ * 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 dns
+
+import (
+	"fmt"
+	"net"
+
+	"istio.io/istio/pilot/pkg/serviceregistry/provider"
+	dnsproto "istio.io/istio/pkg/dns/proto"
+	"istio.io/istio/pkg/env"
+	"istio.io/istio/pkg/slices"
+	netutil "istio.io/istio/pkg/util/net"
+
+	"kmesh.net/kmesh/api/v2/workloadapi"
+	"kmesh.net/kmesh/pkg/controller/workload/cache"
+)
+
+var (
+	clusterDomain = env.Register("CLUSTER_DOMAIN", "cluster.local", "Cluster domain name.").Get()
+)
+
+type NameTableBuilder interface {
+	BuildNameTable() *dnsproto.NameTable
+}
+
+type nameTableBuilder struct {
+	serviceCache  cache.ServiceCache
+	workloadCache cache.WorkloadCache
+}
+
+func NewNameTableBuilder(serviceCache cache.ServiceCache, workloadCache cache.WorkloadCache) NameTableBuilder {
+	return &nameTableBuilder{
+		serviceCache:  serviceCache,
+		workloadCache: workloadCache,
+	}
+}
+
+// THIS FUNC IS MODIFIED BASED ON istio.io/istio/pkg/dns/server/name_table.go
+//
+// BuildNameTable produces a table of hostnames and their associated IPs that can then
+// be used by the agent to resolve DNS. This logic is always active. However, local DNS resolution
+// will only be effective if DNS capture is enabled in the proxy
+func (b *nameTableBuilder) BuildNameTable() *dnsproto.NameTable {
+	out := &dnsproto.NameTable{
+		Table: make(map[string]*dnsproto.NameTable_NameInfo),
+	}
+
+	workloads := b.workloadCache.List()
+	for _, svc := range b.serviceCache.List() {
+		addressList := []string{}
+
+		for _, svcAddress := range svc.Addresses {
+			ip := net.IP(svcAddress.Address)
+
+			if ip.IsUnspecified() {
+				break
+			}
+
+			// Filter out things we cannot parse as IP. Generally this means CIDRs, as anything else
+			// should be caught in validation.
+			if !netutil.IsValidIPAddress(ip.String()) {
+				continue
+			}
+
+			addressList = append(addressList, ip.String())
+		}
+
+		// headless
+		if len(addressList) == 0 {
+			endpoints := lookupHeadlessEndpoints(svc, workloads)
+			for _, endpoint := range endpoints {
+				for _, address := range endpoint.Addresses {
+					ip := net.IP(address)
+					addressList = append(addressList, ip.String())
+				}
+			}
+		}
+
+		if len(addressList) == 0 {
+			continue
+		}
+
+		hostName := svc.Hostname
+		registry := guessServiceRegistry(svc)
+		if ni, f := out.Table[hostName]; !f {
+			nameInfo := &dnsproto.NameTable_NameInfo{
+				Ips:      addressList,
+				Registry: registry.String(),
+			}
+			if registry == provider.Kubernetes {
+				// The agent will take care of resolving a, a.ns, a.ns.svc, etc.
+				// No need to provide a DNS entry for each variant.
+				//
+				// NOTE: This is not done for Kubernetes Multi-Cluster Services (MCS) hosts, in order
+				// to avoid conflicting with the entries for the regular (cluster.local) service.
+				nameInfo.Namespace = svc.Namespace
+				nameInfo.Shortname = svc.Name
+			}
+			out.Table[hostName] = nameInfo
+		} else if provider.ID(ni.Registry) != provider.Kubernetes {
+			// 2 possible cases:
+			// 1. If the SE has multiple addresses(vips) specified, merge the ips
+			// 2. If the previous SE is a decorator of the k8s service, give precedence to the k8s service
+			if registry == provider.Kubernetes {
+				ni.Ips = addressList
+				ni.Registry = string(provider.Kubernetes)
+				ni.Namespace = svc.Namespace
+				ni.Shortname = svc.Name
+			} else {
+				ni.Ips = append(ni.Ips, addressList...)
+			}
+		}
+	}
+
+	return out
+}
+
+func lookupHeadlessEndpoints(svc *workloadapi.Service, workloads []*workloadapi.Workload) []*workloadapi.Workload {
+	key := fmt.Sprintf("%s/%s", svc.Namespace, svc.Hostname)
+	return slices.Filter(workloads, func(w *workloadapi.Workload) bool {
+		if len(w.Addresses) == 0 {
+			return false
+		}
+
+		for _, address := range w.Addresses {
+			ip := net.IP(address)
+			if !netutil.IsValidIPAddress(ip.String()) {
+				return false
+			}
+		}
+
+		for service := range w.Services {
+			if service == key {
+				return true
+			}
+		}
+		return false
+	})
+}
+
+// guessServiceRegistry try to guess whether the service is a kubernetes or an external service
+// by checking if the host name of the service in format <service-name>.<namespace>.svc.<cluster-domain>
+func guessServiceRegistry(svc *workloadapi.Service) provider.ID {
+	kubernetesSvcHostName := fmt.Sprintf("%s.%s.svc.%s", svc.Name, svc.Namespace, clusterDomain)
+	if svc.Hostname == kubernetesSvcHostName {
+		return provider.Kubernetes
+	}
+	return provider.External
+}
diff --git a/pkg/dns/upstream.go b/pkg/dns/upstream.go
new file mode 100644
index 00000000..8003eda3
--- /dev/null
+++ b/pkg/dns/upstream.go
@@ -0,0 +1,68 @@
+/*
+ * 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 dns
+
+import (
+	"time"
+
+	"github.com/miekg/dns"
+)
+
+type upstreamResolver struct {
+	*dns.Client
+	upstreams []string
+}
+
+func NewUpstreamResolver(upstreams ...string) *upstreamResolver {
+	return &upstreamResolver{
+		upstreams: upstreams,
+		Client: &dns.Client{
+			DialTimeout:  5 * time.Second,
+			ReadTimeout:  5 * time.Second,
+			WriteTimeout: 5 * time.Second,
+		},
+	}
+}
+
+func (r *upstreamResolver) WithUpstreams(upstreams ...string) *upstreamResolver {
+	r.upstreams = append(r.upstreams, upstreams...)
+	return r
+}
+
+func (r *upstreamResolver) Resolve(req *dns.Msg) (*dns.Msg, error) {
+	var response *dns.Msg
+
+	for _, upstream := range r.upstreams {
+		resp, _, err := r.Exchange(req, upstream)
+		if err != nil || resp == nil {
+			continue
+		}
+
+		response = resp
+		if resp.Rcode == dns.RcodeSuccess {
+			break
+		}
+	}
+
+	if response == nil {
+		response = new(dns.Msg)
+		response.SetReply(req)
+		response.Rcode = dns.RcodeServerFailure
+	}
+
+	return response, nil
+}



Test patch:
diff --git a/pkg/dns/dns_test.go b/pkg/dns/dns_test.go
index 4cb9523d..c7b6ce54 100644
--- a/pkg/dns/dns_test.go
+++ b/pkg/dns/dns_test.go
@@ -35,7 +35,7 @@ func TestDNS(t *testing.T) {
 	defer close(stopCh)
 	// testDNSResolver.StartAdsDnsResolver(stopCh)
 	dnsServer := fakeDNSServer.Server.PacketConn.LocalAddr().String()
-	testDNSResolver.resolvConfServers = []string{dnsServer}
+	testDNSResolver.delegates = []Resolver{NewUpstreamResolver(dnsServer)}
 	go testDNSResolver.StartDnsResolver(stopCh)
 
 	testCases := []struct {
diff --git a/pkg/dns/nametable_test.go b/pkg/dns/nametable_test.go
new file mode 100644
index 00000000..c722498a
--- /dev/null
+++ b/pkg/dns/nametable_test.go
@@ -0,0 +1,148 @@
+/*
+ * 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 dns
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/testing/protocmp"
+	dnsproto "istio.io/istio/pkg/dns/proto"
+
+	"kmesh.net/kmesh/api/v2/workloadapi"
+	"kmesh.net/kmesh/pkg/controller/workload/cache"
+)
+
+func TestBuildNameTable(t *testing.T) {
+	type TestCase struct {
+		name      string
+		services  []*workloadapi.Service
+		workloads []*workloadapi.Workload
+		want      *dnsproto.NameTable
+	}
+
+	testCases := []TestCase{
+		{
+			name: "Common kubernetes service test",
+			services: []*workloadapi.Service{
+				{
+					Name:      "svc1",
+					Namespace: "ns1",
+					Hostname:  "svc1.ns1.svc.cluster.local",
+					Addresses: []*workloadapi.NetworkAddress{
+						{
+							Address: []byte{10, 0, 0, 1},
+						},
+					},
+				},
+			},
+			want: &dnsproto.NameTable{
+				Table: map[string]*dnsproto.NameTable_NameInfo{
+					"svc1.ns1.svc.cluster.local": {
+						Ips:       []string{"10.0.0.1"},
+						Registry:  "Kubernetes",
+						Namespace: "ns1",
+						Shortname: "svc1",
+					},
+				},
+			},
+		},
+		{
+			name: "Headless service test",
+			services: []*workloadapi.Service{
+				{
+					Name:      "svc1",
+					Namespace: "ns1",
+					Hostname:  "svc1.ns1.svc.cluster.local",
+				},
+			},
+			workloads: []*workloadapi.Workload{
+				{
+					Name:      "workload1",
+					Namespace: "ns1",
+					Addresses: [][]byte{
+						{
+							10, 0, 0, 1,
+						},
+					},
+					Services: map[string]*workloadapi.PortList{
+						"ns1/svc1.ns1.svc.cluster.local": {
+							Ports: []*workloadapi.Port{
+								{
+									ServicePort: 80,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &dnsproto.NameTable{
+				Table: map[string]*dnsproto.NameTable_NameInfo{
+					"svc1.ns1.svc.cluster.local": {
+						Ips:       []string{"10.0.0.1"},
+						Registry:  "Kubernetes",
+						Namespace: "ns1",
+						Shortname: "svc1",
+					},
+				},
+			},
+		},
+		{
+			name: "ServiceEntry test",
+			services: []*workloadapi.Service{
+				{
+					Name:      "svc1",
+					Namespace: "ns1",
+					Hostname:  "kmesh-fake.com",
+					Addresses: []*workloadapi.NetworkAddress{
+						{
+							Address: []byte{240, 0, 0, 1},
+						},
+					},
+				},
+			},
+			want: &dnsproto.NameTable{
+				Table: map[string]*dnsproto.NameTable_NameInfo{
+					"kmesh-fake.com": {
+						Ips:      []string{"240.0.0.1"},
+						Registry: "External",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			serviceCache := cache.NewServiceCache()
+			workloadCache := cache.NewWorkloadCache()
+
+			for _, svc := range tc.services {
+				serviceCache.AddOrUpdateService(svc)
+			}
+
+			for _, workload := range tc.workloads {
+				workloadCache.AddOrUpdateWorkload(workload)
+			}
+
+			ntb := NewNameTableBuilder(serviceCache, workloadCache)
+			got := ntb.BuildNameTable()
+			if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" {
+				t.Fatalf("got diff: %v", diff)
+			}
+		})
+	}
+}
diff --git a/test/e2e/baseline_test.go b/test/e2e/baseline_test.go
index 3c162524..5b5b2431 100644
--- a/test/e2e/baseline_test.go
+++ b/test/e2e/baseline_test.go
@@ -26,6 +26,7 @@ package kmesh
 import (
 	"context"
 	"fmt"
+	"net"
 	"net/http"
 	"net/netip"
 	"os/exec"
@@ -170,6 +171,71 @@ func TestPodIP(t *testing.T) {
 	})
 }
 
+func TestDNSProxy(t *testing.T) {
+	runTest(t, func(t framework.TestContext, src echo.Instance, dst echo.Instance, opt echo.CallOptions) {
+		if opt.Scheme != scheme.HTTP {
+			return
+		}
+		if !dst.Config().HasServiceAddressedWaypointProxy() {
+			return
+		}
+		// skip ipv6 now since it is not supported
+		if net.ParseIP(dst.Address()).To4() == nil {
+			return
+		}
+		t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+			"Destination": dst.Config().Service,
+		}, `apiVersion: networking.istio.io/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: echo-alias
+spec:
+  exportTo:
+  - '*'
+  hosts:
+  - kmesh-fake.com
+  ports:
+  - name: http
+    number: 80
+    protocol: HTTP
+  addresses:
+  - 240.240.0.1
+  resolution: DNS
+`).ApplyOrFail(t)
+		t.ConfigIstio().Eval(apps.Namespace.Name(), map[string]string{
+			"Destination": dst.Config().Service,
+		}, `apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: echo-alias-route
+spec:
+  hosts:
+  - kmesh-fake.com
+  http:
+  - match:
+    - port: 80
+      uri:
+        prefix: /
+    name: route-to-echo
+    route:
+    - destination:
+        host: "{{.Destination}}"
+`).ApplyOrFail(t)
+
+		SetWaypoint(t, apps.Namespace.Name(), "echo-alias", "waypoint", ServiceEntry)
+
+		opt = opt.DeepCopy()
+		opt.Count = 5
+		opt.Timeout = time.Second * 10
+		if opt.HTTP.Headers == nil {
+			opt.HTTP.Headers = map[string][]string{}
+		}
+		opt.Address = "kmesh-fake.com"
+		opt.Check = check.OK()
+		src.CallOrFail(t, opt)
+	})
+}
+
 func TestTrafficSplit(t *testing.T) {
 	runTest(t, func(t framework.TestContext, src echo.Instance, dst echo.Instance, opt echo.CallOptions) {
 		// Need at least one waypoint proxy and HTTP
@@ -876,6 +942,7 @@ const (
 	Namespace Granularity = iota
 	Service
 	Workload
+	ServiceEntry
 )
 
 func UnsetWaypoint(t framework.TestContext, ns string, name string, granularity Granularity) {
@@ -902,6 +969,9 @@ func SetWaypoint(t framework.TestContext, ns string, name string, waypoint strin
 			case Workload:
 				_, err := c.Kube().CoreV1().Pods(ns).Patch(context.TODO(), name, types.MergePatchType, labels, metav1.PatchOptions{})
 				return err
+			case ServiceEntry:
+				_, err := c.Istio().NetworkingV1().ServiceEntries(ns).Patch(context.TODO(), name, types.MergePatchType, labels, metav1.PatchOptions{})
+				return err
 			}
 
 			return nil
diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go
index 987c3d09..3c6ce125 100644
--- a/test/e2e/restart_test.go
+++ b/test/e2e/restart_test.go
@@ -41,6 +41,9 @@ import (
 
 func TestKmeshRestart(t *testing.T) {
 	framework.NewTest(t).Run(func(t framework.TestContext) {
+		// if dns proxy is enabled, when kmesh restarts, the DNS query will fail
+		configureDNSProxy(t, false)
+
 		src := apps.EnrolledToKmesh[0]
 		dst := apps.ServiceWithWaypointAtServiceGranularity
 		options := echo.CallOptions{
@@ -63,11 +66,12 @@ func TestKmeshRestart(t *testing.T) {
 		restartKmesh(t)
 
 		g.Stop().CheckSuccessRate(t, 1)
+
+		configureDNSProxy(t, true)
 	})
 }
 
 func restartKmesh(t framework.TestContext) {
-	patchOpts := metav1.PatchOptions{}
 	patchData := fmt.Sprintf(`{
 			"spec": {
 				"template": {
@@ -79,6 +83,34 @@ func restartKmesh(t framework.TestContext) {
 				}
 			}
 		}`, time.Now().Format(time.RFC3339))
+	patchKmesh(t, patchData)
+}
+
+func configureDNSProxy(t framework.TestContext, enabled bool) {
+	patchData := fmt.Sprintf(`{
+    "spec": {
+        "template": {
+            "spec": {
+                "containers": [
+                    {
+                        "name": "kmesh",
+                        "env": [
+                            {
+                                "name": "KMESH_ENABLE_DNS_PROXY",
+                                "value": "%t"
+                            }
+                        ]
+                    }
+                ]
+            }
+        }
+    }
+}`, enabled)
+	patchKmesh(t, patchData)
+}
+
+func patchKmesh(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 {
diff --git a/test/e2e/run_test.sh b/test/e2e/run_test.sh
index a29e37bc..74fa51bb 100755
--- a/test/e2e/run_test.sh
+++ b/test/e2e/run_test.sh
@@ -114,8 +114,11 @@ function setup_istio() {
 }
 
 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" \
+		$extra_args
 
 	# Wait for all Kmesh pods to be ready.
 	while true; do