Repository: kumahq/kuma
Pull request #2484 (2021-08-03 12:41:31)

feat(dns): Rewrite dns persistence to allow virtual-outbound to be added
Currently the persisted state on the vips was very restraining.
With the addition of virtual-outbounds we'll need something more flexible
This migrates the state to a new format that will be extensible.

It also avoids reusing vips straight away which might avoid some issues when services are added and removed at once.


codecov-commenter: # [Codecov](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq) Report
> Merging [#2484](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq) (6529843) into [master](https://codecov.io/gh/kumahq/kuma/commit/57212439b9c6f7d6149b1e17541c42a041e7b7f6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq) (5721243) will **increase** coverage by `0.00%`.
> The diff coverage is `79.67%`.

[![Impacted file tree graph](https://codecov.io/gh/kumahq/kuma/pull/2484/graphs/tree.svg?width=650&height=150&src=pr&token=D7Yvqs6rwV&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq)](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq)

```diff
@@           Coverage Diff           @@
##           master    #2484   +/-   ##
=======================================
  Coverage   52.28%   52.28%           
=======================================
  Files         865      866    +1     
  Lines       48098    48130   +32     
=======================================
+ Hits        25147    25165   +18     
- Misses      20919    20926    +7     
- Partials     2032     2039    +7     
```


| [Impacted Files](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq) | Coverage Δ | |
|---|---|---|
| [pkg/xds/sync/dataplane\_proxy\_builder.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL3hkcy9zeW5jL2RhdGFwbGFuZV9wcm94eV9idWlsZGVyLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/dns/resolver/resolver.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy9yZXNvbHZlci9yZXNvbHZlci5nbw==) | `66.66% <64.28%> (+1.66%)` | :arrow_up: |
| [pkg/dns/vips/interfaces.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzL2ludGVyZmFjZXMuZ28=) | `54.76% <66.66%> (-19.53%)` | :arrow_down: |
| [pkg/dns/vips/virtual\_outbound.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzL3ZpcnR1YWxfb3V0Ym91bmQuZ28=) | `79.10% <79.10%> (ø)` | |
| [pkg/dns/vips\_allocator.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzX2FsbG9jYXRvci5nbw==) | `74.78% <79.59%> (-1.03%)` | :arrow_down: |
| [pkg/dns/vips/persistence.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzL3BlcnNpc3RlbmNlLmdv) | `65.38% <80.00%> (+0.91%)` | :arrow_up: |
| [pkg/dns/vips/global\_view.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzL2dsb2JhbF92aWV3Lmdv) | `86.20% <86.20%> (ø)` | |
| [pkg/dns/vips\_synchronizer.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL2Rucy92aXBzX3N5bmNocm9uaXplci5nbw==) | `82.85% <100.00%> (+2.85%)` | :arrow_up: |
| [pkg/xds/context/context.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL3hkcy9jb250ZXh0L2NvbnRleHQuZ28=) | `64.70% <100.00%> (-1.97%)` | :arrow_down: |
| [pkg/xds/server/components.go](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq#diff-cGtnL3hkcy9zZXJ2ZXIvY29tcG9uZW50cy5nbw==) | `55.55% <100.00%> (ø)` | |
| ... and [11 more](https://codecov.io/gh/kumahq/kuma/pull/2484/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq). Last update [5721243...6529843](https://codecov.io/gh/kumahq/kuma/pull/2484?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kumahq).


Patch:
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/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/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/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/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 e8554d7eb..226e0419d 100644
--- a/pkg/xds/sync/components.go
+++ b/pkg/xds/sync/components.go
@@ -30,6 +30,8 @@ func defaultDataplaneProxyBuilder(rt core_runtime.Runtime, metadataTracker Datap
 		RateLimitMatcher:      ratelimits.RateLimitMatcher{ResourceManager: rt.ReadOnlyResourceManager()},
 		Zone:                  rt.Config().Multizone.Zone.Name,
 		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 caa8ed603..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,8 +36,10 @@ 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) {
@@ -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
 	}
@@ -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/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/plugins/runtime/gateway/suite_test.go b/pkg/plugins/runtime/gateway/suite_test.go
index b990dae29..30d47a67b 100644
--- a/pkg/plugins/runtime/gateway/suite_test.go
+++ b/pkg/plugins/runtime/gateway/suite_test.go
@@ -107,7 +107,6 @@ func MakeDataplaneProxy(rt runtime.Runtime, key core_model.ResourceKey) *core_xd
 			SdsTlsCert:        nil,
 			AdminProxyKeyPair: nil,
 			CLACache:          nil,
-			DNSResolver:       rt.DNSResolver(),
 		},
 		Mesh: xds_context.MeshContext{
 			Resource:   mesh,
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/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/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)