Repository: kumahq/kuma
Pull request #1807 (2021-04-13 13:39:38)

feat(kuma-cp) generate DNS filter
### Summary

This PR introduces support to configure built-in DNS filter in Kuma.
This is meant to be used only for transparent proxy.

The flow will be as follow:
* IPtables will intercept traffic to DNS on 53 and redirect it to CoreDNS started by Kuma DP
* CoreDNS will be configured to first send a DNS request to Envoy DNS and then if the domain is not found it the fallbacks to /etc/resolv.conf

Current implementation contains one workaround:
* Right now we cannot disable upstream resolvers in DNSFilter therefore we need to configure upstream resolver to dummy resolver that always responds in empty response. CoreDNS will be configured to open extra dns listener with dummy responses.

One question that might occurs is why not use Envoy DNS directly. The reasons are
* DNSFilter does not support SRV records (and probably more since it was introduced just recently)
* We want to start DNS server before Envoy even receives the config. Otherwise, we could not use the hostname as an address for kuma cp

With this change, VIPs do not have to be stable anymore between instances of the control plane. We may drop persisting it but we are keeping it for now for simplicity of implementation.

Other PRs will follow to achieve full functionality.

### Documentation

- Docs will arrive when we have full feature



Patch:
diff --git a/app/kuma-dp/cmd/run.go b/app/kuma-dp/cmd/run.go
index bd3a85792..5e9e5f39f 100644
--- a/app/kuma-dp/cmd/run.go
+++ b/app/kuma-dp/cmd/run.go
@@ -118,7 +118,7 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 			}
 			shouldQuit := setupQuitChannel()
 
-			dataplane, err := envoy.New(envoy.Opts{
+			opts := envoy.Opts{
 				Config:          *cfg,
 				Generator:       rootCtx.BootstrapGenerator,
 				Dataplane:       dp,
@@ -127,7 +127,12 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 				Stderr:          cmd.OutOrStderr(),
 				Quit:            shouldQuit,
 				LogLevel:        rootCtx.LogLevel,
-			})
+			}
+			if cfg.DNS.Enabled {
+				opts.DNSPort = cfg.DNS.EnvoyDNSPort
+				opts.EmptyDNSPort = cfg.DNS.CoreDNSEmptyPort
+			}
+			dataplane, err := envoy.New(opts)
 			if err != nil {
 				return err
 			}
@@ -160,6 +165,12 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.Resource, "dataplane", "", "Dataplane template to apply (YAML or JSON)")
 	cmd.PersistentFlags().StringVarP(&cfg.DataplaneRuntime.ResourcePath, "dataplane-file", "d", "", "Path to Dataplane template to apply (YAML or JSON)")
 	cmd.PersistentFlags().StringToStringVarP(&cfg.DataplaneRuntime.ResourceVars, "dataplane-var", "v", map[string]string{}, "Variables to replace Dataplane template")
+	cmd.PersistentFlags().BoolVar(&cfg.DNS.Enabled, "dns-enabled", cfg.DNS.Enabled, "If true then builtin DNS functionality is enabled and CoreDNS server is started")
+	cmd.PersistentFlags().Uint32Var(&cfg.DNS.EnvoyDNSPort, "dns-envoy-port", cfg.DNS.EnvoyDNSPort, "A port that handles Virtual IP resolving by Envoy. CoreDNS should be configured that it first tries to use this DNS resolver and then the real one")
+	cmd.PersistentFlags().Uint32Var(&cfg.DNS.CoreDNSPort, "dns-coredns-port", cfg.DNS.CoreDNSPort, "A port that handles DNS requests. When transparent proxy is enabled then iptables will redirect DNS traffic to this port.")
+	cmd.PersistentFlags().Uint32Var(&cfg.DNS.CoreDNSEmptyPort, "dns-coredns-empty-port", cfg.DNS.CoreDNSEmptyPort, "A port that always responds with empty NXDOMAIN respond. It is required to implement a fallback to a real DNS.")
+	cmd.PersistentFlags().StringVar(&cfg.DNS.CoreDNSBinaryPath, "dns-coredns-path", cfg.DNS.CoreDNSBinaryPath, "A path to CoreDNS binary.")
+	cmd.PersistentFlags().StringVar(&cfg.DNS.CoreDNSConfigTemplatePath, "dns-coredns-config-template-path", cfg.DNS.CoreDNSConfigTemplatePath, "A path to a CoreDNS config template.")
 	return cmd
 }
 
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy.go b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
index bf0f6e754..f1a11845d 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
@@ -29,6 +29,8 @@ var (
 type BootstrapParams struct {
 	Dataplane        *rest.Resource
 	BootstrapVersion types.BootstrapVersion
+	DNSPort          uint32
+	EmptyDNSPort     uint32
 	EnvoyVersion     EnvoyVersion
 	DynamicMetadata  map[string]string
 }
@@ -40,6 +42,8 @@ type Opts struct {
 	Generator       BootstrapConfigFactoryFunc
 	Dataplane       *rest.Resource
 	DynamicMetadata map[string]string
+	DNSPort         uint32
+	EmptyDNSPort    uint32
 	Stdout          io.Writer
 	Stderr          io.Writer
 	Quit            chan struct{}
@@ -122,6 +126,8 @@ func (e *Envoy) Start(stop <-chan struct{}) error {
 	bootstrapConfig, version, err := e.opts.Generator(e.opts.Config.ControlPlane.URL, e.opts.Config, BootstrapParams{
 		Dataplane:        e.opts.Dataplane,
 		BootstrapVersion: types.BootstrapVersion(e.opts.Config.Dataplane.BootstrapVersion),
+		DNSPort:          e.opts.DNSPort,
+		EmptyDNSPort:     e.opts.EmptyDNSPort,
 		EnvoyVersion:     *envoyVersion,
 		DynamicMetadata:  e.opts.DynamicMetadata,
 	})
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
index 391f3ddd7..6a0e30a30 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
@@ -139,6 +139,8 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 			},
 		},
 		DynamicMetadata: params.DynamicMetadata,
+		DNSPort:         params.DNSPort,
+		EmptyDNSPort:    params.EmptyDNSPort,
 	}
 	jsonBytes, err := json.Marshal(request)
 	if err != nil {
diff --git a/pkg/config/app/kuma-dp/config.go b/pkg/config/app/kuma-dp/config.go
index a0eea9dbe..4b74a625c 100644
--- a/pkg/config/app/kuma-dp/config.go
+++ b/pkg/config/app/kuma-dp/config.go
@@ -30,6 +30,14 @@ func DefaultConfig() Config {
 			BinaryPath: "envoy",
 			ConfigDir:  "", // if left empty, a temporary directory will be generated automatically
 		},
+		DNS: DNS{
+			Enabled:                   false,
+			CoreDNSPort:               15053,
+			EnvoyDNSPort:              15054,
+			CoreDNSEmptyPort:          15055,
+			CoreDNSBinaryPath:         "coredns",
+			CoreDNSConfigTemplatePath: "",
+		},
 	}
 }
 
@@ -41,12 +49,15 @@ type Config struct {
 	Dataplane Dataplane `yaml:"dataplane,omitempty"`
 	// DataplaneRuntime defines the context in which dataplane (Envoy) runs.
 	DataplaneRuntime DataplaneRuntime `yaml:"dataplaneRuntime,omitempty"`
+	// DNS defines a configuration for builtin DNS in Kuma DP
+	DNS DNS `yaml:"dns,omitempty"`
 }
 
 func (c *Config) Sanitize() {
 	c.ControlPlane.Sanitize()
 	c.Dataplane.Sanitize()
 	c.DataplaneRuntime.Sanitize()
+	c.DNS.Sanitize()
 }
 
 // ControlPlane defines coordinates of the Control Plane.
@@ -144,6 +155,9 @@ func (c *Config) Validate() (errs error) {
 	if err := c.DataplaneRuntime.Validate(); err != nil {
 		errs = multierr.Append(errs, errors.Wrapf(err, ".DataplaneRuntime is not valid"))
 	}
+	if err := c.DNS.Validate(); err != nil {
+		errs = multierr.Append(errs, errors.Wrapf(err, ".DNS is not valid"))
+	}
 	return
 }
 
@@ -218,3 +232,40 @@ func (d *ApiServer) Validate() (errs error) {
 	}
 	return
 }
+
+type DNS struct {
+	// If true then builtin DNS functionality is enabled and CoreDNS server is started
+	Enabled bool `yaml:"enabled,omitempty" envconfig:"kuma_dns_enabled"`
+	// CoreDNSPort defines a port that handles DNS requests. When transparent proxy is enabled then iptables will redirect DNS traffic to this port.
+	CoreDNSPort uint32 `yaml:"coreDnsPort,omitempty" envconfig:"kuma_dns_core_dns_port"`
+	// CoreDNSEmptyPort defines a port that always responds with empty NXDOMAIN respond. It is required to implement a fallback to a real DNS
+	CoreDNSEmptyPort uint32 `yaml:"coreDnsEmptyPort,omitempty" envconfig:"kuma_dns_core_dns_empty_port"`
+	// EnvoyDNSPort defines a port that handles Virtual IP resolving by Envoy. CoreDNS should be configured that it first tries to use this DNS resolver and then the real one.
+	EnvoyDNSPort uint32 `yaml:"envoyDnsPort,omitempty" envconfig:"kuma_dns_envoy_dns_port"`
+	// CoreDNSBinaryPath defines a path to CoreDNS binary.
+	CoreDNSBinaryPath string `yaml:"coreDnsBinaryPath,omitempty" envconfig:"kuma_dns_core_dns_binary_path"`
+	// CoreDNSConfigTemplatePath defines a path to a CoreDNS config template.
+	CoreDNSConfigTemplatePath string `yaml:"coreDnsConfigTemplatePath,omitempty" envconfig:"kuma_dns_core_dns_config_template_path"`
+}
+
+func (d *DNS) Sanitize() {
+}
+
+func (d *DNS) Validate() error {
+	if !d.Enabled {
+		return nil
+	}
+	if d.CoreDNSPort > 65353 {
+		return errors.New(".CoreDNSPort has to be in [0, 65353] range")
+	}
+	if d.CoreDNSEmptyPort > 65353 {
+		return errors.New(".CoreDNSEmptyPort has to be in [0, 65353] range")
+	}
+	if d.EnvoyDNSPort > 65353 {
+		return errors.New(".EnvoyDNSPort has to be in [0, 65353] range")
+	}
+	if d.CoreDNSBinaryPath == "" {
+		return errors.New(".CoreDNSBinaryPath cannot be empty")
+	}
+	return nil
+}
diff --git a/pkg/core/xds/metadata.go b/pkg/core/xds/metadata.go
index 7488ae73b..12bf74bf8 100644
--- a/pkg/core/xds/metadata.go
+++ b/pkg/core/xds/metadata.go
@@ -19,6 +19,8 @@ const (
 	fieldDataplaneTokenPath         = "dataplaneTokenPath"
 	fieldDataplaneToken             = "dataplane.token"
 	fieldDataplaneAdminPort         = "dataplane.admin.port"
+	fieldDataplaneDNSPort           = "dataplane.dns.port"
+	fieldDataplaneDNSEmptyPort      = "dataplane.dns.empty.port"
 	fieldDataplaneDataplaneResource = "dataplane.resource"
 	fieldDynamicMetadata            = "dynamicMetadata"
 )
@@ -42,6 +44,8 @@ type DataplaneMetadata struct {
 	DataplaneToken     string
 	DataplaneResource  *core_mesh.DataplaneResource
 	AdminPort          uint32
+	DNSPort            uint32
+	EmptyDNSPort       uint32
 	DynamicMetadata    map[string]string
 }
 
@@ -73,6 +77,20 @@ func (m *DataplaneMetadata) GetAdminPort() uint32 {
 	return m.AdminPort
 }
 
+func (m *DataplaneMetadata) GetDNSPort() uint32 {
+	if m == nil {
+		return 0
+	}
+	return m.DNSPort
+}
+
+func (m *DataplaneMetadata) GetEmptyDNSPort() uint32 {
+	if m == nil {
+		return 0
+	}
+	return m.EmptyDNSPort
+}
+
 func (m *DataplaneMetadata) GetDynamicMetadata(key string) string {
 	if m == nil || m.DynamicMetadata == nil {
 		return ""
@@ -91,13 +109,9 @@ func DataplaneMetadataFromXdsMetadata(xdsMetadata *_struct.Struct) *DataplaneMet
 	if field := xdsMetadata.Fields[fieldDataplaneToken]; field != nil {
 		metadata.DataplaneToken = field.GetStringValue()
 	}
-	if value := xdsMetadata.Fields[fieldDataplaneAdminPort]; value != nil {
-		if port, err := strconv.Atoi(value.GetStringValue()); err == nil {
-			metadata.AdminPort = uint32(port)
-		} else {
-			metadataLog.Error(err, "invalid value in dataplane metadata", "field", fieldDataplaneAdminPort, "value", value)
-		}
-	}
+	metadata.AdminPort = uint32Metadata(xdsMetadata, fieldDataplaneAdminPort)
+	metadata.DNSPort = uint32Metadata(xdsMetadata, fieldDataplaneDNSPort)
+	metadata.EmptyDNSPort = uint32Metadata(xdsMetadata, fieldDataplaneDNSEmptyPort)
 	if value := xdsMetadata.Fields[fieldDataplaneDataplaneResource]; value != nil {
 		res, err := rest.UnmarshallToCore([]byte(value.GetStringValue()))
 		if err != nil {
@@ -118,3 +132,16 @@ func DataplaneMetadataFromXdsMetadata(xdsMetadata *_struct.Struct) *DataplaneMet
 	}
 	return &metadata
 }
+
+func uint32Metadata(xdsMetadata *_struct.Struct, field string) uint32 {
+	value := xdsMetadata.Fields[field]
+	if value == nil {
+		return 0
+	}
+	port, err := strconv.Atoi(value.GetStringValue())
+	if err != nil {
+		metadataLog.Error(err, "invalid value in dataplane metadata", "field", field, "value", value)
+		return 0
+	}
+	return uint32(port)
+}
diff --git a/pkg/dns/vips/interfaces.go b/pkg/dns/vips/interfaces.go
index e17b169d9..8a1f60e4c 100644
--- a/pkg/dns/vips/interfaces.go
+++ b/pkg/dns/vips/interfaces.go
@@ -7,3 +7,11 @@ func (vips List) Append(other List) {
 		vips[k] = v
 	}
 }
+
+func (vips List) FQDNsByIPs() map[string]string {
+	ipToDomain := map[string]string{}
+	for domain, ip := range vips {
+		ipToDomain[ip] = domain
+	}
+	return ipToDomain
+}
diff --git a/pkg/xds/bootstrap/generator.go b/pkg/xds/bootstrap/generator.go
index f83109d4a..8a3541eac 100644
--- a/pkg/xds/bootstrap/generator.go
+++ b/pkg/xds/bootstrap/generator.go
@@ -127,6 +127,11 @@ func (b *bootstrapGenerator) validateRequest(request types.BootstrapRequest) err
 		verr.AddViolation("dataplaneToken", "only one of dataplaneToken and dataplaneTokenField can be defined")
 		return verr.OrNil()
 	}
+	if b.bootstrapVersion(request.BootstrapVersion) == types.BootstrapV2 && request.DNSPort != 0 {
+		verr := validators.ValidationError{}
+		verr.AddViolation("dnsPort", "DNS cannot be used in API V2. Upgrade Kuma DP to API V3")
+		return verr.OrNil()
+	}
 	return nil
 }
 
@@ -227,6 +232,8 @@ func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *co
 		EnvoyBuild:         request.Version.Envoy.Build,
 		HdsEnabled:         b.hdsEnabled,
 		DynamicMetadata:    request.DynamicMetadata,
+		DNSPort:            request.DNSPort,
+		EmptyDNSPort:       request.EmptyDNSPort,
 	}
 	log.WithValues("params", params).Info("Generating bootstrap config")
 	return b.configForParameters(params, request.BootstrapVersion)
@@ -292,21 +299,31 @@ func (b *bootstrapGenerator) verifyAdminPort(adminPort uint32, dataplane *core_m
 	return nil
 }
 
-func (b *bootstrapGenerator) configForParameters(params configParameters, version types.BootstrapVersion) (proto.Message, types.BootstrapVersion, error) {
+func (b *bootstrapGenerator) bootstrapVersion(reqVersion types.BootstrapVersion) types.BootstrapVersion {
+	if reqVersion != "" {
+		return reqVersion
+	}
+	// if client did not overridden bootstrap version, provide bootstrap based on Kuma CP config
+	switch b.config.APIVersion {
+	case envoy_common.APIV2:
+		return types.BootstrapV2
+	case envoy_common.APIV3:
+		return types.BootstrapV3
+	default:
+		return ""
+	}
+}
+
+func (b *bootstrapGenerator) configForParameters(params configParameters, reqVersion types.BootstrapVersion) (proto.Message, types.BootstrapVersion, error) {
+	version := b.bootstrapVersion(reqVersion)
 	switch {
-	// V2
 	case version == types.BootstrapV2:
-		fallthrough
-	case version == "" && b.config.APIVersion == envoy_common.APIV2: // if client did not overridden bootstrap version, provide bootstrap based on Kuma CP config
 		cfg, err := b.configForParametersV2(params)
 		if err != nil {
 			return nil, "", err
 		}
 		return cfg, types.BootstrapV2, nil
-	// V3
 	case version == types.BootstrapV3:
-		fallthrough
-	case version == "" && b.config.APIVersion == envoy_common.APIV3: // if client did not overridden bootstrap version, provide bootstrap based on Kuma CP config
 		cfg, err := b.configForParametersV3(params)
 		if err != nil {
 			return nil, "", err
diff --git a/pkg/xds/bootstrap/parameters.go b/pkg/xds/bootstrap/parameters.go
index 90079e14d..89adca4ed 100644
--- a/pkg/xds/bootstrap/parameters.go
+++ b/pkg/xds/bootstrap/parameters.go
@@ -26,4 +26,6 @@ type configParameters struct {
 	EnvoyBuild         string
 	HdsEnabled         bool
 	DynamicMetadata    map[string]string
+	DNSPort            uint32
+	EmptyDNSPort       uint32
 }
diff --git a/pkg/xds/bootstrap/template_v2.go b/pkg/xds/bootstrap/template_v2.go
index 6915c3d96..179db8bbd 100644
--- a/pkg/xds/bootstrap/template_v2.go
+++ b/pkg/xds/bootstrap/template_v2.go
@@ -43,6 +43,12 @@ admin:
       port_value: {{ .AdminPort }}
 {{ end }}
 
+layered_runtime:
+  layers:
+  - name: kuma
+    static_layer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
+
 stats_config:
   stats_tags:
   - tag_name: name
diff --git a/pkg/xds/bootstrap/template_v3.go b/pkg/xds/bootstrap/template_v3.go
index ae99e77cd..5036bc259 100644
--- a/pkg/xds/bootstrap/template_v3.go
+++ b/pkg/xds/bootstrap/template_v3.go
@@ -16,6 +16,12 @@ node:
 {{end}}
 {{if .AdminPort }}
     dataplane.admin.port: "{{ .AdminPort }}"
+{{ end }}
+{{if .DNSPort }}
+    dataplane.dns.port: "{{ .DNSPort }}"
+{{ end }}
+{{if .EmptyDNSPort }}
+    dataplane.dns.empty.port: "{{ .EmptyDNSPort }}"
 {{ end }}
     version:
       kumaDp:
@@ -43,6 +49,12 @@ admin:
       port_value: {{ .AdminPort }}
 {{ end }}
 
+layered_runtime:
+  layers:
+  - name: kuma
+    static_layer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
+
 stats_config:
   stats_tags:
   - tag_name: name
diff --git a/pkg/xds/bootstrap/types/bootstrap_request.go b/pkg/xds/bootstrap/types/bootstrap_request.go
index 58bd907bb..37cc88a92 100644
--- a/pkg/xds/bootstrap/types/bootstrap_request.go
+++ b/pkg/xds/bootstrap/types/bootstrap_request.go
@@ -26,6 +26,8 @@ type BootstrapRequest struct {
 	DynamicMetadata map[string]string `json:"dynamicMetadata"`
 	// BootstrapVersion is an optional version to override the control plane's default setting
 	BootstrapVersion BootstrapVersion `json:"bootstrapVersion"`
+	DNSPort          uint32           `json:"dnsPort,omitempty"`
+	EmptyDNSPort     uint32           `json:"emptyDnsPort,omitempty"`
 }
 
 type Version struct {
diff --git a/pkg/xds/cache/mesh/snapshot.go b/pkg/xds/cache/mesh/snapshot.go
index 6862b300b..af2444621 100644
--- a/pkg/xds/cache/mesh/snapshot.go
+++ b/pkg/xds/cache/mesh/snapshot.go
@@ -8,10 +8,12 @@ import (
 	"github.com/kumahq/kuma/pkg/core"
 	"github.com/kumahq/kuma/pkg/core/dns/lookup"
 	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/resources/registry"
 	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	"github.com/kumahq/kuma/pkg/dns/vips"
 )
 
 var meshCacheLog = core.Log.WithName("mesh-cache")
@@ -52,6 +54,19 @@ func GetMeshSnapshot(ctx context.Context, meshName string, rm manager.ReadOnlyRe
 				}
 			}
 			snapshot.resources[typ] = meshedDpsAndIngresses
+		case system.ConfigType:
+			configs := &system.ConfigResourceList{}
+			var items []*system.ConfigResource
+			if err := rm.List(ctx, configs); err != nil {
+				return nil, err
+			}
+			for _, config := range configs.Items {
+				if configInHash(config.Meta.GetName(), meshName) {
+					items = append(items, config)
+				}
+			}
+			configs.Items = items
+			snapshot.resources[typ] = configs
 		default:
 			rlist, err := registry.Global().NewList(typ)
 			if err != nil {
@@ -66,6 +81,10 @@ func GetMeshSnapshot(ctx context.Context, meshName string, rm manager.ReadOnlyRe
 	return snapshot, nil
 }
 
+func configInHash(configName string, meshName string) bool {
+	return configName == vips.ConfigKey(meshName)
+}
+
 func (m *meshSnapshot) hash() string {
 	resources := []core_model.Resource{
 		m.mesh,
diff --git a/pkg/xds/context/context.go b/pkg/xds/context/context.go
index 49d6c0231..59650b07d 100644
--- a/pkg/xds/context/context.go
+++ b/pkg/xds/context/context.go
@@ -3,6 +3,7 @@ package context
 import (
 	"io/ioutil"
 
+	"github.com/kumahq/kuma/pkg/dns/resolver"
 	"github.com/kumahq/kuma/pkg/tls"
 
 	"github.com/kumahq/kuma/pkg/envoy/admin"
@@ -28,6 +29,7 @@ type ControlPlaneContext struct {
 	SdsTlsCert        []byte
 	AdminProxyKeyPair *tls.KeyPair
 	CLACache          xds.CLACache
+	DNSResolver       resolver.DNSResolver
 }
 
 func (c Context) SDSLocation() string {
@@ -41,7 +43,7 @@ type MeshContext struct {
 	Hash       string
 }
 
-func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache) (*ControlPlaneContext, error) {
+func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache, dnsResolver resolver.DNSResolver) (*ControlPlaneContext, error) {
 	var sdsCert []byte
 	if config.DpServer.TlsCertFile != "" {
 		c, err := ioutil.ReadFile(config.DpServer.TlsCertFile)
@@ -60,5 +62,6 @@ func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache) (*Co
 		SdsTlsCert:        sdsCert,
 		AdminProxyKeyPair: &adminKeyPair,
 		CLACache:          claCache,
+		DNSResolver:       dnsResolver,
 	}, nil
 }
diff --git a/pkg/xds/envoy/listeners/configurers.go b/pkg/xds/envoy/listeners/configurers.go
index 61f486f5f..d6ed1e550 100644
--- a/pkg/xds/envoy/listeners/configurers.go
+++ b/pkg/xds/envoy/listeners/configurers.go
@@ -361,3 +361,13 @@ func Timeout(timeout *mesh_proto.Timeout_Conf, protocol mesh_core.Protocol) Filt
 		})
 	})
 }
+
+func DNS(vips map[string]string, emptyDnsPort uint32) ListenerBuilderOpt {
+	return ListenerBuilderOptFunc(func(config *ListenerBuilderConfig) {
+		// V2 does not have DNS Filter
+		config.AddV3(&v3.DNSConfigurer{
+			VIPs:         vips,
+			EmptyDNSPort: emptyDnsPort,
+		})
+	})
+}
diff --git a/pkg/xds/envoy/listeners/v3/dns_configurer.go b/pkg/xds/envoy/listeners/v3/dns_configurer.go
new file mode 100644
index 000000000..aae532a79
--- /dev/null
+++ b/pkg/xds/envoy/listeners/v3/dns_configurer.go
@@ -0,0 +1,104 @@
+package v3
+
+import (
+	"sort"
+
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_data_dns "github.com/envoyproxy/go-control-plane/envoy/data/dns/v3"
+	envoy_dns "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dns_filter/v3alpha"
+	v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
+	"github.com/golang/protobuf/ptypes/duration"
+
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type DNSConfigurer struct {
+	VIPs         map[string]string
+	EmptyDNSPort uint32
+}
+
+func (c *DNSConfigurer) Configure(listener *envoy_listener.Listener) error {
+	pbst, err := proto.MarshalAnyDeterministic(c.dnsFilter())
+	if err != nil {
+		return err
+	}
+
+	listener.ListenerFilters = append(listener.ListenerFilters, &envoy_listener.ListenerFilter{
+		Name: "envoy.filters.udp.dns_filter",
+		ConfigType: &envoy_listener.ListenerFilter_TypedConfig{
+			TypedConfig: pbst,
+		},
+	})
+	return nil
+}
+
+func (c *DNSConfigurer) dnsFilter() *envoy_dns.DnsFilterConfig {
+	var virtualDomains []*envoy_data_dns.DnsTable_DnsVirtualDomain
+	for domain, ip := range c.VIPs {
+		virtualDomains = append(virtualDomains, &envoy_data_dns.DnsTable_DnsVirtualDomain{
+			Name: domain,
+			Endpoint: &envoy_data_dns.DnsTable_DnsEndpoint{
+				EndpointConfig: &envoy_data_dns.DnsTable_DnsEndpoint_AddressList{
+					AddressList: &envoy_data_dns.DnsTable_AddressList{
+						Address: []string{
+							ip,
+						},
+					},
+				},
+			},
+			AnswerTtl: &duration.Duration{
+				Seconds: 1,
+			},
+		})
+	}
+	sort.Stable(DnsTableByName(virtualDomains)) // for stable Envoy config
+
+	return &envoy_dns.DnsFilterConfig{
+		StatPrefix: "kuma_dns",
+		ClientConfig: &envoy_dns.DnsFilterConfig_ClientContextConfig{
+			// We configure upstream resolver to resolver that always returns that it could not find the domain (NXDOMAIN)
+			// As for this moment there is no setting to disable upstream resolving.
+			UpstreamResolvers: []*envoy_core.Address{
+				{
+					Address: &envoy_core.Address_SocketAddress{
+						SocketAddress: &envoy_core.SocketAddress{
+							Address: "127.0.0.1",
+							PortSpecifier: &envoy_core.SocketAddress_PortValue{
+								PortValue: c.EmptyDNSPort,
+							},
+						},
+					},
+				},
+			},
+			MaxPendingLookups: 256,
+		},
+		ServerConfig: &envoy_dns.DnsFilterConfig_ServerContextConfig{
+			ConfigSource: &envoy_dns.DnsFilterConfig_ServerContextConfig_InlineDnsTable{
+				InlineDnsTable: &envoy_data_dns.DnsTable{
+					VirtualDomains:     virtualDomains,
+					ExternalRetryCount: 0,
+					KnownSuffixes: []*v3.StringMatcher{
+						{
+							MatchPattern: &v3.StringMatcher_SafeRegex{
+								SafeRegex: &v3.RegexMatcher{
+									EngineType: &v3.RegexMatcher_GoogleRe2{},
+									// This is just an indicator that Envoy will try to resolve any hostname using list of virtual domains
+									Regex: ".*",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+type DnsTableByName []*envoy_data_dns.DnsTable_DnsVirtualDomain
+
+func (a DnsTableByName) Len() int      { return len(a) }
+func (a DnsTableByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a DnsTableByName) Less(i, j int) bool {
+	return a[i].Name < a[j].Name
+}
diff --git a/pkg/xds/envoy/listeners/v3/inbound_listener_configurer.go b/pkg/xds/envoy/listeners/v3/inbound_listener_configurer.go
index 29bad4387..9cea148c0 100644
--- a/pkg/xds/envoy/listeners/v3/inbound_listener_configurer.go
+++ b/pkg/xds/envoy/listeners/v3/inbound_listener_configurer.go
@@ -16,6 +16,7 @@ type InboundListenerConfigurer struct {
 
 func (c *InboundListenerConfigurer) Configure(l *envoy_listener.Listener) error {
 	l.Name = c.ListenerName
+	l.ReusePort = c.Protocol == core_xds.SocketAddressProtocolUDP
 	l.TrafficDirection = envoy_core.TrafficDirection_INBOUND
 	l.Address = &envoy_core.Address{
 		Address: &envoy_core.Address_SocketAddress{
diff --git a/pkg/xds/envoy/names/resource_names.go b/pkg/xds/envoy/names/resource_names.go
index 702f24174..411b2f4ff 100644
--- a/pkg/xds/envoy/names/resource_names.go
+++ b/pkg/xds/envoy/names/resource_names.go
@@ -58,3 +58,7 @@ func GetAdminListenerName() string {
 func GetTracingClusterName(backendName string) string {
 	return fmt.Sprintf("tracing:%s", backendName)
 }
+
+func GetDNSListenerName() string {
+	return "kuma:dns"
+}
diff --git a/pkg/xds/generator/dns_generator.go b/pkg/xds/generator/dns_generator.go
new file mode 100644
index 000000000..a44e6c3fd
--- /dev/null
+++ b/pkg/xds/generator/dns_generator.go
@@ -0,0 +1,72 @@
+package generator
+
+import (
+	"github.com/asaskevich/govalidator"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
+	"github.com/kumahq/kuma/pkg/xds/envoy/names"
+)
+
+// OriginDNS is a marker to indicate by which ProxyGenerator resources were generated.
+const OriginDNS = "dns"
+
+type DNSGenerator struct {
+}
+
+func (g DNSGenerator) Generate(ctx xds_context.Context, proxy *core_xds.Proxy) (*core_xds.ResourceSet, error) {
+	dnsPort := proxy.Metadata.GetDNSPort()
+	emptyDnsPort := proxy.Metadata.GetEmptyDNSPort()
+	if dnsPort == 0 || emptyDnsPort == 0 {
+		return nil, nil
+	}
+
+	if proxy.APIVersion == envoy_common.APIV2 {
+		return nil, nil // DNS is not available for API V2
+	}
+
+	if proxy.Dataplane.Spec.GetNetworking().GetTransparentProxying() == nil {
+		return nil, nil // DNS only makes sense when transparent proxy is used
+	}
+
+	vips := g.computeVIPs(ctx, proxy)
+	listener, err := envoy_listeners.NewListenerBuilder(proxy.APIVersion).
+		Configure(envoy_listeners.InboundListener(names.GetDNSListenerName(), proxy.Dataplane.GetIP(), dnsPort, core_xds.SocketAddressProtocolUDP)).
+		Configure(envoy_listeners.DNS(vips, emptyDnsPort)).
+		Build()
+	if err != nil {
+		return nil, err
+	}
+
+	resources := core_xds.NewResourceSet()
+	resources.Add(&core_xds.Resource{
+		Name:     names.GetDNSListenerName(),
+		Resource: listener,
+		Origin:   OriginDNS,
+	})
+	return resources, nil
+}
+
+func (g DNSGenerator) computeVIPs(ctx xds_context.Context, proxy *core_xds.Proxy) map[string]string {
+	domainsByIPs := ctx.ControlPlane.DNSResolver.GetVIPs().FQDNsByIPs()
+	meshedVips := map[string]string{}
+	for _, outbound := range proxy.Dataplane.Spec.GetNetworking().GetOutbound() {
+		if domain, ok := domainsByIPs[outbound.Address]; ok {
+			// add regular .mesh domain
+			meshedVips[domain+"."+ctx.ControlPlane.DNSResolver.GetDomain()] = outbound.Address
+			// add hostname from address in external service
+			endpoints := proxy.Routing.OutboundTargets[outbound.Tags[mesh_proto.ServiceTag]]
+			for _, endpoint := range endpoints {
+				if govalidator.IsDNSName(endpoint.Target) {
+					if endpoint.ExternalService != nil && endpoint.Target != "" {
+						meshedVips[endpoint.Target] = outbound.Address
+					}
+				}
+			}
+		}
+	}
+	return meshedVips
+}
diff --git a/pkg/xds/generator/proxy_template.go b/pkg/xds/generator/proxy_template.go
index 6ac8a8472..885324d33 100644
--- a/pkg/xds/generator/proxy_template.go
+++ b/pkg/xds/generator/proxy_template.go
@@ -76,6 +76,7 @@ func NewDefaultProxyProfile() ResourceGenerator {
 		DirectAccessProxyGenerator{},
 		TracingProxyGenerator{},
 		ProbeProxyGenerator{},
+		DNSGenerator{},
 	}
 }
 
diff --git a/pkg/xds/server/components.go b/pkg/xds/server/components.go
index 6e2a5998e..73b7d1efa 100644
--- a/pkg/xds/server/components.go
+++ b/pkg/xds/server/components.go
@@ -24,7 +24,9 @@ var (
 		core_mesh.DataplaneInsightType:  true,
 		core_mesh.DataplaneOverviewType: true,
 		core_mesh.ServiceInsightType:    true,
-		core_system.ConfigType:          true,
+	}
+	HashMeshIncludedGlobalResources = map[core_model.ResourceType]bool{
+		core_system.ConfigType: true,
 	}
 )
 
@@ -39,6 +41,9 @@ func meshResourceTypes(exclude map[core_model.ResourceType]bool) []core_model.Re
 			types = append(types, typ)
 		}
 	}
+	for typ := range HashMeshIncludedGlobalResources {
+		types = append(types, typ)
+	}
 	return types
 }
 
@@ -61,7 +66,7 @@ func RegisterXDS(rt core_runtime.Runtime) error {
 	if err != nil {
 		return err
 	}
-	envoyCpCtx, err := xds_context.BuildControlPlaneContext(rt.Config(), claCache)
+	envoyCpCtx, err := xds_context.BuildControlPlaneContext(rt.Config(), claCache, rt.DNSResolver())
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/pkg/config/app/kuma-dp/config_test.go b/pkg/config/app/kuma-dp/config_test.go
index 6a7cae195..856692ece 100644
--- a/pkg/config/app/kuma-dp/config_test.go
+++ b/pkg/config/app/kuma-dp/config_test.go
@@ -63,6 +63,12 @@ var _ = Describe("Config", func() {
 				"KUMA_DATAPLANE_RUNTIME_BINARY_PATH":                     "envoy.sh",
 				"KUMA_DATAPLANE_RUNTIME_CONFIG_DIR":                      "/var/run/envoy",
 				"KUMA_DATAPLANE_RUNTIME_TOKEN_PATH":                      "/tmp/token",
+				"KUMA_DNS_ENABLED":                                       "true",
+				"KUMA_DNS_CORE_DNS_PORT":                                 "5300",
+				"KUMA_DNS_CORE_DNS_EMPTY_PORT":                           "5301",
+				"KUMA_DNS_ENVOY_DNS_PORT":                                "5302",
+				"KUMA_DNS_CORE_DNS_BINARY_PATH":                          "/tmp/coredns",
+				"KUMA_DNS_CORE_DNS_CONFIG_TEMPLATE_PATH":                 "/tmp/Corefile",
 			}
 			for key, value := range env {
 				os.Setenv(key, value)
@@ -88,6 +94,12 @@ var _ = Describe("Config", func() {
 			Expect(cfg.DataplaneRuntime.BinaryPath).To(Equal("envoy.sh"))
 			Expect(cfg.DataplaneRuntime.ConfigDir).To(Equal("/var/run/envoy"))
 			Expect(cfg.DataplaneRuntime.TokenPath).To(Equal("/tmp/token"))
+			Expect(cfg.DNS.Enabled).To(BeTrue())
+			Expect(cfg.DNS.CoreDNSPort).To(Equal(uint32(5300)))
+			Expect(cfg.DNS.CoreDNSEmptyPort).To(Equal(uint32(5301)))
+			Expect(cfg.DNS.EnvoyDNSPort).To(Equal(uint32(5302)))
+			Expect(cfg.DNS.CoreDNSBinaryPath).To(Equal("/tmp/coredns"))
+			Expect(cfg.DNS.CoreDNSConfigTemplatePath).To(Equal("/tmp/Corefile"))
 		})
 	})
 
diff --git a/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml b/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
index 595a4e142..1eb97f74f 100644
--- a/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
+++ b/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
@@ -10,3 +10,8 @@ dataplane:
   bootstrapVersion: ""
 dataplaneRuntime:
   binaryPath: envoy
+dns:
+  coreDnsBinaryPath: coredns
+  coreDnsEmptyPort: 15055
+  coreDnsPort: 15053
+  envoyDnsPort: 15054
diff --git a/pkg/core/xds/metadata_test.go b/pkg/core/xds/metadata_test.go
index d3268ea2f..82ae3d8e7 100644
--- a/pkg/core/xds/metadata_test.go
+++ b/pkg/core/xds/metadata_test.go
@@ -39,11 +39,23 @@ var _ = DescribeTable("DataplaneMetadataFromXdsMetadata",
 						StringValue: "1234",
 					},
 				},
+				"dataplane.dns.port": &pstruct.Value{
+					Kind: &pstruct.Value_StringValue{
+						StringValue: "8000",
+					},
+				},
+				"dataplane.dns.empty.port": &pstruct.Value{
+					Kind: &pstruct.Value_StringValue{
+						StringValue: "8001",
+					},
+				},
 			},
 		},
 		expected: xds.DataplaneMetadata{
 			DataplaneTokenPath: "/tmp/token",
 			AdminPort:          1234,
+			DNSPort:            8000,
+			EmptyDNSPort:       8001,
 		},
 	}),
 )
diff --git a/pkg/xds/bootstrap/generator_test.go b/pkg/xds/bootstrap/generator_test.go
index 93ccd258a..dde7f1ef2 100644
--- a/pkg/xds/bootstrap/generator_test.go
+++ b/pkg/xds/bootstrap/generator_test.go
@@ -142,6 +142,8 @@ var _ = Describe("bootstrapGenerator", func() {
 				AdminPort:      1234,
 				DataplaneToken: "token",
 				Version:        defaultVersion,
+				DNSPort:        53001,
+				EmptyDNSPort:   53002,
 			},
 			expectedConfigFile:       "generator.default-config.golden.yaml",
 			expectedBootstrapVersion: types.BootstrapV3,
@@ -474,5 +476,16 @@ w/vjIriD0mGwwccxbojmEHq4rO4ZrjQNmwvOgxoL2dTm/L9Smr6RXmIgu/0Pnrlq
 			expected: `A data plane proxy is trying to verify the control plane using the certificate which is not a certificate authority (basic constraint 'CA' is set to 'false').
 Provide CA that was used to sign a certificate used in the control plane by using 'kuma-dp run --ca-cert-file=file' or via KUMA_CONTROL_PLANE_CA_CERT_FILE`,
 		}),
+		Entry("when DNS is used in API V2", errTestCase{
+			request: types.BootstrapRequest{
+				Host:             "localhost",
+				Mesh:             "mesh",
+				Name:             "name.namespace",
+				AdminPort:        9901,
+				BootstrapVersion: "2",
+				DNSPort:          1234,
+			},
+			expected: `dnsPort: DNS cannot be used in API V2. Upgrade Kuma DP to API V3`,
+		}),
 	)
 })
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
index a7984c95c..d0b97dd7b 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
@@ -44,6 +44,11 @@ hdsConfig:
       targetUri: localhost:5678
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: default.dp-1.default
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
index cc3d9c4c6..af9b81bb2 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
@@ -50,6 +50,11 @@ hdsConfig:
       targetUri: localhost:5678
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: default.dp-1.default
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
index a6e370363..e8a728fb7 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
@@ -44,6 +44,11 @@ hdsConfig:
       targetUri: localhost:5678
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: default.dp-1
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
index 54f00c7fd..9e9e50e9d 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
@@ -22,6 +22,11 @@ hdsConfig:
       clusterName: ads_cluster
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
index 8b2f09abb..f2590344a 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
@@ -48,6 +48,11 @@ hdsConfig:
       targetUri: localhost:15678
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
index 446d01e1a..e924df15a 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
@@ -16,6 +16,11 @@ hdsConfig:
       clusterName: ads_cluster
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
index 7f8458003..c6c105397 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
@@ -30,11 +30,18 @@ hdsConfig:
       value: token
   setNodeOnFirstMessageOnly: true
   transportApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
   metadata:
     dataplane.admin.port: "1234"
+    dataplane.dns.empty.port: "53002"
+    dataplane.dns.port: "53001"
     dataplane.token: token
     version:
       envoy:
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.golden.yaml
index 8c97efdb6..d961c8f57 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.golden.yaml
@@ -30,6 +30,11 @@ dynamicResources:
   ldsConfig:
     ads: {}
     resourceApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.ipv6.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.ipv6.golden.yaml
index d33fb926f..2da7f7ada 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.ipv6.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config.kubernetes.ipv6.golden.yaml
@@ -30,6 +30,11 @@ dynamicResources:
   ldsConfig:
     ads: {}
     resourceApiVersion: V3
+layeredRuntime:
+  layers:
+  - name: kuma
+    staticLayer:
+      envoy.restart_features.use_apple_api_for_dns_lookups: false
 node:
   cluster: backend
   id: mesh.name.namespace
diff --git a/pkg/xds/envoy/listeners/v3/dns_configurer_test.go b/pkg/xds/envoy/listeners/v3/dns_configurer_test.go
new file mode 100644
index 000000000..6d97cd004
--- /dev/null
+++ b/pkg/xds/envoy/listeners/v3/dns_configurer_test.go
@@ -0,0 +1,96 @@
+package v3_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy/names"
+
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	. "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+var _ = Describe("DNSConfigurer", func() {
+
+	type testCase struct {
+		vips         map[string]string
+		emptyDnsPort uint32
+		expected     string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			listener, err := NewListenerBuilder(envoy.APIV3).
+				Configure(InboundListener(names.GetDNSListenerName(), "192.168.0.1", 1234, xds.SocketAddressProtocolUDP)).
+				Configure(DNS(given.vips, given.emptyDnsPort)).
+				Build()
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			actual, err := util_proto.ToYAML(listener)
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("basic TCP listener", testCase{
+			vips: map[string]string{
+				"something.mesh": "240.0.0.0",
+				"something.com":  "240.0.0.0",
+				"backend.mesh":   "240.0.0.1",
+			},
+			emptyDnsPort: 53002,
+			expected: `
+            address:
+              socketAddress:
+                address: 192.168.0.1
+                portValue: 1234
+                protocol: UDP
+            listenerFilters:
+            - name: envoy.filters.udp.dns_filter
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.filters.udp.dns_filter.v3alpha.DnsFilterConfig
+                clientConfig:
+                  maxPendingLookups: "256"
+                  upstreamResolvers:
+                  - socketAddress:
+                      address: 127.0.0.1
+                      portValue: 53002
+                serverConfig:
+                  inlineDnsTable:
+                    knownSuffixes:
+                    - safeRegex:
+                        googleRe2: {}
+                        regex: .*
+                    virtualDomains:
+                    - answerTtl: 1s
+                      endpoint:
+                        addressList:
+                          address:
+                          - 240.0.0.1
+                      name: backend.mesh
+                    - answerTtl: 1s
+                      endpoint:
+                        addressList:
+                          address:
+                          - 240.0.0.0
+                      name: something.com
+                    - answerTtl: 1s
+                      endpoint:
+                        addressList:
+                          address:
+                          - 240.0.0.0
+                      name: something.mesh
+                statPrefix: kuma_dns
+            name: kuma:dns
+            reusePort: true
+            trafficDirection: INBOUND
+`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/listeners/v3/inbound_listener_configurer_test.go b/pkg/xds/envoy/listeners/v3/inbound_listener_configurer_test.go
index 33453d68b..a21a89bdc 100644
--- a/pkg/xds/envoy/listeners/v3/inbound_listener_configurer_test.go
+++ b/pkg/xds/envoy/listeners/v3/inbound_listener_configurer_test.go
@@ -60,6 +60,7 @@ var _ = Describe("InboundListenerConfigurer", func() {
 			expected: `
             name: inbound:192.168.0.1:8080
             trafficDirection: INBOUND
+            reusePort: true
             address:
               socketAddress:
                 address: 192.168.0.1
diff --git a/pkg/xds/generator/dns_generator_test.go b/pkg/xds/generator/dns_generator_test.go
new file mode 100644
index 000000000..908d33f7c
--- /dev/null
+++ b/pkg/xds/generator/dns_generator_test.go
@@ -0,0 +1,118 @@
+package generator_test
+
+import (
+	"io/ioutil"
+	"path/filepath"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	model "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/dns/resolver"
+	. "github.com/kumahq/kuma/pkg/test/matchers"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+)
+
+var _ = Describe("DNSGenerator", func() {
+
+	type testCase struct {
+		dataplaneFile string
+		expected      string
+	}
+
+	DescribeTable("Generate Envoy xDS resources",
+		func(given testCase) {
+			// setup
+			gen := &generator.DNSGenerator{}
+
+			dnsResolver := resolver.NewDNSResolver("mesh")
+			dnsResolver.SetVIPs(map[string]string{
+				"backend": "240.0.0.0",
+				"httpbin": "240.0.0.1",
+			})
+			ctx := xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-system:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert:  []byte("12345"),
+					DNSResolver: dnsResolver,
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+
+			dataplane := mesh_proto.Dataplane{}
+			dpBytes, err := ioutil.ReadFile(filepath.Join("testdata", "dns", given.dataplaneFile))
+			Expect(err).ToNot(HaveOccurred())
+			Expect(util_proto.FromYAML(dpBytes, &dataplane)).To(Succeed())
+			proxy := &model.Proxy{
+				Id: model.ProxyId{Name: "side-car"},
+				Dataplane: &mesh_core.DataplaneResource{
+					Meta: &test_model.ResourceMeta{
+						Version: "1",
+					},
+					Spec: &dataplane,
+				},
+				APIVersion: envoy_common.APIV3,
+				Routing: model.Routing{
+					OutboundTargets: map[model.ServiceName][]model.Endpoint{
+						"httpbin": {
+							{
+								Target: "httpbin.org",
+							},
+						},
+					},
+				},
+				Metadata: &model.DataplaneMetadata{
+					DNSPort:      53001,
+					EmptyDNSPort: 53002,
+				},
+			}
+
+			// when
+			rs, err := gen.Generate(ctx, proxy)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			// and output matches golden files
+			resp, err := rs.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchGoldenYAML(filepath.Join("testdata", "dns", given.expected)))
+		},
+		Entry("01. DNS enabled", testCase{
+			dataplaneFile: "1-dataplane.input.yaml",
+			expected:      "1-envoy-config.golden.yaml",
+		}),
+		Entry("02. DNS disabled", testCase{
+			dataplaneFile: "2-dataplane.input.yaml",
+			expected:      "2-envoy-config.golden.yaml",
+		}),
+	)
+})
diff --git a/pkg/xds/generator/testdata/dns/1-dataplane.input.yaml b/pkg/xds/generator/testdata/dns/1-dataplane.input.yaml
new file mode 100644
index 000000000..371b55c57
--- /dev/null
+++ b/pkg/xds/generator/testdata/dns/1-dataplane.input.yaml
@@ -0,0 +1,12 @@
+networking:
+  outbound:
+    - port: 80
+      address: 240.0.0.0
+      tags:
+        kuma.io/service: backend
+    - port: 80
+      address: 240.0.0.1
+      tags:
+        kuma.io/service: httpbin
+  transparentProxying:
+    redirectPort: 15001
diff --git a/pkg/xds/generator/testdata/dns/1-envoy-config.golden.yaml b/pkg/xds/generator/testdata/dns/1-envoy-config.golden.yaml
new file mode 100644
index 000000000..d36f619cf
--- /dev/null
+++ b/pkg/xds/generator/testdata/dns/1-envoy-config.golden.yaml
@@ -0,0 +1,41 @@
+resources:
+- name: kuma:dns
+  resource:
+    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+    address:
+      socketAddress:
+        portValue: 53001
+        protocol: UDP
+    listenerFilters:
+    - name: envoy.filters.udp.dns_filter
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.udp.dns_filter.v3alpha.DnsFilterConfig
+        clientConfig:
+          maxPendingLookups: "256"
+          upstreamResolvers:
+          - socketAddress:
+              address: 127.0.0.1
+              portValue: 53002
+        serverConfig:
+          inlineDnsTable:
+            knownSuffixes:
+            - safeRegex:
+                googleRe2: {}
+                regex: .*
+            virtualDomains:
+            - answerTtl: 1s
+              endpoint:
+                addressList:
+                  address:
+                  - 240.0.0.0
+              name: backend.mesh
+            - answerTtl: 1s
+              endpoint:
+                addressList:
+                  address:
+                  - 240.0.0.1
+              name: httpbin.mesh
+        statPrefix: kuma_dns
+    name: kuma:dns
+    reusePort: true
+    trafficDirection: INBOUND
diff --git a/pkg/xds/generator/testdata/dns/2-dataplane.input.yaml b/pkg/xds/generator/testdata/dns/2-dataplane.input.yaml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/pkg/xds/generator/testdata/dns/2-dataplane.input.yaml
@@ -0,0 +1,1 @@
+{}
diff --git a/pkg/xds/generator/testdata/dns/2-envoy-config.golden.yaml b/pkg/xds/generator/testdata/dns/2-envoy-config.golden.yaml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/pkg/xds/generator/testdata/dns/2-envoy-config.golden.yaml
@@ -0,0 +1,1 @@
+{}