Repository: kumahq/kuma
Pull request #1816 (2021-04-14 13:46:53)

feat(kuma-cp) generate DNS filter (backport #1807)
This is an automatic backport of pull request #1807 done by [Mergify](https://mergify.io).

---


<details>
<summary>Mergify commands and options</summary>

<br />

More conditions and actions can be found in the [documentation](https://docs.mergify.io/).

You can also trigger Mergify actions by commenting on this pull request:

- `@Mergifyio refresh` will re-evaluate the rules
- `@Mergifyio rebase` will rebase this PR on its base branch
- `@Mergifyio update` will merge the base branch into this PR
- `@Mergifyio backport <destination>` will backport this PR on `<destination>` branch

Additionally, on Mergify [dashboard](https://dashboard.mergify.io/) you can:

- look at your merge queues
- generate the Mergify configuration with the config editor.

Finally, you can contact us on https://mergify.io/
</details>



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/mk/check.mk b/mk/check.mk
index b25229ce9..ca1fa3ad6 100644
--- a/mk/check.mk
+++ b/mk/check.mk
@@ -49,7 +49,11 @@ imports: ## Dev: Runs goimports in order to organize imports
 helm-docs: ## Dev: Runs helm-docs generator
 	$(HELM_DOCS_PATH) -s="file" --chart-search-root=./deployments/charts
 
+.PHONY: ginkgo/unfocus
+ginkgo/unfocus:
+	$(GOPATH_BIN_DIR)/ginkgo unfocus
+
 .PHONY: check
-check: generate fmt vet docs helm-lint golangci-lint imports tidy helm-docs ## Dev: Run code checks (go fmt, go vet, ...)
+check: generate fmt vet docs helm-lint golangci-lint imports tidy helm-docs ginkgo/unfocus ## Dev: Run code checks (go fmt, go vet, ...)
 	$(MAKE) generate manifests -C pkg/plugins/resources/k8s/native
 	git diff --quiet || test $$(git diff --name-only | grep -v -e 'go.mod$$' -e 'go.sum$$' | wc -l) -eq 0 || ( echo "The following changes (result of code generators and code checks) have been detected:" && git --no-pager diff && false ) # fail if Git working tree is dirty
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 236883641..5adf8fa2d 100644
--- a/pkg/xds/envoy/listeners/configurers.go
+++ b/pkg/xds/envoy/listeners/configurers.go
@@ -359,3 +359,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 @@
+{}