Repository: kumahq/kuma
Pull request #2541 (2021-08-10 11:26:35)

chore(kuma-cp) migrate DiscoveryRequest/Response in KDS to V3
### Summary

KDS is a protocol to exchange Kuma entities between Global and Zone CP.
It is based on xDS (we use go-control-plane), but instead of Envoy resources we put Kuma resources.
Right now we use DiscoveryRequest/Response V2, but V2 protos were removed from Go Control Plane so we need to migrate in order to upgrade Go Control Plane in the future.

The logical xDS definition looks like this
```
service KumaDiscoveryService {
  rpc StreamKumaResources(stream envoy.service.discovery.v3.DiscoveryRequest)
      returns (stream envoy.service.discovery.v3.DiscoveryResponse);
}
```

But we in case of KDS, both Global and Zone CPs are logical servers and clients. That's why we created mux.proto on top of this
```
service MultiplexService {
  rpc StreamMessage(stream Message) returns (stream Message);
}

message Message {
  oneof value {
    envoy.api.v2.DiscoveryRequest request = 1;
    envoy.api.v2.DiscoveryResponse response = 2;
  }
}
```

so on the wire, the real gRPC service is MultiplexService. Global is a real server and Zone is a real client, but logically under the hood - both are servers and clients.

Because `KumaDiscoveryService` is a logical service, I just migrated this to V3 protos.
In the real service I added new fields
```
message Message {
  oneof value {
    envoy.api.v2.DiscoveryRequest request = 1;
    envoy.api.v2.DiscoveryResponse response = 2;
    envoy.service.discovery.v3.DiscoveryRequest requestV3 = 3; # <- new
    envoy.service.discovery.v3.DiscoveryResponse responseV3 = 4; # <- new
  }
}
```

so new CP supports both V2 and V3 DiscoveryRequest.

We decide whether to send V2 or V3 proto on the wire, by a new gRPC MD header that we are sending.
Under the hood we convert between V2 and V3.

This way we can provide smooth migration. In the next major release, we can just remove the old `request`/`response`.

### Full changelog

* Implement support V3 DiscoveryRequest and DiscoveryResponse while keeping the compatibility with V2

### Issues resolved

No Github issue.

### Documentation

- [X] No docs

### Testing

- [ ] Unit tests
- [X] E2E tests (added a test with upgrade of CP)
- [X] Manual testing on Universal
- [ ] Manual testing on Kubernetes 

### Backwards compatibility

- [X] Add `backport-to-stable` label if the code is backwards compatible. Otherwise, list breaking changes.


Patch:
diff --git a/api/mesh/v1alpha1/kds.pb.go b/api/mesh/v1alpha1/kds.pb.go
index 88f3b97e9..f0e2a96bf 100644
--- a/api/mesh/v1alpha1/kds.pb.go
+++ b/api/mesh/v1alpha1/kds.pb.go
@@ -8,7 +8,7 @@ package v1alpha1
 
 import (
 	context "context"
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	grpc "google.golang.org/grpc"
 	codes "google.golang.org/grpc/codes"
 	status "google.golang.org/grpc/status"
@@ -168,54 +168,45 @@ var file_mesh_v1alpha1_kds_proto_rawDesc = []byte{
 	0x6b, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x6b, 0x75, 0x6d, 0x61, 0x2e,
 	0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1f, 0x67,
 	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
-	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c,
-	0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x69, 0x73,
-	0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f,
-	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e,
-	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc8, 0x02, 0x0a, 0x0c, 0x4b, 0x75, 0x6d, 0x61,
-	0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x75, 0x6d, 0x61, 0x2e, 0x6d, 0x65,
-	0x73, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4b, 0x75, 0x6d, 0x61,
-	0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d,
-	0x65, 0x74, 0x61, 0x12, 0x28, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x1a, 0xd2, 0x01,
-	0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65,
-	0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x73, 0x68, 0x12, 0x3f,
-	0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
-	0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
-	0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12,
-	0x47, 0x0a, 0x11, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
-	0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
-	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73,
-	0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
-	0x6f, 0x6e, 0x32, 0xae, 0x02, 0x0a, 0x14, 0x4b, 0x75, 0x6d, 0x61, 0x44, 0x69, 0x73, 0x63, 0x6f,
-	0x76, 0x65, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x44,
-	0x65, 0x6c, 0x74, 0x61, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
-	0x73, 0x12, 0x23, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32,
-	0x2e, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x61,
-	0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x44, 0x69, 0x73, 0x63, 0x6f,
-	0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01,
-	0x12, 0x5a, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65,
-	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e,
-	0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e,
-	0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x55, 0x0a, 0x12,
-	0x46, 0x65, 0x74, 0x63, 0x68, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
-	0x65, 0x73, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
-	0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
-	0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
-	0x6d, 0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x68, 0x71, 0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x2f, 0x61, 0x70,
-	0x69, 0x2f, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62,
-	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a,
+	0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x64, 0x69,
+	0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f,
+	0x76, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc8, 0x02, 0x0a, 0x0c, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65,
+	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x75, 0x6d, 0x61, 0x2e, 0x6d, 0x65, 0x73, 0x68,
+	0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65,
+	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74,
+	0x61, 0x12, 0x28, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x1a, 0xd2, 0x01, 0x0a, 0x04,
+	0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x73, 0x68,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x73, 0x68, 0x12, 0x3f, 0x0a, 0x0d,
+	0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
+	0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x47, 0x0a,
+	0x11, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69,
+	0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+	0x32, 0x8e, 0x01, 0x0a, 0x14, 0x4b, 0x75, 0x6d, 0x61, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65,
+	0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x53, 0x74, 0x72,
+	0x65, 0x61, 0x6d, 0x4b, 0x75, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
+	0x12, 0x2c, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x33, 0x2e, 0x44, 0x69,
+	0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d,
+	0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x64,
+	0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x33, 0x2e, 0x44, 0x69, 0x73, 0x63,
+	0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30,
+	0x01, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x6b, 0x75, 0x6d, 0x61, 0x68, 0x71, 0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f,
+	0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -232,28 +223,22 @@ func file_mesh_v1alpha1_kds_proto_rawDescGZIP() []byte {
 
 var file_mesh_v1alpha1_kds_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
 var file_mesh_v1alpha1_kds_proto_goTypes = []interface{}{
-	(*KumaResource)(nil),              // 0: kuma.mesh.v1alpha1.KumaResource
-	(*KumaResource_Meta)(nil),         // 1: kuma.mesh.v1alpha1.KumaResource.Meta
-	(*anypb.Any)(nil),                 // 2: google.protobuf.Any
-	(*timestamppb.Timestamp)(nil),     // 3: google.protobuf.Timestamp
-	(*v2.DeltaDiscoveryRequest)(nil),  // 4: envoy.api.v2.DeltaDiscoveryRequest
-	(*v2.DiscoveryRequest)(nil),       // 5: envoy.api.v2.DiscoveryRequest
-	(*v2.DeltaDiscoveryResponse)(nil), // 6: envoy.api.v2.DeltaDiscoveryResponse
-	(*v2.DiscoveryResponse)(nil),      // 7: envoy.api.v2.DiscoveryResponse
+	(*KumaResource)(nil),          // 0: kuma.mesh.v1alpha1.KumaResource
+	(*KumaResource_Meta)(nil),     // 1: kuma.mesh.v1alpha1.KumaResource.Meta
+	(*anypb.Any)(nil),             // 2: google.protobuf.Any
+	(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
+	(*v3.DiscoveryRequest)(nil),   // 4: envoy.service.discovery.v3.DiscoveryRequest
+	(*v3.DiscoveryResponse)(nil),  // 5: envoy.service.discovery.v3.DiscoveryResponse
 }
 var file_mesh_v1alpha1_kds_proto_depIdxs = []int32{
 	1, // 0: kuma.mesh.v1alpha1.KumaResource.meta:type_name -> kuma.mesh.v1alpha1.KumaResource.Meta
 	2, // 1: kuma.mesh.v1alpha1.KumaResource.spec:type_name -> google.protobuf.Any
 	3, // 2: kuma.mesh.v1alpha1.KumaResource.Meta.creation_time:type_name -> google.protobuf.Timestamp
 	3, // 3: kuma.mesh.v1alpha1.KumaResource.Meta.modification_time:type_name -> google.protobuf.Timestamp
-	4, // 4: kuma.mesh.v1alpha1.KumaDiscoveryService.DeltaKumaResources:input_type -> envoy.api.v2.DeltaDiscoveryRequest
-	5, // 5: kuma.mesh.v1alpha1.KumaDiscoveryService.StreamKumaResources:input_type -> envoy.api.v2.DiscoveryRequest
-	5, // 6: kuma.mesh.v1alpha1.KumaDiscoveryService.FetchKumaResources:input_type -> envoy.api.v2.DiscoveryRequest
-	6, // 7: kuma.mesh.v1alpha1.KumaDiscoveryService.DeltaKumaResources:output_type -> envoy.api.v2.DeltaDiscoveryResponse
-	7, // 8: kuma.mesh.v1alpha1.KumaDiscoveryService.StreamKumaResources:output_type -> envoy.api.v2.DiscoveryResponse
-	7, // 9: kuma.mesh.v1alpha1.KumaDiscoveryService.FetchKumaResources:output_type -> envoy.api.v2.DiscoveryResponse
-	7, // [7:10] is the sub-list for method output_type
-	4, // [4:7] is the sub-list for method input_type
+	4, // 4: kuma.mesh.v1alpha1.KumaDiscoveryService.StreamKumaResources:input_type -> envoy.service.discovery.v3.DiscoveryRequest
+	5, // 5: kuma.mesh.v1alpha1.KumaDiscoveryService.StreamKumaResources:output_type -> envoy.service.discovery.v3.DiscoveryResponse
+	5, // [5:6] is the sub-list for method output_type
+	4, // [4:5] is the sub-list for method input_type
 	4, // [4:4] is the sub-list for extension type_name
 	4, // [4:4] is the sub-list for extension extendee
 	0, // [0:4] is the sub-list for field type_name
@@ -322,9 +307,7 @@ const _ = grpc.SupportPackageIsVersion6
 //
 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
 type KumaDiscoveryServiceClient interface {
-	DeltaKumaResources(ctx context.Context, opts ...grpc.CallOption) (KumaDiscoveryService_DeltaKumaResourcesClient, error)
 	StreamKumaResources(ctx context.Context, opts ...grpc.CallOption) (KumaDiscoveryService_StreamKumaResourcesClient, error)
-	FetchKumaResources(ctx context.Context, in *v2.DiscoveryRequest, opts ...grpc.CallOption) (*v2.DiscoveryResponse, error)
 }
 
 type kumaDiscoveryServiceClient struct {
@@ -335,39 +318,8 @@ func NewKumaDiscoveryServiceClient(cc grpc.ClientConnInterface) KumaDiscoverySer
 	return &kumaDiscoveryServiceClient{cc}
 }
 
-func (c *kumaDiscoveryServiceClient) DeltaKumaResources(ctx context.Context, opts ...grpc.CallOption) (KumaDiscoveryService_DeltaKumaResourcesClient, error) {
-	stream, err := c.cc.NewStream(ctx, &_KumaDiscoveryService_serviceDesc.Streams[0], "/kuma.mesh.v1alpha1.KumaDiscoveryService/DeltaKumaResources", opts...)
-	if err != nil {
-		return nil, err
-	}
-	x := &kumaDiscoveryServiceDeltaKumaResourcesClient{stream}
-	return x, nil
-}
-
-type KumaDiscoveryService_DeltaKumaResourcesClient interface {
-	Send(*v2.DeltaDiscoveryRequest) error
-	Recv() (*v2.DeltaDiscoveryResponse, error)
-	grpc.ClientStream
-}
-
-type kumaDiscoveryServiceDeltaKumaResourcesClient struct {
-	grpc.ClientStream
-}
-
-func (x *kumaDiscoveryServiceDeltaKumaResourcesClient) Send(m *v2.DeltaDiscoveryRequest) error {
-	return x.ClientStream.SendMsg(m)
-}
-
-func (x *kumaDiscoveryServiceDeltaKumaResourcesClient) Recv() (*v2.DeltaDiscoveryResponse, error) {
-	m := new(v2.DeltaDiscoveryResponse)
-	if err := x.ClientStream.RecvMsg(m); err != nil {
-		return nil, err
-	}
-	return m, nil
-}
-
 func (c *kumaDiscoveryServiceClient) StreamKumaResources(ctx context.Context, opts ...grpc.CallOption) (KumaDiscoveryService_StreamKumaResourcesClient, error) {
-	stream, err := c.cc.NewStream(ctx, &_KumaDiscoveryService_serviceDesc.Streams[1], "/kuma.mesh.v1alpha1.KumaDiscoveryService/StreamKumaResources", opts...)
+	stream, err := c.cc.NewStream(ctx, &_KumaDiscoveryService_serviceDesc.Streams[0], "/kuma.mesh.v1alpha1.KumaDiscoveryService/StreamKumaResources", opts...)
 	if err != nil {
 		return nil, err
 	}
@@ -376,8 +328,8 @@ func (c *kumaDiscoveryServiceClient) StreamKumaResources(ctx context.Context, op
 }
 
 type KumaDiscoveryService_StreamKumaResourcesClient interface {
-	Send(*v2.DiscoveryRequest) error
-	Recv() (*v2.DiscoveryResponse, error)
+	Send(*v3.DiscoveryRequest) error
+	Recv() (*v3.DiscoveryResponse, error)
 	grpc.ClientStream
 }
 
@@ -385,85 +337,42 @@ type kumaDiscoveryServiceStreamKumaResourcesClient struct {
 	grpc.ClientStream
 }
 
-func (x *kumaDiscoveryServiceStreamKumaResourcesClient) Send(m *v2.DiscoveryRequest) error {
+func (x *kumaDiscoveryServiceStreamKumaResourcesClient) Send(m *v3.DiscoveryRequest) error {
 	return x.ClientStream.SendMsg(m)
 }
 
-func (x *kumaDiscoveryServiceStreamKumaResourcesClient) Recv() (*v2.DiscoveryResponse, error) {
-	m := new(v2.DiscoveryResponse)
+func (x *kumaDiscoveryServiceStreamKumaResourcesClient) Recv() (*v3.DiscoveryResponse, error) {
+	m := new(v3.DiscoveryResponse)
 	if err := x.ClientStream.RecvMsg(m); err != nil {
 		return nil, err
 	}
 	return m, nil
 }
 
-func (c *kumaDiscoveryServiceClient) FetchKumaResources(ctx context.Context, in *v2.DiscoveryRequest, opts ...grpc.CallOption) (*v2.DiscoveryResponse, error) {
-	out := new(v2.DiscoveryResponse)
-	err := c.cc.Invoke(ctx, "/kuma.mesh.v1alpha1.KumaDiscoveryService/FetchKumaResources", in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
 // KumaDiscoveryServiceServer is the server API for KumaDiscoveryService service.
 type KumaDiscoveryServiceServer interface {
-	DeltaKumaResources(KumaDiscoveryService_DeltaKumaResourcesServer) error
 	StreamKumaResources(KumaDiscoveryService_StreamKumaResourcesServer) error
-	FetchKumaResources(context.Context, *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error)
 }
 
 // UnimplementedKumaDiscoveryServiceServer can be embedded to have forward compatible implementations.
 type UnimplementedKumaDiscoveryServiceServer struct {
 }
 
-func (*UnimplementedKumaDiscoveryServiceServer) DeltaKumaResources(KumaDiscoveryService_DeltaKumaResourcesServer) error {
-	return status.Errorf(codes.Unimplemented, "method DeltaKumaResources not implemented")
-}
 func (*UnimplementedKumaDiscoveryServiceServer) StreamKumaResources(KumaDiscoveryService_StreamKumaResourcesServer) error {
 	return status.Errorf(codes.Unimplemented, "method StreamKumaResources not implemented")
 }
-func (*UnimplementedKumaDiscoveryServiceServer) FetchKumaResources(context.Context, *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method FetchKumaResources not implemented")
-}
 
 func RegisterKumaDiscoveryServiceServer(s *grpc.Server, srv KumaDiscoveryServiceServer) {
 	s.RegisterService(&_KumaDiscoveryService_serviceDesc, srv)
 }
 
-func _KumaDiscoveryService_DeltaKumaResources_Handler(srv interface{}, stream grpc.ServerStream) error {
-	return srv.(KumaDiscoveryServiceServer).DeltaKumaResources(&kumaDiscoveryServiceDeltaKumaResourcesServer{stream})
-}
-
-type KumaDiscoveryService_DeltaKumaResourcesServer interface {
-	Send(*v2.DeltaDiscoveryResponse) error
-	Recv() (*v2.DeltaDiscoveryRequest, error)
-	grpc.ServerStream
-}
-
-type kumaDiscoveryServiceDeltaKumaResourcesServer struct {
-	grpc.ServerStream
-}
-
-func (x *kumaDiscoveryServiceDeltaKumaResourcesServer) Send(m *v2.DeltaDiscoveryResponse) error {
-	return x.ServerStream.SendMsg(m)
-}
-
-func (x *kumaDiscoveryServiceDeltaKumaResourcesServer) Recv() (*v2.DeltaDiscoveryRequest, error) {
-	m := new(v2.DeltaDiscoveryRequest)
-	if err := x.ServerStream.RecvMsg(m); err != nil {
-		return nil, err
-	}
-	return m, nil
-}
-
 func _KumaDiscoveryService_StreamKumaResources_Handler(srv interface{}, stream grpc.ServerStream) error {
 	return srv.(KumaDiscoveryServiceServer).StreamKumaResources(&kumaDiscoveryServiceStreamKumaResourcesServer{stream})
 }
 
 type KumaDiscoveryService_StreamKumaResourcesServer interface {
-	Send(*v2.DiscoveryResponse) error
-	Recv() (*v2.DiscoveryRequest, error)
+	Send(*v3.DiscoveryResponse) error
+	Recv() (*v3.DiscoveryRequest, error)
 	grpc.ServerStream
 }
 
@@ -471,52 +380,23 @@ type kumaDiscoveryServiceStreamKumaResourcesServer struct {
 	grpc.ServerStream
 }
 
-func (x *kumaDiscoveryServiceStreamKumaResourcesServer) Send(m *v2.DiscoveryResponse) error {
+func (x *kumaDiscoveryServiceStreamKumaResourcesServer) Send(m *v3.DiscoveryResponse) error {
 	return x.ServerStream.SendMsg(m)
 }
 
-func (x *kumaDiscoveryServiceStreamKumaResourcesServer) Recv() (*v2.DiscoveryRequest, error) {
-	m := new(v2.DiscoveryRequest)
+func (x *kumaDiscoveryServiceStreamKumaResourcesServer) Recv() (*v3.DiscoveryRequest, error) {
+	m := new(v3.DiscoveryRequest)
 	if err := x.ServerStream.RecvMsg(m); err != nil {
 		return nil, err
 	}
 	return m, nil
 }
 
-func _KumaDiscoveryService_FetchKumaResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(v2.DiscoveryRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(KumaDiscoveryServiceServer).FetchKumaResources(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/kuma.mesh.v1alpha1.KumaDiscoveryService/FetchKumaResources",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(KumaDiscoveryServiceServer).FetchKumaResources(ctx, req.(*v2.DiscoveryRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
 var _KumaDiscoveryService_serviceDesc = grpc.ServiceDesc{
 	ServiceName: "kuma.mesh.v1alpha1.KumaDiscoveryService",
 	HandlerType: (*KumaDiscoveryServiceServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "FetchKumaResources",
-			Handler:    _KumaDiscoveryService_FetchKumaResources_Handler,
-		},
-	},
+	Methods:     []grpc.MethodDesc{},
 	Streams: []grpc.StreamDesc{
-		{
-			StreamName:    "DeltaKumaResources",
-			Handler:       _KumaDiscoveryService_DeltaKumaResources_Handler,
-			ServerStreams: true,
-			ClientStreams: true,
-		},
 		{
 			StreamName:    "StreamKumaResources",
 			Handler:       _KumaDiscoveryService_StreamKumaResources_Handler,
diff --git a/api/mesh/v1alpha1/kds.proto b/api/mesh/v1alpha1/kds.proto
index f03f0e6db..01349284f 100644
--- a/api/mesh/v1alpha1/kds.proto
+++ b/api/mesh/v1alpha1/kds.proto
@@ -5,19 +5,12 @@ package kuma.mesh.v1alpha1;
 option go_package = "github.com/kumahq/kuma/api/mesh/v1alpha1";
 
 import "google/protobuf/timestamp.proto";
-import "envoy/api/v2/discovery.proto";
+import "envoy/service/discovery/v3/discovery.proto";
 import "google/protobuf/any.proto";
 
 service KumaDiscoveryService {
-
-  rpc DeltaKumaResources(stream envoy.api.v2.DeltaDiscoveryRequest)
-      returns (stream envoy.api.v2.DeltaDiscoveryResponse);
-
-  rpc StreamKumaResources(stream envoy.api.v2.DiscoveryRequest)
-      returns (stream envoy.api.v2.DiscoveryResponse);
-
-  rpc FetchKumaResources(envoy.api.v2.DiscoveryRequest)
-      returns (envoy.api.v2.DiscoveryResponse);
+  rpc StreamKumaResources(stream envoy.service.discovery.v3.DiscoveryRequest)
+      returns (stream envoy.service.discovery.v3.DiscoveryResponse);
 }
 
 message KumaResource {
diff --git a/api/mesh/v1alpha1/mux.pb.go b/api/mesh/v1alpha1/mux.pb.go
index 16dc848e1..bc1a971d5 100644
--- a/api/mesh/v1alpha1/mux.pb.go
+++ b/api/mesh/v1alpha1/mux.pb.go
@@ -9,6 +9,7 @@ package v1alpha1
 import (
 	context "context"
 	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	grpc "google.golang.org/grpc"
 	codes "google.golang.org/grpc/codes"
 	status "google.golang.org/grpc/status"
@@ -31,6 +32,8 @@ type Message struct {
 	unknownFields protoimpl.UnknownFields
 
 	// Types that are assignable to Value:
+	//	*Message_LegacyRequest
+	//	*Message_LegacyResponse
 	//	*Message_Request
 	//	*Message_Response
 	Value isMessage_Value `protobuf_oneof:"value"`
@@ -75,14 +78,28 @@ func (m *Message) GetValue() isMessage_Value {
 	return nil
 }
 
-func (x *Message) GetRequest() *v2.DiscoveryRequest {
+func (x *Message) GetLegacyRequest() *v2.DiscoveryRequest {
+	if x, ok := x.GetValue().(*Message_LegacyRequest); ok {
+		return x.LegacyRequest
+	}
+	return nil
+}
+
+func (x *Message) GetLegacyResponse() *v2.DiscoveryResponse {
+	if x, ok := x.GetValue().(*Message_LegacyResponse); ok {
+		return x.LegacyResponse
+	}
+	return nil
+}
+
+func (x *Message) GetRequest() *v3.DiscoveryRequest {
 	if x, ok := x.GetValue().(*Message_Request); ok {
 		return x.Request
 	}
 	return nil
 }
 
-func (x *Message) GetResponse() *v2.DiscoveryResponse {
+func (x *Message) GetResponse() *v3.DiscoveryResponse {
 	if x, ok := x.GetValue().(*Message_Response); ok {
 		return x.Response
 	}
@@ -93,14 +110,26 @@ type isMessage_Value interface {
 	isMessage_Value()
 }
 
+type Message_LegacyRequest struct {
+	LegacyRequest *v2.DiscoveryRequest `protobuf:"bytes,1,opt,name=legacy_request,json=legacyRequest,proto3,oneof"`
+}
+
+type Message_LegacyResponse struct {
+	LegacyResponse *v2.DiscoveryResponse `protobuf:"bytes,2,opt,name=legacy_response,json=legacyResponse,proto3,oneof"`
+}
+
 type Message_Request struct {
-	Request *v2.DiscoveryRequest `protobuf:"bytes,1,opt,name=request,proto3,oneof"`
+	Request *v3.DiscoveryRequest `protobuf:"bytes,3,opt,name=request,proto3,oneof"`
 }
 
 type Message_Response struct {
-	Response *v2.DiscoveryResponse `protobuf:"bytes,2,opt,name=response,proto3,oneof"`
+	Response *v3.DiscoveryResponse `protobuf:"bytes,4,opt,name=response,proto3,oneof"`
 }
 
+func (*Message_LegacyRequest) isMessage_Value() {}
+
+func (*Message_LegacyResponse) isMessage_Value() {}
+
 func (*Message_Request) isMessage_Value() {}
 
 func (*Message_Response) isMessage_Value() {}
@@ -112,26 +141,39 @@ var file_mesh_v1alpha1_mux_proto_rawDesc = []byte{
 	0x6d, 0x75, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x6b, 0x75, 0x6d, 0x61, 0x2e,
 	0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1c, 0x65,
 	0x6e, 0x76, 0x6f, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x69, 0x73, 0x63,
-	0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8d, 0x01, 0x0a, 0x07,
-	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3a, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72,
-	0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18,
+	0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x65, 0x6e, 0x76,
+	0x6f, 0x79, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f,
+	0x76, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72,
+	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbe, 0x02, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x12, 0x47, 0x0a, 0x0e, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x72, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x65, 0x6e,
+	0x76, 0x6f, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f,
+	0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x6c,
+	0x65, 0x67, 0x61, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0f,
+	0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18,
 	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x61, 0x70,
 	0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x61, 0x0a, 0x10, 0x4d,
-	0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
-	0x4d, 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
-	0x12, 0x1b, 0x2e, 0x6b, 0x75, 0x6d, 0x61, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x31, 0x61,
-	0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1b, 0x2e,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x65, 0x6e, 0x76, 0x6f,
+	0x79, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76,
+	0x65, 0x72, 0x79, 0x2e, 0x76, 0x33, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x4b, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x2e, 0x73, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76,
+	0x33, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+	0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x61, 0x0a, 0x10, 0x4d, 0x75, 0x6c, 0x74,
+	0x69, 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4d, 0x0a, 0x0d,
+	0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e,
 	0x6b, 0x75, 0x6d, 0x61, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
-	0x61, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2a,
-	0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x75, 0x6d,
-	0x61, 0x68, 0x71, 0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6d, 0x65, 0x73,
-	0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x61, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1b, 0x2e, 0x6b, 0x75, 0x6d,
+	0x61, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
+	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2a, 0x5a, 0x28, 0x67,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x68, 0x71,
+	0x2f, 0x6b, 0x75, 0x6d, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76,
+	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -151,17 +193,21 @@ var file_mesh_v1alpha1_mux_proto_goTypes = []interface{}{
 	(*Message)(nil),              // 0: kuma.mesh.v1alpha1.Message
 	(*v2.DiscoveryRequest)(nil),  // 1: envoy.api.v2.DiscoveryRequest
 	(*v2.DiscoveryResponse)(nil), // 2: envoy.api.v2.DiscoveryResponse
+	(*v3.DiscoveryRequest)(nil),  // 3: envoy.service.discovery.v3.DiscoveryRequest
+	(*v3.DiscoveryResponse)(nil), // 4: envoy.service.discovery.v3.DiscoveryResponse
 }
 var file_mesh_v1alpha1_mux_proto_depIdxs = []int32{
-	1, // 0: kuma.mesh.v1alpha1.Message.request:type_name -> envoy.api.v2.DiscoveryRequest
-	2, // 1: kuma.mesh.v1alpha1.Message.response:type_name -> envoy.api.v2.DiscoveryResponse
-	0, // 2: kuma.mesh.v1alpha1.MultiplexService.StreamMessage:input_type -> kuma.mesh.v1alpha1.Message
-	0, // 3: kuma.mesh.v1alpha1.MultiplexService.StreamMessage:output_type -> kuma.mesh.v1alpha1.Message
-	3, // [3:4] is the sub-list for method output_type
-	2, // [2:3] is the sub-list for method input_type
-	2, // [2:2] is the sub-list for extension type_name
-	2, // [2:2] is the sub-list for extension extendee
-	0, // [0:2] is the sub-list for field type_name
+	1, // 0: kuma.mesh.v1alpha1.Message.legacy_request:type_name -> envoy.api.v2.DiscoveryRequest
+	2, // 1: kuma.mesh.v1alpha1.Message.legacy_response:type_name -> envoy.api.v2.DiscoveryResponse
+	3, // 2: kuma.mesh.v1alpha1.Message.request:type_name -> envoy.service.discovery.v3.DiscoveryRequest
+	4, // 3: kuma.mesh.v1alpha1.Message.response:type_name -> envoy.service.discovery.v3.DiscoveryResponse
+	0, // 4: kuma.mesh.v1alpha1.MultiplexService.StreamMessage:input_type -> kuma.mesh.v1alpha1.Message
+	0, // 5: kuma.mesh.v1alpha1.MultiplexService.StreamMessage:output_type -> kuma.mesh.v1alpha1.Message
+	5, // [5:6] is the sub-list for method output_type
+	4, // [4:5] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
 }
 
 func init() { file_mesh_v1alpha1_mux_proto_init() }
@@ -184,6 +230,8 @@ func file_mesh_v1alpha1_mux_proto_init() {
 		}
 	}
 	file_mesh_v1alpha1_mux_proto_msgTypes[0].OneofWrappers = []interface{}{
+		(*Message_LegacyRequest)(nil),
+		(*Message_LegacyResponse)(nil),
 		(*Message_Request)(nil),
 		(*Message_Response)(nil),
 	}
diff --git a/api/mesh/v1alpha1/mux.proto b/api/mesh/v1alpha1/mux.proto
index 887d536e4..6b3d48108 100644
--- a/api/mesh/v1alpha1/mux.proto
+++ b/api/mesh/v1alpha1/mux.proto
@@ -5,6 +5,7 @@ package kuma.mesh.v1alpha1;
 option go_package = "github.com/kumahq/kuma/api/mesh/v1alpha1";
 
 import "envoy/api/v2/discovery.proto";
+import "envoy/service/discovery/v3/discovery.proto";
 
 service MultiplexService {
   rpc StreamMessage(stream Message) returns (stream Message);
@@ -12,7 +13,9 @@ service MultiplexService {
 
 message Message {
   oneof value {
-    envoy.api.v2.DiscoveryRequest request = 1;
-    envoy.api.v2.DiscoveryResponse response = 2;
+    envoy.api.v2.DiscoveryRequest legacy_request = 1;
+    envoy.api.v2.DiscoveryResponse legacy_response = 2;
+    envoy.service.discovery.v3.DiscoveryRequest request = 3;
+    envoy.service.discovery.v3.DiscoveryResponse response = 4;
   }
 }
diff --git a/app/kumactl/cmd/install/context/install_logging_context.go b/app/kumactl/cmd/install/context/install_logging_context.go
new file mode 100644
index 000000000..d607e4c22
--- /dev/null
+++ b/app/kumactl/cmd/install/context/install_logging_context.go
@@ -0,0 +1,17 @@
+package context
+
+type LoggingTemplateArgs struct {
+	Namespace string
+}
+
+type InstallLoggingContext struct {
+	TemplateArgs LoggingTemplateArgs
+}
+
+func DefaultInstallLoggingContext() InstallLoggingContext {
+	return InstallLoggingContext{
+		TemplateArgs: LoggingTemplateArgs{
+			Namespace: "kuma-logging",
+		},
+	}
+}
diff --git a/app/kumactl/cmd/install/install.go b/app/kumactl/cmd/install/install.go
index ef811354d..82566ce51 100644
--- a/app/kumactl/cmd/install/install.go
+++ b/app/kumactl/cmd/install/install.go
@@ -18,7 +18,7 @@ func NewInstallCmd(pctx *kumactl_cmd.RootContext) *cobra.Command {
 	cmd.AddCommand(newInstallMetrics(pctx))
 	cmd.AddCommand(newInstallTracing(pctx))
 	cmd.AddCommand(newInstallDNS())
-	cmd.AddCommand(newInstallLogging())
+	cmd.AddCommand(newInstallLogging(pctx))
 	cmd.AddCommand(newInstallTransparentProxy())
 	cmd.AddCommand(newInstallDemoCmd(&pctx.InstallDemoContext))
 	cmd.AddCommand(newInstallGatewayCmd(pctx))
diff --git a/app/kumactl/cmd/install/install_logging.go b/app/kumactl/cmd/install/install_logging.go
index 5506dbb76..fa6fc53e3 100644
--- a/app/kumactl/cmd/install/install_logging.go
+++ b/app/kumactl/cmd/install/install_logging.go
@@ -5,6 +5,7 @@ import (
 	"github.com/spf13/cobra"
 
 	kumactl_data "github.com/kumahq/kuma/app/kumactl/data"
+	kumactl_cmd "github.com/kumahq/kuma/app/kumactl/pkg/cmd"
 	"github.com/kumahq/kuma/app/kumactl/pkg/install/data"
 	"github.com/kumahq/kuma/app/kumactl/pkg/install/k8s"
 )
@@ -13,12 +14,8 @@ type loggingTemplateArgs struct {
 	Namespace string
 }
 
-func newInstallLogging() *cobra.Command {
-	args := struct {
-		Namespace string
-	}{
-		Namespace: "kuma-logging",
-	}
+func newInstallLogging(pctx *kumactl_cmd.RootContext) *cobra.Command {
+	args := pctx.InstallLoggingContext.TemplateArgs
 	cmd := &cobra.Command{
 		Use:   "logging",
 		Short: "Install Logging backend in Kubernetes cluster (Loki)",
diff --git a/app/kumactl/pkg/cmd/root_context.go b/app/kumactl/pkg/cmd/root_context.go
index e7670728c..37c2bc3df 100644
--- a/app/kumactl/pkg/cmd/root_context.go
+++ b/app/kumactl/pkg/cmd/root_context.go
@@ -57,6 +57,7 @@ type RootContext struct {
 	InstallGatewayKongContext           install_context.InstallGatewayKongContext
 	InstallGatewayKongEnterpriseContext install_context.InstallGatewayKongEnterpriseContext
 	InstallTracingContext               install_context.InstallTracingContext
+	InstallLoggingContext               install_context.InstallLoggingContext
 }
 
 func DefaultRootContext() *RootContext {
@@ -82,6 +83,7 @@ func DefaultRootContext() *RootContext {
 		InstallGatewayKongContext:           install_context.DefaultInstallGatewayKongContext(),
 		InstallGatewayKongEnterpriseContext: install_context.DefaultInstallGatewayKongEnterpriseContext(),
 		InstallTracingContext:               install_context.DefaultInstallTracingContext(),
+		InstallLoggingContext:               install_context.DefaultInstallLoggingContext(),
 	}
 }
 
diff --git a/mk/build.mk b/mk/build.mk
index 58eab3953..fd8af47c6 100644
--- a/mk/build.mk
+++ b/mk/build.mk
@@ -32,6 +32,9 @@ COREDNS_PLUGIN_CFG_PATH ?= $(TOP)/tools/builds/coredns/templates/plugin.cfg
 # List of binaries that we have release build rules for.
 BUILD_RELEASE_BINARIES := kuma-cp kuma-dp kumactl kuma-prometheus-sd coredns
 
+# List of binaries that we have test build roles for.
+BUILD_TEST_BINARIES := test-server
+
 # Setting this variable to any value other than 'N', enables the experimental Kuma
 # gateway plugin. Experimental means "for experiments", NOT "for production".
 BUILD_WITH_EXPERIMENTAL_GATEWAY ?= N
@@ -50,7 +53,7 @@ build: build/release build/test
 build/release: $(patsubst %,build/%,$(BUILD_RELEASE_BINARIES)) ## Dev: Build all binaries
 
 .PHONY: build/test
-build/test: build/test-server
+build/test: $(patsubst %,build/%,$(BUILD_TEST_BINARIES)) ## Dev: Build testing binaries
 
 .PHONY: build/linux-amd64
 build/linux-amd64:
diff --git a/mk/dev.mk b/mk/dev.mk
index 4bcd5072b..e46db9e1a 100644
--- a/mk/dev.mk
+++ b/mk/dev.mk
@@ -261,7 +261,7 @@ dev/envrc: $(KUBECONFIG_DIR)/kind-kuma-current ## Generate .envrc
 		echo "path_add KUBECONFIG $$c" ; \
 	done >> .envrc
 	@echo 'export KUBECONFIG' >> .envrc
-	@for prog in $(BUILD_RELEASE_BINARIES) ; do \
+	@for prog in $(BUILD_RELEASE_BINARIES) $(BUILD_TEST_BINARIES) ; do \
 		echo "PATH_add $(BUILD_ARTIFACTS_DIR)/$$prog" ; \
 	done >> .envrc
 	@direnv allow
diff --git a/pkg/core/resources/store/options.go b/pkg/core/resources/store/options.go
index d977bb2da..51c5a1be3 100644
--- a/pkg/core/resources/store/options.go
+++ b/pkg/core/resources/store/options.go
@@ -12,7 +12,6 @@ type CreateOptions struct {
 	Mesh         string
 	CreationTime time.Time
 	Owner        core_model.Resource
-	Synced       bool
 }
 
 type CreateOptionsFunc func(*CreateOptions)
@@ -48,15 +47,8 @@ func CreateWithOwner(owner core_model.Resource) CreateOptionsFunc {
 	}
 }
 
-func CreateSynced() CreateOptionsFunc {
-	return func(opts *CreateOptions) {
-		opts.Synced = true
-	}
-}
-
 type UpdateOptions struct {
 	ModificationTime time.Time
-	Synced           bool
 }
 
 func ModifiedAt(modificationTime time.Time) UpdateOptionsFunc {
@@ -65,12 +57,6 @@ func ModifiedAt(modificationTime time.Time) UpdateOptionsFunc {
 	}
 }
 
-func UpdateSynced() UpdateOptionsFunc {
-	return func(opts *UpdateOptions) {
-		opts.Synced = true
-	}
-}
-
 type UpdateOptionsFunc func(*UpdateOptions)
 
 func NewUpdateOptions(fs ...UpdateOptionsFunc) *UpdateOptions {
diff --git a/pkg/dns/ip.go b/pkg/dns/ip.go
deleted file mode 100644
index 03b1813aa..000000000
--- a/pkg/dns/ip.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package dns
-
-import (
-	"net"
-	"strings"
-
-	"github.com/Nordix/simple-ipam/pkg/ipam"
-)
-
-type IPAM interface {
-	AllocateIP() (string, error)
-	FreeIP(ip string) error
-	ReserveIP(ip string) error
-}
-
-func IsAddressAlreadyAllocated(err error) bool {
-	return err != nil && strings.HasPrefix(err.Error(), "Address already allocated")
-}
-
-type SimpleIPAM struct {
-	ipam.IPAM
-}
-
-func NewSimpleIPAM(cidr string) (IPAM, error) {
-	newIPAM, err := ipam.New(cidr)
-	if err != nil {
-		return nil, err
-	}
-
-	return &SimpleIPAM{*newIPAM}, nil
-}
-
-func (i *SimpleIPAM) AllocateIP() (string, error) {
-	ip, err := i.Allocate()
-	if err != nil {
-		return "", err
-	}
-
-	return ip.String(), nil
-}
-
-func (i *SimpleIPAM) FreeIP(ip string) error {
-	parsedIP := net.ParseIP(ip)
-
-	// ensure the IP is reserved before deleting it
-	err := i.Reserve(parsedIP)
-	if err != nil && !IsAddressAlreadyAllocated(err) {
-		return err
-	}
-
-	i.Free(parsedIP)
-
-	return nil
-}
-
-func (i *SimpleIPAM) ReserveIP(ip string) error {
-	return i.Reserve(net.ParseIP(ip))
-}
diff --git a/pkg/dns/resolver/resolver.go b/pkg/dns/resolver/resolver.go
index 610ab7651..1206e5ab9 100644
--- a/pkg/dns/resolver/resolver.go
+++ b/pkg/dns/resolver/resolver.go
@@ -12,15 +12,14 @@ import (
 
 type DNSResolver interface {
 	GetDomain() string
-	SetVIPs(list vips.List)
-	GetVIPs() vips.List
+	SetVIPs(map[vips.HostnameEntry]string)
 	ForwardLookupFQDN(name string) (string, error)
 }
 
 type dnsResolver struct {
 	sync.RWMutex
 	domain  string
-	viplist vips.List
+	viplist map[vips.HostnameEntry]string
 }
 
 var _ DNSResolver = &dnsResolver{}
@@ -35,41 +34,39 @@ func (d *dnsResolver) GetDomain() string {
 	return d.domain
 }
 
-func (s *dnsResolver) SetVIPs(list vips.List) {
+func (s *dnsResolver) SetVIPs(list map[vips.HostnameEntry]string) {
 	s.Lock()
 	defer s.Unlock()
 	s.viplist = list
 }
 
-func (s *dnsResolver) GetVIPs() vips.List {
-	s.RLock()
-	defer s.RUnlock()
-	return s.viplist
-}
-
 func (s *dnsResolver) ForwardLookupFQDN(name string) (string, error) {
 	s.RLock()
 	defer s.RUnlock()
-	domain, err := s.domainFromName(name)
-	if err != nil {
-		return "", err
-	}
-
-	if domain != s.domain {
-		return "", errors.Errorf("domain [%s] not found.", domain)
-	}
+	ipFqdn, foundFqdn := s.viplist[vips.NewFqdnEntry(strings.TrimSuffix(name, "."))]
 
-	service, err := s.serviceFromName(name)
+	domain, err := s.domainFromName(name)
 	if err != nil {
 		return "", err
 	}
 
-	ip, found := s.viplist[vips.NewServiceEntry(service)]
-	if !found {
+	if domain == s.domain {
+		service, err := s.serviceFromName(name)
+		if err != nil {
+			return "", err
+		}
+
+		ip, found := s.viplist[vips.NewServiceEntry(service)]
+		if found {
+			return ip, nil
+		} else if foundFqdn {
+			return ipFqdn, nil
+		}
 		return "", errors.Errorf("service [%s] not found in domain [%s].", service, domain)
+	} else if foundFqdn {
+		return ipFqdn, nil
 	}
-
-	return ip, nil
+	return "", errors.Errorf("domain [%s] not found.", domain)
 }
 
 func (s *dnsResolver) domainFromName(name string) (string, error) {
diff --git a/pkg/dns/vips/global_view.go b/pkg/dns/vips/global_view.go
new file mode 100644
index 000000000..d40a001a9
--- /dev/null
+++ b/pkg/dns/vips/global_view.go
@@ -0,0 +1,58 @@
+package vips
+
+import (
+	"net"
+
+	"github.com/Nordix/simple-ipam/pkg/ipam"
+)
+
+// GlobalView keeps a list of all hostname/ips and add the possibility to allocate new ips
+type GlobalView struct {
+	ipam         *ipam.IPAM
+	ipToHostname map[string]HostnameEntry
+	hostnameToIp map[HostnameEntry]string
+}
+
+// Reserve add an ip/host to the list of reserved ips (useful when loading an existing view).
+func (g *GlobalView) Reserve(hostname HostnameEntry, ip string) error {
+	err := g.ipam.Reserve(net.ParseIP(ip))
+	if err != nil {
+		return err
+	}
+	g.ipToHostname[ip] = hostname
+	g.hostnameToIp[hostname] = ip
+	return nil
+}
+
+// Allocate assign an ip to a host
+func (g *GlobalView) Allocate(hostname HostnameEntry) (string, error) {
+	ip := g.hostnameToIp[hostname]
+	if ip != "" {
+		return ip, nil
+	}
+	netIp, err := g.ipam.Allocate()
+	if err != nil {
+		return "", err
+	}
+	ip = netIp.String()
+	g.ipToHostname[ip] = hostname
+	g.hostnameToIp[hostname] = ip
+	return ip, nil
+}
+
+func (g *GlobalView) ToVIPMap() map[HostnameEntry]string {
+	return g.hostnameToIp
+}
+
+func NewGlobalView(cidr string) (*GlobalView, error) {
+	newIPAM, err := ipam.New(cidr)
+	if err != nil {
+		return nil, err
+	}
+
+	return &GlobalView{
+		hostnameToIp: map[HostnameEntry]string{},
+		ipToHostname: map[string]HostnameEntry{},
+		ipam:         newIPAM,
+	}, nil
+}
diff --git a/pkg/dns/vips/interfaces.go b/pkg/dns/vips/interfaces.go
index 9c79c13a2..6e4b4e2f6 100644
--- a/pkg/dns/vips/interfaces.go
+++ b/pkg/dns/vips/interfaces.go
@@ -2,7 +2,6 @@ package vips
 
 import (
 	"fmt"
-	"sort"
 )
 
 type EntryType int
@@ -10,6 +9,7 @@ type EntryType int
 const (
 	Service EntryType = iota
 	Host
+	FullyQualifiedDomain
 )
 
 func (t EntryType) String() string {
@@ -18,63 +18,48 @@ func (t EntryType) String() string {
 		return "service"
 	case Host:
 		return "host"
+	case FullyQualifiedDomain:
+		return "fqdn"
 	default:
 		return "undefined"
 	}
 }
 
-type Entry struct {
+// HostnameEntry is the definition of a DNS entry. The type indicates where the entry comes from
+// (.e.g: Service is auto-generated, FullyQualifiedDomain comes from `virtual-outbound` policies...)
+type HostnameEntry struct {
 	Type EntryType `json:"type"`
 	Name string    `json:"name"`
 }
 
-func (e Entry) String() string {
+func (e HostnameEntry) String() string {
 	return fmt.Sprintf("%s:%s", e.Type, e.Name)
 }
 
-func (e Entry) MarshalText() (text []byte, err error) {
+func (e HostnameEntry) MarshalText() (text []byte, err error) {
 	return []byte(fmt.Sprintf("%d:%s", e.Type, e.Name)), nil
 }
 
-func (e *Entry) UnmarshalText(text []byte) error {
+func (e *HostnameEntry) UnmarshalText(text []byte) error {
 	_, err := fmt.Sscanf(string(text), "%v:%s", &e.Type, &e.Name)
 	return err
 }
 
-func NewHostEntry(host string) Entry {
-	return Entry{Host, host}
-}
-
-func NewServiceEntry(name string) Entry {
-	return Entry{Service, name}
-}
-
-type EntrySet map[Entry]bool
-
-func (s EntrySet) ToArray() (entries []Entry) {
-	for entry := range s {
-		entries = append(entries, entry)
+func (e *HostnameEntry) Less(o *HostnameEntry) bool {
+	if e.Type == o.Type {
+		return e.Name < o.Name
 	}
-	sort.SliceStable(entries, func(i, j int) bool {
-		// Sort by entry type, then name.
-		return entries[i].Type < entries[j].Type ||
-			entries[i].Name < entries[j].Name
-	})
-	return
+	return e.Type < o.Type
 }
 
-type List map[Entry]string
+func NewHostEntry(host string) HostnameEntry {
+	return HostnameEntry{Host, host}
+}
 
-func (vips List) Append(other List) {
-	for k, v := range other {
-		vips[k] = v
-	}
+func NewServiceEntry(name string) HostnameEntry {
+	return HostnameEntry{Service, name}
 }
 
-func (vips List) FQDNsByIPs() map[string]Entry {
-	ipToDomain := map[string]Entry{}
-	for domain, ip := range vips {
-		ipToDomain[ip] = domain
-	}
-	return ipToDomain
+func NewFqdnEntry(name string) HostnameEntry {
+	return HostnameEntry{FullyQualifiedDomain, name}
 }
diff --git a/pkg/dns/vips/persistence.go b/pkg/dns/vips/persistence.go
index fb999a7d5..fa4a4fad5 100644
--- a/pkg/dns/vips/persistence.go
+++ b/pkg/dns/vips/persistence.go
@@ -46,14 +46,13 @@ func NewPersistence(resourceManager manager.ReadOnlyResourceManager, configManag
 	}
 }
 
-func (m *Persistence) Get() (global List, meshed map[string]List, errs error) {
+func (m *Persistence) Get() (meshed map[string]*VirtualOutboundMeshView, errs error) {
 	resourceList := &config_model.ConfigResourceList{}
 	if err := m.configManager.List(context.Background(), resourceList); err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
-	global = List{}
-	meshed = map[string]List{}
+	meshed = map[string]*VirtualOutboundMeshView{}
 	for _, resource := range resourceList.Items {
 		mesh, ok := MeshFromConfigKey(resource.Meta.GetName())
 		if !ok {
@@ -67,53 +66,59 @@ func (m *Persistence) Get() (global List, meshed map[string]List, errs error) {
 			errs = multierr.Append(errs, err)
 			continue
 		}
-		global.Append(v)
 		meshed[mesh] = v
 	}
 	return
 }
 
-func (m *Persistence) unmarshal(config string) (List, error) {
-	v := List{}
-	if err := json.Unmarshal([]byte(config), &v); err == nil {
-		return v, nil
+func (m *Persistence) unmarshal(config string) (*VirtualOutboundMeshView, error) {
+	res := map[HostnameEntry]VirtualOutbound{}
+	if err := json.Unmarshal([]byte(config), &res); err == nil {
+		return NewVirtualOutboundView(res), nil
 	}
-	// backwards compatibility
-	backwardCompatible := map[string]string{}
-	if err := json.Unmarshal([]byte(config), &backwardCompatible); err != nil {
-		return nil, err
+	backCompat := map[HostnameEntry]string{}
+	if err := json.Unmarshal([]byte(config), &backCompat); err != nil {
+		// backwards compatibility
+		backwardCompatible := map[string]string{}
+		if err := json.Unmarshal([]byte(config), &backwardCompatible); err != nil {
+			return nil, err
+		}
+		for service, vip := range backwardCompatible {
+			backCompat[NewServiceEntry(service)] = vip
+		}
 	}
-	v = List{}
-	for service, vip := range backwardCompatible {
-		v[NewServiceEntry(service)] = vip
+	for entry, vip := range backCompat {
+		res[entry] = VirtualOutbound{
+			Address: vip,
+		}
 	}
-	return v, nil
+	return NewVirtualOutboundView(res), nil
 }
 
-func (m *Persistence) GetByMesh(mesh string) (List, error) {
+func (m *Persistence) GetByMesh(mesh string) (*VirtualOutboundMeshView, error) {
 	name := fmt.Sprintf(template, mesh)
 	resource := config_model.NewConfigResource()
 	err := m.configManager.Get(context.Background(), resource, store.GetByKey(name, ""))
 	if err != nil {
 		if store.IsResourceNotFound(err) {
-			return List{}, nil
+			return NewVirtualOutboundView(map[HostnameEntry]VirtualOutbound{}), nil
 		}
 		return nil, err
 	}
 
 	if resource.Spec.Config == "" {
-		return List{}, nil
+		return NewVirtualOutboundView(map[HostnameEntry]VirtualOutbound{}), nil
 	}
 
-	vips, err := m.unmarshal(resource.Spec.GetConfig())
+	virtualOutboundView, err := m.unmarshal(resource.Spec.GetConfig())
 	if err != nil {
 		return nil, errors.Wrap(err, "could not unmarshal")
 	}
 
-	return vips, nil
+	return virtualOutboundView, nil
 }
 
-func (m *Persistence) Set(mesh string, vips List) error {
+func (m *Persistence) Set(mesh string, vips *VirtualOutboundMeshView) error {
 	ctx := context.Background()
 	name := fmt.Sprintf(template, mesh)
 	resource := config_model.NewConfigResource()
@@ -125,7 +130,7 @@ func (m *Persistence) Set(mesh string, vips List) error {
 		create = true
 	}
 
-	jsonBytes, err := json.Marshal(vips)
+	jsonBytes, err := json.Marshal(vips.byHostname)
 	if err != nil {
 		return errors.Wrap(err, "unable to marshall VIP list")
 	}
diff --git a/pkg/dns/vips/virtual_outbound.go b/pkg/dns/vips/virtual_outbound.go
new file mode 100644
index 000000000..64a60f584
--- /dev/null
+++ b/pkg/dns/vips/virtual_outbound.go
@@ -0,0 +1,51 @@
+package vips
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+// VirtualOutbound the description of a hostname -> address and a list of port/tagSet that identifies each outbound.
+type VirtualOutbound struct {
+	// This is not default in the legacy case (hostnames won't be complete)
+	Address   string          `json:"address,omitempty"`
+	Outbounds []OutboundEntry `json:"outbounds,omitempty"`
+}
+
+func (vo *VirtualOutbound) Equal(other *VirtualOutbound) bool {
+	if vo.Address != other.Address || len(vo.Outbounds) != len(other.Outbounds) {
+		return false
+	}
+	for i := range vo.Outbounds {
+		if vo.Outbounds[i].String() != other.Outbounds[i].String() {
+			return false
+		}
+	}
+	return true
+}
+
+const (
+	OriginHost    = "host"
+	OriginService = "service"
+)
+
+type OutboundEntry struct {
+	Port   uint32
+	TagSet map[string]string
+	// A string to identify where this outbound was defined (usually the name of the outbound policy)
+	Origin string
+}
+
+func (mo *OutboundEntry) Less(o *OutboundEntry) bool {
+	return mo.Port < o.Port
+}
+
+func (mo *OutboundEntry) String() string {
+	var tags []string
+	for k, v := range mo.TagSet {
+		tags = append(tags, fmt.Sprintf("%s=%s", k, v))
+	}
+	sort.Strings(tags)
+	return fmt.Sprintf("%s=%d{%s}", mo.Origin, mo.Port, strings.Join(tags, ","))
+}
diff --git a/pkg/dns/vips/virtual_outbound_view.go b/pkg/dns/vips/virtual_outbound_view.go
new file mode 100644
index 000000000..4b9f0fcbc
--- /dev/null
+++ b/pkg/dns/vips/virtual_outbound_view.go
@@ -0,0 +1,114 @@
+package vips
+
+import (
+	"fmt"
+	"sort"
+)
+
+type VirtualOutboundMeshView struct {
+	byHostname map[HostnameEntry]*VirtualOutbound
+}
+
+func NewVirtualOutboundView(all map[HostnameEntry]VirtualOutbound) *VirtualOutboundMeshView {
+	r := VirtualOutboundMeshView{
+		byHostname: map[HostnameEntry]*VirtualOutbound{},
+	}
+	for k := range all {
+		itm := all[k]
+		r.byHostname[k] = &itm
+	}
+	return &r
+}
+
+func (vo *VirtualOutboundMeshView) Get(entry HostnameEntry) *VirtualOutbound {
+	return vo.byHostname[entry]
+}
+
+func (vo *VirtualOutboundMeshView) Add(entry HostnameEntry, outbound OutboundEntry) error {
+	if vo.byHostname[entry] == nil {
+		vo.byHostname[entry] = &VirtualOutbound{Outbounds: []OutboundEntry{outbound}}
+		return nil
+	}
+	for _, existingOutbound := range vo.byHostname[entry].Outbounds {
+		if existingOutbound.Port == outbound.Port {
+			if existingOutbound.String() == outbound.String() {
+				return nil
+			}
+			return fmt.Errorf("can't add %s:%d from %s because it's already used by entity defined in:'%s'", entry.Name, outbound.Port, outbound.Origin, existingOutbound.Origin)
+		}
+	}
+	vo.byHostname[entry].Outbounds = append(vo.byHostname[entry].Outbounds, outbound)
+	sort.SliceStable(vo.byHostname[entry].Outbounds, func(i, j int) bool {
+		return vo.byHostname[entry].Outbounds[i].Less(&vo.byHostname[entry].Outbounds[j])
+	})
+	return nil
+}
+
+func (vo *VirtualOutboundMeshView) HostnameEntries() []HostnameEntry {
+	keys := make([]HostnameEntry, 0, len(vo.byHostname))
+	for k := range vo.byHostname {
+		keys = append(keys, k)
+	}
+	sort.SliceStable(keys, func(i, j int) bool {
+		return keys[i].Less(&keys[j])
+	})
+	return keys
+}
+
+func ToVIPMap(voByMesh map[string]*VirtualOutboundMeshView) map[HostnameEntry]string {
+	vipList := map[HostnameEntry]string{}
+	for _, voView := range voByMesh {
+		for _, v := range voView.HostnameEntries() {
+			vo := voView.Get(v)
+			vipList[v] = vo.Address
+		}
+	}
+	return vipList
+}
+
+// Update merges `new` and `vo` in a new `out` and returns a list of changes.
+func (vo *VirtualOutboundMeshView) Update(new *VirtualOutboundMeshView) (changes []Change, out *VirtualOutboundMeshView) {
+	changes = []Change{}
+	out = NewVirtualOutboundView(map[HostnameEntry]VirtualOutbound{})
+	// Let's find the removed ones (in old but not in new)
+	for entry := range vo.byHostname {
+		if _, ok := new.byHostname[entry]; !ok {
+			changes = append(changes, Change{Type: Remove, Entry: entry})
+		}
+	}
+	for entry, vob := range new.byHostname {
+		oldVob, ok := vo.byHostname[entry]
+		if ok {
+			if !oldVob.Equal(vob) {
+				changes = append(changes, Change{Type: Modify, Entry: entry})
+			}
+		} else {
+			changes = append(changes, Change{Type: Add, Entry: entry})
+		}
+		out.byHostname[entry] = &VirtualOutbound{Address: vob.Address, Outbounds: vob.Outbounds}
+	}
+	sort.Slice(changes, func(i, j int) bool {
+		if changes[i].Entry == changes[j].Entry {
+			return changes[i].Type == Add
+		}
+		return changes[i].Entry.String() < changes[j].Entry.String()
+	})
+	return
+}
+
+type ChangeType string
+
+const (
+	Add    = ChangeType("Add")
+	Remove = ChangeType("Remove")
+	Modify = ChangeType("Modify")
+)
+
+type Change struct {
+	Type  ChangeType
+	Entry HostnameEntry
+}
+
+func (c Change) String() string {
+	return fmt.Sprintf("%s %s", c.Type, c.Entry.String())
+}
diff --git a/pkg/dns/vips_allocator.go b/pkg/dns/vips_allocator.go
index b52cfeda1..138743998 100644
--- a/pkg/dns/vips_allocator.go
+++ b/pkg/dns/vips_allocator.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"time"
 
-	"github.com/pkg/errors"
 	"go.uber.org/multierr"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -84,68 +83,61 @@ func (d *VIPsAllocator) CreateOrUpdateVIPConfig(mesh string) error {
 }
 
 func (d *VIPsAllocator) createOrUpdateVIPConfigs(meshes ...string) (errs error) {
-	global, byMesh, err := d.persistence.Get()
+	byMesh, err := d.persistence.Get()
 	if err != nil {
 		return err
 	}
 
-	ipam, err := d.newIPAM(global)
+	gv, err := vips.NewGlobalView(d.cidr)
 	if err != nil {
 		return err
 	}
+	for _, mesh := range meshes {
+		if _, ok := byMesh[mesh]; !ok {
+			byMesh[mesh] = vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{})
+		}
+		for _, hostEntry := range byMesh[mesh].HostnameEntries() {
+			vo := byMesh[mesh].Get(hostEntry)
+			err := gv.Reserve(hostEntry, vo.Address)
+			if err != nil {
+				return err
+			}
+		}
+	}
 
-	forEachMesh := func(mesh string, meshed vips.List) error {
-		serviceSet, err := BuildServiceSet(d.rm, mesh)
+	forEachMesh := func(mesh string, meshed *vips.VirtualOutboundMeshView) error {
+		newVirtualOutboundView, err := BuildVirtualOutboundMeshView(d.rm, mesh)
 		if err != nil {
 			return err
 		}
 
-		changed, err := UpdateMeshedVIPs(global, meshed, ipam, serviceSet)
+		err = AllocateVIPs(gv, newVirtualOutboundView)
 		if err != nil {
 			// Error might occur only if we run out of VIPs. There is no point to pass it through,
 			// we must notify user in logs and proceed
-			vipsAllocatorLog.Error(err, "failed to allocate new VIPs")
+			vipsAllocatorLog.Error(err, "failed to allocate new VIPs", "mesh", mesh)
 		}
-		if !changed {
+		changes, out := meshed.Update(newVirtualOutboundView)
+		if len(changes) == 0 {
 			return nil
 		}
-		global.Append(meshed)
-
-		return d.persistence.Set(mesh, meshed)
+		vipsAllocatorLog.Info("mesh vip changes", "mesh", mesh, "changes", changes)
+		return d.persistence.Set(mesh, out)
 	}
 
 	for _, mesh := range meshes {
-		meshed, ok := byMesh[mesh]
-		if !ok {
-			meshed = vips.List{}
-		}
-		if err := forEachMesh(mesh, meshed); err != nil {
+		if err := forEachMesh(mesh, byMesh[mesh]); err != nil {
 			errs = multierr.Append(errs, err)
 		}
 	}
 
-	d.resolver.SetVIPs(global)
+	d.resolver.SetVIPs(gv.ToVIPMap())
 
 	return errs
 }
 
-func (d *VIPsAllocator) newIPAM(initialVIPs vips.List) (IPAM, error) {
-	ipam, err := NewSimpleIPAM(d.cidr)
-	if err != nil {
-		return nil, err
-	}
-
-	for _, vip := range initialVIPs {
-		if err := ipam.ReserveIP(vip); err != nil && !IsAddressAlreadyAllocated(err) {
-			return nil, err
-		}
-	}
-
-	return ipam, nil
-}
-
-func BuildServiceSet(rm manager.ReadOnlyResourceManager, mesh string) (vips.EntrySet, error) {
-	serviceSet := make(vips.EntrySet)
+func BuildVirtualOutboundMeshView(rm manager.ReadOnlyResourceManager, mesh string) (*vips.VirtualOutboundMeshView, error) {
+	outboundSet := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{})
 
 	dataplanes := core_mesh.DataplaneResourceList{}
 	if err := rm.List(context.Background(), &dataplanes); err != nil {
@@ -158,19 +150,18 @@ func BuildServiceSet(rm manager.ReadOnlyResourceManager, mesh string) (vips.Entr
 			_ = filteredDataplanes.AddItem(d)
 		}
 	}
-
+	var err error
 	for _, dp := range filteredDataplanes.Items {
 		// backwards compatibility
 		if dp.Spec.IsIngress() {
 			for _, service := range dp.Spec.GetNetworking().GetIngress().GetAvailableServices() {
-				if service.Mesh != mesh {
-					continue
+				if service.Mesh == mesh {
+					err = multierr.Append(err, addDefault(outboundSet, service.GetTags()[mesh_proto.ServiceTag], 0))
 				}
-				serviceSet[vips.NewServiceEntry(service.Tags[mesh_proto.ServiceTag])] = true
 			}
 		} else {
 			for _, inbound := range dp.Spec.GetNetworking().GetInbound() {
-				serviceSet[vips.NewServiceEntry(inbound.GetService())] = true
+				err = multierr.Append(err, addDefault(outboundSet, inbound.GetService(), 0))
 			}
 		}
 	}
@@ -182,10 +173,9 @@ func BuildServiceSet(rm manager.ReadOnlyResourceManager, mesh string) (vips.Entr
 
 	for _, zi := range zoneIngresses.Items {
 		for _, service := range zi.Spec.GetAvailableServices() {
-			if service.Mesh != mesh {
-				continue
+			if service.Mesh == mesh {
+				err = multierr.Append(err, addDefault(outboundSet, service.GetTags()[mesh_proto.ServiceTag], 0))
 			}
-			serviceSet[vips.NewServiceEntry(service.Tags[mesh_proto.ServiceTag])] = true
 		}
 	}
 
@@ -194,41 +184,40 @@ func BuildServiceSet(rm manager.ReadOnlyResourceManager, mesh string) (vips.Entr
 		return nil, err
 	}
 	for _, es := range externalServices.Items {
-		serviceSet[vips.NewServiceEntry(es.Spec.GetService())] = true
-		serviceSet[vips.NewHostEntry(es.Spec.GetHost())] = true
+		err = multierr.Append(err, addDefault(outboundSet, es.Spec.GetService(), es.Spec.GetPortUInt32()))
+		err = multierr.Append(err, outboundSet.Add(vips.NewHostEntry(es.Spec.GetHost()), vips.OutboundEntry{
+			Port:   es.Spec.GetPortUInt32(),
+			TagSet: map[string]string{mesh_proto.ServiceTag: es.Spec.GetService()},
+			Origin: vips.OriginHost,
+		}))
 	}
 
-	return serviceSet, nil
+	if err != nil {
+		return nil, err
+	}
+	return outboundSet, nil
 }
 
-func UpdateMeshedVIPs(global, meshed vips.List, ipam IPAM, entrySet vips.EntrySet) (updated bool, errs error) {
-	for _, service := range entrySet.ToArray() {
-		_, found := meshed[service]
-		if found {
-			continue
-		}
-		ip, found := global[service]
-		if found {
-			meshed[service] = ip
-			updated = true
-			continue
-		}
-		ip, err := ipam.AllocateIP()
-		if err != nil {
-			errs = multierr.Append(errs, errors.Wrapf(err, "unable to allocate an ip for service %s", service))
-			continue
-		}
-		meshed[service] = ip
-		updated = true
-		vipsAllocatorLog.Info("adding", "service", service, "ip", ip)
-	}
-	for service, ip := range meshed {
-		if _, found := entrySet[service]; !found {
-			updated = true
-			_ = ipam.FreeIP(ip)
-			delete(meshed, service)
-			vipsAllocatorLog.Info("deleting", "service", service, "ip", ip)
+func AllocateVIPs(global *vips.GlobalView, voView *vips.VirtualOutboundMeshView) (errs error) {
+	// Assign ips for all services
+	for _, hostnameEntry := range voView.HostnameEntries() {
+		vo := voView.Get(hostnameEntry)
+		if vo.Address == "" {
+			ip, err := global.Allocate(hostnameEntry)
+			if err != nil {
+				errs = multierr.Append(errs, err)
+			} else {
+				vo.Address = ip
+			}
 		}
 	}
-	return
+	return errs
+}
+
+func addDefault(outboundSet *vips.VirtualOutboundMeshView, service string, port uint32) error {
+	return outboundSet.Add(vips.NewServiceEntry(service), vips.OutboundEntry{
+		TagSet: map[string]string{mesh_proto.ServiceTag: service},
+		Origin: vips.OriginService,
+		Port:   port,
+	})
 }
diff --git a/pkg/dns/vips_synchronizer.go b/pkg/dns/vips_synchronizer.go
index 8f9147cab..fc0fab917 100644
--- a/pkg/dns/vips_synchronizer.go
+++ b/pkg/dns/vips_synchronizer.go
@@ -63,10 +63,10 @@ func (d *vipsSynchronizer) synchronize() error {
 	if d.leadInfo.IsLeader() {
 		return nil // when CP is leader we skip this because VIP allocator updates DNSResolver
 	}
-	vipList, _, err := d.persistence.Get()
+	voByMesh, err := d.persistence.Get()
 	if err != nil {
 		return err
 	}
-	d.resolver.SetVIPs(vipList)
+	d.resolver.SetVIPs(vips.ToVIPMap(voByMesh))
 	return nil
 }
diff --git a/pkg/kds/cache/snapshot.go b/pkg/kds/cache/snapshot.go
index 09b04ace5..e0a6d4d51 100644
--- a/pkg/kds/cache/snapshot.go
+++ b/pkg/kds/cache/snapshot.go
@@ -4,13 +4,13 @@ import (
 	"fmt"
 
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
-	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v2"
+	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
 	"github.com/pkg/errors"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	"github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/resources/registry"
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 type ResourceBuilder interface {
@@ -18,7 +18,7 @@ type ResourceBuilder interface {
 
 type SnapshotBuilder interface {
 	With(typ string, resources []envoy_types.Resource) SnapshotBuilder
-	Build(version string) util_xds.Snapshot
+	Build(version string) util_xds_v3.Snapshot
 }
 
 type builder struct {
@@ -37,7 +37,7 @@ func (b *builder) With(typ string, resources []envoy_types.Resource) SnapshotBui
 	return b
 }
 
-func (b *builder) Build(version string) util_xds.Snapshot {
+func (b *builder) Build(version string) util_xds_v3.Snapshot {
 	snapshot := &Snapshot{Resources: map[string]envoy_cache.Resources{}}
 	for _, typ := range snapshot.GetSupportedTypes() {
 		snapshot.Resources[typ] = envoy_cache.NewResources(version, nil)
@@ -57,7 +57,7 @@ type Snapshot struct {
 	Resources map[string]envoy_cache.Resources
 }
 
-var _ util_xds.Snapshot = &Snapshot{}
+var _ util_xds_v3.Snapshot = &Snapshot{}
 
 func (s *Snapshot) GetSupportedTypes() (types []string) {
 	for _, def := range registry.Global().ObjectTypes(model.HasKdsEnabled()) {
@@ -110,7 +110,7 @@ func (s *Snapshot) GetVersion(typ string) string {
 	return ""
 }
 
-func (s *Snapshot) WithVersion(typ string, version string) util_xds.Snapshot {
+func (s *Snapshot) WithVersion(typ string, version string) util_xds_v3.Snapshot {
 	if s == nil {
 		return nil
 	}
diff --git a/pkg/kds/client/stream.go b/pkg/kds/client/stream.go
index 81f34acb0..b6b4623cc 100644
--- a/pkg/kds/client/stream.go
+++ b/pkg/kds/client/stream.go
@@ -3,8 +3,8 @@ package client
 import (
 	"fmt"
 
-	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"google.golang.org/genproto/googleapis/rpc/status"
 	"google.golang.org/protobuf/types/known/structpb"
 
@@ -28,16 +28,16 @@ var _ KDSStream = &stream{}
 
 type stream struct {
 	streamClient   mesh_proto.KumaDiscoveryService_StreamKumaResourcesClient
-	latestACKed    map[string]*envoy.DiscoveryResponse
-	latestReceived map[string]*envoy.DiscoveryResponse
+	latestACKed    map[string]*envoy_sd.DiscoveryResponse
+	latestReceived map[string]*envoy_sd.DiscoveryResponse
 	clientId       string
 }
 
 func NewKDSStream(s mesh_proto.KumaDiscoveryService_StreamKumaResourcesClient, clientId string) KDSStream {
 	return &stream{
 		streamClient:   s,
-		latestACKed:    make(map[string]*envoy.DiscoveryResponse),
-		latestReceived: make(map[string]*envoy.DiscoveryResponse),
+		latestACKed:    make(map[string]*envoy_sd.DiscoveryResponse),
+		latestReceived: make(map[string]*envoy_sd.DiscoveryResponse),
 		clientId:       clientId,
 	}
 }
@@ -54,7 +54,7 @@ func (s *stream) DiscoveryRequest(resourceType model.ResourceType) error {
 	if err != nil {
 		return err
 	}
-	return s.streamClient.Send(&envoy.DiscoveryRequest{
+	return s.streamClient.Send(&envoy_sd.DiscoveryRequest{
 		VersionInfo:   "",
 		ResponseNonce: "",
 		Node: &envoy_core.Node{
@@ -88,7 +88,7 @@ func (s *stream) ACK(typ string) error {
 	if latestReceived == nil {
 		return nil
 	}
-	err := s.streamClient.Send(&envoy.DiscoveryRequest{
+	err := s.streamClient.Send(&envoy_sd.DiscoveryRequest{
 		VersionInfo:   latestReceived.VersionInfo,
 		ResponseNonce: latestReceived.Nonce,
 		ResourceNames: []string{},
@@ -109,7 +109,7 @@ func (s *stream) NACK(typ string, err error) error {
 		return nil
 	}
 	latestACKed := s.latestACKed[typ]
-	return s.streamClient.Send(&envoy.DiscoveryRequest{
+	return s.streamClient.Send(&envoy_sd.DiscoveryRequest{
 		VersionInfo:   latestACKed.GetVersionInfo(),
 		ResponseNonce: latestReceived.Nonce,
 		ResourceNames: []string{},
diff --git a/pkg/kds/mux/client.go b/pkg/kds/mux/client.go
index 0c63d6397..83ec458c7 100644
--- a/pkg/kds/mux/client.go
+++ b/pkg/kds/mux/client.go
@@ -71,8 +71,11 @@ func (c *client) Start(stop <-chan struct{}) (errs error) {
 	}()
 	muxClient := mesh_proto.NewMultiplexServiceClient(conn)
 
-	withClientIDCtx := metadata.AppendToOutgoingContext(c.ctx, "client-id", c.clientID)
-	stream, err := muxClient.StreamMessage(withClientIDCtx)
+	withKDSCtx := metadata.AppendToOutgoingContext(c.ctx,
+		"client-id", c.clientID,
+		KDSVersionHeaderKey, KDSVersionV3,
+	)
+	stream, err := muxClient.StreamMessage(withKDSCtx)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/kds/mux/clientstream.go b/pkg/kds/mux/clientstream.go
index 97bd6182d..19aa5c50b 100644
--- a/pkg/kds/mux/clientstream.go
+++ b/pkg/kds/mux/clientstream.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"io"
 
-	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"google.golang.org/grpc/metadata"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -12,18 +12,30 @@ import (
 
 type kdsClientStream struct {
 	MultiplexStream
-	responses chan *envoy_api_v2.DiscoveryResponse
+	responses chan *envoy_sd.DiscoveryResponse
 }
 
-func (k *kdsClientStream) put(response *envoy_api_v2.DiscoveryResponse) {
+func (k *kdsClientStream) put(response *envoy_sd.DiscoveryResponse) {
 	k.responses <- response
 }
 
-func (k *kdsClientStream) Send(request *envoy_api_v2.DiscoveryRequest) error {
-	return k.MultiplexStream.Send(&mesh_proto.Message{Value: &mesh_proto.Message_Request{Request: request}})
+func (k *kdsClientStream) Send(request *envoy_sd.DiscoveryRequest) error {
+	var msg *mesh_proto.Message
+
+	kdsVersion := KDSVersion(k.Context())
+	switch kdsVersion {
+	case KDSVersionV2:
+		msg = &mesh_proto.Message{Value: &mesh_proto.Message_LegacyRequest{LegacyRequest: DiscoveryRequestV2(request)}}
+	case KDSVersionV3:
+		msg = &mesh_proto.Message{Value: &mesh_proto.Message_Request{Request: request}}
+	default:
+		return UnsupportedKDSVersion(kdsVersion)
+	}
+
+	return k.MultiplexStream.Send(msg)
 }
 
-func (k *kdsClientStream) Recv() (*envoy_api_v2.DiscoveryResponse, error) {
+func (k *kdsClientStream) Recv() (*envoy_sd.DiscoveryResponse, error) {
 	if r, ok := <-k.responses; ok {
 		return r, nil
 	}
diff --git a/pkg/kds/mux/serverstream.go b/pkg/kds/mux/serverstream.go
index 2a996bc13..f935b6308 100644
--- a/pkg/kds/mux/serverstream.go
+++ b/pkg/kds/mux/serverstream.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"io"
 
-	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"google.golang.org/grpc/metadata"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -18,18 +18,30 @@ type MultiplexStream interface {
 
 type kdsServerStream struct {
 	MultiplexStream
-	requests chan *envoy_api_v2.DiscoveryRequest
+	requests chan *envoy_sd.DiscoveryRequest
 }
 
-func (k *kdsServerStream) put(request *envoy_api_v2.DiscoveryRequest) {
+func (k *kdsServerStream) put(request *envoy_sd.DiscoveryRequest) {
 	k.requests <- request
 }
 
-func (k *kdsServerStream) Send(response *envoy_api_v2.DiscoveryResponse) error {
-	return k.MultiplexStream.Send(&mesh_proto.Message{Value: &mesh_proto.Message_Response{Response: response}})
+func (k *kdsServerStream) Send(response *envoy_sd.DiscoveryResponse) error {
+	var msg *mesh_proto.Message
+
+	kdsVersion := KDSVersion(k.Context())
+	switch kdsVersion {
+	case KDSVersionV2:
+		msg = &mesh_proto.Message{Value: &mesh_proto.Message_LegacyResponse{LegacyResponse: DiscoveryResponseV2(response)}}
+	case KDSVersionV3:
+		msg = &mesh_proto.Message{Value: &mesh_proto.Message_Response{Response: response}}
+	default:
+		return UnsupportedKDSVersion(kdsVersion)
+	}
+
+	return k.MultiplexStream.Send(msg)
 }
 
-func (k *kdsServerStream) Recv() (*envoy_api_v2.DiscoveryRequest, error) {
+func (k *kdsServerStream) Recv() (*envoy_sd.DiscoveryRequest, error) {
 	if r, ok := <-k.requests; ok {
 		return r, nil
 	}
diff --git a/pkg/kds/mux/session.go b/pkg/kds/mux/session.go
index 3df4ba2be..86cfc753f 100644
--- a/pkg/kds/mux/session.go
+++ b/pkg/kds/mux/session.go
@@ -3,7 +3,7 @@ package mux
 import (
 	"sync/atomic"
 
-	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 )
@@ -31,11 +31,11 @@ func NewSession(peerID string, stream MultiplexStream, stop <-chan struct{}) Ses
 		done:   make(chan struct{}, 1),
 		err:    make(chan error, 1),
 		serverStream: &kdsServerStream{
-			requests:        make(chan *envoy_api_v2.DiscoveryRequest, 1),
+			requests:        make(chan *envoy_sd.DiscoveryRequest, 1),
 			MultiplexStream: stream,
 		},
 		clientStream: &kdsClientStream{
-			responses:       make(chan *envoy_api_v2.DiscoveryResponse, 1),
+			responses:       make(chan *envoy_sd.DiscoveryResponse, 1),
 			MultiplexStream: stream,
 		},
 		closed: int32(0),
@@ -65,8 +65,12 @@ func (s *session) handle(stream MultiplexStream, stop <-chan struct{}) error {
 			return err
 		}
 		switch v := msg.Value.(type) {
+		case *mesh_proto.Message_LegacyRequest:
+			s.serverStream.put(DiscoveryRequestV3(v.LegacyRequest))
 		case *mesh_proto.Message_Request:
 			s.serverStream.put(v.Request)
+		case *mesh_proto.Message_LegacyResponse:
+			s.clientStream.put(DiscoveryResponseV3(v.LegacyResponse))
 		case *mesh_proto.Message_Response:
 			s.clientStream.put(v.Response)
 		}
diff --git a/pkg/kds/mux/version.go b/pkg/kds/mux/version.go
new file mode 100644
index 000000000..e82c7a0fb
--- /dev/null
+++ b/pkg/kds/mux/version.go
@@ -0,0 +1,92 @@
+package mux
+
+import (
+	"context"
+
+	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_sd_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
+	"github.com/pkg/errors"
+	"google.golang.org/grpc/metadata"
+)
+
+var KDSVersionHeaderKey = "kds-version"
+var KDSVersionV2 = "v2"
+var KDSVersionV3 = "v3"
+
+func KDSVersion(ctx context.Context) string {
+	md, found := metadata.FromIncomingContext(ctx)
+	if found {
+		version := md.Get(KDSVersionHeaderKey)
+		if len(version) == 1 {
+			return version[0]
+		}
+	}
+
+	md, found = metadata.FromOutgoingContext(ctx)
+	if found {
+		version := md.Get(KDSVersionHeaderKey)
+		if len(version) == 1 {
+			return version[0]
+		}
+	}
+
+	return KDSVersionV2
+}
+
+func UnsupportedKDSVersion(version string) error {
+	return errors.Errorf("invalid KDS version %s. Supported versions %s %s", version, KDSVersionV2, KDSVersionV3)
+}
+
+func DiscoveryRequestV2(request *envoy_sd_v3.DiscoveryRequest) *envoy_api_v2.DiscoveryRequest {
+	return &envoy_api_v2.DiscoveryRequest{
+		VersionInfo: request.VersionInfo,
+		Node: &envoy_core_v2.Node{
+			Id:       request.Node.Id,
+			Metadata: request.Node.Metadata,
+		},
+		ResourceNames: request.ResourceNames,
+		TypeUrl:       request.TypeUrl,
+		ResponseNonce: request.ResponseNonce,
+		ErrorDetail:   request.ErrorDetail,
+	}
+}
+
+func DiscoveryRequestV3(request *envoy_api_v2.DiscoveryRequest) *envoy_sd_v3.DiscoveryRequest {
+	return &envoy_sd_v3.DiscoveryRequest{
+		VersionInfo: request.VersionInfo,
+		Node: &envoy_core_v3.Node{
+			Id:       request.Node.Id,
+			Metadata: request.Node.Metadata,
+		},
+		ResourceNames: request.ResourceNames,
+		TypeUrl:       request.TypeUrl,
+		ResponseNonce: request.ResponseNonce,
+		ErrorDetail:   request.ErrorDetail,
+	}
+}
+
+func DiscoveryResponseV2(response *envoy_sd_v3.DiscoveryResponse) *envoy_api_v2.DiscoveryResponse {
+	return &envoy_api_v2.DiscoveryResponse{
+		VersionInfo: response.VersionInfo,
+		Resources:   response.Resources,
+		TypeUrl:     response.TypeUrl,
+		Nonce:       response.Nonce,
+		ControlPlane: &envoy_core_v2.ControlPlane{
+			Identifier: response.ControlPlane.Identifier,
+		},
+	}
+}
+
+func DiscoveryResponseV3(response *envoy_api_v2.DiscoveryResponse) *envoy_sd_v3.DiscoveryResponse {
+	return &envoy_sd_v3.DiscoveryResponse{
+		VersionInfo: response.VersionInfo,
+		Resources:   response.Resources,
+		TypeUrl:     response.TypeUrl,
+		Nonce:       response.Nonce,
+		ControlPlane: &envoy_core_v3.ControlPlane{
+			Identifier: response.ControlPlane.Identifier,
+		},
+	}
+}
diff --git a/pkg/kds/reconcile/interfaces.go b/pkg/kds/reconcile/interfaces.go
index 786964344..159bdc9e9 100644
--- a/pkg/kds/reconcile/interfaces.go
+++ b/pkg/kds/reconcile/interfaces.go
@@ -3,9 +3,9 @@ package reconcile
 import (
 	"context"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 // Reconciler re-computes configuration for a given node.
@@ -15,5 +15,5 @@ type Reconciler interface {
 
 // Generates a snapshot of xDS resources for a given node.
 type SnapshotGenerator interface {
-	GenerateSnapshot(context.Context, *envoy_core.Node) (util_xds.Snapshot, error)
+	GenerateSnapshot(context.Context, *envoy_core.Node) (util_xds_v3.Snapshot, error)
 }
diff --git a/pkg/kds/reconcile/reconciler.go b/pkg/kds/reconcile/reconciler.go
index de27c88de..b4e1911d3 100644
--- a/pkg/kds/reconcile/reconciler.go
+++ b/pkg/kds/reconcile/reconciler.go
@@ -3,17 +3,17 @@ package reconcile
 import (
 	"context"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
 
 	config_core "github.com/kumahq/kuma/pkg/config/core"
 	"github.com/kumahq/kuma/pkg/core"
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 var log = core.Log.WithName("kds").WithName("reconcile")
 
-func NewReconciler(hasher envoy_cache.NodeHash, cache util_xds.SnapshotCache, generator SnapshotGenerator, versioner util_xds.SnapshotVersioner, mode config_core.CpMode) Reconciler {
+func NewReconciler(hasher envoy_cache.NodeHash, cache util_xds_v3.SnapshotCache, generator SnapshotGenerator, versioner util_xds_v3.SnapshotVersioner, mode config_core.CpMode) Reconciler {
 	return &reconciler{
 		hasher:    hasher,
 		cache:     cache,
@@ -25,9 +25,9 @@ func NewReconciler(hasher envoy_cache.NodeHash, cache util_xds.SnapshotCache, ge
 
 type reconciler struct {
 	hasher    envoy_cache.NodeHash
-	cache     util_xds.SnapshotCache
+	cache     util_xds_v3.SnapshotCache
 	generator SnapshotGenerator
-	versioner util_xds.SnapshotVersioner
+	versioner util_xds_v3.SnapshotVersioner
 	mode      config_core.CpMode
 }
 
@@ -46,7 +46,7 @@ func (r *reconciler) Reconcile(ctx context.Context, node *envoy_core.Node) error
 	return r.cache.SetSnapshot(id, new)
 }
 
-func (r *reconciler) logChanges(new util_xds.Snapshot, old util_xds.Snapshot, node *envoy_core.Node) {
+func (r *reconciler) logChanges(new util_xds_v3.Snapshot, old util_xds_v3.Snapshot, node *envoy_core.Node) {
 	for _, typ := range new.GetSupportedTypes() {
 		if old != nil && old.GetVersion(typ) != new.GetVersion(typ) {
 			client := node.Id
diff --git a/pkg/kds/reconcile/snapshot_generator.go b/pkg/kds/reconcile/snapshot_generator.go
index fadc3e87b..f9aafa7f5 100644
--- a/pkg/kds/reconcile/snapshot_generator.go
+++ b/pkg/kds/reconcile/snapshot_generator.go
@@ -3,7 +3,7 @@ package reconcile
 import (
 	"context"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
 
 	core_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
@@ -11,7 +11,7 @@ import (
 	"github.com/kumahq/kuma/pkg/core/resources/registry"
 	"github.com/kumahq/kuma/pkg/kds/cache"
 	"github.com/kumahq/kuma/pkg/kds/util"
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 type ResourceFilter func(clusterID string, r model.Resource) bool
@@ -34,7 +34,7 @@ type snapshotGenerator struct {
 	resourceFilter  ResourceFilter
 }
 
-func (s *snapshotGenerator) GenerateSnapshot(ctx context.Context, node *envoy_core.Node) (util_xds.Snapshot, error) {
+func (s *snapshotGenerator) GenerateSnapshot(ctx context.Context, node *envoy_core.Node) (util_xds_v3.Snapshot, error) {
 	builder := cache.NewSnapshotBuilder()
 	for _, typ := range s.resourceTypes {
 		resources, err := s.getResources(ctx, typ, node)
diff --git a/pkg/kds/server/components.go b/pkg/kds/server/components.go
index 4550c2512..35d705043 100644
--- a/pkg/kds/server/components.go
+++ b/pkg/kds/server/components.go
@@ -4,9 +4,9 @@ import (
 	"context"
 	"time"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v2"
-	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
+	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v3"
 	"github.com/go-logr/logr"
 	"github.com/prometheus/client_golang/prometheus"
 
@@ -17,13 +17,13 @@ import (
 	core_metrics "github.com/kumahq/kuma/pkg/metrics"
 	util_watchdog "github.com/kumahq/kuma/pkg/util/watchdog"
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
-	util_xds_v2 "github.com/kumahq/kuma/pkg/util/xds/v2"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 func New(log logr.Logger, rt core_runtime.Runtime, providedTypes []model.ResourceType, serverID string, refresh time.Duration, filter reconcile.ResourceFilter, insight bool) (Server, error) {
 	hasher, cache := newKDSContext(log)
 	generator := reconcile.NewSnapshotGenerator(rt.ReadOnlyResourceManager(), providedTypes, filter)
-	versioner := util_xds.SnapshotAutoVersioner{UUID: core.NewUUID}
+	versioner := util_xds_v3.SnapshotAutoVersioner{UUID: core.NewUUID}
 	reconciler := reconcile.NewReconciler(hasher, cache, generator, versioner, rt.Config().Mode)
 	syncTracker, err := newSyncTracker(log, reconciler, refresh, rt.Metrics())
 	if err != nil {
@@ -33,11 +33,11 @@ func New(log logr.Logger, rt core_runtime.Runtime, providedTypes []model.Resourc
 	if err != nil {
 		return nil, err
 	}
-	callbacks := util_xds_v2.CallbacksChain{
+	callbacks := util_xds_v3.CallbacksChain{
 		&typeAdjustCallbacks{},
-		util_xds_v2.NewControlPlaneIdCallbacks(serverID),
-		util_xds_v2.AdaptCallbacks(util_xds.LoggingCallbacks{Log: log}),
-		util_xds_v2.AdaptCallbacks(statsCallbacks),
+		util_xds_v3.NewControlPlaneIdCallbacks(serverID),
+		util_xds_v3.AdaptCallbacks(util_xds.LoggingCallbacks{Log: log}),
+		util_xds_v3.AdaptCallbacks(statsCallbacks),
 		syncTracker,
 	}
 	if insight {
@@ -75,7 +75,7 @@ func newSyncTracker(log logr.Logger, reconciler reconcile.Reconciler, refresh ti
 	if err := metrics.Register(kdsGenerationsErrors); err != nil {
 		return nil, err
 	}
-	return util_xds_v2.NewWatchdogCallbacks(func(ctx context.Context, node *envoy_core.Node, streamID int64) (util_watchdog.Watchdog, error) {
+	return util_xds_v3.NewWatchdogCallbacks(func(ctx context.Context, node *envoy_core.Node, streamID int64) (util_watchdog.Watchdog, error) {
 		log := log.WithValues("streamID", streamID, "node", node)
 		return &util_watchdog.SimpleWatchdog{
 			NewTicker: func() *time.Ticker {
@@ -97,10 +97,10 @@ func newSyncTracker(log logr.Logger, reconciler reconcile.Reconciler, refresh ti
 	}), nil
 }
 
-func newKDSContext(log logr.Logger) (envoy_cache.NodeHash, util_xds.SnapshotCache) {
+func newKDSContext(log logr.Logger) (envoy_cache.NodeHash, util_xds_v3.SnapshotCache) {
 	hasher := hasher{}
 	logger := util_xds.NewLogger(log)
-	return hasher, util_xds.NewSnapshotCache(false, hasher, logger)
+	return hasher, util_xds_v3.NewSnapshotCache(false, hasher, logger)
 }
 
 type hasher struct {
diff --git a/pkg/kds/server/kds.go b/pkg/kds/server/kds.go
index 6b9a480f0..aeda5ad48 100644
--- a/pkg/kds/server/kds.go
+++ b/pkg/kds/server/kds.go
@@ -3,10 +3,9 @@ package server
 import (
 	"context"
 
-	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v2"
-	"github.com/envoyproxy/go-control-plane/pkg/server/sotw/v2"
-	envoy_server "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	envoy_cache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
+	"github.com/envoyproxy/go-control-plane/pkg/server/sotw/v3"
+	envoy_server "github.com/envoyproxy/go-control-plane/pkg/server/v3"
 	"github.com/go-logr/logr"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -27,14 +26,6 @@ type server struct {
 	sotw.Server
 }
 
-func (s *server) DeltaKumaResources(resourcesServer mesh_proto.KumaDiscoveryService_DeltaKumaResourcesServer) error {
-	panic("not supported") // we don't use Delta XDS in KDS
-}
-
 func (s *server) StreamKumaResources(stream mesh_proto.KumaDiscoveryService_StreamKumaResourcesServer) error {
 	return s.StreamHandler(stream, "")
 }
-
-func (s *server) FetchKumaResources(ctx context.Context, request *envoy.DiscoveryRequest) (*envoy.DiscoveryResponse, error) {
-	panic("not supported") // we don't need to support Fetch
-}
diff --git a/pkg/kds/server/status_tracker.go b/pkg/kds/server/status_tracker.go
index 0e04a578f..53ae1d7fe 100644
--- a/pkg/kds/server/status_tracker.go
+++ b/pkg/kds/server/status_tracker.go
@@ -4,8 +4,8 @@ import (
 	"context"
 	"sync"
 
-	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
+	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v3"
 	"github.com/go-logr/logr"
 	"github.com/golang/protobuf/proto"
 	"google.golang.org/protobuf/types/known/structpb"
@@ -16,7 +16,7 @@ import (
 	core_runtime "github.com/kumahq/kuma/pkg/core/runtime"
 	"github.com/kumahq/kuma/pkg/kds/util"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
-	util_xds_v2 "github.com/kumahq/kuma/pkg/util/xds/v2"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 type StatusTracker interface {
@@ -43,7 +43,7 @@ func NewStatusTracker(runtimeInfo core_runtime.RuntimeInfo,
 var _ StatusTracker = &statusTracker{}
 
 type statusTracker struct {
-	util_xds_v2.NoopCallbacks
+	util_xds_v3.NoopCallbacks
 	runtimeInfo      core_runtime.RuntimeInfo
 	createStatusSink ZoneInsightSinkFactoryFunc
 	mu               sync.RWMutex // protects access to the fields below
@@ -107,7 +107,7 @@ func (c *statusTracker) OnStreamClosed(streamID int64) {
 
 // OnStreamRequest is called once a request is received on a stream.
 // Returning an error will end processing and close the stream. OnStreamClosed will still be called.
-func (c *statusTracker) OnStreamRequest(streamID int64, req *envoy.DiscoveryRequest) error {
+func (c *statusTracker) OnStreamRequest(streamID int64, req *envoy_sd.DiscoveryRequest) error {
 	c.mu.RLock() // read access to the map of all ADS streams
 	defer c.mu.RUnlock()
 
@@ -143,7 +143,7 @@ func (c *statusTracker) OnStreamRequest(streamID int64, req *envoy.DiscoveryRequ
 }
 
 // OnStreamResponse is called immediately prior to sending a response on a stream.
-func (c *statusTracker) OnStreamResponse(streamID int64, req *envoy.DiscoveryRequest, resp *envoy.DiscoveryResponse) {
+func (c *statusTracker) OnStreamResponse(streamID int64, req *envoy_sd.DiscoveryRequest, resp *envoy_sd.DiscoveryResponse) {
 	c.mu.RLock() // read access to the map of all ADS streams
 	defer c.mu.RUnlock()
 
diff --git a/pkg/kds/server/type_adjust_callbacks.go b/pkg/kds/server/type_adjust_callbacks.go
index 7e371729b..a09f8e046 100644
--- a/pkg/kds/server/type_adjust_callbacks.go
+++ b/pkg/kds/server/type_adjust_callbacks.go
@@ -1,10 +1,10 @@
 package server
 
 import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 
 	"github.com/kumahq/kuma/pkg/kds"
-	util_xds_v2 "github.com/kumahq/kuma/pkg/util/xds/v2"
+	util_xds_v3 "github.com/kumahq/kuma/pkg/util/xds/v3"
 )
 
 // We are using go-control-plane's server and cache for KDS exchange.
@@ -16,10 +16,10 @@ import (
 // Ignores the TypeURL from marshalling operation and overrides it with TypeURL of the request.
 // If we pass wrong TypeURL in envoy_api.DiscoveryResponse#Resources we won't be able to unmarshall it, therefore we need to adjust the type.
 type typeAdjustCallbacks struct {
-	util_xds_v2.NoopCallbacks
+	util_xds_v3.NoopCallbacks
 }
 
-func (c *typeAdjustCallbacks) OnStreamResponse(streamID int64, req *envoy_api.DiscoveryRequest, resp *envoy_api.DiscoveryResponse) {
+func (c *typeAdjustCallbacks) OnStreamResponse(streamID int64, req *envoy_sd.DiscoveryRequest, resp *envoy_sd.DiscoveryResponse) {
 	for _, res := range resp.Resources {
 		res.TypeUrl = kds.KumaResource
 	}
diff --git a/pkg/kds/store/sync.go b/pkg/kds/store/sync.go
index 519566234..a6ade782c 100644
--- a/pkg/kds/store/sync.go
+++ b/pkg/kds/store/sync.go
@@ -141,7 +141,6 @@ func (s *syncResourceStore) Sync(upstream model.ResourceList, fs ...SyncOptionFu
 		createOpts := []store.CreateOptionsFunc{
 			store.CreateBy(rk),
 			store.CreatedAt(creationTime),
-			store.CreateSynced(),
 		}
 		if opts.Zone != "" {
 			createOpts = append(createOpts, store.CreateWithOwner(zone))
@@ -157,7 +156,7 @@ func (s *syncResourceStore) Sync(upstream model.ResourceList, fs ...SyncOptionFu
 		// some stores manage ModificationTime time on they own (Kubernetes), in order to be consistent
 		// we set ModificationTime when we add to downstream store. This time is almost the same with ModificationTime
 		// from upstream store, because we update downstream only when resource have changed in upstream
-		if err := s.resourceStore.Update(ctx, r, store.ModifiedAt(now), store.UpdateSynced()); err != nil {
+		if err := s.resourceStore.Update(ctx, r, store.ModifiedAt(now)); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/kds/util/resource.go b/pkg/kds/util/resource.go
index 0d1c42bc1..9795493f0 100644
--- a/pkg/kds/util/resource.go
+++ b/pkg/kds/util/resource.go
@@ -3,7 +3,7 @@ package util
 import (
 	"fmt"
 
-	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -13,7 +13,7 @@ import (
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 )
 
-func ToCoreResourceList(response *envoy.DiscoveryResponse) (model.ResourceList, error) {
+func ToCoreResourceList(response *envoy_sd.DiscoveryResponse) (model.ResourceList, error) {
 	krs := []*mesh_proto.KumaResource{}
 	for _, r := range response.Resources {
 		kr := &mesh_proto.KumaResource{}
diff --git a/pkg/plugins/common/k8s/names.go b/pkg/plugins/common/k8s/names.go
index dd30204c9..7be75b2d5 100644
--- a/pkg/plugins/common/k8s/names.go
+++ b/pkg/plugins/common/k8s/names.go
@@ -15,8 +15,8 @@ const (
 	// The value has a format of a Kubernetes label name.
 	k8sNameComponent = "k8s.kuma.io/name"
 
-	// k8sSynced identifies that resource was synced
-	K8sSynced = "k8s.kuma.io/synced"
+	// K8sMeshDefaultsGenerated identifies that default resources for mesh were successfully generated
+	K8sMeshDefaultsGenerated = "k8s.kuma.io/mesh-defaults-generated"
 
 	// Kubernetes secret type to differentiate Kuma System secrets. Secret is bound to a mesh
 	MeshSecretType = "system.kuma.io/secret"
diff --git a/pkg/plugins/resources/k8s/store.go b/pkg/plugins/resources/k8s/store.go
index 972f18ea6..a7df5f99f 100644
--- a/pkg/plugins/resources/k8s/store.go
+++ b/pkg/plugins/resources/k8s/store.go
@@ -60,10 +60,6 @@ func (s *KubernetesStore) Create(ctx context.Context, r core_model.Resource, fs
 		}
 	}
 
-	if opts.Synced {
-		markAsSynced(obj)
-	}
-
 	if err := s.Client.Create(ctx, obj); err != nil {
 		if kube_apierrs.IsAlreadyExists(err) {
 			return store.ErrorResourceAlreadyExists(r.Descriptor().Name, opts.Name, opts.Mesh)
@@ -77,27 +73,12 @@ func (s *KubernetesStore) Create(ctx context.Context, r core_model.Resource, fs
 	return nil
 }
 
-func markAsSynced(obj k8s_model.KubernetesObject) {
-	annotations := obj.GetAnnotations()
-	if annotations == nil {
-		annotations = map[string]string{}
-	}
-	annotations[k8s_common.K8sSynced] = "true"
-	obj.SetAnnotations(annotations)
-}
-
 func (s *KubernetesStore) Update(ctx context.Context, r core_model.Resource, fs ...store.UpdateOptionsFunc) error {
-	opts := store.NewUpdateOptions(fs...)
-
 	obj, err := s.Converter.ToKubernetesObject(r)
 	if err != nil {
 		return errors.Wrapf(err, "failed to convert core model of type %s into k8s counterpart", r.Descriptor().Name)
 	}
 
-	if opts.Synced {
-		markAsSynced(obj)
-	}
-
 	if err := s.Client.Update(ctx, obj); err != nil {
 		if kube_apierrs.IsConflict(err) {
 			return store.ErrorResourceConflict(r.Descriptor().Name, r.GetMeta().GetName(), r.GetMeta().GetMesh())
diff --git a/pkg/plugins/runtime/gateway/listener_generator.go b/pkg/plugins/runtime/gateway/listener_generator.go
index ce6cebe5e..f58a42f96 100644
--- a/pkg/plugins/runtime/gateway/listener_generator.go
+++ b/pkg/plugins/runtime/gateway/listener_generator.go
@@ -1,21 +1,243 @@
 package gateway
 
 import (
+	"context"
+	"fmt"
+	"time"
+
+	envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
 	"github.com/pkg/errors"
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/wrapperspb"
 
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	"github.com/kumahq/kuma/pkg/core/policy"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
-	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/core/resources/store"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/listeners/v3"
+	envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names"
 	"github.com/kumahq/kuma/pkg/xds/generator"
 )
 
-// ListenerGenerator generates Kuma gateway listeners. Not implemented.
+const DefaultConnectionBuffer = 32 * 1024
+const DefaultRequestHeadersTimeoutMsec = 500 * time.Millisecond
+const DefaultStreamIdleTimeoutMsec = 5 * time.Second
+const DefaultConcurrentStreams = 100
+const DefaultInitialStreamWindowSize = 64 * 1024
+const DefaultInitialConnectionWindowSize = 1024 * 1024
+const DefaultIdleTimeout = 5 * time.Minute
+
+type gatewayPolicyAdaptor struct {
+	*core_mesh.GatewayResource
+}
+
+func (g gatewayPolicyAdaptor) Selectors() []*mesh_proto.Selector {
+	return g.Sources()
+}
+
+func findMatchingGateway(m manager.ReadOnlyResourceManager, dp *core_mesh.DataplaneResource) *core_mesh.GatewayResource {
+	gatewayList := &core_mesh.GatewayResourceList{}
+
+	if err := m.List(context.Background(), gatewayList, store.ListByMesh(dp.Meta.GetMesh())); err != nil {
+		return nil
+	}
+
+	candidates := make([]policy.DataplanePolicy, len(gatewayList.Items))
+	for i, gw := range gatewayList.Items {
+		candidates[i] = gatewayPolicyAdaptor{gw}
+	}
+
+	if p := policy.SelectDataplanePolicy(dp, candidates); p != nil {
+		return p.(gatewayPolicyAdaptor).GatewayResource
+	}
+
+	return nil
+}
+
+var _ policy.DataplanePolicy = gatewayPolicyAdaptor{}
+
+// ListenerGenerator generates Kuma gateway listeners.
 type ListenerGenerator struct {
 	Resources manager.ReadOnlyResourceManager
 }
 
 var _ generator.ResourceGenerator = ListenerGenerator{}
 
-func (ListenerGenerator) Generate(ctx xds_context.Context, proxy *xds.Proxy) (*xds.ResourceSet, error) {
-	return nil, errors.New("not implemented")
+func (l ListenerGenerator) Generate(_ xds_context.Context, proxy *core_xds.Proxy) (*core_xds.ResourceSet, error) {
+	gw := findMatchingGateway(l.Resources, proxy.Dataplane)
+	if gw == nil {
+		log.V(1).Info("no matching gateway for dataplane",
+			"name", proxy.Dataplane.Meta.GetName(),
+			"mesh", proxy.Dataplane.Meta.GetMesh(),
+		)
+
+		return nil, nil
+	}
+
+	log.V(1).Info(fmt.Sprintf("matched gateway %q to dataplane %q",
+		gw.Meta.GetName(), proxy.Dataplane.Meta.GetName()))
+
+	// Multiple listener specifications can have the same port. If
+	// they are compatible, then we can collapse those specifications
+	// down to a single listener.
+	collapsed := map[uint32][]*mesh_proto.Gateway_Listener{}
+	for _, ep := range gw.Spec.GetConf().GetListeners() {
+		collapsed[ep.GetPort()] = append(collapsed[ep.GetPort()], ep)
+	}
+
+	// A Gateway is a single service across all listeners.
+	service := proxy.Dataplane.Spec.GetIdentifyingService()
+
+	resources := core_xds.NewResourceSet()
+
+	// Generate a listener resource for each port.
+	for port, listeners := range collapsed {
+		protocol := listeners[0].GetProtocol()
+		address := proxy.Dataplane.Spec.GetNetworking().Address
+
+		log.V(1).Info("generating listener",
+			"address", address,
+			"port", port,
+			"protocol", protocol,
+		)
+
+		// TODO(jpeach) verify that the listeners are compatible.
+		// TODO(jpeach) hoist the compatibility check and use it in Gateway validation.
+
+		// This check forces all listeners on the port to have
+		// the same protocol, which is unnecessarily strict. We
+		// cal allow TLS and HTTPS on the same port, for example.
+		for i := range listeners {
+			if listeners[i].GetProtocol() != listeners[0].GetProtocol() {
+				return nil, errors.Errorf("cannot collapse listener protocols %s and %s",
+					listeners[i].GetProtocol(), listeners[0].GetProtocol(),
+				)
+			}
+		}
+
+		switch protocol {
+		case mesh_proto.Gateway_Listener_UDP:
+			fallthrough
+		case mesh_proto.Gateway_Listener_TCP:
+			fallthrough
+		case mesh_proto.Gateway_Listener_TLS:
+			fallthrough
+		case mesh_proto.Gateway_Listener_HTTPS:
+			return nil, errors.Errorf("unsupported protocol %q", protocol)
+		}
+
+		filters := envoy_listeners.NewFilterChainBuilder(proxy.APIVersion)
+
+		// For HTTP protocols, add the connection manager and
+		// (for now), send all traffic to the default route,
+		// just to keep the xDS snapshot consistent.
+		switch protocol {
+		case mesh_proto.Gateway_Listener_HTTP:
+			filters.Configure(
+				envoy_listeners.HttpConnectionManager(service, false),
+				envoy_listeners.HttpDynamicRoute(DefaultRouteName),
+			)
+		case mesh_proto.Gateway_Listener_HTTPS:
+			filters.Configure(
+				envoy_listeners.HttpConnectionManager(service, true),
+				envoy_listeners.HttpDynamicRoute(DefaultRouteName),
+			)
+		}
+
+		// Add edge proxy recommendations.
+		filters.Configure(
+			envoy_listeners.AddFilterChainConfigurer(
+				v3.HttpConnectionManagerMustConfigureFunc(func(hcm *envoy_hcm.HttpConnectionManager) {
+					hcm.ServerName = "Kuma Gateway"
+
+					hcm.NormalizePath = wrapperspb.Bool(true)
+					hcm.MergeSlashes = true
+
+					// TODO(jpeach) set path_with_escaped_slashes_action when we upgrade to Envoy v1.19.
+
+					hcm.RequestHeadersTimeout = durationpb.New(DefaultRequestHeadersTimeoutMsec)
+					hcm.StreamIdleTimeout = durationpb.New(DefaultStreamIdleTimeoutMsec)
+
+					hcm.CommonHttpProtocolOptions = &envoy_config_core_v3.HttpProtocolOptions{
+						IdleTimeout:                  durationpb.New(DefaultIdleTimeout),
+						HeadersWithUnderscoresAction: envoy_config_core_v3.HttpProtocolOptions_REJECT_REQUEST,
+					}
+
+					hcm.Http2ProtocolOptions = &envoy_config_core_v3.Http2ProtocolOptions{
+						MaxConcurrentStreams:        wrapperspb.UInt32(DefaultConcurrentStreams),
+						InitialStreamWindowSize:     wrapperspb.UInt32(DefaultInitialStreamWindowSize),
+						InitialConnectionWindowSize: wrapperspb.UInt32(DefaultInitialConnectionWindowSize),
+						AllowConnect:                true,
+					}
+				}),
+			),
+		)
+
+		// Tracing and logging have to be configured after the HttpConnectionManager is enabled.
+		filters.Configure(
+			envoy_listeners.Tracing(proxy.Policies.TracingBackend, service),
+			// XXX Logging policy doesn't work at all. The logging backend is selected by
+			// matching against outbound service names, and gateway dataplanes don't have
+			// any of those.
+			envoy_listeners.HttpAccessLog(
+				proxy.Dataplane.Meta.GetMesh(),
+				envoy.TrafficDirectionInbound,
+				service, // Source service is the gateway service.
+				"*",     // Destination service could be anywhere, depending on the routes.
+				proxy.Policies.Logs[service],
+				proxy,
+			),
+		)
+
+		// TODO(jpeach) add compressor filter.
+		// TODO(jpeach) add decompressor filter.
+		// TODO(jpeach) add grpc_web filter.
+		// TODO(jpeach) add grpc_stats filter.
+
+		listener := envoy_listeners.NewListenerBuilder(proxy.APIVersion)
+		listener.Configure(
+			envoy_listeners.InboundListener(
+				envoy_names.GetGatewayListenerName(gw.Meta.GetName(), protocol.String(), port),
+				address, port, core_xds.SocketAddressProtocolTCP),
+			// Limit default buffering for edge connections.
+			envoy_listeners.ConnectionBufferLimit(DefaultConnectionBuffer),
+			// Roughly balance incoming connections.
+			envoy_listeners.EnableReusePort(true),
+			// Always sniff for TLS.
+			envoy_listeners.TLSInspector(),
+		)
+
+		// TODO(jpeach) if proxy protocol is enabled, add the listener filter.
+
+		// Now, for each of the collapsed listeners on this port, configure the
+		for range listeners {
+			switch protocol {
+			case mesh_proto.Gateway_Listener_HTTPS:
+				// TODO(jpeach) add a SNI listener to match the hostname
+				// and apply the right set of dynamic HTTP routes.
+			case mesh_proto.Gateway_Listener_TLS:
+				// TODO(jpeach) add a SNI listener to match the hostname
+				// and apply the right set of dynamic TCP or TLS routes.
+			}
+		}
+
+		listener.Configure(
+			envoy_listeners.FilterChain(filters),
+		)
+
+		resourceSet, err := BuildResourceSet(listener)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to generate listener for port %d", port)
+		}
+
+		resources.AddSet(resourceSet)
+	}
+
+	return resources, nil
 }
diff --git a/pkg/plugins/runtime/gateway/route_generator.go b/pkg/plugins/runtime/gateway/route_generator.go
index e0ca902ac..69e437b7f 100644
--- a/pkg/plugins/runtime/gateway/route_generator.go
+++ b/pkg/plugins/runtime/gateway/route_generator.go
@@ -1,8 +1,6 @@
 package gateway
 
 import (
-	"github.com/pkg/errors"
-
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
 	"github.com/kumahq/kuma/pkg/core/xds"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
@@ -19,7 +17,8 @@ type RouteGenerator struct {
 var _ generator.ResourceGenerator = &RouteGenerator{}
 
 func (r RouteGenerator) Generate(ctx xds_context.Context, proxy *xds.Proxy) (*xds.ResourceSet, error) {
-	return nil, errors.New("not implemented")
+	log.V(2).Info("Gateway route generation not implemented")
+	return nil, nil
 }
 
 // DefaultRouteName is the well-known name of the default route configuration.
diff --git a/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller.go b/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller.go
index e75b3e7b3..dd87d6da4 100644
--- a/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller.go
+++ b/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller.go
@@ -1,15 +1,24 @@
 package controllers
 
 import (
+	"context"
+
 	"github.com/pkg/errors"
 	kube_core "k8s.io/api/core/v1"
+	kube_apierrs "k8s.io/apimachinery/pkg/api/errors"
 	kube_ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 
+	"github.com/kumahq/kuma/pkg/core"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"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/store"
 	defaults_mesh "github.com/kumahq/kuma/pkg/defaults/mesh"
+	common_k8s "github.com/kumahq/kuma/pkg/plugins/common/k8s"
+	"github.com/kumahq/kuma/pkg/plugins/resources/k8s"
 	mesh_k8s "github.com/kumahq/kuma/pkg/plugins/resources/k8s/native/api/v1alpha1"
 )
 
@@ -19,9 +28,32 @@ type MeshDefaultsReconciler struct {
 }
 
 func (r *MeshDefaultsReconciler) Reconcile(req kube_ctrl.Request) (kube_ctrl.Result, error) {
+	mesh := core_mesh.NewMeshResource()
+	if err := r.ResourceManager.Get(context.Background(), mesh, store.GetByKey(req.Name, core_model.NoMesh)); err != nil {
+		if kube_apierrs.IsNotFound(err) {
+			return kube_ctrl.Result{}, nil
+		}
+		return kube_ctrl.Result{}, errors.Wrap(err, "could not get default mesh resources")
+	}
+
+	// Before creating default policies for the mesh we want to ensure that this mesh wasn't processed before.
+	// We can't rely on filtering by CreateFunc, because apparently it sends the create event every time resource
+	// is added to the underlying Informer. That's why on the Kuma CP restart Mesh will be processed the second time
+	if processed := mesh.GetMeta().(*k8s.KubernetesMetaAdapter).GetAnnotations()[common_k8s.K8sMeshDefaultsGenerated]; processed == "true" {
+		return kube_ctrl.Result{}, nil
+	}
+
 	if err := defaults_mesh.EnsureDefaultMeshResources(r.ResourceManager, req.Name); err != nil {
 		return kube_ctrl.Result{}, errors.Wrap(err, "could not create default mesh resources")
 	}
+
+	if mesh.GetMeta().(*k8s.KubernetesMetaAdapter).GetAnnotations() == nil {
+		mesh.GetMeta().(*k8s.KubernetesMetaAdapter).Annotations = map[string]string{}
+	}
+	mesh.GetMeta().(*k8s.KubernetesMetaAdapter).GetAnnotations()[common_k8s.K8sMeshDefaultsGenerated] = "true"
+	if err := r.ResourceManager.Update(context.Background(), mesh, store.ModifiedAt(core.Now())); err != nil {
+		return kube_ctrl.Result{}, errors.Wrap(err, "could not update default mesh resources")
+	}
 	return kube_ctrl.Result{}, nil
 }
 
diff --git a/pkg/xds/context/context.go b/pkg/xds/context/context.go
index f1e743b63..951ee4c47 100644
--- a/pkg/xds/context/context.go
+++ b/pkg/xds/context/context.go
@@ -6,7 +6,6 @@ import (
 	kuma_cp "github.com/kumahq/kuma/pkg/config/app/kuma-cp"
 	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/xds"
-	"github.com/kumahq/kuma/pkg/dns/resolver"
 	"github.com/kumahq/kuma/pkg/envoy/admin"
 	"github.com/kumahq/kuma/pkg/tls"
 )
@@ -27,7 +26,6 @@ type ControlPlaneContext struct {
 	SdsTlsCert        []byte
 	AdminProxyKeyPair *tls.KeyPair
 	CLACache          xds.CLACache
-	DNSResolver       resolver.DNSResolver
 }
 
 func (c Context) SDSLocation() string {
@@ -41,7 +39,7 @@ type MeshContext struct {
 	Hash       string
 }
 
-func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache, dnsResolver resolver.DNSResolver) (*ControlPlaneContext, error) {
+func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache) (*ControlPlaneContext, error) {
 	var sdsCert []byte
 	if config.DpServer.TlsCertFile != "" {
 		c, err := ioutil.ReadFile(config.DpServer.TlsCertFile)
@@ -60,6 +58,5 @@ func BuildControlPlaneContext(config kuma_cp.Config, claCache xds.CLACache, dnsR
 		SdsTlsCert:        sdsCert,
 		AdminProxyKeyPair: &adminKeyPair,
 		CLACache:          claCache,
-		DNSResolver:       dnsResolver,
 	}, nil
 }
diff --git a/pkg/xds/envoy/listeners/v3/configurer.go b/pkg/xds/envoy/listeners/v3/configurer.go
index b6b7f9fc0..402f067ae 100644
--- a/pkg/xds/envoy/listeners/v3/configurer.go
+++ b/pkg/xds/envoy/listeners/v3/configurer.go
@@ -2,6 +2,7 @@ package v3
 
 import (
 	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
 )
 
 // ListenerConfigurer is responsible for configuring a single aspect of the entire Envoy listener,
@@ -18,6 +19,34 @@ type FilterChainConfigurer interface {
 	Configure(filterChain *envoy_listener.FilterChain) error
 }
 
+// HttpConnectionManagerConfigureFunc adapts a HttpConnectionManager
+// configuration function to the FilterChainConfigurer interface.
+type HttpConnectionManagerConfigureFunc func(hcm *envoy_hcm.HttpConnectionManager) error
+
+func (f HttpConnectionManagerConfigureFunc) Configure(filterChain *envoy_listener.FilterChain) error {
+	if f != nil {
+		return UpdateHTTPConnectionManager(filterChain, f)
+	}
+
+	return nil
+}
+
+// HttpConnectionManagerMustConfigureFunc adapts a HttpConnectionManager
+// configuration function that never fails to the FilterChainConfigurer
+// interface.
+type HttpConnectionManagerMustConfigureFunc func(hcm *envoy_hcm.HttpConnectionManager)
+
+func (f HttpConnectionManagerMustConfigureFunc) Configure(filterChain *envoy_listener.FilterChain) error {
+	if f != nil {
+		return UpdateHTTPConnectionManager(filterChain, func(hcm *envoy_hcm.HttpConnectionManager) error {
+			f(hcm)
+			return nil
+		})
+	}
+
+	return nil
+}
+
 // ListenerConfigureFunc adapts a configuration function to the
 // ListenerConfigurer interface.
 type ListenerConfigureFunc func(listener *envoy_listener.Listener) error
diff --git a/pkg/xds/envoy/names/resource_names.go b/pkg/xds/envoy/names/resource_names.go
index 4a0c72ab7..6c47a90f7 100644
--- a/pkg/xds/envoy/names/resource_names.go
+++ b/pkg/xds/envoy/names/resource_names.go
@@ -70,3 +70,7 @@ func GetTracingClusterName(backendName string) string {
 func GetDNSListenerName() string {
 	return "kuma:dns"
 }
+
+func GetGatewayListenerName(gatewayName string, protoName string, port uint32) string {
+	return strings.Join([]string{gatewayName, protoName, strconv.Itoa(int(port))}, ":")
+}
diff --git a/pkg/xds/server/components.go b/pkg/xds/server/components.go
index 9e05bb62f..89f003d88 100644
--- a/pkg/xds/server/components.go
+++ b/pkg/xds/server/components.go
@@ -62,7 +62,7 @@ func RegisterXDS(rt core_runtime.Runtime) error {
 	if err != nil {
 		return err
 	}
-	envoyCpCtx, err := xds_context.BuildControlPlaneContext(rt.Config(), claCache, rt.DNSResolver())
+	envoyCpCtx, err := xds_context.BuildControlPlaneContext(rt.Config(), claCache)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/xds/sync/components.go b/pkg/xds/sync/components.go
index e93f85993..226e0419d 100644
--- a/pkg/xds/sync/components.go
+++ b/pkg/xds/sync/components.go
@@ -29,7 +29,9 @@ func defaultDataplaneProxyBuilder(rt core_runtime.Runtime, metadataTracker Datap
 		FaultInjectionMatcher: faultinjections.FaultInjectionMatcher{ResourceManager: rt.ReadOnlyResourceManager()},
 		RateLimitMatcher:      ratelimits.RateLimitMatcher{ResourceManager: rt.ReadOnlyResourceManager()},
 		Zone:                  rt.Config().Multizone.Zone.Name,
-		apiVersion:            apiVersion,
+		APIVersion:            apiVersion,
+		ConfigManager:         rt.ConfigManager(),
+		TopLevelDomain:        rt.Config().DNSServer.Domain,
 	}
 }
 
diff --git a/pkg/xds/sync/dataplane_proxy_builder.go b/pkg/xds/sync/dataplane_proxy_builder.go
index e5ac8d03e..49541fe83 100644
--- a/pkg/xds/sync/dataplane_proxy_builder.go
+++ b/pkg/xds/sync/dataplane_proxy_builder.go
@@ -5,6 +5,7 @@ import (
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	"github.com/kumahq/kuma/pkg/core"
+	config_manager "github.com/kumahq/kuma/pkg/core/config/manager"
 	"github.com/kumahq/kuma/pkg/core/datasource"
 	"github.com/kumahq/kuma/pkg/core/dns/lookup"
 	"github.com/kumahq/kuma/pkg/core/faultinjections"
@@ -16,7 +17,7 @@ import (
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
 	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
 	"github.com/kumahq/kuma/pkg/core/xds"
-	"github.com/kumahq/kuma/pkg/dns/resolver"
+	"github.com/kumahq/kuma/pkg/dns/vips"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	"github.com/kumahq/kuma/pkg/xds/envoy"
 	xds_topology "github.com/kumahq/kuma/pkg/xds/topology"
@@ -35,11 +36,13 @@ type DataplaneProxyBuilder struct {
 	FaultInjectionMatcher faultinjections.FaultInjectionMatcher
 	RateLimitMatcher      ratelimits.RateLimitMatcher
 
-	Zone       string
-	apiVersion envoy.APIVersion
+	Zone           string
+	APIVersion     envoy.APIVersion
+	ConfigManager  config_manager.ConfigManager
+	TopLevelDomain string
 }
 
-func (p *DataplaneProxyBuilder) build(key core_model.ResourceKey, envoyContext *xds_context.Context) (*xds.Proxy, error) {
+func (p *DataplaneProxyBuilder) Build(key core_model.ResourceKey, envoyContext *xds_context.Context) (*xds.Proxy, error) {
 	ctx := context.Background()
 
 	dp, err := p.resolveDataplane(ctx, key)
@@ -47,7 +50,7 @@ func (p *DataplaneProxyBuilder) build(key core_model.ResourceKey, envoyContext *
 		return nil, err
 	}
 
-	routing, destinations, err := p.resolveRouting(ctx, &envoyContext.Mesh, envoyContext.ControlPlane.DNSResolver, dp)
+	routing, destinations, err := p.resolveRouting(ctx, &envoyContext.Mesh, dp)
 	if err != nil {
 		return nil, err
 	}
@@ -59,7 +62,7 @@ func (p *DataplaneProxyBuilder) build(key core_model.ResourceKey, envoyContext *
 
 	proxy := &xds.Proxy{
 		Id:         xds.FromResourceKey(key),
-		APIVersion: p.apiVersion,
+		APIVersion: p.APIVersion,
 		Dataplane:  dp,
 		Metadata:   p.MetadataTracker.Metadata(key),
 		Routing:    *routing,
@@ -88,7 +91,6 @@ func (p *DataplaneProxyBuilder) resolveDataplane(ctx context.Context, key core_m
 func (p *DataplaneProxyBuilder) resolveRouting(
 	ctx context.Context,
 	meshContext *xds_context.MeshContext,
-	dnsResolver resolver.DNSResolver,
 	dataplane *core_mesh.DataplaneResource,
 ) (*xds.Routing, xds.DestinationMap, error) {
 	externalServices := &core_mesh.ExternalServiceResourceList{}
@@ -109,16 +111,21 @@ func (p *DataplaneProxyBuilder) resolveRouting(
 	var domains []xds.VIPDomains
 	outbounds := dataplane.Spec.Networking.Outbound
 	if dataplane.Spec.Networking.GetTransparentProxying() != nil {
+		pers := vips.NewPersistence(p.CachingResManager, p.ConfigManager)
+		virtualOutboundView, err := pers.GetByMesh(dataplane.Meta.GetMesh())
+		if err != nil {
+			return nil, nil, err
+		}
 		// resolve all the domains
-		domains, outbounds = xds_topology.VIPOutbounds(core_model.MetaToResourceKey(dataplane.Meta), meshContext.Dataplanes.Items, zoneIngresses.Items, dnsResolver.GetVIPs(), dnsResolver.GetDomain(), matchedExternalServices)
+		domains, outbounds = xds_topology.VIPOutbounds(virtualOutboundView, p.TopLevelDomain)
 
-		// Update the outbound of the dataplane with the vips
-		vips := map[string]bool{}
+		// Update the outbound of the dataplane with the generatedVips
+		generatedVips := map[string]bool{}
 		for _, ob := range outbounds {
-			vips[ob.Address] = true
+			generatedVips[ob.Address] = true
 		}
 		for _, outbound := range dataplane.Spec.Networking.GetOutbound() {
-			if vips[outbound.Address] { // Useful while we still have resources with computed vip outbounds
+			if generatedVips[outbound.Address] { // Useful while we still have resources with computed vip outbounds
 				continue
 			}
 			outbounds = append(outbounds, outbound)
diff --git a/pkg/xds/sync/dataplane_watchdog.go b/pkg/xds/sync/dataplane_watchdog.go
index 8aa0126d6..2a7e98757 100644
--- a/pkg/xds/sync/dataplane_watchdog.go
+++ b/pkg/xds/sync/dataplane_watchdog.go
@@ -106,7 +106,7 @@ func (d *DataplaneWatchdog) syncDataplane() error {
 	if err != nil {
 		return err
 	}
-	proxy, err := d.dataplaneProxyBuilder.build(d.key, envoyCtx)
+	proxy, err := d.dataplaneProxyBuilder.Build(d.key, envoyCtx)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/xds/topology/dns.go b/pkg/xds/topology/dns.go
index 4bf75a839..ab44e507e 100644
--- a/pkg/xds/topology/dns.go
+++ b/pkg/xds/topology/dns.go
@@ -1,16 +1,11 @@
 package topology
 
 import (
-	"sort"
-	"strconv"
 	"strings"
 
 	"github.com/asaskevich/govalidator"
-	"github.com/pkg/errors"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
-	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
-	"github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/dns/vips"
 )
@@ -18,146 +13,67 @@ import (
 const VIPListenPort = uint32(80)
 
 func VIPOutbounds(
-	resourceKey model.ResourceKey,
-	dataplanes []*core_mesh.DataplaneResource,
-	zoneIngresses []*core_mesh.ZoneIngressResource,
-	vipList vips.List,
+	virtualOutboundView *vips.VirtualOutboundMeshView,
 	tldomain string,
-	externalServices []*core_mesh.ExternalServiceResource,
 ) ([]xds.VIPDomains, []*mesh_proto.Dataplane_Networking_Outbound) {
-	type vipEntry struct {
-		ip        string
-		port      uint32
-		entryType vips.EntryType
-	}
-	serviceVIPMap := map[string][]vipEntry{}
-	services := []string{}
-	for _, dataplane := range dataplanes {
-		// backwards compatibility
-		if dataplane.Spec.IsIngress() {
-			for _, service := range dataplane.Spec.GetNetworking().GetIngress().GetAvailableServices() {
-				if service.Mesh == resourceKey.Mesh {
-					// Only add outbounds for services in the same mesh
-					inService := service.Tags[mesh_proto.ServiceTag]
-					if _, found := serviceVIPMap[inService]; !found {
-						vip, err := ForwardLookup(vipList, vips.NewServiceEntry(inService))
-						if err == nil {
-							serviceVIPMap[inService] = append(serviceVIPMap[inService], vipEntry{vip, VIPListenPort, vips.Service})
-							services = append(services, inService)
-						}
-					}
-				}
-			}
-		} else {
-			for _, inbound := range dataplane.Spec.GetNetworking().GetInbound() {
-				inService := inbound.GetTags()[mesh_proto.ServiceTag]
-				if _, found := serviceVIPMap[inService]; !found {
-					vip, err := ForwardLookup(vipList, vips.NewServiceEntry(inService))
-					if err == nil {
-						serviceVIPMap[inService] = append(serviceVIPMap[inService], vipEntry{vip, VIPListenPort, vips.Service})
-						services = append(services, inService)
-					}
-				}
-			}
-		}
-	}
-
-	for _, zi := range zoneIngresses {
-		for _, service := range zi.Spec.GetAvailableServices() {
-			if service.Mesh == resourceKey.Mesh {
-				// Only add outbounds for services in the same mesh
-				inService := service.Tags[mesh_proto.ServiceTag]
-				if _, found := serviceVIPMap[inService]; !found {
-					vip, err := ForwardLookup(vipList, vips.NewServiceEntry(inService))
-					if err == nil {
-						serviceVIPMap[inService] = append(serviceVIPMap[inService], vipEntry{vip, VIPListenPort, vips.Service})
-						services = append(services, inService)
-					}
-				}
-			}
-		}
-	}
-
-	externalServicesByServiceName := map[string]*core_mesh.ExternalServiceResource{}
-	for _, externalService := range externalServices {
-		inService := externalService.Spec.Tags[mesh_proto.ServiceTag]
-		externalServicesByServiceName[inService] = externalService
-		host := externalService.Spec.GetHost()
-		if _, found := serviceVIPMap[inService]; !found {
-			vip1, err := ForwardLookup(vipList, vips.NewHostEntry(host))
-			if err == nil {
-				port := externalService.Spec.GetPort()
-				var p32 uint32
-				if p64, err := strconv.ParseUint(port, 10, 32); err != nil {
-					p32 = VIPListenPort
-				} else {
-					p32 = uint32(p64)
-				}
-				serviceVIPMap[inService] = append(serviceVIPMap[inService], vipEntry{vip1, p32, vips.Host})
-				services = append(services, inService)
-			}
-			vip2, err := ForwardLookup(vipList, vips.NewServiceEntry(inService))
-			if err == nil {
-				port := externalService.Spec.GetPort()
-				var p32 uint32
-				if p64, err := strconv.ParseUint(port, 10, 32); err != nil {
-					p32 = VIPListenPort
-				} else {
-					p32 = uint32(p64)
-				}
-				serviceVIPMap[inService] = append(serviceVIPMap[inService], vipEntry{vip2, p32, vips.Service})
-				services = append(services, inService)
-			}
-		}
-	}
-
-	sort.Strings(services)
 	var vipDomains []xds.VIPDomains
 	var outbounds []*mesh_proto.Dataplane_Networking_Outbound
-	for _, service := range services {
-		entries := serviceVIPMap[service]
-		for _, entry := range entries {
-			outbounds = append(outbounds, &mesh_proto.Dataplane_Networking_Outbound{
-				Address: entry.ip,
-				Tags:    map[string]string{mesh_proto.ServiceTag: service},
-				Port:    entry.port,
-			})
-			vip := xds.VIPDomains{
-				Address: entry.ip,
-			}
-			switch entry.entryType {
-			case vips.Service:
-				// add regular .mesh domain
-				vip.Domains = []string{service + "." + tldomain}
-				cleanedDomain := strings.ReplaceAll(service, "_", ".") + "." + tldomain
-				if cleanedDomain != vip.Domains[0] {
-					vip.Domains = append(vip.Domains, cleanedDomain)
+	for _, key := range virtualOutboundView.HostnameEntries() {
+		voutbound := virtualOutboundView.Get(key)
+		if voutbound.Address == "" {
+			continue
+		}
+		domain := xds.VIPDomains{Address: voutbound.Address}
+		switch key.Type {
+		case vips.Host, vips.FullyQualifiedDomain:
+			seenGlobalVip := false
+			for _, ob := range voutbound.Outbounds {
+				if !govalidator.IsDNSName(key.Name) {
+					continue
 				}
-				// todo (lobkovilya): backwards compatibility, could be deleted in the next major release Kuma 1.2.x
-				if entry.port != VIPListenPort {
+				domain.Domains = []string{key.Name}
+				seenGlobalVip = seenGlobalVip || ob.Port == VIPListenPort
+				if ob.Port != 0 {
 					outbounds = append(outbounds, &mesh_proto.Dataplane_Networking_Outbound{
-						Address: entry.ip,
-						Tags:    map[string]string{mesh_proto.ServiceTag: service},
-						Port:    VIPListenPort,
+						Address: voutbound.Address,
+						Port:    ob.Port,
+						Tags:    ob.TagSet,
 					})
 				}
-			case vips.Host:
-				host := externalServicesByServiceName[service].Spec.GetHost()
-				if govalidator.IsDNSName(host) {
-					vip.Domains = append(vip.Domains, host)
-				}
 			}
-			vipDomains = append(vipDomains, vip)
+			// TODO remove the `vips.Host` on the next major version it's there for backward compatibility
+			if key.Type == vips.Host && !seenGlobalVip && len(voutbound.Outbounds) > 0 {
+				outbounds = append(outbounds, &mesh_proto.Dataplane_Networking_Outbound{
+					Address: voutbound.Address,
+					Port:    VIPListenPort,
+					Tags:    voutbound.Outbounds[0].TagSet,
+				})
+			}
+		case vips.Service:
+			ob := voutbound.Outbounds[0]
+			service := ob.TagSet[mesh_proto.ServiceTag]
+			domain.Domains = []string{service + "." + tldomain}
+			cleanedDomain := strings.ReplaceAll(service, "_", ".") + "." + tldomain
+			if cleanedDomain != domain.Domains[0] {
+				domain.Domains = append(domain.Domains, cleanedDomain)
+			}
+			if ob.Port != 0 {
+				outbounds = append(outbounds, &mesh_proto.Dataplane_Networking_Outbound{
+					Address: voutbound.Address,
+					Port:    ob.Port,
+					Tags:    ob.TagSet,
+				})
+			}
+			// TODO this should be a else once we remove backward compatibility
+			if ob.Port != VIPListenPort {
+				outbounds = append(outbounds, &mesh_proto.Dataplane_Networking_Outbound{
+					Address: voutbound.Address,
+					Port:    VIPListenPort,
+					Tags:    ob.TagSet,
+				})
+			}
 		}
+		vipDomains = append(vipDomains, domain)
 	}
-
 	return vipDomains, outbounds
 }
-
-func ForwardLookup(vips vips.List, entry vips.Entry) (string, error) {
-	ip, found := vips[entry]
-	if !found {
-		return "", errors.Errorf("entry name [%s] not found", entry.Name)
-	}
-	return ip, nil
-}


Test patch:
diff --git a/api/mesh/v1alpha1/dataplane_helpers_test.go b/api/mesh/v1alpha1/dataplane_helpers_test.go
index de5b90044..9edc85052 100644
--- a/api/mesh/v1alpha1/dataplane_helpers_test.go
+++ b/api/mesh/v1alpha1/dataplane_helpers_test.go
@@ -10,7 +10,7 @@ import (
 
 var _ = Describe("MultiValueTagSet", func() {
 
-	Describe("Keys()", func() {
+	Describe("HostnameEntries()", func() {
 		type testCase struct {
 			value    MultiValueTagSet
 			expected []string
diff --git a/pkg/dns/ip_test.go b/pkg/dns/ip_test.go
deleted file mode 100644
index 89eb252e9..000000000
--- a/pkg/dns/ip_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package dns_test
-
-import (
-	"math"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-
-	. "github.com/kumahq/kuma/pkg/dns"
-)
-
-var _ = Describe("DNS ip", func() {
-
-	It("should allocate and free IP", func() {
-		// given
-		ipam, err := NewSimpleIPAM("192.168.0.1/32")
-		Expect(err).ToNot(HaveOccurred())
-		Expect(ipam).ToNot(BeNil())
-
-		// when
-		ip1, err := ipam.AllocateIP()
-		// then
-		Expect(err).ToNot(HaveOccurred())
-
-		// when
-		ip2, err := ipam.AllocateIP()
-		// then
-		Expect(err).To(HaveOccurred())
-
-		// when
-		err = ipam.FreeIP(ip1)
-		// then
-		Expect(err).ToNot(HaveOccurred())
-
-		// when
-		err = ipam.FreeIP(ip2)
-		// then
-		Expect(err).To(HaveOccurred())
-	})
-
-	It("should allocate 2^16 IP addresses", func() {
-		// given
-		ipam, err := NewSimpleIPAM("240.0.0.0/4")
-		Expect(err).ToNot(HaveOccurred())
-		Expect(ipam).ToNot(BeNil())
-
-		for i := 0; i < math.MaxInt16; i++ {
-			// when
-			_, err := ipam.AllocateIP()
-			// then
-			Expect(err).ToNot(HaveOccurred())
-		}
-	})
-})
diff --git a/pkg/dns/server_test.go b/pkg/dns/server_test.go
index 91aecbe4a..bd1dabf00 100644
--- a/pkg/dns/server_test.go
+++ b/pkg/dns/server_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/miekg/dns"
 	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
 	. "github.com/kumahq/kuma/pkg/dns"
@@ -20,7 +21,6 @@ import (
 var _ = Describe("DNS server", func() {
 
 	Describe("Network Operation", func() {
-		var ip string
 		var port uint32
 		stop := make(chan struct{})
 		done := make(chan struct{})
@@ -55,36 +55,101 @@ var _ = Describe("DNS server", func() {
 			<-done
 		})
 
-		It("should resolve", func() {
-			// given
-			var err error
-			dnsResolver.SetVIPs(vips.List{
-				vips.NewServiceEntry("service"): "240.0.0.1",
-			})
-			ip, err = dnsResolver.ForwardLookupFQDN("service.mesh")
-			Expect(err).ToNot(HaveOccurred())
-
-			// when
-			client := new(dns.Client)
-			message := new(dns.Msg)
-			_ = message.SetQuestion("service.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("service.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))
-		})
+		type dnsTestCase struct {
+			givenVips map[vips.HostnameEntry]string
+			whenQuery string
+			whenType  dns.Type
+			// If ip is empty this will be consider as a dns miss
+			thenIp string
+		}
+		DescribeTable("simple point resolve",
+			func(tc dnsTestCase) {
+				// given
+				var err error
+				dnsResolver.SetVIPs(tc.givenVips)
+
+				// when
+				client := new(dns.Client)
+				message := new(dns.Msg)
+				q := tc.whenQuery + "."
+				_ = message.SetQuestion(q, uint16(tc.whenType))
+				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(test_metrics.FindMetric(metrics, "dns_server")).ToNot(BeNil())
+				if tc.thenIp != "" {
+					Expect(response.Answer).To(HaveLen(1))
+					Expect(response.Answer[0].String()).To(Equal(fmt.Sprintf("%s\t60\tIN\tA\t%s", q, tc.thenIp)))
+					// and metrics are published
+					Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "resolved").Counter.GetValue()).To(Equal(1.0))
+				} else {
+					Expect(response.Answer).To(HaveLen(0))
+					// and metrics are published
+					Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "unresolved").Counter.GetValue()).To(Equal(1.0))
+				}
+			},
+			Entry("should resolve", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{vips.NewServiceEntry("service"): "240.0.0.1"},
+				whenQuery: "service.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.1",
+			}),
+			Entry("should not resolve with no vips", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{},
+				whenQuery: "service.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "",
+			}),
+			Entry("should not resolve", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{vips.NewServiceEntry("service"): "240.0.0.1"},
+				whenQuery: "not-service.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "",
+			}),
+			Entry("should resolve services with '.'", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{vips.NewServiceEntry("my.service"): "240.0.0.1"},
+				whenQuery: "my.service.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.1",
+			}),
+			Entry("should resolve converted services with '.'", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{vips.NewServiceEntry("my-service_test-namespace_svc_80"): "240.0.0.1"},
+				whenQuery: "my-service.test-namespace.svc.80.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.1",
+			}),
+			Entry("should resolve fqdn service with .mesh", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{vips.NewFqdnEntry("my.service.foo.mesh"): "240.0.0.1"},
+				whenQuery: "my.service.foo.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.1",
+			}),
+			Entry("should resolve, service entry has priority over fqdn entry", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{
+					vips.NewFqdnEntry("my.service.foo.mesh"): "240.0.0.2",
+					vips.NewServiceEntry("my.service.foo"):   "240.0.0.1",
+				},
+				whenQuery: "my.service.foo.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.1",
+			}),
+			Entry("should resolve, simple fqdn entry", dnsTestCase{
+				givenVips: map[vips.HostnameEntry]string{
+					vips.NewFqdnEntry("service.com.mesh"): "240.0.0.2",
+				},
+				whenQuery: "service.com.mesh",
+				whenType:  dns.Type(dns.TypeA),
+				thenIp:    "240.0.0.2",
+			}),
+		)
 
 		It("should resolve concurrent", func() {
 			// given
-			dnsResolver.SetVIPs(vips.List{
+			dnsResolver.SetVIPs(map[vips.HostnameEntry]string{
 				vips.NewServiceEntry("service"): "240.0.0.1",
 			})
 			ip, err := dnsResolver.ForwardLookupFQDN("service.mesh")
@@ -116,7 +181,7 @@ var _ = Describe("DNS server", func() {
 
 		It("should resolve IPv6 concurrent", func() {
 			// given
-			dnsResolver.SetVIPs(vips.List{
+			dnsResolver.SetVIPs(map[vips.HostnameEntry]string{
 				vips.NewServiceEntry("service"): "fd00::1",
 			})
 			ip, err := dnsResolver.ForwardLookupFQDN("service.mesh")
@@ -145,110 +210,6 @@ var _ = Describe("DNS server", func() {
 				<-resolved
 			}
 		})
-
-		It("should not resolve", func() {
-			// given
-			var err error
-			dnsResolver.SetVIPs(vips.List{
-				vips.NewServiceEntry("service"): "240.0.0.1",
-			})
-			ip, err = dnsResolver.ForwardLookupFQDN("service.mesh")
-			Expect(err).ToNot(HaveOccurred())
-
-			// when
-			client := new(dns.Client)
-			message := new(dns.Msg)
-			_ = message.SetQuestion("backend.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(err).ToNot(HaveOccurred())
-			// and
-			Expect(len(response.Answer)).To(Equal(0))
-
-			// and metrics are published
-			Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "unresolved").Counter.GetValue()).To(Equal(1.0))
-		})
-
-		It("should not resolve when no vips", func() {
-			// given
-			dnsResolver.SetVIPs(vips.List{})
-
-			// when
-			client := new(dns.Client)
-			message := new(dns.Msg)
-			_ = message.SetQuestion("service.mesh.", dns.TypeA)
-			var response *dns.Msg
-			var err error
-			Eventually(func() error {
-				response, _, err = client.Exchange(message, fmt.Sprintf("127.0.0.1:%d", port))
-				return err
-			}).ShouldNot(HaveOccurred())
-			// then
-			Expect(err).ToNot(HaveOccurred())
-			// and
-			Expect(len(response.Answer)).To(Equal(0))
-
-			// and metrics are published
-			Expect(test_metrics.FindMetric(metrics, "dns_server_resolution", "result", "unresolved").Counter.GetValue()).To(Equal(1.0))
-		})
-
-		It("should resolve services with '.'", func() {
-			// given
-			var err error
-			dnsResolver.SetVIPs(vips.List{
-				vips.NewServiceEntry("my.service"): "240.0.0.1",
-			})
-			ip, err = dnsResolver.ForwardLookupFQDN("my.service.mesh")
-			Expect(err).ToNot(HaveOccurred())
-
-			// when
-			client := new(dns.Client)
-			message := new(dns.Msg)
-			_ = message.SetQuestion("my.service.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.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))
-		})
-
-		It("should resolve converted services with '.'", func() {
-			// given
-			var err error
-			dnsResolver.SetVIPs(vips.List{
-				vips.NewServiceEntry("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))
-		})
 	})
 
 	Describe("host operation", func() {
diff --git a/pkg/dns/vips/global_view_test.go b/pkg/dns/vips/global_view_test.go
new file mode 100644
index 000000000..ff43af300
--- /dev/null
+++ b/pkg/dns/vips/global_view_test.go
@@ -0,0 +1,108 @@
+package vips_test
+
+import (
+	"fmt"
+	"math"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/dns/vips"
+)
+
+var _ = Describe("global view", func() {
+
+	It("should fail when no more ips", func() {
+		// given
+		gv, err := vips.NewGlobalView("192.168.0.1/32")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gv).ToNot(BeNil())
+
+		// when
+		ip1, err := gv.Allocate(vips.NewServiceEntry("foo.bar"))
+		// then
+		Expect(err).ToNot(HaveOccurred())
+		Expect(ip1).ToNot(Equal(""))
+
+		// when
+		ip2, err := gv.Allocate(vips.NewServiceEntry("bar.bar"))
+		// then
+		Expect(err).To(HaveOccurred())
+		Expect(ip2).To(Equal(""))
+	})
+
+	It("should allocate IPs", func() {
+		// given
+		gv, err := vips.NewGlobalView("192.168.0.1/24")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gv).ToNot(BeNil())
+
+		// when
+		ip1, err := gv.Allocate(vips.NewServiceEntry("foo.bar"))
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		ip2, err := gv.Allocate(vips.NewServiceEntry("bar.bar"))
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// then
+		vips := map[vips.HostnameEntry]string{
+			vips.NewServiceEntry("foo.bar"): ip1,
+			vips.NewServiceEntry("bar.bar"): ip2,
+		}
+
+		Expect(gv.ToVIPMap()).To(Equal(vips))
+	})
+
+	It("should allocate 2^16 IP addresses", func() {
+		// given
+		gv, err := vips.NewGlobalView("240.0.0.0/4")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gv).ToNot(BeNil())
+
+		for i := 0; i < math.MaxInt16; i++ {
+			// when
+			_, err := gv.Allocate(vips.NewHostEntry(fmt.Sprintf("foo-%d.mesh", i)))
+			// then
+			Expect(err).ToNot(HaveOccurred())
+		}
+	})
+
+	It("should give the same ip if we ask for a host already allocated", func() {
+		// given
+		host := vips.NewServiceEntry("foo.com")
+		gv, err := vips.NewGlobalView("240.0.0.0/4")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gv).ToNot(BeNil())
+
+		err = gv.Reserve(host, "240.0.0.1")
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		ip, err := gv.Allocate(host)
+		Expect(err).ToNot(HaveOccurred())
+
+		// then
+		Expect(ip).To(Equal("240.0.0.1"))
+	})
+
+	It("should give the same ip if when allocating it twice", func() {
+		// given
+		host := vips.NewServiceEntry("foo.com")
+		gv, err := vips.NewGlobalView("240.0.0.0/4")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(gv).ToNot(BeNil())
+
+		ip, err := gv.Allocate(host)
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		ip2, err := gv.Allocate(host)
+		Expect(err).ToNot(HaveOccurred())
+
+		// then
+		Expect(ip2).To(Equal(ip))
+	})
+})
diff --git a/pkg/dns/vips/persistence_test.go b/pkg/dns/vips/persistence_test.go
index 9ecae696b..197d9003d 100644
--- a/pkg/dns/vips/persistence_test.go
+++ b/pkg/dns/vips/persistence_test.go
@@ -109,36 +109,15 @@ var _ = Describe("Meshed Persistence", func() {
 			})
 		})
 
-		It("should merge vips from several configs", func() {
-			actual, _, err := meshedPersistence.Get()
-			Expect(err).ToNot(HaveOccurred())
-
-			expected := vips.List{
-				vips.NewServiceEntry("backend"):    "240.0.0.1",
-				vips.NewServiceEntry("backend_2"):  "240.0.1.1",
-				vips.NewServiceEntry("backend_3"):  "240.0.2.1",
-				vips.NewServiceEntry("frontend"):   "240.0.0.3",
-				vips.NewServiceEntry("frontend_2"): "240.0.1.3",
-				vips.NewServiceEntry("frontend_3"): "240.0.2.3",
-				vips.NewServiceEntry("postgres"):   "240.0.0.0",
-				vips.NewServiceEntry("postgres_2"): "240.0.1.0",
-				vips.NewServiceEntry("postgres_3"): "240.0.2.0",
-				vips.NewServiceEntry("redis"):      "240.0.0.2",
-				vips.NewServiceEntry("redis_2"):    "240.0.1.2",
-				vips.NewServiceEntry("redis_3"):    "240.0.2.2",
-			}
-			Expect(actual).To(Equal(expected))
-		})
-
 		It("should return vips for mesh", func() {
 			actual, err := meshedPersistence.GetByMesh("mesh-2")
 			Expect(err).ToNot(HaveOccurred())
-			expected := vips.List{
-				vips.NewServiceEntry("backend_2"):  "240.0.1.1",
-				vips.NewServiceEntry("frontend_2"): "240.0.1.3",
-				vips.NewServiceEntry("postgres_2"): "240.0.1.0",
-				vips.NewServiceEntry("redis_2"):    "240.0.1.2",
-			}
+			expected := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend_2"):  {Address: "240.0.1.1"},
+				vips.NewServiceEntry("frontend_2"): {Address: "240.0.1.3"},
+				vips.NewServiceEntry("postgres_2"): {Address: "240.0.1.0"},
+				vips.NewServiceEntry("redis_2"):    {Address: "240.0.1.2"},
+			})
 			Expect(actual).To(Equal(expected))
 		})
 	})
@@ -154,18 +133,19 @@ var _ = Describe("Meshed Persistence", func() {
 		})
 
 		It("should create a new config", func() {
-			vipsMesh1 := vips.List{
-				vips.NewServiceEntry("backend"): "240.0.0.1",
-			}
+			vipsMesh1 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend"): {Address: "240.0.0.1"},
+			})
+
 			err := meshedPersistence.Set("mesh-1", vipsMesh1)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(countingCm.get).To(Equal(1))
 			Expect(countingCm.create).To(Equal(1))
 			Expect(countingCm.update).To(Equal(0))
 
-			vipsMesh2 := vips.List{
-				vips.NewServiceEntry("frontend"): "240.0.0.2",
-			}
+			vipsMesh2 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("frontend"): {Address: "240.0.0.2"},
+			})
 			err = meshedPersistence.Set("mesh-2", vipsMesh2)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(countingCm.get).To(Equal(2))
@@ -174,16 +154,16 @@ var _ = Describe("Meshed Persistence", func() {
 		})
 
 		It("should update existing config", func() {
-			vipsMesh1 := vips.List{
-				vips.NewServiceEntry("backend"): "240.0.0.1",
-			}
+			vipsMesh1 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend"): vips.VirtualOutbound{Address: "240.0.0.1"},
+			})
 			err := meshedPersistence.Set("mesh-1", vipsMesh1)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(countingCm.get).To(Equal(1))
 			Expect(countingCm.create).To(Equal(1))
 			Expect(countingCm.update).To(Equal(0))
 
-			vipsMesh1[vips.NewServiceEntry("frontend")] = "240.0.0.2"
+			Expect(vipsMesh1.Add(vips.NewServiceEntry("frontend"), vips.OutboundEntry{})).ToNot(HaveOccurred())
 			err = meshedPersistence.Set("mesh-1", vipsMesh1)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(countingCm.get).To(Equal(2))
@@ -206,57 +186,60 @@ var _ = Describe("Meshed Persistence", func() {
 						Meta: &model.ResourceMeta{Name: "kuma-mesh-2-dns-vips"},
 						Spec: &config_proto.Config{Config: `{"0:backend_2":"240.0.1.1","0:frontend_2":"240.0.1.3","1:host.com":"240.0.1.4"}`},
 					},
+					"kuma-mesh-3-dns-vips": {
+						Meta: &model.ResourceMeta{Name: "kuma-mesh-3-dns-vips"},
+						Spec: &config_proto.Config{Config: `{"0:backend_3":{"address":"240.0.2.1"},"0:frontend_3":{"address":"240.0.2.3"},"1:host.com":{"address":"240.0.1.4"}}`},
+					},
 				},
 			}
 			meshedPersistence = vips.NewPersistence(rm, countingCm)
 		})
 
-		It("should return global and meshed vips", func() {
+		It("should return meshed vips", func() {
 			// when
-			global, meshed, err := meshedPersistence.Get()
+			meshed, err := meshedPersistence.Get()
 			Expect(err).ToNot(HaveOccurred())
 
-			// then
-			expectedGlobal := vips.List{
-				vips.NewServiceEntry("backend"):    "240.0.0.1",
-				vips.NewServiceEntry("frontend"):   "240.0.0.3",
-				vips.NewServiceEntry("postgres"):   "240.0.0.0",
-				vips.NewServiceEntry("redis"):      "240.0.0.2",
-				vips.NewServiceEntry("backend_2"):  "240.0.1.1",
-				vips.NewServiceEntry("frontend_2"): "240.0.1.3",
-				vips.NewHostEntry("host.com"):      "240.0.1.4",
-			}
-			Expect(global).To(Equal(expectedGlobal))
 			// and then
-			expectedMesh1 := vips.List{
-				vips.NewServiceEntry("backend"):  "240.0.0.1",
-				vips.NewServiceEntry("frontend"): "240.0.0.3",
-				vips.NewServiceEntry("postgres"): "240.0.0.0",
-				vips.NewServiceEntry("redis"):    "240.0.0.2",
-			}
-			Expect(meshed["mesh-1"]).To(Equal(expectedMesh1))
+			expectedMesh1 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend"):  {Address: "240.0.0.1"},
+				vips.NewServiceEntry("frontend"): {Address: "240.0.0.3"},
+				vips.NewServiceEntry("postgres"): {Address: "240.0.0.0"},
+				vips.NewServiceEntry("redis"):    {Address: "240.0.0.2"},
+			})
+			Expect(meshed["mesh-1"].HostnameEntries()).To(Equal(expectedMesh1.HostnameEntries()))
+			// and then
+			expectedMesh2 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend_2"):  {Address: "240.0.1.1"},
+				vips.NewServiceEntry("frontend_2"): {Address: "240.0.1.3"},
+				vips.NewHostEntry("host.com"):      {Address: "240.0.1.4"},
+			})
+			Expect(meshed["mesh-2"].HostnameEntries()).To(Equal(expectedMesh2.HostnameEntries()))
 			// and then
-			expectedMesh2 := vips.List{
-				vips.NewServiceEntry("backend_2"):  "240.0.1.1",
-				vips.NewServiceEntry("frontend_2"): "240.0.1.3",
-				vips.NewHostEntry("host.com"):      "240.0.1.4",
+			expectedMesh3 := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend_3"):  {Address: "240.0.2.1"},
+				vips.NewServiceEntry("frontend_3"): {Address: "240.0.2.3"},
+				vips.NewHostEntry("host.com"):      {Address: "240.0.1.4"},
+			})
+			Expect(meshed["mesh-3"]).To(WithTransform(func(vo *vips.VirtualOutboundMeshView) []vips.HostnameEntry {
+				return vo.HostnameEntries()
+			}, Equal(expectedMesh3.HostnameEntries())))
+			for _, k := range meshed["mesh-3"].HostnameEntries() {
+				Expect(meshed["mesh-3"].Get(k)).To(Equal(expectedMesh3.Get(k)))
 			}
-			Expect(meshed["mesh-2"]).To(Equal(expectedMesh2))
 		})
 
 		It("should update old version with new one", func() {
-			newVIPs := vips.List{
-				vips.NewServiceEntry("backend"):  "240.0.0.1",
-				vips.NewServiceEntry("frontend"): "240.0.0.3",
-				vips.NewServiceEntry("postgres"): "240.0.0.0",
-				vips.NewServiceEntry("redis"):    "240.0.0.2",
-				vips.NewHostEntry("kuma.io"):     "240.0.1.4",
-			}
+			newVIPs := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("backend"):  {Address: "240.0.0.1"},
+				vips.NewServiceEntry("frontend"): {Address: "240.0.0.3"},
+				vips.NewHostEntry("kuma.io"):     {Address: "240.0.1.4"},
+			})
 			err := meshedPersistence.Set("mesh-1", newVIPs)
 			Expect(err).ToNot(HaveOccurred())
 
 			Expect(countingCm.updates).To(HaveLen(1))
-			Expect(countingCm.updates[0].Spec.Config).To(Equal(`{"0:backend":"240.0.0.1","0:frontend":"240.0.0.3","0:postgres":"240.0.0.0","0:redis":"240.0.0.2","1:kuma.io":"240.0.1.4"}`))
+			Expect(countingCm.updates[0].Spec.Config).To(Equal(`{"0:backend":{"address":"240.0.0.1"},"0:frontend":{"address":"240.0.0.3"},"1:kuma.io":{"address":"240.0.1.4"}}`))
 		})
 	})
 })
diff --git a/pkg/dns/vips/virtual_outbound_view_test.go b/pkg/dns/vips/virtual_outbound_view_test.go
new file mode 100644
index 000000000..6a521267c
--- /dev/null
+++ b/pkg/dns/vips/virtual_outbound_view_test.go
@@ -0,0 +1,66 @@
+package vips_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/dns/vips"
+)
+
+var _ = Describe("Virtual outbound", func() {
+	type updateTestCase struct {
+		given                map[vips.HostnameEntry]vips.VirtualOutbound
+		when                 map[vips.HostnameEntry]vips.VirtualOutbound
+		thenChanges          []vips.Change
+		thenVirtualOutbounds map[vips.HostnameEntry]vips.VirtualOutbound
+	}
+	DescribeTable("Update",
+		func(tc updateTestCase) {
+			changes, out := vips.NewVirtualOutboundView(tc.given).Update(vips.NewVirtualOutboundView(tc.when))
+
+			Expect(tc.thenChanges).To(Equal(changes))
+			expected := vips.NewVirtualOutboundView(tc.thenVirtualOutbounds)
+			Expect(expected.HostnameEntries()).To(Equal(out.HostnameEntries()))
+			for _, k := range expected.HostnameEntries() {
+				Expect(expected.Get(k)).To(Equal(out.Get(k)))
+			}
+		},
+		Entry("same noop", updateTestCase{
+			given:                map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+			when:                 map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+			thenChanges:          []vips.Change{},
+			thenVirtualOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+		}),
+		Entry("from empty", updateTestCase{
+			given:                map[vips.HostnameEntry]vips.VirtualOutbound{},
+			when:                 map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+			thenChanges:          []vips.Change{{Type: vips.Add, Entry: vips.NewHostEntry("foo")}},
+			thenVirtualOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+		}),
+		Entry("to empty", updateTestCase{
+			given:                map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+			when:                 map[vips.HostnameEntry]vips.VirtualOutbound{},
+			thenChanges:          []vips.Change{{Type: vips.Remove, Entry: vips.NewHostEntry("foo")}},
+			thenVirtualOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{},
+		}),
+		Entry("add one/remove one", updateTestCase{
+			given:                map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1"}},
+			when:                 map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("bar"): {Address: "240.0.0.1"}},
+			thenChanges:          []vips.Change{{Type: vips.Add, Entry: vips.NewHostEntry("bar")}, {Type: vips.Remove, Entry: vips.NewHostEntry("foo")}},
+			thenVirtualOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("bar"): {Address: "240.0.0.1"}},
+		}),
+		Entry("add extra outbound", updateTestCase{
+			given: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1", Outbounds: []vips.OutboundEntry{{Port: 80, TagSet: map[string]string{"foo": "bar"}, Origin: "my-policy"}}}},
+			when: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1", Outbounds: []vips.OutboundEntry{
+				{Port: 80, TagSet: map[string]string{"foo": "bar"}, Origin: "my-policy"},
+				{Port: 81, TagSet: map[string]string{"foo": "baz"}, Origin: "my-policy2"},
+			}}},
+			thenChanges: []vips.Change{{Type: vips.Modify, Entry: vips.NewHostEntry("foo")}},
+			thenVirtualOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{vips.NewHostEntry("foo"): {Address: "240.0.0.1", Outbounds: []vips.OutboundEntry{
+				{Port: 80, TagSet: map[string]string{"foo": "bar"}, Origin: "my-policy"},
+				{Port: 81, TagSet: map[string]string{"foo": "baz"}, Origin: "my-policy2"},
+			}}},
+		}),
+	)
+})
diff --git a/pkg/dns/vips_allocator_test.go b/pkg/dns/vips_allocator_test.go
index ca1af76f3..96481789a 100644
--- a/pkg/dns/vips_allocator_test.go
+++ b/pkg/dns/vips_allocator_test.go
@@ -88,7 +88,7 @@ var _ = Describe("VIP Allocator", func() {
 		// then
 		vipList, err := persistence.GetByMesh("mesh-1")
 		Expect(err).ToNot(HaveOccurred())
-		Expect(vipList).To(HaveLen(2))
+		Expect(vipList.HostnameEntries()).To(HaveLen(2))
 
 		vipList, err = persistence.GetByMesh("mesh-2")
 		Expect(err).ToNot(HaveOccurred())
@@ -99,7 +99,7 @@ var _ = Describe("VIP Allocator", func() {
 			Expect(ip).To(HavePrefix("240.0.0"))
 		}
 
-		Expect(vipList).To(HaveLen(1))
+		Expect(vipList.HostnameEntries()).To(HaveLen(1))
 	})
 
 	It("should respect already allocated VIPs in case of IPAM restarts", func() {
@@ -107,10 +107,10 @@ var _ = Describe("VIP Allocator", func() {
 		persistence := vips.NewPersistence(rm, cm)
 		// we add VIPs directly to the 'persistence' object
 		// that emulates situation when IPAM is fresh and doesn't aware of allocated VIPs
-		err := persistence.Set("mesh-1", vips.List{
-			vips.NewServiceEntry("frontend"): "240.0.0.0",
-			vips.NewServiceEntry("backend"):  "240.0.0.1",
-		})
+		err := persistence.Set("mesh-1", vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+			vips.NewServiceEntry("frontend"): {Address: "240.0.0.0"},
+			vips.NewServiceEntry("backend"):  {Address: "240.0.0.1"},
+		}))
 		Expect(err).ToNot(HaveOccurred())
 
 		err = rm.Create(context.Background(), &mesh.DataplaneResource{Spec: dp("database")}, store.CreateByKey("dp-3", "mesh-1"))
@@ -123,11 +123,15 @@ var _ = Describe("VIP Allocator", func() {
 		vipList, err := persistence.GetByMesh("mesh-1")
 		Expect(err).ToNot(HaveOccurred())
 		// then
-		Expect(vipList).To(Equal(vips.List{
-			vips.NewServiceEntry("frontend"): "240.0.0.0",
-			vips.NewServiceEntry("backend"):  "240.0.0.1",
-			vips.NewServiceEntry("database"): "240.0.0.2",
-		}))
+		expected := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{
+			vips.NewServiceEntry("backend"):  {Address: "240.0.0.1"},
+			vips.NewServiceEntry("database"): {Address: "240.0.0.2"},
+			vips.NewServiceEntry("frontend"): {Address: "240.0.0.0"},
+		})
+		Expect(vipList.HostnameEntries()).To(Equal(expected.HostnameEntries()))
+		for _, k := range vipList.HostnameEntries() {
+			Expect(vipList.Get(k).Address).To(Equal(expected.Get(k).Address))
+		}
 	})
 
 	It("should return error if failed to update VIP config", func() {
@@ -166,7 +170,7 @@ var _ = Describe("VIP Allocator", func() {
 	})
 })
 
-var _ = Describe("BuildServiceSet", func() {
+var _ = Describe("BuildVirtualOutboundMeshView", func() {
 	var rm manager.ResourceManager
 
 	BeforeEach(func() {
@@ -252,115 +256,62 @@ var _ = Describe("BuildServiceSet", func() {
 		Expect(err).ToNot(HaveOccurred())
 
 		// when
-		serviceSet, err := dns.BuildServiceSet(rm, "mesh-1")
+		serviceSet, err := dns.BuildVirtualOutboundMeshView(rm, "mesh-1")
 		Expect(err).ToNot(HaveOccurred())
 
 		// then
-		Expect(serviceSet).To(Equal(vips.EntrySet{
-			vips.NewServiceEntry("backend"):           true,
-			vips.NewServiceEntry("frontend"):          true,
-			vips.NewServiceEntry("database"):          true,
-			vips.NewServiceEntry("metrics"):           true,
-			vips.NewServiceEntry("ingress-svc"):       true,
-			vips.NewServiceEntry("es-backend"):        true,
-			vips.NewHostEntry("external.service.com"): true,
-		}))
+		expected := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{})
+		Expect(expected.Add(vips.NewServiceEntry("backend"), vips.OutboundEntry{Port: 0, TagSet: map[string]string{mesh_proto.ServiceTag: "backend"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewServiceEntry("database"), vips.OutboundEntry{Port: 0, TagSet: map[string]string{mesh_proto.ServiceTag: "database"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewServiceEntry("metrics"), vips.OutboundEntry{Port: 0, TagSet: map[string]string{mesh_proto.ServiceTag: "metrics"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewServiceEntry("frontend"), vips.OutboundEntry{Port: 0, TagSet: map[string]string{mesh_proto.ServiceTag: "frontend"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewServiceEntry("ingress-svc"), vips.OutboundEntry{Port: 0, TagSet: map[string]string{mesh_proto.ServiceTag: "ingress-svc"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewServiceEntry("es-backend"), vips.OutboundEntry{Port: 8080, TagSet: map[string]string{mesh_proto.ServiceTag: "es-backend"}, Origin: "service"})).ToNot(HaveOccurred())
+		Expect(expected.Add(vips.NewHostEntry("external.service.com"), vips.OutboundEntry{Port: 8080, TagSet: map[string]string{mesh_proto.ServiceTag: "es-backend"}, Origin: "host"})).ToNot(HaveOccurred())
+
+		Expect(serviceSet.HostnameEntries()).To(Equal(expected.HostnameEntries()))
+		for _, k := range serviceSet.HostnameEntries() {
+			Expect(serviceSet.Get(k)).To(Equal(expected.Get(k)))
+		}
 	})
 })
 
-var _ = Describe("UpdateMeshedVIPs", func() {
+var _ = Describe("AllocateVIPs", func() {
 	It("should allocate new VIPs", func() {
 		// setup
-		vipsList := vips.List{}
-		ipam, err := dns.NewSimpleIPAM("240.0.0.0/4")
+		gv, err := vips.NewGlobalView("240.0.0.0/4")
 		Expect(err).ToNot(HaveOccurred())
-		serviceSet := vips.EntrySet{
-			vips.NewServiceEntry("backend"):  true,
-			vips.NewServiceEntry("frontend"): true,
-		}
+		serviceSet := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{})
+		Expect(serviceSet.Add(vips.NewServiceEntry("backend"), vips.OutboundEntry{TagSet: map[string]string{mesh_proto.ServiceTag: "backend"}})).ToNot(HaveOccurred())
+		Expect(serviceSet.Add(vips.NewServiceEntry("frontend"), vips.OutboundEntry{TagSet: map[string]string{mesh_proto.ServiceTag: "frontend"}})).ToNot(HaveOccurred())
 		// when
-		updated, err := dns.UpdateMeshedVIPs(vipsList, vipsList, ipam, serviceSet)
+		err = dns.AllocateVIPs(gv, serviceSet)
 		Expect(err).ToNot(HaveOccurred())
 		// then
-		Expect(err).ToNot(HaveOccurred())
-		Expect(updated).To(BeTrue())
-		Expect(vipsList).To(Equal(vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-		}))
-	})
-
-	It("should free IP for deleted service", func() {
-		// setup
-		vipsList := vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-		}
-		ipam, err := dns.NewSimpleIPAM("240.0.0.0/4")
-		Expect(err).ToNot(HaveOccurred())
-		serviceSet := vips.EntrySet{
-			vips.NewServiceEntry("backend"): true,
-		}
-		// when
-		updated, err := dns.UpdateMeshedVIPs(vipsList, vipsList, ipam, serviceSet)
-		Expect(err).ToNot(HaveOccurred())
-		// then
-		Expect(updated).To(BeTrue())
-		Expect(vipsList).To(Equal(vips.List{
-			vips.NewServiceEntry("backend"): "240.0.0.0",
-		}))
-	})
-
-	It("should return updated=false if nothing changed", func() {
-		// setup
-		vipsList := vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-		}
-		ipam, err := dns.NewSimpleIPAM("240.0.0.0/4")
-		Expect(err).ToNot(HaveOccurred())
-		serviceSet := vips.EntrySet{
-			vips.NewServiceEntry("backend"):  true,
-			vips.NewServiceEntry("frontend"): true,
-		}
-		// when
-		updated, err := dns.UpdateMeshedVIPs(vipsList, vipsList, ipam, serviceSet)
-		Expect(err).ToNot(HaveOccurred())
-		// then
-		Expect(updated).To(BeFalse())
-		Expect(vipsList).To(Equal(vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-		}))
+		Expect(serviceSet.HostnameEntries()).To(Equal([]vips.HostnameEntry{vips.NewServiceEntry("backend"), vips.NewServiceEntry("frontend")}))
+		Expect(serviceSet.Get(vips.NewServiceEntry("backend")).Address).ToNot(BeEmpty())
+		Expect(serviceSet.Get(vips.NewServiceEntry("frontend")).Address).ToNot(BeEmpty())
 	})
 
 	It("should generate the same VIP for services across meshes", func() {
 		// setup
-		global := vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-			vips.NewServiceEntry("database"): "240.0.0.10",
-		}
-		meshed := vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-		}
-		ipam, err := dns.NewSimpleIPAM("240.0.0.0/4")
+		gv, err := vips.NewGlobalView("240.0.0.0/4")
 		Expect(err).ToNot(HaveOccurred())
-		serviceSet := vips.EntrySet{
-			vips.NewServiceEntry("backend"):  true,
-			vips.NewServiceEntry("frontend"): true,
-			vips.NewServiceEntry("database"): true,
-		}
+		Expect(gv.Reserve(vips.NewServiceEntry("backend"), "240.0.0.0")).ToNot(HaveOccurred())
+		Expect(gv.Reserve(vips.NewServiceEntry("frontend"), "240.0.0.1")).ToNot(HaveOccurred())
+		Expect(gv.Reserve(vips.NewServiceEntry("database"), "240.0.0.10")).ToNot(HaveOccurred())
+		Expect(err).ToNot(HaveOccurred())
+		serviceSet := vips.NewVirtualOutboundView(map[vips.HostnameEntry]vips.VirtualOutbound{})
+		Expect(serviceSet.Add(vips.NewServiceEntry("backend"), vips.OutboundEntry{Origin: "default", TagSet: map[string]string{mesh_proto.ServiceTag: "backend"}})).ToNot(HaveOccurred())
+		Expect(serviceSet.Add(vips.NewServiceEntry("frontend"), vips.OutboundEntry{Origin: "default", TagSet: map[string]string{mesh_proto.ServiceTag: "frontend"}})).ToNot(HaveOccurred())
+		Expect(serviceSet.Add(vips.NewServiceEntry("database"), vips.OutboundEntry{Origin: "default", TagSet: map[string]string{mesh_proto.ServiceTag: "database"}})).ToNot(HaveOccurred())
 		// when
-		updated, err := dns.UpdateMeshedVIPs(global, meshed, ipam, serviceSet)
+		err = dns.AllocateVIPs(gv, serviceSet)
 		Expect(err).ToNot(HaveOccurred())
 		// then
-		Expect(updated).To(BeTrue())
-		Expect(meshed).To(Equal(vips.List{
-			vips.NewServiceEntry("backend"):  "240.0.0.0",
-			vips.NewServiceEntry("frontend"): "240.0.0.1",
-			vips.NewServiceEntry("database"): "240.0.0.10",
-		}))
+		Expect(serviceSet.HostnameEntries()).To(Equal([]vips.HostnameEntry{vips.NewServiceEntry("backend"), vips.NewServiceEntry("database"), vips.NewServiceEntry("frontend")}))
+		Expect(serviceSet.Get(vips.NewServiceEntry("backend")).Address).To(Equal("240.0.0.0"))
+		Expect(serviceSet.Get(vips.NewServiceEntry("frontend")).Address).To(Equal("240.0.0.1"))
+		Expect(serviceSet.Get(vips.NewServiceEntry("database")).Address).To(Equal("240.0.0.10"))
 	})
 })
diff --git a/pkg/kds/client/sink_test.go b/pkg/kds/client/sink_test.go
index bbdd5053d..611b5119b 100644
--- a/pkg/kds/client/sink_test.go
+++ b/pkg/kds/client/sink_test.go
@@ -3,7 +3,7 @@ package client_test
 import (
 	"time"
 
-	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
@@ -35,12 +35,12 @@ var _ = Describe("KDS Sink", func() {
 		}
 	})
 
-	It("", func() {
+	It("should verify KDS exchange", func() {
 		vrf := kds_verifier.New().
-			Exec(kds_verifier.WaitRequest(defaultTimeout, func(req *envoy_api_v2.DiscoveryRequest) {
+			Exec(kds_verifier.WaitRequest(defaultTimeout, func(req *envoy_sd.DiscoveryRequest) {
 				Expect(req.TypeUrl).To(Equal(string(mesh.MeshType)))
 			})).
-			Exec(kds_verifier.WaitRequest(defaultTimeout, func(req *envoy_api_v2.DiscoveryRequest) {
+			Exec(kds_verifier.WaitRequest(defaultTimeout, func(req *envoy_sd.DiscoveryRequest) {
 				Expect(req.TypeUrl).To(Equal(string(mesh.DataplaneType)))
 			})).
 			Exec(kds_verifier.DiscoveryResponse(
@@ -48,7 +48,7 @@ var _ = Describe("KDS Sink", func() {
 					{Meta: &test_model.ResourceMeta{Name: "mesh1"}, Spec: samples.Mesh1},
 					{Meta: &test_model.ResourceMeta{Name: "mesh2"}, Spec: samples.Mesh2},
 				}}, "1", "1")).
-			Exec(kds_verifier.WaitRequest(defaultTimeout, func(rs *envoy_api_v2.DiscoveryRequest) {
+			Exec(kds_verifier.WaitRequest(defaultTimeout, func(rs *envoy_sd.DiscoveryRequest) {
 				Expect(rs.VersionInfo).To(Equal("1"))
 				Expect(rs.ResponseNonce).To(Equal("1"))
 				Expect(rs.TypeUrl).To(Equal(string(mesh.MeshType)))
diff --git a/pkg/kds/mux/session_test.go b/pkg/kds/mux/session_test.go
index e88c4dec8..497c71bac 100644
--- a/pkg/kds/mux/session_test.go
+++ b/pkg/kds/mux/session_test.go
@@ -5,9 +5,10 @@ import (
 	"sync"
 	"time"
 
-	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
+	"google.golang.org/grpc/metadata"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	"github.com/kumahq/kuma/pkg/kds/mux"
@@ -29,7 +30,7 @@ func (t *testMultiplexStream) Recv() (*mesh_proto.Message, error) {
 }
 
 func (t *testMultiplexStream) Context() context.Context {
-	panic("implement me")
+	return metadata.AppendToOutgoingContext(context.Background(), mux.KDSVersionHeaderKey, mux.KDSVersionV3)
 }
 
 var _ = Describe("Multiplex Session", func() {
@@ -46,28 +47,28 @@ var _ = Describe("Multiplex Session", func() {
 		})
 
 		It("should Send to clientSession's ClientStream and Recv from serverSession's ServerStream", func() {
-			err := clientSession.ClientStream().Send(&envoy_api_v2.DiscoveryRequest{VersionInfo: "1"})
+			err := clientSession.ClientStream().Send(&envoy_sd.DiscoveryRequest{VersionInfo: "1"})
 			Expect(err).ToNot(HaveOccurred())
 			msg, err := serverSession.ServerStream().Recv()
 			Expect(err).ToNot(HaveOccurred())
 			Expect(msg.VersionInfo).To(Equal("1"))
 		})
 		It("should Send to serverSession's ServerStream and Recv from clientSession's ClientStream", func() {
-			err := serverSession.ServerStream().Send(&envoy_api_v2.DiscoveryResponse{VersionInfo: "2"})
+			err := serverSession.ServerStream().Send(&envoy_sd.DiscoveryResponse{VersionInfo: "2"})
 			Expect(err).ToNot(HaveOccurred())
 			msg, err := clientSession.ClientStream().Recv()
 			Expect(err).ToNot(HaveOccurred())
 			Expect(msg.VersionInfo).To(Equal("2"))
 		})
 		It("should Send to clientSession's ServerStream and Recv from serverSession's ClientStream", func() {
-			err := clientSession.ServerStream().Send(&envoy_api_v2.DiscoveryResponse{VersionInfo: "3"})
+			err := clientSession.ServerStream().Send(&envoy_sd.DiscoveryResponse{VersionInfo: "3"})
 			Expect(err).ToNot(HaveOccurred())
 			msg, err := serverSession.ClientStream().Recv()
 			Expect(err).ToNot(HaveOccurred())
 			Expect(msg.VersionInfo).To(Equal("3"))
 		})
 		It("should Send to serverSession's ClientStream and Recv from clientSession's ServerStream", func() {
-			err := serverSession.ClientStream().Send(&envoy_api_v2.DiscoveryRequest{VersionInfo: "4"})
+			err := serverSession.ClientStream().Send(&envoy_sd.DiscoveryRequest{VersionInfo: "4"})
 			Expect(err).ToNot(HaveOccurred())
 			msg, err := clientSession.ServerStream().Recv()
 			Expect(err).ToNot(HaveOccurred())
@@ -98,9 +99,9 @@ var _ = Describe("Multiplex Session", func() {
 					wg.Done()
 				}()
 
-				err := clientSession.ServerStream().Send(&envoy_api_v2.DiscoveryResponse{VersionInfo: "2"})
+				err := clientSession.ServerStream().Send(&envoy_sd.DiscoveryResponse{VersionInfo: "2"})
 				Expect(err).ToNot(HaveOccurred())
-				err = clientSession.ClientStream().Send(&envoy_api_v2.DiscoveryRequest{VersionInfo: "1"})
+				err = clientSession.ClientStream().Send(&envoy_sd.DiscoveryRequest{VersionInfo: "1"})
 				Expect(err).ToNot(HaveOccurred())
 
 				resp, err := serverSession.ClientStream().Recv()
diff --git a/pkg/kds/server/server_test.go b/pkg/kds/server/server_test.go
index 2cc707e43..d82382f3a 100644
--- a/pkg/kds/server/server_test.go
+++ b/pkg/kds/server/server_test.go
@@ -6,8 +6,8 @@ import (
 	"sync"
 	"time"
 
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"github.com/golang/protobuf/proto"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -50,8 +50,8 @@ var _ = Describe("KDS Server", func() {
 			ResourceStore:      s,
 			MockStream:         stream,
 			Wg:                 wg,
-			Responses:          map[string]*v2.DiscoveryResponse{},
-			LastACKedResponses: map[string]*v2.DiscoveryResponse{},
+			Responses:          map[string]*envoy_sd.DiscoveryResponse{},
+			LastACKedResponses: map[string]*envoy_sd.DiscoveryResponse{},
 		}
 	})
 
diff --git a/pkg/plugins/runtime/gateway/listener_generator_test.go b/pkg/plugins/runtime/gateway/listener_generator_test.go
index fe599e218..0f2a0cfbe 100644
--- a/pkg/plugins/runtime/gateway/listener_generator_test.go
+++ b/pkg/plugins/runtime/gateway/listener_generator_test.go
@@ -1,59 +1,189 @@
 package gateway_test
 
 import (
+	"encoding/json"
+	"path"
+
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
+	"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
 	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
-	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	"github.com/kumahq/kuma/pkg/core/runtime"
-	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/test/matchers"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
-	"github.com/kumahq/kuma/pkg/xds/envoy"
 	xds_server "github.com/kumahq/kuma/pkg/xds/server/v3"
 )
 
 var _ = Describe("Gateway Listener", func() {
 	var rt runtime.Runtime
 
+	Do := func(gateway string) (cache.Snapshot, error) {
+		serverCtx := xds_server.NewXdsContext()
+		reconciler := xds_server.DefaultReconciler(rt, serverCtx)
+
+		Expect(StoreInlineFixture(rt, []byte(gateway))).To(Succeed())
+
+		// Unmarshal the gateway YAML again so that we can figure
+		// out which mesh it's in.
+		r, err := rest.UnmarshallToCore([]byte(gateway))
+		Expect(err).To(Succeed())
+
+		// We expect there to be a Dataplane fixture named
+		// "default" in the current mesh.
+		proxy := MakeDataplaneProxy(rt,
+			core_model.ResourceKey{Mesh: r.GetMeta().GetMesh(), Name: "default"})
+
+		Expect(proxy.Dataplane.Spec.IsBuiltinGateway()).To(BeTrue())
+
+		if err := reconciler.Reconcile(xds_context.Context{}, proxy); err != nil {
+			return cache.Snapshot{}, err
+		}
+
+		return serverCtx.Cache().GetSnapshot(proxy.Id.String())
+	}
+
 	BeforeEach(func() {
 		var err error
 
 		rt, err = BuildRuntime()
 		Expect(err).To(Succeed(), "build runtime instance")
 
-		Expect(StoreNamedFixture(rt, "mesh-default.yaml"))
-		Expect(StoreNamedFixture(rt, "dataplane-default.yaml"))
+		Expect(StoreNamedFixture(rt, "mesh-default.yaml")).To(Succeed())
+		Expect(StoreNamedFixture(rt, "dataplane-default.yaml")).To(Succeed())
+
+		Expect(StoreNamedFixture(rt, "mesh-tracing.yaml")).To(Succeed())
+		Expect(StoreNamedFixture(rt, "dataplane-tracing.yaml")).To(Succeed())
+		Expect(StoreNamedFixture(rt, "traffictrace.yaml")).To(Succeed())
+
+		Expect(StoreNamedFixture(rt, "mesh-logging.yaml")).To(Succeed())
+		Expect(StoreNamedFixture(rt, "dataplane-logging.yaml")).To(Succeed())
+		Expect(StoreNamedFixture(rt, "trafficlog.yaml")).To(Succeed())
 	})
 
-	It("should not be implemented", func() {
-		serverCtx := xds_server.NewXdsContext()
-		reconciler := xds_server.DefaultReconciler(rt, serverCtx)
+	DescribeTable("Generate Envoy xDS resources",
+		func(golden string, gateway string) {
+			snap, err := Do(gateway)
+			Expect(err).To(Succeed())
 
-		dp, err := FetchNamedFixture(rt, core_mesh.DataplaneType,
-			core_model.ResourceKey{Mesh: "default", Name: "default"})
-		Expect(err).To(Succeed())
+			out, err := json.MarshalIndent(
+				MakeProtoResource(snap.Resources[envoy_types.Listener]), "", "  ")
+			Expect(err).To(Succeed())
 
-		proxy := xds.Proxy{
-			Id:         xds.FromResourceKey(core_model.MetaToResourceKey(dp.GetMeta())),
-			APIVersion: envoy.APIV3,
-			Dataplane:  dp.(*core_mesh.DataplaneResource),
-			Metadata:   nil,
-			Routing:    xds.Routing{},
-			Policies:   xds.MatchedPolicies{},
-		}
+			Expect(out).To(matchers.MatchGoldenJSON(path.Join("testdata", golden)))
+		},
+		Entry("should generate a single listener",
+			"01-gateway-listener.yaml", `
+type: Gateway
+mesh: default
+name: edge-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 8080
+    protocol: HTTP
+    tags:
+      port: http/8080
+`),
+		Entry("should generate a multiple listeners",
+			"02-gateway-listener.yaml", `
+type: Gateway
+mesh: default
+name: edge-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 8080
+    protocol: HTTP
+    tags:
+      port: http/8080
+  - port: 9090
+    protocol: HTTP
+    tags:
+      port: http/9090
+`),
+		Entry("should generate listener tracing",
+			"03-gateway-listener.yaml", `
+type: Gateway
+mesh: tracing
+name: tracing-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 8080
+    protocol: HTTP
+    tags:
+      port: http/8080
+`),
+		Entry("should generate listener logging",
+			"04-gateway-listener.yaml", `
+type: Gateway
+mesh: logging
+name: logging-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 8080
+    protocol: HTTP
+    tags:
+      port: http/8080
+`),
+	)
 
-		Expect(proxy.Dataplane.Spec.IsBuiltinGateway()).To(BeTrue())
+	DescribeTable("fail to generate xDS resources",
+		func(errMsg string, gateway string) {
+			_, err := Do(gateway)
+			Expect(err).ToNot(Succeed())
+			Expect(err.Error()).To(ContainSubstring(errMsg))
+		},
 
-		// The gateway listener generator isn't implemented,
-		// so we expect this should fail.
-		err = reconciler.Reconcile(xds_context.Context{}, &proxy)
-		Expect(err).To(Not(Succeed()))
-		Expect(err.Error()).To(ContainSubstring("not implemented"))
+		Entry("incompatible listeners",
+			"cannot collapse listener protocols", `
+type: Gateway
+mesh: default
+name: edge-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 8080
+    protocol: HTTP
+    tags:
+      port: http/8080
+  - port: 8080
+    protocol: HTTPS
+    tags:
+      port: http/9090
+`,
+		),
 
-		snap, err := serverCtx.Cache().GetSnapshot(proxy.Id.String())
-		Expect(err).To(Not(Succeed()))
-		Expect(snap.Resources[envoy_types.Listener].Items).To(BeEmpty())
-	})
+		Entry("unsupported protocol",
+			"unsupported protocol", `
+type: Gateway
+mesh: default
+name: edge-gateway
+sources:
+- match:
+    kuma.io/service: gateway-default
+conf:
+  listeners:
+  - port: 443
+    protocol: HTTPS
+    tags:
+      port: http/443
+`,
+		),
+	)
 })
diff --git a/pkg/plugins/runtime/gateway/suite_test.go b/pkg/plugins/runtime/gateway/suite_test.go
index b2a7af983..30d47a67b 100644
--- a/pkg/plugins/runtime/gateway/suite_test.go
+++ b/pkg/plugins/runtime/gateway/suite_test.go
@@ -6,24 +6,120 @@ import (
 	"path"
 	"testing"
 
+	cache_v3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
+	"github.com/golang/protobuf/proto"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
 	kuma_cp "github.com/kumahq/kuma/pkg/config/app/kuma-cp"
+	"github.com/kumahq/kuma/pkg/core/faultinjections"
+	"github.com/kumahq/kuma/pkg/core/logs"
+	"github.com/kumahq/kuma/pkg/core/permissions"
 	"github.com/kumahq/kuma/pkg/core/plugins"
+	"github.com/kumahq/kuma/pkg/core/ratelimits"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	"github.com/kumahq/kuma/pkg/core/resources/registry"
 	"github.com/kumahq/kuma/pkg/core/resources/store"
 	"github.com/kumahq/kuma/pkg/core/runtime"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/test"
 	test_runtime "github.com/kumahq/kuma/pkg/test/runtime"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/sync"
 )
 
 func TestGateway(t *testing.T) {
 	test.RunSpecs(t, "Gateway Suite")
 }
 
+type ProtoMessage struct {
+	Message proto.Message
+}
+
+func (p ProtoMessage) MarshalJSON() ([]byte, error) {
+	return util_proto.ToJSON(p.Message)
+}
+
+type ProtoResource struct {
+	Resources map[string]ProtoMessage
+}
+
+// MakeProtoResource wraps Go Control Plane resources in a map that
+// implements the json.Marshaler so that the resulting JSON fully
+// expands embedded Any protobufs (which are otherwise serialized
+// as byte arrays).
+func MakeProtoResource(resources cache_v3.Resources) ProtoResource {
+	result := ProtoResource{
+		Resources: map[string]ProtoMessage{},
+	}
+
+	for name, values := range resources.Items {
+		result.Resources[name] = ProtoMessage{
+			Message: values.Resource,
+		}
+	}
+
+	return result
+}
+
+type mockMetadataTracker struct{}
+
+func (m mockMetadataTracker) Metadata(dpKey core_model.ResourceKey) *core_xds.DataplaneMetadata {
+	return nil
+}
+
+func MakeDataplaneProxy(rt runtime.Runtime, key core_model.ResourceKey) *core_xds.Proxy {
+	b := sync.DataplaneProxyBuilder{
+		CachingResManager:    rt.ReadOnlyResourceManager(),
+		NonCachingResManager: rt.ResourceManager(),
+		LookupIP:             rt.LookupIP(),
+		DataSourceLoader:     rt.DataSourceLoader(),
+		MetadataTracker:      mockMetadataTracker{},
+		PermissionMatcher: permissions.TrafficPermissionsMatcher{
+			ResourceManager: rt.ReadOnlyResourceManager(),
+		},
+		LogsMatcher: logs.TrafficLogsMatcher{
+			ResourceManager: rt.ReadOnlyResourceManager(),
+		},
+		FaultInjectionMatcher: faultinjections.FaultInjectionMatcher{
+			ResourceManager: rt.ReadOnlyResourceManager(),
+		},
+		RateLimitMatcher: ratelimits.RateLimitMatcher{
+			ResourceManager: rt.ReadOnlyResourceManager(),
+		},
+		Zone:       rt.Config().Multizone.Zone.Name,
+		APIVersion: envoy.APIV3,
+	}
+
+	mesh := core_mesh.NewMeshResource()
+	Expect(rt.ReadOnlyResourceManager().Get(context.TODO(), mesh, store.GetByKey(key.Mesh, core_model.NoMesh))).
+		To(Succeed())
+
+	dataplanes := core_mesh.DataplaneResourceList{}
+	Expect(rt.ResourceManager().List(context.TODO(), &dataplanes, store.ListByMesh(key.Mesh))).To(Succeed())
+
+	proxy, err := b.Build(key, &xds_context.Context{
+		ControlPlane: &xds_context.ControlPlaneContext{
+			SdsTlsCert:        nil,
+			AdminProxyKeyPair: nil,
+			CLACache:          nil,
+		},
+		Mesh: xds_context.MeshContext{
+			Resource:   mesh,
+			Dataplanes: &dataplanes,
+		},
+		ConnectionInfo:   xds_context.ConnectionInfo{},
+		EnvoyAdminClient: nil,
+	})
+	Expect(err).To(Succeed())
+
+	return proxy
+}
+
 // FetchNamedFixture retrieves the named resource from the runtime
 // resource manager.
 func FetchNamedFixture(
@@ -51,7 +147,12 @@ func StoreNamedFixture(rt runtime.Runtime, name string) error {
 		return err
 	}
 
-	r, err := rest.UnmarshallToCore(bytes)
+	return StoreInlineFixture(rt, bytes)
+}
+
+// StoreInlineFixture stores the given YAML object in the runtime resource manager.
+func StoreInlineFixture(rt runtime.Runtime, object []byte) error {
+	r, err := rest.UnmarshallToCore(object)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/plugins/runtime/gateway/testdata/01-gateway-listener.yaml b/pkg/plugins/runtime/gateway/testdata/01-gateway-listener.yaml
new file mode 100644
index 000000000..a11d1cfc1
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/01-gateway-listener.yaml
@@ -0,0 +1,64 @@
+{
+  "Resources": {
+    "edge-gateway:HTTP:8080": {
+      "name": "edge-gateway:HTTP:8080",
+      "address": {
+        "socketAddress": {
+          "address": "192.168.1.1",
+          "portValue": 8080
+        }
+      },
+      "filterChains": [
+        {
+          "filters": [
+            {
+              "name": "envoy.filters.network.http_connection_manager",
+              "typedConfig": {
+                "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+                "statPrefix": "gateway-default",
+                "rds": {
+                  "configSource": {
+                    "ads": {},
+                    "resourceApiVersion": "V3"
+                  },
+                  "routeConfigName": "default-route"
+                },
+                "httpFilters": [
+                  {
+                    "name": "envoy.filters.http.router"
+                  }
+                ],
+                "commonHttpProtocolOptions": {
+                  "idleTimeout": "300s",
+                  "headersWithUnderscoresAction": "REJECT_REQUEST"
+                },
+                "http2ProtocolOptions": {
+                  "maxConcurrentStreams": 100,
+                  "initialStreamWindowSize": 65536,
+                  "initialConnectionWindowSize": 1048576,
+                  "allowConnect": true
+                },
+                "serverName": "Kuma Gateway",
+                "streamIdleTimeout": "5s",
+                "requestHeadersTimeout": "0.500s",
+                "normalizePath": true,
+                "mergeSlashes": true
+              }
+            }
+          ]
+        }
+      ],
+      "perConnectionBufferLimitBytes": 32768,
+      "listenerFilters": [
+        {
+          "name": "envoy.filters.listener.tls_inspector",
+          "typedConfig": {
+            "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
+          }
+        }
+      ],
+      "trafficDirection": "INBOUND",
+      "reusePort": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/pkg/plugins/runtime/gateway/testdata/02-gateway-listener.yaml b/pkg/plugins/runtime/gateway/testdata/02-gateway-listener.yaml
new file mode 100644
index 000000000..77665e25c
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/02-gateway-listener.yaml
@@ -0,0 +1,124 @@
+{
+  "Resources": {
+    "edge-gateway:HTTP:8080": {
+      "name": "edge-gateway:HTTP:8080",
+      "address": {
+        "socketAddress": {
+          "address": "192.168.1.1",
+          "portValue": 8080
+        }
+      },
+      "filterChains": [
+        {
+          "filters": [
+            {
+              "name": "envoy.filters.network.http_connection_manager",
+              "typedConfig": {
+                "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+                "statPrefix": "gateway-default",
+                "rds": {
+                  "configSource": {
+                    "ads": {},
+                    "resourceApiVersion": "V3"
+                  },
+                  "routeConfigName": "default-route"
+                },
+                "httpFilters": [
+                  {
+                    "name": "envoy.filters.http.router"
+                  }
+                ],
+                "commonHttpProtocolOptions": {
+                  "idleTimeout": "300s",
+                  "headersWithUnderscoresAction": "REJECT_REQUEST"
+                },
+                "http2ProtocolOptions": {
+                  "maxConcurrentStreams": 100,
+                  "initialStreamWindowSize": 65536,
+                  "initialConnectionWindowSize": 1048576,
+                  "allowConnect": true
+                },
+                "serverName": "Kuma Gateway",
+                "streamIdleTimeout": "5s",
+                "requestHeadersTimeout": "0.500s",
+                "normalizePath": true,
+                "mergeSlashes": true
+              }
+            }
+          ]
+        }
+      ],
+      "perConnectionBufferLimitBytes": 32768,
+      "listenerFilters": [
+        {
+          "name": "envoy.filters.listener.tls_inspector",
+          "typedConfig": {
+            "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
+          }
+        }
+      ],
+      "trafficDirection": "INBOUND",
+      "reusePort": true
+    },
+    "edge-gateway:HTTP:9090": {
+      "name": "edge-gateway:HTTP:9090",
+      "address": {
+        "socketAddress": {
+          "address": "192.168.1.1",
+          "portValue": 9090
+        }
+      },
+      "filterChains": [
+        {
+          "filters": [
+            {
+              "name": "envoy.filters.network.http_connection_manager",
+              "typedConfig": {
+                "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+                "statPrefix": "gateway-default",
+                "rds": {
+                  "configSource": {
+                    "ads": {},
+                    "resourceApiVersion": "V3"
+                  },
+                  "routeConfigName": "default-route"
+                },
+                "httpFilters": [
+                  {
+                    "name": "envoy.filters.http.router"
+                  }
+                ],
+                "commonHttpProtocolOptions": {
+                  "idleTimeout": "300s",
+                  "headersWithUnderscoresAction": "REJECT_REQUEST"
+                },
+                "http2ProtocolOptions": {
+                  "maxConcurrentStreams": 100,
+                  "initialStreamWindowSize": 65536,
+                  "initialConnectionWindowSize": 1048576,
+                  "allowConnect": true
+                },
+                "serverName": "Kuma Gateway",
+                "streamIdleTimeout": "5s",
+                "requestHeadersTimeout": "0.500s",
+                "normalizePath": true,
+                "mergeSlashes": true
+              }
+            }
+          ]
+        }
+      ],
+      "perConnectionBufferLimitBytes": 32768,
+      "listenerFilters": [
+        {
+          "name": "envoy.filters.listener.tls_inspector",
+          "typedConfig": {
+            "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
+          }
+        }
+      ],
+      "trafficDirection": "INBOUND",
+      "reusePort": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/pkg/plugins/runtime/gateway/testdata/03-gateway-listener.yaml b/pkg/plugins/runtime/gateway/testdata/03-gateway-listener.yaml
new file mode 100644
index 000000000..7c415bec4
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/03-gateway-listener.yaml
@@ -0,0 +1,79 @@
+{
+  "Resources": {
+    "tracing-gateway:HTTP:8080": {
+      "name": "tracing-gateway:HTTP:8080",
+      "address": {
+        "socketAddress": {
+          "address": "192.168.1.1",
+          "portValue": 8080
+        }
+      },
+      "filterChains": [
+        {
+          "filters": [
+            {
+              "name": "envoy.filters.network.http_connection_manager",
+              "typedConfig": {
+                "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+                "statPrefix": "gateway-default",
+                "rds": {
+                  "configSource": {
+                    "ads": {},
+                    "resourceApiVersion": "V3"
+                  },
+                  "routeConfigName": "default-route"
+                },
+                "httpFilters": [
+                  {
+                    "name": "envoy.filters.http.router"
+                  }
+                ],
+                "tracing": {
+                  "overallSampling": {
+                    "value": 100
+                  },
+                  "provider": {
+                    "name": "envoy.zipkin",
+                    "typedConfig": {
+                      "@type": "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig",
+                      "collectorCluster": "tracing:jaeger-collector",
+                      "collectorEndpoint": "/api/v2/spans",
+                      "collectorEndpointVersion": "HTTP_JSON",
+                      "collectorHostname": "jaeger-collector.kuma-tracing:9411"
+                    }
+                  }
+                },
+                "commonHttpProtocolOptions": {
+                  "idleTimeout": "300s",
+                  "headersWithUnderscoresAction": "REJECT_REQUEST"
+                },
+                "http2ProtocolOptions": {
+                  "maxConcurrentStreams": 100,
+                  "initialStreamWindowSize": 65536,
+                  "initialConnectionWindowSize": 1048576,
+                  "allowConnect": true
+                },
+                "serverName": "Kuma Gateway",
+                "streamIdleTimeout": "5s",
+                "requestHeadersTimeout": "0.500s",
+                "normalizePath": true,
+                "mergeSlashes": true
+              }
+            }
+          ]
+        }
+      ],
+      "perConnectionBufferLimitBytes": 32768,
+      "listenerFilters": [
+        {
+          "name": "envoy.filters.listener.tls_inspector",
+          "typedConfig": {
+            "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
+          }
+        }
+      ],
+      "trafficDirection": "INBOUND",
+      "reusePort": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/pkg/plugins/runtime/gateway/testdata/04-gateway-listener.yaml b/pkg/plugins/runtime/gateway/testdata/04-gateway-listener.yaml
new file mode 100644
index 000000000..46354c27e
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/04-gateway-listener.yaml
@@ -0,0 +1,64 @@
+{
+  "Resources": {
+    "logging-gateway:HTTP:8080": {
+      "name": "logging-gateway:HTTP:8080",
+      "address": {
+        "socketAddress": {
+          "address": "192.168.1.1",
+          "portValue": 8080
+        }
+      },
+      "filterChains": [
+        {
+          "filters": [
+            {
+              "name": "envoy.filters.network.http_connection_manager",
+              "typedConfig": {
+                "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+                "statPrefix": "gateway-default",
+                "rds": {
+                  "configSource": {
+                    "ads": {},
+                    "resourceApiVersion": "V3"
+                  },
+                  "routeConfigName": "default-route"
+                },
+                "httpFilters": [
+                  {
+                    "name": "envoy.filters.http.router"
+                  }
+                ],
+                "commonHttpProtocolOptions": {
+                  "idleTimeout": "300s",
+                  "headersWithUnderscoresAction": "REJECT_REQUEST"
+                },
+                "http2ProtocolOptions": {
+                  "maxConcurrentStreams": 100,
+                  "initialStreamWindowSize": 65536,
+                  "initialConnectionWindowSize": 1048576,
+                  "allowConnect": true
+                },
+                "serverName": "Kuma Gateway",
+                "streamIdleTimeout": "5s",
+                "requestHeadersTimeout": "0.500s",
+                "normalizePath": true,
+                "mergeSlashes": true
+              }
+            }
+          ]
+        }
+      ],
+      "perConnectionBufferLimitBytes": 32768,
+      "listenerFilters": [
+        {
+          "name": "envoy.filters.listener.tls_inspector",
+          "typedConfig": {
+            "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
+          }
+        }
+      ],
+      "trafficDirection": "INBOUND",
+      "reusePort": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/pkg/plugins/runtime/gateway/testdata/dataplane-default.yaml b/pkg/plugins/runtime/gateway/testdata/dataplane-default.yaml
old mode 100755
new mode 100644
diff --git a/pkg/plugins/runtime/gateway/testdata/dataplane-logging.yaml b/pkg/plugins/runtime/gateway/testdata/dataplane-logging.yaml
new file mode 100644
index 000000000..daa5e5fdb
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/dataplane-logging.yaml
@@ -0,0 +1,10 @@
+type: Dataplane
+mesh: logging
+name: default
+networking:
+  address: 192.168.1.1
+  gateway:
+    type: BUILTIN
+    tags:
+      kuma.io/service: gateway-default
+
diff --git a/pkg/plugins/runtime/gateway/testdata/dataplane-tracing.yaml b/pkg/plugins/runtime/gateway/testdata/dataplane-tracing.yaml
new file mode 100644
index 000000000..4e1d1b54a
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/dataplane-tracing.yaml
@@ -0,0 +1,10 @@
+type: Dataplane
+mesh: tracing
+name: default
+networking:
+  address: 192.168.1.1
+  gateway:
+    type: BUILTIN
+    tags:
+      kuma.io/service: gateway-default
+
diff --git a/pkg/plugins/runtime/gateway/testdata/mesh-logging.yaml b/pkg/plugins/runtime/gateway/testdata/mesh-logging.yaml
new file mode 100644
index 000000000..238628ff6
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/mesh-logging.yaml
@@ -0,0 +1,14 @@
+type: Mesh
+name: logging
+logging:
+  defaultBackend: file
+  backends:
+    - name: logstash
+      format: '{"start_time": "%START_TIME%", "source": "%KUMA_SOURCE_SERVICE%", "destination": "%KUMA_DESTINATION_SERVICE%", "source_address": "%KUMA_SOURCE_ADDRESS_WITHOUT_PORT%", "destination_address": "%UPSTREAM_HOST%", "duration_millis": "%DURATION%", "bytes_received": "%BYTES_RECEIVED%", "bytes_sent": "%BYTES_SENT%"}'
+      type: tcp
+      conf:
+        address: 127.0.0.1:5000
+    - name: file
+      type: file
+      conf:
+        path: /tmp/access.log
diff --git a/pkg/plugins/runtime/gateway/testdata/mesh-tracing.yaml b/pkg/plugins/runtime/gateway/testdata/mesh-tracing.yaml
new file mode 100644
index 000000000..b5e7c87da
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/mesh-tracing.yaml
@@ -0,0 +1,10 @@
+type: Mesh
+name: tracing
+tracing:
+  defaultBackend: jaeger-collector
+  backends:
+  - name: jaeger-collector
+    type: zipkin
+    sampling: 100.0
+    conf:
+      url: http://jaeger-collector.kuma-tracing:9411/api/v2/spans
diff --git a/pkg/plugins/runtime/gateway/testdata/trafficlog.yaml b/pkg/plugins/runtime/gateway/testdata/trafficlog.yaml
new file mode 100644
index 000000000..51341410c
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/trafficlog.yaml
@@ -0,0 +1,9 @@
+type: TrafficLog
+name: all-traffic
+mesh: logging
+sources:
+  - match:
+      kuma.io/service: '*'
+destinations:
+  - match:
+      kuma.io/service: '*'
diff --git a/pkg/plugins/runtime/gateway/testdata/traffictrace.yaml b/pkg/plugins/runtime/gateway/testdata/traffictrace.yaml
new file mode 100644
index 000000000..bf14bd8b9
--- /dev/null
+++ b/pkg/plugins/runtime/gateway/testdata/traffictrace.yaml
@@ -0,0 +1,8 @@
+type: TrafficTrace
+name: trace-all
+mesh: tracing
+selectors:
+- match:
+    kuma.io/service: '*'
+conf:
+  backend: jaeger-collector
diff --git a/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller_test.go b/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller_test.go
new file mode 100644
index 000000000..83193e72d
--- /dev/null
+++ b/pkg/plugins/runtime/k8s/controllers/mesh_defaults_controller_test.go
@@ -0,0 +1,97 @@
+package controllers_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	kube_types "k8s.io/apimachinery/pkg/types"
+	kube_ctrl "sigs.k8s.io/controller-runtime"
+	kube_client "sigs.k8s.io/controller-runtime/pkg/client"
+	kube_client_fake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	kube_reconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
+	resources_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
+	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
+	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	secret_cipher "github.com/kumahq/kuma/pkg/core/secrets/cipher"
+	secret_manager "github.com/kumahq/kuma/pkg/core/secrets/manager"
+	"github.com/kumahq/kuma/pkg/plugins/resources/k8s"
+	"github.com/kumahq/kuma/pkg/plugins/runtime/k8s/controllers"
+	secrets_k8s "github.com/kumahq/kuma/pkg/plugins/secrets/k8s"
+)
+
+var _ = Describe("MeshDefaultsReconciler", func() {
+
+	var kubeClient kube_client.Client
+	var resourceManager resources_manager.ResourceManager
+	var reconciler kube_reconcile.Reconciler
+
+	BeforeEach(func() {
+		kubeClient = kube_client_fake.NewFakeClientWithScheme(k8sClientScheme)
+		store, err := k8s.NewStore(kubeClient, k8sClientScheme, k8s.NewSimpleConverter())
+		Expect(err).ToNot(HaveOccurred())
+		secretStore, err := secrets_k8s.NewStore(kubeClient, kubeClient, "default")
+		Expect(err).ToNot(HaveOccurred())
+
+		resourceManager = resources_manager.NewResourceManager(store)
+		customizableManager := resources_manager.NewCustomizableResourceManager(resourceManager, nil)
+		customizableManager.Customize(
+			system.SecretType,
+			secret_manager.NewSecretManager(
+				secretStore,
+				secret_cipher.None(),
+				secret_manager.ValidateDelete(func(ctx context.Context, secretName string, secretMesh string) error { return nil })),
+		)
+
+		reconciler = &controllers.MeshDefaultsReconciler{
+			ResourceManager: customizableManager,
+		}
+	})
+
+	createMesh := func() {
+		Expect(
+			resourceManager.Create(context.Background(), mesh.NewMeshResource(), core_store.CreateByKey("default", core_model.NoMesh)),
+		).To(Succeed())
+	}
+
+	hasTrafficPermissions := func() bool {
+		trafficPermissions := &mesh.TrafficPermissionResourceList{}
+		Expect(
+			resourceManager.List(context.Background(), trafficPermissions, core_store.ListByMesh("default")),
+		).To(Succeed())
+		return len(trafficPermissions.Items) == 1
+	}
+
+	reconcile := func() {
+		_, err := reconciler.Reconcile(kube_ctrl.Request{
+			NamespacedName: kube_types.NamespacedName{
+				Name: "default",
+			},
+		})
+		Expect(err).ToNot(HaveOccurred())
+	}
+
+	deleteTrafficPermission := func() {
+		Expect(
+			resourceManager.Delete(context.Background(), mesh.NewTrafficPermissionResource(),
+				core_store.DeleteByKey("allow-all-default", "default")),
+		).To(Succeed())
+	}
+
+	It("should not create a new default policy if it was deleted", func() {
+		createMesh()
+		Expect(hasTrafficPermissions()).To(BeFalse())
+
+		reconcile()
+		Expect(hasTrafficPermissions()).To(BeTrue())
+
+		deleteTrafficPermission()
+		Expect(hasTrafficPermissions()).To(BeFalse())
+
+		reconcile()
+		Expect(hasTrafficPermissions()).To(BeFalse())
+	})
+})
diff --git a/pkg/test/grpc/clientstream.go b/pkg/test/grpc/clientstream.go
index a4427b062..099824cef 100644
--- a/pkg/test/grpc/clientstream.go
+++ b/pkg/test/grpc/clientstream.go
@@ -3,15 +3,15 @@ package grpc
 import (
 	"context"
 
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"github.com/pkg/errors"
 	"google.golang.org/grpc"
 )
 
 type MockClientStream struct {
 	Ctx    context.Context
-	SentCh chan *v2.DiscoveryRequest
-	RecvCh chan *v2.DiscoveryResponse
+	SentCh chan *envoy_sd.DiscoveryRequest
+	RecvCh chan *envoy_sd.DiscoveryResponse
 	grpc.ClientStream
 }
 
@@ -19,12 +19,12 @@ func (stream *MockClientStream) Context() context.Context {
 	return stream.Ctx
 }
 
-func (stream *MockClientStream) Send(resp *v2.DiscoveryRequest) error {
+func (stream *MockClientStream) Send(resp *envoy_sd.DiscoveryRequest) error {
 	stream.SentCh <- resp
 	return nil
 }
 
-func (stream *MockClientStream) Recv() (*v2.DiscoveryResponse, error) {
+func (stream *MockClientStream) Recv() (*envoy_sd.DiscoveryResponse, error) {
 	req, more := <-stream.RecvCh
 	if !more {
 		return nil, errors.New("empty")
@@ -35,8 +35,8 @@ func (stream *MockClientStream) Recv() (*v2.DiscoveryResponse, error) {
 func MakeMockClientStream() *MockClientStream {
 	return &MockClientStream{
 		Ctx:    context.Background(),
-		RecvCh: make(chan *v2.DiscoveryResponse, 10),
-		SentCh: make(chan *v2.DiscoveryRequest, 10),
+		RecvCh: make(chan *envoy_sd.DiscoveryResponse, 10),
+		SentCh: make(chan *envoy_sd.DiscoveryRequest, 10),
 	}
 }
 
diff --git a/pkg/test/grpc/serverstream.go b/pkg/test/grpc/serverstream.go
index 74a48cdb5..a9b95ac96 100644
--- a/pkg/test/grpc/serverstream.go
+++ b/pkg/test/grpc/serverstream.go
@@ -3,15 +3,15 @@ package grpc
 import (
 	"context"
 
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"github.com/pkg/errors"
 	"google.golang.org/grpc"
 )
 
 type MockServerStream struct {
 	Ctx    context.Context
-	RecvCh chan *v2.DiscoveryRequest
-	SentCh chan *v2.DiscoveryResponse
+	RecvCh chan *envoy_sd.DiscoveryRequest
+	SentCh chan *envoy_sd.DiscoveryResponse
 	Nonce  int
 	grpc.ServerStream
 }
@@ -20,14 +20,14 @@ func (stream *MockServerStream) Context() context.Context {
 	return stream.Ctx
 }
 
-func (stream *MockServerStream) Send(resp *v2.DiscoveryResponse) error {
+func (stream *MockServerStream) Send(resp *envoy_sd.DiscoveryResponse) error {
 	// check that nonce is monotonically incrementing
 	stream.Nonce++
 	stream.SentCh <- resp
 	return nil
 }
 
-func (stream *MockServerStream) Recv() (*v2.DiscoveryRequest, error) {
+func (stream *MockServerStream) Recv() (*envoy_sd.DiscoveryRequest, error) {
 	req, more := <-stream.RecvCh
 	if !more {
 		return nil, errors.New("empty")
@@ -36,8 +36,8 @@ func (stream *MockServerStream) Recv() (*v2.DiscoveryRequest, error) {
 }
 
 func (stream *MockServerStream) ClientStream(stopCh chan struct{}) *MockClientStream {
-	sentCh := make(chan *v2.DiscoveryRequest)
-	recvCh := make(chan *v2.DiscoveryResponse)
+	sentCh := make(chan *envoy_sd.DiscoveryRequest)
+	recvCh := make(chan *envoy_sd.DiscoveryResponse)
 	go func() {
 		for {
 			r, more := <-sentCh
@@ -70,7 +70,7 @@ func (stream *MockServerStream) ClientStream(stopCh chan struct{}) *MockClientSt
 func MakeMockStream() *MockServerStream {
 	return &MockServerStream{
 		Ctx:    context.Background(),
-		SentCh: make(chan *v2.DiscoveryResponse, 10),
-		RecvCh: make(chan *v2.DiscoveryRequest, 10),
+		SentCh: make(chan *envoy_sd.DiscoveryResponse, 10),
+		RecvCh: make(chan *envoy_sd.DiscoveryRequest, 10),
 	}
 }
diff --git a/pkg/test/kds/verifier/context.go b/pkg/test/kds/verifier/context.go
index 1fbf483f6..5e064013c 100644
--- a/pkg/test/kds/verifier/context.go
+++ b/pkg/test/kds/verifier/context.go
@@ -3,7 +3,7 @@ package verifier
 import (
 	"sync"
 
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 
 	"github.com/kumahq/kuma/pkg/core/resources/store"
 	test_grpc "github.com/kumahq/kuma/pkg/test/grpc"
@@ -14,10 +14,10 @@ type TestContext interface {
 	ServerStream() *test_grpc.MockServerStream
 	ClientStream() *test_grpc.MockClientStream
 	Stop() chan struct{}
-	SaveLastResponse(typ string, response *v2.DiscoveryResponse)
-	LastResponse(typeURL string) *v2.DiscoveryResponse
-	SaveLastACKedResponse(typ string, response *v2.DiscoveryResponse)
-	LastACKedResponse(typ string) *v2.DiscoveryResponse
+	SaveLastResponse(typ string, response *envoy_sd.DiscoveryResponse)
+	LastResponse(typeURL string) *envoy_sd.DiscoveryResponse
+	SaveLastACKedResponse(typ string, response *envoy_sd.DiscoveryResponse)
+	LastACKedResponse(typ string) *envoy_sd.DiscoveryResponse
 	WaitGroup() *sync.WaitGroup
 }
 
@@ -27,8 +27,8 @@ type TestContextImpl struct {
 	MockClientStream   *test_grpc.MockClientStream
 	StopCh             chan struct{}
 	Wg                 *sync.WaitGroup
-	Responses          map[string]*v2.DiscoveryResponse
-	LastACKedResponses map[string]*v2.DiscoveryResponse
+	Responses          map[string]*envoy_sd.DiscoveryResponse
+	LastACKedResponses map[string]*envoy_sd.DiscoveryResponse
 }
 
 func (t *TestContextImpl) Store() store.ResourceStore {
@@ -47,19 +47,19 @@ func (t *TestContextImpl) Stop() chan struct{} {
 	return t.StopCh
 }
 
-func (t *TestContextImpl) SaveLastResponse(typ string, response *v2.DiscoveryResponse) {
+func (t *TestContextImpl) SaveLastResponse(typ string, response *envoy_sd.DiscoveryResponse) {
 	t.Responses[typ] = response
 }
 
-func (t *TestContextImpl) LastResponse(typ string) *v2.DiscoveryResponse {
+func (t *TestContextImpl) LastResponse(typ string) *envoy_sd.DiscoveryResponse {
 	return t.Responses[typ]
 }
 
-func (t *TestContextImpl) SaveLastACKedResponse(typ string, response *v2.DiscoveryResponse) {
+func (t *TestContextImpl) SaveLastACKedResponse(typ string, response *envoy_sd.DiscoveryResponse) {
 	t.LastACKedResponses[typ] = response
 }
 
-func (t *TestContextImpl) LastACKedResponse(typ string) *v2.DiscoveryResponse {
+func (t *TestContextImpl) LastACKedResponse(typ string) *envoy_sd.DiscoveryResponse {
 	return t.LastACKedResponses[typ]
 }
 
diff --git a/pkg/test/kds/verifier/executables.go b/pkg/test/kds/verifier/executables.go
index 812d3d924..cd1061018 100644
--- a/pkg/test/kds/verifier/executables.go
+++ b/pkg/test/kds/verifier/executables.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"time"
 
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_sd "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	"github.com/golang/protobuf/ptypes/any"
 
 	"github.com/kumahq/kuma/pkg/core/resources/model"
@@ -23,7 +23,7 @@ func Create(ctx context.Context, r model.Resource, opts ...store.CreateOptionsFu
 
 func DiscoveryRequest(node *envoy_core.Node, resourceType model.ResourceType) Executable {
 	return func(tc TestContext) error {
-		tc.ServerStream().RecvCh <- &v2.DiscoveryRequest{
+		tc.ServerStream().RecvCh <- &envoy_sd.DiscoveryRequest{
 			Node:    node,
 			TypeUrl: string(resourceType),
 		}
@@ -33,7 +33,7 @@ func DiscoveryRequest(node *envoy_core.Node, resourceType model.ResourceType) Ex
 
 func ACK(node *envoy_core.Node, resourceType model.ResourceType) Executable {
 	return func(tc TestContext) error {
-		tc.ServerStream().RecvCh <- &v2.DiscoveryRequest{
+		tc.ServerStream().RecvCh <- &envoy_sd.DiscoveryRequest{
 			Node:          node,
 			TypeUrl:       string(resourceType),
 			ResponseNonce: tc.LastResponse(string(resourceType)).Nonce,
@@ -46,7 +46,7 @@ func ACK(node *envoy_core.Node, resourceType model.ResourceType) Executable {
 
 func NACK(node *envoy_core.Node, resourceType model.ResourceType) Executable {
 	return func(tc TestContext) error {
-		tc.ServerStream().RecvCh <- &v2.DiscoveryRequest{
+		tc.ServerStream().RecvCh <- &envoy_sd.DiscoveryRequest{
 			Node:          node,
 			TypeUrl:       string(resourceType),
 			ResponseNonce: tc.LastResponse(string(resourceType)).Nonce,
@@ -94,7 +94,7 @@ func CloseStream() Executable {
 	}
 }
 
-func WaitRequest(timeout time.Duration, testFunc func(rs *v2.DiscoveryRequest)) Executable {
+func WaitRequest(timeout time.Duration, testFunc func(rs *envoy_sd.DiscoveryRequest)) Executable {
 	return func(tc TestContext) error {
 		select {
 		case req := <-tc.ClientStream().SentCh:
@@ -120,7 +120,7 @@ func DiscoveryResponse(rs model.ResourceList, nonce, version string) Executable
 			}
 			resources = append(resources, pbaby)
 		}
-		tc.ClientStream().RecvCh <- &v2.DiscoveryResponse{
+		tc.ClientStream().RecvCh <- &envoy_sd.DiscoveryResponse{
 			TypeUrl:     string(rs.GetItemType()),
 			Nonce:       nonce,
 			VersionInfo: version,
diff --git a/pkg/xds/generator/dns_generator_test.go b/pkg/xds/generator/dns_generator_test.go
index b4b0082be..f3cae3fc7 100644
--- a/pkg/xds/generator/dns_generator_test.go
+++ b/pkg/xds/generator/dns_generator_test.go
@@ -11,8 +11,6 @@ import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_mesh "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/dns/vips"
 	. "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"
@@ -32,19 +30,12 @@ var _ = Describe("DNSGenerator", func() {
 		func(given testCase) {
 			// setup
 			gen := &generator.DNSGenerator{}
-
-			dnsResolver := resolver.NewDNSResolver("mesh")
-			dnsResolver.SetVIPs(vips.List{
-				vips.NewServiceEntry("backend_test-ns_svc_8080"): "240.0.0.0",
-				vips.NewServiceEntry("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,
+					SdsTlsCert: []byte("12345"),
 				},
 				Mesh: xds_context.MeshContext{
 					Resource: &core_mesh.MeshResource{
diff --git a/pkg/xds/topology/dns_test.go b/pkg/xds/topology/dns_test.go
index 14772ff98..84ebb6080 100644
--- a/pkg/xds/topology/dns_test.go
+++ b/pkg/xds/topology/dns_test.go
@@ -1,343 +1,156 @@
 package topology_test
 
 import (
-	"fmt"
-	"strconv"
-
 	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
-	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
-	"github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/dns/vips"
-	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
 	"github.com/kumahq/kuma/pkg/xds/topology"
 )
 
 var _ = Describe("VIPOutbounds", func() {
 
-	It("should update outbounds", func() {
-		dataplane := &core_mesh.DataplaneResource{
-			Meta: &test_model.ResourceMeta{
-				Name: "dp1",
-				Mesh: "default",
-			},
-			Spec: &mesh_proto.Dataplane{
-				Networking: &mesh_proto.Dataplane_Networking{
-					Address: "192.168.0.1",
-					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
-						{
-							Port: 8080,
-							Tags: map[string]string{
-								"kuma.io/service": "backend",
-							},
-						},
+	type outboundTestCase struct {
+		whenOutbounds map[vips.HostnameEntry]vips.VirtualOutbound
+		thenVips      []xds.VIPDomains
+		thenOutbounds []*mesh_proto.Dataplane_Networking_Outbound
+	}
+	DescribeTable("compute outbounds",
+		func(tc outboundTestCase) {
+			vobView := vips.NewVirtualOutboundView(tc.whenOutbounds)
+
+			vips, outbounds := topology.VIPOutbounds(vobView, "mesh")
+
+			Expect(vips).To(Equal(tc.thenVips))
+			Expect(outbounds).To(Equal(tc.thenOutbounds))
+		},
+		Entry("empty", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{},
+		}),
+		Entry("host port backcompat", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewHostEntry("example.com"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{Port: 1234, TagSet: map[string]string{mesh_proto.ServiceTag: "foo"}},
 					},
 				},
 			},
-		}
-
-		// given
-		dataplanes := core_mesh.DataplaneResourceList{}
-		externalServices := &core_mesh.ExternalServiceResourceList{}
-		vipList := vips.List{}
-		for i := 1; i <= 5; i++ {
-			service := "service-" + strconv.Itoa(i)
-			vip := fmt.Sprintf("240.0.0.%d", i)
-			vipList[vips.NewServiceEntry(service)] = vip
-
-			dataplanes.Items = append(dataplanes.Items, &core_mesh.DataplaneResource{
-				Meta: &test_model.ResourceMeta{
-					Name: "dp" + strconv.Itoa(i),
-					Mesh: "default",
-				},
-				Spec: &mesh_proto.Dataplane{
-					Networking: &mesh_proto.Dataplane_Networking{
-						Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
-							{
-								Port:    uint32(1234 + i),
-								Address: vip,
-								Tags: map[string]string{
-									"kuma.io/service": service,
-								},
-							},
-						},
-					},
-				},
-			})
-		}
-
-		// when
-		domains, outbounds := topology.VIPOutbounds(model.MetaToResourceKey(dataplane.Meta), dataplanes.Items, nil, vipList, "mesh", externalServices.Items)
-		// and
-		Expect(outbounds).To(HaveLen(5))
-		// and
-		Expect(outbounds[4].GetTags()[mesh_proto.ServiceTag]).To(Equal("service-5"))
-		// and
-		Expect(outbounds[4].Port).To(Equal(topology.VIPListenPort))
-		// and
-		Expect(domains).To(HaveLen(5))
-		Expect(domains[4].Domains).To(Equal([]string{"service-5.mesh"}))
-	})
-
-	It("shouldn't add outbounds from other meshes", func() {
-		dataplane := &core_mesh.DataplaneResource{
-			Meta: &test_model.ResourceMeta{
-				Name: "dp1",
-				Mesh: "default",
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.1", Domains: []string{"example.com"}},
 			},
-			Spec: &mesh_proto.Dataplane{
-				Networking: &mesh_proto.Dataplane_Networking{
-					Address: "192.168.0.1",
-					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
-						{
-							Port: 8080,
-							Tags: map[string]string{
-								"kuma.io/service": "backend",
-							},
-						},
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.1", Port: 1234, Tags: map[string]string{mesh_proto.ServiceTag: "foo"}},
+				{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "foo"}},
+			},
+		}),
+		Entry("host port no backcompat when using port 80", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewHostEntry("example.com"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{Port: 81, TagSet: map[string]string{mesh_proto.ServiceTag: "bar"}},
+						{Port: 80, TagSet: map[string]string{mesh_proto.ServiceTag: "foo"}},
 					},
 				},
 			},
-		}
-
-		// given
-		vipList := vips.List{
-			vips.NewServiceEntry("service-a"): "240.0.0.1",
-			vips.NewServiceEntry("service-b"): "240.0.0.2",
-		}
-		services := []*mesh_proto.Dataplane_Networking_Ingress_AvailableService{
-			{
-				Mesh: "default",
-				Tags: map[string]string{
-					"kuma.io/service": "service-a",
-				},
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.1", Domains: []string{"example.com"}},
 			},
-			{
-				Mesh: "other",
-				Tags: map[string]string{
-					"kuma.io/service": "service-b",
-				},
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.1", Port: 81, Tags: map[string]string{mesh_proto.ServiceTag: "bar"}},
+				{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "foo"}},
 			},
-		}
-		externalServices := &core_mesh.ExternalServiceResourceList{}
-		dataplanes := core_mesh.DataplaneResourceList{
-			Items: []*core_mesh.DataplaneResource{
-				{
-					Meta: &test_model.ResourceMeta{
-						Name: "dp-ingress",
-						Mesh: "default",
-					},
-					Spec: &mesh_proto.Dataplane{
-						Networking: &mesh_proto.Dataplane_Networking{
-							Ingress: &mesh_proto.Dataplane_Networking_Ingress{
-								AvailableServices: services,
-							},
-						},
+		}),
+		Entry("with no address doesn't generate vips or outbounds", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("example"): {
+					Address: "",
+					Outbounds: []vips.OutboundEntry{
+						{Port: 1234, TagSet: map[string]string{mesh_proto.ServiceTag: "foo"}},
 					},
 				},
 			},
-		}
-
-		// when
-		domains, outbounds := topology.VIPOutbounds(model.MetaToResourceKey(dataplane.Meta), dataplanes.Items, nil, vipList, "mesh", externalServices.Items)
-		// and
-		Expect(outbounds).To(HaveLen(1))
-		// and
-		Expect(outbounds[0].GetTags()[mesh_proto.ServiceTag]).To(Equal("service-a"))
-		// and
-		Expect(outbounds[0].Port).To(Equal(topology.VIPListenPort))
-		// and
-		Expect(domains).To(HaveLen(1))
-		Expect(domains[0].Domains).To(Equal([]string{"service-a.mesh"}))
-	})
-
-	It("should preserve ExternalService port", func() {
-		dataplane := &core_mesh.DataplaneResource{
-			Meta: &test_model.ResourceMeta{Name: "dp1", Mesh: "default"},
-			Spec: &mesh_proto.Dataplane{
-				Networking: &mesh_proto.Dataplane_Networking{
-					Address: "192.168.0.1",
-					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{{
-						Port: 8080,
-						Tags: map[string]string{
-							"kuma.io/service": "backend",
-						}},
+		}),
+		Entry("service generates hostnames", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("example"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{TagSet: map[string]string{mesh_proto.ServiceTag: "example"}},
 					},
 				},
 			},
-		}
-
-		otherDataplanes := []*core_mesh.DataplaneResource{}
-		vipList := vips.List{}
-		for i := 1; i <= 5; i++ {
-			service := "service-" + strconv.Itoa(i)
-			vip := fmt.Sprintf("240.0.0.%d", i)
-			vipList[vips.NewServiceEntry(service)] = vip
-
-			otherDataplanes = append(otherDataplanes, &core_mesh.DataplaneResource{
-				Meta: &test_model.ResourceMeta{
-					Name: "dp" + strconv.Itoa(i),
-					Mesh: "default",
-				},
-				Spec: &mesh_proto.Dataplane{
-					Networking: &mesh_proto.Dataplane_Networking{
-						Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
-							{
-								Port:    uint32(1234 + i),
-								Address: vip,
-								Tags: map[string]string{
-									"kuma.io/service": service,
-								},
-							},
-						},
-					},
-				},
-			})
-		}
-
-		externalServices := []*core_mesh.ExternalServiceResource{
-			{
-				Meta: &test_model.ResourceMeta{Name: "es1", Mesh: "default"},
-				Spec: &mesh_proto.ExternalService{
-					Networking: &mesh_proto.ExternalService_Networking{
-						Address: "1.1.1.1:1234",
-					},
-					Tags: map[string]string{
-						mesh_proto.ServiceTag: "first-external-service",
-					},
-				},
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.1", Domains: []string{"example.mesh"}},
 			},
-			{
-				Meta: &test_model.ResourceMeta{Name: "es2", Mesh: "default"},
-				Spec: &mesh_proto.ExternalService{
-					Networking: &mesh_proto.ExternalService_Networking{
-						Address: "2.2.2.2:4321",
-					},
-					Tags: map[string]string{
-						mesh_proto.ServiceTag: "second-external-service",
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "example"}},
+			},
+		}),
+		Entry("service with port add backcompat", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("example"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{TagSet: map[string]string{mesh_proto.ServiceTag: "example"}, Port: 1234},
 					},
 				},
 			},
-			{
-				Meta: &test_model.ResourceMeta{Name: "es2", Mesh: "default"},
-				Spec: &mesh_proto.ExternalService{
-					Networking: &mesh_proto.ExternalService_Networking{
-						Address: "kuma.io",
-					},
-					Tags: map[string]string{
-						mesh_proto.ServiceTag: "third-external-service",
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.1", Domains: []string{"example.mesh"}},
+			},
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.1", Port: 1234, Tags: map[string]string{mesh_proto.ServiceTag: "example"}},
+				{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "example"}},
+			},
+		}),
+		Entry("service normalizes hostnames", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewServiceEntry("example_svc_80"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{TagSet: map[string]string{mesh_proto.ServiceTag: "example_svc_80"}},
 					},
 				},
 			},
-		}
-		vipList[vips.NewServiceEntry("first-external-service")] = "240.0.0.6"
-		vipList[vips.NewServiceEntry("second-external-service")] = "240.0.0.7"
-		vipList[vips.NewServiceEntry("third-external-service")] = "240.0.0.8"
-
-		actualVips, actualOutbounds := topology.VIPOutbounds(model.MetaToResourceKey(dataplane.Meta), otherDataplanes, nil, vipList, "mesh", externalServices)
-
-		expectedVips := []xds.VIPDomains{
-			{Address: "240.0.0.6", Domains: []string{"first-external-service.mesh"}},
-			{Address: "240.0.0.7", Domains: []string{"second-external-service.mesh"}},
-			{Address: "240.0.0.1", Domains: []string{"service-1.mesh"}},
-			{Address: "240.0.0.2", Domains: []string{"service-2.mesh"}},
-			{Address: "240.0.0.3", Domains: []string{"service-3.mesh"}},
-			{Address: "240.0.0.4", Domains: []string{"service-4.mesh"}},
-			{Address: "240.0.0.5", Domains: []string{"service-5.mesh"}},
-			{Address: "240.0.0.8", Domains: []string{"third-external-service.mesh"}},
-		}
-		Expect(actualVips).To(Equal(expectedVips))
-		expectedOutbounds := []*mesh_proto.Dataplane_Networking_Outbound{
-			{Address: "240.0.0.6", Port: 1234, Tags: map[string]string{mesh_proto.ServiceTag: "first-external-service"}},
-			{Address: "240.0.0.6", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "first-external-service"}},
-			{Address: "240.0.0.7", Port: 4321, Tags: map[string]string{mesh_proto.ServiceTag: "second-external-service"}},
-			{Address: "240.0.0.7", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "second-external-service"}},
-			{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "service-1"}},
-			{Address: "240.0.0.2", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "service-2"}},
-			{Address: "240.0.0.3", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "service-3"}},
-			{Address: "240.0.0.4", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "service-4"}},
-			{Address: "240.0.0.5", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "service-5"}},
-			{Address: "240.0.0.8", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "third-external-service"}},
-		}
-		Expect(actualOutbounds).To(Equal(expectedOutbounds))
-	})
-
-	It("should take ingresses into account", func() {
-		dataplane := &core_mesh.DataplaneResource{
-			Meta: &test_model.ResourceMeta{Name: "dp1", Mesh: "default"},
-			Spec: &mesh_proto.Dataplane{
-				Networking: &mesh_proto.Dataplane_Networking{
-					Address: "192.168.0.1",
-					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{{
-						Port: 8080,
-						Tags: map[string]string{
-							"kuma.io/service": "backend",
-						}},
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.1", Domains: []string{"example_svc_80.mesh", "example.svc.80.mesh"}},
+			},
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "example_svc_80"}},
+			},
+		}),
+		Entry("multi outbounds work", outboundTestCase{
+			whenOutbounds: map[vips.HostnameEntry]vips.VirtualOutbound{
+				vips.NewFqdnEntry("my-foo-service-generated.mesh"): {
+					Address: "240.0.0.1",
+					Outbounds: []vips.OutboundEntry{
+						{Port: 1234, TagSet: map[string]string{mesh_proto.ServiceTag: "foo", "version": "1"}},
+						{Port: 1235, TagSet: map[string]string{mesh_proto.ServiceTag: "foo", "version": "2"}},
 					},
 				},
-			},
-		}
-
-		vipList := vips.List{
-			vips.NewServiceEntry("old-ingress-svc-1"): "240.0.0.0",
-			vips.NewServiceEntry("old-ingress-svc-2"): "240.0.0.1",
-			vips.NewServiceEntry("new-ingress-svc-1"): "240.0.0.2",
-			vips.NewServiceEntry("new-ingress-svc-2"): "240.0.0.3",
-		}
-
-		otherDataplanes := []*core_mesh.DataplaneResource{{
-			Meta: &test_model.ResourceMeta{Name: "old-ingress", Mesh: "default"},
-			Spec: &mesh_proto.Dataplane{
-				Networking: &mesh_proto.Dataplane_Networking{
-					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{{
-						Port:    10001,
-						Address: "192.168.0.2",
-						Tags: map[string]string{
-							"kuma.io/service": "ingress",
-						},
-					}},
-					Ingress: &mesh_proto.Dataplane_Networking_Ingress{
-						AvailableServices: []*mesh_proto.Dataplane_Networking_Ingress_AvailableService{
-							{Mesh: "default", Tags: map[string]string{mesh_proto.ServiceTag: "old-ingress-svc-1"}},
-							{Mesh: "default", Tags: map[string]string{mesh_proto.ServiceTag: "old-ingress-svc-2"}},
-						},
+				vips.NewFqdnEntry("my-bar-service-generated.mesh"): {
+					Address: "240.0.0.2",
+					Outbounds: []vips.OutboundEntry{
+						{Port: 1234, TagSet: map[string]string{mesh_proto.ServiceTag: "bar", "version": "1"}},
+						{Port: 1235, TagSet: map[string]string{mesh_proto.ServiceTag: "bar", "version": "2"}},
 					},
 				},
 			},
-		}}
-
-		zoneIngresses := []*core_mesh.ZoneIngressResource{{
-			Meta: &test_model.ResourceMeta{Name: "new-ingress", Mesh: model.NoMesh},
-			Spec: &mesh_proto.ZoneIngress{
-				Networking: &mesh_proto.ZoneIngress_Networking{
-					Address: "192.168.0.3",
-					Port:    10001,
-				},
-				AvailableServices: []*mesh_proto.ZoneIngress_AvailableService{
-					{Mesh: "default", Tags: map[string]string{mesh_proto.ServiceTag: "new-ingress-svc-1"}},
-					{Mesh: "default", Tags: map[string]string{mesh_proto.ServiceTag: "new-ingress-svc-2"}},
-				},
+			thenVips: []xds.VIPDomains{
+				{Address: "240.0.0.2", Domains: []string{"my-bar-service-generated.mesh"}},
+				{Address: "240.0.0.1", Domains: []string{"my-foo-service-generated.mesh"}},
 			},
-		}}
-
-		actualVips, actualOutbounds := topology.VIPOutbounds(model.MetaToResourceKey(dataplane.Meta), otherDataplanes, zoneIngresses, vipList, "mesh", nil)
-
-		expectedVips := []xds.VIPDomains{
-			{Address: "240.0.0.2", Domains: []string{"new-ingress-svc-1.mesh"}},
-			{Address: "240.0.0.3", Domains: []string{"new-ingress-svc-2.mesh"}},
-			{Address: "240.0.0.0", Domains: []string{"old-ingress-svc-1.mesh"}},
-			{Address: "240.0.0.1", Domains: []string{"old-ingress-svc-2.mesh"}},
-		}
-		Expect(actualVips).To(Equal(expectedVips))
-		expectedOutbounds := []*mesh_proto.Dataplane_Networking_Outbound{
-			{Address: "240.0.0.2", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "new-ingress-svc-1"}},
-			{Address: "240.0.0.3", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "new-ingress-svc-2"}},
-			{Address: "240.0.0.0", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "old-ingress-svc-1"}},
-			{Address: "240.0.0.1", Port: 80, Tags: map[string]string{mesh_proto.ServiceTag: "old-ingress-svc-2"}},
-		}
-		Expect(actualOutbounds).To(Equal(expectedOutbounds))
-	})
+			thenOutbounds: []*mesh_proto.Dataplane_Networking_Outbound{
+				{Address: "240.0.0.2", Port: 1234, Tags: map[string]string{mesh_proto.ServiceTag: "bar", "version": "1"}},
+				{Address: "240.0.0.2", Port: 1235, Tags: map[string]string{mesh_proto.ServiceTag: "bar", "version": "2"}},
+				{Address: "240.0.0.1", Port: 1234, Tags: map[string]string{mesh_proto.ServiceTag: "foo", "version": "1"}},
+				{Address: "240.0.0.1", Port: 1235, Tags: map[string]string{mesh_proto.ServiceTag: "foo", "version": "2"}},
+			},
+		}),
+	)
 })
diff --git a/test/e2e/compatibility/compatibility_suite_test.go b/test/e2e/compatibility/compatibility_suite_test.go
index 0d5077d9e..62ad6d9e8 100644
--- a/test/e2e/compatibility/compatibility_suite_test.go
+++ b/test/e2e/compatibility/compatibility_suite_test.go
@@ -9,7 +9,7 @@ import (
 
 func TestE2ECompatibility(t *testing.T) {
 	if framework.IsK8sClustersStarted() {
-		test.RunSpecs(t, "E2E Deploy Suite")
+		test.RunSpecs(t, "E2E Compatibility Suite")
 	} else {
 		t.SkipNow()
 	}
diff --git a/test/e2e/compatibility/cp_compatibility_kubernetes_multizone.go b/test/e2e/compatibility/cp_compatibility_kubernetes_multizone.go
new file mode 100644
index 000000000..76c6d9851
--- /dev/null
+++ b/test/e2e/compatibility/cp_compatibility_kubernetes_multizone.go
@@ -0,0 +1,134 @@
+package compatibility
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/gruntwork-io/terratest/modules/k8s"
+	"github.com/gruntwork-io/terratest/modules/random"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/config/core"
+	. "github.com/kumahq/kuma/test/framework"
+)
+
+var OldChart = "0.6.3"
+var UpstreamImageRegistry = "kumahq"
+
+func CpCompatibilityMultizoneKubernetes() {
+	var globalCluster Cluster
+	var globalReleaseName string
+	var globalDeployOptsFuncs = KumaK8sDeployOpts
+
+	var zoneCluster Cluster
+	var zoneDeployOptsFuncs = KumaZoneK8sDeployOpts
+	var zoneReleaseName string
+
+	BeforeEach(func() {
+		// Global CP
+		c, err := NewK8sClusterWithTimeout(
+			NewTestingT(),
+			Kuma1,
+			Silent,
+			6*time.Second)
+		Expect(err).ToNot(HaveOccurred())
+		globalCluster = c.WithRetries(60)
+
+		globalReleaseName = fmt.Sprintf(
+			"kuma-%s",
+			strings.ToLower(random.UniqueId()),
+		)
+
+		globalDeployOptsFuncs = append(globalDeployOptsFuncs,
+			WithInstallationMode(HelmInstallationMode),
+			WithHelmChartPath(HelmRepo),
+			WithHelmReleaseName(globalReleaseName),
+			WithHelmChartVersion(OldChart),
+			WithoutHelmOpt("global.image.tag"),
+			WithHelmOpt("global.image.registry", UpstreamImageRegistry))
+
+		err = NewClusterSetup().
+			Install(Kuma(core.Global, globalDeployOptsFuncs...)).
+			Setup(globalCluster)
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(globalCluster.VerifyKuma()).To(Succeed())
+
+		// Zone CP
+		c, err = NewK8sClusterWithTimeout(
+			NewTestingT(),
+			Kuma2,
+			Silent,
+			6*time.Second)
+		Expect(err).ToNot(HaveOccurred())
+		zoneCluster = c.WithRetries(60)
+
+		zoneReleaseName = fmt.Sprintf(
+			"kuma-%s",
+			strings.ToLower(random.UniqueId()),
+		)
+
+		zoneDeployOptsFuncs = append(zoneDeployOptsFuncs,
+			WithInstallationMode(HelmInstallationMode),
+			WithHelmChartPath(HelmRepo),
+			WithHelmReleaseName(zoneReleaseName),
+			WithHelmChartVersion(OldChart),
+			WithoutHelmOpt("global.image.tag"),
+			WithHelmOpt("global.image.registry", UpstreamImageRegistry),
+			WithGlobalAddress(globalCluster.GetKuma().GetKDSServerAddress()),
+			WithHelmOpt("ingress.enabled", "true"),
+		)
+
+		err = NewClusterSetup().
+			Install(Kuma(core.Zone, zoneDeployOptsFuncs...)).
+			Install(NamespaceWithSidecarInjection(TestNamespace)).
+			Setup(zoneCluster)
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(zoneCluster.VerifyKuma()).To(Succeed())
+	})
+
+	AfterEach(func() {
+		if ShouldSkipCleanup() {
+			return
+		}
+
+		Expect(zoneCluster.DeleteKuma(zoneDeployOptsFuncs...)).To(Succeed())
+		Expect(zoneCluster.DismissCluster()).To(Succeed())
+
+		Expect(globalCluster.DeleteKuma(globalDeployOptsFuncs...)).To(Succeed())
+		Expect(globalCluster.DismissCluster()).To(Succeed())
+	})
+
+	It("should sync resources between new global and old zone", func() {
+		// when global is upgraded
+		upgradeOptsFuncs := append(KumaK8sDeployOpts, WithHelmReleaseName(globalReleaseName))
+		err := globalCluster.(*K8sCluster).UpgradeKuma(core.Global, upgradeOptsFuncs...)
+		Expect(err).ToNot(HaveOccurred())
+
+		// and new resource is created on Global
+		err = YamlK8s(`
+apiVersion: kuma.io/v1alpha1
+kind: Mesh
+metadata:
+  name: demo
+`)(globalCluster)
+
+		// then the resource is synchronized when old remote is connected (KDS is backwards compatible)
+		Expect(err).ToNot(HaveOccurred())
+		Eventually(func() (string, error) {
+			return k8s.RunKubectlAndGetOutputE(zoneCluster.GetTesting(), zoneCluster.GetKubectlOptions(), "get", "meshes")
+		}, "30s", "1s").Should(ContainSubstring("demo"))
+
+		// when new resources is created on Zone
+		err = DemoClientK8s("default")(zoneCluster)
+
+		// then resource is synchronized to Global
+		Expect(err).ToNot(HaveOccurred())
+		Eventually(func() (string, error) {
+			return k8s.RunKubectlAndGetOutputE(globalCluster.GetTesting(), globalCluster.GetKubectlOptions("default"), "get", "dataplanes")
+		}, "30s", "1s").Should(ContainSubstring("demo-client"))
+	})
+}
diff --git a/test/e2e/compatibility/cp_compatibility_kubernetes_multizone_test.go b/test/e2e/compatibility/cp_compatibility_kubernetes_multizone_test.go
new file mode 100644
index 000000000..1e1549b5f
--- /dev/null
+++ b/test/e2e/compatibility/cp_compatibility_kubernetes_multizone_test.go
@@ -0,0 +1,9 @@
+package compatibility_test
+
+import (
+	. "github.com/onsi/ginkgo"
+
+	"github.com/kumahq/kuma/test/e2e/compatibility"
+)
+
+var _ = Describe("Test Kubernetes Multizone Compatibility", compatibility.CpCompatibilityMultizoneKubernetes)
diff --git a/test/e2e/trafficpermission/kubernetes/e2e_suite_test.go b/test/e2e/trafficpermission/kubernetes/e2e_suite_test.go
new file mode 100644
index 000000000..23af7e467
--- /dev/null
+++ b/test/e2e/trafficpermission/kubernetes/e2e_suite_test.go
@@ -0,0 +1,16 @@
+package kubernetes_test
+
+import (
+	"testing"
+
+	"github.com/kumahq/kuma/pkg/test"
+	"github.com/kumahq/kuma/test/framework"
+)
+
+func TestE2ETrafficPermissionKubernetes(t *testing.T) {
+	if framework.IsK8sClustersStarted() {
+		test.RunSpecs(t, "Traffic Permission Kubernetes Suite")
+	} else {
+		t.SkipNow()
+	}
+}
diff --git a/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s.go b/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s.go
new file mode 100644
index 000000000..f4131f87d
--- /dev/null
+++ b/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s.go
@@ -0,0 +1,83 @@
+package kubernetes
+
+import (
+	"strings"
+	"time"
+
+	"github.com/gruntwork-io/terratest/modules/k8s"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	config_core "github.com/kumahq/kuma/pkg/config/core"
+	. "github.com/kumahq/kuma/test/framework"
+)
+
+func TrafficPermission() {
+	var k8sCluster Cluster
+	var optsKubernetes = KumaK8sDeployOpts
+
+	E2EBeforeSuite(func() {
+		k8sClusters, err := NewK8sClusters([]string{Kuma1}, Silent)
+		Expect(err).ToNot(HaveOccurred())
+
+		k8sCluster = k8sClusters.GetCluster(Kuma1)
+
+		Expect(Kuma(config_core.Standalone, optsKubernetes...)(k8sCluster)).To(Succeed())
+		Expect(k8sCluster.VerifyKuma()).To(Succeed())
+	})
+
+	E2EAfterSuite(func() {
+		Expect(k8sCluster.DeleteKuma(optsKubernetes...)).To(Succeed())
+		Expect(k8sCluster.DismissCluster()).To(Succeed())
+	})
+
+	removeDefaultTrafficPermission := func() {
+		err := k8s.RunKubectlE(k8sCluster.GetTesting(), k8sCluster.GetKubectlOptions(), "delete", "trafficpermission", "allow-all-default")
+		Expect(err).ToNot(HaveOccurred())
+	}
+
+	noDefaultTrafficPermission := func() {
+		Eventually(func() bool {
+			out, err := k8s.RunKubectlAndGetOutputE(k8sCluster.GetTesting(), k8sCluster.GetKubectlOptions(), "get", "trafficpermissions")
+			if err != nil {
+				return false
+			}
+			return !strings.Contains(out, "allow-all-default")
+		}, "30s", "1s").Should(BeTrue())
+	}
+
+	defaultPoliciesCreated := func() {
+		Eventually(func() bool {
+			out, err := k8s.RunKubectlAndGetOutputE(k8sCluster.GetTesting(), k8sCluster.GetKubectlOptions(), "get", "meshes", "-o", "yaml")
+			if err != nil {
+				return false
+			}
+			return strings.Contains(out, "k8s.kuma.io/mesh-defaults-generated")
+		}, "30s", "1s").Should(BeTrue())
+	}
+
+	restartKumaCP := func() {
+		pods := k8sCluster.GetKuma().(*K8sControlPlane).GetKumaCPPods()
+		Expect(pods).To(HaveLen(1))
+		err := k8s.RunKubectlE(k8sCluster.GetTesting(), k8sCluster.GetKubectlOptions(), "delete", "pod", pods[0].GetName(), "-n", pods[0].GetNamespace())
+		Expect(err).ToNot(HaveOccurred())
+		Expect(k8sCluster.(*K8sCluster).WaitApp(KumaServiceName, KumaNamespace, 1)).To(Succeed())
+	}
+
+	It("should not create deleted default traffic permission after Kuma CP restart", func() {
+		// given
+		defaultPoliciesCreated()
+
+		// when
+		removeDefaultTrafficPermission()
+		// then
+		noDefaultTrafficPermission()
+
+		// when
+		restartKumaCP()
+		// and when
+		time.Sleep(10 * time.Second)
+		// then
+		noDefaultTrafficPermission()
+	})
+}
diff --git a/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s_test.go b/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s_test.go
new file mode 100644
index 000000000..cdea67a67
--- /dev/null
+++ b/test/e2e/trafficpermission/kubernetes/traffic_permission_k8s_test.go
@@ -0,0 +1,9 @@
+package kubernetes_test
+
+import (
+	. "github.com/onsi/ginkgo"
+
+	"github.com/kumahq/kuma/test/e2e/trafficpermission/kubernetes"
+)
+
+var _ = Describe("Traffic Permission on Kubernetes", kubernetes.TrafficPermission)
diff --git a/test/framework/setup.go b/test/framework/setup.go
index b65a0f130..f9f14a455 100644
--- a/test/framework/setup.go
+++ b/test/framework/setup.go
@@ -277,6 +277,17 @@ spec:
 	)
 }
 
+func NamespaceWithSidecarInjection(namespace string) InstallFunc {
+	return YamlK8s(fmt.Sprintf(`
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: %s
+  annotations:
+    kuma.io/sidecar-injection: "enabled"
+`, namespace))
+}
+
 func DemoClientJobK8s(mesh, destination string) InstallFunc {
 	const name = "demo-job-client"
 	deployment := `