Repository: kumahq/kuma
Pull request #1512 (2021-02-04 07:56:31)

chore(deps): bump github.com/miekg/dns from 1.1.35 to 1.1.38
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.35 to 1.1.38.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/miekg/dns/commit/35023fab5c1d57c9ac56da33a897af8794eb9567"><code>35023fa</code></a> Release 1.1.38</li>
<li><a href="https://github.com/miekg/dns/commit/2fd5af9f929bce8219f9616ac8510d121483f108"><code>2fd5af9</code></a> Validate Rdlength and off in UnpackRRWithHeader (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1215">#1215</a>)</li>
<li><a href="https://github.com/miekg/dns/commit/e6df8867af89f0937ec814a02ad75c3b7935708a"><code>e6df886</code></a> Fix Rdlength related parsing bug in RFC3597 records (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1214">#1214</a>)</li>
<li><a href="https://github.com/miekg/dns/commit/ba2d042a5738d65837e982fc72e0a8eeee891e1d"><code>ba2d042</code></a> Release 1.1.37</li>
<li><a href="https://github.com/miekg/dns/commit/a1362108bebe882eafc8af63ad9e649be328d7b3"><code>a136210</code></a> Merge branch 'master' of github.com:miekg/dns</li>
<li><a href="https://github.com/miekg/dns/commit/a25c26b5ed5463a9597ebd9daa51255470f3d4dd"><code>a25c26b</code></a> Release 1.1.36</li>
<li><a href="https://github.com/miekg/dns/commit/13238cb6adfeaa10002578a7fa18fc5947702e30"><code>13238cb</code></a> Support parsing known RR types in RFC 3597 format (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1211">#1211</a>)</li>
<li><a href="https://github.com/miekg/dns/commit/f9dc403cff55715445aed909308e8c974cf9b472"><code>f9dc403</code></a> Fix potentially truncated int casts in $GENERATE code (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1212">#1212</a>)</li>
<li><a href="https://github.com/miekg/dns/commit/731b191cabd155bcbc9a61b7ff4c758088bc104d"><code>731b191</code></a> Add a link to <a href="https://github.com/bodgit/tsig">https://github.com/bodgit/tsig</a> (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1206">#1206</a>)</li>
<li><a href="https://github.com/miekg/dns/commit/59aea23afe5598de8cfac8c7421b13fc7d3f45ce"><code>59aea23</code></a> Add GSS-TSIG support (<a href="https://github-redirect.dependabot.com/miekg/dns/issues/1201">#1201</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/miekg/dns/compare/v1.1.35...v1.1.38">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/miekg/dns&package-manager=go_modules&previous-version=1.1.35&new-version=1.1.38)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/kumahq/kuma?pullRequest=1512) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/kumahq/kuma?pullRequest=1512) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/kumahq/kuma?pullRequest=1512) it.</sub>

Patch:
diff --git a/app/kuma-dp/cmd/context.go b/app/kuma-dp/cmd/context.go
index 47ba7efac..dc547cecd 100644
--- a/app/kuma-dp/cmd/context.go
+++ b/app/kuma-dp/cmd/context.go
@@ -13,9 +13,10 @@ import (
 
 // RootContext contains variables, functions and components that can be overridden when extending kuma-dp or running the test.
 type RootContext struct {
-	ComponentManager   component.Manager
-	BootstrapGenerator envoy.BootstrapConfigFactoryFunc
-	Config             *kumadp.Config
+	ComponentManager         component.Manager
+	BootstrapGenerator       envoy.BootstrapConfigFactoryFunc
+	BootstrapDynamicMetadata map[string]string
+	Config                   *kumadp.Config
 }
 
 func DefaultRootContext() *RootContext {
@@ -26,6 +27,7 @@ func DefaultRootContext() *RootContext {
 			Timeout:   10 * time.Second,
 			Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
 		}),
-		Config: &config,
+		Config:                   &config,
+		BootstrapDynamicMetadata: map[string]string{},
 	}
 }
diff --git a/app/kuma-dp/cmd/run.go b/app/kuma-dp/cmd/run.go
index 30e595f60..e155ac406 100644
--- a/app/kuma-dp/cmd/run.go
+++ b/app/kuma-dp/cmd/run.go
@@ -116,12 +116,13 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 			shouldQuit := setupQuitChannel()
 
 			dataplane, err := envoy.New(envoy.Opts{
-				Config:    *cfg,
-				Generator: rootCtx.BootstrapGenerator,
-				Dataplane: dp,
-				Stdout:    cmd.OutOrStdout(),
-				Stderr:    cmd.OutOrStderr(),
-				Quit:      shouldQuit,
+				Config:          *cfg,
+				Generator:       rootCtx.BootstrapGenerator,
+				Dataplane:       dp,
+				DynamicMetadata: rootCtx.BootstrapDynamicMetadata,
+				Stdout:          cmd.OutOrStdout(),
+				Stderr:          cmd.OutOrStderr(),
+				Quit:            shouldQuit,
 			})
 			if err != nil {
 				return err
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy.go b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
index fe9426771..173e4dd85 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
@@ -25,15 +25,23 @@ var (
 	runLog = core.Log.WithName("kuma-dp").WithName("run").WithName("envoy")
 )
 
-type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config, dp *rest.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error)
+type BootstrapParams struct {
+	Dataplane        *rest.Resource
+	BootstrapVersion types.BootstrapVersion
+	EnvoyVersion     EnvoyVersion
+	DynamicMetadata  map[string]string
+}
+
+type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config, params BootstrapParams) ([]byte, types.BootstrapVersion, error)
 
 type Opts struct {
-	Config    kuma_dp.Config
-	Generator BootstrapConfigFactoryFunc
-	Dataplane *rest.Resource
-	Stdout    io.Writer
-	Stderr    io.Writer
-	Quit      chan struct{}
+	Config          kuma_dp.Config
+	Generator       BootstrapConfigFactoryFunc
+	Dataplane       *rest.Resource
+	DynamicMetadata map[string]string
+	Stdout          io.Writer
+	Stderr          io.Writer
+	Quit            chan struct{}
 }
 
 func New(opts Opts) (*Envoy, error) {
@@ -109,7 +117,12 @@ func (e *Envoy) Start(stop <-chan struct{}) error {
 	}
 	runLog.Info("fetched Envoy version", "version", envoyVersion)
 	runLog.Info("generating bootstrap configuration")
-	bootstrapConfig, version, err := e.opts.Generator(e.opts.Config.ControlPlane.URL, e.opts.Config, e.opts.Dataplane, types.BootstrapVersion(e.opts.Config.Dataplane.BootstrapVersion), *envoyVersion)
+	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),
+		EnvoyVersion:     *envoyVersion,
+		DynamicMetadata:  e.opts.DynamicMetadata,
+	})
 	if err != nil {
 		return errors.Errorf("Failed to generate Envoy bootstrap config. %v", err)
 	}
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
index 460ee5c30..544e02ccd 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
@@ -16,7 +16,6 @@ import (
 
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
 	"github.com/kumahq/kuma/pkg/core"
-	rest_types "github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	kuma_version "github.com/kumahq/kuma/pkg/version"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 )
@@ -43,7 +42,7 @@ func IsInvalidRequestErr(err error) bool {
 	return strings.HasPrefix(err.Error(), "Invalid request: ")
 }
 
-func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_types.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, params BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 	bootstrapUrl, err := net_url.Parse(url)
 	if err != nil {
 		return nil, "", err
@@ -74,7 +73,7 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_type
 	var version types.BootstrapVersion
 	err = retry.Do(context.Background(), backoff, func(ctx context.Context) error {
 		log.Info("trying to fetch bootstrap configuration from the Control Plane")
-		respBytes, version, err = b.requestForBootstrap(bootstrapUrl, cfg, dp, bootstrapVersion, ev)
+		respBytes, version, err = b.requestForBootstrap(bootstrapUrl, cfg, params)
 		if err == nil {
 			return nil
 		}
@@ -96,11 +95,11 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_type
 	return respBytes, version, nil
 }
 
-func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config, dp *rest_types.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config, params BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 	url.Path = "/bootstrap"
 	var dataplaneResource string
-	if dp != nil {
-		dpJSON, err := json.Marshal(dp)
+	if params.Dataplane != nil {
+		dpJSON, err := json.Marshal(params.Dataplane)
 		if err != nil {
 			return nil, "", err
 		}
@@ -114,7 +113,7 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 		AdminPort:          cfg.Dataplane.AdminPort.Lowest(),
 		DataplaneTokenPath: cfg.DataplaneRuntime.TokenPath,
 		DataplaneResource:  dataplaneResource,
-		BootstrapVersion:   bootstrapVersion,
+		BootstrapVersion:   params.BootstrapVersion,
 		Version: types.Version{
 			KumaDp: types.KumaDpVersion{
 				Version:   kuma_version.Build.Version,
@@ -123,10 +122,11 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 				BuildDate: kuma_version.Build.BuildDate,
 			},
 			Envoy: types.EnvoyVersion{
-				Version: ev.Version,
-				Build:   ev.Build,
+				Version: params.EnvoyVersion.Version,
+				Build:   params.EnvoyVersion.Build,
 			},
 		},
+		DynamicMetadata: params.DynamicMetadata,
 	}
 	jsonBytes, err := json.Marshal(request)
 	if err != nil {
diff --git a/docs/proposals/dns-naming-compliance.md b/docs/proposals/dns-naming-compliance.md
new file mode 100644
index 000000000..896f44345
--- /dev/null
+++ b/docs/proposals/dns-naming-compliance.md
@@ -0,0 +1,90 @@
+# DNS compliant names in Kuma
+
+## Current state
+
+### Service Naming
+
+In a broader sense, Kuma uses the `kuma.io/service` tag to mark a certain combination of ip/address and port as a particular service, which then
+can be used in the Policies selectors.
+
+On Kubernetes, this tag is generated automatically and is an underscore delimited combination of the K8s Service Name, Namespace, and Port: `<name>_<namespace>_svc_<port>`.
+
+On Universal, one still has to generate the name in an arbitrary form.
+
+### Cross-zone communication
+
+To account for multizone and hybrid deployments, we have extended the naming scheme through a DNS resolving service by adding a `.mesh` suffix.
+This unifies the naming of services in a way that it does not matter where the service was instantiated. Of course the naming scheme in Kubernetes
+is still applied, but now we can also also call `<name>_<namespace>_svc_<port>.mesh`.
+
+By adding the transparent proxy capabilities to Universal, we can now refer to services in the `.mesh` domain too.
+
+The DNS server in the kuma-cp will generate and assign virtual IPs (VIPs) so that any service name resolves to it, and that
+the side-cars get an additional listener for the service cluster. The default VIPs CIDR is `240.0.0.1`.
+
+### Exposing services outside the mesh
+
+The typical way to expose services from within the mesh to the external consumers is through a gateway dataplane proxy (one the Kuma side) and
+a reverse proxy on the customer-facing side. In this architecture, the traffic routes on the proxy should refer to Kuma services so that the
+listeners on the side-car Envoy, are hit with the traffic and then load-balanced through the available local and remote endpoints.
+
+On Kubernetes this happens using an Ingress resource of the form:
+
+```yaml
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: marketplace
+  namespace: default
+  annotations:
+    kubernetes.io/ingress.class: kong
+    kubernetes.io/ingress.global-static-ip-name: "kong-demo-ip"
+spec:
+  rules:
+  - host: localhost
+    http:
+      paths:
+      - path: /
+        backend:
+          serviceName: frontend
+          servicePort: 80
+```
+
+Where `serviceName: frontend` is the name of a Kubernetes Service object, which will be used by the proxy to redirect the incoming requests.
+This same service in Kuma namespace will be called `frontend_default_svc_80` and can also be resolved as a DNS name `frontend_default_svc_80.mesh`.
+
+However, the problem is that the Kubernetes Ingress Resource enforces 2 rules here:
+* `serviceName` can refer only to a Kubernetes Service
+* this service can not contain `_` or `.`
+
+### Illustrating the problem
+To illustrate the problem further, let's look at the option to manually create a Service of type `ExternalName`
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: frontend
+  namespace: default
+  annotations:
+    ingress.kubernetes.io/service-upstream: "true"
+spec:
+  type: ExternalName
+  externalName: frontend_default_svc_80.mesh
+```
+
+```shell
+The Service "frontend" is invalid: spec.externalName: Invalid value: "frontend_default_svc_80.mesh": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
+```
+
+The DNS name we try to supply is invalid according to the relevant RFCs and the Kubernetes embedded checks. So we are not even able to apply that resource.
+
+## Proposal for improvements
+
+In addition to the already existing Kubernetes generated `frontend_default_svc_80.mesh`, we can add a couple of compliant aliases:
+* use dashes as separators `frontend-default-svc-80.mesh`. This can get compllicated, cosider a service name `my-complex-service`
+  which naturally translates to `my-complex-service_default_svc_80.mesh`, and then the all dashes version `my-complex-service-default-svc-80.mesh`
+* use dots as separators `frontend.default.svc.80.mesh`. The complex service version then will be slighly more readable `my-complex-service.default.svc.80.mesh`
+
+All three of these should refer to the same VIP. We can have a `kumactl` command that enumerates the services and ServiceInsights and expose
+the DNS names associated with it.
diff --git a/go.mod b/go.mod
index 56fb62ff0..f20e57ebb 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/go-kit/kit v0.10.0
 	github.com/go-logr/logr v0.1.0
 	github.com/go-logr/zapr v0.1.1
-	github.com/gogo/protobuf v1.3.1
+	github.com/gogo/protobuf v1.3.2
 	github.com/golang-migrate/migrate/v4 v4.14.1
 	github.com/golang/protobuf v1.4.3
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
@@ -28,7 +28,7 @@ require (
 	github.com/kumahq/kuma/pkg/plugins/resources/k8s/native v0.0.0-00010101000000-000000000000
 	github.com/kumahq/kuma/pkg/transparentproxy/istio v0.0.0-00010101000000-000000000000
 	github.com/lib/pq v1.9.0
-	github.com/miekg/dns v1.1.35
+	github.com/miekg/dns v1.1.38
 	github.com/onsi/ginkgo v1.15.0
 	github.com/onsi/gomega v1.10.5
 	github.com/patrickmn/go-cache v2.1.0+incompatible
diff --git a/go.sum b/go.sum
index e8d425078..ad5e71b76 100644
--- a/go.sum
+++ b/go.sum
@@ -485,6 +485,8 @@ github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5
 github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE=
 github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -738,6 +740,7 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
 github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
@@ -819,8 +822,8 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
 github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
-github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
-github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/miekg/dns v1.1.38 h1:MtIY+fmHUVVgv1AXzmKMWcwdCYxTRPG1EDjpqF4RCEw=
+github.com/miekg/dns v1.1.38/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -1495,6 +1498,7 @@ golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200806022845-90696ccdc692/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
@@ -1502,6 +1506,7 @@ golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210111221946-d33bae441459 h1:/pJfBBZw1zeMQA6vRpalDcuw4VJ/B6+N5obtmG0Xiv8=
 golang.org/x/tools v0.0.0-20210111221946-d33bae441459/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/core/xds/metadata.go b/pkg/core/xds/metadata.go
index 8cd2d3941..79a976315 100644
--- a/pkg/core/xds/metadata.go
+++ b/pkg/core/xds/metadata.go
@@ -19,6 +19,7 @@ const (
 	fieldDataplaneTokenPath         = "dataplaneTokenPath"
 	fieldDataplaneAdminPort         = "dataplane.admin.port"
 	fieldDataplaneDataplaneResource = "dataplane.resource"
+	fieldDynamicMetadata            = "dynamicMetadata"
 )
 
 // DataplaneMetadata represents environment-specific part of a dataplane configuration.
@@ -39,6 +40,7 @@ type DataplaneMetadata struct {
 	DataplaneTokenPath string
 	DataplaneResource  *core_mesh.DataplaneResource
 	AdminPort          uint32
+	DynamicMetadata    map[string]string
 }
 
 func (m *DataplaneMetadata) GetDataplaneTokenPath() string {
@@ -62,6 +64,13 @@ func (m *DataplaneMetadata) GetAdminPort() uint32 {
 	return m.AdminPort
 }
 
+func (m *DataplaneMetadata) GetDynamicMetadata(key string) string {
+	if m == nil || m.DynamicMetadata == nil {
+		return ""
+	}
+	return m.DynamicMetadata[key]
+}
+
 func DataplaneMetadataFromXdsMetadata(xdsMetadata *_struct.Struct) *DataplaneMetadata {
 	metadata := DataplaneMetadata{}
 	if xdsMetadata == nil {
@@ -88,5 +97,12 @@ func DataplaneMetadataFromXdsMetadata(xdsMetadata *_struct.Struct) *DataplaneMet
 		}
 		metadata.DataplaneResource = dp
 	}
+	if value := xdsMetadata.Fields[fieldDynamicMetadata]; value != nil {
+		dynamicMetadata := map[string]string{}
+		for field, val := range value.GetStructValue().GetFields() {
+			dynamicMetadata[field] = val.GetStringValue()
+		}
+		metadata.DynamicMetadata = dynamicMetadata
+	}
 	return &metadata
 }
diff --git a/pkg/dns/components.go b/pkg/dns/components.go
index e489f0738..6c679a88e 100644
--- a/pkg/dns/components.go
+++ b/pkg/dns/components.go
@@ -19,6 +19,7 @@ func Setup(rt runtime.Runtime) error {
 		rt.Config().DNSServer.Port,
 		rt.DNSResolver(),
 		rt.Metrics(),
+		DnsNameToKumaCompliant,
 	)
 	if err != nil {
 		return err
diff --git a/pkg/dns/name_modifier.go b/pkg/dns/name_modifier.go
new file mode 100644
index 000000000..67ae4b7a7
--- /dev/null
+++ b/pkg/dns/name_modifier.go
@@ -0,0 +1,26 @@
+package dns
+
+import "strings"
+
+func DnsNameToKumaCompliant(name string) (string, error) {
+	// the request might be of the form:
+	//  service-name.namespace.something-else.mesh.
+	// it will always end with a dot, as specified by https://tools.ietf.org/html/rfc1034#section-3.1
+	//  `Since a complete domain name ends with the root label, this leads to a printed form which ends in a dot.`
+	countDots := strings.Count(name, ".")
+	toReplace := countDots
+	switch countDots {
+	case 0:
+		return name, nil
+	case 1:
+		if name[len(name)-1] == '.' {
+			return name, nil
+		}
+	default:
+		if name[len(name)-1] == '.' {
+			toReplace--
+		}
+	}
+
+	return strings.Replace(name, ".", "_", toReplace-1), nil
+}
diff --git a/pkg/dns/server.go b/pkg/dns/server.go
index 26c9b76a9..186e9b9f9 100644
--- a/pkg/dns/server.go
+++ b/pkg/dns/server.go
@@ -21,15 +21,18 @@ type DNSServer interface {
 	NeedLeaderElection() bool
 }
 
+type NameModifier = func(qName string) (string, error)
+
 type SimpleDNSServer struct {
 	address  string
 	resolver resolver.DNSResolver
 
 	latencyMetric    prometheus.Summary
 	resolutionMetric *prometheus.CounterVec
+	nameModifier     NameModifier
 }
 
-func NewDNSServer(port uint32, resolver resolver.DNSResolver, metrics core_metrics.Metrics) (DNSServer, error) {
+func NewDNSServer(port uint32, resolver resolver.DNSResolver, metrics core_metrics.Metrics, modifier NameModifier) (DNSServer, error) {
 	handler := &SimpleDNSServer{
 		address:  fmt.Sprintf("0.0.0.0:%d", port),
 		resolver: resolver,
@@ -42,6 +45,7 @@ func NewDNSServer(port uint32, resolver resolver.DNSResolver, metrics core_metri
 			Name: "dns_server_resolution",
 			Help: "Counter for DNS Server resolutions",
 		}, []string{"result"}),
+		nameModifier: modifier,
 	}
 	if err := metrics.Register(handler.latencyMetric); err != nil {
 		return nil, err
@@ -57,8 +61,8 @@ func (h *SimpleDNSServer) parseQuery(m *dns.Msg) {
 	for _, q := range m.Question {
 		switch q.Qtype {
 		case dns.TypeA:
-			serverLog.V(1).Info("query for " + q.Name)
-			ip, err := h.resolver.ForwardLookupFQDN(q.Name)
+			serverLog.V(1).Info("received a query for " + q.Name)
+			ip, err := h.lookup(q.Name)
 			if err != nil {
 				serverLog.V(1).Info("unable to resolve", "Name", q.Name, "error", err.Error())
 				h.resolutionMetric.WithLabelValues("unresolved").Inc()
@@ -132,3 +136,24 @@ func (h *SimpleDNSServer) registerDNSHandler() {
 		h.handleDNSRequest(writer, msg)
 	})
 }
+
+func (h *SimpleDNSServer) lookup(qName string) (string, error) {
+	ip, err := h.resolver.ForwardLookupFQDN(qName)
+	if err != nil {
+		if h.nameModifier == nil {
+			return "", err
+		}
+
+		modifiedName, err := h.nameModifier(qName)
+		if err != nil {
+			return "", err
+		}
+
+		ip, err = h.resolver.ForwardLookupFQDN(modifiedName)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	return ip, nil
+}
diff --git a/pkg/xds/bootstrap/generator.go b/pkg/xds/bootstrap/generator.go
index 1819641ca..8b0410e62 100644
--- a/pkg/xds/bootstrap/generator.go
+++ b/pkg/xds/bootstrap/generator.go
@@ -207,6 +207,7 @@ func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *co
 		EnvoyVersion:       request.Version.Envoy.Version,
 		EnvoyBuild:         request.Version.Envoy.Build,
 		HdsEnabled:         b.hdsEnabled,
+		DynamicMetadata:    request.DynamicMetadata,
 	}
 	log.WithValues("params", params).Info("Generating bootstrap config")
 	return b.configForParameters(params, request.BootstrapVersion)
diff --git a/pkg/xds/bootstrap/parameters.go b/pkg/xds/bootstrap/parameters.go
index bdc5adc9e..3a7b4ad3c 100644
--- a/pkg/xds/bootstrap/parameters.go
+++ b/pkg/xds/bootstrap/parameters.go
@@ -22,4 +22,5 @@ type configParameters struct {
 	EnvoyVersion       string
 	EnvoyBuild         string
 	HdsEnabled         bool
+	DynamicMetadata    map[string]string
 }
diff --git a/pkg/xds/bootstrap/template_v2.go b/pkg/xds/bootstrap/template_v2.go
index fa42790d0..2e2407199 100644
--- a/pkg/xds/bootstrap/template_v2.go
+++ b/pkg/xds/bootstrap/template_v2.go
@@ -23,6 +23,12 @@ node:
       envoy:
         version: "{{ .EnvoyVersion }}"
         build: "{{ .EnvoyBuild }}"
+{{if .DynamicMetadata }}
+    dynamicMetadata:
+{{ range $key, $value := .DynamicMetadata }}
+      {{ $key }}: "{{ $value }}"
+{{ end }}
+{{ end }}
 
 {{if .AdminPort }}
 admin:
diff --git a/pkg/xds/bootstrap/template_v3.go b/pkg/xds/bootstrap/template_v3.go
index adcd4fcf1..ff648aeea 100644
--- a/pkg/xds/bootstrap/template_v3.go
+++ b/pkg/xds/bootstrap/template_v3.go
@@ -23,6 +23,12 @@ node:
       envoy:
         version: "{{ .EnvoyVersion }}"
         build: "{{ .EnvoyBuild }}"
+{{if .DynamicMetadata }}
+    dynamicMetadata:
+{{ range $key, $value := .DynamicMetadata }}
+      {{ $key }}: "{{ $value }}"
+{{ end }}
+{{ end }}
 
 {{if .AdminPort }}
 admin:
diff --git a/pkg/xds/bootstrap/types/bootstrap_request.go b/pkg/xds/bootstrap/types/bootstrap_request.go
index 66fa2239d..4d4a6d31f 100644
--- a/pkg/xds/bootstrap/types/bootstrap_request.go
+++ b/pkg/xds/bootstrap/types/bootstrap_request.go
@@ -13,13 +13,14 @@ const (
 const BootstrapVersionHeader = "kuma-bootstrap-version"
 
 type BootstrapRequest struct {
-	Mesh               string  `json:"mesh"`
-	Name               string  `json:"name"`
-	AdminPort          uint32  `json:"adminPort,omitempty"`
-	DataplaneTokenPath string  `json:"dataplaneTokenPath,omitempty"`
-	DataplaneResource  string  `json:"dataplaneResource,omitempty"`
-	Host               string  `json:"-"`
-	Version            Version `json:"version"`
+	Mesh               string            `json:"mesh"`
+	Name               string            `json:"name"`
+	AdminPort          uint32            `json:"adminPort,omitempty"`
+	DataplaneTokenPath string            `json:"dataplaneTokenPath,omitempty"`
+	DataplaneResource  string            `json:"dataplaneResource,omitempty"`
+	Host               string            `json:"-"`
+	Version            Version           `json:"version"`
+	DynamicMetadata    map[string]string `json:"dynamicMetadata"`
 	// BootstrapVersion is an optional version to override the control plane's default setting
 	BootstrapVersion BootstrapVersion `json:"bootstrapVersion"`
 }


Test patch:
diff --git a/app/kuma-dp/cmd/run_test.go b/app/kuma-dp/cmd/run_test.go
index 90afe22f2..140dcdcb1 100644
--- a/app/kuma-dp/cmd/run_test.go
+++ b/app/kuma-dp/cmd/run_test.go
@@ -13,7 +13,6 @@ import (
 	"strings"
 	"syscall"
 
-	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 
 	. "github.com/onsi/ginkgo"
@@ -106,7 +105,7 @@ var _ = Describe("run", func() {
 
 			// given
 			rootCtx := DefaultRootContext()
-			rootCtx.BootstrapGenerator = func(_ string, cfg kumadp.Config, _ *rest.Resource, _ types.BootstrapVersion, version envoy.EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+			rootCtx.BootstrapGenerator = func(_ string, cfg kumadp.Config, _ envoy.BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 				respBytes, err := ioutil.ReadFile(filepath.Join("testdata", "bootstrap-config.golden.yaml"))
 				Expect(err).ToNot(HaveOccurred())
 				return respBytes, "", nil
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
index 93d792e57..502aab88c 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
@@ -16,7 +16,6 @@ import (
 	. "github.com/onsi/gomega"
 
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
-	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 )
 
@@ -69,7 +68,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+			sampleConfig := func(string, kuma_dp.Config, BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 				return []byte(`node:
   id: example`), types.BootstrapV2, nil
 			}
@@ -137,7 +136,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+			sampleConfig := func(string, kuma_dp.Config, BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 				return nil, "", nil
 			}
 
@@ -179,7 +178,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+			sampleConfig := func(string, kuma_dp.Config, BootstrapParams) ([]byte, types.BootstrapVersion, error) {
 				return nil, "", nil
 			}
 
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
index cf8f3cd35..d9bcb2107 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
@@ -29,6 +29,7 @@ var _ = Describe("Remote Bootstrap", func() {
 		config                   kuma_dp.Config
 		bootstrapVersion         types.BootstrapVersion
 		dataplane                *rest.Resource
+		dynamicMetadata          map[string]string
 		expectedBootstrapRequest string
 	}
 
@@ -65,10 +66,16 @@ var _ = Describe("Remote Bootstrap", func() {
 		generator := NewRemoteBootstrapGenerator(http.DefaultClient)
 
 		// when
-		config, version, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config, given.dataplane, given.bootstrapVersion, EnvoyVersion{
-			Build:   "hash/1.15.0/RELEASE",
-			Version: "1.15.0",
-		})
+		params := BootstrapParams{
+			Dataplane:        given.dataplane,
+			BootstrapVersion: given.bootstrapVersion,
+			EnvoyVersion: EnvoyVersion{
+				Build:   "hash/1.15.0/RELEASE",
+				Version: "1.15.0",
+			},
+			DynamicMetadata: given.dynamicMetadata,
+		}
+		config, version, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config, params)
 
 		// then
 		Expect(version).To(Equal(given.bootstrapVersion))
@@ -93,6 +100,9 @@ var _ = Describe("Remote Bootstrap", func() {
 							Name: "sample",
 						},
 					},
+					dynamicMetadata: map[string]string{
+						"test": "value",
+					},
 					expectedBootstrapRequest: `
 					{
 					  "mesh": "demo",
@@ -112,7 +122,10 @@ var _ = Describe("Remote Bootstrap", func() {
 						  "build": "hash/1.15.0/RELEASE"
 						}
 					  },
-					  "bootstrapVersion": "2"
+					  "bootstrapVersion": "2",
+					  "dynamicMetadata": {
+					    "test": "value"
+					  }
 					}`,
 				}
 			}()),
@@ -154,7 +167,8 @@ var _ = Describe("Remote Bootstrap", func() {
                           "build": "hash/1.15.0/RELEASE"
                         }
                       },
-                      "bootstrapVersion": "3"
+                      "bootstrapVersion": "3",
+					  "dynamicMetadata": null
                     }`,
 				}
 			}()),
@@ -193,7 +207,8 @@ var _ = Describe("Remote Bootstrap", func() {
                           "build": "hash/1.15.0/RELEASE"
                         }
                       },
-                      "bootstrapVersion": ""
+                      "bootstrapVersion": "",
+					  "dynamicMetadata": null
                     }`,
 				}
 			}()),
@@ -226,13 +241,16 @@ var _ = Describe("Remote Bootstrap", func() {
 		// when
 		cfg := kuma_dp.DefaultConfig()
 		cfg.ControlPlane.Retry.Backoff = 10 * time.Millisecond
-		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg, &rest.Resource{
-			Meta: rest.ResourceMeta{
-				Type: "Dataplane",
-				Mesh: "default",
-				Name: "dp-1",
+		params := BootstrapParams{
+			Dataplane: &rest.Resource{
+				Meta: rest.ResourceMeta{
+					Type: "Dataplane",
+					Mesh: "default",
+					Name: "dp-1",
+				},
 			},
-		}, "", EnvoyVersion{})
+		}
+		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg, params)
 
 		// then
 		Expect(err).ToNot(HaveOccurred())
@@ -258,9 +276,16 @@ var _ = Describe("Remote Bootstrap", func() {
 		config := kuma_dp.DefaultConfig()
 		config.ControlPlane.Retry.Backoff = 10 * time.Millisecond
 		config.ControlPlane.Retry.MaxDuration = 100 * time.Millisecond
-		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), config, &rest.Resource{
-			Meta: rest.ResourceMeta{Mesh: "default", Name: "dp-1"},
-		}, "", EnvoyVersion{})
+		params := BootstrapParams{
+			Dataplane: &rest.Resource{
+				Meta: rest.ResourceMeta{
+					Type: "Dataplane",
+					Mesh: "default",
+					Name: "dp-1",
+				},
+			},
+		}
+		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), config, params)
 
 		// then
 		Expect(err).To(MatchError("retryable: Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection."))
diff --git a/pkg/dns/name_modifier_test.go b/pkg/dns/name_modifier_test.go
new file mode 100644
index 000000000..b0cc65989
--- /dev/null
+++ b/pkg/dns/name_modifier_test.go
@@ -0,0 +1,57 @@
+package dns_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/dns"
+)
+
+var _ = Describe("DNS name modifier", func() {
+	type testCase struct {
+		input    string
+		expected string
+	}
+
+	DescribeTable("should modify names",
+		func(given testCase) {
+			// when
+			actual, err := dns.DnsNameToKumaCompliant(given.input)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("empty", testCase{
+			input:    "",
+			expected: "",
+		}),
+		Entry("one last dot", testCase{
+			input:    "mesh.",
+			expected: "mesh.",
+		}),
+		Entry("one dot", testCase{
+			input:    ".mesh",
+			expected: ".mesh",
+		}),
+		Entry("two dots with last", testCase{
+			input:    "a.mesh.",
+			expected: "a.mesh.",
+		}),
+		Entry("two dots", testCase{
+			input:    "a.b.mesh",
+			expected: "a_b.mesh",
+		}),
+		Entry("three dots with last", testCase{
+			input:    "a.b.mesh.",
+			expected: "a_b.mesh.",
+		}),
+		Entry("three dots", testCase{
+			input:    "a.b.c.mesh",
+			expected: "a_b_c.mesh",
+		}),
+	)
+
+})
diff --git a/pkg/dns/server_test.go b/pkg/dns/server_test.go
index b3ecc9c4c..f9e3e3a2c 100644
--- a/pkg/dns/server_test.go
+++ b/pkg/dns/server_test.go
@@ -37,7 +37,7 @@ var _ = Describe("DNS server", func() {
 			m, err := core_metrics.NewMetrics("Standalone")
 			metrics = m
 			Expect(err).ToNot(HaveOccurred())
-			server, err := NewDNSServer(port, dnsResolver, metrics)
+			server, err := NewDNSServer(port, dnsResolver, metrics, DnsNameToKumaCompliant)
 			Expect(err).ToNot(HaveOccurred())
 
 			// given
@@ -190,5 +190,32 @@ var _ = Describe("DNS server", func() {
 			Expect(test_metrics.FindMetric(metrics, "dns_server")).ToNot(BeNil())
 			Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "resolved").Counter.GetValue()).To(Equal(1.0))
 		})
+
+		It("should resolve converted services with '.'", func() {
+			// given
+			var err error
+			dnsResolver.SetVIPs(vips.List{
+				"my-service_test-namespace_svc_80": "240.0.0.1",
+			})
+			ip, err = dnsResolver.ForwardLookupFQDN("my-service_test-namespace_svc_80.mesh")
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			client := new(dns.Client)
+			message := new(dns.Msg)
+			_ = message.SetQuestion("my-service.test-namespace.svc.80.mesh.", dns.TypeA)
+			var response *dns.Msg
+			Eventually(func() error {
+				response, _, err = client.Exchange(message, fmt.Sprintf("127.0.0.1:%d", port))
+				return err
+			}).ShouldNot(HaveOccurred())
+
+			// then
+			Expect(response.Answer[0].String()).To(Equal(fmt.Sprintf("my-service.test-namespace.svc.80.mesh.\t60\tIN\tA\t%s", ip)))
+
+			// and metrics are published
+			Expect(test_metrics.FindMetric(metrics, "dns_server")).ToNot(BeNil())
+			Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "resolved").Counter.GetValue()).To(Equal(1.0))
+		})
 	})
 })
diff --git a/pkg/xds/bootstrap/generator_test.go b/pkg/xds/bootstrap/generator_test.go
index 1e8d80a28..b92506958 100644
--- a/pkg/xds/bootstrap/generator_test.go
+++ b/pkg/xds/bootstrap/generator_test.go
@@ -192,6 +192,9 @@ var _ = Describe("bootstrapGenerator", func() {
 				Name:               "name.namespace",
 				AdminPort:          1234,
 				DataplaneTokenPath: "/tmp/token",
+				DynamicMetadata: map[string]string{
+					"test": "value",
+				},
 				DataplaneResource: `
 {
   "type": "Dataplane",
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
index 29bfe62e7..a00bf6e79 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
@@ -59,6 +59,8 @@ node:
       8443, "tags": { "kuma.io/protocol": "http2", "kuma.io/service": "backend" }
       }, ] } }'
     dataplaneTokenPath: /tmp/token
+    dynamicMetadata:
+      test: value
     version:
       envoy:
         build: hash/1.15.0/RELEASE
diff --git a/test/e2e/kuma_deploy_universal_transparent_proxy_test.go b/test/e2e/kuma_deploy_universal_transparent_proxy_test.go
index c7e8c13f6..4c072444c 100644
--- a/test/e2e/kuma_deploy_universal_transparent_proxy_test.go
+++ b/test/e2e/kuma_deploy_universal_transparent_proxy_test.go
@@ -14,7 +14,7 @@ import (
 
 var _ = Describe("Test Universal Transparent Proxy deployment", func() {
 
-	const iterations = 100
+	const iterations = 10
 
 	var cluster Cluster
 	var deployOptsFuncs []DeployOptionsFunc
@@ -65,6 +65,19 @@ var _ = Describe("Test Universal Transparent Proxy deployment", func() {
 					}
 					return "should retry", errors.Errorf("should retry")
 				})
+			retry.DoWithRetry(cluster.GetTesting(), "curl remote service with dots",
+				DefaultRetries, DefaultTimeout,
+				func() (string, error) {
+					stdout, _, err := cluster.ExecWithRetries("", "", "demo-client",
+						"curl", "-v", "-m", "3", "echo-server.kuma-test.svc.8080.mesh")
+					if err != nil {
+						return "should retry", err
+					}
+					if strings.Contains(stdout, "HTTP/1.1 200 OK") {
+						return "Accessing service successful", nil
+					}
+					return "should retry", errors.Errorf("should retry")
+				})
 		}
 	})
 })