Repository: cilium/cilium
Pull request #12927 (2020-08-19 11:46:16)

policy: Do not dump selections on logs
Dumping all security identities selected by a cached selectors can
make for huge logs if there are a thousands of PODs in the cluster,
so don't do it.

Signed-off-by: Jarno Rajahalme <jarno@covalent.io>


jrajahalme: test-me-please

Patch:
diff --git a/.gitattributes b/.gitattributes
index 3050924faf..da25b05e45 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
 go.sum linguist-generated
 examples/kubernetes/connectivity-check/connectivity-*.yaml linguist-generated
 examples/crds/*.yaml linguist-generated
+Documentation/cmdref/** linguist-generated
diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md
deleted file mode 100644
index 5394f7ed9a..0000000000
--- a/.github/ISSUE_TEMPLATE/issue_template.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: Support Request
-about: Support request or question related to Cilium
-title: ''
-labels: ''
-assignees: ''
-
----
-
-## Support
-
-<!--
-
-If you have usage questions, please try the [slack
-channel](http://cilium.io/slack) and see the [FAQ](https://goo.gl/qG2YmU)
-first.
-
--->
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index ed364a4d03..f21e8032df 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -59,6 +59,7 @@ cilium-agent [flags]
       --disable-iptables-feeder-rules strings         Chains to ignore when installing feeder rules.
       --egress-masquerade-interfaces string           Limit egress masquerading to interface selector
       --enable-auto-protect-node-port-range           Append NodePort range to net.ipv4.ip_local_reserved_ports if it overlaps with ephemeral port range (net.ipv4.ip_local_port_range) (default true)
+      --enable-bandwidth-manager                      Enable BPF bandwidth manager
       --enable-bpf-clock-probe                        Enable BPF clock source probing for more efficient tick retrieval
       --enable-bpf-masquerade                         Masquerade packets from endpoints leaving the host with BPF instead of iptables
       --enable-endpoint-health-checking               Enable connectivity health checking between virtual endpoints (default true)
diff --git a/Documentation/cmdref/cilium_bpf.md b/Documentation/cmdref/cilium_bpf.md
index 38e4c247e1..94e8d45b2d 100644
--- a/Documentation/cmdref/cilium_bpf.md
+++ b/Documentation/cmdref/cilium_bpf.md
@@ -25,6 +25,7 @@ Direct access to local BPF maps
 ### SEE ALSO
 
 * [cilium](../cilium)	 - CLI
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
 * [cilium bpf ct](../cilium_bpf_ct)	 - Connection tracking tables
 * [cilium bpf endpoint](../cilium_bpf_endpoint)	 - Local endpoint map
 * [cilium bpf fs](../cilium_bpf_fs)	 - BPF filesystem mount
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth.md b/Documentation/cmdref/cilium_bpf_bandwidth.md
new file mode 100644
index 0000000000..ad1780d98f
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth.md
@@ -0,0 +1,29 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth
+
+BPF datapath bandwidth settings
+
+### Synopsis
+
+BPF datapath bandwidth settings
+
+### Options
+
+```
+  -h, --help   help for bandwidth
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf](../cilium_bpf)	 - Direct access to local BPF maps
+* [cilium bpf bandwidth list](../cilium_bpf_bandwidth_list)	 - List BPF datapath bandwidth settings
+
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth_list.md b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
new file mode 100644
index 0000000000..4c91646689
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
@@ -0,0 +1,33 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth list
+
+List BPF datapath bandwidth settings
+
+### Synopsis
+
+List BPF datapath bandwidth settings
+
+```
+cilium bpf bandwidth list [flags]
+```
+
+### Options
+
+```
+  -h, --help            help for list
+  -o, --output string   json| jsonpath='{}'
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
+
diff --git a/Documentation/gettingstarted/bandwidth-manager.rst b/Documentation/gettingstarted/bandwidth-manager.rst
new file mode 100644
index 0000000000..dcd7e5bf13
--- /dev/null
+++ b/Documentation/gettingstarted/bandwidth-manager.rst
@@ -0,0 +1,144 @@
+.. only:: not (epub or latex or html)
+
+    WARNING: You are looking at unreleased Cilium documentation.
+    Please use the official rendered version released here:
+    https://docs.cilium.io
+
+.. _bandwidth-manager:
+
+************************
+Bandwidth Manager (beta)
+************************
+
+This guide explains how to configure Cilium's bandwidth manager to
+optimize TCP and UDP workloads and efficiently rate limit individual Pods
+if needed through the help of EDT (Earliest Departure Time) and eBPF.
+
+The bandwidth manager does not rely on CNI chaining and is natively integrated
+into Cilium instead. Hence, it does not make use of the `bandwidth CNI
+<https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping>`_
+plugin. Due to scalability concerns in particular for multi-queue network
+interfaces, it is not recommended to use the bandwidth CNI plugin which is
+based on TBF (Token Bucket Filter) instead of EDT.
+
+Cilium's bandwidth manager supports the ``kubernetes.io/egress-bandwidth`` Pod
+annotation which is enforced on egress at the native host networking devices.
+The bandwidth enforcement is supported for direct routing as well as tunneling
+mode in Cilium.
+
+The ``kubernetes.io/ingress-bandwidth`` annotation is not supported and also not
+recommended to use. Limiting bandwidth happens natively at the egress point of
+networking devices in order to reduce or pace bandwidth usage on the wire.
+Enforcing at ingress would add yet another layer of buffer queueing right in the
+critical fast-path of a node via ``ifb`` device where ingress traffic first needs
+to be redirected to the ``ifb``'s egress point in order to perform shaping before
+traffic can go up the stack. At this point traffic has already occupied the
+bandwidth usage on the wire, and the node has already spent resources on
+processing the packet. ``kubernetes.io/ingress-bandwidth`` annotation is ignored
+by Cilium's bandwidth manager.
+
+.. note::
+
+   Bandwidth Manager requires a v5.0.x or more recent Linux kernel.
+
+.. include:: k8s-install-download-release.rst
+
+The Cilium bandwidth manager is enabled by default for new deployments via Helm:
+
+.. parsed-literal::
+
+   helm install cilium |CHART_RELEASE| \\
+     --namespace kube-system
+
+The option for Helm is controllable through ``global.bandwidthManager`` with a
+possible setting of ``true`` (default) and ``false``.
+
+The native host networking devices are auto detected as native devices which have
+the default route on the host or have Kubernetes InternalIP or ExternalIP assigned.
+InternalIP is preferred over ExternalIP if both exist. To change and manually specify
+the devices, set their names in the ``global.devices`` helm option, e.g.
+``global.devices='{eth0,eth1,eth2}'``. Each listed device has to be named the same
+on all Cilium managed nodes.
+
+Verify that the Cilium Pod has come up correctly:
+
+.. code:: bash
+
+    kubectl -n kube-system get pods -l k8s-app=cilium
+    NAME                READY     STATUS    RESTARTS   AGE
+    cilium-crf7f        1/1       Running   0          10m
+
+In order to verify whether the bandwidth manager feature has been enabled in Cilium,
+the ``cilium status`` CLI command provides visibility through the ``BandwidthManager``
+info line. It also dumps a list of devices on which the egress bandwidth limitation
+is enforced:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep BandwidthManager
+    BandwidthManager:       EDT with BPF   [eth0]
+
+Assuming we have a multi-node cluster, in the next step, we deploy a netperf Pod on
+the node named foobar. The following example deployment yaml limits the egress
+bandwidth of the netperf Pod on the node's physical device:
+
+.. parsed-literal::
+
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: netperf
+    spec:
+      selector:
+        matchLabels:
+          run: netperf
+      replicas: 1
+      template:
+        metadata:
+          labels:
+            run: netperf
+          annotations:
+            kubernetes.io/egress-bandwidth: "10M"
+        spec:
+          nodeName: foobar
+          containers:
+          - name: netperf
+            image: cilium/netperf
+            ports:
+            - containerPort: 12865
+
+Once up and running, ``netperf`` client can be invoked from a different node in the
+cluster using the Pod's IP (in this case ``10.217.0.254``) directly. The test streaming
+direction is from the netperf deployment towards the client, hence ``TCP_MAERTS``:
+
+.. parsed-literal::
+
+  netperf -t TCP_MAERTS -H 10.217.0.254
+  MIGRATED TCP MAERTS TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 10.217.0.254 () port 0 AF_INET
+  Recv   Send    Send
+  Socket Socket  Message  Elapsed
+  Size   Size    Size     Time     Throughput
+  bytes  bytes   bytes    secs.    10^6bits/sec
+
+   87380  16384  16384    10.00       9.56
+
+As can be seen, egress traffic of the netperf Pod has been limited to 10Mbit per second.
+
+In order to introspect current endpoint bandwidth settings from BPF side, the following
+command can be run:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium bpf bandwidth list
+    IDENTITY   EGRESS BANDWIDTH (BitsPerSec)
+    491        10M
+
+Each Pod is represented in Cilium as an :ref:`endpoint` which has an identity. The above
+identity can then be correlated with the ``cilium endpoint list`` command.
+
+Limitations
+###########
+
+    * Bandwidth enforcement currently does not work in combination with L7 Cilium Network Policies.
+      In case they select the Pod at egress, then the bandwidth enforcement will be disabled for
+      those Pods.
diff --git a/Documentation/gettingstarted/index.rst b/Documentation/gettingstarted/index.rst
index 1efe35f23e..ede8a1a8a4 100644
--- a/Documentation/gettingstarted/index.rst
+++ b/Documentation/gettingstarted/index.rst
@@ -63,6 +63,7 @@ Advanced Networking
    encryption
    host-services
    kubeproxy-free
+   bandwidth-manager
    kata
    ipam
 
diff --git a/Documentation/spelling_wordlist.txt b/Documentation/spelling_wordlist.txt
index 55aceb4ec3..b3c2cb2b82 100644
--- a/Documentation/spelling_wordlist.txt
+++ b/Documentation/spelling_wordlist.txt
@@ -92,6 +92,7 @@ Lund
 Lx
 Majkowski
 Marek
+Mbit
 Meetup
 Mellanox
 Memcached
@@ -504,6 +505,7 @@ netdevice
 netdevices
 netdevsim
 netfilter
+netperf
 netsec
 netvsc
 newproto
diff --git a/api/v1/models/bandwidth_manager.go b/api/v1/models/bandwidth_manager.go
new file mode 100644
index 0000000000..9e97e198a9
--- /dev/null
+++ b/api/v1/models/bandwidth_manager.go
@@ -0,0 +1,51 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// Copyright 2017-2020 Authors of Cilium
+// SPDX-License-Identifier: Apache-2.0
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+)
+
+// BandwidthManager Status of bandwidth manager
+//
+// +k8s:deepcopy-gen=true
+//
+// swagger:model BandwidthManager
+type BandwidthManager struct {
+
+	// devices
+	Devices []string `json:"devices"`
+
+	// Is bandwidth manager enabled
+	Enabled bool `json:"enabled,omitempty"`
+}
+
+// Validate validates this bandwidth manager
+func (m *BandwidthManager) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *BandwidthManager) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *BandwidthManager) UnmarshalBinary(b []byte) error {
+	var res BandwidthManager
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/api/v1/models/status_response.go b/api/v1/models/status_response.go
index 3427f45523..6688e2379f 100644
--- a/api/v1/models/status_response.go
+++ b/api/v1/models/status_response.go
@@ -22,6 +22,9 @@ import (
 // swagger:model StatusResponse
 type StatusResponse struct {
 
+	// Status of bandwidth manager
+	BandwidthManager *BandwidthManager `json:"bandwidth-manager,omitempty"`
+
 	// Status of BPF maps
 	BpfMaps *BPFMapStatus `json:"bpf-maps,omitempty"`
 
@@ -78,6 +81,10 @@ type StatusResponse struct {
 func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	var res []error
 
+	if err := m.validateBandwidthManager(formats); err != nil {
+		res = append(res, err)
+	}
+
 	if err := m.validateBpfMaps(formats); err != nil {
 		res = append(res, err)
 	}
@@ -144,6 +151,24 @@ func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
+func (m *StatusResponse) validateBandwidthManager(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.BandwidthManager) { // not required
+		return nil
+	}
+
+	if m.BandwidthManager != nil {
+		if err := m.BandwidthManager.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("bandwidth-manager")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (m *StatusResponse) validateBpfMaps(formats strfmt.Registry) error {
 
 	if swag.IsZero(m.BpfMaps) { // not required
diff --git a/api/v1/models/zz_generated.deepcopy.go b/api/v1/models/zz_generated.deepcopy.go
index 08ba3e693f..12a8edcc90 100644
--- a/api/v1/models/zz_generated.deepcopy.go
+++ b/api/v1/models/zz_generated.deepcopy.go
@@ -49,6 +49,27 @@ func (in *BPFMapStatus) DeepCopy() *BPFMapStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BandwidthManager) DeepCopyInto(out *BandwidthManager) {
+	*out = *in
+	if in.Devices != nil {
+		in, out := &in.Devices, &out.Devices
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BandwidthManager.
+func (in *BandwidthManager) DeepCopy() *BandwidthManager {
+	if in == nil {
+		return nil
+	}
+	out := new(BandwidthManager)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CIDRPolicy) DeepCopyInto(out *CIDRPolicy) {
 	*out = *in
@@ -732,6 +753,11 @@ func (in *RequestResponseStatistics) DeepCopy() *RequestResponseStatistics {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StatusResponse) DeepCopyInto(out *StatusResponse) {
 	*out = *in
+	if in.BandwidthManager != nil {
+		in, out := &in.BandwidthManager, &out.BandwidthManager
+		*out = new(BandwidthManager)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.BpfMaps != nil {
 		in, out := &in.BpfMaps, &out.BpfMaps
 		*out = new(BPFMapStatus)
diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml
index c32a1e2a27..92fbaa8dca 100644
--- a/api/v1/openapi.yaml
+++ b/api/v1/openapi.yaml
@@ -1597,6 +1597,9 @@ definitions:
       proxy:
         description: Status of proxy
         "$ref": "#/definitions/ProxyStatus"
+      bandwidth-manager:
+        description: Status of bandwidth manager
+        "$ref": "#/definitions/BandwidthManager"
       masquerading:
         description: Status of masquerading
         "$ref": "#/definitions/Masquerading"
@@ -2380,6 +2383,20 @@ definitions:
       forwarded:
         description: Number of messages forwarded
         type: integer
+  BandwidthManager:
+    description: |-
+      Status of bandwidth manager
+
+      +k8s:deepcopy-gen=true
+    type: object
+    properties:
+      enabled:
+        description: Is bandwidth manager enabled
+        type: boolean
+      devices:
+        type: array
+        items:
+          type: string
   Masquerading:
     description: |-
       Status of masquerading
diff --git a/api/v1/server/embedded_spec.go b/api/v1/server/embedded_spec.go
index e52d56536d..a3ec578731 100644
--- a/api/v1/server/embedded_spec.go
+++ b/api/v1/server/embedded_spec.go
@@ -1432,6 +1432,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -3286,6 +3302,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
@@ -5105,6 +5125,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -7240,6 +7276,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
diff --git a/bpf/Makefile b/bpf/Makefile
index a0c355d764..d3ca769e43 100644
--- a/bpf/Makefile
+++ b/bpf/Makefile
@@ -56,6 +56,8 @@ LB_OPTIONS = \
 	-DENABLE_IPV4:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_SRC_RANGE_CHECK \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER:-DENABLE_SRC_RANGE_CHECK
 
 # These options are intended to max out the BPF program complexity. it is load
 # tested as well.
diff --git a/bpf/bpf_alignchecker.c b/bpf/bpf_alignchecker.c
index dcbcb89dbe..95c70c81d9 100644
--- a/bpf/bpf_alignchecker.c
+++ b/bpf/bpf_alignchecker.c
@@ -81,6 +81,8 @@ int main(void)
 	DECLARE(struct, lb_affinity_match, iter);
 	DECLARE(struct, lb4_src_range_key, iter);
 	DECLARE(struct, lb6_src_range_key, iter);
+	DECLARE(struct, edt_id, iter);
+	DECLARE(struct, edt_info, iter);
 
 	return 0;
 }
diff --git a/bpf/bpf_host.c b/bpf/bpf_host.c
index aa30cbe003..dab48a5455 100644
--- a/bpf/bpf_host.c
+++ b/bpf/bpf_host.c
@@ -26,6 +26,7 @@
 #endif
 
 #include "lib/common.h"
+#include "lib/edt.h"
 #include "lib/arp.h"
 #include "lib/maps.h"
 #include "lib/ipv6.h"
@@ -883,6 +884,10 @@ int from_netdev(struct __ctx_buff *ctx)
 __section("from-host")
 int from_host(struct __ctx_buff *ctx)
 {
+	/* Traffic from the host ns going through cilium_host device must
+	 * not be subject to EDT rate-limiting.
+	 */
+	edt_set_aggregate(ctx, 0);
 	return handle_netdev(ctx, true);
 }
 
@@ -927,14 +932,22 @@ int to_netdev(struct __ctx_buff *ctx __maybe_unused)
 		ret = DROP_UNKNOWN_L3;
 		break;
 	}
-
-
 out:
 	if (IS_ERR(ret))
 		return send_drop_notify_error(ctx, src_id, ret, CTX_ACT_DROP,
 					      METRIC_EGRESS);
 #endif /* ENABLE_HOST_FIREWALL */
 
+#if defined(ENABLE_BANDWIDTH_MANAGER)
+	ret = edt_sched_departure(ctx);
+	/* No send_drop_notify_error() here given we're rate-limiting. */
+	if (ret == CTX_ACT_DROP) {
+		update_metrics(ctx_full_len(ctx), METRIC_EGRESS,
+			       -DROP_EDT_HORIZON);
+		return ret;
+	}
+#endif
+
 #if defined(ENABLE_NODEPORT) && \
 	(!defined(ENABLE_DSR) || \
 	 (defined(ENABLE_DSR) && defined(ENABLE_DSR_HYBRID)) || \
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 332d51e224..1f044f9e86 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -16,6 +16,7 @@
 #include "lib/config.h"
 #include "lib/maps.h"
 #include "lib/arp.h"
+#include "lib/edt.h"
 #include "lib/ipv6.h"
 #include "lib/ipv4.h"
 #include "lib/icmp6.h"
@@ -801,6 +802,7 @@ int handle_xgress(struct __ctx_buff *ctx)
 	int ret;
 
 	bpf_clear_meta(ctx);
+	edt_set_aggregate(ctx, LXC_ID);
 
 	send_trace_notify(ctx, TRACE_FROM_LXC, SECLABEL, 0, 0, 0, 0,
 			  TRACE_PAYLOAD_LEN);
diff --git a/bpf/include/bpf/ctx/skb.h b/bpf/include/bpf/ctx/skb.h
index c821347d45..204b15295a 100644
--- a/bpf/include/bpf/ctx/skb.h
+++ b/bpf/include/bpf/ctx/skb.h
@@ -68,6 +68,12 @@ ctx_full_len(const struct __sk_buff *ctx)
 	return ctx->len;
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct __sk_buff *ctx)
+{
+	return ctx->wire_len;
+}
+
 static __always_inline __maybe_unused void
 ctx_store_meta(struct __sk_buff *ctx, const __u32 off, __u32 data)
 {
diff --git a/bpf/include/bpf/ctx/xdp.h b/bpf/include/bpf/ctx/xdp.h
index 48cd35d0d6..df8c300db4 100644
--- a/bpf/include/bpf/ctx/xdp.h
+++ b/bpf/include/bpf/ctx/xdp.h
@@ -251,6 +251,12 @@ ctx_full_len(const struct xdp_md *ctx)
 	return ctx_data_end(ctx) - ctx_data(ctx);
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct xdp_md *ctx)
+{
+	return ctx_full_len(ctx);
+}
+
 struct bpf_elf_map __section_maps cilium_xdp_scratch = {
 	.type		= BPF_MAP_TYPE_PERCPU_ARRAY,
 	.size_key	= sizeof(int),
diff --git a/bpf/include/linux/bpf.h b/bpf/include/linux/bpf.h
index f13e3e6f21..bc5e109b57 100644
--- a/bpf/include/linux/bpf.h
+++ b/bpf/include/linux/bpf.h
@@ -3590,19 +3590,12 @@ struct __sk_buff {
 	/* ... here. */
 
 	__u32 data_meta;
-
-#if 0
-	/* TODO: compiled out for now since this cases verifier breakage
-	 * on older kernels resulting in invalid bpf_context access. Needs
-	 * follow-up investigation.
-	 */
 	__bpf_md_ptr(struct bpf_flow_keys *, flow_keys);
 	__u64 tstamp;
 	__u32 wire_len;
 	__u32 gso_segs;
 	__bpf_md_ptr(struct bpf_sock *, sk);
 	__u32 gso_size;
-#endif
 };
 
 struct bpf_tunnel_key {
@@ -3927,7 +3920,12 @@ struct bpf_sock_addr {
 	__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.
 				 * Stored in network byte order.
 				 */
-#if 0 /* TODO, see comment in __sk_buff */
+
+	/* TODO: compiled out for now since this cases verifier breakage
+	 * on older kernels resulting in invalid bpf_context access. Needs
+	 * follow-up investigation.
+	 */
+#if 0
 	__bpf_md_ptr(struct bpf_sock *, sk);
 #endif
 };
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index fbbdc190bf..7e741b745b 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -208,6 +208,17 @@ struct endpoint_info {
 	__u32		pad[4];
 };
 
+struct edt_id {
+	__u64		id;
+};
+
+struct edt_info {
+	__u64		bps;
+	__u64		t_last;
+	__u64		t_horizon_drop;
+	__u64		pad[4];
+};
+
 struct remote_endpoint_info {
 	__u32		sec_label;
 	__u32		tunnel_endpoint;
@@ -347,11 +358,11 @@ enum {
 #define DROP_UNUSED8		-159 /* unused */
 #define DROP_NO_TUNNEL_ENDPOINT -160
 #define DROP_UNUSED9		-161 /* unused */
-#define DROP_UNUSED10		-162 /* unused */
-#define DROP_UNKNOWN_CT			-163
-#define DROP_HOST_UNREACHABLE		-164
+#define DROP_EDT_HORIZON	-162
+#define DROP_UNKNOWN_CT		-163
+#define DROP_HOST_UNREACHABLE	-164
 #define DROP_NO_CONFIG		-165
-#define DROP_UNSUPPORTED_L2		-166
+#define DROP_UNSUPPORTED_L2	-166
 #define DROP_NAT_NO_MAPPING	-167
 #define DROP_NAT_UNSUPP_PROTO	-168
 #define DROP_NO_FIB		-169
diff --git a/bpf/lib/edt.h b/bpf/lib/edt.h
new file mode 100644
index 0000000000..c00ebeda0b
--- /dev/null
+++ b/bpf/lib/edt.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2020 Authors of Cilium */
+
+#ifndef __EDT_H_
+#define __EDT_H_
+
+#include "common.h"
+#include "time.h"
+#include "maps.h"
+
+#ifdef ENABLE_BANDWIDTH_MANAGER
+static __always_inline void edt_set_aggregate(struct __ctx_buff *ctx,
+					      __u32 aggregate)
+{
+	/* 16 bit as current used aggregate, and preserved in host ns. */
+	ctx->queue_mapping = aggregate;
+}
+
+static __always_inline __u32 edt_get_aggregate(struct __ctx_buff *ctx)
+{
+	__u32 aggregate = ctx->queue_mapping;
+
+	/* We need to reset queue mapping here such that new mapping will
+	 * be performed based on skb hash. See netdev_pick_tx().
+	 */
+	ctx->queue_mapping = 0;
+
+	return aggregate;
+}
+
+static __always_inline int edt_sched_departure(struct __ctx_buff *ctx)
+{
+	__u64 delay, now, t, t_next;
+	struct edt_id aggregate;
+	struct edt_info *info;
+	__u16 proto;
+
+	if (!validate_ethertype(ctx, &proto))
+		return CTX_ACT_OK;
+	if (proto != bpf_htons(ETH_P_IP) &&
+	    proto != bpf_htons(ETH_P_IPV6))
+		return CTX_ACT_OK;
+
+	aggregate.id = edt_get_aggregate(ctx);
+	if (!aggregate.id)
+		return CTX_ACT_OK;
+
+	info = map_lookup_elem(&THROTTLE_MAP, &aggregate);
+	if (!info)
+		return CTX_ACT_OK;
+
+	now = ktime_get_ns();
+	t = ctx->tstamp;
+	if (t < now)
+		t = now;
+	delay = ((__u64)ctx_wire_len(ctx)) * NSEC_PER_SEC / info->bps;
+	t_next = READ_ONCE(info->t_last) + delay;
+	if (t_next <= t) {
+		WRITE_ONCE(info->t_last, t);
+		return CTX_ACT_OK;
+	}
+	/* FQ implements a drop horizon, see also 39d010504e6b ("net_sched:
+	 * sch_fq: add horizon attribute"). However, we explicitly need the
+	 * drop horizon here to i) avoid having t_last messed up and ii) to
+	 * potentially allow for per aggregate control.
+	 */
+	if (t_next - now >= info->t_horizon_drop)
+		return CTX_ACT_DROP;
+	WRITE_ONCE(info->t_last, t_next);
+	ctx->tstamp = t_next;
+	return CTX_ACT_OK;
+}
+#else
+static __always_inline void
+edt_set_aggregate(struct __ctx_buff *ctx __maybe_unused,
+		  __u32 aggregate __maybe_unused)
+{
+}
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+#endif /* __EDT_H_ */
diff --git a/bpf/lib/maps.h b/bpf/lib/maps.h
index 9cbb9ddadd..bfc21f442f 100644
--- a/bpf/lib/maps.h
+++ b/bpf/lib/maps.h
@@ -42,6 +42,17 @@ struct bpf_elf_map __section_maps POLICY_CALL_MAP = {
 };
 #endif /* SKIP_POLICY_MAP */
 
+#ifdef ENABLE_BANDWIDTH_MANAGER
+struct bpf_elf_map __section_maps THROTTLE_MAP = {
+	.type		= BPF_MAP_TYPE_HASH,
+	.size_key	= sizeof(struct edt_id),
+	.size_value	= sizeof(struct edt_info),
+	.pinning	= PIN_GLOBAL_NS,
+	.max_elem	= THROTTLE_MAP_SIZE,
+	.flags		= BPF_F_NO_PREALLOC,
+};
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+
 /* Map to link endpoint id to per endpoint cilium_policy map */
 #ifdef SOCKMAP
 struct bpf_elf_map __section_maps EP_POLICY_MAP = {
diff --git a/bpf/lib/overloadable_skb.h b/bpf/lib/overloadable_skb.h
index 7a563f872d..7b66a722e1 100644
--- a/bpf/lib/overloadable_skb.h
+++ b/bpf/lib/overloadable_skb.h
@@ -9,11 +9,11 @@ bpf_clear_meta(struct __sk_buff *ctx)
 {
 	__u32 zero = 0;
 
-	ctx->cb[0] = zero;
-	ctx->cb[1] = zero;
-	ctx->cb[2] = zero;
-	ctx->cb[3] = zero;
-	ctx->cb[4] = zero;
+	WRITE_ONCE(ctx->cb[0], zero);
+	WRITE_ONCE(ctx->cb[1], zero);
+	WRITE_ONCE(ctx->cb[2], zero);
+	WRITE_ONCE(ctx->cb[3], zero);
+	WRITE_ONCE(ctx->cb[4], zero);
 }
 
 /**
diff --git a/bpf/node_config.h b/bpf/node_config.h
index 7d0598ef12..d1cac7e9c9 100644
--- a/bpf/node_config.h
+++ b/bpf/node_config.h
@@ -91,6 +91,8 @@ DEFINE_IPV6(HOST_IP, 0xbe, 0xef, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0xa, 0x
 #define LB4_AFFINITY_MAP test_cilium_lb4_affinity
 #define LB6_AFFINITY_MAP test_cilium_lb6_affinity
 #define LB_AFFINITY_MATCH_MAP test_cilium_lb_affinity_match
+#define THROTTLE_MAP test_cilium_throttle
+#define THROTTLE_MAP_SIZE 65536
 #define ENABLE_ARP_RESPONDER
 #define TUNNEL_ENDPOINT_MAP_SIZE 65536
 #define ENDPOINTS_MAP_SIZE 65536
diff --git a/bugtool/cmd/configuration.go b/bugtool/cmd/configuration.go
index 8939cdf0b1..f37590c45a 100644
--- a/bugtool/cmd/configuration.go
+++ b/bugtool/cmd/configuration.go
@@ -54,10 +54,13 @@ func defaultCommands(confDir string, cmdDir string, k8sPods []string) []string {
 		"ip -6 n",
 		"ss -t -p -a -i -s",
 		"ss -u -p -a -i -s",
+		"tc qdisc show",
+		"tc -d -s qdisc show",
 		"uname -a",
 		"top -b -n 1",
 		"uptime",
 		"dmesg --time-format=iso",
+		"sysctl -a",
 		"bpftool map show",
 		"bpftool prog show",
 		// LB and CT map for debugging services; using bpftool for a reliable dump
@@ -231,6 +234,7 @@ func copyCiliumInfoCommands(cmdDir string, k8sPods []string) []string {
 		"cilium metrics list",
 		"cilium fqdn cache list",
 		"cilium config",
+		"cilium bpf bandwidth list",
 		"cilium bpf tunnel list",
 		"cilium bpf lb list",
 		"cilium bpf endpoint list",
diff --git a/cilium/cmd/bpf_bandwidth.go b/cilium/cmd/bpf_bandwidth.go
new file mode 100644
index 0000000000..d374e8f8ad
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth.go
@@ -0,0 +1,29 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// bpfBandwidthCmd represents the bpf_bandwidth command
+var bpfBandwidthCmd = &cobra.Command{
+	Use:   "bandwidth",
+	Short: "BPF datapath bandwidth settings",
+}
+
+func init() {
+	bpfCmd.AddCommand(bpfBandwidthCmd)
+}
diff --git a/cilium/cmd/bpf_bandwidth_list.go b/cilium/cmd/bpf_bandwidth_list.go
new file mode 100644
index 0000000000..80d4656b80
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth_list.go
@@ -0,0 +1,104 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+
+	"github.com/spf13/cobra"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+var bpfBandwidthListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List BPF datapath bandwidth settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		common.RequireRootPrivilege("cilium bpf bandwidth list")
+
+		bpfBandwidthList := make(map[string][]string)
+		if err := bwmap.ThrottleMap.Dump(bpfBandwidthList); err != nil {
+			fmt.Fprintf(os.Stderr, "error dumping contents of map: %s\n", err)
+			os.Exit(1)
+		}
+
+		if command.OutputJSON() {
+			if err := command.PrintOutput(bpfBandwidthList); err != nil {
+				fmt.Fprintf(os.Stderr, "error getting output of map in JSON: %s\n", err)
+				os.Exit(1)
+			}
+			return
+		}
+
+		listBandwidth(bpfBandwidthList)
+	},
+}
+
+func listBandwidth(bpfBandwidthList map[string][]string) {
+	if len(bpfBandwidthList) == 0 {
+		fmt.Fprintf(os.Stderr, "No entries found.\n")
+		return
+	}
+
+	const (
+		labelsIDTitle   = "IDENTITY"
+		labelsBandwidth = "EGRESS BANDWIDTH (BitsPerSec)"
+	)
+
+	w := tabwriter.NewWriter(os.Stdout, 5, 0, 3, ' ', 0)
+	fmt.Fprintf(w, "%s\t%s\n", labelsIDTitle, labelsBandwidth)
+
+	const numColumns = 2
+	rows := [][numColumns]string{}
+
+	for key, value := range bpfBandwidthList {
+		bps, _ := strconv.Atoi(value[0])
+		bps *= 8
+		quantity := resource.NewQuantity(int64(bps), resource.DecimalSI)
+		rows = append(rows, [numColumns]string{key, quantity.String()})
+	}
+
+	sort.Slice(rows, func(i, j int) bool {
+		for k := 0; k < numColumns; k++ {
+			c := strings.Compare(rows[i][k], rows[j][k])
+
+			if c != 0 {
+				return c < 0
+			}
+		}
+
+		return false
+	})
+
+	for _, r := range rows {
+		fmt.Fprintf(w, "%s\t%s\n", r[0], r[1])
+	}
+
+	w.Flush()
+}
+
+func init() {
+	bpfBandwidthCmd.AddCommand(bpfBandwidthListCmd)
+	command.AddJSONOutput(bpfBandwidthListCmd)
+}
diff --git a/contrib/coccinelle/const.cocci b/contrib/coccinelle/const.cocci
index 5d1e6515ef..53a6e66fdf 100644
--- a/contrib/coccinelle/const.cocci
+++ b/contrib/coccinelle/const.cocci
@@ -43,6 +43,7 @@ position p;
       when != &x->z
       when != e = x
       when != WRITE_ONCE(x->z, ...)
+      when != WRITE_ONCE(x->z[...], ...)
       when != f(..., x, ...)
       // Special case for the addr field since it's an array in several
       // structures.
diff --git a/daemon/cmd/daemon.go b/daemon/cmd/daemon.go
index ebdd28349d..051ee73793 100644
--- a/daemon/cmd/daemon.go
+++ b/daemon/cmd/daemon.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/cilium/cilium/api/v1/models"
 	health "github.com/cilium/cilium/cilium-health/launch"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/clustermesh"
 	"github.com/cilium/cilium/pkg/controller"
@@ -201,6 +202,8 @@ func (d *Daemon) init() error {
 	sockops.SkmsgDisable()
 
 	if !option.Config.DryMode {
+		bandwidth.InitBandwidthManager()
+
 		if err := d.createNodeConfigHeaderfile(); err != nil {
 			return err
 		}
@@ -434,12 +437,17 @@ func NewDaemon(ctx context.Context, epMgr *endpointmanager.EndpointManager, dp d
 		bootstrapStats.k8sInit.End(true)
 	}
 
+	// Perform an early probe on the underlying kernel on whether BandwidthManager
+	// can be supported or not. This needs to be done before detectNativeDevices()
+	// as BandwidthManager needs these to be available for setup.
+	bandwidth.ProbeBandwidthManager()
+
 	// The kube-proxy replacement and host-fw devices detection should happen after
 	// establishing a connection to kube-apiserver, but before starting a k8s watcher.
 	// This is because the device detection requires self (Cilium)Node object,
 	// and the k8s service watcher depends on option.Config.EnableNodePort flag
 	// which can be modified after the device detection.
-	detectDevicesForNodePortAndHostFirewall(isKubeProxyReplacementStrict)
+	detectNativeDevices(isKubeProxyReplacementStrict)
 	finishKubeProxyReplacementInit(isKubeProxyReplacementStrict)
 
 	// BPF masquerade depends on BPF NodePort, so the following checks should
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 248c6c9510..9be19ba4b8 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -520,6 +520,9 @@ func init() {
 	flags.Bool(option.EnableSVCSourceRangeCheck, true, "Enable check of service source ranges (currently, only for LoadBalancer)")
 	option.BindEnv(option.EnableSVCSourceRangeCheck)
 
+	flags.Bool(option.EnableBandwidthManager, false, "Enable BPF bandwidth manager")
+	option.BindEnv(option.EnableBandwidthManager)
+
 	flags.String(option.NodePortMode, option.NodePortModeSNAT, "BPF NodePort mode (\"snat\", \"dsr\", \"hybrid\")")
 	option.BindEnv(option.NodePortMode)
 
diff --git a/daemon/cmd/endpoint.go b/daemon/cmd/endpoint.go
index b9f094d25e..a6518694f5 100644
--- a/daemon/cmd/endpoint.go
+++ b/daemon/cmd/endpoint.go
@@ -26,6 +26,7 @@ import (
 	. "github.com/cilium/cilium/api/v1/server/restapi/endpoint"
 	"github.com/cilium/cilium/pkg/annotation"
 	"github.com/cilium/cilium/pkg/api"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/endpoint"
 	endpointid "github.com/cilium/cilium/pkg/endpoint/id"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
@@ -371,7 +372,7 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 	defer d.endpointCreations.EndCreateRequest(ep)
 
 	if ep.K8sNamespaceAndPodNameIsSet() && k8s.IsEnabled() {
-		pod, cp, identityLabels, info, _, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
+		pod, cp, identityLabels, info, annotations, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
 		if err != nil {
 			ep.Logger("api").WithError(err).Warning("Unable to fetch kubernetes labels")
 		} else {
@@ -381,6 +382,20 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 			}
 			addLabels.MergeLabels(identityLabels)
 			infoLabels.MergeLabels(info)
+			if _, ok := annotations[bandwidth.IngressBandwidth]; ok {
+				log.WithFields(logrus.Fields{
+					logfields.K8sPodName:  epTemplate.K8sNamespace + "/" + epTemplate.K8sPodName,
+					logfields.Annotations: logfields.Repr(annotations),
+				}).Warningf("Endpoint has %s annotation which is unsupported. This annotation is ignored.",
+					bandwidth.IngressBandwidth)
+			}
+			if _, ok := annotations[bandwidth.EgressBandwidth]; ok && !option.Config.EnableBandwidthManager {
+				log.WithFields(logrus.Fields{
+					logfields.K8sPodName:  epTemplate.K8sNamespace + "/" + epTemplate.K8sPodName,
+					logfields.Annotations: logfields.Repr(annotations),
+				}).Warningf("Endpoint has %s annotation, but BPF bandwidth manager is disabled. This annotation is ignored.",
+					bandwidth.EgressBandwidth)
+			}
 		}
 	}
 
@@ -430,9 +445,15 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 			if err != nil {
 				return "", err
 			}
-
 			return p.Annotations[annotation.ProxyVisibility], nil
 		})
+		ep.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+			p, err := d.k8sWatcher.GetCachedPod(ns, podName)
+			if err != nil {
+				return "", err
+			}
+			return p.Annotations[bandwidth.EgressBandwidth], nil
+		})
 	}
 
 	regenTriggered := ep.UpdateLabels(ctx, addLabels, infoLabels, true)
diff --git a/daemon/cmd/kube_proxy_replacement.go b/daemon/cmd/kube_proxy_replacement.go
index 4fbf4b50c6..78cef11291 100644
--- a/daemon/cmd/kube_proxy_replacement.go
+++ b/daemon/cmd/kube_proxy_replacement.go
@@ -233,11 +233,10 @@ func initKubeProxyReplacementOptions() (strict bool) {
 	return
 }
 
-// detectDevicesForNodePortAndHostFirewall tries to detect bpf_host devices
-// (if needed).
-func detectDevicesForNodePortAndHostFirewall(strict bool) {
+// detectNativeDevices tries to detect bpf_host devices (if needed).
+func detectNativeDevices(strict bool) {
 	detectNodePortDevs := len(option.Config.Devices) == 0 &&
-		(option.Config.EnableNodePort || option.Config.EnableHostFirewall)
+		(option.Config.EnableNodePort || option.Config.EnableHostFirewall || option.Config.EnableBandwidthManager)
 	detectDirectRoutingDev := option.Config.EnableNodePort &&
 		option.Config.DirectRoutingDevice == ""
 	if detectNodePortDevs || detectDirectRoutingDev {
diff --git a/daemon/cmd/status.go b/daemon/cmd/status.go
index 248435fa5f..8562e4b39d 100644
--- a/daemon/cmd/status.go
+++ b/daemon/cmd/status.go
@@ -141,6 +141,24 @@ func (d *Daemon) getMasqueradingStatus() *models.Masquerading {
 	return s
 }
 
+func (d *Daemon) getBandwidthManagerStatus() *models.BandwidthManager {
+	s := &models.BandwidthManager{
+		Enabled: option.Config.EnableBandwidthManager,
+	}
+
+	if !option.Config.EnableBandwidthManager {
+		return s
+	}
+
+	devices := make([]string, len(option.Config.Devices))
+	for i, iface := range option.Config.Devices {
+		devices[i] = iface
+	}
+
+	s.Devices = devices
+	return s
+}
+
 func (d *Daemon) getKubeProxyReplacementStatus() *models.KubeProxyReplacement {
 	if !k8s.IsEnabled() {
 		return &models.KubeProxyReplacement{Mode: models.KubeProxyReplacementModeDisabled}
@@ -830,6 +848,7 @@ func (d *Daemon) startStatusCollector() {
 	}
 
 	d.statusResponse.Masquerading = d.getMasqueradingStatus()
+	d.statusResponse.BandwidthManager = d.getBandwidthManagerStatus()
 	d.statusResponse.BpfMaps = d.getBPFMapStatus()
 
 	d.statusCollector = status.NewCollector(probes, status.Config{})
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
index 7b8c2f3701..8169aa18a9 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -184,8 +190,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-c-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40001
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -199,7 +208,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -210,7 +219,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-c
@@ -316,7 +325,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -327,7 +336,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -420,14 +429,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -470,7 +479,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -481,7 +490,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -578,7 +587,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -589,7 +598,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-intra-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -604,7 +613,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -657,7 +666,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -668,7 +677,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-multi-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -683,7 +692,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -736,7 +745,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -747,7 +756,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -762,7 +771,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -815,7 +824,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -826,7 +835,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -841,7 +850,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -894,7 +903,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -905,7 +914,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -920,7 +929,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -973,7 +982,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -984,7 +993,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -999,7 +1008,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1052,7 +1061,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -1063,7 +1072,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1116,7 +1125,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -1127,7 +1136,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1180,7 +1189,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -1191,7 +1200,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1245,7 +1254,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -1256,7 +1265,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1542,7 +1551,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -1560,7 +1570,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -1579,7 +1590,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -1597,7 +1609,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -1634,7 +1647,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -1709,7 +1723,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
@@ -1790,7 +1804,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1834,7 +1848,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1878,7 +1892,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1922,7 +1936,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1966,7 +1980,7 @@ spec:
   ingress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml b/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
index c0b0a2fefc..3080249160 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -251,14 +257,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -301,7 +307,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -312,7 +318,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -419,7 +425,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -430,7 +436,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -483,7 +489,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -494,7 +500,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -548,7 +554,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -559,7 +565,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -717,7 +723,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -735,7 +742,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -754,7 +762,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -829,7 +838,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml b/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
index 1349b06a41..e606942579 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-c-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40001
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -33,7 +36,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -44,7 +47,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-c
@@ -150,7 +153,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -161,7 +164,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-intra-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -176,7 +179,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -229,7 +232,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -240,7 +243,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-multi-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -255,7 +258,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -308,7 +311,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -319,7 +322,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -334,7 +337,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -387,7 +390,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -398,7 +401,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -413,7 +416,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -466,7 +469,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -477,7 +480,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -492,7 +495,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -545,7 +548,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -556,7 +559,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -571,7 +574,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -600,7 +603,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -618,7 +622,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -660,7 +665,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -704,7 +709,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -748,7 +753,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -792,7 +797,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -836,7 +841,7 @@ spec:
   ingress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -865,8 +870,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -879,7 +887,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -890,7 +898,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -909,7 +917,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -935,8 +944,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -950,7 +962,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -961,7 +973,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -980,7 +992,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml b/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
index e9753583fa..776fab622b 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
@@ -658,8 +658,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -672,7 +675,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -683,7 +686,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -702,7 +705,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -728,8 +732,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -743,7 +750,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -754,7 +761,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -773,7 +780,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
index 30c897023a..2c377c5070 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -305,14 +311,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -503,7 +509,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -521,7 +528,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -540,7 +548,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -615,7 +624,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check.yaml b/examples/kubernetes/connectivity-check/connectivity-check.yaml
index dd11d3dffd..4e93187069 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -305,14 +311,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -463,7 +469,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -474,7 +480,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -527,7 +533,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -538,7 +544,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -591,7 +597,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -602,7 +608,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -656,7 +662,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -667,7 +673,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -825,7 +831,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -843,7 +850,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -862,7 +870,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -937,7 +946,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/defaults.cue b/examples/kubernetes/connectivity-check/defaults.cue
index d017abc446..b0d9c9282f 100644
--- a/examples/kubernetes/connectivity-check/defaults.cue
+++ b/examples/kubernetes/connectivity-check/defaults.cue
@@ -10,13 +10,13 @@ deployment: [ID=_]: {
 
 	// readinessProbe target name
 	if ID =~ "^pod-to-a.*$" || ID =~ "^host-to-a.*$" {
-		_probeTarget: *"echo-a" | string
+		_probeTarget: *"echo-a:8080" | string
 	}
 	if ID =~ "^pod-to-b.*$" || ID =~ "^host-to-b.*$" {
-		_probeTarget: *"echo-b" | string
+		_probeTarget: *"echo-b:8080" | string
 	}
 	if ID =~ "^pod-to-c.*$" || ID =~ "^host-to-c.*$" {
-		_probeTarget: *"echo-c" | string
+		_probeTarget: *"echo-c:8080" | string
 	}
 }
 
diff --git a/examples/kubernetes/connectivity-check/echo-servers.cue b/examples/kubernetes/connectivity-check/echo-servers.cue
index 77b698dfc7..9f75f82f6a 100644
--- a/examples/kubernetes/connectivity-check/echo-servers.cue
+++ b/examples/kubernetes/connectivity-check/echo-servers.cue
@@ -3,7 +3,7 @@ package connectivity_check
 // Default parameters for echo servers (may be overridden).
 _echoDeployment: {
 	_image:       "docker.io/cilium/json-mock:1.2"
-	_probeTarget: *"localhost" | string
+	_probeTarget: *"localhost:8080" | string
 	_probePath:   ""
 }
 
@@ -15,19 +15,21 @@ _echoDeploymentWithHostPort: _echoDeployment & {
 
 // Regular service exposed via ClusterIP.
 deployment: "echo-a": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeClusterIP: true
 	metadata: labels: component: "network-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http"}]}]
 }
 
 // Service exposed via NodePort + headless svc.
 deployment: "echo-b": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeNodePort: true
 	_exposeHeadless: true
 	_nodePort:       31313
 
 	metadata: labels: component: "services-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80, hostPort: 40000}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http", hostPort: 40000}]}]
 }
 // Expose hostport by deploying a host pod and adding a headless service with no port.
 deployment: "echo-b-host": _echoDeploymentWithHostPort & {
@@ -39,7 +41,7 @@ deployment: "echo-b-host": _echoDeploymentWithHostPort & {
 
 ingressL7Policy: {
 	_allowDNS: true
-	_port:     *"80" | string
+	_port:     *"8080" | string
 	_rules: [{
 		toPorts: [{
 			ports: [{
@@ -59,11 +61,12 @@ ingressL7Policy: {
 
 // Service with policy applied.
 deployment: "echo-c": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeClusterIP: true
 	_exposeHeadless:  true
 
 	metadata: labels: component: "proxy-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80, hostPort: 40001}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http", hostPort: 40001}]}]
 }
 ingressCNP: "echo-c": ingressL7Policy & {}
 // Expose hostport by deploying a host pod and adding a headless service with no port.
diff --git a/examples/kubernetes/connectivity-check/policy.cue b/examples/kubernetes/connectivity-check/policy.cue
index 01e6141735..a58cce7780 100644
--- a/examples/kubernetes/connectivity-check/policy.cue
+++ b/examples/kubernetes/connectivity-check/policy.cue
@@ -28,7 +28,7 @@ egressCNP: "pod-to-a-allowed-cnp":  _policyResource & {
 		}]
 		toPorts: [{
 			ports: [{
-				port:     "80"
+				port:     "8080"
 				protocol: "TCP"
 			}]
 		}]
diff --git a/examples/kubernetes/connectivity-check/proxy.cue b/examples/kubernetes/connectivity-check/proxy.cue
index d6424aabea..075a975c84 100644
--- a/examples/kubernetes/connectivity-check/proxy.cue
+++ b/examples/kubernetes/connectivity-check/proxy.cue
@@ -13,7 +13,7 @@ _proxyResource: {
 _egressL7Policy: {
 	_allowDNS: true
 
-	_port:   *"80" | string
+	_port:   *"8080" | string
 	_target: *"" | string
 	_rules: [{
 		if _target != "" {
diff --git a/examples/kubernetes/connectivity-check/resources.cue b/examples/kubernetes/connectivity-check/resources.cue
index 3d85dd0486..40d78359ca 100644
--- a/examples/kubernetes/connectivity-check/resources.cue
+++ b/examples/kubernetes/connectivity-check/resources.cue
@@ -273,6 +273,7 @@ for x in [deployment] for k, v in x {
 				if p._expose {
 					let Port = p.containerPort // Port is an alias
 					port: *Port | int
+					name: p._portName
 					if v._exposeNodePort {
 						nodePort: v._nodePort
 					}
@@ -300,6 +301,7 @@ for x in [deployment] for k, v in x {
 				if p._expose {
 					let Port = p.containerPort // Port is an alias
 					port: *Port | int
+					name: p._portName
 				},
 			]
 		}
diff --git a/examples/kubernetes/connectivity-check/services.cue b/examples/kubernetes/connectivity-check/services.cue
index ce502f86bc..91b1478d68 100644
--- a/examples/kubernetes/connectivity-check/services.cue
+++ b/examples/kubernetes/connectivity-check/services.cue
@@ -2,7 +2,7 @@ package connectivity_check
 
 deployment: [ID=_]: {
 	if ID =~ "^[-_a-zA-Z0-9]*-headless$" {
-		_probeTarget: "echo-b-headless"
+		_probeTarget: "echo-b-headless:8080"
 	}
 }
 
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index bdc25c8d74..eae40a6a8f 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -400,6 +400,7 @@ data:
   iptables-lock-timeout: {{ .Values.global.iptablesLockTimeout | quote }}
 {{- end }}
   auto-direct-node-routes: {{ .Values.global.autoDirectNodeRoutes | quote }}
+  enable-bandwidth-manager: {{ .Values.global.bandwidthManager | quote }}
 {{- if .Values.global.nativeRoutingCIDR }}
   native-routing-cidr: {{ .Values.global.nativeRoutingCIDR }}
 {{- end }}
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 847b2b13e8..5da17a6448 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -335,6 +335,10 @@ global:
   # hostFirewall enables the enforcement of host policies in the BPF datapath
   hostFirewall: false
 
+  # bandwidthManager enables BBR and FQ + EDT + BPF based rate-limiting for
+  # Pods that are annotated with kubernetes.io/egress-bandwidth.
+  bandwidthManager: true
+
   # kubeProxyReplacement enables kube-proxy replacement in Cilium BPF datapath
   kubeProxyReplacement: "probe"
 
diff --git a/install/kubernetes/experimental-install.yaml b/install/kubernetes/experimental-install.yaml
index c5a984e5d8..c94fa95ea2 100644
--- a/install/kubernetes/experimental-install.yaml
+++ b/install/kubernetes/experimental-install.yaml
@@ -125,6 +125,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/install/kubernetes/quick-install.yaml b/install/kubernetes/quick-install.yaml
index a34b267659..6209c36cfb 100644
--- a/install/kubernetes/quick-install.yaml
+++ b/install/kubernetes/quick-install.yaml
@@ -111,6 +111,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/pkg/bandwidth/bandwidth.go b/pkg/bandwidth/bandwidth.go
new file mode 100644
index 0000000000..74f1159f46
--- /dev/null
+++ b/pkg/bandwidth/bandwidth.go
@@ -0,0 +1,164 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bandwidth
+
+import (
+	"github.com/cilium/cilium/pkg/datapath/linux/probes"
+	"github.com/cilium/cilium/pkg/logging"
+	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/sysctl"
+	"github.com/sirupsen/logrus"
+	"github.com/vishvananda/netlink"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+const (
+	subsystem = "bandwidth-manager"
+
+	// EgressBandwidth is the K8s Pod annotation.
+	EgressBandwidth = "kubernetes.io/egress-bandwidth"
+	// IngressBandwidth is the K8s Pod annotation.
+	IngressBandwidth = "kubernetes.io/ingress-bandwidth"
+)
+
+var log = logging.DefaultLogger.WithField(logfields.LogSubsys, subsystem)
+
+func GetBytesPerSec(bandwidth string) (uint64, error) {
+	res, err := resource.ParseQuantity(bandwidth)
+	if err != nil {
+		return 0, err
+	}
+	return uint64(res.Value() / 8), err
+}
+
+func ProbeBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if _, err := sysctl.Read("net.core.default_qdisc"); err != nil {
+		log.Warn("BPF bandwidth manager could not read procfs. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	kernelGood := false
+	if h := probes.NewProbeManager().GetHelpers("sched_cls"); h != nil {
+		// We at least need 5.0 kernel for native TCP EDT integration.
+		// Below helper is available for 5.0 kernels and onwards.
+		if _, ok := h["bpf_sk_release"]; ok {
+			kernelGood = true
+		}
+	}
+	if !kernelGood {
+		log.Warn("BPF bandwidth manager needs kernel 5.0 or newer. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+}
+
+func InitBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if len(option.Config.Devices) == 0 {
+		log.Warn("BPF bandwidth manager could not detect host devices. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	log.Info("Setting up BPF bandwidth manager")
+
+	if _, err := bwmap.ThrottleMap.OpenOrCreate(); err != nil {
+		log.WithError(err).Fatal("Failed to access ThrottleMap")
+	}
+
+	type setting struct {
+		name string
+		val  string
+	}
+	baseSettings := []setting{
+		{"net.core.netdev_max_backlog", "1000"},
+		{"net.core.somaxconn", "4096"},
+		{"net.core.default_qdisc", "fq"},
+		{"net.ipv4.tcp_congestion_control", "bbr"},
+		{"net.ipv4.tcp_max_syn_backlog", "4096"},
+	}
+	for _, s := range baseSettings {
+		log.WithFields(logrus.Fields{
+			logfields.SysParamName:  s.name,
+			logfields.SysParamValue: s.val,
+		}).Info("Setting sysctl")
+		if err := sysctl.Write(s.name, s.val); err != nil {
+			log.WithError(err).WithFields(logrus.Fields{
+				logfields.SysParamName:  s.name,
+				logfields.SysParamValue: s.val,
+			}).Fatal("Failed to set sysctl needed by BPF bandwidth manager.")
+		}
+	}
+
+	for _, device := range option.Config.Devices {
+		link, err := netlink.LinkByName(device)
+		if err != nil {
+			log.WithError(err).WithField("device", device).Warn("Link does not exist")
+			continue
+		}
+		// We strictly want to avoid a down/up cycle on the device at
+		// runtime, so given we've changed the default qdisc to FQ, we
+		// need to reset the root qdisc, and then set up MQ which will
+		// automatically get FQ leaf qdiscs (given it's been default).
+		qdisc := &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "noqueue",
+		}
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			log.WithError(err).WithField("device", device).
+				Fatalf("Cannot replace root Qdisc to %s", qdisc.QdiscType)
+		}
+		qdisc = &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "mq",
+		}
+		which := "mq with fq leafs"
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			// No MQ support, so just replace to FQ directly.
+			fq := &netlink.Fq{
+				QdiscAttrs: netlink.QdiscAttrs{
+					LinkIndex: link.Attrs().Index,
+					Parent:    netlink.HANDLE_ROOT,
+				},
+				Pacing: 1,
+			}
+			// At this point there is nothing we can do about
+			// it if we fail here, so hard bail out.
+			if err = netlink.QdiscReplace(fq); err != nil {
+				log.WithError(err).WithField("device", device).
+					Fatalf("Cannot replace root Qdisc to %s", fq.Type())
+			}
+			which = "fq"
+		}
+		log.WithField("device", device).Infof("Setting qdisc to %s", which)
+	}
+}
diff --git a/pkg/bandwidth/doc.go b/pkg/bandwidth/doc.go
new file mode 100644
index 0000000000..9dd2769508
--- /dev/null
+++ b/pkg/bandwidth/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bandwidth provides efficient EDT-based rate-limiting.
+package bandwidth
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 819bcc613a..6607cfedc4 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -400,6 +400,17 @@ func FormatStatusResponse(w io.Writer, sr *models.StatusResponse, sd StatusDetai
 		}
 	}
 
+	if sr.BandwidthManager != nil {
+		var status string
+		if !sr.BandwidthManager.Enabled {
+			status = "Disabled"
+		} else {
+			status = fmt.Sprintf("EDT with BPF\t[%s]",
+				strings.Join(sr.BandwidthManager.Devices, ", "))
+		}
+		fmt.Fprintf(w, "BandwidthManager:\t%s\n", status)
+	}
+
 	if sr.Masquerading != nil {
 		var status string
 		if !sr.Masquerading.Enabled {
diff --git a/pkg/datapath/alignchecker/alignchecker.go b/pkg/datapath/alignchecker/alignchecker.go
index 6f9e3b1eff..f75deaddc8 100644
--- a/pkg/datapath/alignchecker/alignchecker.go
+++ b/pkg/datapath/alignchecker/alignchecker.go
@@ -19,6 +19,7 @@ import (
 
 	check "github.com/cilium/cilium/pkg/alignchecker"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/eppolicymap"
 	"github.com/cilium/cilium/pkg/maps/eventsmap"
@@ -86,6 +87,8 @@ func CheckStructAlignments(path string) error {
 		"lb_affinity_val":   {reflect.TypeOf(lbmap.AffinityValue{})},
 		"lb4_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey4{})},
 		"lb6_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey6{})},
+		"edt_id":            {reflect.TypeOf(bwmap.EdtId{})},
+		"edt_info":          {reflect.TypeOf(bwmap.EdtInfo{})},
 	}
 	if err := check.CheckStructAlignments(path, toCheck, true); err != nil {
 		return err
diff --git a/pkg/datapath/linux/config/config.go b/pkg/datapath/linux/config/config.go
index 1dc2e0c1bc..6926893205 100644
--- a/pkg/datapath/linux/config/config.go
+++ b/pkg/datapath/linux/config/config.go
@@ -33,6 +33,7 @@ import (
 	"github.com/cilium/cilium/pkg/defaults"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/labels"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
 	"github.com/cilium/cilium/pkg/maps/callsmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/encrypt"
@@ -316,6 +317,12 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC
 		}
 	}
 
+	if option.Config.EnableBandwidthManager {
+		cDefinesMap["ENABLE_BANDWIDTH_MANAGER"] = "1"
+		cDefinesMap["THROTTLE_MAP"] = bwmap.MapName
+		cDefinesMap["THROTTLE_MAP_SIZE"] = fmt.Sprintf("%d", bwmap.MapSize)
+	}
+
 	if option.Config.EnableHostFirewall {
 		cDefinesMap["ENABLE_HOST_FIREWALL"] = "1"
 	}
diff --git a/pkg/datapath/loader/base.go b/pkg/datapath/loader/base.go
index a19c56a51f..469bebf4a7 100644
--- a/pkg/datapath/loader/base.go
+++ b/pkg/datapath/loader/base.go
@@ -129,6 +129,7 @@ func (l *Loader) Reinitialize(ctx context.Context, o datapath.BaseProgramOwner,
 		{"net.core.bpf_jit_enable", "1", true},
 		{"net.ipv4.conf.all.rp_filter", "0", false},
 		{"kernel.unprivileged_bpf_disabled", "1", true},
+		{"kernel.timer_migration", "0", true},
 	}
 
 	// Lock so that endpoints cannot be built while we are compile base programs.
@@ -290,11 +291,16 @@ func (l *Loader) Reinitialize(ctx context.Context, o datapath.BaseProgramOwner,
 	args[initArgNrCPUs] = fmt.Sprintf("%d", common.GetNumPossibleCPUs(log))
 
 	clockSource := []string{"ktime", "jiffies"}
-	log.Infof("Setting up base BPF datapath (BPF %s instruction set, %s clock source)",
-		args[initBPFCPU], clockSource[option.Config.ClockSource])
+	log.WithFields(logrus.Fields{
+		logfields.BPFInsnSet:     args[initBPFCPU],
+		logfields.BPFClockSource: clockSource[option.Config.ClockSource],
+	}).Info("Setting up BPF datapath")
 
 	for _, s := range sysSettings {
-		log.Infof("Setting sysctl %s=%s", s.name, s.val)
+		log.WithFields(logrus.Fields{
+			logfields.SysParamName:  s.name,
+			logfields.SysParamValue: s.val,
+		}).Info("Setting sysctl")
 		if err := sysctl.Write(s.name, s.val); err != nil {
 			if !s.ignoreErr {
 				return fmt.Errorf("Failed to sysctl -w %s=%s: %s", s.name, s.val, err)
diff --git a/pkg/datapath/loader/cache.go b/pkg/datapath/loader/cache.go
index 618e1db614..8cacad8d56 100644
--- a/pkg/datapath/loader/cache.go
+++ b/pkg/datapath/loader/cache.go
@@ -58,6 +58,7 @@ var ignoredELFPrefixes = []string{
 	"cilium_tunnel",              // Global
 	"cilium_ipv4_frag_datagrams", // Global
 	"cilium_ipmasq",              // Global
+	"cilium_throttle",            // Global
 	"from-container",             // Prog name
 	"to-container",               // Prog name
 	"from-netdev",                // Prog name
diff --git a/pkg/datapath/maps/map.go b/pkg/datapath/maps/map.go
index 74928d03b9..6e0a7650c7 100644
--- a/pkg/datapath/maps/map.go
+++ b/pkg/datapath/maps/map.go
@@ -165,6 +165,10 @@ func (ms *MapSweeper) RemoveDisabledMaps() {
 		maps = append(maps, "cilium_ipv4_frag_datagrams")
 	}
 
+	if !option.Config.EnableBandwidthManager {
+		maps = append(maps, "cilium_throttle")
+	}
+
 	// Can be removed with Cilium 1.8
 	maps = append(maps, []string{
 		"cilium_policy_reserved_1",
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index 6860b69318..5301f7690d 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -31,6 +31,7 @@ import (
 	"github.com/cilium/cilium/api/v1/models"
 	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/annotation"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/controller"
@@ -160,6 +161,9 @@ type Endpoint struct {
 	// information of the endpoint has changed
 	identityRevision int
 
+	// bps is the egress rate of the endpoint
+	bps uint64
+
 	// mac is the MAC address of the endpoint
 	//
 	mac mac.MAC // Container MAC address.
@@ -1646,6 +1650,13 @@ func (e *Endpoint) RunMetadataResolver(resolveMetadata MetadataResolverCB) {
 					}
 					return annotations[annotation.ProxyVisibility], nil
 				})
+				e.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+					_, _, _, _, annotations, err := resolveMetadata(ns, podName)
+					if err != nil {
+						return "", err
+					}
+					return annotations[bandwidth.EgressBandwidth], nil
+				})
 				e.UpdateLabels(ctx, identityLabels, info, true)
 				close(done)
 				return nil
diff --git a/pkg/endpoint/events.go b/pkg/endpoint/events.go
index 241bf314f4..b04f60de2d 100644
--- a/pkg/endpoint/events.go
+++ b/pkg/endpoint/events.go
@@ -15,8 +15,13 @@
 package endpoint
 
 import (
+	"strconv"
+
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/eventqueue"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 
 	"github.com/sirupsen/logrus"
@@ -179,3 +184,65 @@ func (ev *EndpointPolicyVisibilityEvent) Handle(res chan interface{}) {
 	}
 	return
 }
+
+// EndpointPolicyBandwidthEvent contains all fields necessary to update
+// the Pod's bandwidth policy.
+type EndpointPolicyBandwidthEvent struct {
+	ep     *Endpoint
+	annoCB AnnotationsResolverCB
+}
+
+// Handle handles the policy bandwidth update.
+func (ev *EndpointPolicyBandwidthEvent) Handle(res chan interface{}) {
+	var bps uint64
+
+	e := ev.ep
+	if err := e.lockAlive(); err != nil {
+		// If the endpoint is being deleted, we don't need to
+		// update its bandwidth policy.
+		res <- &EndpointRegenerationResult{
+			err: nil,
+		}
+		return
+	}
+	defer func() {
+		e.unlock()
+	}()
+
+	bandwidthEgress, err := ev.annoCB(e.K8sNamespace, e.K8sPodName)
+	if err != nil || !option.Config.EnableBandwidthManager {
+		res <- &EndpointRegenerationResult{
+			err: err,
+		}
+		return
+	}
+	if bandwidthEgress != "" {
+		bps, err = bandwidth.GetBytesPerSec(bandwidthEgress)
+		if err == nil {
+			err = bwmap.Update(e.ID, bps)
+		}
+	} else {
+		err = bwmap.Delete(e.ID)
+	}
+	if err != nil {
+		res <- &EndpointRegenerationResult{
+			err: err,
+		}
+		return
+	}
+
+	bpsOld := "inf"
+	bpsNew := "inf"
+	if e.bps != 0 {
+		bpsOld = strconv.FormatUint(e.bps, 10)
+	}
+	if bps != 0 {
+		bpsNew = strconv.FormatUint(bps, 10)
+	}
+	e.getLogger().Debugf("Updating %s from %s to %s bytes/sec", bandwidth.EgressBandwidth,
+		bpsOld, bpsNew)
+	e.bps = bps
+	res <- &EndpointRegenerationResult{
+		err: nil,
+	}
+}
diff --git a/pkg/endpoint/policy.go b/pkg/endpoint/policy.go
index a8e5944228..37f67f94ba 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -852,6 +852,20 @@ func (e *Endpoint) UpdateVisibilityPolicy(annoCB AnnotationsResolverCB) {
 	<-ch
 }
 
+// UpdateBandwidthPolicy updates the egress bandwidth of this endpoint to
+// progagate the throttle rate to the BPF data path.
+func (e *Endpoint) UpdateBandwidthPolicy(annoCB AnnotationsResolverCB) {
+	ch, err := e.eventQueue.Enqueue(eventqueue.NewEvent(&EndpointPolicyBandwidthEvent{
+		ep:     e,
+		annoCB: annoCB,
+	}))
+	if err != nil {
+		e.getLogger().WithError(err).Error("Unable to enqueue endpoint policy bandwidth event")
+		return
+	}
+	<-ch
+}
+
 // GetRealizedPolicyRuleLabelsForKey returns the list of policy rule labels
 // which match a given flow key (in host byte-order). The returned
 // LabelArrayList must not be modified. This function is exported to be
diff --git a/pkg/k8s/watchers/pod.go b/pkg/k8s/watchers/pod.go
index 70f52695fa..a6eeb8f96c 100644
--- a/pkg/k8s/watchers/pod.go
+++ b/pkg/k8s/watchers/pod.go
@@ -25,6 +25,7 @@ import (
 	"sync"
 
 	"github.com/cilium/cilium/pkg/annotation"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/comparator"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/endpoint"
@@ -224,7 +225,9 @@ func (k *K8sWatcher) updateK8sPodV1(oldK8sPod, newK8sPod *slim_corev1.Pod) error
 	// Check annotation updates.
 	oldAnno := oldK8sPod.ObjectMeta.Annotations
 	newAnno := newK8sPod.ObjectMeta.Annotations
-	annotationsChanged := !k8s.AnnotationsEqual([]string{annotation.ProxyVisibility}, oldAnno, newAnno)
+	annoChangedProxy := !k8s.AnnotationsEqual([]string{annotation.ProxyVisibility}, oldAnno, newAnno)
+	annoChangedBandwidth := !k8s.AnnotationsEqual([]string{bandwidth.EgressBandwidth}, oldAnno, newAnno)
+	annotationsChanged := annoChangedProxy || annoChangedBandwidth
 
 	// Check label updates too.
 	oldPodLabels := oldK8sPod.ObjectMeta.Labels
@@ -255,13 +258,24 @@ func (k *K8sWatcher) updateK8sPodV1(oldK8sPod, newK8sPod *slim_corev1.Pod) error
 	}
 
 	if annotationsChanged {
-		podEP.UpdateVisibilityPolicy(func(ns, podName string) (proxyVisibility string, err error) {
-			p, err := k.GetCachedPod(ns, podName)
-			if err != nil {
-				return "", nil
-			}
-			return p.ObjectMeta.Annotations[annotation.ProxyVisibility], nil
-		})
+		if annoChangedProxy {
+			podEP.UpdateVisibilityPolicy(func(ns, podName string) (proxyVisibility string, err error) {
+				p, err := k.GetCachedPod(ns, podName)
+				if err != nil {
+					return "", nil
+				}
+				return p.ObjectMeta.Annotations[annotation.ProxyVisibility], nil
+			})
+		}
+		if annoChangedBandwidth {
+			podEP.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+				p, err := k.GetCachedPod(ns, podName)
+				if err != nil {
+					return "", nil
+				}
+				return p.ObjectMeta.Annotations[bandwidth.EgressBandwidth], nil
+			})
+		}
 		realizePodAnnotationUpdate(podEP)
 	}
 	return nil
diff --git a/pkg/kvstore/etcd.go b/pkg/kvstore/etcd.go
index 50d5e2ff72..eee6aad6fb 100644
--- a/pkg/kvstore/etcd.go
+++ b/pkg/kvstore/etcd.go
@@ -51,9 +51,11 @@ const (
 	// EtcdBackendName is the backend name for etcd
 	EtcdBackendName = "etcd"
 
-	EtcdAddrOption       = "etcd.address"
-	isEtcdOperatorOption = "etcd.operator"
-	EtcdOptionConfig     = "etcd.config"
+	EtcdAddrOption               = "etcd.address"
+	isEtcdOperatorOption         = "etcd.operator"
+	EtcdOptionConfig             = "etcd.config"
+	etcdOptionKeepAliveHeartbeat = "etcd.keepaliveHeartbeat"
+	etcdOptionKeepAliveTimeout   = "etcd.keepaliveTimeout"
 
 	// EtcdRateLimitOption specifies maximum kv operations per second
 	EtcdRateLimitOption = "etcd.qps"
@@ -116,6 +118,20 @@ func newEtcdModule() backendModule {
 			EtcdOptionConfig: &backendOption{
 				description: "Path to etcd configuration file",
 			},
+			etcdOptionKeepAliveTimeout: &backendOption{
+				description: "Timeout after which an unanswered heartbeat triggers the connection to be closed",
+				validate: func(v string) error {
+					_, err := time.ParseDuration(v)
+					return err
+				},
+			},
+			etcdOptionKeepAliveHeartbeat: &backendOption{
+				description: "Heartbeat interval to keep gRPC connection alive",
+				validate: func(v string) error {
+					_, err := time.ParseDuration(v)
+					return err
+				},
+			},
 			EtcdRateLimitOption: &backendOption{
 				description: "Rate limit in kv store operations per second",
 				validate: func(v string) error {
@@ -162,6 +178,12 @@ func shuffleEndpoints(endpoints []string) {
 	})
 }
 
+type clientOptions struct {
+	KeepAliveHeartbeat time.Duration
+	KeepAliveTimeout   time.Duration
+	RateLimit          int
+}
+
 func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (BackendOperations, chan error) {
 	errChan := make(chan error, 10)
 
@@ -170,10 +192,23 @@ func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (Backend
 
 	rateLimitOpt, rateLimitSet := e.opts[EtcdRateLimitOption]
 
-	rateLimit := defaults.KVstoreQPS
+	clientOptions := clientOptions{
+		KeepAliveHeartbeat: 15 * time.Second,
+		KeepAliveTimeout:   25 * time.Second,
+		RateLimit:          defaults.KVstoreQPS,
+	}
+
 	if rateLimitSet {
 		// error is discarded here because this option has validation
-		rateLimit, _ = strconv.Atoi(rateLimitOpt.value)
+		clientOptions.RateLimit, _ = strconv.Atoi(rateLimitOpt.value)
+	}
+
+	if o, ok := e.opts[etcdOptionKeepAliveTimeout]; ok && o.value != "" {
+		clientOptions.KeepAliveTimeout, _ = time.ParseDuration(o.value)
+	}
+
+	if o, ok := e.opts[etcdOptionKeepAliveHeartbeat]; ok && o.value != "" {
+		clientOptions.KeepAliveHeartbeat, _ = time.ParseDuration(o.value)
 	}
 
 	var configPath string
@@ -211,7 +246,7 @@ func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (Backend
 	for {
 		// connectEtcdClient will close errChan when the connection attempt has
 		// been successful
-		backend, err := connectEtcdClient(ctx, e.config, configPath, errChan, rateLimit, opts)
+		backend, err := connectEtcdClient(ctx, e.config, configPath, errChan, clientOptions, opts)
 		switch {
 		case os.IsNotExist(err):
 			log.WithError(err).Info("Waiting for all etcd configuration files to be available")
@@ -625,7 +660,7 @@ func (e *etcdClient) renewLockSession(ctx context.Context) error {
 	return nil
 }
 
-func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath string, errChan chan error, rateLimit int, opts *ExtraOptions) (BackendOperations, error) {
+func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath string, errChan chan error, clientOptions clientOptions, opts *ExtraOptions) (BackendOperations, error) {
 	if cfgPath != "" {
 		cfg, err := newConfig(cfgPath)
 		if err != nil {
@@ -645,6 +680,11 @@ func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath strin
 	// block until DialTimeout is reached or a connection to the server
 	// is made.
 	config.DialTimeout = 0
+	// Ping the server to verify if the server connection is still valid
+	config.DialKeepAliveTime = clientOptions.KeepAliveHeartbeat
+	// Timeout if the server does not reply within 15 seconds and close the
+	// connection. Ideally it should be lower than staleLockTimeout
+	config.DialKeepAliveTimeout = clientOptions.KeepAliveTimeout
 	c, err := client.New(*config)
 	if err != nil {
 		return nil, err
@@ -691,7 +731,7 @@ func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath strin
 		latestStatusSnapshot: "Waiting for initial connection to be established",
 		stopStatusChecker:    make(chan struct{}),
 		extraOptions:         opts,
-		limiter:              rate.NewLimiter(rate.Limit(rateLimit), rateLimit),
+		limiter:              rate.NewLimiter(rate.Limit(clientOptions.RateLimit), clientOptions.RateLimit),
 		statusCheckErrors:    make(chan error, 128),
 	}
 
diff --git a/pkg/logging/logfields/logfields.go b/pkg/logging/logfields/logfields.go
index fd3fad3dc7..0aaa6b2e09 100644
--- a/pkg/logging/logfields/logfields.go
+++ b/pkg/logging/logfields/logfields.go
@@ -16,6 +16,8 @@
 package logfields
 
 const (
+	// Annotations are any annotations for Pods
+	Annotations = "annotations"
 
 	// LogSubsys is the field denoting the subsystem when logging
 	LogSubsys = "subsys"
@@ -250,6 +252,12 @@ const (
 	// CiliumNetworkPolicyName is the name of a CiliumNetworkPolicy
 	CiliumNetworkPolicyName = "ciliumNetworkPolicyName"
 
+	// BPFClockSource denotes the internal clock source (ktime vs jiffies)
+	BPFClockSource = "bpfClockSource"
+
+	// BPFInsnSet denotes the instruction set version
+	BPFInsnSet = "bpfInsnSet"
+
 	// BPFMapKey is a key from a BPF map
 	BPFMapKey = "bpfMapKey"
 
diff --git a/pkg/maps/bwmap/bwmap.go b/pkg/maps/bwmap/bwmap.go
new file mode 100644
index 0000000000..a93845d4c4
--- /dev/null
+++ b/pkg/maps/bwmap/bwmap.go
@@ -0,0 +1,79 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bwmap
+
+import (
+	"fmt"
+	"time"
+	"unsafe"
+
+	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/maps/lxcmap"
+)
+
+const (
+	MapName = "cilium_throttle"
+	// Flow aggregate is per Pod, so same size as Endpoint map.
+	MapSize = lxcmap.MaxEntries
+
+	// DefaultDropHorizon represents maximum allowed departure
+	// time delta in future. Given applications can set SO_TXTIME
+	// from user space this is a limit to prevent buggy applications
+	// to fill the FQ qdisc.
+	DefaultDropHorizon = 2 * time.Second
+)
+
+type EdtId struct {
+	Id uint64 `align:"id"`
+}
+
+func (k *EdtId) GetKeyPtr() unsafe.Pointer  { return unsafe.Pointer(k) }
+func (k *EdtId) NewValue() bpf.MapValue     { return &EdtInfo{} }
+func (k *EdtId) String() string             { return fmt.Sprintf("%d", int(k.Id)) }
+func (k *EdtId) DeepCopyMapKey() bpf.MapKey { return &EdtId{k.Id} }
+
+type EdtInfo struct {
+	Bps             uint64    `align:"bps"`
+	TimeLast        uint64    `align:"t_last"`
+	TimeHorizonDrop uint64    `align:"t_horizon_drop"`
+	Pad             [4]uint64 `align:"pad"`
+}
+
+func (v *EdtInfo) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(v) }
+func (v *EdtInfo) String() string              { return fmt.Sprintf("%d", int(v.Bps)) }
+func (v *EdtInfo) DeepCopyMapValue() bpf.MapValue {
+	return &EdtInfo{v.Bps, v.TimeLast, v.TimeHorizonDrop, v.Pad}
+}
+
+var ThrottleMap = bpf.NewMap(
+	MapName,
+	bpf.MapTypeHash,
+	&EdtId{}, int(unsafe.Sizeof(EdtId{})),
+	&EdtInfo{}, int(unsafe.Sizeof(EdtInfo{})),
+	MapSize,
+	bpf.BPF_F_NO_PREALLOC, 0,
+	bpf.ConvertKeyValue,
+).WithCache()
+
+func Update(Id uint16, Bps uint64) error {
+	return ThrottleMap.Update(
+		&EdtId{Id: uint64(Id)},
+		&EdtInfo{Bps: Bps, TimeHorizonDrop: uint64(DefaultDropHorizon)})
+}
+
+func Delete(Id uint16) error {
+	return ThrottleMap.Delete(
+		&EdtId{Id: uint64(Id)})
+}
diff --git a/pkg/maps/bwmap/doc.go b/pkg/maps/bwmap/doc.go
new file mode 100644
index 0000000000..ba343845ac
--- /dev/null
+++ b/pkg/maps/bwmap/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bwmap represents the BPF map used to enforce Pod bandwidth
+// limitations via EDT (Earliest Departure Time) + BPF.
+// +groupName=maps
+package bwmap
diff --git a/pkg/monitor/api/drop.go b/pkg/monitor/api/drop.go
index dcc0b59565..8686c5473b 100644
--- a/pkg/monitor/api/drop.go
+++ b/pkg/monitor/api/drop.go
@@ -64,6 +64,7 @@ var errors = map[uint8]string{
 	158: "Service backend not found",
 	160: "No tunnel/encapsulation endpoint (datapath BUG!)",
 	161: "Failed to insert into proxymap", // Unused
+	162: "Reached EDT rate-limiting drop horizon",
 	163: "Unknown connection tracking state",
 	164: "Local host is unreachable",
 	165: "No configuration available to perform policy decision", // Unused
diff --git a/pkg/option/config.go b/pkg/option/config.go
index 2c10a2cb85..be33770400 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -266,6 +266,9 @@ const (
 	// conflicting marks.
 	EnableIdentityMark = "enable-identity-mark"
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager = "enable-bandwidth-manager"
+
 	// LibDir enables the directory path to store runtime build environment
 	LibDir = "lib-dir"
 
@@ -1744,6 +1747,9 @@ type DaemonConfig struct {
 	// features in BPF datapath
 	KubeProxyReplacement string
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager bool
+
 	// EnableExternalIPs enables implementation of k8s services with externalIPs in datapath
 	EnableExternalIPs bool
 
@@ -2334,6 +2340,7 @@ func (c *DaemonConfig) Populate() {
 	c.EnableAutoProtectNodePortRange = viper.GetBool(EnableAutoProtectNodePortRange)
 	c.KubeProxyReplacement = viper.GetString(KubeProxyReplacement)
 	c.EnableSessionAffinity = viper.GetBool(EnableSessionAffinity)
+	c.EnableBandwidthManager = viper.GetBool(EnableBandwidthManager)
 	c.EnableHostFirewall = viper.GetBool(EnableHostFirewall)
 	c.EncryptInterface = viper.GetString(EncryptInterface)
 	c.EncryptNode = viper.GetBool(EncryptNode)
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index ca8f8e8f78..6f2ba5e73b 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -346,10 +346,9 @@ func (l4 *L4Filter) ToMapState(policyOwner PolicyOwner, direction trafficdirecti
 //
 // The caller is responsible for making sure the same identity is not
 // present in both 'added' and 'deleted'.
-func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 	log.WithFields(logrus.Fields{
 		logfields.EndpointSelector: selector,
-		logfields.PolicyID:         selections,
 		logfields.AddedPolicyID:    added,
 		logfields.DeletedPolicyID:  deleted,
 	}).Debug("identities selected by L4Filter updated")
diff --git a/pkg/policy/selectorcache.go b/pkg/policy/selectorcache.go
index 3a500046e2..f413948eea 100644
--- a/pkg/policy/selectorcache.go
+++ b/pkg/policy/selectorcache.go
@@ -106,7 +106,7 @@ type CachedSelectionUser interface {
 	//
 	// The caller is responsible for making sure the same identity is not
 	// present in both 'added' and 'deleted'.
-	IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity)
+	IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity)
 }
 
 // identitySelector is the internal interface for all selectors in the
@@ -378,7 +378,7 @@ type fqdnSelector struct {
 func (f *fqdnSelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range f.users {
 		// pass 'f' to the user as '*fqdnSelector'
-		user.IdentitySelectionUpdated(f, f.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(f, added, deleted)
 	}
 }
 
@@ -437,7 +437,7 @@ type labelIdentitySelector struct {
 func (l *labelIdentitySelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range l.users {
 		// pass 'l' to the user as '*labelIdentitySelector'
-		user.IdentitySelectionUpdated(l, l.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(l, added, deleted)
 	}
 }
 


Test patch:
diff --git a/pkg/envoy/server_test.go b/pkg/envoy/server_test.go
index aca398e145..c8b9aa344a 100644
--- a/pkg/envoy/server_test.go
+++ b/pkg/envoy/server_test.go
@@ -39,7 +39,7 @@ type ServerSuite struct{}
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 var (
diff --git a/pkg/fqdn/dnsproxy/proxy_test.go b/pkg/fqdn/dnsproxy/proxy_test.go
index 0073aa8eb6..1be0bceb88 100644
--- a/pkg/fqdn/dnsproxy/proxy_test.go
+++ b/pkg/fqdn/dnsproxy/proxy_test.go
@@ -136,7 +136,7 @@ func serveDNS(w dns.ResponseWriter, r *dns.Msg) {
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 // Setup identities, ports and endpoint IDs we will need
diff --git a/pkg/k8s/network_policy_test.go b/pkg/k8s/network_policy_test.go
index d244f9c40e..487b8121ba 100644
--- a/pkg/k8s/network_policy_test.go
+++ b/pkg/k8s/network_policy_test.go
@@ -107,7 +107,7 @@ func testNewPolicyRepository() *policy.Repository {
 	return repo
 }
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 func (s *K8sSuite) TestParseNetworkPolicyIngress(c *C) {
diff --git a/pkg/policy/selectorcache_test.go b/pkg/policy/selectorcache_test.go
index 7ed6c40a29..6ab3be9dc5 100644
--- a/pkg/policy/selectorcache_test.go
+++ b/pkg/policy/selectorcache_test.go
@@ -34,7 +34,7 @@ var _ = Suite(&SelectorCacheTestSuite{})
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 type cachedSelectionUser struct {
@@ -106,11 +106,13 @@ func (csu *cachedSelectionUser) RemoveSelector(sel CachedSelector) {
 	csu.c.Assert(csu.notifications, Equals, notifications)
 }
 
-func (csu *cachedSelectionUser) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (csu *cachedSelectionUser) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 	csu.notifications++
 	csu.adds += len(added)
 	csu.deletes += len(deleted)
 
+	selections := selector.GetSelections()
+
 	// Validate added & deleted against the selections
 	for _, add := range added {
 		csu.c.Assert(haveNid(add, selections), Equals, true)