Repository: linkerd/linkerd2
Pull request #12528 (2024-04-30 17:13:24)

Update to latest version of kube_codegen.sh
We use the `kube_codegen.sh` script from the [kubernetes/code-generator](https://github.com/kubernetes/code-generator) repo to generate Go bindings for CRDs defined in this repo. The arguments and flags for that script changed in Kubernetes 1.30, which broke our usage of that script when this repo's Kubernetes dependencies were updated in #12463.

For the full set of changes, see: https://github.com/kubernetes/code-generator/compare/599d00c...de98b55#diff-beaa4412ca0edb2451061daa9570ce25858ec41951938fc60f17e2370462ad8e

In this change, I'm updating `bin/update-codegen.sh` to work with the latest version of the `kube_codegen.sh` script.

To validate, check out this branch and run `bin/update-codegen.sh`. The script should exit cleanly, and there should be no changes to the previously-generated Go bindings.


Patch:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 95624b192..18e1a49a4 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -185,6 +185,7 @@ jobs:
         test:
           - cni-calico-deep
           - deep
+          - deep-native-sidecar
     runs-on: ubuntu-22.04
     timeout-minutes: 15
     steps:
diff --git a/Cargo.lock b/Cargo.lock
index 11f5aea87..3db08f9cc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1145,9 +1145,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
 [[package]]
 name = "linked-hash-map"
diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh
index 86f23ee34..1f9cb33f9 100755
--- a/bin/update-codegen.sh
+++ b/bin/update-codegen.sh
@@ -7,12 +7,14 @@ set -o pipefail
 SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
 SCRIPT_ROOT="$(dirname "${SCRIPT_DIR}")"
 GEN_VER=$( awk '/k8s.io\/code-generator/ { print $2 }' "${SCRIPT_ROOT}/go.mod" )
-CODEGEN_PKG=target/code-generator-${GEN_VER}
+KUBE_OPEN_API_VER=$( awk '/k8s.io\/kube-openapi/ { print $2 }' "${SCRIPT_ROOT}/go.mod" )
+CODEGEN_PKG=$(mktemp -d -t "code-generator-${GEN_VER}.XXX")/code-generator
 
-if [[ ! -d "$CODEGEN_PKG" ]]; then
-    mkdir -p "$CODEGEN_PKG"
-    git clone --depth 1 --branch "$GEN_VER" https://github.com/kubernetes/code-generator "$CODEGEN_PKG"
-fi
+git clone --depth 1 --branch "$GEN_VER" https://github.com/kubernetes/code-generator "$CODEGEN_PKG"
+(
+    cd "$CODEGEN_PKG"
+    go get k8s.io/kube-openapi/pkg/common@"$KUBE_OPEN_API_VER"
+)
 
 # Remove previously generated code
 rm -rf "${SCRIPT_ROOT}/controller/gen/client/clientset/*"
@@ -34,9 +36,8 @@ mkdir -p "${SCRIPT_ROOT}/github.com/linkerd"
 ln -s "$(realpath "${SCRIPT_ROOT}")" "${SCRIPT_ROOT}/github.com/linkerd/linkerd2"
 
 kube::codegen::gen_helpers \
-    --input-pkg-root github.com/linkerd/linkerd2/controller/gen/apis \
-    --output-base "${SCRIPT_ROOT}" \
-    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt"
+    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt" \
+    github.com/linkerd/linkerd2/controller/gen/apis
 
 if [[ -n "${API_KNOWN_VIOLATIONS_DIR:-}" ]]; then
     report_filename="${API_KNOWN_VIOLATIONS_DIR}/codegen_violation_exceptions.list"
@@ -46,19 +47,19 @@ if [[ -n "${API_KNOWN_VIOLATIONS_DIR:-}" ]]; then
 fi
 
 kube::codegen::gen_openapi \
-    --input-pkg-root github.com/linkerd/linkerd2/controller/gen/apis \
-    --output-pkg-root github.com/linkerd/linkerd2/controller/gen\
-    --output-base "${SCRIPT_ROOT}" \
+    --output-pkg github.com/linkerd/linkerd2/controller/gen \
+    --output-dir "${SCRIPT_ROOT}/controller/gen/client" \
     --report-filename "${report_filename:-"/dev/null"}" \
     ${update_report:+"${update_report}"} \
-    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt"
+    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt" \
+    github.com/linkerd/linkerd2/controller/gen/apis
 
 kube::codegen::gen_client \
     --with-watch \
-    --input-pkg-root github.com/linkerd/linkerd2/controller/gen/apis \
-    --output-pkg-root github.com/linkerd/linkerd2/controller/gen/client \
-    --output-base "${SCRIPT_ROOT}" \
-    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt"
+    --output-pkg github.com/linkerd/linkerd2/controller/gen/client \
+    --output-dir "${SCRIPT_ROOT}/controller/gen/client" \
+    --boilerplate "${SCRIPT_ROOT}/controller/gen/boilerplate.go.txt" \
+    github.com/linkerd/linkerd2/controller/gen/apis
 
 # Once the code has been generated, we can remove the symlink.
 rm -rf "${SCRIPT_ROOT}/github.com"
diff --git a/charts/linkerd-control-plane/templates/destination.yaml b/charts/linkerd-control-plane/templates/destination.yaml
index 0f2992281..6d12557a1 100644
--- a/charts/linkerd-control-plane/templates/destination.yaml
+++ b/charts/linkerd-control-plane/templates/destination.yaml
@@ -206,6 +206,9 @@ spec:
         - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}}
         - -default-opaque-ports={{.Values.proxy.opaquePorts}}
         - -enable-pprof={{.Values.enablePprof | default false}}
+        {{- if (.Values.destinationController).meshedHttp2ClientProtobuf }}
+        - --meshed-http2-client-params={{ toJson .Values.destinationController.meshedHttp2ClientProtobuf }}
+        {{- end }}
         {{- range (.Values.destinationController).additionalArgs }}
         - {{ . }}
         {{- end }}
diff --git a/charts/linkerd-control-plane/values.yaml b/charts/linkerd-control-plane/values.yaml
index 8e2298f87..e8705bfec 100644
--- a/charts/linkerd-control-plane/values.yaml
+++ b/charts/linkerd-control-plane/values.yaml
@@ -369,6 +369,15 @@ controllerUID: 2103
 # destinationProxyResources -- CPU, Memory and Ephemeral Storage resources required by proxy injected into destination pod (see `proxy.resources` for sub-fields)
 #destinationProxyResources:
 
+destinationController:
+  meshedHttp2ClientProtobuf:
+    keep_alive:
+      interval:
+        seconds: 10
+      timeout:
+        seconds: 3
+      while_idle: true
+
 # debug configuration
 debugContainer:
   image:
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 098908e26..5d4709265 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -40,6 +40,7 @@ type (
 		ip      string
 		weight  uint32
 		labels  map[string]string
+		http2   *destinationPb.Http2ClientParams
 	}
 )
 
@@ -207,6 +208,7 @@ func requestEndpointsFromAPI(client destinationPb.DestinationClient, token strin
 					ip:      getIP(tcpAddr),
 					weight:  addr.GetWeight(),
 					labels:  addr.GetMetricLabels(),
+					http2:   addr.GetHttp2(),
 				})
 			}
 		}
@@ -239,6 +241,8 @@ type rowEndpoint struct {
 	Service   string `json:"service"`
 	Weight    uint32 `json:"weight"`
 
+	Http2 *destinationPb.Http2ClientParams `json:"http2,omitempty"`
+
 	Labels map[string]string `json:"labels"`
 }
 
@@ -267,6 +271,7 @@ func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, option
 					Service:   serviceID,
 					Weight:    pod.weight,
 					Labels:    pod.labels,
+					Http2:     pod.http2,
 				}
 
 				endpointsTables[namespace] = append(endpointsTables[namespace], row)
diff --git a/controller/api/destination/endpoint_profile_translator.go b/controller/api/destination/endpoint_profile_translator.go
index 48da6a84f..c5aeef331 100644
--- a/controller/api/destination/endpoint_profile_translator.go
+++ b/controller/api/destination/endpoint_profile_translator.go
@@ -17,6 +17,8 @@ type endpointProfileTranslator struct {
 	identityTrustDomain string
 	defaultOpaquePorts  map[uint32]struct{}
 
+	meshedHttp2ClientParams *pb.Http2ClientParams
+
 	stream    pb.Destination_GetProfileServer
 	endStream chan struct{}
 
@@ -46,6 +48,7 @@ func newEndpointProfileTranslator(
 	controllerNS,
 	identityTrustDomain string,
 	defaultOpaquePorts map[uint32]struct{},
+	meshedHTTP2ClientParams *pb.Http2ClientParams,
 	stream pb.Destination_GetProfileServer,
 	endStream chan struct{},
 	log *logging.Entry,
@@ -56,6 +59,8 @@ func newEndpointProfileTranslator(
 		identityTrustDomain: identityTrustDomain,
 		defaultOpaquePorts:  defaultOpaquePorts,
 
+		meshedHttp2ClientParams: meshedHTTP2ClientParams,
+
 		stream:    stream,
 		endStream: endStream,
 		updates:   make(chan *watcher.Address, updateQueueCapacity),
@@ -127,6 +132,7 @@ func (ept *endpointProfileTranslator) update(address *watcher.Address) {
 			address.IP, address.Port, err)
 		return
 	}
+	ept.log.Debugf("Created endpoint: %+v", endpoint)
 
 	_, opaqueProtocol := opaquePorts[address.Port]
 	profile := &pb.DestinationProfile{
@@ -152,9 +158,10 @@ func (ept *endpointProfileTranslator) createEndpoint(address watcher.Address, op
 	var weightedAddr *pb.WeightedAddr
 	var err error
 	if address.ExternalWorkload != nil {
-		weightedAddr, err = createWeightedAddrForExternalWorkload(address, opaquePorts)
+		weightedAddr, err = createWeightedAddrForExternalWorkload(address, opaquePorts, ept.meshedHttp2ClientParams)
 	} else {
-		weightedAddr, err = createWeightedAddr(address, opaquePorts, ept.enableH2Upgrade, ept.identityTrustDomain, ept.controllerNS)
+		weightedAddr, err = createWeightedAddr(address, opaquePorts,
+			ept.enableH2Upgrade, ept.identityTrustDomain, ept.controllerNS, ept.meshedHttp2ClientParams)
 	}
 	if err != nil {
 		return nil, err
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 60b68ddee..0667633db 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -43,6 +43,8 @@ type (
 		enableEndpointFiltering,
 		extEndpointZoneWeights bool
 
+		meshedHTTP2ClientParams *pb.Http2ClientParams
+
 		availableEndpoints watcher.AddressSet
 		filteredSnapshot   watcher.AddressSet
 		stream             pb.Destination_GetServer
@@ -83,6 +85,7 @@ func newEndpointTranslator(
 	enableH2Upgrade,
 	enableEndpointFiltering,
 	extEndpointZoneWeights bool,
+	meshedHTTP2ClientParams *pb.Http2ClientParams,
 	service string,
 	srcNodeName string,
 	defaultOpaquePorts map[uint32]struct{},
@@ -113,6 +116,8 @@ func newEndpointTranslator(
 		enableH2Upgrade,
 		enableEndpointFiltering,
 		extEndpointZoneWeights,
+		meshedHTTP2ClientParams,
+
 		availableEndpoints,
 		filteredSnapshot,
 		stream,
@@ -375,14 +380,15 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 		)
 		if address.Pod != nil {
 			opaquePorts = watcher.GetAnnotatedOpaquePorts(address.Pod, et.defaultOpaquePorts)
-			wa, err = createWeightedAddr(address, opaquePorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS)
+			wa, err = createWeightedAddr(address, opaquePorts,
+				et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.meshedHTTP2ClientParams)
 			if err != nil {
 				et.log.Errorf("Failed to translate Pod endpoints to weighted addr: %s", err)
 				continue
 			}
 		} else if address.ExternalWorkload != nil {
 			opaquePorts = watcher.GetAnnotatedOpaquePortsForExternalWorkload(address.ExternalWorkload, et.defaultOpaquePorts)
-			wa, err = createWeightedAddrForExternalWorkload(address, opaquePorts)
+			wa, err = createWeightedAddrForExternalWorkload(address, opaquePorts, et.meshedHTTP2ClientParams)
 			if err != nil {
 				et.log.Errorf("Failed to translate ExternalWorkload endpoints to weighted addr: %s", err)
 				continue
@@ -424,6 +430,7 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 						},
 					}
 				}
+				wa.Http2 = et.meshedHTTP2ClientParams
 			}
 		}
 
@@ -488,6 +495,7 @@ func toAddr(address watcher.Address) (*net.TcpAddress, error) {
 func createWeightedAddrForExternalWorkload(
 	address watcher.Address,
 	opaquePorts map[uint32]struct{},
+	http2 *pb.Http2ClientParams,
 ) (*pb.WeightedAddr, error) {
 	tcpAddr, err := toAddr(address)
 	if err != nil {
@@ -508,6 +516,8 @@ func createWeightedAddrForExternalWorkload(
 	}
 
 	weightedAddr.ProtocolHint = &pb.ProtocolHint{}
+	weightedAddr.Http2 = http2
+
 	_, opaquePort := opaquePorts[address.Port]
 	// If address is set as opaque by a Server, or its port is set as
 	// opaque by annotation or default value, then set the hinted protocol to
@@ -559,6 +569,7 @@ func createWeightedAddr(
 	enableH2Upgrade bool,
 	identityTrustDomain string,
 	controllerNS string,
+	meshedHttp2 *pb.Http2ClientParams,
 ) (*pb.WeightedAddr, error) {
 	tcpAddr, err := toAddr(address)
 	if err != nil {
@@ -593,6 +604,7 @@ func createWeightedAddr(
 	_, isSkippedInboundPort := skippedInboundPorts[address.Port]
 
 	if controllerNSLabel != "" && !isSkippedInboundPort {
+		weightedAddr.Http2 = meshedHttp2
 		weightedAddr.ProtocolHint = &pb.ProtocolHint{}
 
 		_, opaquePort := opaquePorts[address.Port]
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index d3098857b..93a5f1299 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -34,6 +34,8 @@ type (
 		EnableEndpointSlices,
 		ExtEndpointZoneWeights bool
 
+		MeshedHttp2ClientParams *pb.Http2ClientParams
+
 		DefaultOpaquePorts map[uint32]struct{}
 	}
 
@@ -187,6 +189,7 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 			s.config.EnableH2Upgrade,
 			false, // Disable endpoint filtering for remote discovery.
 			s.config.ExtEndpointZoneWeights,
+			s.config.MeshedHttp2ClientParams,
 			fmt.Sprintf("%s.%s.svc.%s:%d", remoteSvc, service.Namespace, remoteConfig.ClusterDomain, port),
 			token.NodeName,
 			s.config.DefaultOpaquePorts,
@@ -218,6 +221,7 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 			s.config.EnableH2Upgrade,
 			true,
 			s.config.ExtEndpointZoneWeights,
+			s.config.MeshedHttp2ClientParams,
 			dest.GetPath(),
 			token.NodeName,
 			s.config.DefaultOpaquePorts,
@@ -509,6 +513,7 @@ func (s *server) subscribeToEndpointProfile(
 		s.config.ControllerNS,
 		s.config.IdentityTrustDomain,
 		s.config.DefaultOpaquePorts,
+		s.config.MeshedHttp2ClientParams,
 		stream,
 		streamEnd,
 		log,
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index cc8bcc9ef..2363b4cad 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -2,6 +2,7 @@ package destination
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"flag"
 	"net"
@@ -10,6 +11,7 @@ import (
 	"os/signal"
 	"syscall"
 
+	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2/controller/api/destination"
 	externalworkload "github.com/linkerd/linkerd2/controller/api/destination/external-workload"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
@@ -50,8 +52,24 @@ func Main(args []string) {
 	extEndpointZoneWeights := cmd.Bool("ext-endpoint-zone-weights", false,
 		"Enable setting endpoint weighting based on zone locality")
 
+	// Cluster-wide defaults for meshed HTTP/2 client parameters.. These only
+	// apply to meshed connections, as we don't want to conflict with HTTP/2
+	// servers that enforce policies that limit client keep-alive behavior. The
+	// inbound proxy does not enforce such policies, so we're free to use
+	// defaults for meshed HTTP/2 connections.
+	meshedHTTP2ClientParamsJSON := cmd.String("meshed-http2-client-params", "",
+		"HTTP/2 client parameters for meshed connections in JSON format")
+
 	flags.ConfigureAndParse(cmd, args)
 
+	var meshedHTTP2ClientParams *pb.Http2ClientParams
+	if meshedHTTP2ClientParamsJSON != nil && *meshedHTTP2ClientParamsJSON != "" {
+		meshedHTTP2ClientParams = &pb.Http2ClientParams{}
+		if err := json.Unmarshal([]byte(*meshedHTTP2ClientParamsJSON), meshedHTTP2ClientParams); err != nil {
+			log.Fatalf("Failed to parse meshed HTTP/2 client parameters: %s", err)
+		}
+	}
+
 	ready := false
 	adminServer := admin.NewServer(*metricsAddr, *enablePprof, &ready)
 
@@ -143,13 +161,14 @@ func Main(args []string) {
 	}
 
 	config := destination.Config{
-		ControllerNS:           *controllerNamespace,
-		IdentityTrustDomain:    *trustDomain,
-		ClusterDomain:          *clusterDomain,
-		DefaultOpaquePorts:     opaquePorts,
-		EnableH2Upgrade:        *enableH2Upgrade,
-		EnableEndpointSlices:   *enableEndpointSlices,
-		ExtEndpointZoneWeights: *extEndpointZoneWeights,
+		ControllerNS:            *controllerNamespace,
+		IdentityTrustDomain:     *trustDomain,
+		ClusterDomain:           *clusterDomain,
+		DefaultOpaquePorts:      opaquePorts,
+		EnableH2Upgrade:         *enableH2Upgrade,
+		EnableEndpointSlices:    *enableEndpointSlices,
+		ExtEndpointZoneWeights:  *extEndpointZoneWeights,
+		MeshedHttp2ClientParams: meshedHTTP2ClientParams,
 	}
 	server, err := destination.NewServer(
 		*addr,
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 609d6212b..fa9d4a5f0 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -59,6 +59,8 @@ type (
 		LinkerdVersion               string                 `json:"linkerdVersion"`
 		RevisionHistoryLimit         uint                   `json:"revisionHistoryLimit"`
 
+		DestinationController map[string]interface{} `json:"destinationController"`
+
 		PodAnnotations    map[string]string `json:"podAnnotations"`
 		PodLabels         map[string]string `json:"podLabels"`
 		PriorityClassName string            `json:"priorityClassName"`


Test patch:
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index 09604648b..1081f10ef 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -12,7 +12,7 @@ testdir="$bindir"/../test/integration
 
 ##### Test setup helpers #####
 
-export default_test_names=(deep viz external helm-upgrade uninstall upgrade-edge default-policy-deny rsa-ca)
+export default_test_names=(deep deep-native-sidecar viz external helm-upgrade uninstall upgrade-edge default-policy-deny rsa-ca)
 export external_resource_test_names=(external-resources)
 export all_test_names=(cluster-domain cni-calico-deep multicluster "${default_test_names[*]}" "${external_resource_test_names[*]}")
 images_load_default=(proxy controller policy-controller web metrics-api tap)
@@ -23,7 +23,7 @@ tests_usage() {
 
 Optionally specify a test with the --name flag: [${all_test_names[*]}]
 
-Note: The cluster-domain, cni-calico-deep and multicluster tests require a custom cluster configuration (see bin/_test-helpers.sh)
+Note: The cluster-domain, deep-native-sidecar cni-calico-deep and multicluster tests require a custom cluster configuration (see bin/_test-helpers.sh)
 
 Usage:
     ${progname} [--images docker|archive|skip] [--name test-name] [--skip-cluster-create] /path/to/linkerd
@@ -444,6 +444,10 @@ run_deep_test() {
   run_test "$testdir/deep/..."
 }
 
+run_deep-native-sidecar_test() {
+  run_test "$testdir/deep/..." --native-sidecar
+}
+
 run_default-policy-deny_test() {
   export default_inbound_policy='deny'
   run_test "$testdir/install/..."
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index f0435bb1f..813292918 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1459,6 +1467,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 32da45dc2..1c004630c 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 3884bd7b9..0571ac9e6 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 32da45dc2..1c004630c 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
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 e321fa261..b8ff1014d 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index 6f3fc4e84..b6145ead8 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1447,6 +1455,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 043bc1f03..f97f23a25 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 1
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources:
       cpu:
@@ -1580,6 +1588,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index edd29d6ce..ad907dbbf 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 1
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources:
       cpu:
@@ -1580,6 +1588,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 4756daf29..d7923f0bb 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -445,6 +445,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: true
@@ -1389,6 +1397,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index f2366327d..687d2090f 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -515,6 +515,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1433,6 +1441,7 @@ spec:
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
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 bde3ef9ac..bb722a1ab 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -515,6 +515,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 1
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources:
       cpu:
@@ -1555,6 +1563,7 @@ spec:
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 05005f084..79aea9d55 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -515,6 +515,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 1
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources:
       cpu:
@@ -1567,6 +1575,7 @@ spec:
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
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 61e185b7c..360f16fb2 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -510,6 +510,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 1
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources:
       cpu:
@@ -1545,6 +1553,7 @@ spec:
         - -identity-trust-domain=test.trust.domain
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index ab3466d07..ff0e6abe2 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1452,6 +1460,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index a8d680af6..454fdf917 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -505,6 +505,7 @@ data:
         name: DebugImageName
         pullPolicy: DebugImagePullPolicy
         version: DebugVersion
+    destinationController: null
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1953,7 +1954,7 @@ spec:
 ---
 apiVersion: v1
 data:
-  linkerd-config-overrides: Y2xpVmVyc2lvbjogQ2xpVmVyc2lvbgpjbHVzdGVyTmV0d29ya3M6IENsdXN0ZXJOZXR3b3Jrcwpjb250cm9sUGxhbmVUcmFjaW5nTmFtZXNwYWNlOiAiIgpjb250cm9sbGVyOiBudWxsCmNvbnRyb2xsZXJJbWFnZTogQ29udHJvbGxlckltYWdlCmNvbnRyb2xsZXJMb2dGb3JtYXQ6IENvbnRyb2xsZXJMb2dGb3JtYXQKY29udHJvbGxlckxvZ0xldmVsOiBDb250cm9sbGVyTG9nTGV2ZWwKZGVidWdDb250YWluZXI6CiAgaW1hZ2U6CiAgICBuYW1lOiBEZWJ1Z0ltYWdlTmFtZQogICAgcHVsbFBvbGljeTogRGVidWdJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IERlYnVnVmVyc2lvbgplbmFibGVFbmRwb2ludFNsaWNlczogZmFsc2UKaGVhcnRiZWF0U2NoZWR1bGU6IDEgMiAzIDQgNQppZGVudGl0eToKICBpc3N1ZXI6CiAgICB0bHM6CiAgICAgIGNydFBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogICAgICAgIE1JSUJ3RENDQVdlZ0F3SUJBZ0lSQUpSSWdaOFJ0TzhFd2cxWGVwZjhUNDR3Q2dZSUtvWkl6ajBFQXdJd0tURW4KICAgICAgICBNQ1VHQTFVRUF4TWVhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUI0WERUSXdNRGd5CiAgICAgICAgT0RBM01UTTBOMW9YRFRNd01EZ3lOakEzTVRNME4xb3dLVEVuTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdQogICAgICAgIGEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTEvRnAKICAgICAgICBmY1JuRGNlZEw2QWpVYVhZUHY0RElNQmFKdWZPSTVOV3R5K1hTWDdKalhnWnRNNzJkUXZSYVlhbnV4RDM2RHQxCiAgICAgICAgMi9KeHlpU2d4S1dSZG9heSthTndNRzR3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQgogICAgICAgIEFmOENBUUF3SFFZRFZSME9CQllFRkkxV25ycU1ZS2FISE9vK3pweWlpRHEycE8wS01Da0dBMVVkRVFRaU1DQ0MKICAgICAgICBIbWxrWlc1MGFYUjVMbXhwYm10bGNtUXVZMngxYzNSbGNpNXNiMk5oYkRBS0JnZ3Foa2pPUFFRREFnTkhBREJFCiAgICAgICAgQWlBdHVvSTVYdUN0ckdWUnpTbVJUbDJyYTI4YVY5TXlUVTdkNXFuVEFGSEtTZ0lnUktDdmx1T1NnQTVPMjFwNQogICAgICAgIDUxdGRybWtIRVpScjBxbExTSmRIWWdFZk16az0KICAgICAgICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCiAgICAgIGtleVBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gRUMgUFJJVkFURSBLRVktLS0tLQogICAgICAgIE1IY0NBUUVFSUFBZThuZmJ6WnU5Yy9PQjIrOHhKTTBGejdOVXdUUWF6dWxrRk5zNFRJNStvQW9HQ0NxR1NNNDkKICAgICAgICBBd0VIb1VRRFFnQUUxL0ZwZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyCiAgICAgICAgZFF2UmFZYW51eEQzNkR0MTIvSnh5aVNneEtXUmRvYXkrUT09CiAgICAgICAgLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQppZGVudGl0eVRydXN0QW5jaG9yc1BFTTogfAogIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogIE1JSUJ3VENDQVdhZ0F3SUJBZ0lRZURacDVsRGFJeWdRNVVmTUtackZBVEFLQmdncWhrak9QUVFEQWpBcE1TY3cKICBKUVlEVlFRREV4NXBaR1Z1ZEdsMGVTNXNhVzVyWlhKa0xtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qQXdPREk0CiAgTURjeE1qUTNXaGNOTXpBd09ESTJNRGN4TWpRM1dqQXBNU2N3SlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1cgogIFpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFScWM3MFoKICBsMXZndzc5cmpCNXVTSVRJQ1VBNkd5ZnZTRmZjdUlpczdCL1hGU2trd0FIVTVTL3MxQUFQK1IwVFg3SEJXVUM0CiAgdWFHNFdXc2l3SktObjdtZ28zQXdiakFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQgogIC93SUJBVEFkQmdOVkhRNEVGZ1FVNVl0alZWUGZkN0k3TkxIc24yQzI2RUJ5R1Ywd0tRWURWUjBSQkNJd0lJSWUKICBhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDCiAgSVFDTjdsQkZMRER2ang2VjArWGtqcEtFUlJzSllmNWFkTXZubG9GbDQ4aWxKZ0loQU50eGhuZGNyK1FKUHVDOAogIHZnVUMwZDIvOUZNdWVJVk1iKzQ2V1RDT2pzcXIKICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCmltYWdlUHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CmltYWdlUHVsbFNlY3JldHM6IG51bGwKbGlua2VyZFZlcnNpb246IExpbmtlcmRWZXJzaW9uCm5ldHdvcmtWYWxpZGF0b3I6CiAgZW5hYmxlU2VjdXJpdHlDb250ZXh0OiBmYWxzZQpwb2RNb25pdG9yOiBudWxsCnBvbGljeUNvbnRyb2xsZXI6CiAgaW1hZ2U6CiAgICBuYW1lOiBQb2xpY3lDb250cm9sbGVySW1hZ2VOYW1lCiAgICBwdWxsUG9saWN5OiBJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IFBvbGljeUNvbnRyb2xsZXJWZXJzaW9uCiAgbG9nTGV2ZWw6IGxvZy1sZXZlbAogIHJlc291cmNlczoKICAgIGNwdToKICAgICAgbGltaXQ6IGNwdS1saW1pdAogICAgICByZXF1ZXN0OiBjcHUtcmVxdWVzdAogICAgbWVtb3J5OgogICAgICBsaW1pdDogbWVtb3J5LWxpbWl0CiAgICAgIHJlcXVlc3Q6IG1lbW9yeS1yZXF1ZXN0CnBvbGljeVZhbGlkYXRvcjoKICBjYUJ1bmRsZTogcG9saWN5IHZhbGlkYXRvciBDQSBidW5kbGUKICBleHRlcm5hbFNlY3JldDogdHJ1ZQpwcmlvcml0eUNsYXNzTmFtZTogUHJpb3JpdHlDbGFzc05hbWUKcHJvZmlsZVZhbGlkYXRvcjoKICBjYUJ1bmRsZTogcHJvZmlsZSB2YWxpZGF0b3IgQ0EgYnVuZGxlCiAgZXh0ZXJuYWxTZWNyZXQ6IHRydWUKcHJveHk6CiAgY29udHJvbDogbnVsbAogIGRlZmF1bHRJbmJvdW5kUG9saWN5OiBkZWZhdWx0LWFsbG93LXBvbGljeQogIGltYWdlOgogICAgbmFtZTogUHJveHlJbWFnZU5hbWUKICAgIHB1bGxQb2xpY3k6IEltYWdlUHVsbFBvbGljeQogICAgdmVyc2lvbjogUHJveHlWZXJzaW9uCiAgaW5ib3VuZENvbm5lY3RUaW1lb3V0OiAiIgogIGluYm91bmREaXNjb3ZlcnlDYWNoZVVudXNlZFRpbWVvdXQ6ICIiCiAgbG9nTGV2ZWw6IHdhcm4sbGlua2VyZD1pbmZvCiAgb3BhcXVlUG9ydHM6IDI1LDQ0Myw1ODcsMzMwNiw1NDMyLDExMjExCiAgb3V0Ym91bmRDb25uZWN0VGltZW91dDogIiIKICBvdXRib3VuZERpc2NvdmVyeUNhY2hlVW51c2VkVGltZW91dDogIiIKICByZXNvdXJjZXM6CiAgICBjcHU6CiAgICAgIGxpbWl0OiBjcHUtbGltaXQKICAgICAgcmVxdWVzdDogY3B1LXJlcXVlc3QKICAgIG1lbW9yeToKICAgICAgbGltaXQ6IG1lbW9yeS1saW1pdAogICAgICByZXF1ZXN0OiBtZW1vcnktcmVxdWVzdAogIHN0YXJ0dXBQcm9iZTogbnVsbApwcm94eUNvbnRhaW5lck5hbWU6IFByb3h5Q29udGFpbmVyTmFtZQpwcm94eUluaXQ6CiAgaWdub3JlSW5ib3VuZFBvcnRzOiAiIgogIGlnbm9yZU91dGJvdW5kUG9ydHM6ICI0NDMiCiAgaW1hZ2U6CiAgICBuYW1lOiBQcm94eUluaXRJbWFnZU5hbWUKICAgIHB1bGxQb2xpY3k6IEltYWdlUHVsbFBvbGljeQogICAgdmVyc2lvbjogUHJveHlJbml0VmVyc2lvbgogIGt1YmVBUElTZXJ2ZXJQb3J0czogIiIKICByZXNvdXJjZXM6CiAgICBjcHU6CiAgICAgIHJlcXVlc3Q6IDEwbQogICAgbWVtb3J5OgogICAgICBsaW1pdDogNTBNaQogICAgICByZXF1ZXN0OiAxME1pCnByb3h5SW5qZWN0b3I6CiAgY2FCdW5kbGU6IHByb3h5IGluamVjdG9yIENBIGJ1bmRsZQogIGV4dGVybmFsU2VjcmV0OiB0cnVlCndlYmhvb2tGYWlsdXJlUG9saWN5OiBXZWJob29rRmFpbHVyZVBvbGljeQo=
+  linkerd-config-overrides: Y2xpVmVyc2lvbjogQ2xpVmVyc2lvbgpjbHVzdGVyTmV0d29ya3M6IENsdXN0ZXJOZXR3b3Jrcwpjb250cm9sUGxhbmVUcmFjaW5nTmFtZXNwYWNlOiAiIgpjb250cm9sbGVyOiBudWxsCmNvbnRyb2xsZXJJbWFnZTogQ29udHJvbGxlckltYWdlCmNvbnRyb2xsZXJMb2dGb3JtYXQ6IENvbnRyb2xsZXJMb2dGb3JtYXQKY29udHJvbGxlckxvZ0xldmVsOiBDb250cm9sbGVyTG9nTGV2ZWwKZGVidWdDb250YWluZXI6CiAgaW1hZ2U6CiAgICBuYW1lOiBEZWJ1Z0ltYWdlTmFtZQogICAgcHVsbFBvbGljeTogRGVidWdJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IERlYnVnVmVyc2lvbgpkZXN0aW5hdGlvbkNvbnRyb2xsZXI6IG51bGwKZW5hYmxlRW5kcG9pbnRTbGljZXM6IGZhbHNlCmhlYXJ0YmVhdFNjaGVkdWxlOiAxIDIgMyA0IDUKaWRlbnRpdHk6CiAgaXNzdWVyOgogICAgdGxzOgogICAgICBjcnRQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICAgICAgICBNSUlCd0RDQ0FXZWdBd0lCQWdJUkFKUklnWjhSdE84RXdnMVhlcGY4VDQ0d0NnWUlLb1pJemowRUF3SXdLVEVuCiAgICAgICAgTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01CNFhEVEl3TURneQogICAgICAgIE9EQTNNVE0wTjFvWERUTXdNRGd5TmpBM01UTTBOMW93S1RFbk1DVUdBMVVFQXhNZWFXUmxiblJwZEhrdWJHbHUKICAgICAgICBhMlZ5WkM1amJIVnpkR1Z5TG14dlkyRnNNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxL0ZwCiAgICAgICAgZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyZFF2UmFZYW51eEQzNkR0MQogICAgICAgIDIvSnh5aVNneEtXUmRvYXkrYU53TUc0d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKICAgICAgICBBZjhDQVFBd0hRWURWUjBPQkJZRUZJMVducnFNWUthSEhPbyt6cHlpaURxMnBPMEtNQ2tHQTFVZEVRUWlNQ0NDCiAgICAgICAgSG1sa1pXNTBhWFI1TG14cGJtdGxjbVF1WTJ4MWMzUmxjaTVzYjJOaGJEQUtCZ2dxaGtqT1BRUURBZ05IQURCRQogICAgICAgIEFpQXR1b0k1WHVDdHJHVlJ6U21SVGwycmEyOGFWOU15VFU3ZDVxblRBRkhLU2dJZ1JLQ3ZsdU9TZ0E1TzIxcDUKICAgICAgICA1MXRkcm1rSEVaUnIwcWxMU0pkSFlnRWZNems9CiAgICAgICAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQogICAgICBrZXlQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIEVDIFBSSVZBVEUgS0VZLS0tLS0KICAgICAgICBNSGNDQVFFRUlBQWU4bmZielp1OWMvT0IyKzh4Sk0wRno3TlV3VFFhenVsa0ZOczRUSTUrb0FvR0NDcUdTTTQ5CiAgICAgICAgQXdFSG9VUURRZ0FFMS9GcGZjUm5EY2VkTDZBalVhWFlQdjRESU1CYUp1Zk9JNU5XdHkrWFNYN0pqWGdadE03MgogICAgICAgIGRRdlJhWWFudXhEMzZEdDEyL0p4eWlTZ3hLV1Jkb2F5K1E9PQogICAgICAgIC0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0KaWRlbnRpdHlUcnVzdEFuY2hvcnNQRU06IHwKICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICBNSUlCd1RDQ0FXYWdBd0lCQWdJUWVEWnA1bERhSXlnUTVVZk1LWnJGQVRBS0JnZ3Foa2pPUFFRREFqQXBNU2N3CiAgSlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1clpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d0hoY05NakF3T0RJNAogIE1EY3hNalEzV2hjTk16QXdPREkyTURjeE1qUTNXakFwTVNjd0pRWURWUVFERXg1cFpHVnVkR2wwZVM1c2FXNXIKICBaWEprTG1Oc2RYTjBaWEl1Ykc5allXd3dXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnFjNzBaCiAgbDF2Z3c3OXJqQjV1U0lUSUNVQTZHeWZ2U0ZmY3VJaXM3Qi9YRlNra3dBSFU1Uy9zMUFBUCtSMFRYN0hCV1VDNAogIHVhRzRXV3Npd0pLTm43bWdvM0F3YmpBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKICAvd0lCQVRBZEJnTlZIUTRFRmdRVTVZdGpWVlBmZDdJN05MSHNuMkMyNkVCeUdWMHdLUVlEVlIwUkJDSXdJSUllCiAgYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Bb0dDQ3FHU000OUJBTUNBMGtBTUVZQwogIElRQ043bEJGTEREdmp4NlYwK1hranBLRVJSc0pZZjVhZE12bmxvRmw0OGlsSmdJaEFOdHhobmRjcitRSlB1QzgKICB2Z1VDMGQyLzlGTXVlSVZNYis0NldUQ09qc3FyCiAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQppbWFnZVB1bGxQb2xpY3k6IEltYWdlUHVsbFBvbGljeQppbWFnZVB1bGxTZWNyZXRzOiBudWxsCmxpbmtlcmRWZXJzaW9uOiBMaW5rZXJkVmVyc2lvbgpuZXR3b3JrVmFsaWRhdG9yOgogIGVuYWJsZVNlY3VyaXR5Q29udGV4dDogZmFsc2UKcG9kTW9uaXRvcjogbnVsbApwb2xpY3lDb250cm9sbGVyOgogIGltYWdlOgogICAgbmFtZTogUG9saWN5Q29udHJvbGxlckltYWdlTmFtZQogICAgcHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CiAgICB2ZXJzaW9uOiBQb2xpY3lDb250cm9sbGVyVmVyc2lvbgogIGxvZ0xldmVsOiBsb2ctbGV2ZWwKICByZXNvdXJjZXM6CiAgICBjcHU6CiAgICAgIGxpbWl0OiBjcHUtbGltaXQKICAgICAgcmVxdWVzdDogY3B1LXJlcXVlc3QKICAgIG1lbW9yeToKICAgICAgbGltaXQ6IG1lbW9yeS1saW1pdAogICAgICByZXF1ZXN0OiBtZW1vcnktcmVxdWVzdApwb2xpY3lWYWxpZGF0b3I6CiAgY2FCdW5kbGU6IHBvbGljeSB2YWxpZGF0b3IgQ0EgYnVuZGxlCiAgZXh0ZXJuYWxTZWNyZXQ6IHRydWUKcHJpb3JpdHlDbGFzc05hbWU6IFByaW9yaXR5Q2xhc3NOYW1lCnByb2ZpbGVWYWxpZGF0b3I6CiAgY2FCdW5kbGU6IHByb2ZpbGUgdmFsaWRhdG9yIENBIGJ1bmRsZQogIGV4dGVybmFsU2VjcmV0OiB0cnVlCnByb3h5OgogIGNvbnRyb2w6IG51bGwKICBkZWZhdWx0SW5ib3VuZFBvbGljeTogZGVmYXVsdC1hbGxvdy1wb2xpY3kKICBpbWFnZToKICAgIG5hbWU6IFByb3h5SW1hZ2VOYW1lCiAgICBwdWxsUG9saWN5OiBJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IFByb3h5VmVyc2lvbgogIGluYm91bmRDb25uZWN0VGltZW91dDogIiIKICBpbmJvdW5kRGlzY292ZXJ5Q2FjaGVVbnVzZWRUaW1lb3V0OiAiIgogIGxvZ0xldmVsOiB3YXJuLGxpbmtlcmQ9aW5mbwogIG9wYXF1ZVBvcnRzOiAyNSw0NDMsNTg3LDMzMDYsNTQzMiwxMTIxMQogIG91dGJvdW5kQ29ubmVjdFRpbWVvdXQ6ICIiCiAgb3V0Ym91bmREaXNjb3ZlcnlDYWNoZVVudXNlZFRpbWVvdXQ6ICIiCiAgcmVzb3VyY2VzOgogICAgY3B1OgogICAgICBsaW1pdDogY3B1LWxpbWl0CiAgICAgIHJlcXVlc3Q6IGNwdS1yZXF1ZXN0CiAgICBtZW1vcnk6CiAgICAgIGxpbWl0OiBtZW1vcnktbGltaXQKICAgICAgcmVxdWVzdDogbWVtb3J5LXJlcXVlc3QKICBzdGFydHVwUHJvYmU6IG51bGwKcHJveHlDb250YWluZXJOYW1lOiBQcm94eUNvbnRhaW5lck5hbWUKcHJveHlJbml0OgogIGlnbm9yZUluYm91bmRQb3J0czogIiIKICBpZ25vcmVPdXRib3VuZFBvcnRzOiAiNDQzIgogIGltYWdlOgogICAgbmFtZTogUHJveHlJbml0SW1hZ2VOYW1lCiAgICBwdWxsUG9saWN5OiBJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IFByb3h5SW5pdFZlcnNpb24KICBrdWJlQVBJU2VydmVyUG9ydHM6ICIiCiAgcmVzb3VyY2VzOgogICAgY3B1OgogICAgICByZXF1ZXN0OiAxMG0KICAgIG1lbW9yeToKICAgICAgbGltaXQ6IDUwTWkKICAgICAgcmVxdWVzdDogMTBNaQpwcm94eUluamVjdG9yOgogIGNhQnVuZGxlOiBwcm94eSBpbmplY3RvciBDQSBidW5kbGUKICBleHRlcm5hbFNlY3JldDogdHJ1ZQp3ZWJob29rRmFpbHVyZVBvbGljeTogV2ViaG9va0ZhaWx1cmVQb2xpY3kK
 kind: Secret
 metadata:
   creationTimestamp: null
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 8d6726b41..f88f3faa5 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=cluster.local
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 7d966e430..210b58d40 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -514,6 +514,14 @@ data:
       rollingUpdate:
         maxSurge: 25%
         maxUnavailable: 25%
+    destinationController:
+      meshedHttp2ClientProtobuf:
+        keep_alive:
+          interval:
+            seconds: 10
+          timeout:
+            seconds: 3
+          while_idle: true
     destinationProxyResources: null
     destinationResources: null
     disableHeartBeat: false
@@ -1458,6 +1466,7 @@ spec:
         - -identity-trust-domain=example.com
         - -default-opaque-ports=25,587,3306,4444,5432,6379,9300,11211
         - -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
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/controller/api/destination/endpoint_profile_translator_test.go b/controller/api/destination/endpoint_profile_translator_test.go
index 88e2f016e..a35c69db6 100644
--- a/controller/api/destination/endpoint_profile_translator_test.go
+++ b/controller/api/destination/endpoint_profile_translator_test.go
@@ -40,7 +40,7 @@ func TestEndpointProfileTranslator(t *testing.T) {
 		}
 		log := logging.WithField("test", t.Name())
 		translator := newEndpointProfileTranslator(
-			true, "cluster", "identity", make(map[uint32]struct{}),
+			true, "cluster", "identity", make(map[uint32]struct{}), nil,
 			mockGetProfileServer,
 			nil,
 			log,
@@ -84,7 +84,7 @@ func TestEndpointProfileTranslator(t *testing.T) {
 		log := logging.WithField("test", t.Name())
 		endStream := make(chan struct{})
 		translator := newEndpointProfileTranslator(
-			true, "cluster", "identity", make(map[uint32]struct{}),
+			true, "cluster", "identity", make(map[uint32]struct{}), nil,
 			mockGetProfileServer,
 			endStream,
 			log,
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index 62ff6ab3d..8f9687f2a 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -4,9 +4,11 @@ import (
 	"context"
 	"fmt"
 	gonet "net"
+	"reflect"
 	"testing"
 	"time"
 
+	"github.com/golang/protobuf/ptypes/duration"
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
@@ -114,6 +116,90 @@ func TestGet(t *testing.T) {
 		}
 	})
 
+	t.Run("Sets meshed HTTP/2 client params", func(t *testing.T) {
+		server := makeServer(t)
+		http2Params := pb.Http2ClientParams{
+			KeepAlive: &pb.Http2ClientParams_KeepAlive{
+				Timeout:  &duration.Duration{Seconds: 10},
+				Interval: &duration.Duration{Seconds: 20},
+			},
+		}
+		server.config.MeshedHttp2ClientParams = &http2Params
+		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
+			}
+		}()
+
+		select {
+		case update := <-stream.updates:
+			add, ok := update.GetUpdate().(*pb.Update_Add)
+			if !ok {
+				t.Fatalf("Update expected to be an add, but was %+v", update)
+			}
+			addr := add.Add.Addrs[0]
+			if !reflect.DeepEqual(addr.GetHttp2(), &http2Params) {
+				t.Fatalf("Expected HTTP/2 client params to be %v, but got %v", &http2Params, addr.GetHttp2())
+			}
+		case err := <-errs:
+			t.Fatalf("Got error: %s", err)
+		}
+	})
+
+	t.Run("Does not set unmeshed HTTP/2 client params", func(t *testing.T) {
+		server := makeServer(t)
+		http2Params := pb.Http2ClientParams{
+			KeepAlive: &pb.Http2ClientParams_KeepAlive{
+				Timeout:  &duration.Duration{Seconds: 10},
+				Interval: &duration.Duration{Seconds: 20},
+			},
+		}
+		server.config.MeshedHttp2ClientParams = &http2Params
+		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", "name2.ns.svc.mycluster.local", port)}, stream)
+			if err != nil {
+				errs <- err
+			}
+		}()
+
+		select {
+		case update := <-stream.updates:
+			add, ok := update.GetUpdate().(*pb.Update_Add)
+			if !ok {
+				t.Fatalf("Update expected to be an add, but was %+v", update)
+			}
+			addr := add.Add.Addrs[0]
+			if addr.GetHttp2() != nil {
+				t.Fatalf("Expected HTTP/2 client params to be nil, but got %v", addr.GetHttp2())
+			}
+		case err := <-errs:
+			t.Fatalf("Got error: %s", err)
+		}
+	})
+
 	t.Run("Return endpoint with unknown protocol hint and identity when service name contains skipped inbound port", func(t *testing.T) {
 		server := makeServer(t)
 		defer server.clusterStore.UnregisterGauges()
@@ -466,6 +552,13 @@ func TestGetProfiles(t *testing.T) {
 
 	t.Run("Return profile with endpoint when using pod IP", func(t *testing.T) {
 		server := makeServer(t)
+		http2Params := pb.Http2ClientParams{
+			KeepAlive: &pb.Http2ClientParams_KeepAlive{
+				Timeout:  &duration.Duration{Seconds: 10},
+				Interval: &duration.Duration{Seconds: 20},
+			},
+		}
+		server.config.MeshedHttp2ClientParams = &http2Params
 		defer server.clusterStore.UnregisterGauges()
 
 		stream := profileStream(t, server, podIP1, port, "ns:ns")
@@ -506,6 +599,9 @@ func TestGetProfiles(t *testing.T) {
 		if first.Endpoint.Addr.String() != epAddr.String() {
 			t.Fatalf("Expected endpoint IP to be %s, but it was %s", epAddr.Ip, first.Endpoint.Addr.Ip)
 		}
+		if !reflect.DeepEqual(first.Endpoint.GetHttp2(), &http2Params) {
+			t.Fatalf("Expected HTTP/2 client params to be %v, but got %v", &http2Params, first.Endpoint.GetHttp2())
+		}
 	})
 
 	t.Run("Return profile with endpoint when using pod secondary IP", func(t *testing.T) {
@@ -554,6 +650,13 @@ func TestGetProfiles(t *testing.T) {
 
 	t.Run("Return profile with endpoint when using externalworkload IP", func(t *testing.T) {
 		server := makeServer(t)
+		http2Params := pb.Http2ClientParams{
+			KeepAlive: &pb.Http2ClientParams_KeepAlive{
+				Timeout:  &duration.Duration{Seconds: 10},
+				Interval: &duration.Duration{Seconds: 20},
+			},
+		}
+		server.config.MeshedHttp2ClientParams = &http2Params
 		defer server.clusterStore.UnregisterGauges()
 
 		stream := profileStream(t, server, externalWorkloadIP, port, "ns:ns")
@@ -594,6 +697,9 @@ func TestGetProfiles(t *testing.T) {
 		if first.Endpoint.Addr.String() != epAddr.String() {
 			t.Fatalf("Expected endpoint IP to be %s, but it was %s", epAddr.Ip, first.Endpoint.Addr.Ip)
 		}
+		if !reflect.DeepEqual(first.Endpoint.GetHttp2(), &http2Params) {
+			t.Fatalf("Expected HTTP/2 client params to be %v, but got %v", &http2Params, first.Endpoint.GetHttp2())
+		}
 	})
 
 	t.Run("Return default profile when IP does not map to service or pod", func(t *testing.T) {
@@ -623,6 +729,9 @@ func TestGetProfiles(t *testing.T) {
 		if profile.Endpoint.GetProtocolHint().GetOpaqueTransport() != nil {
 			t.Fatalf("Expected no opaque transport but found one")
 		}
+		if profile.GetEndpoint().GetHttp2() != nil {
+			t.Fatalf("Expected no HTTP/2 client parameters but found one")
+		}
 	})
 
 	t.Run("Return profile with no protocol hint when pod does not have label", func(t *testing.T) {
@@ -1060,6 +1169,7 @@ func TestTokenStructure(t *testing.T) {
 }
 
 func updateAddAddress(t *testing.T, update *pb.Update) []string {
+	t.Helper()
 	add, ok := update.GetUpdate().(*pb.Update_Add)
 	if !ok {
 		t.Fatalf("Update expected to be an add, but was %+v", update)
diff --git a/controller/api/destination/test_util.go b/controller/api/destination/test_util.go
index f50c37fce..246aaee19 100644
--- a/controller/api/destination/test_util.go
+++ b/controller/api/destination/test_util.go
@@ -13,6 +13,7 @@ import (
 )
 
 func makeServer(t *testing.T) *server {
+	t.Helper()
 	srv, _ := getServerWithClient(t)
 	return srv
 }
@@ -100,6 +101,58 @@ spec:
       name: linkerd-proxy`,
 		`
 apiVersion: v1
+kind: Service
+metadata:
+  name: name2
+  namespace: ns
+spec:
+  type: LoadBalancer
+  clusterIP: 172.17.99.0
+  clusterIPs:
+  - 172.17.99.0
+  - 2001:db8::99
+  ports:
+  - port: 8989`,
+		`
+apiVersion: discovery.k8s.io/v1
+kind: EndpointSlice
+metadata:
+  name: name2-ipv4
+  namespace: ns
+  labels:
+    kubernetes.io/service-name: name2
+addressType: IPv4
+endpoints:
+- addresses:
+  - 172.17.0.13
+  targetRef:
+    kind: Pod
+    name: name2-2
+    namespace: ns
+ports:
+- port: 8989
+  protocol: TCP`,
+		`
+apiVersion: discovery.k8s.io/v1
+kind: EndpointSlice
+metadata:
+  name: name2-ipv6
+  namespace: ns
+  labels:
+    kubernetes.io/service-name: name2
+addressType: IPv6
+endpoints:
+- addresses:
+  - 2001:db8::78
+  targetRef:
+    kind: Pod
+    name: name2-2
+    namespace: ns
+ports:
+- port: 8989
+  protocol: TCP`,
+		`
+apiVersion: v1
 kind: Pod
 metadata:
   name: name2-2
@@ -108,7 +161,8 @@ status:
   phase: Succeeded
   podIP: 172.17.0.13
   podIPs:
-  - ip: 172.17.0.13`,
+  - ip: 172.17.0.13
+  - ip: 2001:db8::78`,
 		`
 apiVersion: v1
 kind: Pod
@@ -876,6 +930,7 @@ metadata:
 		true,
 		true,  // enableEndpointFiltering
 		false, // extEndpointZoneWeights
+		nil,   // meshedHttp2ClientParams
 		"service-name.service-ns",
 		"test-123",
 		map[uint32]struct{}{},
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index ac0e9e6ad..274f72a9d 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -89,6 +89,15 @@ func TestNewValues(t *testing.T) {
 			ServiceMirror: &PodMonitorComponent{Enabled: true},
 			Proxy:         &PodMonitorComponent{Enabled: true},
 		},
+		DestinationController: map[string]interface{}{
+			"meshedHttp2ClientProtobuf": map[string]interface{}{
+				"keep_alive": map[string]interface{}{
+					"interval":   map[string]interface{}{"seconds": 10.0},
+					"timeout":    map[string]interface{}{"seconds": 3.0},
+					"while_idle": true,
+				},
+			},
+		},
 		PolicyController: &PolicyController{
 			Image: &Image{
 				Name: "cr.l5d.io/linkerd/policy-controller",
diff --git a/test/integration/deep/endpoints/endpoints_test.go b/test/integration/deep/endpoints/endpoints_test.go
index da34dd418..02f98cd62 100644
--- a/test/integration/deep/endpoints/endpoints_test.go
+++ b/test/integration/deep/endpoints/endpoints_test.go
@@ -113,6 +113,7 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "pod": "linkerd-destination\-[a-f0-9]+\-[a-z0-9]+",
     "service": "linkerd-dst\.\S*",
     "weight": \d+,
+    "http2": \{(?s).*\},
     "labels": \{(?s).*\}
   \}
 \]`,
@@ -129,6 +130,7 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "pod": "linkerd-identity\-[a-f0-9]+\-[a-z0-9]+",
     "service": "linkerd-identity\.\S*",
     "weight": \d+,
+    "http2": \{(?s).*\},
     "labels": \{(?s).*\}
   \}
 \]`,
@@ -145,6 +147,7 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "pod": "linkerd-proxy-injector-[a-f0-9]+\-[a-z0-9]+",
     "service": "linkerd-proxy-injector\.\S*",
     "weight": \d+,
+    "http2": \{(?s).*\},
     "labels": \{(?s).*\}
   \}
 \]`,
@@ -161,6 +164,7 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "pod": "nginx-[a-f0-9]+\-[a-z0-9]+",
     "service": "nginx\.\S*",
     "weight": \d+,
+    "http2": \{(?s).*\},
     "labels": \{(?s).*\}
   \}
 \]`,
diff --git a/test/integration/deep/install_test.go b/test/integration/deep/install_test.go
index 2124d797c..d29d9fdef 100644
--- a/test/integration/deep/install_test.go
+++ b/test/integration/deep/install_test.go
@@ -122,6 +122,10 @@ func TestInstall(t *testing.T) {
 		cmd = append(cmd, "--linkerd-cni-enabled")
 	}
 
+	if TestHelper.NativeSidecar() {
+		cmd = append(cmd, "--set", "proxy.nativeSidecar=true")
+	}
+
 	// Pipe cmd & args to `linkerd`
 	out, err = TestHelper.LinkerdRun(cmd...)
 	if err != nil {
@@ -134,5 +138,9 @@ func TestInstall(t *testing.T) {
 			"'kubectl apply' command failed\n%s", out)
 	}
 
-	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
+	out, err = TestHelper.LinkerdRun("check", "--wait=3m")
+	if err != nil {
+		testutil.AnnotatedFatalf(t, "'linkerd check' command failed",
+			"'linkerd check' command failed\n%s", out)
+	}
 }
diff --git a/test/integration/viz/install_test.go b/test/integration/viz/install_test.go
index e6ccd16c7..6c2c4ba48 100644
--- a/test/integration/viz/install_test.go
+++ b/test/integration/viz/install_test.go
@@ -84,6 +84,10 @@ func TestInstallVizHA(t *testing.T) {
 		"--ha",
 	}
 
+	if TestHelper.NativeSidecar() {
+		cmd = append(cmd, "--set", "proxy.nativeSidecar=true")
+	}
+
 	out, err := TestHelper.LinkerdRun(cmd...)
 	if err != nil {
 		testutil.AnnotatedFatal(t, "'linkerd viz install' command failed", err)
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 74f2d8415..e19e8e233 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -38,6 +38,7 @@ type TestHelper struct {
 	uninstall            bool
 	cni                  bool
 	calico               bool
+	nativeSidecar        bool
 	defaultInboundPolicy string
 	httpClient           http.Client
 	KubernetesHelper
@@ -207,6 +208,7 @@ func NewTestHelper() *TestHelper {
 	uninstall := flag.Bool("uninstall", false, "whether to run the 'linkerd uninstall' integration test")
 	cni := flag.Bool("cni", false, "whether to install linkerd with CNI enabled")
 	calico := flag.Bool("calico", false, "whether to install calico CNI plugin")
+	nativeSidecar := flag.Bool("native-sidecar", false, "whether to install using native sidecar injection")
 	defaultInboundPolicy := flag.String("default-inbound-policy", "", "if non-empty, passed to --set proxy.defaultInboundPolicy at linkerd's install time")
 	flag.Parse()
 
@@ -252,6 +254,7 @@ func NewTestHelper() *TestHelper {
 		externalPrometheus:   *externalPrometheus,
 		cni:                  *cni,
 		calico:               *calico,
+		nativeSidecar:        *nativeSidecar,
 		uninstall:            *uninstall,
 		defaultInboundPolicy: *defaultInboundPolicy,
 	}
@@ -396,6 +399,11 @@ func (h *TestHelper) Calico() bool {
 	return h.calico
 }
 
+// NativeSidecar determines whether native sidecar injection is enabled
+func (h *TestHelper) NativeSidecar() bool {
+	return h.nativeSidecar
+}
+
 // AddInstalledExtension adds an extension name to installedExtensions to
 // track the currently installed linkerd extensions.
 func (h *TestHelper) AddInstalledExtension(extensionName string) {