Repository: cilium/cilium
Pull request #17788 (2021-11-05 00:31:12)

Additional FQDN selector identity tracking fixes
Review commit-by-commit.

Here's the top bugfix commit message:

    FQDN selectors hold references to CIDR identities, one for each
    'fqdnSelector.cachedSelections' entry. Previously, these would be
    associated with the fqdnSelector during creation of the selector but
    they were not released when the fqdnSelector is deleted from the cache.
    
    Commit de10e82639cc ("fqdn: Move identity allocation to FQDN selector")
    fixed a similar bug recently during the update of existing selectors;
    this code is balancing the release for identities allocated during the
    initial creation of each FQDN selector.

joestringer: /test
joestringer: /test
joestringer: Looking into failures:
* [ ] [test-gke](https://jenkins.cilium.io/job/Cilium-PR-K8s-GKE/6965/) - BeforeAll failures in two tests
* [x] [test-1.21-4.19](https://jenkins.cilium.io/job/Cilium-PR-K8s-1.21-kernel-4.19/191/) - Hit #17895.
* [ ] [test-1.22-4.9](https://jenkins.cilium.io/job/Cilium-PR-K8s-1.22-kernel-4.9/181/) - `pod-to-a-allowed-cnp` seems to be failing to connect, need to dig further to understand whether this is similar to previous reports of portmap chaining tests failing or whether something is going on here related to the selector changes. The connection that is having trouble is not expected to be affected by FQDN policy selector changes so there's not an obvious link to the PR at first glance.
* [ ] [test-runtime](https://jenkins.cilium.io/job/Cilium-PR-Runtime-net-next/658/) - Looks a lot like a super old issue https://github.com/cilium/cilium/issues/6347 , needs more digging.
joestringer: Jarno and I discussed some of the CI failures out-of-band, and came to the conclusion that the first commit changes too much behaviour for the current model of how we handle pod identities: Specifically, it assumes that endpoint add will update the SelectorCache and therefore the endpoint identity change or endpoint delete should *also* notify the SelectorCache. This is not true, the selectorcache references to global identities is intended to be tied to their allocation/release in the kvstore/apiserver. I will remove the behaviour change on that side from the first commit and split out the changes a bit more + document the intent behind those changes.
joestringer: Here's the latest diff against the previous version:

```
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index fa804c357afc..a7d638df0394 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -1131,7 +1131,7 @@ func (e *Endpoint) leaveLocked(proxyWaitGroup *completion.WaitGroup, conf Delete
                releaseCtx, cancel := context.WithTimeout(context.Background(), option.Config.KVstoreConnectivityTimeout)
                defer cancel()
 
-               _, err := e.allocator.Release(releaseCtx, e.SecurityIdentity, true)
+               _, err := e.allocator.Release(releaseCtx, e.SecurityIdentity, false)
                if err != nil {
                        errors = append(errors, fmt.Errorf("unable to release identity: %s", err))
                }
@@ -1856,7 +1856,16 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
        allocateCtx, cancel := context.WithTimeout(ctx, option.Config.KVstoreConnectivityTimeout)
        defer cancel()
 
-       allocatedIdentity, _, err := e.allocator.AllocateIdentity(allocateCtx, newLabels, true)
+       // Typically, SelectorCache notification happens from the identityWatcher,
+       // requiring a round-trip to the kvstore to start updating policies for
+       // other endpoints on the node.
+       //
+       // To get a jump start on plumbing the handling of the identity for
+       // this endpoint, trigger the early notification via this call. If the
+       // identity is new, then this will start updating the policy for other
+       // co-located endpoints without having to wait for that RTT.
+       notifySelectorCache := true
+       allocatedIdentity, _, err := e.allocator.AllocateIdentity(allocateCtx, newLabels, notifySelectorCache)
        if err != nil {
                err = fmt.Errorf("unable to resolve identity: %s", err)
                e.LogStatus(Other, Warning, fmt.Sprintf("%s (will retry)", err.Error()))
@@ -1872,7 +1881,7 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
        defer cancel()
 
        releaseNewlyAllocatedIdentity := func() {
-               _, err := e.allocator.Release(releaseCtx, allocatedIdentity, true)
+               _, err := e.allocator.Release(releaseCtx, allocatedIdentity, false)
                if err != nil {
                        // non fatal error as keys will expire after lease expires but log it
                        elog.WithFields(logrus.Fields{logfields.Identity: allocatedIdentity.ID}).
@@ -1932,7 +1941,7 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
        e.SetIdentity(allocatedIdentity, false)
 
        if oldIdentity != nil {
-               _, err := e.allocator.Release(releaseCtx, oldIdentity, true)
+               _, err := e.allocator.Release(releaseCtx, oldIdentity, false)
                if err != nil {
                        elog.WithFields(logrus.Fields{logfields.Identity: oldIdentity.ID}).
                                WithError(err).Warn("Unable to release old endpoint identity")
```
joestringer: /test

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000000..467fc98ebd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,83 @@
+name: Bug Report
+description: Report a bug encountered while operating Cilium
+labels: ["kind/bug", "needs/triage"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report! If you have usage questions, please try the [slack channel](http://slack.cilium.io/) and see the [FAQ](https://github.com/cilium/cilium/issues?utf8=%E2%9C%93&q=is:issue+label:kind/question+) first.
+
+        **Important**: For security related issues: We strongly encourage you to report security vulnerabilities to the private security mailing list: security@cilium.io - first, before disclosing them in any public forums.
+  - type: checkboxes
+    attributes:
+      label: Is there an existing issue for this?
+      description: Please search to see if an issue already exists for the bug you encountered.
+      options:
+      - label: I have searched the existing issues
+        required: true
+  - type: textarea
+    id: what-happened
+    attributes:
+      label: What happened?
+      description: Also tell us, what did you expect to happen?
+      placeholder: |
+        1. In this environment...
+        2. With this config...
+        3. Run '...'
+        4. See error...
+      value: "A bug happened!"
+    validations:
+      required: true
+  - type: textarea
+    id: cilium-version
+    attributes:
+      label: Cilium Version
+      description: What version of the software was running when you discovered this issue? (run `cilium version`)
+    validations:
+      required: true
+  - type: textarea
+    id: kernel-version
+    attributes:
+      label: Kernel Version
+      description: Which kernel version was Cilium running on? (run `uname -a`)
+    validations:
+      required: true
+  - type: textarea
+    id: k8s-version
+    attributes:
+      label: Kubernetes Version
+      description: Which Kubernetes version are you running? (run `kubectl version`)
+    validations:
+      required: true
+  - type: textarea
+    id: sysdump
+    attributes:
+      label: Sysdump
+      description: |
+        - [Install Cilium CLI](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-default/#install-the-cilium-cli).
+        - Run `cilium sysdump` command
+      placeholder: |
+        Upload the sysdump file here
+  - type: textarea
+    id: logs
+    attributes:
+      label: Relevant log output
+      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
+      render: shell
+  - type: textarea
+    attributes:
+      label: Anything else?
+      description: |
+        Links? References? Anything that will provide more context about the issue you are encountering!
+
+        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+    validations:
+      required: false
+  - type: checkboxes
+    id: terms
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/cilium/cilium/blob/master/CODE_OF_CONDUCT.md)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true
diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md
deleted file mode 100644
index 8e572c6acf..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_template.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-name: Bug Report
-about: Report a bug encountered while operating Cilium
-title: ''
-labels: 'kind/bug'
-assignees: ''
-
----
-
-<!--
-
-If you have usage questions, please try the [slack
-channel](http://cilium.io/slack) and see the [FAQ](https://goo.gl/qG2YmU)
-first.
-
-Choose either "Proposal" or "Bug report"
-
--->
-
-## Bug report
-
-<!--
-
-Important: For security related issues: We strongly encourage you to report
-security vulnerabilities to our private security mailing list:
-security@cilium.io - first, before disclosing them in any public forums.
-
--->
-
-**General Information**
-
-- Cilium version (run `cilium version`)
-- Kernel version (run `uname -a`)
-- Orchestration system version in use (e.g. `kubectl version`, ...)
-- Link to relevant artifacts (policies, deployments scripts, ...)
-- Generate and upload a sysdump:
-
-  - [Install Cilium CLI](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-default/#install-the-cilium-cli).
-  - Run `cilium sysdump` command:
-
-    ```
-    cilium sysdump
-    ```
-
-**How to reproduce the issue**
-
-1. instruction 1
-2. instruction 2
diff --git a/AUTHORS b/AUTHORS
index 1a6a8558f3..40d2f1df1b 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -58,7 +58,6 @@ Charles-Edouard Brétéché                charled.breteche@gmail.com
 Charles-Henri Guérin                    charles-henri.guerin@zenika.com
 chenyahui                               chenyahui9@jd.com
 Chen Yaqi                               chenyaqi01@baidu.com
-chez-shanpu                             tomoki.sugiura@mail.shanpu.info
 Chris Tarazi                            chris@isovalent.com
 Christian Hörtnagl                      christian2@univie.ac.at
 Christian Hüning                        christian.huening@finleap.com
@@ -362,4 +361,4 @@ project:
 Brenden Blanco                          bblanco@plumgrid.com
 Jakub Kicinski                          jakub.kicinski@netronome.com
 Salvatore Orlando                       salv.orlando@gmail.com
-Tomás Senart                            tsenart@gmail.com
\ No newline at end of file
+Tomás Senart                            tsenart@gmail.com
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index e25d13e812..6685015777 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -225,6 +225,7 @@ cilium-agent [flags]
       --proxy-prometheus-port int                            Port to serve Envoy metrics on. Default 0 (disabled).
       --read-cni-conf string                                 Read to the CNI configuration at specified path to extract per node configuration
       --restore                                              Restores state, if possible, from previous daemon (default true)
+      --route-metric int                                     Overwrite the metric used by cilium when adding routes to its 'cilium_host' device
       --sidecar-istio-proxy-image string                     Regular expression matching compatible Istio sidecar istio-proxy container image names (default "cilium/istio_proxy")
       --single-cluster-route                                 Use a single cluster route instead of per node routes
       --socket-path string                                   Sets daemon's socket path to listen for connections (default "/var/run/cilium/cilium.sock")
diff --git a/Documentation/concepts/kubernetes/compatibility-table.rst b/Documentation/concepts/kubernetes/compatibility-table.rst
index aeda776ef4..98b172c9b6 100644
--- a/Documentation/concepts/kubernetes/compatibility-table.rst
+++ b/Documentation/concepts/kubernetes/compatibility-table.rst
@@ -58,5 +58,7 @@
 +-----------------+----------------+
 | v1.11.0-rc1     | 1.24.2         |
 +-----------------+----------------+
+| v1.11.0-rc2     | 1.24.2         |
++-----------------+----------------+
 | latest / master | 1.24.2         |
 +-----------------+----------------+
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 9f86dd4814..9376e75475 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -525,7 +525,7 @@ static __always_inline int handle_ipv4_from_lxc(struct __ctx_buff *ctx,
 	__u32 __maybe_unused tunnel_endpoint = 0;
 	__u8 __maybe_unused encrypt_key = 0;
 	__u32 monitor = 0;
-	__u8 reason;
+	__u8 ct_ret;
 	bool hairpin_flow = false; /* endpoint wants to access itself via service IP */
 	__u8 policy_match_type = POLICY_MATCH_NONE;
 	__u8 audited = 0;
@@ -600,15 +600,13 @@ skip_service_lookup:
 	 * POLICY_SKIP if the packet is a reply packet to an existing incoming
 	 * connection.
 	 */
-	ret = ct_lookup4(get_ct_map4(&tuple), &tuple, ctx, l4_off, CT_EGRESS,
-			 &ct_state, &monitor);
-	if (ret < 0)
-		return ret;
-
-	reason = ret;
+	ct_ret = ct_lookup4(get_ct_map4(&tuple), &tuple, ctx, l4_off, CT_EGRESS,
+			    &ct_state, &monitor);
+	if (ct_ret < 0)
+		return ct_ret;
 
 	/* Check it this is return traffic to an ingress proxy. */
-	if ((ret == CT_REPLY || ret == CT_RELATED) && ct_state.proxy_redirect) {
+	if ((ct_ret == CT_REPLY || ct_ret == CT_RELATED) && ct_state.proxy_redirect) {
 		/* Stack will do a socket match and deliver locally. */
 		return ctx_redirect_to_proxy4(ctx, &tuple, 0, false);
 	}
@@ -657,7 +655,7 @@ skip_service_lookup:
 	verdict = policy_can_egress4(ctx, &tuple, SECLABEL, *dst_id,
 				     &policy_match_type, &audited);
 
-	if (ret != CT_REPLY && ret != CT_RELATED && verdict < 0) {
+	if (ct_ret != CT_REPLY && ct_ret != CT_RELATED && verdict < 0) {
 		send_policy_verdict_notify(ctx, *dst_id, tuple.dport,
 					   tuple.nexthdr, POLICY_EGRESS, 0,
 					   verdict, policy_match_type, audited);
@@ -665,7 +663,7 @@ skip_service_lookup:
 	}
 
 skip_policy_enforcement:
-	switch (ret) {
+	switch (ct_ret) {
 	case CT_NEW:
 		if (!hairpin_flow)
 			send_policy_verdict_notify(ctx, *dst_id, tuple.dport,
@@ -735,10 +733,10 @@ ct_recreate4:
 
 	hairpin_flow |= ct_state.loopback;
 
-	if (redirect_to_proxy(verdict, reason)) {
+	if (redirect_to_proxy(verdict, ct_ret)) {
 		/* Trace the packet before it is forwarded to proxy */
 		send_trace_notify(ctx, TRACE_TO_PROXY, SECLABEL, 0,
-				  bpf_ntohs(verdict), 0, reason, monitor);
+				  bpf_ntohs(verdict), 0, ct_ret, monitor);
 		return ctx_redirect_to_proxy4(ctx, &tuple, verdict, false);
 	}
 
@@ -799,6 +797,14 @@ ct_recreate4:
 		if (is_cluster_destination(ip4, *dst_id, tunnel_endpoint))
 			goto skip_egress_gateway;
 
+		/* If the packet is a reply or is related, it means that outside
+		 * has initiated the connection, and so we should skip egress
+		 * gateway, since an egress policy is only matching connections
+		 * originating from a pod.
+		 */
+		if (ct_ret == CT_REPLY || ct_ret == CT_RELATED)
+			goto skip_egress_gateway;
+
 		info = lookup_ip4_egress_endpoint(ip4->saddr, ip4->daddr);
 		if (!info)
 			goto skip_egress_gateway;
@@ -856,7 +862,7 @@ skip_egress_gateway:
 to_host:
 	if (is_defined(ENABLE_HOST_FIREWALL) && *dst_id == HOST_ID) {
 		send_trace_notify(ctx, TRACE_TO_HOST, SECLABEL, HOST_ID, 0,
-				  HOST_IFINDEX, reason, monitor);
+				  HOST_IFINDEX, ct_ret, monitor);
 		return ctx_redirect(ctx, HOST_IFINDEX, BPF_F_INGRESS);
 	}
 #endif
@@ -898,7 +904,7 @@ pass_to_stack:
 encrypt_to_stack:
 #endif
 	send_trace_notify(ctx, TRACE_TO_STACK, SECLABEL, *dst_id, 0, 0,
-			  reason, monitor);
+			  ct_ret, monitor);
 	cilium_dbg_capture(ctx, DBG_CAPTURE_DELIVERY, 0);
 	return CTX_ACT_OK;
 }
diff --git a/bpf/lib/nodeport.h b/bpf/lib/nodeport.h
index 841f86865a..83e2096fbf 100644
--- a/bpf/lib/nodeport.h
+++ b/bpf/lib/nodeport.h
@@ -1144,7 +1144,32 @@ static __always_inline bool snat_v4_needed(struct __ctx_buff *ctx, __be32 *addr,
 		if (identity_is_remote_node(info->sec_label))
 			return false;
 #endif
-#if defined(ENABLE_EGRESS_GATEWAY)
+
+		/* Check if this packet belongs to reply traffic coming from a
+		 * local endpoint.
+		 *
+		 * If ep is NULL, it means there's no endpoint running on the
+		 * node which matches the packet source IP, which means we can
+		 * skip the CT lookup since this cannot be reply traffic.
+		 */
+		if (ep) {
+			bool is_reply = false;
+			struct ipv4_ct_tuple tuple = {
+				.nexthdr = ip4->protocol,
+				.daddr = ip4->daddr,
+				.saddr = ip4->saddr
+			};
+
+			/* If the packet is a reply it means that outside has
+			 * initiated the connection, so no need to SNAT the
+			 * reply.
+			 */
+			if (!ct_is_reply4(get_ct_map4(&tuple), ctx, ETH_HLEN + ipv4_hdrlen(ip4),
+					  &tuple, &is_reply) && is_reply)
+				return false;
+		}
+
+ #if defined(ENABLE_EGRESS_GATEWAY)
 		/* Check egress gateway policy only for traffic which matches
 		 * one of the following conditions.
 		 *  - Not from a local endpoint (inc. local host): that tells us
@@ -1173,25 +1198,7 @@ static __always_inline bool snat_v4_needed(struct __ctx_buff *ctx, __be32 *addr,
 			}
 		}
 #endif
-
 		if (ep) {
-			bool is_reply = false;
-			struct ipv4_ct_tuple tuple = {
-				.nexthdr = ip4->protocol,
-				.daddr = ip4->daddr,
-				.saddr = ip4->saddr
-			};
-
-			/* The packet is a reply, which means that outside
-			 * has initiated the connection, so no need to SNAT
-			 * the reply.
-			 */
-			if (!ct_is_reply4(get_ct_map4(&tuple), ctx,
-					  ETH_HLEN + ipv4_hdrlen(ip4),
-					  &tuple, &is_reply) &&
-			    is_reply)
-				return false;
-
 			*from_endpoint = true;
 			*addr = IPV4_MASQUERADE;
 			return true;
diff --git a/clustermesh-apiserver/vmmanager.go b/clustermesh-apiserver/vmmanager.go
index 5a1f2f9e53..30f9267825 100644
--- a/clustermesh-apiserver/vmmanager.go
+++ b/clustermesh-apiserver/vmmanager.go
@@ -170,6 +170,7 @@ func (m *VMManager) OnUpdate(k store.Key) {
 			nk := nodeOverrideFromCEW(n, cew)
 
 			log.Debugf("CEW: VM Cilium Node updated: %v -> %v", n, nk)
+			// FIXME: GH-17909 Balance this call with a call to release the identity.
 			id := m.AllocateNodeIdentity(nk)
 			if id != nil {
 				nid := id.ID.Uint32()
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 55ca6477f8..a1c930ed32 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -723,6 +723,9 @@ func initializeFlags() {
 	flags.Int(option.MTUName, 0, "Overwrite auto-detected MTU of underlying network")
 	option.BindEnv(option.MTUName)
 
+	flags.Int(option.RouteMetric, 0, "Overwrite the metric used by cilium when adding routes to its 'cilium_host' device")
+	option.BindEnv(option.RouteMetric)
+
 	flags.Bool(option.PrependIptablesChainsName, true, "Prepend custom iptables chains instead of appending")
 	option.BindEnvWithLegacyEnvFallback(option.PrependIptablesChainsName, "CILIUM_PREPEND_IPTABLES_CHAIN")
 
@@ -1411,6 +1414,10 @@ func initEnv(cmd *cobra.Command) {
 		option.Config.EnableBandwidthManager = false
 	}
 
+	if option.Config.EnableIPv6Masquerade && option.Config.EnableBPFMasquerade {
+		log.Fatal("BPF masquerade is not supported for IPv6.")
+	}
+
 	// If there is one device specified, use it to derive better default
 	// allocation prefixes
 	node.InitDefaultPrefix(option.Config.DirectRoutingDevice)
diff --git a/go.mod b/go.mod
index 1fe7c7d9bd..bd2f1f6b0e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,16 +3,16 @@ module github.com/cilium/cilium
 go 1.17
 
 require (
-	github.com/Azure/azure-sdk-for-go v59.2.0+incompatible
+	github.com/Azure/azure-sdk-for-go v59.3.0+incompatible
 	github.com/Azure/go-autorest/autorest v0.11.22
 	github.com/Azure/go-autorest/autorest/adal v0.9.17
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.9
 	github.com/Azure/go-autorest/autorest/to v0.4.0
-	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1331
+	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1334
 	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
-	github.com/aws/aws-sdk-go-v2 v1.11.0
+	github.com/aws/aws-sdk-go-v2 v1.11.1
 	github.com/aws/aws-sdk-go-v2/config v1.10.0
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.21.0
 	github.com/aws/smithy-go v1.9.0
 	github.com/blang/semver/v4 v4.0.0
@@ -25,7 +25,7 @@ require (
 	github.com/containernetworking/cni v1.0.1
 	github.com/containernetworking/plugins v1.0.1
 	github.com/davecgh/go-spew v1.1.1
-	github.com/docker/docker v20.10.10+incompatible
+	github.com/docker/docker v20.10.11+incompatible
 	github.com/docker/libnetwork v0.8.0-dev.2.0.20210525090646-64b7a4574d14
 	github.com/fsnotify/fsnotify v1.5.1
 	github.com/go-openapi/errors v0.20.1
diff --git a/go.sum b/go.sum
index dc01aff7ff..10061eeb24 100644
--- a/go.sum
+++ b/go.sum
@@ -45,8 +45,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go v59.2.0+incompatible h1:mbxiZy1K820hQ+dI+YIO/+a0wQDYqOu18BAGe4lXjVk=
-github.com/Azure/azure-sdk-for-go v59.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ=
+github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -137,8 +137,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
 github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=
-github.com/aliyun/alibaba-cloud-sdk-go v1.61.1331 h1:ZHq8IcJduKoedSNx4YwI1hKxDV2k3bR8yBMFMKMh1H8=
-github.com/aliyun/alibaba-cloud-sdk-go v1.61.1331/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1334 h1:srHjV1RSRu3x34mBmpniAq8kIWd+OU7P3uazQp79OlE=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1334/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
@@ -164,14 +164,16 @@ github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
 github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
 github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
-github.com/aws/aws-sdk-go-v2 v1.11.0 h1:HxyD62DyNhCfiFGUHqJ/xITD6rAjJ7Dm/2nLxLmO4Ag=
 github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
+github.com/aws/aws-sdk-go-v2 v1.11.1 h1:GzvOVAdTbWxhEMRK4FfiblkGverOkAT0UodDxC1jHQM=
+github.com/aws/aws-sdk-go-v2 v1.11.1/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
 github.com/aws/aws-sdk-go-v2/config v1.10.0 h1:4i+/7DmCQCAls5Z61giur0LOPZ3PXFwnSIw7hRamzws=
 github.com/aws/aws-sdk-go-v2/config v1.10.0/go.mod h1:xuqoV5etD3N3B8Ts9je4ijgAv6mb+6NiOPFMUhwRcjA=
 github.com/aws/aws-sdk-go-v2/credentials v1.6.0 h1:L3O6osQTlzLKRmiTphw2QJuD21EFapWCX4IipiRJhAE=
 github.com/aws/aws-sdk-go-v2/credentials v1.6.0/go.mod h1:rQkYdQPDXRrvPLeEuCNwSgtwMzBo9eDGWlTNC69Sh/0=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0 h1:OpZjuUy8Jt3CA1WgJgBC5Bz+uOjE5Ppx4NFTRaooUuA=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0/go.mod h1:5E1J3/TTYy6z909QNR0QnXGBpfESYGDqd3O0zqONghU=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1 h1:pXwGBINU30CsjYztV/IyCgA7QKp99Q8wM4Gb0Ls3rB0=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1/go.mod h1:MYiG3oeEcmrdBOV7JOIWhionzyRZJWCnByS5FmvhAoU=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0 h1:zY8cNmbBXt3pzjgWgdIbzpQ6qxoCwt+Nx9JbrAf2mbY=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0/go.mod h1:NO3Q5ZTTQtO2xIg2+xTXYDiT7knSejfeDm7WGDaOo0U=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.0 h1:Z3aR/OXBnkYK9zXkNkfitHX6SmUBzSsx8VMHbH4Lvhw=
@@ -412,8 +414,8 @@ github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TT
 github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v20.10.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM=
-github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
+github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
 github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
 github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
diff --git a/install/kubernetes/cilium/files/nodeinit/poststart.bash b/install/kubernetes/cilium/files/nodeinit/poststart-eni.bash
similarity index 100%
rename from install/kubernetes/cilium/files/nodeinit/poststart.bash
rename to install/kubernetes/cilium/files/nodeinit/poststart-eni.bash
diff --git a/install/kubernetes/cilium/templates/cilium-nodeinit/daemonset.yaml b/install/kubernetes/cilium/templates/cilium-nodeinit/daemonset.yaml
index c6660f24f1..f2411a04c6 100644
--- a/install/kubernetes/cilium/templates/cilium-nodeinit/daemonset.yaml
+++ b/install/kubernetes/cilium/templates/cilium-nodeinit/daemonset.yaml
@@ -51,14 +51,14 @@ spec:
             - mountPath: /run/xtables.lock
               name: xtables-lock
           lifecycle:
-{{- if and (.Values.eni.enabled) (not .Values.enableIPv4Masquerade) }}
+{{- if .Values.eni.enabled }}
             postStart:
               exec:
                 command:
                   - "/bin/sh"
                   - "-c"
                   - |
-                    {{- tpl (.Files.Get "files/nodeinit/poststart.bash") . | nindent 20 }}
+                    {{- tpl (.Files.Get "files/nodeinit/poststart-eni.bash") . | nindent 20 }}
 {{- end }}
 {{- if .Values.nodeinit.revertReconfigureKubelet }}
             preStop:
diff --git a/pkg/datapath/linux/node.go b/pkg/datapath/linux/node.go
index b758f1d0ad..bbea345e38 100644
--- a/pkg/datapath/linux/node.go
+++ b/pkg/datapath/linux/node.go
@@ -354,11 +354,12 @@ func (n *linuxNodeHandler) createNodeRouteSpec(prefix *cidr.CIDR, isLocalNode bo
 
 	// The default routing table accounts for encryption overhead for encrypt-node traffic
 	return route.Route{
-		Nexthop: &nexthop,
-		Local:   local,
-		Device:  n.datapathConfig.HostDevice,
-		Prefix:  *prefix.IPNet,
-		MTU:     mtu,
+		Nexthop:  &nexthop,
+		Local:    local,
+		Device:   n.datapathConfig.HostDevice,
+		Prefix:   *prefix.IPNet,
+		MTU:      mtu,
+		Priority: option.Config.RouteMetric,
 	}, nil
 }
 
diff --git a/pkg/datapath/linux/route/route.go b/pkg/datapath/linux/route/route.go
index a0ece2f807..a5430ddaf3 100644
--- a/pkg/datapath/linux/route/route.go
+++ b/pkg/datapath/linux/route/route.go
@@ -14,15 +14,16 @@ import (
 )
 
 type Route struct {
-	Prefix  net.IPNet
-	Nexthop *net.IP
-	Local   net.IP
-	Device  string
-	MTU     int
-	Proto   int
-	Scope   netlink.Scope
-	Table   int
-	Type    int
+	Prefix   net.IPNet
+	Nexthop  *net.IP
+	Local    net.IP
+	Device   string
+	MTU      int
+	Priority int
+	Proto    int
+	Scope    netlink.Scope
+	Table    int
+	Type     int
 }
 
 // LogFields returns the route attributes as logrus.Fields map
@@ -59,6 +60,9 @@ func (r *Route) ToIPCommand(dev string) []string {
 		res = append(res, "-6")
 	}
 	res = append(res, "route", "add", r.Prefix.String())
+	if r.Priority != 0 {
+		res = append(res, "metric", fmt.Sprintf("%d", r.Priority))
+	}
 	if r.Nexthop != nil {
 		res = append(res, "via", r.Nexthop.String())
 	}
diff --git a/pkg/datapath/linux/route/route_linux.go b/pkg/datapath/linux/route/route_linux.go
index ee5f32527e..e128effc2d 100644
--- a/pkg/datapath/linux/route/route_linux.go
+++ b/pkg/datapath/linux/route/route_linux.go
@@ -42,6 +42,7 @@ func (r *Route) getNetlinkRoute() netlink.Route {
 		Dst:      &r.Prefix,
 		Src:      r.Local,
 		MTU:      r.MTU,
+		Priority: r.Priority,
 		Protocol: netlink.RouteProtocol(r.Proto),
 		Table:    r.Table,
 		Type:     r.Type,
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index 0b1ed9f6ee..a7d638df03 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -1131,7 +1131,7 @@ func (e *Endpoint) leaveLocked(proxyWaitGroup *completion.WaitGroup, conf Delete
 		releaseCtx, cancel := context.WithTimeout(context.Background(), option.Config.KVstoreConnectivityTimeout)
 		defer cancel()
 
-		_, err := e.allocator.Release(releaseCtx, e.SecurityIdentity)
+		_, err := e.allocator.Release(releaseCtx, e.SecurityIdentity, false)
 		if err != nil {
 			errors = append(errors, fmt.Errorf("unable to release identity: %s", err))
 		}
@@ -1856,7 +1856,16 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
 	allocateCtx, cancel := context.WithTimeout(ctx, option.Config.KVstoreConnectivityTimeout)
 	defer cancel()
 
-	allocatedIdentity, _, err := e.allocator.AllocateIdentity(allocateCtx, newLabels, true)
+	// Typically, SelectorCache notification happens from the identityWatcher,
+	// requiring a round-trip to the kvstore to start updating policies for
+	// other endpoints on the node.
+	//
+	// To get a jump start on plumbing the handling of the identity for
+	// this endpoint, trigger the early notification via this call. If the
+	// identity is new, then this will start updating the policy for other
+	// co-located endpoints without having to wait for that RTT.
+	notifySelectorCache := true
+	allocatedIdentity, _, err := e.allocator.AllocateIdentity(allocateCtx, newLabels, notifySelectorCache)
 	if err != nil {
 		err = fmt.Errorf("unable to resolve identity: %s", err)
 		e.LogStatus(Other, Warning, fmt.Sprintf("%s (will retry)", err.Error()))
@@ -1872,7 +1881,7 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
 	defer cancel()
 
 	releaseNewlyAllocatedIdentity := func() {
-		_, err := e.allocator.Release(releaseCtx, allocatedIdentity)
+		_, err := e.allocator.Release(releaseCtx, allocatedIdentity, false)
 		if err != nil {
 			// non fatal error as keys will expire after lease expires but log it
 			elog.WithFields(logrus.Fields{logfields.Identity: allocatedIdentity.ID}).
@@ -1932,7 +1941,7 @@ func (e *Endpoint) identityLabelsChanged(ctx context.Context, myChangeRev int) (
 	e.SetIdentity(allocatedIdentity, false)
 
 	if oldIdentity != nil {
-		_, err := e.allocator.Release(releaseCtx, oldIdentity)
+		_, err := e.allocator.Release(releaseCtx, oldIdentity, false)
 		if err != nil {
 			elog.WithFields(logrus.Fields{logfields.Identity: oldIdentity.ID}).
 				WithError(err).Warn("Unable to release old endpoint identity")
diff --git a/pkg/fqdn/helpers.go b/pkg/fqdn/helpers.go
index 2f664556bc..3fc52bbce0 100644
--- a/pkg/fqdn/helpers.go
+++ b/pkg/fqdn/helpers.go
@@ -14,13 +14,13 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-// mapSelectorsToIPs iterates through a set of FQDNSelectors and evalutes whether
-// they match the DNS Names in the cache. If so, the set of IPs which the cache
-// maintains as mapping to each DNS Name are mapped to the matching FQDNSelector.
-// Returns the mapping of DNSName to set of IPs which back said DNS name, the
-// set of FQDNSelectors which do not map to any IPs, and the set of
-// FQDNSelectors mapping to a set of IPs.
-func mapSelectorsToIPs(fqdnSelectors map[api.FQDNSelector]struct{}, cache *DNSCache) (selectorsMissingIPs []api.FQDNSelector, selectorIPMapping map[api.FQDNSelector][]net.IP) {
+// MapSelectorsToIPsLocked iterates through a set of FQDNSelectors and evalutes
+// whether they match the DNS Names in the cache. If so, the set of IPs which
+// the cache maintains as mapping to each DNS Name are mapped to the matching
+// FQDNSelector. Returns the mapping of DNSName to set of IPs which back said
+// DNS name, the set of FQDNSelectors which do not map to any IPs, and the set
+// of FQDNSelectors mapping to a set of IPs.
+func (n *NameManager) MapSelectorsToIPsLocked(fqdnSelectors map[api.FQDNSelector]struct{}) (selectorsMissingIPs []api.FQDNSelector, selectorIPMapping map[api.FQDNSelector][]net.IP) {
 	missing := make(map[api.FQDNSelector]struct{}) // a set to dedup missing dnsNames
 	selectorIPMapping = make(map[api.FQDNSelector][]net.IP)
 
@@ -32,7 +32,7 @@ func mapSelectorsToIPs(fqdnSelectors map[api.FQDNSelector]struct{}, cache *DNSCa
 		// lookup matching DNS names
 		if len(ToFQDN.MatchName) > 0 {
 			dnsName := prepareMatchName(ToFQDN.MatchName)
-			lookupIPs := cache.Lookup(dnsName)
+			lookupIPs := n.cache.Lookup(dnsName)
 
 			// Mark this FQDNSelector as having no IPs corresponding to it.
 			// FQDNSelectors are guaranteed to have only their MatchName OR
@@ -62,7 +62,7 @@ func mapSelectorsToIPs(fqdnSelectors map[api.FQDNSelector]struct{}, cache *DNSCa
 			if patternRE, err = regexp.Compile(patternREStr); err != nil {
 				log.WithError(err).Error("Error compiling matchPattern")
 			}
-			lookupIPs := cache.LookupByRegexp(patternRE)
+			lookupIPs := n.cache.LookupByRegexp(patternRE)
 
 			// Mark this pattern missing; it will be unmarked in the loop below
 			missing[ToFQDN] = struct{}{}
diff --git a/pkg/fqdn/name_manager.go b/pkg/fqdn/name_manager.go
index 54bbb949f6..2dfafdac96 100644
--- a/pkg/fqdn/name_manager.go
+++ b/pkg/fqdn/name_manager.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/cilium/cilium/api/v1/models"
 	"github.com/cilium/cilium/pkg/identity"
-	"github.com/cilium/cilium/pkg/identity/cache"
 	"github.com/cilium/cilium/pkg/ip"
 	"github.com/cilium/cilium/pkg/lock"
 	"github.com/cilium/cilium/pkg/logging/logfields"
@@ -83,11 +82,11 @@ func (n *NameManager) Unlock() {
 // the FQDNSelector. All IPs which correspond to the DNS names which match this
 // Selector will be returned as CIDR identities, as other DNS Names which have
 // already been resolved may match this FQDNSelector.
-func (n *NameManager) RegisterForIdentityUpdatesLocked(idAllocator cache.IdentityAllocator, selector api.FQDNSelector) []identity.NumericIdentity {
+func (n *NameManager) RegisterForIdentityUpdatesLocked(selector api.FQDNSelector) {
 	_, exists := n.allSelectors[selector]
 	if exists {
 		log.WithField("fqdnSelector", selector).Warning("FQDNSelector was already registered for updates, returning without any identities")
-		return nil
+		return
 	}
 
 	// This error should never occur since the FQDNSelector has already been
@@ -95,40 +94,15 @@ func (n *NameManager) RegisterForIdentityUpdatesLocked(idAllocator cache.Identit
 	regex, err := selector.ToRegex()
 	if err != nil {
 		log.WithError(err).WithField("fqdnSelector", selector).Error("FQDNSelector did not compile to valid regex")
-		return nil
+		return
 	}
 
 	n.allSelectors[selector] = regex
-	_, selectorIPMapping := mapSelectorsToIPs(map[api.FQDNSelector]struct{}{selector: {}}, n.cache)
-
-	// Allocate identities for each IPNet and then map to selector
-	selectorIPs := selectorIPMapping[selector]
-	log.WithFields(logrus.Fields{
-		"fqdnSelector": selector,
-		"ips":          selectorIPs,
-	}).Debug("getting identities for IPs associated with FQDNSelector")
-	var currentlyAllocatedIdentities []*identity.Identity
-	// TODO: Consider if upserts to ipcache should be delayed until endpoint policies have been
-	// updated. This is the path from policy updates rather than for DNS proxy results. Hence
-	// any existing IPs would typically already have been pushed to the ipcache as they would
-	// not be newly allocated. We need the 'allocation' here to get a reference count on the
-	// allocations.
-	if currentlyAllocatedIdentities, err = idAllocator.AllocateCIDRsForIPs(selectorIPs, nil); err != nil {
-		log.WithError(err).WithField("prefixes", selectorIPs).Warn(
-			"failed to allocate identities for IPs")
-		return nil
-	}
-	numIDs := make([]identity.NumericIdentity, 0, len(currentlyAllocatedIdentities))
-	for i := range currentlyAllocatedIdentities {
-		numIDs = append(numIDs, currentlyAllocatedIdentities[i].ID)
-	}
-
-	return numIDs
 }
 
 // UnregisterForIdentityUpdatesLocked removes this FQDNSelector from the set of
-// FQDNSelectors which are being tracked by the NameManager. No more updates for IPs
-// which correspond to said selector are propagated.
+// FQDNSelectors which are being tracked by the NameManager. No more updates
+// for IPs which correspond to said selector are propagated.
 func (n *NameManager) UnregisterForIdentityUpdatesLocked(selector api.FQDNSelector) {
 	delete(n.allSelectors, selector)
 }
@@ -203,7 +177,7 @@ func (n *NameManager) ForceGenerateDNS(ctx context.Context, namesToRegen []strin
 		}
 	}
 
-	namesMissingIPs, selectorIPMapping := mapSelectorsToIPs(affectedFQDNSels, n.cache)
+	namesMissingIPs, selectorIPMapping := n.MapSelectorsToIPsLocked(affectedFQDNSels)
 	if len(namesMissingIPs) != 0 {
 		log.WithField(logfields.DNSName, namesMissingIPs).
 			Debug("No IPs to insert when generating DNS name selected by ToFQDN rule")
@@ -269,7 +243,7 @@ perDNSName:
 // map. Returns the set of FQDNSelectors which map to no IPs, and a mapping
 // of FQDNSelectors to IPs.
 func (n *NameManager) generateSelectorUpdates(fqdnSelectors map[api.FQDNSelector]struct{}) (namesMissingIPs []api.FQDNSelector, selectorIPMapping map[api.FQDNSelector][]net.IP) {
-	return mapSelectorsToIPs(fqdnSelectors, n.cache)
+	return n.MapSelectorsToIPsLocked(fqdnSelectors)
 }
 
 // updateIPsName will update the IPs for dnsName. It always retains a copy of
diff --git a/pkg/identity/cache/allocator.go b/pkg/identity/cache/allocator.go
index 2009af24d2..b92c26244f 100644
--- a/pkg/identity/cache/allocator.go
+++ b/pkg/identity/cache/allocator.go
@@ -122,7 +122,7 @@ type IdentityAllocator interface {
 
 	// Release is the reverse operation of AllocateIdentity() and releases the
 	// specified identity.
-	Release(context.Context, *identity.Identity) (released bool, err error)
+	Release(context.Context, *identity.Identity, bool) (released bool, err error)
 
 	// ReleaseSlice is the slice variant of Release().
 	ReleaseSlice(context.Context, IdentityAllocatorOwner, []*identity.Identity) error
@@ -404,11 +404,17 @@ func (m *CachingIdentityAllocator) AllocateIdentity(ctx context.Context, lbls la
 // Release is the reverse operation of AllocateIdentity() and releases the
 // identity again. This function may result in kvstore operations.
 // After the last user has released the ID, the returned lastUse value is true.
-func (m *CachingIdentityAllocator) Release(ctx context.Context, id *identity.Identity) (released bool, err error) {
+func (m *CachingIdentityAllocator) Release(ctx context.Context, id *identity.Identity, notifyOwner bool) (released bool, err error) {
 	defer func() {
 		if released {
 			metrics.Identity.Dec()
 		}
+		if m.owner != nil && released && notifyOwner {
+			deleted := IdentityCache{
+				id.ID: id.LabelArray,
+			}
+			m.owner.UpdateIdentities(nil, deleted)
+		}
 	}()
 
 	// Ignore reserved identities.
@@ -418,15 +424,7 @@ func (m *CachingIdentityAllocator) Release(ctx context.Context, id *identity.Ide
 
 	if !identity.RequiresGlobalIdentity(id.Labels) {
 		<-m.localIdentityAllocatorInitialized
-		lastUse := m.localIdentities.release(id)
-		// Notify release of locally managed identities on last use
-		if m.owner != nil && lastUse {
-			deleted := IdentityCache{
-				id.ID: id.LabelArray,
-			}
-			m.owner.UpdateIdentities(nil, deleted)
-		}
-		return lastUse, nil
+		return m.localIdentities.release(id), nil
 	}
 
 	// This will block until the kvstore can be accessed and all identities
@@ -458,7 +456,7 @@ func (m *CachingIdentityAllocator) ReleaseSlice(ctx context.Context, owner Ident
 		if id == nil {
 			continue
 		}
-		_, err2 := m.Release(ctx, id)
+		_, err2 := m.Release(ctx, id, false)
 		if err2 != nil {
 			log.WithError(err2).WithFields(logrus.Fields{
 				logfields.Identity: id,
diff --git a/pkg/ipcache/cidr.go b/pkg/ipcache/cidr.go
index d0d14fb47b..c215ba1303 100644
--- a/pkg/ipcache/cidr.go
+++ b/pkg/ipcache/cidr.go
@@ -140,7 +140,7 @@ func allocate(prefix *net.IPNet, lbls labels.Labels) (*identity.Identity, bool,
 
 func releaseCIDRIdentities(ctx context.Context, identities map[string]*identity.Identity) {
 	for prefix, id := range identities {
-		released, err := IdentityAllocator.Release(ctx, id)
+		released, err := IdentityAllocator.Release(ctx, id, false)
 		if err != nil {
 			log.WithFields(logrus.Fields{
 				logfields.Identity: id,
diff --git a/pkg/ipcache/metadata.go b/pkg/ipcache/metadata.go
index 8523d6f046..d1cebe2c4c 100644
--- a/pkg/ipcache/metadata.go
+++ b/pkg/ipcache/metadata.go
@@ -138,7 +138,7 @@ func InjectLabels(src source.Source, updater identityUpdater, triggerer policyTr
 			// Unlikely, but to balance the allocation / release
 			// we must either add the identity to `toUpsert`, or
 			// immediately release it again. Otherwise it will leak.
-			if _, err := IdentityAllocator.Release(context.TODO(), id); err != nil {
+			if _, err := IdentityAllocator.Release(context.TODO(), id, false); err != nil {
 				log.WithError(err).WithFields(logrus.Fields{
 					logfields.IPAddr: prefix,
 					logfields.Labels: lbls,
@@ -193,7 +193,7 @@ func injectLabels(prefix string, lbls labels.Labels) (*identity.Identity, bool,
 				logfields.IdentityLabels: realID.Labels,
 			})
 
-			released, err := IdentityAllocator.Release(context.TODO(), realID)
+			released, err := IdentityAllocator.Release(context.TODO(), realID, false)
 			if err != nil {
 				scopedLog.WithError(err).Warn(
 					"Failed to release previously assigned identity to IP, this might be a leak.",
@@ -365,7 +365,7 @@ func RemoveLabels(prefix string, lbls labels.Labels, src source.Source) labels.L
 	if realID == nil {
 		return nil
 	}
-	released, err := IdentityAllocator.Release(context.TODO(), realID)
+	released, err := IdentityAllocator.Release(context.TODO(), realID, false)
 	if err != nil {
 		log.WithError(err).WithFields(logrus.Fields{
 			logfields.IPAddr:         prefix,
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 9f79105c26..dbfe49d16d 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -1260,6 +1260,7 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 				Namespace: Namespace,
 				Name:      "endpoint_propagation_delay_seconds",
 				Help:      "CiliumEndpoint roundtrip propagation delay in seconds",
+				Buckets:   []float64{.05, .1, 1, 5, 30, 60, 120, 240, 300, 600},
 			}, []string{})
 
 			collectors = append(collectors, EndpointPropagationDelay)
diff --git a/pkg/option/config.go b/pkg/option/config.go
index e76c5a0835..d953d7f524 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -445,6 +445,9 @@ const (
 	// MTUName is the name of the MTU option
 	MTUName = "mtu"
 
+	// RouteMetric is the name of the route-metric option
+	RouteMetric = "route-metric"
+
 	// DatapathMode is the name of the DatapathMode option
 	DatapathMode = "datapath-mode"
 
@@ -1282,6 +1285,9 @@ type DaemonConfig struct {
 	// MTU is the maximum transmission unit of the underlying network
 	MTU int
 
+	// RouteMetric is the metric used for the routes added to the cilium_host device
+	RouteMetric int
+
 	// ClusterName is the name of the cluster
 	ClusterName string
 
@@ -2268,6 +2274,10 @@ func (c *DaemonConfig) Validate() error {
 		return fmt.Errorf("MTU '%d' cannot be negative", c.MTU)
 	}
 
+	if c.RouteMetric < 0 {
+		return fmt.Errorf("RouteMetric '%d' cannot be negative", c.RouteMetric)
+	}
+
 	if c.IPAM == ipamOption.IPAMENI && c.EnableIPv6 {
 		return fmt.Errorf("IPv6 cannot be enabled in ENI IPAM mode")
 	}
@@ -2572,6 +2582,7 @@ func (c *DaemonConfig) Populate() {
 	c.ProxyPrometheusPort = viper.GetInt(ProxyPrometheusPort)
 	c.ReadCNIConfiguration = viper.GetString(ReadCNIConfiguration)
 	c.RestoreState = viper.GetBool(Restore)
+	c.RouteMetric = viper.GetInt(RouteMetric)
 	c.RunDir = viper.GetString(StateDir)
 	c.SidecarIstioProxyImage = viper.GetString(SidecarIstioProxyImage)
 	c.UseSingleClusterRoute = viper.GetBool(SingleClusterRouteName)
diff --git a/pkg/policy/selectorcache.go b/pkg/policy/selectorcache.go
index ed8b53aee4..45311ff1f5 100644
--- a/pkg/policy/selectorcache.go
+++ b/pkg/policy/selectorcache.go
@@ -7,6 +7,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"net"
 	"sort"
 	"strings"
 	"sync"
@@ -143,6 +144,11 @@ type identitySelector interface {
 	// Called with NameManager and SelectorCache locks held
 	removeUser(CachedSelectionUser, identityNotifier) (last bool)
 
+	// releaseIdentityMappings must be called exactly once upon release of
+	// this selector to ensure that resources are cleaned up upon deletion
+	// of the selector.
+	releaseIdentityMappings(cache.IdentityAllocator)
+
 	// This may be called while the NameManager lock is held. wg.Wait()
 	// returns after user notifications have been completed, which may require
 	// taking Endpoint and SelectorCache locks, so these locks must not be
@@ -439,6 +445,86 @@ func (f *fqdnSelector) notifyUsers(sc *SelectorCache, added, deleted []identity.
 	}
 }
 
+// allocateIdentityMappings is an initializer for 'fqdnSelector' that takes a
+// slice of IPs that should be associated with the selector and caches those
+// selections. No notifications are propagated to the users.
+//
+// This function may be called at initialization for a new fqdnSelector, or
+// upon new use of an FQDN selector with the same string representation of a
+// previously-cached selector (for example the same toFQDNs match pattern in
+// different rules).
+//
+// Calls to allocateIdentityMappings() will allocate identities and associate
+// them with the fqdnSelector, therefore the corresponding
+// releaseIdentityMappings() function must also be eventually called to clean
+// up this selector.
+func (f *fqdnSelector) allocateIdentityMappings(idAllocator cache.IdentityAllocator, selectorIPMapping map[api.FQDNSelector][]net.IP) {
+	// We don't know whether the IPs are associated with this selector
+	// until we map those IPs to identities, which requires potentially
+	// allocating a CIDR identity for those IPs. Therefore, below we
+	// unconditionally allocate identities for all IPs in
+	// 'selectorIPMapping', then find out if any are duplicated with the
+	// existing selector content, and then release any that are already
+	// referenced by this selector. The new IPs will be then added to the
+	// cached selections and the corresponding identity reference will be
+	// released in releaseIdentityMappings(). This balances the
+	// allocation/release of all identities allocated from this function.
+	var (
+		currentlyAllocatedIdentities []*identity.Identity
+		selectorIPs                  []net.IP
+		ids                          []identity.NumericIdentity
+		err                          error
+	)
+
+	// Allocate identities for each IPNet and then map to selector
+	selectorIPs = selectorIPMapping[f.selector]
+	log.WithFields(logrus.Fields{
+		"fqdnSelector": f.selector,
+		"ips":          selectorIPs,
+	}).Debug("getting identities for IPs associated with FQDNSelector")
+
+	// TODO: Consider if upserts to ipcache should be delayed until endpoint policies have been
+	// updated. This is the path from policy updates rather than for DNS proxy results. Hence
+	// any existing IPs would typically already have been pushed to the ipcache as they would
+	// not be newly allocated. We need the 'allocation' here to get a reference count on the
+	// allocations.
+	if currentlyAllocatedIdentities, err = idAllocator.AllocateCIDRsForIPs(selectorIPs, nil); err != nil {
+		log.WithError(err).WithField("prefixes", selectorIPs).Warn(
+			"failed to allocate identities for IPs")
+		return
+	}
+	ids = make([]identity.NumericIdentity, 0, len(currentlyAllocatedIdentities))
+	for i := range currentlyAllocatedIdentities {
+		ids = append(ids, currentlyAllocatedIdentities[i].ID)
+	}
+
+	identitiesToRelease := make([]identity.NumericIdentity, 0, len(ids))
+	for _, id := range ids {
+		if _, exists := f.cachedSelections[id]; exists {
+			identitiesToRelease = append(identitiesToRelease, id)
+		}
+		f.cachedSelections[id] = struct{}{}
+	}
+
+	ctx, cancel := context.WithTimeout(context.TODO(), option.Config.KVstoreConnectivityTimeout)
+	defer cancel()
+	idAllocator.ReleaseCIDRIdentitiesByID(ctx, identitiesToRelease)
+}
+
+// releaseIdentityMappings must be called exactly once for the received
+// 'fqdnSelector' in order to release CIDR identity references held in this
+// selector's cachedSelections.
+func (f *fqdnSelector) releaseIdentityMappings(idAllocator cache.IdentityAllocator) {
+	ids := make([]identity.NumericIdentity, 0, len(f.cachedSelections))
+	for id := range f.cachedSelections {
+		ids = append(ids, id)
+	}
+
+	ctx, cancel := context.WithTimeout(context.TODO(), option.Config.KVstoreConnectivityTimeout)
+	defer cancel()
+	idAllocator.ReleaseCIDRIdentitiesByID(ctx, ids)
+}
+
 // identityNotifier provides a means for other subsystems to be made aware of a
 // given FQDNSelector (currently pkg/fqdn) so that said subsystems can notify
 // the SelectorCache about new IPs (via CIDR Identities) which correspond to
@@ -447,36 +533,20 @@ func (f *fqdnSelector) notifyUsers(sc *SelectorCache, added, deleted []identity.
 // relationship is contained only via DNS responses, which are handled
 // externally.
 type identityNotifier interface {
-	// Lock must be held during any calls to RegisterForIdentityUpdatesLocked or
-	// UnregisterForIdentityUpdatesLocked.
+	// Lock must be held during any calls to *Locked functions below.
 	Lock()
 
-	// Unlock must be called after calls to RegisterForIdentityUpdatesLocked or
-	// UnregisterForIdentityUpdatesLocked are done.
+	// Unlock must be called after calls to *Locked functions below.
 	Unlock()
 
 	// RegisterForIdentityUpdatesLocked exposes this FQDNSelector so that identities
-	// for IPs contained in a DNS response that matches said selector can be
-	// propagated back to the SelectorCache via `UpdateFQDNSelector`. When called,
-	// implementers (currently `pkg/fqdn/RuleGen`) should iterate over all DNS
-	// names that they are aware of, and see if they match the FQDNSelector.
-	// All IPs which correspond to the DNS names which match this Selector will
-	// be returned as CIDR identities, as other DNS Names which have already
-	// been resolved may match this FQDNSelector.
-	// Once this function is called, the SelectorCache will be updated any time
-	// new IPs are resolved for DNS names which match this FQDNSelector.
-	// This function is only called when the SelectorCache has been made aware
-	// of this FQDNSelector for the first time, since we only need to get the
-	// set of CIDR identities which match this FQDNSelector already from the
-	// identityNotifier on the first pass; any subsequent updates will eventually
-	// call `UpdateFQDNSelector`.
+	// for IPs contained in a DNS response that matches said selector can
+	// be propagated back to the SelectorCache via `UpdateFQDNSelector`.
 	//
-	// The code currently allocates identities inside this function, but
-	// relies on the caller handling the identity release themselves.
-	// In future it likely makes sense to move that code out of the
-	// identityNotifier implementation into the caller to better balance
-	// the allocate/release calls.
-	RegisterForIdentityUpdatesLocked(allocator cache.IdentityAllocator, selector api.FQDNSelector) (identities []identity.NumericIdentity)
+	// This function should only be called when the SelectorCache has been
+	// made aware of the FQDNSelector for the first time; subsequent
+	// updates to the selectors should be made via `UpdateFQDNSelector`.
+	RegisterForIdentityUpdatesLocked(selector api.FQDNSelector)
 
 	// UnregisterForIdentityUpdatesLocked removes this FQDNSelector from the set of
 	// FQDNSelectors which are being tracked by the identityNotifier. The result
@@ -485,6 +555,12 @@ type identityNotifier interface {
 	// This occurs when there are no more users of a given FQDNSelector for the
 	// SelectorCache.
 	UnregisterForIdentityUpdatesLocked(selector api.FQDNSelector)
+
+	// MapSelectorsToIPsLocked returns a slice of IPs that may be
+	// associated with the specified FQDN selector, based on the
+	// currently-known DNS mappings for the IPs held inside the
+	// identityNotifier.
+	MapSelectorsToIPsLocked(map[api.FQDNSelector]struct{}) (selectorsMissingIPs []api.FQDNSelector, selectorIPMapping map[api.FQDNSelector][]net.IP)
 }
 
 type labelIdentitySelector struct {
@@ -530,6 +606,10 @@ func (l *labelIdentitySelector) matches(identity scIdentity) bool {
 	return l.matchesNamespace(identity.namespace) && l.selector.Matches(identity.lbls)
 }
 
+func (l *labelIdentitySelector) releaseIdentityMappings(idAllocator cache.IdentityAllocator) {
+	// labelIdentitySelectors don't retain identity references, so no-op.
+}
+
 //
 // CachedSelector implementation (== Public API)
 //
@@ -710,15 +790,9 @@ func (sc *SelectorCache) AddFQDNSelector(user CachedSelectionUser, fqdnSelec api
 	// If this is called twice, one of the results will arbitrarily contain
 	// a real slice of ids, while the other will receive nil. We must fold
 	// them together below.
-	ids := sc.localIdentityNotifier.RegisterForIdentityUpdatesLocked(sc.idAllocator, newFQDNSel.selector)
-
-	// Do not go through the identity cache to see what identities "match" this
-	// selector. This has to be updated via whatever is getting the CIDR identities
-	// which correspond go this FQDNSelector.
-	// Alternatively , we could go through the CIDR identities in the cache
-	// provided they have some 'field' which shows which FQDNs they correspond
-	// to? This would require we keep some set in the Identity for the CIDR.
-	// Is this feasible?
+	sc.localIdentityNotifier.RegisterForIdentityUpdatesLocked(newFQDNSel.selector)
+	selectors := map[api.FQDNSelector]struct{}{newFQDNSel.selector: {}}
+	_, selectorIPMapping := sc.localIdentityNotifier.MapSelectorsToIPsLocked(selectors)
 
 	// Note: No notifications are sent for the existing
 	// identities. Caller must use GetSelections() to get the
@@ -738,13 +812,12 @@ func (sc *SelectorCache) AddFQDNSelector(user CachedSelectionUser, fqdnSelec api
 		sc.selectors[key] = newFQDNSel
 	}
 
-	// Add the ids from the slice above to the FQDN selector in the cache.
-	// This could plausibly happen twice, once with an empty 'ids' slice
-	// and once with the real 'ids' slice. Either way, they are added to
-	// the selector that is stored in 'sc.selectors[]'
-	for _, id := range ids {
-		newFQDNSel.cachedSelections[id] = struct{}{}
-	}
+	// Allocate identities corresponding to the slice of IPs identified as
+	// being selected by this FQDN selector above. This could plausibly
+	// happen twice, once with an empty 'ids' slice and once with the real
+	// 'ids' slice. Either way, they are added to the selector that is
+	// stored in 'sc.selectors[]'.
+	newFQDNSel.allocateIdentityMappings(sc.idAllocator, selectorIPMapping)
 	newFQDNSel.updateSelections()
 
 	return newFQDNSel, newFQDNSel.addUser(user)
@@ -824,6 +897,7 @@ func (sc *SelectorCache) removeSelectorLocked(selector CachedSelector, user Cach
 		if sel.removeUser(user, sc.localIdentityNotifier) {
 			delete(sc.selectors, key)
 		}
+		sel.releaseIdentityMappings(sc.idAllocator)
 	}
 }
 
diff --git a/vendor/github.com/Azure/azure-sdk-for-go/version/version.go b/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
index a0221ab2b3..fe0b1fcf06 100644
--- a/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
+++ b/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
@@ -4,4 +4,4 @@ package version
 // Licensed under the MIT License. See License.txt in the project root for license information.
 
 // Number contains the semantic version of this SDK.
-const Number = "v59.2.0"
+const Number = "v59.3.0"
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/CHANGELOG.md
index 579640f0a7..b2166e796c 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/CHANGELOG.md
+++ b/vendor/github.com/aws/aws-sdk-go-v2/CHANGELOG.md
@@ -1,3 +1,154 @@
+# Release (2021-11-19)
+
+## General Highlights
+* **Dependency Update**: Updated to the latest SDK module versions
+
+## Module Highlights
+* `github.com/aws/aws-sdk-go-v2`: v1.11.1
+  * **Bug Fix**: Fixed a bug that prevented aws.EndpointResolverWithOptionsFunc from satisfying the aws.EndpointResolverWithOptions interface.
+* `github.com/aws/aws-sdk-go-v2/service/amplifybackend`: [v1.8.0](service/amplifybackend/CHANGELOG.md#v180-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/apigateway`: [v1.10.0](service/apigateway/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/appconfig`: [v1.7.0](service/appconfig/CHANGELOG.md#v170-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/appconfigdata`: [v1.0.0](service/appconfigdata/CHANGELOG.md#v100-2021-11-19)
+  * **Release**: New AWS service client module
+* `github.com/aws/aws-sdk-go-v2/service/applicationinsights`: [v1.8.0](service/applicationinsights/CHANGELOG.md#v180-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/appstream`: [v1.10.0](service/appstream/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/auditmanager`: [v1.12.0](service/auditmanager/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/batch`: [v1.11.0](service/batch/CHANGELOG.md#v1110-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/chime`: [v1.14.0](service/chime/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/chimesdkmeetings`: [v1.1.0](service/chimesdkmeetings/CHANGELOG.md#v110-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/cloudformation`: [v1.14.0](service/cloudformation/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/cloudtrail`: [v1.10.0](service/cloudtrail/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/cloudwatch`: [v1.12.0](service/cloudwatch/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.12.0](service/connect/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/databasemigrationservice`: [v1.12.0](service/databasemigrationservice/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/databrew`: [v1.13.0](service/databrew/CHANGELOG.md#v1130-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/devopsguru`: [v1.10.0](service/devopsguru/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/drs`: [v1.0.0](service/drs/CHANGELOG.md#v100-2021-11-19)
+  * **Release**: New AWS service client module
+* `github.com/aws/aws-sdk-go-v2/service/dynamodbstreams`: [v1.8.0](service/dynamodbstreams/CHANGELOG.md#v180-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.23.0](service/ec2/CHANGELOG.md#v1230-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/eks`: [v1.14.0](service/eks/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/forecast`: [v1.14.0](service/forecast/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/ivs`: [v1.10.0](service/ivs/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/kafka`: [v1.10.0](service/kafka/CHANGELOG.md#v1100-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/kendra`: [v1.16.0](service/kendra/CHANGELOG.md#v1160-2021-11-19)
+  * **Announcement**: Fix API modeling bug incorrectly generating `DocumentAttributeValue` type as a union instead of a structure. This update corrects this bug by correcting the `DocumentAttributeValue` type to be a `struct` instead of an `interface`. This change also removes the `DocumentAttributeValueMember` types. To migrate to this change your application using service/kendra will need to be updated to use struct members in `DocumentAttributeValue` instead of `DocumentAttributeValueMember` types.
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/kms`: [v1.11.0](service/kms/CHANGELOG.md#v1110-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/lambda`: [v1.13.0](service/lambda/CHANGELOG.md#v1130-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/lexmodelsv2`: [v1.13.0](service/lexmodelsv2/CHANGELOG.md#v1130-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/lexruntimev2`: [v1.9.0](service/lexruntimev2/CHANGELOG.md#v190-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/location`: [v1.9.0](service/location/CHANGELOG.md#v190-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/mediaconvert`: [v1.15.0](service/mediaconvert/CHANGELOG.md#v1150-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/medialive`: [v1.14.0](service/medialive/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/mgn`: [v1.7.0](service/mgn/CHANGELOG.md#v170-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/migrationhubstrategy`: [v1.0.0](service/migrationhubstrategy/CHANGELOG.md#v100-2021-11-19)
+  * **Release**: New AWS service client module
+* `github.com/aws/aws-sdk-go-v2/service/qldb`: [v1.9.0](service/qldb/CHANGELOG.md#v190-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/qldbsession`: [v1.9.0](service/qldbsession/CHANGELOG.md#v190-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/redshift`: [v1.15.0](service/redshift/CHANGELOG.md#v1150-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/sns`: [v1.12.0](service/sns/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/ssm`: [v1.16.0](service/ssm/CHANGELOG.md#v1160-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/transfer`: [v1.12.0](service/transfer/CHANGELOG.md#v1120-2021-11-19)
+  * **Feature**: API client updated
+* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.14.0](service/wafv2/CHANGELOG.md#v1140-2021-11-19)
+  * **Feature**: API client updated
+
+# Release (2021-11-12)
+
+## General Highlights
+* **Feature**: Service clients now support custom endpoints that have an initial URI path defined.
+* **Feature**: Waiters now have a `WaitForOutput` method, which can be used to retrieve the output of the successful wait operation. Thank you to [Andrew Haines](https://github.com/haines) for contributing this feature.
+* **Dependency Update**: Updated to the latest SDK module versions
+
+## Module Highlights
+* `github.com/aws/aws-sdk-go-v2/service/backup`: [v1.9.0](service/backup/CHANGELOG.md#v190-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/batch`: [v1.10.0](service/batch/CHANGELOG.md#v1100-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/chimesdkmeetings`: [v1.0.0](service/chimesdkmeetings/CHANGELOG.md#v100-2021-11-12)
+  * **Release**: New AWS service client module
+* `github.com/aws/aws-sdk-go-v2/service/computeoptimizer`: [v1.12.0](service/computeoptimizer/CHANGELOG.md#v1120-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.11.0](service/connect/CHANGELOG.md#v1110-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/docdb`: [v1.12.0](service/docdb/CHANGELOG.md#v1120-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/dynamodb`: [v1.8.0](service/dynamodb/CHANGELOG.md#v180-2021-11-12)
+  * **Documentation**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.22.0](service/ec2/CHANGELOG.md#v1220-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/ecs`: [v1.12.0](service/ecs/CHANGELOG.md#v1120-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/gamelift`: [v1.9.0](service/gamelift/CHANGELOG.md#v190-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/greengrassv2`: [v1.9.0](service/greengrassv2/CHANGELOG.md#v190-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/health`: [v1.10.0](service/health/CHANGELOG.md#v1100-2021-11-12)
+  * **Documentation**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/identitystore`: [v1.9.0](service/identitystore/CHANGELOG.md#v190-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/iotwireless`: [v1.12.0](service/iotwireless/CHANGELOG.md#v1120-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/neptune`: [v1.11.0](service/neptune/CHANGELOG.md#v1110-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/rds`: [v1.12.0](service/rds/CHANGELOG.md#v1120-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/resiliencehub`: [v1.0.0](service/resiliencehub/CHANGELOG.md#v100-2021-11-12)
+  * **Release**: New AWS service client module
+* `github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi`: [v1.8.0](service/resourcegroupstaggingapi/CHANGELOG.md#v180-2021-11-12)
+  * **Documentation**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/s3control`: [v1.14.0](service/s3control/CHANGELOG.md#v1140-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.19.0](service/sagemaker/CHANGELOG.md#v1190-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/sagemakerruntime`: [v1.10.0](service/sagemakerruntime/CHANGELOG.md#v1100-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/ssmincidents`: [v1.7.0](service/ssmincidents/CHANGELOG.md#v170-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/transcribe`: [v1.11.0](service/transcribe/CHANGELOG.md#v1110-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/translate`: [v1.7.0](service/translate/CHANGELOG.md#v170-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.13.0](service/wafv2/CHANGELOG.md#v1130-2021-11-12)
+  * **Feature**: Updated service to latest API model.
+
 # Release (2021-11-06)
 
 ## General Highlights
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/Makefile b/vendor/github.com/aws/aws-sdk-go-v2/Makefile
index 6e2a1117cd..d463fbcc50 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/Makefile
+++ b/vendor/github.com/aws/aws-sdk-go-v2/Makefile
@@ -39,6 +39,7 @@ ENDPOINT_PREFIX_JSON=${CODEGEN_RESOURCES_PATH}/endpoint-prefix.json
 LICENSE_FILE=$(shell pwd)/LICENSE.txt
 
 SMITHY_GO_VERSION ?=
+PRE_RELEASE_VERSION ?=
 RELEASE_MANIFEST_FILE ?=
 RELEASE_CHGLOG_DESC_FILE ?=
 
@@ -57,6 +58,12 @@ REPOTOOLS_CMD_EDIT_MODULE_DEPENDENCY = ${REPOTOOLS_MODULE}/cmd/editmoduledepende
 REPOTOOLS_CALCULATE_RELEASE_VERBOSE ?= false
 REPOTOOLS_CALCULATE_RELEASE_VERBOSE_FLAG=-v=${REPOTOOLS_CALCULATE_RELEASE_VERBOSE}
 
+REPOTOOLS_CALCULATE_RELEASE_ADDITIONAL_ARGS ?=
+
+ifneq ($(PRE_RELEASE_VERSION),)
+	REPOTOOLS_CALCULATE_RELEASE_ADDITIONAL_ARGS += -preview=${PRE_RELEASE_VERSION}
+endif
+
 .PHONY: all
 all: generate unit
 
@@ -398,7 +405,7 @@ ls-changes:
 	go run ${REPOTOOLS_CMD_CHANGELOG} ls
 
 preview-release:
-	go run ${REPOTOOLS_CMD_CALCULATE_RELEASE} ${REPOTOOLS_CALCULATE_RELEASE_VERBOSE_FLAG}
+	go run ${REPOTOOLS_CMD_CALCULATE_RELEASE} ${REPOTOOLS_CALCULATE_RELEASE_VERBOSE_FLAG} ${REPOTOOLS_CALCULATE_RELEASE_ADDITIONAL_ARGS}
 
 pre-release-validation:
 	@if [[ -z "${RELEASE_MANIFEST_FILE}" ]]; then \
@@ -409,7 +416,7 @@ pre-release-validation:
 	fi
 
 release: pre-release-validation
-	go run ${REPOTOOLS_CMD_CALCULATE_RELEASE} -o ${RELEASE_MANIFEST_FILE} ${REPOTOOLS_CALCULATE_RELEASE_VERBOSE_FLAG}
+	go run ${REPOTOOLS_CMD_CALCULATE_RELEASE} -o ${RELEASE_MANIFEST_FILE} ${REPOTOOLS_CALCULATE_RELEASE_VERBOSE_FLAG} ${REPOTOOLS_CALCULATE_RELEASE_ADDITIONAL_ARGS}
 	go run ${REPOTOOLS_CMD_UPDATE_REQUIRES} -release ${RELEASE_MANIFEST_FILE}
 	go run ${REPOTOOLS_CMD_UPDATE_MODULE_METADATA} -release ${RELEASE_MANIFEST_FILE}
 	go run ${REPOTOOLS_CMD_GENERATE_CHANGELOG} -release ${RELEASE_MANIFEST_FILE} -o ${RELEASE_CHGLOG_DESC_FILE}
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.go
index 43e14bca63..706f13d2f1 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.go
+++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.go
@@ -183,11 +183,11 @@ type EndpointResolverWithOptions interface {
 }
 
 // EndpointResolverWithOptionsFunc wraps a function to satisfy the EndpointResolverWithOptions interface.
-type EndpointResolverWithOptionsFunc func(service, region string, options interface{}) (Endpoint, error)
+type EndpointResolverWithOptionsFunc func(service, region string, options ...interface{}) (Endpoint, error)
 
 // ResolveEndpoint calls the wrapped function and returns the results.
-func (e EndpointResolverWithOptionsFunc) ResolveEndpoint(service, region string, options interface{}) (Endpoint, error) {
-	return e(service, region, options)
+func (e EndpointResolverWithOptionsFunc) ResolveEndpoint(service, region string, options ...interface{}) (Endpoint, error) {
+	return e(service, region, options...)
 }
 
 // GetDisableHTTPS takes a service's EndpointResolverOptions and returns the DisableHTTPS value.
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go
index abbaf9b9bc..b162ad3d59 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go
+++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go
@@ -3,4 +3,4 @@
 package aws
 
 // goModuleVersion is the tagged release for this module
-const goModuleVersion = "1.11.0"
+const goModuleVersion = "1.11.1"
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md
index 2a2fe38777..799ac74c8e 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md
+++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md
@@ -1,3 +1,7 @@
+# v1.8.1 (2021-11-19)
+
+* **Dependency Update**: Updated to the latest SDK module versions
+
 # v1.8.0 (2021-11-06)
 
 * **Feature**: Updated `github.com/aws/smithy-go` to latest version
diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go
index 0aebc899c4..536b6fca8c 100644
--- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go
+++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go
@@ -3,4 +3,4 @@
 package imds
 
 // goModuleVersion is the tagged release for this module
-const goModuleVersion = "1.8.0"
+const goModuleVersion = "1.8.1"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index b6c4292fba..1073255155 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,4 +1,4 @@
-# github.com/Azure/azure-sdk-for-go v59.2.0+incompatible
+# github.com/Azure/azure-sdk-for-go v59.3.0+incompatible
 ## explicit
 github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-03-01/compute
 github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-11-01/network
@@ -47,7 +47,7 @@ github.com/PuerkitoBio/urlesc
 # github.com/StackExchange/wmi v1.2.1
 ## explicit; go 1.13
 github.com/StackExchange/wmi
-# github.com/aliyun/alibaba-cloud-sdk-go v1.61.1331
+# github.com/aliyun/alibaba-cloud-sdk-go v1.61.1334
 ## explicit; go 1.13
 github.com/aliyun/alibaba-cloud-sdk-go/sdk
 github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth
@@ -70,7 +70,7 @@ github.com/armon/go-metrics
 # github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
 ## explicit; go 1.13
 github.com/asaskevich/govalidator
-# github.com/aws/aws-sdk-go-v2 v1.11.0
+# github.com/aws/aws-sdk-go-v2 v1.11.1
 ## explicit; go 1.15
 github.com/aws/aws-sdk-go-v2
 github.com/aws/aws-sdk-go-v2/aws
@@ -102,7 +102,7 @@ github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client
 github.com/aws/aws-sdk-go-v2/credentials/processcreds
 github.com/aws/aws-sdk-go-v2/credentials/ssocreds
 github.com/aws/aws-sdk-go-v2/credentials/stscreds
-# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0
+# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1
 ## explicit; go 1.15
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config
@@ -261,7 +261,7 @@ github.com/dimchansky/utfbom
 github.com/docker/distribution/digestset
 github.com/docker/distribution/reference
 github.com/docker/distribution/registry/api/errcode
-# github.com/docker/docker v20.10.10+incompatible
+# github.com/docker/docker v20.10.11+incompatible
 ## explicit
 github.com/docker/docker/api
 github.com/docker/docker/api/types


Test patch:
diff --git a/daemon/cmd/fqdn_test.go b/daemon/cmd/fqdn_test.go
index e053e95494..3d503753ef 100644
--- a/daemon/cmd/fqdn_test.go
+++ b/daemon/cmd/fqdn_test.go
@@ -169,8 +169,7 @@ func (ds *DaemonFQDNSuite) TestFQDNIdentityReferenceCounting(c *C) {
 	selectorsToAdd := api.FQDNSelectorSlice{ciliumIOSel, ciliumIOSelMatchPattern, ebpfIOSel}
 	nameManager.Lock()
 	for _, sel := range selectorsToAdd {
-		ids := nameManager.RegisterForIdentityUpdatesLocked(idAllocator, sel)
-		c.Assert(ids, Not(IsNil))
+		nameManager.RegisterForIdentityUpdatesLocked(sel)
 	}
 	nameManager.Unlock()
 
diff --git a/daemon/cmd/policy_test.go b/daemon/cmd/policy_test.go
index 7edb685512..2151aa6ef6 100644
--- a/daemon/cmd/policy_test.go
+++ b/daemon/cmd/policy_test.go
@@ -278,23 +278,23 @@ func (ds *DaemonSuite) TestUpdateConsumerMap(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 	prodBarLbls := labels.Labels{lblBar.Key: lblBar, lblProd.Key: lblProd}
 	prodBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), prodBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), prodBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), prodBarSecLblsCtx, false)
 	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
 	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx, false)
 	prodFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblProd.Key: lblProd}
 	prodFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), prodFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), prodFooSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), prodFooSecLblsCtx, false)
 	prodFooJoeLbls := labels.Labels{lblFoo.Key: lblFoo, lblProd.Key: lblProd, lblJoe.Key: lblJoe}
 	prodFooJoeSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), prodFooJoeLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), prodFooJoeSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), prodFooJoeSecLblsCtx, false)
 
 	// Prepare endpoints
 	cleanup, err2 := prepareEndpointDirs()
@@ -411,11 +411,11 @@ func (ds *DaemonSuite) TestL4_L7_Shadowing(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
 	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx, false)
 
 	rules := api.Rules{
 		{
@@ -495,11 +495,11 @@ func (ds *DaemonSuite) TestL4_L7_ShadowingShortCircuit(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
 	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx, false)
 
 	rules := api.Rules{
 		{
@@ -573,15 +573,15 @@ func (ds *DaemonSuite) TestL3_dependent_L7(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
 	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx, false)
 	qaJoeLbls := labels.Labels{lblJoe.Key: lblJoe, lblQA.Key: lblQA}
 	qaJoeSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaJoeLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaJoeSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaJoeSecLblsCtx, false)
 
 	rules := api.Rules{
 		{
@@ -735,7 +735,7 @@ func (ds *DaemonSuite) TestRemovePolicy(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 
 	rules := api.Rules{
 		{
@@ -820,7 +820,7 @@ func (ds *DaemonSuite) TestIncrementalPolicy(c *C) {
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx, false)
 
 	rules := api.Rules{
 		{
@@ -909,7 +909,7 @@ func (ds *DaemonSuite) TestIncrementalPolicy(c *C) {
 	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
 	qaFooID, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
 	c.Assert(err, Equals, nil)
-	defer ds.d.identityAllocator.Release(context.Background(), qaFooID)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooID, false)
 
 	// Regenerate endpoint
 	ds.regenerateEndpoint(c, e)
diff --git a/pkg/endpoint/endpoint_status_test.go b/pkg/endpoint/endpoint_status_test.go
index a622dbc189..deb9c88159 100644
--- a/pkg/endpoint/endpoint_status_test.go
+++ b/pkg/endpoint/endpoint_status_test.go
@@ -263,7 +263,7 @@ func (s *EndpointSuite) TestgetEndpointPolicyMapState(c *check.C) {
 	fooLbls := labels.Labels{"": labels.ParseLabel("foo")}
 	fooIdentity, _, err := e.allocator.AllocateIdentity(context.Background(), fooLbls, false)
 	c.Assert(err, check.Equals, nil)
-	defer s.mgr.Release(context.Background(), fooIdentity)
+	defer s.mgr.Release(context.Background(), fooIdentity, false)
 
 	e.desiredPolicy = policy.NewEndpointPolicy(s.repo)
 	e.desiredPolicy.IngressPolicyEnabled = true
diff --git a/pkg/fqdn/helpers_test.go b/pkg/fqdn/helpers_test.go
index 0503dd9356..a6ed0c02e2 100644
--- a/pkg/fqdn/helpers_test.go
+++ b/pkg/fqdn/helpers_test.go
@@ -86,22 +86,26 @@ func (ds *DNSCacheTestSuite) BenchmarkKeepUniqueNames(c *C) {
 func (ds *DNSCacheTestSuite) TestMapIPsToSelectors(c *C) {
 
 	var (
-		ciliumIP1 = net.ParseIP("1.2.3.4")
-		ciliumIP2 = net.ParseIP("1.2.3.5")
+		ciliumIP1   = net.ParseIP("1.2.3.4")
+		ciliumIP2   = net.ParseIP("1.2.3.5")
+		nameManager = NewNameManager(Config{
+			MinTTL: 1,
+			Cache:  NewDNSCache(0),
+		})
 	)
 
 	log.Level = logrus.DebugLevel
 
 	// Create DNS cache
 	now := time.Now()
-	cache := NewDNSCache(60)
+	cache := nameManager.cache
 
 	selectors := map[api.FQDNSelector]struct{}{
 		ciliumIOSel: {},
 	}
 
 	// Empty cache.
-	selsMissingIPs, selIPMapping := mapSelectorsToIPs(selectors, cache)
+	selsMissingIPs, selIPMapping := nameManager.MapSelectorsToIPsLocked(selectors)
 	c.Assert(len(selsMissingIPs), Equals, 1)
 	c.Assert(selsMissingIPs[0], Equals, ciliumIOSel)
 	c.Assert(len(selIPMapping), Equals, 0)
@@ -109,7 +113,7 @@ func (ds *DNSCacheTestSuite) TestMapIPsToSelectors(c *C) {
 	// Just one IP.
 	changed := cache.Update(now, prepareMatchName(ciliumIOSel.MatchName), []net.IP{ciliumIP1}, 100)
 	c.Assert(changed, Equals, true)
-	selsMissingIPs, selIPMapping = mapSelectorsToIPs(selectors, cache)
+	selsMissingIPs, selIPMapping = nameManager.MapSelectorsToIPsLocked(selectors)
 	c.Assert(len(selsMissingIPs), Equals, 0)
 	c.Assert(len(selIPMapping), Equals, 1)
 	ciliumIPs, ok := selIPMapping[ciliumIOSel]
@@ -120,7 +124,7 @@ func (ds *DNSCacheTestSuite) TestMapIPsToSelectors(c *C) {
 	// Two IPs now.
 	changed = cache.Update(now, prepareMatchName(ciliumIOSel.MatchName), []net.IP{ciliumIP1, ciliumIP2}, 100)
 	c.Assert(changed, Equals, true)
-	selsMissingIPs, selIPMapping = mapSelectorsToIPs(selectors, cache)
+	selsMissingIPs, selIPMapping = nameManager.MapSelectorsToIPsLocked(selectors)
 	c.Assert(len(selsMissingIPs), Equals, 0)
 	c.Assert(len(selIPMapping), Equals, 1)
 	ciliumIPs, ok = selIPMapping[ciliumIOSel]
@@ -133,7 +137,7 @@ func (ds *DNSCacheTestSuite) TestMapIPsToSelectors(c *C) {
 	selectors = map[api.FQDNSelector]struct{}{
 		ciliumIOSelMatchPattern: {},
 	}
-	selsMissingIPs, selIPMapping = mapSelectorsToIPs(selectors, cache)
+	selsMissingIPs, selIPMapping = nameManager.MapSelectorsToIPsLocked(selectors)
 	c.Assert(len(selsMissingIPs), Equals, 0)
 	c.Assert(len(selIPMapping), Equals, 1)
 	ciliumIPs, ok = selIPMapping[ciliumIOSelMatchPattern]
@@ -146,7 +150,7 @@ func (ds *DNSCacheTestSuite) TestMapIPsToSelectors(c *C) {
 		ciliumIOSelMatchPattern: {},
 		ciliumIOSel:             {},
 	}
-	selsMissingIPs, selIPMapping = mapSelectorsToIPs(selectors, cache)
+	selsMissingIPs, selIPMapping = nameManager.MapSelectorsToIPsLocked(selectors)
 	c.Assert(len(selsMissingIPs), Equals, 0)
 	c.Assert(len(selIPMapping), Equals, 2)
 	ciliumIPs, ok = selIPMapping[ciliumIOSelMatchPattern]
diff --git a/pkg/fqdn/name_manager_test.go b/pkg/fqdn/name_manager_test.go
index 7875e99d55..9600596d03 100644
--- a/pkg/fqdn/name_manager_test.go
+++ b/pkg/fqdn/name_manager_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/cilium/cilium/pkg/fqdn/dns"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/policy/api"
-	testidentity "github.com/cilium/cilium/pkg/testutils/identity"
 
 	. "gopkg.in/check.v1"
 )
@@ -29,8 +28,7 @@ func (ds *FQDNTestSuite) TestNameManagerCIDRGeneration(c *C) {
 	var (
 		selIPMap map[api.FQDNSelector][]net.IP
 
-		identityAllocator = testidentity.NewMockIdentityAllocator(nil)
-		nameManager       = NewNameManager(Config{
+		nameManager = NewNameManager(Config{
 			MinTTL: 1,
 			Cache:  NewDNSCache(0),
 
@@ -45,10 +43,8 @@ func (ds *FQDNTestSuite) TestNameManagerCIDRGeneration(c *C) {
 
 	// add rules
 	nameManager.Lock()
-	ids := nameManager.RegisterForIdentityUpdatesLocked(identityAllocator, ciliumIOSel)
+	nameManager.RegisterForIdentityUpdatesLocked(ciliumIOSel)
 	nameManager.Unlock()
-	c.Assert(len(ids), Equals, 0)
-	c.Assert(ids, Not(IsNil))
 
 	// poll DNS once, check that we only generate 1 rule (for 1 IP) and that we
 	// still have 1 ToFQDN rule, and that the IP is correct
@@ -78,8 +74,7 @@ func (ds *FQDNTestSuite) TestNameManagerMultiIPUpdate(c *C) {
 	var (
 		selIPMap map[api.FQDNSelector][]net.IP
 
-		identityAllocator = testidentity.NewMockIdentityAllocator(nil)
-		nameManager       = NewNameManager(Config{
+		nameManager = NewNameManager(Config{
 			MinTTL: 1,
 			Cache:  NewDNSCache(0),
 
@@ -96,8 +91,7 @@ func (ds *FQDNTestSuite) TestNameManagerMultiIPUpdate(c *C) {
 	selectorsToAdd := api.FQDNSelectorSlice{ciliumIOSel, githubSel}
 	nameManager.Lock()
 	for _, sel := range selectorsToAdd {
-		ids := nameManager.RegisterForIdentityUpdatesLocked(identityAllocator, sel)
-		c.Assert(ids, Not(IsNil))
+		nameManager.RegisterForIdentityUpdatesLocked(sel)
 	}
 	nameManager.Unlock()
 
@@ -136,8 +130,7 @@ func (ds *FQDNTestSuite) TestNameManagerMultiIPUpdate(c *C) {
 
 	// Second registration fails because IdenitityAllocator is not initialized
 	nameManager.Lock()
-	ids := nameManager.RegisterForIdentityUpdatesLocked(identityAllocator, githubSel)
-	c.Assert(ids, IsNil)
+	nameManager.RegisterForIdentityUpdatesLocked(githubSel)
 
 	nameManager.UnregisterForIdentityUpdatesLocked(githubSel)
 	_, exists := nameManager.allSelectors[githubSel]
diff --git a/pkg/identity/cache/allocation_test.go b/pkg/identity/cache/allocation_test.go
index 80ccc3ea08..103eada056 100644
--- a/pkg/identity/cache/allocation_test.go
+++ b/pkg/identity/cache/allocation_test.go
@@ -267,10 +267,10 @@ func (ias *IdentityAllocatorSuite) TestAllocator(c *C) {
 	c.Assert(err, IsNil)
 	c.Assert(id1a.ID, Equals, id1b.ID)
 
-	released, err := mgr.Release(context.Background(), id1a)
+	released, err := mgr.Release(context.Background(), id1a, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, false)
-	released, err = mgr.Release(context.Background(), id1b)
+	released, err = mgr.Release(context.Background(), id1b, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
 	// KV-store still keeps the ID even when a single node has released it.
@@ -312,13 +312,13 @@ func (ias *IdentityAllocatorSuite) TestAllocator(c *C) {
 	c.Assert(owner.WaitUntilID(id3.ID), Not(Equals), 0)
 	c.Assert(owner.GetIdentity(id3.ID), checker.DeepEquals, lbls3.LabelArray())
 
-	released, err = mgr.Release(context.Background(), id1b)
+	released, err = mgr.Release(context.Background(), id1b, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
-	released, err = mgr.Release(context.Background(), id2)
+	released, err = mgr.Release(context.Background(), id2, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
-	released, err = mgr.Release(context.Background(), id3)
+	released, err = mgr.Release(context.Background(), id3, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
 
@@ -356,7 +356,7 @@ func (ias *IdentityAllocatorSuite) TestLocalAllocation(c *C) {
 	c.Assert(cache[id.ID], Not(IsNil))
 
 	// 1st Release, not released
-	released, err := mgr.Release(context.Background(), id)
+	released, err := mgr.Release(context.Background(), id, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, false)
 
@@ -364,7 +364,7 @@ func (ias *IdentityAllocatorSuite) TestLocalAllocation(c *C) {
 	c.Assert(owner.GetIdentity(id.ID), checker.DeepEquals, lbls1.LabelArray())
 
 	// 2nd Release, released
-	released, err = mgr.Release(context.Background(), id)
+	released, err = mgr.Release(context.Background(), id, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
 
@@ -382,7 +382,7 @@ func (ias *IdentityAllocatorSuite) TestLocalAllocation(c *C) {
 	c.Assert(isNew, Equals, true)
 	c.Assert(id.ID.HasLocalScope(), Equals, true)
 
-	released, err = mgr.Release(context.Background(), id)
+	released, err = mgr.Release(context.Background(), id, false)
 	c.Assert(err, IsNil)
 	c.Assert(released, Equals, true)
 
diff --git a/pkg/policy/selectorcache_test.go b/pkg/policy/selectorcache_test.go
index 867f2b0532..27bf28c970 100644
--- a/pkg/policy/selectorcache_test.go
+++ b/pkg/policy/selectorcache_test.go
@@ -613,65 +613,6 @@ func (ds *SelectorCacheTestSuite) TestIdentityUpdatesMultipleUsers(c *C) {
 	c.Assert(len(sc.selectors), Equals, 0)
 }
 
-func (ds *SelectorCacheTestSuite) TestIdentityNotifier(c *C) {
-	sc := testNewSelectorCache(cache.IdentityCache{})
-	idNotifier, ok := sc.localIdentityNotifier.(*testidentity.DummyIdentityNotifier)
-	c.Assert(ok, Equals, true)
-	c.Assert(idNotifier, Not(IsNil))
-
-	// Add some identities to the identity cache
-	googleSel := api.FQDNSelector{MatchName: "google.com"}
-	ciliumSel := api.FQDNSelector{MatchName: "cilium.io"}
-
-	// Nothing should be registered yet.
-	c.Assert(idNotifier.IsRegistered(ciliumSel), Equals, false)
-	c.Assert(idNotifier.IsRegistered(googleSel), Equals, false)
-
-	injectedIDs := []identity.NumericIdentity{1000, 1001, 1002}
-	idNotifier.InjectIdentitiesForSelector(ciliumSel, injectedIDs)
-
-	// Add a user without adding identities explicitly. The identityNotifier
-	// should have populated them for us.
-	user1 := newUser(c, "user1", sc)
-	cached := user1.AddFQDNSelector(ciliumSel)
-	_, exists := sc.selectors[ciliumSel.String()]
-	c.Assert(exists, Equals, true)
-
-	selections := cached.GetSelections()
-	c.Assert(len(selections), Equals, 3)
-	for i, selection := range selections {
-		c.Assert(selection, Equals, injectedIDs[i])
-	}
-
-	// Add another selector from the same user
-	cached2 := user1.AddFQDNSelector(googleSel)
-	c.Assert(cached2, Not(Equals), cached)
-
-	selections2 := cached2.GetSelections()
-	c.Assert(len(selections2), Equals, 0)
-
-	wg := &sync.WaitGroup{}
-	sc.RemoveIdentitiesFQDNSelectors([]api.FQDNSelector{
-		googleSel,
-		ciliumSel,
-	}, wg)
-	wg.Wait()
-
-	selections = cached.GetSelections()
-	c.Assert(len(selections), Equals, 0)
-
-	selections2 = cached2.GetSelections()
-	c.Assert(len(selections2), Equals, 0)
-
-	sc.RemoveSelector(cached, user1)
-	c.Assert(idNotifier.IsRegistered(ciliumSel), Equals, false)
-	c.Assert(idNotifier.IsRegistered(googleSel), Equals, true)
-
-	sc.RemoveSelector(cached2, user1)
-	c.Assert(idNotifier.IsRegistered(googleSel), Equals, false)
-
-}
-
 func testNewSelectorCache(ids cache.IdentityCache) *SelectorCache {
 	sc := NewSelectorCache(testidentity.NewMockIdentityAllocator(ids), ids)
 	sc.SetLocalIdentityNotifier(testidentity.NewDummyIdentityNotifier())
diff --git a/pkg/testutils/identity/allocator.go b/pkg/testutils/identity/allocator.go
index 79632163a9..31939e433e 100644
--- a/pkg/testutils/identity/allocator.go
+++ b/pkg/testutils/identity/allocator.go
@@ -98,7 +98,7 @@ func (f *MockIdentityAllocator) AllocateIdentity(_ context.Context, lbls labels.
 
 // Release releases a fake identity. It is meant to generally mock the
 // canonical identity release logic.
-func (f *MockIdentityAllocator) Release(_ context.Context, id *identity.Identity) (released bool, err error) {
+func (f *MockIdentityAllocator) Release(_ context.Context, id *identity.Identity, _ bool) (released bool, err error) {
 	count, ok := f.idRefCount[int(id.ID)]
 	if !ok {
 		return false, nil
@@ -118,7 +118,7 @@ func (f *MockIdentityAllocator) Release(_ context.Context, id *identity.Identity
 // ReleaseSlice wraps Release for slices.
 func (f *MockIdentityAllocator) ReleaseSlice(ctx context.Context, _ cache.IdentityAllocatorOwner, identities []*identity.Identity) error {
 	for _, id := range identities {
-		if _, err := f.Release(ctx, id); err != nil {
+		if _, err := f.Release(ctx, id, false); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/testutils/identity/notifier.go b/pkg/testutils/identity/notifier.go
index 4cd0e2b215..c4fd8758d0 100644
--- a/pkg/testutils/identity/notifier.go
+++ b/pkg/testutils/identity/notifier.go
@@ -4,8 +4,9 @@
 package testidentity
 
 import (
+	"net"
+
 	"github.com/cilium/cilium/pkg/identity"
-	"github.com/cilium/cilium/pkg/identity/cache"
 	"github.com/cilium/cilium/pkg/lock"
 	"github.com/cilium/cilium/pkg/policy/api"
 )
@@ -36,12 +37,10 @@ func (d *DummyIdentityNotifier) Unlock() {
 // RegisterForIdentityUpdatesLocked starts managing this selector.
 //
 // It doesn't implement the identity allocation semantics of the interface.
-func (d *DummyIdentityNotifier) RegisterForIdentityUpdatesLocked(allocator cache.IdentityAllocator, selector api.FQDNSelector) (identities []identity.NumericIdentity) {
-	ids, ok := d.selectors[selector]
-	if !ok {
+func (d *DummyIdentityNotifier) RegisterForIdentityUpdatesLocked(selector api.FQDNSelector) {
+	if _, ok := d.selectors[selector]; !ok {
 		d.selectors[selector] = []identity.NumericIdentity{}
 	}
-	return ids
 }
 
 // UnregisterForIdentityUpdatesLocked stops managing this selector.
@@ -49,12 +48,8 @@ func (d *DummyIdentityNotifier) UnregisterForIdentityUpdatesLocked(selector api.
 	delete(d.selectors, selector)
 }
 
-func (d *DummyIdentityNotifier) InjectIdentitiesForSelector(fqdnSel api.FQDNSelector, ids []identity.NumericIdentity) {
-	d.selectors[fqdnSel] = ids
-}
-
-// IsRegistered returns whether this selector is being managed.
-func (d *DummyIdentityNotifier) IsRegistered(selector api.FQDNSelector) bool {
-	_, ok := d.selectors[selector]
-	return ok
+// MapSelectorsToIPsLocked is a dummy implementation that does not implement
+// the selectors of the real implementation.
+func (d *DummyIdentityNotifier) MapSelectorsToIPsLocked(fqdnSelectors map[api.FQDNSelector]struct{}) (selectorsMissingIPs []api.FQDNSelector, selectorIPMapping map[api.FQDNSelector][]net.IP) {
+	return nil, nil
 }
diff --git a/test/helpers/kubectl.go b/test/helpers/kubectl.go
index be2b7e5ac6..0d4323c1ee 100644
--- a/test/helpers/kubectl.go
+++ b/test/helpers/kubectl.go
@@ -2370,6 +2370,7 @@ func (kub *Kubectl) overwriteHelmOptions(options map[string]string) error {
 
 		if RunsOn419OrLaterKernel() {
 			opts["bpf.masquerade"] = "true"
+			opts["enableIPv6Masquerade"] = "false"
 		}
 
 		for key, value := range opts {
diff --git a/test/k8sT/Egress.go b/test/k8sT/Egress.go
index 9496f0fccd..96ba046bca 100644
--- a/test/k8sT/Egress.go
+++ b/test/k8sT/Egress.go
@@ -15,6 +15,7 @@
 package k8sTest
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"strings"
@@ -22,11 +23,18 @@ import (
 	. "github.com/cilium/cilium/test/ginkgo-ext"
 	"github.com/cilium/cilium/test/helpers"
 	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
 )
 
 var _ = SkipDescribeIf(func() bool {
 	return helpers.RunsOnEKS() || helpers.RunsOnGKE() || helpers.DoesNotRunWithKubeProxyReplacement() || helpers.DoesNotExistNodeWithoutCilium() || helpers.DoesNotRunOn54OrLaterKernel()
 }, "K8sEgressGatewayTest", func() {
+	const (
+		namespaceSelector = "ns=cilium-test"
+		testDS            = "zgroup=testDS"
+		testDSClient      = "zgroup=testDSClient"
+	)
+
 	var (
 		kubectl         *helpers.Kubectl
 		ciliumFilename  string
@@ -40,8 +48,6 @@ var _ = SkipDescribeIf(func() bool {
 		assignIPYAML string
 		echoPodYAML  string
 		policyYAML   string
-
-		namespaceSelector string = "ns=cilium-test"
 	)
 
 	runEchoServer := func() {
@@ -120,14 +126,14 @@ var _ = SkipDescribeIf(func() bool {
 		if fromGateway {
 			hostIP = k8s2IP
 		}
-		srcPod, _ := fetchPodsWithOffset(kubectl, randomNamespace, "client", "zgroup=testDSClient", hostIP, false, 1)
+		srcPod, _ := fetchPodsWithOffset(kubectl, randomNamespace, "client", testDSClient, hostIP, false, 1)
 
 		res := kubectl.ExecPodCmd(randomNamespace, srcPod, helpers.CurlFail("http://%s:80", outsideIP))
 		res.ExpectSuccess()
 		res.ExpectMatchesRegexp(fmt.Sprintf("client_address=::ffff:%s\n", egressIP))
 	}
 
-	testConnectivity := func(fromGateway bool) {
+	testConnectivity := func(fromGateway bool, ciliumOpts map[string]string) {
 		if fromGateway {
 			By("Check connectivity from gateway node")
 		} else {
@@ -149,6 +155,59 @@ var _ = SkipDescribeIf(func() bool {
 		// DNS query should work (pod-to-pod connectivity)
 		res = kubectl.ExecPodCmd(randomNamespace, srcPod, "dig kubernetes +time=2")
 		res.ExpectSuccess()
+
+		// When connecting from outside the cluster to a nodeport service whose pods are
+		// selected by an egress policy, the reply traffic should not be SNATed with the
+		// egress IP
+		var extIPsService v1.Service
+		err := kubectl.Get(randomNamespace, fmt.Sprintf("service %s", "test-external-ips")).Unmarshal(&extIPsService)
+		ExpectWithOffset(1, err).Should(BeNil(), "Can not retrieve service %s", "test-external-ips")
+
+		res = kubectl.Patch(randomNamespace, "service", "test-external-ips",
+			fmt.Sprintf(`{"spec":{"externalIPs":["%s"],  "externalTrafficPolicy": "Local"}}`, hostIP))
+		ExpectWithOffset(1, res).Should(helpers.CMDSuccess(), "Error patching external IP service with node IP")
+
+		outsideNodeName, outsideNodeIP := kubectl.GetNodeInfo(helpers.GetNodeWithoutCilium())
+
+		res = kubectl.ExecInHostNetNS(context.TODO(), outsideNodeName,
+			helpers.CurlFail("http://%s:%d", hostIP, extIPsService.Spec.Ports[0].Port))
+		res.ExpectSuccess()
+		res.ExpectMatchesRegexp(fmt.Sprintf("client_address=::ffff:%s\n", outsideNodeIP))
+
+		if ciliumOpts["tunnel"] == "disabled" {
+			// When connecting from outside the cluster directly to a pod which is
+			// selected by an egress policy, the reply traffic should not be SNATed with
+			// the egress IP (only connections originating from these pods should go
+			// through egress gateway).
+			//
+			// This test is executed only when Cilium is running in direct routing mode,
+			// since we can simply add a route on the node outside the cluster to direct
+			// pod's traffic to the node where the pod is running (while in tunneling
+			// mode we would need the external node to send the traffic over the tunnel)
+			_, targetPodJSON := fetchPodsWithOffset(kubectl, randomNamespace, "server", testDS, hostIP, false, 1)
+
+			targetPodHostIP, err := targetPodJSON.Filter("{.status.hostIP}")
+			Expect(err).Should(BeNil(), "Cannot get target pod host IP")
+
+			targetPodIP, err := targetPodJSON.Filter("{.status.podIP}")
+			Expect(err).Should(BeNil(), "Cannot get target pod IP")
+
+			// Add a route for the target pod's IP on the node running without Cilium to
+			// allow reaching it from outside the cluster
+			res = kubectl.AddIPRoute(outsideNodeName, targetPodIP.String(), targetPodHostIP.String(), false)
+			Expect(res).Should(helpers.CMDSuccess(),
+				"Error adding IP route for %s via %s", targetPodIP.String(), targetPodHostIP.String())
+			defer func() {
+				res := kubectl.DelIPRoute(outsideNodeName, targetPodIP.String(), targetPodHostIP.String())
+				Expect(res).Should(helpers.CMDSuccess(),
+					"Error removing IP route for %s via %s", targetPodIP.String(), targetPodHostIP.String())
+			}()
+
+			res = kubectl.ExecInHostNetNS(context.TODO(), outsideNodeName,
+				helpers.CurlFail("http://%s:%d", targetPodIP.String(), 80))
+			res.ExpectSuccess()
+			res.ExpectMatchesRegexp(fmt.Sprintf("client_address=::ffff:%s\n", outsideNodeIP))
+		}
 	}
 
 	applyEgressPolicy := func() {
@@ -181,8 +240,8 @@ var _ = SkipDescribeIf(func() bool {
 
 			Context("no egress gw policy", func() {
 				It("connectivity works", func() {
-					testConnectivity(false)
-					testConnectivity(true)
+					testConnectivity(false, ciliumOpts)
+					testConnectivity(true, ciliumOpts)
 				})
 			})
 
@@ -203,8 +262,8 @@ var _ = SkipDescribeIf(func() bool {
 				It("both egress gw and basic connectivity work", func() {
 					testEgressGateway(false)
 					testEgressGateway(true)
-					testConnectivity(false)
-					testConnectivity(true)
+					testConnectivity(false, ciliumOpts)
+					testConnectivity(true, ciliumOpts)
 				})
 			})
 		})
diff --git a/test/k8sT/manifests/egress-nat-policy.yaml b/test/k8sT/manifests/egress-nat-policy.yaml
index 2e38d4b7b5..02635081c0 100755
--- a/test/k8sT/manifests/egress-nat-policy.yaml
+++ b/test/k8sT/manifests/egress-nat-policy.yaml
@@ -31,3 +31,22 @@ spec:
   destinationCIDRs:
   - 0.0.0.0/0
   egressSourceIP: 192.0.2.13 # It's a black hole, https://datatracker.ietf.org/doc/html/rfc5737#section-3
+---
+# egress-testds is a policy matching all traffic (i.e. destination CIDR
+# 0.0.0.0/0) from the zgroup=testDS echo server pods.
+#
+# This policy is used to test that reply traffic from pods that are selected by
+# an egress policy is not SNATed with the egress IP (only connections
+# originating from these pods should go through egress gateway).
+apiVersion: cilium.io/v2alpha1
+kind: CiliumEgressNATPolicy
+metadata:
+  name: egress-testds
+spec:
+  egress:
+  - podSelector:
+      matchLabels:
+        zgroup: testDS
+  destinationCIDRs:
+  - 0.0.0.0/0
+  egressSourceIP: INPUT_EGRESS_IP