Repository: linkerd/linkerd2
Pull request #12428 (2024-04-12 21:49:19)

Add IPv6 support for the destination controller
Services in dual-stack mode result in the creation of two EndpointSlices, one for each IP family. Before this change, the Get Destination API would nondeterministically return the address for any of those ES, depending on which one was processed last by the controller because they would overwrite each other.

As part of the ongoing effort to support IPv6/dual-stack networks, this change fixes that behavior giving preference to IPv6 addresses whenever a service exposes both families.

We introduce a new field `ipv6Only` for the `servicePublisher` struct that is set to `true` for any service that exposes an IPv6 address. Whenever an ES is added or updated, if `ipv6Only` is true, it's ignored if its addressType is IPv4.

Also the server unit tests were updated to segregate the tests and resources dealing with the IPv4/IPv6/dual-stack cases.

alpeb: Yes, a service's `.spec.ipFamilies` field is mutable. An update to it would trigger the associated EndpointSlices to be updated or deleted as well. In theory an ES event should come after the creation/update event for the service it refers to in its `ownerReference`, but I see your concern, there's nothing guaranteeing us that.

I like your suggestion about having the endpoints watcher index all the IPs and have the translator filter the appropriate ones. I think we'd also have to track the service ipFamilies entry into the AddressSet. So we'd have to pull the ES' associated service from the lister for that, assuming it's already there, which is a softer assumption than the ordering one mentioned above, but a similar one we already make when we pull the associated pod object and that doesn't seem to have caused any issues. And so the translator would have all the information to do the filtering. Does that make sense?
adleong: Yeah I agree we probably need to track `AddressFamilyMode` or something on AddressSet to track if we're in ipv4 or ipv6 mode.  We're already watching Services so I don't think we need dedicated lookups for this.  In other words:
* when we get an ES update, add the endpoints to either the ipv4 or ipv6 map depending on what kind of address it is
* when we get a Service update, set the AddressFamilyMode depending on the ipFamilies

This way, even if the address mode of a service toggles back and forth, we'll always have the full set of addresses for both address families and we can just swap between them.
alpeb: Turns out adding a separate map for IPv6 addresses implies a lot of changes and added complexity. I went for an alternative approach, expanding the key in the `Addresses` map to include the IP family.

So the translator keeps both IPv4 and IPv6 addresses in `availableEndpoints` and additional logic in `diffEndpoints` determines what adds/removes to consider. This logic accounts for the presence/absence of addresses for different families in `availableEndpoints`, without needing to have to also keep track of the services `ipFamilies` entry. This also avoids having to add logic handling service object updates (that don't imply ES changes) into the translator. This approach tested well (manually), but I'm not sure how readable that is, so I'm open to suggestions.

In the latest change I'm also consuming the `disableIPv6` helm value in the destination controller, that we need to account for as well.
alpeb: Ok I've replaced `prefer-ipv6` with `enable-ipv6`, and added a new `selectAddressFamily()` filtering function triggered after `filterAddresses()` but before `diffEndpoints()` (which remains the same).

Also there are a new set of unit tests in `server_ipv6_test.go`, and in the `TestEndpointTranslatorForPods` tests there's a couple of new cases to test the interaction with zone filtering.


Patch:
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 20a405f70..4a43c62f9 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b
       - run: go install gotest.tools/gotestsum@v0.4.2
       - run: gotestsum -- -cover -coverprofile=coverage.out -v -mod=readonly ./...
-      - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed
+      - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be
         with:
           files: ./coverage.out
           flags: unittests,golang
@@ -41,7 +41,7 @@ jobs:
           export NODE_ENV=test
           bin/web --frozen-lockfile
           bin/web test --reporters="jest-progress-bar-reporter" --reporters="./gh_ann_reporter.js" --coverage
-      - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed
+      - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be
         with:
           directory: ./web/app/coverage
           flags: unittests,javascript
@@ -58,6 +58,6 @@ jobs:
       - shell: bash
         run: mkdir -p target && cd target && bin/scurl -v https://github.com/xd009642/tarpaulin/releases/download/0.27.3/cargo-tarpaulin-x86_64-unknown-linux-musl.tar.gz | tar zxvf - && chmod 755 cargo-tarpaulin
       - run: target/cargo-tarpaulin tarpaulin --workspace --out Xml
-      - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed
+      - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be
         with:
           flags: unittests,rust
diff --git a/.proxy-version b/.proxy-version
index c3ed4e099..f1e97508d 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.229.0
+v2.230.0
diff --git a/Cargo.lock b/Cargo.lock
index 3db08f9cc..3cb2da26a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -244,9 +244,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cc"
-version = "1.0.95"
+version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd"
 
 [[package]]
 name = "cfg-if"
@@ -386,9 +386,9 @@ dependencies = [
 
 [[package]]
 name = "data-encoding"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
 
 [[package]]
 name = "deflate"
@@ -1565,9 +1565,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pest"
-version = "2.7.9"
+version = "2.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95"
+checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8"
 dependencies = [
  "memchr",
  "thiserror",
@@ -1576,9 +1576,9 @@ dependencies = [
 
 [[package]]
 name = "pest_derive"
-version = "2.7.9"
+version = "2.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c"
+checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459"
 dependencies = [
  "pest",
  "pest_generator",
@@ -1586,9 +1586,9 @@ dependencies = [
 
 [[package]]
 name = "pest_generator"
-version = "2.7.9"
+version = "2.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd"
+checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687"
 dependencies = [
  "pest",
  "pest_meta",
@@ -1599,9 +1599,9 @@ dependencies = [
 
 [[package]]
 name = "pest_meta"
-version = "2.7.9"
+version = "2.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca"
+checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd"
 dependencies = [
  "once_cell",
  "pest",
@@ -2004,9 +2004,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.199"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
+checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
 dependencies = [
  "serde_derive",
 ]
@@ -2023,9 +2023,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.199"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
+checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/charts/linkerd-control-plane/templates/destination.yaml b/charts/linkerd-control-plane/templates/destination.yaml
index 6d12557a1..91d8fc67f 100644
--- a/charts/linkerd-control-plane/templates/destination.yaml
+++ b/charts/linkerd-control-plane/templates/destination.yaml
@@ -205,6 +205,7 @@ spec:
         - -cluster-domain={{.Values.clusterDomain}}
         - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}}
         - -default-opaque-ports={{.Values.proxy.opaquePorts}}
+        - -enable-ipv6={{not .Values.disableIPv6}}
         - -enable-pprof={{.Values.enablePprof | default false}}
         {{- if (.Values.destinationController).meshedHttp2ClientProtobuf }}
         - --meshed-http2-client-params={{ toJson .Values.destinationController.meshedHttp2ClientProtobuf }}
diff --git a/charts/partials/templates/_proxy.tpl b/charts/partials/templates/_proxy.tpl
index 103ebb61c..3699e2a17 100644
--- a/charts/partials/templates/_proxy.tpl
+++ b/charts/partials/templates/_proxy.tpl
@@ -76,13 +76,16 @@ env:
   value: "365d"
 {{ end -}}
 - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-  value: 0.0.0.0:{{.Values.proxy.ports.control}}
+  value: "[::]:{{.Values.proxy.ports.control}}"
 - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-  value: 0.0.0.0:{{.Values.proxy.ports.admin}}
+  value: "[::]:{{.Values.proxy.ports.admin}}"
+{{- /* Deprecated, superseded by LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS since proxy's v2.228.0 (deployed since edge-24.4.5) */}}
 - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-  value: 127.0.0.1:{{.Values.proxy.ports.outbound}}
+  value: "127.0.0.1:{{.Values.proxy.ports.outbound}}"
+- name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+  value: "127.0.0.1:{{.Values.proxy.ports.outbound}},[::1]:{{.Values.proxy.ports.outbound}}"
 - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-  value: 0.0.0.0:{{.Values.proxy.ports.inbound}}
+  value: "[::]:{{.Values.proxy.ports.inbound}}"
 - name: LINKERD2_PROXY_INBOUND_IPS
   valueFrom:
     fieldRef:
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 0667633db..9c75e8fa4 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -2,9 +2,8 @@ package destination
 
 import (
 	"fmt"
+	"net/netip"
 	"reflect"
-	"strconv"
-	"strings"
 
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2-proxy-api/go/net"
@@ -41,6 +40,8 @@ type (
 
 		enableH2Upgrade,
 		enableEndpointFiltering,
+		enableIPv6,
+
 		extEndpointZoneWeights bool
 
 		meshedHTTP2ClientParams *pb.Http2ClientParams
@@ -84,6 +85,7 @@ func newEndpointTranslator(
 	identityTrustDomain string,
 	enableH2Upgrade,
 	enableEndpointFiltering,
+	enableIPv6,
 	extEndpointZoneWeights bool,
 	meshedHTTP2ClientParams *pb.Http2ClientParams,
 	service string,
@@ -115,6 +117,7 @@ func newEndpointTranslator(
 		defaultOpaquePorts,
 		enableH2Upgrade,
 		enableEndpointFiltering,
+		enableIPv6,
 		extEndpointZoneWeights,
 		meshedHTTP2ClientParams,
 
@@ -240,6 +243,7 @@ func (et *endpointTranslator) noEndpoints(exists bool) {
 
 func (et *endpointTranslator) sendFilteredUpdate() {
 	filtered := et.filterAddresses()
+	filtered = et.selectAddressFamily(filtered)
 	diffAdd, diffRemove := et.diffEndpoints(filtered)
 
 	if len(diffAdd.Addresses) > 0 {
@@ -252,6 +256,33 @@ func (et *endpointTranslator) sendFilteredUpdate() {
 	et.filteredSnapshot = filtered
 }
 
+func (et *endpointTranslator) selectAddressFamily(addresses watcher.AddressSet) watcher.AddressSet {
+	filtered := make(map[watcher.ID]watcher.Address)
+	for id, addr := range addresses.Addresses {
+		if id.IPFamily == corev1.IPv6Protocol && !et.enableIPv6 {
+			continue
+		}
+
+		if id.IPFamily == corev1.IPv4Protocol && et.enableIPv6 {
+			// Only consider IPv4 address for which there's not already an IPv6
+			// alternative
+			altID := id
+			altID.IPFamily = corev1.IPv6Protocol
+			if _, ok := addresses.Addresses[altID]; ok {
+				continue
+			}
+		}
+
+		filtered[id] = addr
+	}
+
+	return watcher.AddressSet{
+		Addresses:          filtered,
+		Labels:             addresses.Labels,
+		LocalTrafficPolicy: addresses.LocalTrafficPolicy,
+	}
+}
+
 // filterAddresses is responsible for filtering endpoints based on the node's
 // topology zone. The client will only receive endpoints with the same
 // consumption zone as the node. An endpoints consumption zone is set
@@ -686,12 +717,12 @@ func getInboundPort(podSpec *corev1.PodSpec) (uint32, error) {
 			if envVar.Name != envInboundListenAddr {
 				continue
 			}
-			addr := strings.Split(envVar.Value, ":")
-			port, err := strconv.ParseUint(addr[1], 10, 32)
+			addrPort, err := netip.ParseAddrPort(envVar.Value)
 			if err != nil {
 				return 0, fmt.Errorf("failed to parse inbound port for proxy container: %w", err)
 			}
-			return uint32(port), nil
+
+			return uint32(addrPort.Port()), nil
 		}
 	}
 	return 0, fmt.Errorf("failed to find %s environment variable in any container for given pod spec", envInboundListenAddr)
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 93a5f1299..bde9ff7ba 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -32,6 +32,7 @@ type (
 
 		EnableH2Upgrade,
 		EnableEndpointSlices,
+		EnableIPv6,
 		ExtEndpointZoneWeights bool
 
 		MeshedHttp2ClientParams *pb.Http2ClientParams
@@ -188,6 +189,7 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 			remoteConfig.TrustDomain,
 			s.config.EnableH2Upgrade,
 			false, // Disable endpoint filtering for remote discovery.
+			s.config.EnableIPv6,
 			s.config.ExtEndpointZoneWeights,
 			s.config.MeshedHttp2ClientParams,
 			fmt.Sprintf("%s.%s.svc.%s:%d", remoteSvc, service.Namespace, remoteConfig.ClusterDomain, port),
@@ -220,6 +222,7 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 			s.config.IdentityTrustDomain,
 			s.config.EnableH2Upgrade,
 			true,
+			s.config.EnableIPv6,
 			s.config.ExtEndpointZoneWeights,
 			s.config.MeshedHttp2ClientParams,
 			dest.GetPath(),
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index e2be89ef7..b4e9dba00 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -60,6 +60,12 @@ type (
 	}
 
 	// AddressSet is a set of Address, indexed by ID.
+	// The ID can be either:
+	// 1) A reference to service: id.Name contains both the service name and
+	// the target IP and port (see newServiceRefAddress)
+	// 2) A reference to a pod: id.Name refers to the pod's name, and
+	// id.IPFamily refers to the ES AddressType (see newPodRefAddress).
+	// 3) A reference to an ExternalWorkload: id.Name refers to the EW's name.
 	AddressSet struct {
 		Addresses          map[ID]Address
 		Labels             map[string]string
@@ -365,7 +371,7 @@ func (ew *EndpointsWatcher) addEndpoints(obj interface{}) {
 		return
 	}
 
-	id := ServiceID{endpoints.Namespace, endpoints.Name}
+	id := ServiceID{Namespace: endpoints.Namespace, Name: endpoints.Name}
 	sp := ew.getOrNewServicePublisher(id)
 	sp.updateEndpoints(endpoints)
 }
@@ -389,7 +395,7 @@ func (ew *EndpointsWatcher) updateEndpoints(oldObj interface{}, newObj interface
 		endpointsInformerLag.Observe(delta.Seconds())
 	}
 
-	id := ServiceID{newEndpoints.Namespace, newEndpoints.Name}
+	id := ServiceID{Namespace: newEndpoints.Namespace, Name: newEndpoints.Name}
 	sp := ew.getOrNewServicePublisher(id)
 	sp.updateEndpoints(newEndpoints)
 }
@@ -601,7 +607,8 @@ func (sp *servicePublisher) deleteEndpoints() {
 func (sp *servicePublisher) addEndpointSlice(newSlice *discovery.EndpointSlice) {
 	sp.Lock()
 	defer sp.Unlock()
-	sp.log.Debugf("Adding EndpointSlice for %s", sp.id)
+
+	sp.log.Debugf("Adding ES %s/%s", newSlice.Namespace, newSlice.Name)
 	for _, port := range sp.ports {
 		port.addEndpointSlice(newSlice)
 	}
@@ -610,7 +617,8 @@ func (sp *servicePublisher) addEndpointSlice(newSlice *discovery.EndpointSlice)
 func (sp *servicePublisher) updateEndpointSlice(oldSlice *discovery.EndpointSlice, newSlice *discovery.EndpointSlice) {
 	sp.Lock()
 	defer sp.Unlock()
-	sp.log.Debugf("Updating EndpointSlice for %s", sp.id)
+
+	sp.log.Debugf("Updating ES %s/%s", oldSlice.Namespace, oldSlice.Name)
 	for _, port := range sp.ports {
 		port.updateEndpointSlice(oldSlice, newSlice)
 	}
@@ -619,7 +627,8 @@ func (sp *servicePublisher) updateEndpointSlice(oldSlice *discovery.EndpointSlic
 func (sp *servicePublisher) deleteEndpointSlice(es *discovery.EndpointSlice) {
 	sp.Lock()
 	defer sp.Unlock()
-	sp.log.Debugf("Deleting EndpointSlice for %s", sp.id)
+
+	sp.log.Debugf("Deleting ES %s/%s", es.Namespace, es.Name)
 	for _, port := range sp.ports {
 		port.deleteEndpointSlice(es)
 	}
@@ -926,7 +935,13 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 
 		if endpoint.TargetRef.Kind == endpointTargetRefPod {
 			for _, IPAddr := range endpoint.Addresses {
-				address, id, err := pp.newPodRefAddress(resolvedPort, IPAddr, endpoint.TargetRef.Name, endpoint.TargetRef.Namespace)
+				address, id, err := pp.newPodRefAddress(
+					resolvedPort,
+					es.AddressType,
+					IPAddr,
+					endpoint.TargetRef.Name,
+					endpoint.TargetRef.Namespace,
+				)
 				if err != nil {
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
@@ -1023,6 +1038,7 @@ func (pp *portPublisher) endpointSliceToIDs(es *discovery.EndpointSlice) []ID {
 			ids = append(ids, PodID{
 				Name:      endpoint.TargetRef.Name,
 				Namespace: endpoint.TargetRef.Namespace,
+				IPFamily:  corev1.IPFamily(es.AddressType),
 			})
 		} else if endpoint.TargetRef.Kind == endpointTargetRefExternalWorkload {
 			ids = append(ids, ExternalWorkloadID{
@@ -1062,7 +1078,13 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 			}
 
 			if endpoint.TargetRef.Kind == endpointTargetRefPod {
-				address, id, err := pp.newPodRefAddress(resolvedPort, endpoint.IP, endpoint.TargetRef.Name, endpoint.TargetRef.Namespace)
+				address, id, err := pp.newPodRefAddress(
+					resolvedPort,
+					"",
+					endpoint.IP,
+					endpoint.TargetRef.Name,
+					endpoint.TargetRef.Namespace,
+				)
 				if err != nil {
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
@@ -1095,10 +1117,17 @@ func (pp *portPublisher) newServiceRefAddress(endpointPort Port, endpointIP, ser
 	return Address{IP: endpointIP, Port: endpointPort}, id
 }
 
-func (pp *portPublisher) newPodRefAddress(endpointPort Port, endpointIP, podName, podNamespace string) (Address, PodID, error) {
+func (pp *portPublisher) newPodRefAddress(
+	endpointPort Port,
+	ipFamily discovery.AddressType,
+	endpointIP,
+	podName,
+	podNamespace string,
+) (Address, PodID, error) {
 	id := PodID{
 		Name:      podName,
 		Namespace: podNamespace,
+		IPFamily:  corev1.IPFamily(ipFamily),
 	}
 	pod, err := pp.k8sAPI.Pod().Lister().Pods(id.Namespace).Get(id.Name)
 	if err != nil {
@@ -1462,12 +1491,12 @@ func getEndpointSliceServiceID(es *discovery.EndpointSlice) (ServiceID, error) {
 	}
 
 	if svc, ok := es.Labels[discovery.LabelServiceName]; ok {
-		return ServiceID{es.Namespace, svc}, nil
+		return ServiceID{Namespace: es.Namespace, Name: svc}, nil
 	}
 
 	for _, ref := range es.OwnerReferences {
 		if ref.Kind == "Service" && ref.Name != "" {
-			return ServiceID{es.Namespace, ref.Name}, nil
+			return ServiceID{Namespace: es.Namespace, Name: ref.Name}, nil
 		}
 	}
 
diff --git a/controller/api/destination/watcher/k8s.go b/controller/api/destination/watcher/k8s.go
index 06a1a57a2..e802ef079 100644
--- a/controller/api/destination/watcher/k8s.go
+++ b/controller/api/destination/watcher/k8s.go
@@ -33,6 +33,9 @@ type (
 	ID struct {
 		Namespace string
 		Name      string
+
+		// Only used for PodID
+		IPFamily corev1.IPFamily
 	}
 	// ServiceID is the namespace-qualified name of a service.
 	ServiceID = ID
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 2363b4cad..5532ee8ae 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -36,6 +36,8 @@ func Main(args []string) {
 		"Enable transparently upgraded HTTP2 connections among pods in the service mesh")
 	enableEndpointSlices := cmd.Bool("enable-endpoint-slices", true,
 		"Enable the usage of EndpointSlice informers and resources")
+	enableIPv6 := cmd.Bool("enable-ipv6", true,
+		"Set to true to allow discovering IPv6 endpoints and preferring IPv6 when both IPv4 and IPv6 are available")
 	trustDomain := cmd.String("identity-trust-domain", "", "configures the name suffix used for identities")
 	clusterDomain := cmd.String("cluster-domain", "", "kubernetes cluster domain")
 	defaultOpaquePorts := cmd.String("default-opaque-ports", "", "configures the default opaque ports")
@@ -62,6 +64,10 @@ func Main(args []string) {
 
 	flags.ConfigureAndParse(cmd, args)
 
+	if *enableIPv6 && !*enableEndpointSlices {
+		log.Fatal("If --enable-ipv6=true then --enable-endpoint-slices needs to be true")
+	}
+
 	var meshedHTTP2ClientParams *pb.Http2ClientParams
 	if meshedHTTP2ClientParamsJSON != nil && *meshedHTTP2ClientParamsJSON != "" {
 		meshedHTTP2ClientParams = &pb.Http2ClientParams{}
@@ -167,6 +173,7 @@ func Main(args []string) {
 		DefaultOpaquePorts:      opaquePorts,
 		EnableH2Upgrade:         *enableH2Upgrade,
 		EnableEndpointSlices:    *enableEndpointSlices,
+		EnableIPv6:              *enableIPv6,
 		ExtEndpointZoneWeights:  *extEndpointZoneWeights,
 		MeshedHttp2ClientParams: meshedHTTP2ClientParams,
 	}
diff --git a/controller/gen/apis/server/v1beta1/doc.go b/controller/gen/apis/server/v1beta1/doc.go
new file mode 100644
index 000000000..ffd023fe6
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/doc.go
@@ -0,0 +1,3 @@
+// +k8s:deepcopy-gen=package
+
+package v1beta1
diff --git a/controller/gen/apis/server/v1beta1/register.go b/controller/gen/apis/server/v1beta1/register.go
new file mode 100644
index 000000000..175a7ce81
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/register.go
@@ -0,0 +1,49 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/linkerd/linkerd2/controller/gen/apis/server"
+)
+
+var (
+	// SchemeGroupVersion is the identifier for the API which includes the name
+	// of the group and the version of the API.
+	SchemeGroupVersion = schema.GroupVersion{
+		Group:   server.GroupName,
+		Version: "v1beta1",
+	}
+
+	// SchemeBuilder collects functions that add things to a scheme. It's to
+	// allow code to compile without explicitly referencing generated types.
+	// You should declare one in each package that will have generated deep
+	// copy or conversion functions.
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+
+	// AddToScheme applies all the stored functions to the scheme. A non-nil error
+	// indicates that one function failed and the attempt was abandoned.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified
+// GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&Server{},
+		&ServerList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/controller/gen/apis/server/v1beta1/types.go b/controller/gen/apis/server/v1beta1/types.go
new file mode 100644
index 000000000..5b5211deb
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/types.go
@@ -0,0 +1,45 @@
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+// +genclient
+// +genclient:noStatus
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +groupName=policy.linkerd.io
+
+type Server struct {
+	// TypeMeta is the metadata for the resource, like kind and apiversion
+	metav1.TypeMeta `json:",inline"`
+
+	// ObjectMeta contains the metadata for the particular object, including
+	// things like...
+	//  - name
+	//  - namespace
+	//  - self link
+	//  - labels
+	//  - ... etc ...
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// Spec is the custom resource spec
+	Spec ServerSpec `json:"spec"`
+}
+
+// ServerSpec specifies a Server resource.
+type ServerSpec struct {
+	PodSelector   *metav1.LabelSelector `json:"podSelector"`
+	Port          intstr.IntOrString    `json:"port,omitempty"`
+	ProxyProtocol string                `json:"proxyProtocol,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// ServerList is a list of Server resources.
+type ServerList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []Server `json:"items"`
+}
diff --git a/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go b/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..9a6fa4257
--- /dev/null
+++ b/controller/gen/apis/server/v1beta1/zz_generated.deepcopy.go
@@ -0,0 +1,109 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Server) DeepCopyInto(out *Server) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Server.
+func (in *Server) DeepCopy() *Server {
+	if in == nil {
+		return nil
+	}
+	out := new(Server)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Server) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerList) DeepCopyInto(out *ServerList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Server, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerList.
+func (in *ServerList) DeepCopy() *ServerList {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ServerList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServerSpec) DeepCopyInto(out *ServerSpec) {
+	*out = *in
+	if in.PodSelector != nil {
+		in, out := &in.PodSelector, &out.PodSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	out.Port = in.Port
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec.
+func (in *ServerSpec) DeepCopy() *ServerSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ServerSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/controller/gen/apis/server/v1beta2/types.go b/controller/gen/apis/server/v1beta2/types.go
index 44768db32..f27004fd6 100644
--- a/controller/gen/apis/server/v1beta2/types.go
+++ b/controller/gen/apis/server/v1beta2/types.go
@@ -29,8 +29,8 @@ type Server struct {
 
 // ServerSpec specifies a Server resource.
 type ServerSpec struct {
-	PodSelector              *metav1.LabelSelector `json:"podSelector"`
-	ExternalWorkloadSelector *metav1.LabelSelector `json:"externalWorkloadSelector"`
+	PodSelector              *metav1.LabelSelector `json:"podSelector,omitempty"`
+	ExternalWorkloadSelector *metav1.LabelSelector `json:"externalWorkloadSelector,omitempty"`
 	Port                     intstr.IntOrString    `json:"port,omitempty"`
 	ProxyProtocol            string                `json:"proxyProtocol,omitempty"`
 }
diff --git a/controller/gen/client/clientset/versioned/clientset.go b/controller/gen/client/clientset/versioned/clientset.go
index b02da86c7..dad7724bf 100644
--- a/controller/gen/client/clientset/versioned/clientset.go
+++ b/controller/gen/client/clientset/versioned/clientset.go
@@ -26,6 +26,7 @@ import (
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
 	serverv1beta2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta2"
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serviceprofile/v1alpha2"
@@ -40,6 +41,7 @@ type Interface interface {
 	LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface
 	PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface
 	PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface
+	ServerV1beta1() serverv1beta1.ServerV1beta1Interface
 	ServerV1beta2() serverv1beta2.ServerV1beta2Interface
 	ServerauthorizationV1beta1() serverauthorizationv1beta1.ServerauthorizationV1beta1Interface
 	LinkerdV1alpha2() linkerdv1alpha2.LinkerdV1alpha2Interface
@@ -52,6 +54,7 @@ type Clientset struct {
 	linkV1alpha1               *linkv1alpha1.LinkV1alpha1Client
 	policyV1alpha1             *policyv1alpha1.PolicyV1alpha1Client
 	policyV1beta3              *policyv1beta3.PolicyV1beta3Client
+	serverV1beta1              *serverv1beta1.ServerV1beta1Client
 	serverV1beta2              *serverv1beta2.ServerV1beta2Client
 	serverauthorizationV1beta1 *serverauthorizationv1beta1.ServerauthorizationV1beta1Client
 	linkerdV1alpha2            *linkerdv1alpha2.LinkerdV1alpha2Client
@@ -77,6 +80,11 @@ func (c *Clientset) PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface {
 	return c.policyV1beta3
 }
 
+// ServerV1beta1 retrieves the ServerV1beta1Client
+func (c *Clientset) ServerV1beta1() serverv1beta1.ServerV1beta1Interface {
+	return c.serverV1beta1
+}
+
 // ServerV1beta2 retrieves the ServerV1beta2Client
 func (c *Clientset) ServerV1beta2() serverv1beta2.ServerV1beta2Interface {
 	return c.serverV1beta2
@@ -152,6 +160,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
 	if err != nil {
 		return nil, err
 	}
+	cs.serverV1beta1, err = serverv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
 	cs.serverV1beta2, err = serverv1beta2.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
@@ -189,6 +201,7 @@ func New(c rest.Interface) *Clientset {
 	cs.linkV1alpha1 = linkv1alpha1.New(c)
 	cs.policyV1alpha1 = policyv1alpha1.New(c)
 	cs.policyV1beta3 = policyv1beta3.New(c)
+	cs.serverV1beta1 = serverv1beta1.New(c)
 	cs.serverV1beta2 = serverv1beta2.New(c)
 	cs.serverauthorizationV1beta1 = serverauthorizationv1beta1.New(c)
 	cs.linkerdV1alpha2 = linkerdv1alpha2.New(c)
diff --git a/controller/gen/client/clientset/versioned/fake/clientset_generated.go b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
index 72a6c2637..07b93877a 100644
--- a/controller/gen/client/clientset/versioned/fake/clientset_generated.go
+++ b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
@@ -28,6 +28,8 @@ import (
 	fakepolicyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1/fake"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3"
 	fakepolicyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3/fake"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
+	fakeserverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake"
 	serverv1beta2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta2"
 	fakeserverv1beta2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta2/fake"
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/serverauthorization/v1beta1"
@@ -111,6 +113,11 @@ func (c *Clientset) PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface {
 	return &fakepolicyv1beta3.FakePolicyV1beta3{Fake: &c.Fake}
 }
 
+// ServerV1beta1 retrieves the ServerV1beta1Client
+func (c *Clientset) ServerV1beta1() serverv1beta1.ServerV1beta1Interface {
+	return &fakeserverv1beta1.FakeServerV1beta1{Fake: &c.Fake}
+}
+
 // ServerV1beta2 retrieves the ServerV1beta2Client
 func (c *Clientset) ServerV1beta2() serverv1beta2.ServerV1beta2Interface {
 	return &fakeserverv1beta2.FakeServerV1beta2{Fake: &c.Fake}
diff --git a/controller/gen/client/clientset/versioned/fake/register.go b/controller/gen/client/clientset/versioned/fake/register.go
index c4aa0e327..8ebba89fb 100644
--- a/controller/gen/client/clientset/versioned/fake/register.go
+++ b/controller/gen/client/clientset/versioned/fake/register.go
@@ -23,6 +23,7 @@ import (
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
 	serverv1beta2 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -41,6 +42,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 	linkv1alpha1.AddToScheme,
 	policyv1alpha1.AddToScheme,
 	policyv1beta3.AddToScheme,
+	serverv1beta1.AddToScheme,
 	serverv1beta2.AddToScheme,
 	serverauthorizationv1beta1.AddToScheme,
 	linkerdv1alpha2.AddToScheme,
diff --git a/controller/gen/client/clientset/versioned/scheme/register.go b/controller/gen/client/clientset/versioned/scheme/register.go
index 192576aa8..0da750f79 100644
--- a/controller/gen/client/clientset/versioned/scheme/register.go
+++ b/controller/gen/client/clientset/versioned/scheme/register.go
@@ -23,6 +23,7 @@ import (
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
 	serverv1beta2 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	linkerdv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -41,6 +42,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 	linkv1alpha1.AddToScheme,
 	policyv1alpha1.AddToScheme,
 	policyv1beta3.AddToScheme,
+	serverv1beta1.AddToScheme,
 	serverv1beta2.AddToScheme,
 	serverauthorizationv1beta1.AddToScheme,
 	linkerdv1alpha2.AddToScheme,
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go
new file mode 100644
index 000000000..771101956
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1beta1
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go
new file mode 100644
index 000000000..16f443990
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go
new file mode 100644
index 000000000..c9199ab13
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server.go
@@ -0,0 +1,129 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeServers implements ServerInterface
+type FakeServers struct {
+	Fake *FakeServerV1beta1
+	ns   string
+}
+
+var serversResource = v1beta1.SchemeGroupVersion.WithResource("servers")
+
+var serversKind = v1beta1.SchemeGroupVersion.WithKind("Server")
+
+// Get takes name of the server, and returns the corresponding server object, and an error if there is any.
+func (c *FakeServers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(serversResource, c.ns, name), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// List takes label and field selectors, and returns the list of Servers that match those selectors.
+func (c *FakeServers) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(serversResource, serversKind, c.ns, opts), &v1beta1.ServerList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta1.ServerList{ListMeta: obj.(*v1beta1.ServerList).ListMeta}
+	for _, item := range obj.(*v1beta1.ServerList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested servers.
+func (c *FakeServers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(serversResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a server and creates it.  Returns the server's representation of the server, and an error, if there is any.
+func (c *FakeServers) Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(serversResource, c.ns, server), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// Update takes the representation of a server and updates it. Returns the server's representation of the server, and an error, if there is any.
+func (c *FakeServers) Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(serversResource, c.ns, server), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
+
+// Delete takes name of the server and deletes it. Returns an error if one occurs.
+func (c *FakeServers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteActionWithOptions(serversResource, c.ns, name, opts), &v1beta1.Server{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeServers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(serversResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta1.ServerList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched server.
+func (c *FakeServers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(serversResource, c.ns, name, pt, data, subresources...), &v1beta1.Server{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.Server), err
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go
new file mode 100644
index 000000000..ccb82dc2b
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/fake/fake_server_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeServerV1beta1 struct {
+	*testing.Fake
+}
+
+func (c *FakeServerV1beta1) Servers(namespace string) v1beta1.ServerInterface {
+	return &FakeServers{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeServerV1beta1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go
new file mode 100644
index 000000000..386f7bb3c
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+type ServerExpansion interface{}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go
new file mode 100644
index 000000000..0296afa77
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server.go
@@ -0,0 +1,178 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	"time"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	scheme "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// ServersGetter has a method to return a ServerInterface.
+// A group's client should implement this interface.
+type ServersGetter interface {
+	Servers(namespace string) ServerInterface
+}
+
+// ServerInterface has methods to work with Server resources.
+type ServerInterface interface {
+	Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (*v1beta1.Server, error)
+	Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (*v1beta1.Server, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.Server, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.ServerList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error)
+	ServerExpansion
+}
+
+// servers implements ServerInterface
+type servers struct {
+	client rest.Interface
+	ns     string
+}
+
+// newServers returns a Servers
+func newServers(c *ServerV1beta1Client, namespace string) *servers {
+	return &servers{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the server, and returns the corresponding server object, and an error if there is any.
+func (c *servers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of Servers that match those selectors.
+func (c *servers) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ServerList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta1.ServerList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested servers.
+func (c *servers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a server and creates it.  Returns the server's representation of the server, and an error, if there is any.
+func (c *servers) Create(ctx context.Context, server *v1beta1.Server, opts v1.CreateOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(server).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a server and updates it. Returns the server's representation of the server, and an error, if there is any.
+func (c *servers) Update(ctx context.Context, server *v1beta1.Server, opts v1.UpdateOptions) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(server.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(server).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the server and deletes it. Returns an error if one occurs.
+func (c *servers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *servers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("servers").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched server.
+func (c *servers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Server, err error) {
+	result = &v1beta1.Server{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("servers").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go
new file mode 100644
index 000000000..b23b1848f
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/server/v1beta1/server_client.go
@@ -0,0 +1,107 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"net/http"
+
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ServerV1beta1Interface interface {
+	RESTClient() rest.Interface
+	ServersGetter
+}
+
+// ServerV1beta1Client is used to interact with features provided by the server group.
+type ServerV1beta1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ServerV1beta1Client) Servers(namespace string) ServerInterface {
+	return newServers(c, namespace)
+}
+
+// NewForConfig creates a new ServerV1beta1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*ServerV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new ServerV1beta1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ServerV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &ServerV1beta1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ServerV1beta1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ServerV1beta1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ServerV1beta1Client for the given RESTClient.
+func New(c rest.Interface) *ServerV1beta1Client {
+	return &ServerV1beta1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ServerV1beta1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/controller/gen/client/informers/externalversions/generic.go b/controller/gen/client/informers/externalversions/generic.go
index d49fbc0fe..0a5bff745 100644
--- a/controller/gen/client/informers/externalversions/generic.go
+++ b/controller/gen/client/informers/externalversions/generic.go
@@ -25,6 +25,7 @@ import (
 	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	v1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
 	v1beta2 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -84,6 +85,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1beta3.SchemeGroupVersion.WithResource("httproutes"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Policy().V1beta3().HTTPRoutes().Informer()}, nil
 
+		// Group=server, Version=v1beta1
+	case serverv1beta1.SchemeGroupVersion.WithResource("servers"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Server().V1beta1().Servers().Informer()}, nil
+
 		// Group=server, Version=v1beta2
 	case v1beta2.SchemeGroupVersion.WithResource("servers"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Server().V1beta2().Servers().Informer()}, nil
diff --git a/controller/gen/client/informers/externalversions/server/interface.go b/controller/gen/client/informers/externalversions/server/interface.go
index 1d0bc3547..9e78a70b0 100644
--- a/controller/gen/client/informers/externalversions/server/interface.go
+++ b/controller/gen/client/informers/externalversions/server/interface.go
@@ -20,11 +20,14 @@ package server
 
 import (
 	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta1"
 	v1beta2 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta2"
 )
 
 // Interface provides access to each of this group's versions.
 type Interface interface {
+	// V1beta1 provides access to shared informers for resources in V1beta1.
+	V1beta1() v1beta1.Interface
 	// V1beta2 provides access to shared informers for resources in V1beta2.
 	V1beta2() v1beta2.Interface
 }
@@ -40,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// V1beta1 returns a new v1beta1.Interface.
+func (g *group) V1beta1() v1beta1.Interface {
+	return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
+}
+
 // V1beta2 returns a new v1beta2.Interface.
 func (g *group) V1beta2() v1beta2.Interface {
 	return v1beta2.New(g.factory, g.namespace, g.tweakListOptions)
diff --git a/controller/gen/client/informers/externalversions/server/v1beta1/interface.go b/controller/gen/client/informers/externalversions/server/v1beta1/interface.go
new file mode 100644
index 000000000..717142453
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/server/v1beta1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// Servers returns a ServerInformer.
+	Servers() ServerInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// Servers returns a ServerInformer.
+func (v *version) Servers() ServerInformer {
+	return &serverInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/controller/gen/client/informers/externalversions/server/v1beta1/server.go b/controller/gen/client/informers/externalversions/server/v1beta1/server.go
new file mode 100644
index 000000000..4267426a4
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/server/v1beta1/server.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	time "time"
+
+	serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/listers/server/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// ServerInformer provides access to a shared informer and lister for
+// Servers.
+type ServerInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta1.ServerLister
+}
+
+type serverInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewServerInformer constructs a new informer for Server type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewServerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredServerInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredServerInformer constructs a new informer for Server type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredServerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerV1beta1().Servers(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ServerV1beta1().Servers(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&serverv1beta1.Server{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *serverInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredServerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *serverInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&serverv1beta1.Server{}, f.defaultInformer)
+}
+
+func (f *serverInformer) Lister() v1beta1.ServerLister {
+	return v1beta1.NewServerLister(f.Informer().GetIndexer())
+}
diff --git a/controller/gen/client/listers/server/v1beta1/expansion_generated.go b/controller/gen/client/listers/server/v1beta1/expansion_generated.go
new file mode 100644
index 000000000..195815706
--- /dev/null
+++ b/controller/gen/client/listers/server/v1beta1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+// ServerListerExpansion allows custom methods to be added to
+// ServerLister.
+type ServerListerExpansion interface{}
+
+// ServerNamespaceListerExpansion allows custom methods to be added to
+// ServerNamespaceLister.
+type ServerNamespaceListerExpansion interface{}
diff --git a/controller/gen/client/listers/server/v1beta1/server.go b/controller/gen/client/listers/server/v1beta1/server.go
new file mode 100644
index 000000000..c2486f96b
--- /dev/null
+++ b/controller/gen/client/listers/server/v1beta1/server.go
@@ -0,0 +1,99 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// ServerLister helps list Servers.
+// All objects returned here must be treated as read-only.
+type ServerLister interface {
+	// List lists all Servers in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.Server, err error)
+	// Servers returns an object that can list and get Servers.
+	Servers(namespace string) ServerNamespaceLister
+	ServerListerExpansion
+}
+
+// serverLister implements the ServerLister interface.
+type serverLister struct {
+	indexer cache.Indexer
+}
+
+// NewServerLister returns a new ServerLister.
+func NewServerLister(indexer cache.Indexer) ServerLister {
+	return &serverLister{indexer: indexer}
+}
+
+// List lists all Servers in the indexer.
+func (s *serverLister) List(selector labels.Selector) (ret []*v1beta1.Server, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.Server))
+	})
+	return ret, err
+}
+
+// Servers returns an object that can list and get Servers.
+func (s *serverLister) Servers(namespace string) ServerNamespaceLister {
+	return serverNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// ServerNamespaceLister helps list and get Servers.
+// All objects returned here must be treated as read-only.
+type ServerNamespaceLister interface {
+	// List lists all Servers in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1beta1.Server, err error)
+	// Get retrieves the Server from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1beta1.Server, error)
+	ServerNamespaceListerExpansion
+}
+
+// serverNamespaceLister implements the ServerNamespaceLister
+// interface.
+type serverNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all Servers in the indexer for a given namespace.
+func (s serverNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.Server, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.Server))
+	})
+	return ret, err
+}
+
+// Get retrieves the Server from the indexer for a given namespace and name.
+func (s serverNamespaceLister) Get(name string) (*v1beta1.Server, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta1.Resource("server"), name)
+	}
+	return obj.(*v1beta1.Server), nil
+}
diff --git a/controller/proxy-injector/fake/data/pod-with-debug.patch.json b/controller/proxy-injector/fake/data/pod-with-debug.patch.json
index 1b6055815..d6aca4db6 100644
--- a/controller/proxy-injector/fake/data/pod-with-debug.patch.json
+++ b/controller/proxy-injector/fake/data/pod-with-debug.patch.json
@@ -239,19 +239,23 @@
         },
         {
           "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
-          "value": "0.0.0.0:4190"
+          "value": "[::]:4190"
         },
         {
           "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
-          "value": "0.0.0.0:4191"
+          "value": "[::]:4191"
         },
         {
           "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
           "value": "127.0.0.1:4140"
         },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS",
+          "value": "127.0.0.1:4140,[::1]:4140"
+        },
         {
           "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
-          "value": "0.0.0.0:4143"
+          "value": "[::]:4143"
         },
         {
           "name": "LINKERD2_PROXY_INBOUND_IPS",
diff --git a/controller/proxy-injector/fake/data/pod-with-ns-annotations.patch.json b/controller/proxy-injector/fake/data/pod-with-ns-annotations.patch.json
index 588504e0d..e7b457094 100644
--- a/controller/proxy-injector/fake/data/pod-with-ns-annotations.patch.json
+++ b/controller/proxy-injector/fake/data/pod-with-ns-annotations.patch.json
@@ -225,19 +225,23 @@
         },
         {
           "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
-          "value": "0.0.0.0:4190"
+          "value": "[::]:4190"
         },
         {
           "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
-          "value": "0.0.0.0:4191"
+          "value": "[::]:4191"
         },
         {
           "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
           "value": "127.0.0.1:4140"
         },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS",
+          "value": "127.0.0.1:4140,[::1]:4140"
+        },
         {
           "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
-          "value": "0.0.0.0:4143"
+          "value": "[::]:4143"
         },
         {
           "name": "LINKERD2_PROXY_INBOUND_IPS",
diff --git a/controller/proxy-injector/fake/data/pod.patch.json b/controller/proxy-injector/fake/data/pod.patch.json
index 6e0eb540c..86df83760 100644
--- a/controller/proxy-injector/fake/data/pod.patch.json
+++ b/controller/proxy-injector/fake/data/pod.patch.json
@@ -215,19 +215,23 @@
         },
         {
           "name": "LINKERD2_PROXY_CONTROL_LISTEN_ADDR",
-          "value": "0.0.0.0:4190"
+          "value": "[::]:4190"
         },
         {
           "name": "LINKERD2_PROXY_ADMIN_LISTEN_ADDR",
-          "value": "0.0.0.0:4191"
+          "value": "[::]:4191"
         },
         {
           "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR",
           "value": "127.0.0.1:4140"
         },
+        {
+          "name": "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS",
+          "value": "127.0.0.1:4140,[::1]:4140"
+        },
         {
           "name": "LINKERD2_PROXY_INBOUND_LISTEN_ADDR",
-          "value": "0.0.0.0:4143"
+          "value": "[::]:4143"
         },
         {
           "name": "LINKERD2_PROXY_INBOUND_IPS",
diff --git a/controller/proxy-injector/webhook.go b/controller/proxy-injector/webhook.go
index e2cb147a2..5c757c533 100644
--- a/controller/proxy-injector/webhook.go
+++ b/controller/proxy-injector/webhook.go
@@ -105,7 +105,7 @@ func Inject(linkerdNamespace string) webhook.Handler {
 
 			// If the pod did not inherit the opaque ports annotation from the
 			// namespace, then add the default value from the config values. This
-			// ensures that the generated patch always sets the opaue ports
+			// ensures that the generated patch always sets the opaque ports
 			// annotation.
 			if !resourceConfig.HasWorkloadAnnotation(pkgK8s.ProxyOpaquePortsAnnotation) {
 				defaultPorts := strings.Split(resourceConfig.GetValues().Proxy.OpaquePorts, ",")
diff --git a/go.mod b/go.mod
index f7b815140..ece6ab34b 100644
--- a/go.mod
+++ b/go.mod
@@ -39,7 +39,7 @@ require (
 	golang.org/x/tools v0.20.0
 	google.golang.org/grpc v1.63.2
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
-	google.golang.org/protobuf v1.33.0
+	google.golang.org/protobuf v1.34.0
 	gopkg.in/yaml.v2 v2.4.0
 	helm.sh/helm/v3 v3.14.4
 	k8s.io/api v0.30.0
diff --git a/go.sum b/go.sum
index 815f1f72f..804afd416 100644
--- a/go.sum
+++ b/go.sum
@@ -723,8 +723,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/jaeger/injector/mutator/patch.go b/jaeger/injector/mutator/patch.go
index 01f2b8835..e78251797 100644
--- a/jaeger/injector/mutator/patch.go
+++ b/jaeger/injector/mutator/patch.go
@@ -44,13 +44,13 @@ const tpl = `[
     "value": {
        "downwardAPI": {
          "items": [
-     {
-             "fieldRef": {
-               "fieldPath": "metadata.labels"
-       },
-             "path": "labels"
-     }
-   ]
+            {
+              "fieldRef": {
+                "fieldPath": "metadata.labels"
+              },
+              "path": "labels"
+            }
+          ]
        },
        "name": "linkerd-podinfo"
      }
diff --git a/multicluster/values/values.go b/multicluster/values/values.go
index db97532a0..52168a99a 100644
--- a/multicluster/values/values.go
+++ b/multicluster/values/values.go
@@ -8,6 +8,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/chartutil"
+	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/yaml"
 )
 
@@ -37,6 +38,9 @@ type Values struct {
 	TargetClusterName              string   `json:"targetClusterName"`
 	EnablePodAntiAffinity          bool     `json:"enablePodAntiAffinity"`
 	RevisionHistoryLimit           uint32   `json:"revisionHistoryLimit"`
+
+	ServiceMirrorAdditionalEnv   []corev1.EnvVar `json:"serviceMirrorAdditionalEnv"`
+	ServiceMirrorExperimentalEnv []corev1.EnvVar `json:"serviceMirrorExperimentalEnv"`
 }
 
 // Gateway contains all options related to the Gateway Service
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index fa9d4a5f0..a89f8f187 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -60,6 +60,8 @@ type (
 		RevisionHistoryLimit         uint                   `json:"revisionHistoryLimit"`
 
 		DestinationController map[string]interface{} `json:"destinationController"`
+		Heartbeat             map[string]interface{} `json:"heartbeat"`
+		SPValidator           map[string]interface{} `json:"spValidator"`
 
 		PodAnnotations    map[string]string `json:"podAnnotations"`
 		PodLabels         map[string]string `json:"podLabels"`
@@ -72,7 +74,7 @@ type (
 		NetworkValidator *NetworkValidator `json:"networkValidator"`
 		Identity         *Identity         `json:"identity"`
 		DebugContainer   *DebugContainer   `json:"debugContainer"`
-		ProxyInjector    *Webhook          `json:"proxyInjector"`
+		ProxyInjector    *ProxyInjector    `json:"proxyInjector"`
 		ProfileValidator *Webhook          `json:"profileValidator"`
 		PolicyValidator  *Webhook          `json:"policyValidator"`
 		NodeSelector     map[string]string `json:"nodeSelector"`
@@ -285,6 +287,9 @@ type (
 		ServiceAccountTokenProjection bool     `json:"serviceAccountTokenProjection"`
 		Issuer                        *Issuer  `json:"issuer"`
 		KubeAPI                       *KubeAPI `json:"kubeAPI"`
+
+		AdditionalEnv   []corev1.EnvVar `json:"additionalEnv"`
+		ExperimentalEnv []corev1.EnvVar `json:"experimentalEnv"`
 	}
 
 	// Issuer has the Helm variables of the identity issuer
@@ -301,6 +306,13 @@ type (
 		ClientBurst int     `json:"clientBurst"`
 	}
 
+	// ProxyInjector configures the proxy-injector webhook
+	ProxyInjector struct {
+		Webhook
+		AdditionalEnv   []corev1.EnvVar `json:"additionalEnv"`
+		ExperimentalEnv []corev1.EnvVar `json:"experimentalEnv"`
+	}
+
 	// Webhook Helm variables for a webhook
 	Webhook struct {
 		*TLS
diff --git a/policy-controller/k8s/status/src/index.rs b/policy-controller/k8s/status/src/index.rs
index 841731c3a..8b45d5c05 100644
--- a/policy-controller/k8s/status/src/index.rs
+++ b/policy-controller/k8s/status/src/index.rs
@@ -107,7 +107,7 @@ impl ControllerMetrics {
     pub fn register(prom: &mut Registry) -> Self {
         let patch_succeeded = Counter::default();
         prom.register(
-            "patchs",
+            "patches",
             "Count of successful patch operations",
             patch_succeeded.clone(),
         );


Test patch:
diff --git a/cli/cmd/testdata/inject-filepath/expected/injected_nginx.yaml b/cli/cmd/testdata/inject-filepath/expected/injected_nginx.yaml
index eaf2029f3..c40bbf9c9 100644
--- a/cli/cmd/testdata/inject-filepath/expected/injected_nginx.yaml
+++ b/cli/cmd/testdata/inject-filepath/expected/injected_nginx.yaml
@@ -64,13 +64,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject-filepath/expected/injected_nginx_redis.yaml b/cli/cmd/testdata/inject-filepath/expected/injected_nginx_redis.yaml
index 28ade44b2..5fa0ce5c2 100644
--- a/cli/cmd/testdata/inject-filepath/expected/injected_nginx_redis.yaml
+++ b/cli/cmd/testdata/inject-filepath/expected/injected_nginx_redis.yaml
@@ -64,13 +64,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -295,13 +297,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject-filepath/expected/injected_redis.yaml b/cli/cmd/testdata/inject-filepath/expected/injected_redis.yaml
index 9f6ad099f..a6edda66a 100644
--- a/cli/cmd/testdata/inject-filepath/expected/injected_redis.yaml
+++ b/cli/cmd/testdata/inject-filepath/expected/injected_redis.yaml
@@ -64,13 +64,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_contour.golden.yml b/cli/cmd/testdata/inject_contour.golden.yml
index 38f7ff4d2..337dbe693 100644
--- a/cli/cmd/testdata/inject_contour.golden.yml
+++ b/cli/cmd/testdata/inject_contour.golden.yml
@@ -72,13 +72,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_already_injected.golden.yml b/cli/cmd/testdata/inject_emojivoto_already_injected.golden.yml
index 52d13ae89..75fc456b2 100644
--- a/cli/cmd/testdata/inject_emojivoto_already_injected.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_already_injected.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -308,13 +310,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -550,13 +554,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -792,13 +798,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment.golden.yml
index 61aafe767..204bfe8bb 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_access_log.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_access_log.golden.yml
index 194a958ff..1db8a08a1 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_access_log.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_access_log.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml
index 274b14638..c493d21e6 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_capabilities.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_capabilities.golden.yml
index d208fbe05..50b828cdd 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_capabilities.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_capabilities.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_config_overrides.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_config_overrides.golden.yml
index c91e73f63..40dcfe997 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_config_overrides.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_config_overrides.golden.yml
@@ -76,13 +76,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:9998
+          value: '[::]:9998'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_controller_name.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_controller_name.golden.yml
index 3e57b11d6..10eec0d80 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_controller_name.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_controller_name.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -308,13 +310,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_debug.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_debug.golden.yml
index c5e06ce6e..292209049 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_debug.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_debug.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_empty_resources.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_empty_resources.golden.yml
index fa8e983c4..5152ed61e 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_empty_resources.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_empty_resources.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_hostNetwork_false.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_hostNetwork_false.golden.yml
index a48476a77..2a06c1af6 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_hostNetwork_false.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_hostNetwork_false.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_native_sidecar.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_native_sidecar.golden.yml
index f45574cc6..08b4fa500 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_native_sidecar.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_native_sidecar.golden.yml
@@ -119,13 +119,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_no_init_container.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_no_init_container.golden.yml
index c7dd17b8b..d38d02b39 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_no_init_container.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_no_init_container.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml
index bd5337853..fbd85a5e2 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_overridden.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_overridden.golden.yml
index be4fdc0e7..df20a48ac 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_overridden.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_overridden.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:1234
+          value: '[::]:1234'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_params.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_params.golden.yml
index 26501b96a..cfd442c2d 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_params.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_params.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_proxyignores.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_proxyignores.golden.yml
index ef09c6b7b..c8f47cda8 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_proxyignores.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_proxyignores.golden.yml
@@ -68,13 +68,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_udp.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_udp.golden.yml
index 6d8f007d7..f80a1a459 100644
--- a/cli/cmd/testdata/inject_emojivoto_deployment_udp.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_udp.golden.yml
@@ -66,13 +66,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_list.golden.yml b/cli/cmd/testdata/inject_emojivoto_list.golden.yml
index ce9beadc5..75ba9360c 100644
--- a/cli/cmd/testdata/inject_emojivoto_list.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_list.golden.yml
@@ -68,13 +68,15 @@ items:
           - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
             value: 90s
           - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-            value: 0.0.0.0:4190
+            value: '[::]:4190'
           - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-            value: 0.0.0.0:4191
+            value: '[::]:4191'
           - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
             value: 127.0.0.1:4140
+          - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+            value: 127.0.0.1:4140,[::1]:4140
           - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-            value: 0.0.0.0:4143
+            value: '[::]:4143'
           - name: LINKERD2_PROXY_INBOUND_IPS
             valueFrom:
               fieldRef:
@@ -309,13 +311,15 @@ items:
           - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
             value: 90s
           - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-            value: 0.0.0.0:4190
+            value: '[::]:4190'
           - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-            value: 0.0.0.0:4191
+            value: '[::]:4191'
           - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
             value: 127.0.0.1:4140
+          - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+            value: 127.0.0.1:4140,[::1]:4140
           - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-            value: 0.0.0.0:4143
+            value: '[::]:4143'
           - name: LINKERD2_PROXY_INBOUND_IPS
             valueFrom:
               fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_list_empty_resources.golden.yml b/cli/cmd/testdata/inject_emojivoto_list_empty_resources.golden.yml
index b271f2d78..b5e3be0d7 100644
--- a/cli/cmd/testdata/inject_emojivoto_list_empty_resources.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_list_empty_resources.golden.yml
@@ -68,13 +68,15 @@ items:
           - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
             value: 90s
           - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-            value: 0.0.0.0:4190
+            value: '[::]:4190'
           - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-            value: 0.0.0.0:4191
+            value: '[::]:4191'
           - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
             value: 127.0.0.1:4140
+          - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+            value: 127.0.0.1:4140,[::1]:4140
           - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-            value: 0.0.0.0:4143
+            value: '[::]:4143'
           - name: LINKERD2_PROXY_INBOUND_IPS
             valueFrom:
               fieldRef:
@@ -309,13 +311,15 @@ items:
           - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
             value: 90s
           - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-            value: 0.0.0.0:4190
+            value: '[::]:4190'
           - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-            value: 0.0.0.0:4191
+            value: '[::]:4191'
           - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
             value: 127.0.0.1:4140
+          - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+            value: 127.0.0.1:4140,[::1]:4140
           - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-            value: 0.0.0.0:4143
+            value: '[::]:4143'
           - name: LINKERD2_PROXY_INBOUND_IPS
             valueFrom:
               fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_pod.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
index b3bb60358..92b337576 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod.golden.yml
@@ -58,13 +58,15 @@ spec:
     - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
       value: 90s
     - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-      value: 0.0.0.0:4190
+      value: '[::]:4190'
     - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-      value: 0.0.0.0:4191
+      value: '[::]:4191'
     - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
       value: 127.0.0.1:4140
+    - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+      value: 127.0.0.1:4140,[::1]:4140
     - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-      value: 0.0.0.0:4143
+      value: '[::]:4143'
     - name: LINKERD2_PROXY_INBOUND_IPS
       valueFrom:
         fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
index 914a3c140..4a4141227 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_ingress.golden.yml
@@ -59,13 +59,15 @@ spec:
     - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
       value: 90s
     - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-      value: 0.0.0.0:4190
+      value: '[::]:4190'
     - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-      value: 0.0.0.0:4191
+      value: '[::]:4191'
     - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
       value: 127.0.0.1:4140
+    - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+      value: 127.0.0.1:4140,[::1]:4140
     - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-      value: 0.0.0.0:4143
+      value: '[::]:4143'
     - name: LINKERD2_PROXY_INBOUND_IPS
       valueFrom:
         fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
index 288bc26c9..80a1b7e15 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_proxyignores.golden.yml
@@ -60,13 +60,15 @@ spec:
     - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
       value: 90s
     - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-      value: 0.0.0.0:4190
+      value: '[::]:4190'
     - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-      value: 0.0.0.0:4191
+      value: '[::]:4191'
     - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
       value: 127.0.0.1:4140
+    - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+      value: 127.0.0.1:4140,[::1]:4140
     - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-      value: 0.0.0.0:4143
+      value: '[::]:4143'
     - name: LINKERD2_PROXY_INBOUND_IPS
       valueFrom:
         fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml b/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
index f1d4a3c50..06ab98f47 100644
--- a/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_pod_with_requests.golden.yml
@@ -62,13 +62,15 @@ spec:
     - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
       value: 90s
     - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-      value: 0.0.0.0:4190
+      value: '[::]:4190'
     - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-      value: 0.0.0.0:4191
+      value: '[::]:4191'
     - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
       value: 127.0.0.1:4140
+    - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+      value: 127.0.0.1:4140,[::1]:4140
     - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-      value: 0.0.0.0:4143
+      value: '[::]:4143'
     - name: LINKERD2_PROXY_INBOUND_IPS
       valueFrom:
         fieldRef:
diff --git a/cli/cmd/testdata/inject_emojivoto_statefulset.golden.yml b/cli/cmd/testdata/inject_emojivoto_statefulset.golden.yml
index cc9e0f773..784cac25a 100644
--- a/cli/cmd/testdata/inject_emojivoto_statefulset.golden.yml
+++ b/cli/cmd/testdata/inject_emojivoto_statefulset.golden.yml
@@ -67,13 +67,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_gettest_deployment.good.golden.yml b/cli/cmd/testdata/inject_gettest_deployment.good.golden.yml
index b1f3ba380..fdb03e607 100644
--- a/cli/cmd/testdata/inject_gettest_deployment.good.golden.yml
+++ b/cli/cmd/testdata/inject_gettest_deployment.good.golden.yml
@@ -62,13 +62,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -306,13 +308,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/inject_tap_deployment.input.yml b/cli/cmd/testdata/inject_tap_deployment.input.yml
index beb6513b8..2ba52fee2 100644
--- a/cli/cmd/testdata/inject_tap_deployment.input.yml
+++ b/cli/cmd/testdata/inject_tap_deployment.input.yml
@@ -93,13 +93,13 @@ spec:
         - name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS
           value: 10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
-        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "[::]:4191"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
           value: svc.cluster.local.
         - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
diff --git a/cli/cmd/testdata/inject_tap_deployment_debug.golden.yml b/cli/cmd/testdata/inject_tap_deployment_debug.golden.yml
index 60fd45fbc..c3e133d2f 100644
--- a/cli/cmd/testdata/inject_tap_deployment_debug.golden.yml
+++ b/cli/cmd/testdata/inject_tap_deployment_debug.golden.yml
@@ -83,13 +83,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: 90s
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: '[::]:4190'
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: '[::]:4191'
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
           value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: 127.0.0.1:4140,[::1]:4140
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: '[::]:4143'
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 813292918..3491927a3 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1007,13 +1013,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1352,13 +1360,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1466,6 +1476,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         - -trace-collector=collector.linkerd-jaeger.svc.cluster.local:55678
@@ -1798,13 +1809,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 1c004630c..1554d1ae3 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 0571ac9e6..e23b71d84 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: my.custom.registry/linkerd-io/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 1c004630c..1554d1ae3 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index b8ff1014d..e2565a2a0 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index b6145ead8..fe1138bed 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1342,13 +1350,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1454,6 +1464,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1778,13 +1789,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index f97f23a25..2e5eb9d48 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -539,6 +539,7 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: true
     enablePodDisruptionBudget: true
+    heartbeat: null
     heartbeatResources:
       cpu:
         limit: ""
@@ -552,6 +553,8 @@ data:
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: true
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -779,8 +782,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -807,6 +812,7 @@ data:
         limit: 250Mi
         request: 50Mi
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Fail
 ---
@@ -1083,13 +1089,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1468,13 +1476,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1587,6 +1597,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1949,13 +1960,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index ad907dbbf..2ff16790d 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -539,6 +539,7 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: true
     enablePodDisruptionBudget: true
+    heartbeat: null
     heartbeatResources:
       cpu:
         limit: ""
@@ -552,6 +553,8 @@ data:
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: true
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -779,8 +782,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -807,6 +812,7 @@ data:
         limit: 250Mi
         request: 50Mi
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Fail
 ---
@@ -1083,13 +1089,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1468,13 +1476,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1587,6 +1597,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1949,13 +1960,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index d7923f0bb..e1eb7409e 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -461,10 +461,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -683,8 +686,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -702,6 +707,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -937,13 +943,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1282,13 +1290,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1396,6 +1406,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1667,13 +1678,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index 687d2090f..923039b72 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -531,10 +531,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -729,8 +732,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: test-proxy-injector-ca-bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -748,6 +753,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tap:
       caBundle: test-tap-ca-bundle
       externalSecret: true
@@ -979,13 +985,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1326,13 +1334,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1440,6 +1450,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:linkerd-version
@@ -1775,13 +1786,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
index bb722a1ab..59c93ac12 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -540,6 +540,7 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: true
     enablePodDisruptionBudget: true
+    heartbeat: null
     heartbeatResources:
       cpu:
         limit: ""
@@ -553,6 +554,8 @@ data:
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: true
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -756,8 +759,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: test-proxy-injector-ca-bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -784,6 +789,7 @@ data:
         limit: 250Mi
         request: 50Mi
     revisionHistoryLimit: 10
+    spValidator: null
     tap:
       caBundle: test-tap-ca-bundle
       externalSecret: true
@@ -1056,13 +1062,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1443,13 +1451,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1562,6 +1572,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:linkerd-version
@@ -1928,13 +1939,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 79aea9d55..499f31d6d 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -540,6 +540,7 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: true
     enablePodDisruptionBudget: true
+    heartbeat: null
     heartbeatResources:
       cpu:
         limit: ""
@@ -553,6 +554,8 @@ data:
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: true
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -760,8 +763,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: test-proxy-injector-ca-bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -788,6 +793,7 @@ data:
         limit: 250Mi
         request: 50Mi
     revisionHistoryLimit: 10
+    spValidator: null
     tap:
       caBundle: test-tap-ca-bundle
       externalSecret: true
@@ -1064,13 +1070,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1455,13 +1463,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1574,6 +1584,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:linkerd-version
@@ -1948,13 +1959,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index 360f16fb2..9a21dad6c 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -535,6 +535,7 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: true
     enablePodDisruptionBudget: true
+    heartbeat: null
     heartbeatResources:
       cpu:
         limit: ""
@@ -548,6 +549,8 @@ data:
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: true
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -751,8 +754,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: test-proxy-injector-ca-bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -774,6 +779,7 @@ data:
         limit: 250Mi
         request: 50Mi
     revisionHistoryLimit: 10
+    spValidator: null
     tap:
       caBundle: test-tap-ca-bundle
       externalSecret: true
@@ -1046,13 +1052,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1433,13 +1441,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1552,6 +1562,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:linkerd-version
@@ -1918,13 +1929,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index ff0e6abe2..79644bcd9 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1345,13 +1353,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1459,6 +1469,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1784,13 +1795,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 454fdf917..87ae7fed2 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -514,10 +514,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -705,8 +708,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -724,6 +729,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: WebhookFailurePolicy
 ---
@@ -951,13 +957,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1285,13 +1293,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1397,6 +1407,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,443,587,3306,5432,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         image: ControllerImage:LinkerdVersion
         imagePullPolicy: ImagePullPolicy
@@ -1725,13 +1736,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index f88f3faa5..e1336c069 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 210b58d40..bc703722a 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -530,10 +530,13 @@ data:
     enableH2Upgrade: true
     enablePodAntiAffinity: false
     enablePodDisruptionBudget: false
+    heartbeat: null
     heartbeatResources: null
     heartbeatSchedule: 1 2 3 4 5
     highAvailability: false
     identity:
+      additionalEnv: null
+      experimentalEnv: null
       externalCA: false
       issuer:
         clockSkewAllowance: 20s
@@ -752,8 +755,10 @@ data:
         name: linkerd-proxy-init-xtables-lock
         readOnly: false
     proxyInjector:
+      additionalEnv: null
       caBundle: proxy injector CA bundle
       crtPEM: ""
+      experimentalEnv: null
       externalSecret: true
       injectCaFrom: ""
       injectCaFromSecret: ""
@@ -771,6 +776,7 @@ data:
     proxyInjectorProxyResources: null
     proxyInjectorResources: null
     revisionHistoryLimit: 10
+    spValidator: null
     tolerations: null
     webhookFailurePolicy: Ignore
 ---
@@ -1006,13 +1012,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1351,13 +1359,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
@@ -1465,6 +1475,7 @@ spec:
         - -cluster-domain=example.com
         - -identity-trust-domain=example.com
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
+        - -enable-ipv6=true
         - -enable-pprof=false
         - --meshed-http2-client-params={"keep_alive":{"interval":{"seconds":10},"timeout":{"seconds":3},"while_idle":true}}
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
@@ -1796,13 +1807,15 @@ spec:
         - name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT
           value: "90s"
         - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
-          value: 0.0.0.0:4190
+          value: "[::]:4190"
         - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
-          value: 0.0.0.0:4191
+          value: "[::]:4191"
         - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
-          value: 127.0.0.1:4140
+          value: "127.0.0.1:4140"
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS
+          value: "127.0.0.1:4140,[::1]:4140"
         - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
-          value: 0.0.0.0:4143
+          value: "[::]:4143"
         - name: LINKERD2_PROXY_INBOUND_IPS
           valueFrom:
             fieldRef:
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 8b6d2bbad..4b8f856a2 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -2,6 +2,7 @@ package destination
 
 import (
 	"fmt"
+	"net/netip"
 	"sort"
 	"strings"
 	"sync"
@@ -41,6 +42,26 @@ var (
 		OwnerName: "rc-name",
 	}
 
+	pod1IPv6 = watcher.Address{
+		IP:   "2001:0db8:85a3:0000:0000:8a2e:0370:7333",
+		Port: 1,
+		Pod: &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "pod1",
+				Namespace: "ns",
+				Labels: map[string]string{
+					k8s.ControllerNSLabel:    "linkerd",
+					k8s.ProxyDeploymentLabel: "deployment-name",
+				},
+			},
+			Spec: corev1.PodSpec{
+				ServiceAccountName: "serviceaccount-name",
+			},
+		},
+		OwnerKind: "replicationcontroller",
+		OwnerName: "rc-name",
+	}
+
 	pod2 = watcher.Address{
 		IP:   "1.1.1.2",
 		Port: 2,
@@ -400,8 +421,8 @@ func TestEndpointTranslatorForPods(t *testing.T) {
 		translator.Start()
 		defer translator.Stop()
 
-		translator.Add(mkAddressSetForPods(pod1, pod2))
-		translator.Remove(mkAddressSetForPods(pod2))
+		translator.Add(mkAddressSetForPods(t, pod1, pod2))
+		translator.Remove(mkAddressSetForPods(t, pod2))
 
 		expectedNumUpdates := 2
 		<-mockGetServer.updatesReceived // Add
@@ -417,8 +438,8 @@ func TestEndpointTranslatorForPods(t *testing.T) {
 		translator.Start()
 		defer translator.Stop()
 
-		translator.Add(mkAddressSetForPods(pod1, pod2, pod3))
-		translator.Remove(mkAddressSetForPods(pod3))
+		translator.Add(mkAddressSetForPods(t, pod1, pod2, pod3))
+		translator.Remove(mkAddressSetForPods(t, pod3))
 
 		addressesAdded := (<-mockGetServer.updatesReceived).GetAdd().Addrs
 		actualNumberOfAdded := len(addressesAdded)
@@ -447,7 +468,7 @@ func TestEndpointTranslatorForPods(t *testing.T) {
 		translator.Start()
 		defer translator.Stop()
 
-		translator.Add(mkAddressSetForPods(pod1))
+		translator.Add(mkAddressSetForPods(t, pod1))
 
 		update := <-mockGetServer.updatesReceived
 
@@ -479,7 +500,7 @@ func TestEndpointTranslatorForPods(t *testing.T) {
 		translator.Start()
 		defer translator.Stop()
 
-		translator.Add(mkAddressSetForPods(pod1))
+		translator.Add(mkAddressSetForPods(t, pod1))
 
 		addrs := (<-mockGetServer.updatesReceived).GetAdd().GetAddrs()
 		if len(addrs) != 1 {
@@ -518,6 +539,56 @@ func TestEndpointTranslatorForPods(t *testing.T) {
 			t.Fatalf("ProtocolHint: %v", diff)
 		}
 	})
+
+	t.Run("Sends IPv6 only when pod has both IPv4 and IPv6", func(t *testing.T) {
+		mockGetServer, translator := makeEndpointTranslator(t)
+		translator.Start()
+		defer translator.Stop()
+
+		translator.Add(mkAddressSetForPods(t, pod1, pod1IPv6))
+
+		addrs := (<-mockGetServer.updatesReceived).GetAdd().GetAddrs()
+		if len(addrs) != 1 {
+			t.Fatalf("Expected [1] address returned, got %v", addrs)
+		}
+		if ipPort := addr.ProxyAddressToString(addrs[0].GetAddr()); ipPort != "[2001:db8:85a3::8a2e:370:7333]:1" {
+			t.Fatalf("Expected address to be [%s], got [%s]", "[2001:db8:85a3::8a2e:370:7333]:1", ipPort)
+		}
+
+		if updates := len(mockGetServer.updatesReceived); updates > 0 {
+			t.Fatalf("Expected to receive no more messages, received [%d]", updates)
+		}
+	})
+
+	t.Run("Sends IPv4 only when pod has both IPv4 and IPv6 but the latter in another zone ", func(t *testing.T) {
+		mockGetServer, translator := makeEndpointTranslator(t)
+		translator.Start()
+		defer translator.Stop()
+
+		pod1West1a := pod1
+		pod1West1a.ForZones = []v1.ForZone{
+			{Name: "west-1a"},
+		}
+
+		pod1IPv6West1b := pod1IPv6
+		pod1IPv6West1b.ForZones = []v1.ForZone{
+			{Name: "west-1b"},
+		}
+
+		translator.Add(mkAddressSetForPods(t, pod1West1a, pod1IPv6West1b))
+
+		addrs := (<-mockGetServer.updatesReceived).GetAdd().GetAddrs()
+		if len(addrs) != 1 {
+			t.Fatalf("Expected [1] address returned, got %v", addrs)
+		}
+		if ipPort := addr.ProxyAddressToString(addrs[0].GetAddr()); ipPort != "1.1.1.1:1" {
+			t.Fatalf("Expected address to be [%s], got [%s]", "1.1.1.1:1", ipPort)
+		}
+
+		if updates := len(mockGetServer.updatesReceived); updates > 0 {
+			t.Fatalf("Expected to receive no more messages, received [%d]", updates)
+		}
+	})
 }
 
 func TestEndpointTranslatorExternalWorkloads(t *testing.T) {
@@ -790,6 +861,39 @@ func TestConcurrency(t *testing.T) {
 	wg.Wait()
 }
 
+func TestGetInboundPort(t *testing.T) {
+	podSpec := &corev1.PodSpec{
+		Containers: []corev1.Container{
+			{
+				Name: k8s.ProxyContainerName,
+				Env: []corev1.EnvVar{
+					{
+						Name:  envInboundListenAddr,
+						Value: "1.2.3.4:8080",
+					},
+				},
+			},
+		},
+	}
+
+	port, err := getInboundPort(podSpec)
+	if err != nil {
+		t.Fatalf("Unexpected error: %s", err)
+	}
+	if port != 8080 {
+		t.Fatalf("Expecting port [%d], got [%d]", 8080, port)
+	}
+
+	podSpec.Containers[0].Env[0].Value = "[2001:db8::94]:8080"
+	port, err = getInboundPort(podSpec)
+	if err != nil {
+		t.Fatalf("Unexpected error: %s", err)
+	}
+	if port != 8080 {
+		t.Fatalf("Expecting port [%d], got [%d]", 8080, port)
+	}
+}
+
 func mkAddressSetForServices(gatewayAddresses ...watcher.Address) watcher.AddressSet {
 	set := watcher.AddressSet{
 		Addresses: make(map[watcher.ServiceID]watcher.Address),
@@ -809,13 +913,30 @@ func mkAddressSetForServices(gatewayAddresses ...watcher.Address) watcher.Addres
 	return set
 }
 
-func mkAddressSetForPods(podAddresses ...watcher.Address) watcher.AddressSet {
+func mkAddressSetForPods(t *testing.T, podAddresses ...watcher.Address) watcher.AddressSet {
+	t.Helper()
+
 	set := watcher.AddressSet{
 		Addresses: make(map[watcher.PodID]watcher.Address),
 		Labels:    map[string]string{"service": "service-name", "namespace": "service-ns"},
 	}
 	for _, p := range podAddresses {
-		id := watcher.PodID{Name: p.Pod.Name, Namespace: p.Pod.Namespace}
+		// The IP family is set on the PodID used to index the
+		// watcher.Address; here we simply detect it
+		fam := corev1.IPv4Protocol
+		addr, err := netip.ParseAddr(p.IP)
+		if err != nil {
+			t.Fatalf("Invalid IP '%s': %s", p.IP, err)
+		}
+		if addr.Is6() {
+			fam = corev1.IPv6Protocol
+		}
+
+		id := watcher.PodID{
+			Name:      p.Pod.Name,
+			Namespace: p.Pod.Namespace,
+			IPFamily:  fam,
+		}
 		set.Addresses[id] = p
 	}
 	return set
diff --git a/controller/api/destination/server_ipv6_test.go b/controller/api/destination/server_ipv6_test.go
new file mode 100644
index 000000000..0f058352f
--- /dev/null
+++ b/controller/api/destination/server_ipv6_test.go
@@ -0,0 +1,179 @@
+package destination
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
+	"github.com/linkerd/linkerd2/controller/api/util"
+	corev1 "k8s.io/api/core/v1"
+	discovery "k8s.io/api/discovery/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestIPv6(t *testing.T) {
+	port := int32(port)
+	protocol := corev1.ProtocolTCP
+
+	server := makeServer(t)
+	defer server.clusterStore.UnregisterGauges()
+
+	stream := &bufferingGetStream{
+		updates:          make(chan *pb.Update, 50),
+		MockServerStream: util.NewMockServerStream(),
+	}
+	defer stream.Cancel()
+
+	t.Run("Return only IPv6 endpoint for dual-stack service", func(t *testing.T) {
+		testReturnEndpointsForServer(t, server, stream, fullyQualifiedNameDual, podIPv6Dual, uint32(port))
+	})
+
+	t.Run("Returns only IPv4 endpoint when service becomes single-stack IPv4", func(t *testing.T) {
+		patch := []byte(`{"spec":{"clusterIPs": ["172.17.13.0"], "ipFamilies":["IPv4"]}}`)
+		_, err := server.k8sAPI.Client.CoreV1().Services("ns").Patch(context.Background(), "name-ds", types.MergePatchType, patch, metav1.PatchOptions{})
+		if err != nil {
+			t.Fatalf("Failed patching name-ds service: %s", err)
+		}
+		if err = server.k8sAPI.Client.DiscoveryV1().EndpointSlices("ns").Delete(context.Background(), "name-ds-ipv6", metav1.DeleteOptions{}); err != nil {
+			t.Fatalf("Failed deleting name-ds-ipv6 ES: %s", err)
+		}
+
+		update := <-stream.updates
+		if updateAddAddress(t, update)[0] != "172.17.0.19:8989" {
+			t.Fatalf("Expected %s but got %s", "172.17.0.19:8989", updateAddAddress(t, update)[0])
+		}
+
+		update = <-stream.updates
+		if updateRemoveAddress(t, update)[0] != "[2001:db8::94]:8989" {
+			t.Fatalf("Expected %s but got %s", "[2001:db8::94]:8989", updateRemoveAddress(t, update)[0])
+		}
+	})
+
+	t.Run("Returns only IPv6 endpoint when service becomes dual-stack again", func(t *testing.T) {
+		// We patch the service to become dual-stack again and we add the IPv6
+		// ES. We should receive the events for the removal of the IPv4 ES and
+		// the addition of the IPv6 one.
+		patch := []byte(`{"spec":{"clusterIPs": ["172.17.13.0","2001:db8::88"], "ipFamilies":["IPv4","IPv6"]}}`)
+		_, err := server.k8sAPI.Client.CoreV1().Services("ns").Patch(context.Background(), "name-ds", types.MergePatchType, patch, metav1.PatchOptions{})
+		if err != nil {
+			t.Fatalf("Failed patching name-ds service: %s", err)
+		}
+
+		es := &discovery.EndpointSlice{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "EndpointSlice",
+				APIVersion: "discovery.k8s.io/v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "name-ds-ipv6",
+				Namespace: "ns",
+				Labels: map[string]string{
+					"kubernetes.io/service-name": "name-ds",
+				},
+			},
+			AddressType: discovery.AddressTypeIPv6,
+			Ports: []discovery.EndpointPort{
+				{
+					Port:     &port,
+					Protocol: &protocol,
+				},
+			},
+			Endpoints: []discovery.Endpoint{
+				{
+					Addresses: []string{"2001:db8::94"},
+					TargetRef: &corev1.ObjectReference{
+						Kind:      "Pod",
+						Namespace: "ns",
+						Name:      "name-ds",
+					},
+				},
+			},
+		}
+		if _, err := server.k8sAPI.Client.DiscoveryV1().EndpointSlices("ns").Create(context.Background(), es, metav1.CreateOptions{}); err != nil {
+			t.Fatalf("Failed creating name-ds-ipv6 ES: %s", err)
+		}
+
+		update := <-stream.updates
+		if updateAddAddress(t, update)[0] != "[2001:db8::94]:8989" {
+			t.Fatalf("Expected %s but got %s", "[2001:db8::94]:8989", updateAddAddress(t, update)[0])
+		}
+
+		update = <-stream.updates
+		if updateRemoveAddress(t, update)[0] != "172.17.0.19:8989" {
+			t.Fatalf("Expected %s but got %s", "172.17.0.19:8989", updateRemoveAddress(t, update)[0])
+		}
+	})
+
+	t.Run("Doesn't return anything when adding an IPv4 to the dual-stack service", func(t *testing.T) {
+		es := &discovery.EndpointSlice{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "EndpointSlice",
+				APIVersion: "discovery.k8s.io/v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "name-ds-ipv4-2",
+				Namespace: "ns",
+				Labels: map[string]string{
+					"kubernetes.io/service-name": "name-ds",
+				},
+			},
+			AddressType: discovery.AddressTypeIPv4,
+			Ports: []discovery.EndpointPort{
+				{
+					Port:     &port,
+					Protocol: &protocol,
+				},
+			},
+			Endpoints: []discovery.Endpoint{
+				{
+					Addresses: []string{"172.17.0.20"},
+					TargetRef: &corev1.ObjectReference{
+						Kind:      "Pod",
+						Namespace: "ns",
+						Name:      "name-ds",
+					},
+				},
+			},
+		}
+		if _, err := server.k8sAPI.Client.DiscoveryV1().EndpointSlices("ns").Create(context.Background(), es, metav1.CreateOptions{}); err != nil {
+			t.Fatalf("Failed creating name-ds-ipv4-2 ES: %s", err)
+		}
+
+		time.Sleep(50 * time.Millisecond)
+
+		if len(stream.updates) != 0 {
+			t.Fatalf("Expected no events but got %#v", stream.updates)
+		}
+	})
+
+	t.Run("Doesn't return anything when removing an IPv4 ES from the dual-stack service", func(t *testing.T) {
+		if err := server.k8sAPI.Client.DiscoveryV1().EndpointSlices("ns").Delete(context.Background(), "name-ds-ipv4-2", metav1.DeleteOptions{}); err != nil {
+			t.Fatalf("Failed deleting name-ds-ipv4-2 ES: %s", err)
+		}
+
+		time.Sleep(50 * time.Millisecond)
+
+		if len(stream.updates) != 0 {
+			t.Fatalf("Expected no events but got %#v", stream.updates)
+		}
+	})
+
+	t.Run("Doesn't return anything when the service becomes single-stack IPv6", func(t *testing.T) {
+		patch := []byte(`{"spec":{"clusterIPs": ["2001:db8::88"], "ipFamilies":["IPv6"]}}`)
+		_, err := server.k8sAPI.Client.CoreV1().Services("ns").Patch(context.Background(), "name-ds", types.MergePatchType, patch, metav1.PatchOptions{})
+		if err != nil {
+			t.Fatalf("Failed patching name-ds service: %s", err)
+		}
+		if err := server.k8sAPI.Client.DiscoveryV1().EndpointSlices("ns").Delete(context.Background(), "name-ds-ipv4", metav1.DeleteOptions{}); err != nil {
+			t.Fatalf("Failed deleting name-ds-ipv4 ES: %s", err)
+		}
+
+		time.Sleep(50 * time.Millisecond)
+
+		if len(stream.updates) != 0 {
+			t.Fatalf("Expected no events but got %#v", stream.updates)
+		}
+	})
+}
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index 8f9687f2a..e7a1c8d66 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	gonet "net"
+	"net/netip"
 	"reflect"
 	"testing"
 	"time"
@@ -27,6 +28,8 @@ import (
 )
 
 const fullyQualifiedName = "name1.ns.svc.mycluster.local"
+const fullyQualifiedNameIPv6 = "name-ipv6.ns.svc.mycluster.local"
+const fullyQualifiedNameDual = "name-ds.ns.svc.mycluster.local"
 const fullyQualifiedNameOpaque = "name3.ns.svc.mycluster.local"
 const fullyQualifiedNameOpaqueService = "name4.ns.svc.mycluster.local"
 const fullyQualifiedNameSkipped = "name5.ns.svc.mycluster.local"
@@ -36,6 +39,7 @@ const clusterIPv6 = "2001:db8::88"
 const clusterIPOpaque = "172.17.12.1"
 const podIP1 = "172.17.0.12"
 const podIP1v6 = "2001:db8::68"
+const podIPv6Dual = "2001:db8::94"
 const podIP2 = "172.17.0.13"
 const podIPOpaque = "172.17.0.14"
 const podIPSkipped = "172.17.0.15"
@@ -82,38 +86,16 @@ func TestGet(t *testing.T) {
 		}
 	})
 
-	t.Run("Returns endpoints", func(t *testing.T) {
-		server := makeServer(t)
-		defer server.clusterStore.UnregisterGauges()
-
-		stream := &bufferingGetStream{
-			updates:          make(chan *pb.Update, 50),
-			MockServerStream: util.NewMockServerStream(),
-		}
-		defer stream.Cancel()
-		errs := make(chan error)
-
-		// server.Get blocks until the grpc stream is complete so we call it
-		// in a goroutine and watch stream.updates for updates.
-		go func() {
-			err := server.Get(&pb.GetDestination{Scheme: "k8s", Path: fmt.Sprintf("%s:%d", fullyQualifiedName, port)}, stream)
-			if err != nil {
-				errs <- err
-			}
-		}()
+	t.Run("Returns endpoints (IPv4)", func(t *testing.T) {
+		testReturnEndpoints(t, fullyQualifiedName, podIP1, port)
+	})
 
-		select {
-		case update := <-stream.updates:
-			if updateAddAddress(t, update)[0] != fmt.Sprintf("%s:%d", podIP1, port) {
-				t.Fatalf("Expected %s but got %s", fmt.Sprintf("%s:%d", podIP1, port), updateAddAddress(t, update)[0])
-			}
+	t.Run("Returns endpoints (IPv6)", func(t *testing.T) {
+		testReturnEndpoints(t, fullyQualifiedNameIPv6, podIP1v6, port)
+	})
 
-			if len(stream.updates) != 0 {
-				t.Fatalf("Expected 1 update but got %d: %v", 1+len(stream.updates), stream.updates)
-			}
-		case err := <-errs:
-			t.Fatalf("Got error: %s", err)
-		}
+	t.Run("Returns endpoints (dual-stack)", func(t *testing.T) {
+		testReturnEndpoints(t, fullyQualifiedNameDual, podIPv6Dual, port)
 	})
 
 	t.Run("Sets meshed HTTP/2 client params", func(t *testing.T) {
@@ -494,7 +476,7 @@ func TestGetProfiles(t *testing.T) {
 		stream := profileStream(t, server, clusterIPv6, port, "")
 		defer stream.Cancel()
 		profile := assertSingleProfile(t, stream.Updates())
-		if profile.FullyQualifiedName != fullyQualifiedName {
+		if profile.FullyQualifiedName != fullyQualifiedNameDual {
 			t.Fatalf("Expected fully qualified name '%s', but got '%s'", fullyQualifiedName, profile.FullyQualifiedName)
 		}
 		if profile.OpaqueProtocol {
@@ -608,10 +590,10 @@ func TestGetProfiles(t *testing.T) {
 		server := makeServer(t)
 		defer server.clusterStore.UnregisterGauges()
 
-		stream := profileStream(t, server, podIP1v6, port, "ns:ns")
+		stream := profileStream(t, server, podIPv6Dual, port, "ns:ns")
 		defer stream.Cancel()
 
-		epAddr, err := toAddress(podIP1v6, port)
+		epAddr, err := toAddress(podIPv6Dual, port)
 		if err != nil {
 			t.Fatalf("Got error: %s", err)
 		}
@@ -1181,6 +1163,19 @@ func updateAddAddress(t *testing.T, update *pb.Update) []string {
 	return ips
 }
 
+func updateRemoveAddress(t *testing.T, update *pb.Update) []string {
+	t.Helper()
+	add, ok := update.GetUpdate().(*pb.Update_Remove)
+	if !ok {
+		t.Fatalf("Update expected to be a remove, but was %+v", update)
+	}
+	ips := []string{}
+	for _, ip := range add.Remove.Addrs {
+		ips = append(ips, addr.ProxyAddressToString(ip))
+	}
+	return ips
+}
+
 func toAddress(path string, port uint32) (*net.TcpAddress, error) {
 	ip, err := addr.ParseProxyIP(path)
 	if err != nil {
@@ -1196,7 +1191,6 @@ func TestIpWatcherGetSvcID(t *testing.T) {
 	name := "service"
 	namespace := "test"
 	clusterIP := "10.245.0.1"
-	clusterIPv6 := "2001:db8::68"
 	k8sConfigs := `
 apiVersion: v1
 kind: Service
@@ -1208,7 +1202,7 @@ spec:
   clusterIP: 10.245.0.1
   clusterIPs:
   - 10.245.0.1
-  - 2001:db8::68
+  - 2001:db8::88
   ports:
   - port: 1234`
 
@@ -1264,6 +1258,57 @@ spec:
 	})
 }
 
+func testReturnEndpoints(t *testing.T, fqdn, ip string, port uint32) {
+	t.Helper()
+
+	server := makeServer(t)
+	defer server.clusterStore.UnregisterGauges()
+
+	stream := &bufferingGetStream{
+		updates:          make(chan *pb.Update, 50),
+		MockServerStream: util.NewMockServerStream(),
+	}
+	defer stream.Cancel()
+
+	testReturnEndpointsForServer(t, server, stream, fqdn, ip, port)
+}
+
+func testReturnEndpointsForServer(t *testing.T, server *server, stream *bufferingGetStream, fqdn, ip string, port uint32) {
+	t.Helper()
+
+	errs := make(chan error)
+	// server.Get blocks until the grpc stream is complete so we call it
+	// in a goroutine and watch stream.updates for updates.
+	go func() {
+		err := server.Get(&pb.GetDestination{Scheme: "k8s", Path: fmt.Sprintf("%s:%d", fqdn, port)}, stream)
+		if err != nil {
+			errs <- err
+		}
+	}()
+
+	addr := fmt.Sprintf("%s:%d", ip, port)
+	parsedIP, err := netip.ParseAddr(ip)
+	if err != nil {
+		t.Fatalf("Invalid IP [%s]: %s", ip, err)
+	}
+	if parsedIP.Is6() {
+		addr = fmt.Sprintf("[%s]:%d", ip, port)
+	}
+
+	select {
+	case update := <-stream.updates:
+		if updateAddAddress(t, update)[0] != addr {
+			t.Fatalf("Expected %s but got %s", addr, updateAddAddress(t, update)[0])
+		}
+
+		if len(stream.updates) != 0 {
+			t.Fatalf("Expected 1 update but got %d: %v", 1+len(stream.updates), stream.updates)
+		}
+	case err := <-errs:
+		t.Fatalf("Got error: %s", err)
+	}
+}
+
 func assertSingleProfile(t *testing.T, updates []*pb.DestinationProfile) *pb.DestinationProfile {
 	t.Helper()
 	// Under normal conditions the creation of resources by the fake API will
diff --git a/controller/api/destination/test_util.go b/controller/api/destination/test_util.go
index 246aaee19..041232d40 100644
--- a/controller/api/destination/test_util.go
+++ b/controller/api/destination/test_util.go
@@ -32,10 +32,11 @@ metadata:
   namespace: ns
 spec:
   type: LoadBalancer
+  ipFamilies:
+  - IPv4
   clusterIP: 172.17.12.0
   clusterIPs:
   - 172.17.12.0
-  - 2001:db8::88
   ports:
   - port: 8989`,
 		`
@@ -55,25 +56,6 @@ endpoints:
     name: name1-1
     namespace: ns
 ports:
-- port: 8989
-  protocol: TCP`,
-		`
-apiVersion: discovery.k8s.io/v1
-kind: EndpointSlice
-metadata:
-  name: name1-ipv6
-  namespace: ns
-  labels:
-    kubernetes.io/service-name: name1
-addressType: IPv6
-endpoints:
-- addresses:
-  - 2001:db8::90
-  targetRef:
-    kind: Pod
-    name: name1-1
-    namespace: ns
-ports:
 - port: 8989
   protocol: TCP`,
 		`
@@ -92,7 +74,6 @@ status:
   podIP: 172.17.0.12
   podIPs:
   - ip: 172.17.0.12
-  - ip: 2001:db8::68
 spec:
   containers:
     - env:
@@ -752,7 +733,8 @@ ports:
 - port: 80
   protocol: TCP`,
 	}
-	extenalNameResources := []string{
+
+	externalNameResources := []string{
 		`
 apiVersion: v1
 kind: Service
@@ -764,6 +746,158 @@ spec:
   externalName: linkerd.io`,
 	}
 
+	ipv6 := []string{
+		`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-ipv6
+  namespace: ns
+spec:
+  type: ClusterIP
+  ipFamilies:
+  - IPv6
+  clusterIP: 2001:db8::93
+  clusterIPs:
+  - 2001:db8::93
+  ports:
+  - port: 8989`,
+		`
+apiVersion: discovery.k8s.io/v1
+kind: EndpointSlice
+metadata:
+  name: name-ipv6
+  namespace: ns
+  labels:
+    kubernetes.io/service-name: name-ipv6
+addressType: IPv6
+endpoints:
+- addresses:
+  - 2001:db8::68
+  targetRef:
+    kind: Pod
+    name: name-ipv6
+    namespace: ns
+ports:
+- port: 8989
+  protocol: TCP`,
+		`
+apiVersion: v1
+kind: Pod
+metadata:
+  labels:
+    linkerd.io/control-plane-ns: linkerd
+  name: name-ipv6
+  namespace: ns
+status:
+  phase: Running
+  conditions:
+  - type: Ready
+    status: "True"
+  podIP: 2001:db8::68
+  podIPs:
+  - ip: 2001:db8::68
+spec:
+  containers:
+    - env:
+      - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
+        value: 0.0.0.0:4143
+      name: linkerd-proxy`,
+	}
+
+	dualStack := []string{
+		`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-ds
+  namespace: ns
+spec:
+  type: ClusterIP
+  ipFamilies:
+  - IPv4
+  - IPv6
+  clusterIP: 172.17.13.0
+  clusterIPs:
+  - 172.17.13.0
+  - 2001:db8::88
+  ports:
+  - port: 8989`,
+		`
+apiVersion: discovery.k8s.io/v1
+kind: EndpointSlice
+metadata:
+  name: name-ds-ipv4
+  namespace: ns
+  labels:
+    kubernetes.io/service-name: name-ds
+addressType: IPv4
+endpoints:
+- addresses:
+  - 172.17.0.19
+  targetRef:
+    kind: Pod
+    name: name-ds
+    namespace: ns
+ports:
+- port: 8989
+  protocol: TCP`,
+		`
+apiVersion: discovery.k8s.io/v1
+kind: EndpointSlice
+metadata:
+  name: name-ds-ipv6
+  namespace: ns
+  labels:
+    kubernetes.io/service-name: name-ds
+addressType: IPv6
+endpoints:
+- addresses:
+  - 2001:db8::94
+  targetRef:
+    kind: Pod
+    name: name-ds
+    namespace: ns
+ports:
+- port: 8989
+  protocol: TCP`,
+		`
+apiVersion: v1
+kind: Pod
+metadata:
+  labels:
+    linkerd.io/control-plane-ns: linkerd
+  name: name-ds
+  namespace: ns
+status:
+  phase: Running
+  conditions:
+  - type: Ready
+    status: "True"
+  podIP: 172.17.0.19
+  podIPs:
+  - ip: 172.17.0.19
+  - ip: 2001:db8::94
+spec:
+  containers:
+    - env:
+      - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
+        value: 0.0.0.0:4143
+      name: linkerd-proxy`,
+		`
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: name-ds.ns.svc.mycluster.local
+  namespace: ns
+spec:
+  routes:
+  - name: route1
+    isRetryable: false
+    condition:
+      pathRegex: "/a/b/c"`,
+	}
+
 	res := append(meshedPodResources, clientSP...)
 	res = append(res, unmeshedPod)
 	res = append(res, meshedOpaquePodResources...)
@@ -776,7 +910,9 @@ spec:
 	res = append(res, mirrorServiceResources...)
 	res = append(res, destinationCredentialsResources...)
 	res = append(res, externalWorkloads...)
-	res = append(res, extenalNameResources...)
+	res = append(res, externalNameResources...)
+	res = append(res, ipv6...)
+	res = append(res, dualStack...)
 	k8sAPI, l5dClient, err := k8s.NewFakeAPIWithL5dClient(res...)
 	if err != nil {
 		t.Fatalf("NewFakeAPIWithL5dClient returned an error: %s", err)
@@ -833,6 +969,7 @@ spec:
 		pb.UnimplementedDestinationServer{},
 		Config{
 			EnableH2Upgrade:     true,
+			EnableIPv6:          true,
 			ControllerNS:        "linkerd",
 			ClusterDomain:       "mycluster.local",
 			IdentityTrustDomain: "trust.domain",
@@ -928,6 +1065,7 @@ metadata:
 		"linkerd",
 		"trust.domain",
 		true,
+		true,
 		true,  // enableEndpointFiltering
 		false, // extEndpointZoneWeights
 		nil,   // meshedHttp2ClientParams
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index 274f72a9d..3779376af 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -246,7 +246,7 @@ func TestNewValues(t *testing.T) {
 			},
 		},
 
-		ProxyInjector:    &Webhook{TLS: &TLS{}, NamespaceSelector: namespaceSelectorInjector},
+		ProxyInjector:    &ProxyInjector{Webhook: Webhook{TLS: &TLS{}, NamespaceSelector: namespaceSelectorInjector}},
 		ProfileValidator: &Webhook{TLS: &TLS{}, NamespaceSelector: namespaceSelectorSimple},
 		PolicyValidator:  &Webhook{TLS: &TLS{}, NamespaceSelector: namespaceSelectorSimple},
 	}
diff --git a/test/integration/deep/norelay/norelay_test.go b/test/integration/deep/norelay/norelay_test.go
index 194e9b9c2..3ef89de1b 100644
--- a/test/integration/deep/norelay/norelay_test.go
+++ b/test/integration/deep/norelay/norelay_test.go
@@ -3,6 +3,7 @@ package norelay
 import (
 	"context"
 	"fmt"
+	"net"
 	"os"
 	"strings"
 	"testing"
@@ -48,18 +49,10 @@ func TestNoRelay(t *testing.T) {
 			}
 		}
 
-		ip, err := TestHelper.Kubectl(
-			"", "-n", ns, "get", "po", "-l", "app=server-relay",
-			"-o", "jsonpath='{range .items[*]}{@.status.podIP}{end}'",
-		)
-		if err != nil {
-			testutil.AnnotatedFatalf(t, "failed to retrieve server-relay IP",
-				"failed to retrieve server-relay IP: %s\n%s", err, ip)
-		}
-		relayIP := strings.Trim(ip, "'")
+		relayIPPort := getPodIPPort(t, ns, "app=server-relay", 4140)
 		o, err := TestHelper.Kubectl(
 			"", "-n", ns, "exec", "deploy/client",
-			"--", "curl", "-f", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIP+":4140",
+			"--", "curl", "-f", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIPPort,
 		)
 		if err == nil || err.Error() != "exit status 22" {
 			testutil.AnnotatedFatalf(t, "no error or unexpected error returned",
@@ -70,8 +63,9 @@ func TestNoRelay(t *testing.T) {
 
 // TestRelay validates the previous test by running the same scenario but
 // forcing an open relay by changing the value of
-// LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR from 127.0.0.1:4140 to 0.0.0.0:4140,
-// which is not possible without manually changing the injected proxy yaml
+// LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS from 127.0.0.1:4140,[::1]:4140 to
+// 0.0.0.0:4140, which is not possible without manually changing the injected
+// proxy yaml
 //
 // We don't care if this behavior breaks--it's not a supported configuration.
 // However, this test is oddly useful in finding bugs in ingress-mode proxy
@@ -79,7 +73,11 @@ func TestNoRelay(t *testing.T) {
 func TestRelay(t *testing.T) {
 	ctx := context.Background()
 	deployments := getDeployments(t)
-	deployments["server-relay"] = strings.ReplaceAll(deployments["server-relay"], "127.0.0.1:4140", "0.0.0.0:4140")
+	deployments["server-relay"] = strings.ReplaceAll(
+		deployments["server-relay"],
+		"127.0.0.1:4140,[::1]:4140",
+		"'[::]:4140'",
+	)
 	TestHelper.WithDataPlaneNamespace(ctx, "relay-test", map[string]string{}, t, func(t *testing.T, ns string) {
 		for name, res := range deployments {
 			out, err := TestHelper.KubectlApply(res, ns)
@@ -91,12 +89,12 @@ func TestRelay(t *testing.T) {
 			}
 		}
 		waitForPods(t, ctx, ns, deployments)
-		relayIP := getPodIp(t, ns, "app=server-relay")
+		relayIPPort := getPodIPPort(t, ns, "app=server-relay", 4140)
 
 		// Send a request to the outbound proxy port with a header that should route internally.
 		o, err := TestHelper.Kubectl(
 			"", "-n", ns, "exec", "deploy/client",
-			"--", "curl", "-fsv", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIP+":4140",
+			"--", "curl", "-fsv", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIPPort,
 		)
 		if err != nil {
 			log, err := TestHelper.Kubectl(
@@ -148,7 +146,7 @@ func getDeployments(t *testing.T) map[string]string {
 	return deploys
 }
 
-func getPodIp(t *testing.T, ns, selector string) string {
+func getPodIPPort(t *testing.T, ns, selector string, port int) string {
 	t.Helper()
 	ip, err := TestHelper.Kubectl(
 		"", "-n", ns, "get", "po", "-l", selector,
@@ -158,7 +156,16 @@ func getPodIp(t *testing.T, ns, selector string) string {
 		testutil.AnnotatedFatalf(t, "failed to retrieve pod IP",
 			"failed to retrieve pod IP: %s", err)
 	}
-	return strings.Trim(ip, "'")
+	ip = strings.Trim(ip, "'")
+	parsedIP := net.ParseIP(ip)
+	if parsedIP == nil {
+		testutil.AnnotatedFatalf(t, "invalid pod IP",
+			"invalid pod IP: %s", err)
+	}
+	if parsedIP.To4() != nil {
+		return fmt.Sprintf("%s:%d", ip, port)
+	}
+	return fmt.Sprintf("[%s]:%d", ip, port)
 }
 
 func waitForPods(t *testing.T, ctx context.Context, ns string, deployments map[string]string) {
diff --git a/testutil/inject_validator.go b/testutil/inject_validator.go
index 4320f17cd..f76743b09 100644
--- a/testutil/inject_validator.go
+++ b/testutil/inject_validator.go
@@ -106,7 +106,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 	}
 
 	if iv.AdminPort != 0 {
-		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_ADMIN_LISTEN_ADDR", fmt.Sprintf("0.0.0.0:%d", iv.AdminPort)); err != nil {
+		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_ADMIN_LISTEN_ADDR", fmt.Sprintf("[::]:%d", iv.AdminPort)); err != nil {
 			return err
 		}
 		if proxyContainer.LivenessProbe.HTTPGet.Port.IntVal != int32(iv.AdminPort) {
@@ -122,7 +122,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 	}
 
 	if iv.ControlPort != 0 {
-		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_CONTROL_LISTEN_ADDR", fmt.Sprintf("0.0.0.0:%d", iv.ControlPort)); err != nil {
+		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_CONTROL_LISTEN_ADDR", fmt.Sprintf("[::]:%d", iv.ControlPort)); err != nil {
 			return err
 		}
 	}
@@ -140,7 +140,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 	}
 
 	if iv.InboundPort != 0 {
-		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_INBOUND_LISTEN_ADDR", fmt.Sprintf("0.0.0.0:%d", iv.InboundPort)); err != nil {
+		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_INBOUND_LISTEN_ADDR", fmt.Sprintf("[::]:%d", iv.InboundPort)); err != nil {
 			return err
 		}
 		if proxyContainer.LivenessProbe.HTTPGet.Port.IntVal != int32(iv.AdminPort) {
@@ -155,7 +155,11 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 	}
 
 	if iv.OutboundPort != 0 {
-		if err := iv.validateEnvVar(proxyContainer, "LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR", fmt.Sprintf("127.0.0.1:%d", iv.OutboundPort)); err != nil {
+		if err := iv.validateEnvVar(
+			proxyContainer,
+			"LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS",
+			fmt.Sprintf("127.0.0.1:%d,[::1]:%d", iv.OutboundPort, iv.OutboundPort),
+		); err != nil {
 			return err
 		}
 	}
diff --git a/testutil/prommatch/common.go b/testutil/prommatch/common.go
index 717d4fa18..a04c44405 100644
--- a/testutil/prommatch/common.go
+++ b/testutil/prommatch/common.go
@@ -3,16 +3,14 @@ package prommatch
 import "regexp"
 
 var (
-	addressRe = regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+`)
-	iPRe      = regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
-	portRe    = regexp.MustCompile(`\d+`)
+	portRe = regexp.MustCompile(`\d+`)
 )
 
 // TargetAddrLabels match series with proper target_addr, target_port, and target_ip.
 func TargetAddrLabels() Labels {
 	return Labels{
-		"target_addr": Like(addressRe),
-		"target_ip":   Like(iPRe),
+		"target_addr": IsAddr(),
+		"target_ip":   IsIP(),
 		"target_port": Like(portRe),
 	}
 }
diff --git a/testutil/prommatch/prommatch.go b/testutil/prommatch/prommatch.go
index 33487490a..f182d648b 100644
--- a/testutil/prommatch/prommatch.go
+++ b/testutil/prommatch/prommatch.go
@@ -18,6 +18,8 @@ package prommatch
 import (
 	"bytes"
 	"fmt"
+	"net"
+	"net/netip"
 	"regexp"
 
 	dto "github.com/prometheus/client_model/go"
@@ -146,6 +148,24 @@ func HasPositiveValue() Expression {
 	})
 }
 
+// IsAddr is used to check if the value is an IP:port combo, where IP can be
+// an IPv4 or an IPv6
+func IsAddr() LabelMatcher {
+	return func(s string) bool {
+		if _, err := netip.ParseAddrPort(s); err != nil {
+			return false
+		}
+		return true
+	}
+}
+
+// IsIP use used to check if the value is an IPv4 or IPv6
+func IsIP() LabelMatcher {
+	return func(s string) bool {
+		return net.ParseIP(s) != nil
+	}
+}
+
 func hasName(metricName string) Expression {
 	return funcMatcher(func(sp *model.Sample) bool {
 		return sp.Metric[model.MetricNameLabel] == model.LabelValue(metricName)