Repository: cilium/cilium
Pull request #12949 (2020-08-21 15:49:48)

envoy: Add Prometheus metrics listener
Add Envoy listener for prometheus metrics at port cunfigured via command line option
'--proxy-prometheus-port'. Default 0 (== disabled).

The corresponding Helm value 'global.proxy.prometheus.port' defaults to 9095, but is
only configured if 'global.prometheus.enabled' is set to 'true'.

Signed-off-by: Jarno Rajahalme <jarno@covalent.io>

```release-note
Envoy metrics from the Cilium host proxy are exported via a prometheus port.
```



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/.github/workflows/images.yaml b/.github/workflows/images.yaml
index 351597cf20..32ddc41de8 100644
--- a/.github/workflows/images.yaml
+++ b/.github/workflows/images.yaml
@@ -70,6 +70,14 @@ jobs:
         with:
           entrypoint: make
           args: -C images cilium-image PUSH=true
+      - uses: docker://docker.io/cilium/image-maker:bc81755ec8f6c5afcb10a416cef73f99a35fee2c
+        name: Run make cilium-test-image
+        env:
+          DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
+          DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+        with:
+          entrypoint: make
+          args: -C images cilium-test-image PUSH=true
       - uses: docker://docker.io/cilium/image-maker:bc81755ec8f6c5afcb10a416cef73f99a35fee2c
         name: Run make operator-image
         env:
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index ed364a4d03..7f6ac32d83 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)
@@ -177,6 +178,7 @@ cilium-agent [flags]
       --prepend-iptables-chains                       Prepend custom iptables chains instead of appending (default true)
       --prometheus-serve-addr string                  IP:Port on which to serve prometheus metrics (pass ":Port" to bind on all interfaces, "" is off)
       --proxy-connect-timeout uint                    Time after which a TCP connect attempt is considered failed unless completed (in seconds) (default 1)
+      --proxy-prometheus-port int                     Port to serve Envoy metrics on. Default 0 (disabled).
       --read-cni-conf string                          Read to the CNI configuration at specified path to extract per node configuration
       --restore                                       Restores state, if possible, from previous daemon (default true)
       --sidecar-istio-proxy-image string              Regular expression matching compatible Istio sidecar istio-proxy container image names (default "cilium/istio_proxy")
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/configuration/metrics.rst b/Documentation/configuration/metrics.rst
index 9f4b59f73a..441356490d 100644
--- a/Documentation/configuration/metrics.rst
+++ b/Documentation/configuration/metrics.rst
@@ -23,18 +23,20 @@ Cilium Metrics
 ==============
 
 Cilium metrics provide insights into the state of Cilium itself, namely
-of the ``cilium-agent`` and ``cilium-operator`` processes. To run Cilium with
-Prometheus metrics enabled, deploy it with the
+of the ``cilium-agent``, ``cilium-envoy``, and ``cilium-operator`` processes.
+To run Cilium with Prometheus metrics enabled, deploy it with the
 ``global.prometheus.enabled=true`` Helm value set.
 
-Cilium metrics are exported under the ``cilium_`` Prometheus namespace.
+Cilium metrics are exported under the ``cilium_`` Prometheus namespace. Envoy
+metrics are exported under the ``envoy_`` Prometheus namespace, of which the
+Cilium-defined metrics are exported under the ``envoy_cilium_`` namespace.
 When running and collecting in Kubernetes they will be tagged with a pod name
 and namespace.
 
 Installation
 ------------
 
-You can enable metrics for ``cilium-agent`` with the Helm value
+You can enable metrics for ``cilium-agent`` (including Envoy) with the Helm value
 ``global.prometheus.enabled=true``. To enable metrics for ``cilium-operator``,
 use ``global.operatorPrometheus.enabled=true``.
 
@@ -45,8 +47,8 @@ use ``global.operatorPrometheus.enabled=true``.
      --set global.prometheus.enabled=true \\
      --set global.operatorPrometheus.enabled=true
 
-The ports can be configured via
-``global.prometheus.port`` or ``global.operatorPrometheus.port`` respectively.
+The ports can be configured via ``global.prometheus.port``,
+``global.proxy.prometheus.port``, or ``global.operatorPrometheus.port`` respectively.
 
 When metrics are enabled, all Cilium components will have the following
 annotations. They can be used to signal Prometheus whether to scrape metrics:
@@ -56,7 +58,18 @@ annotations. They can be used to signal Prometheus whether to scrape metrics:
         prometheus.io/scrape: "true"
         prometheus.io/port: "9090"
 
-Prometheus will pick up the Cilium metrics automatically if the following
+To collect Envoy metrics the Cilium chart will create a Kubernetes headless
+service named ``cilium-agent`` with the ``prometheus.io/scrape:'true'`` annotation set:
+
+.. code-block:: yaml
+
+        prometheus.io/scrape: "true"
+        prometheus.io/port: "9095"
+
+This additional headless service in addition to the other Cilium components is needed
+as each component can only have one Prometheus scrape and port annotation.
+
+Prometheus will pick up the Cilium and Envoy metrics automatically if the following
 option is set in the ``scrape_configs`` section:
 
 .. code-block:: yaml
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..99e10e65bb 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
@@ -729,12 +737,11 @@ func numWorkerThreads() int {
 }
 
 // SendNotification sends an agent notification to the monitor
-func (d *Daemon) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (d *Daemon) SendNotification(notification monitorAPI.AgentNotifyMessage) error {
 	if option.Config.DryMode {
 		return nil
 	}
-	event := monitorAPI.AgentNotify{Type: typ, Text: text}
-	return d.monitorAgent.SendEvent(monitorAPI.MessageTypeAgent, event)
+	return d.monitorAgent.SendEvent(monitorAPI.MessageTypeAgent, notification)
 }
 
 // GetNodeSuffix returns the suffix to be appended to kvstore keys of this
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 248c6c9510..6be79a764d 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -404,6 +404,9 @@ func init() {
 	flags.Uint(option.ProxyConnectTimeout, 1, "Time after which a TCP connect attempt is considered failed unless completed (in seconds)")
 	option.BindEnv(option.ProxyConnectTimeout)
 
+	flags.Int(option.ProxyPrometheusPort, 0, "Port to serve Envoy metrics on. Default 0 (disabled).")
+	option.BindEnv(option.ProxyPrometheusPort)
+
 	flags.Bool(option.DisableEnvoyVersionCheck, false, "Do not perform Envoy binary version check on startup")
 	flags.MarkHidden(option.DisableEnvoyVersionCheck)
 	// Disable version check if Envoy build is disabled
@@ -520,6 +523,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)
 
@@ -1393,11 +1399,9 @@ func runDaemon() {
 	srv.ConfigureAPI()
 	bootstrapStats.initAPI.End(true)
 
-	repr, err := monitorAPI.TimeRepr(time.Now())
+	err = d.SendNotification(monitorAPI.StartMessage(time.Now()))
 	if err != nil {
-		log.WithError(err).Warn("Failed to generate agent start monitor message")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyStart, repr)
+		log.WithError(err).Warn("Failed to send agent start monitor message")
 	}
 
 	log.WithField("bootstrapTime", time.Since(bootstrapTimestamp)).
diff --git a/daemon/cmd/endpoint.go b/daemon/cmd/endpoint.go
index b9f094d25e..a98e20155a 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)
@@ -614,11 +635,7 @@ func (d *Daemon) deleteEndpoint(ep *endpoint.Endpoint) int {
 
 // NotifyMonitorDeleted notifies the monitor that an endpoint has been deleted.
 func (d *Daemon) NotifyMonitorDeleted(ep *endpoint.Endpoint) {
-	repr, err := monitorAPI.EndpointDeleteRepr(ep)
-	// Ignore endpoint deletion if EndpointDeleteRepr != nil
-	if err == nil {
-		d.SendNotification(monitorAPI.AgentNotifyEndpointDeleted, repr)
-	}
+	d.SendNotification(monitorAPI.EndpointDeleteMessage(ep))
 }
 
 // deleteEndpointQuiet sets the endpoint into disconnecting state and removes
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/policy.go b/daemon/cmd/policy.go
index fe6b9e8b97..b78120d079 100644
--- a/daemon/cmd/policy.go
+++ b/daemon/cmd/policy.go
@@ -404,11 +404,9 @@ func (d *Daemon) policyAdd(sourceRules policyAPI.Rules, opts *policy.AddOptions,
 	for _, r := range sourceRules {
 		labels = append(labels, r.Labels.GetModel()...)
 	}
-	repr, err := monitorAPI.PolicyUpdateRepr(len(sourceRules), labels, newRev)
+	err = d.SendNotification(monitorAPI.PolicyUpdateMessage(len(sourceRules), labels, newRev))
 	if err != nil {
-		logger.WithField(logfields.PolicyRevision, newRev).Warn("Failed to represent policy update as monitor notification")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyPolicyUpdated, repr)
+		logger.WithField(logfields.PolicyRevision, newRev).Warn("Failed to send policy update as monitor notification")
 	}
 
 	if option.Config.SelectiveRegeneration {
@@ -635,11 +633,9 @@ func (d *Daemon) policyDelete(labels labels.LabelArray, res chan interface{}) {
 		d.TriggerPolicyUpdates(true, "policy rules deleted")
 	}
 
-	repr, err := monitorAPI.PolicyDeleteRepr(deleted, labels.GetModel(), rev)
+	err := d.SendNotification(monitorAPI.PolicyDeleteMessage(deleted, labels.GetModel(), rev))
 	if err != nil {
-		log.WithField(logfields.PolicyRevision, rev).Warn("Failed to represent policy update as monitor notification")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyPolicyDeleted, repr)
+		log.WithField(logfields.PolicyRevision, rev).Warn("Failed to send policy update as monitor notification")
 	}
 
 	return
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/images/Makefile b/images/Makefile
index 4bfde41e69..a3a5bbd561 100644
--- a/images/Makefile
+++ b/images/Makefile
@@ -46,6 +46,9 @@ check-builder-image:
 cilium-image: .buildx_builder
 	ROOT_CONTEXT=true scripts/build-image.sh cilium-dev images/cilium linux/amd64,linux/arm64 $(OUTPUT) "$$(cat .buildx_builder)" $(REGISTRIES)
 
+cilium-test-image: .buildx_builder
+	ROOT_CONTEXT=true scripts/build-image.sh cilium-test-dev images/cilium-test linux/amd64,linux/arm64 $(OUTPUT) "$$(cat .buildx_builder)" $(REGISTRIES)
+
 operator-image: .buildx_builder
 	ROOT_CONTEXT=true scripts/build-image.sh operator-dev images/operator linux/amd64,linux/arm64 $(OUTPUT) "$$(cat .buildx_builder)" $(REGISTRIES)
 
diff --git a/install/kubernetes/cilium/charts/agent/templates/daemonset.yaml b/install/kubernetes/cilium/charts/agent/templates/daemonset.yaml
index 7ac86ffad2..40e0817016 100644
--- a/install/kubernetes/cilium/charts/agent/templates/daemonset.yaml
+++ b/install/kubernetes/cilium/charts/agent/templates/daemonset.yaml
@@ -177,11 +177,15 @@ spec:
         name: cilium-agent
 {{- if or .Values.global.prometheus.enabled .Values.global.hubble.metrics.enabled }}
         ports:
-{{- if .Values.global.prometheus.enabled }}                
+{{- if .Values.global.prometheus.enabled }}
         - containerPort: {{ .Values.global.prometheus.port }}
           hostPort: {{ .Values.global.prometheus.port }}
           name: prometheus
           protocol: TCP
+        - containerPort: {{ .Values.global.proxy.prometheus.port }}
+          hostPort: {{ .Values.global.proxy.prometheus.port }}
+          name: envoy-metrics
+          protocol: TCP
 {{- end }}
 {{- if .Values.global.hubble.metrics.enabled }}
         - containerPort: {{ .Values.global.hubble.metrics.port }}
diff --git a/install/kubernetes/cilium/charts/agent/templates/svc.yaml b/install/kubernetes/cilium/charts/agent/templates/svc.yaml
index cfb262032c..205702ee3c 100644
--- a/install/kubernetes/cilium/charts/agent/templates/svc.yaml
+++ b/install/kubernetes/cilium/charts/agent/templates/svc.yaml
@@ -14,6 +14,31 @@ spec:
     port: {{ .Values.global.prometheus.port }}
     protocol: TCP
     targetPort: prometheus
+  - name: envoy-metrics
+    port: {{ .Values.global.proxy.prometheus.port }}
+    protocol: TCP
+    targetPort: envoy-metrics
+  selector:
+    k8s-app: cilium
+{{- else if .Values.global.prometheus.enabled }}
+kind: Service
+apiVersion: v1
+metadata:
+  name: cilium-agent
+  namespace: {{ .Release.Namespace }}
+  annotations:
+    prometheus.io/scrape: 'true'
+    prometheus.io/port: {{ .Values.global.proxy.prometheus.port | quote }}
+  labels:
+    k8s-app: cilium
+spec:
+  clusterIP: None
+  type: ClusterIP
+  ports:
+  - name: envoy-metrics
+    port: {{ .Values.global.proxy.prometheus.port }}
+    protocol: TCP
+    targetPort: envoy-metrics
   selector:
     k8s-app: cilium
 {{- end }}
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index bdc25c8d74..303789445b 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -133,6 +133,9 @@ data:
   # NOTE that this will open the port on ALL nodes where Cilium pods are
   # scheduled.
   prometheus-serve-addr: ":{{ .Values.global.prometheus.port }}"
+  # Port to expose Envoy metrics (e.g. "9095"). Envoy metrics listener will be disabled if this
+  # field is not set.
+  proxy-prometheus-port: "{{ .Values.global.proxy.prometheus.port }}"
 {{- end }}
 
 {{- if .Values.global.operatorPrometheus.enabled }}
@@ -400,6 +403,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/charts/nodeinit/templates/daemonset.yaml b/install/kubernetes/cilium/charts/nodeinit/templates/daemonset.yaml
index 72b32a1415..ea55bc195a 100644
--- a/install/kubernetes/cilium/charts/nodeinit/templates/daemonset.yaml
+++ b/install/kubernetes/cilium/charts/nodeinit/templates/daemonset.yaml
@@ -55,7 +55,7 @@ spec:
                     fi
 
                     echo "Waiting on pods to stop..."
-                    if grep -q 'docker' /etc/crictl.yaml; then
+                    if [ ! -f /etc/crictl.yaml ] || grep -q 'docker' /etc/crictl.yaml; then
                       # Works for COS, ubuntu
                       while docker ps | grep -v "node-init" | grep -q "POD_cilium"; do sleep 1; done
                     else
@@ -199,7 +199,7 @@ spec:
 
 {{- if .Values.restartPods }}
               echo "Restarting kubenet managed pods"
-              if grep -q 'docker' /etc/crictl.yaml; then
+              if [ ! -f /etc/crictl.yaml ] || grep -q 'docker' /etc/crictl.yaml; then
                 # Works for COS, ubuntu
                 # Note the first line is the containerID with a trailing \r
                 for f in `find /var/lib/cni/networks/ -type f ! -name lock ! -name last_reserved_ip.0`; do docker rm -f "$(sed 's/\r//;1q' $f)" || true; done
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 847b2b13e8..aad2a67e9c 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"
 
@@ -543,6 +547,10 @@ global:
     # container image names
     sidecarImageRegex: "cilium/istio_proxy"
 
+    prometheus:
+      # Port to expose Envoy metrics on (e.g. "9095"). Prometheus metrics will be exposed on "/metrics".
+      port: "9095"
+
   endpointHealthChecking:
     enabled: true
 
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/ipcache/listener.go b/pkg/datapath/ipcache/listener.go
index 808339018c..0d83443090 100644
--- a/pkg/datapath/ipcache/listener.go
+++ b/pkg/datapath/ipcache/listener.go
@@ -47,7 +47,7 @@ type datapath interface {
 
 // monitor is an interface not notify the monitor about changes to the ipcache
 type monitorNotify interface {
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 }
 
 // BPFListener implements the ipcache.IPIdentityMappingBPFListener
@@ -105,15 +105,15 @@ func (l *BPFListener) notifyMonitor(modType ipcache.CacheModification,
 		oldIdentityPtr = &oldIdentity
 	}
 
-	repr, err := monitorAPI.IPCacheNotificationRepr(cidr.String(), newIdentity, oldIdentityPtr,
-		newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
-	if err == nil {
-		switch modType {
-		case ipcache.Upsert:
-			l.monitorNotify.SendNotification(monitorAPI.AgentNotifyIPCacheUpserted, repr)
-		case ipcache.Delete:
-			l.monitorNotify.SendNotification(monitorAPI.AgentNotifyIPCacheDeleted, repr)
-		}
+	switch modType {
+	case ipcache.Upsert:
+		msg := monitorAPI.IPCacheUpsertedMessage(cidr.String(), newIdentity, oldIdentityPtr,
+			newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
+		l.monitorNotify.SendNotification(msg)
+	case ipcache.Delete:
+		msg := monitorAPI.IPCacheDeletedMessage(cidr.String(), newIdentity, oldIdentityPtr,
+			newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
+		l.monitorNotify.SendNotification(msg)
 	}
 }
 
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..d1e5f98ce9 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -689,20 +689,10 @@ func (e *Endpoint) startRegenerationFailureHandler() {
 }
 
 func (e *Endpoint) notifyEndpointRegeneration(err error) {
-	repr, reprerr := monitorAPI.EndpointRegenRepr(e, err)
+	reprerr := e.owner.SendNotification(monitorAPI.EndpointRegenMessage(e, err))
 	if reprerr != nil {
 		e.getLogger().WithError(reprerr).Warn("Notifying monitor about endpoint regeneration failed")
 	}
-
-	if err != nil {
-		if reprerr == nil && !option.Config.DryMode {
-			e.owner.SendNotification(monitorAPI.AgentNotifyEndpointRegenerateFail, repr)
-		}
-	} else {
-		if reprerr == nil && !option.Config.DryMode {
-			e.owner.SendNotification(monitorAPI.AgentNotifyEndpointRegenerateSuccess, repr)
-		}
-	}
 }
 
 // FormatGlobalEndpointID returns the global ID of endpoint in the format
@@ -852,6 +842,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/endpoint/regeneration/owner.go b/pkg/endpoint/regeneration/owner.go
index 5d7dbcac41..2c37e31489 100644
--- a/pkg/endpoint/regeneration/owner.go
+++ b/pkg/endpoint/regeneration/owner.go
@@ -42,7 +42,7 @@ type Owner interface {
 	GetCIDRPrefixLengths() (s6, s4 []int)
 
 	// SendNotification is called to emit an agent notification
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 
 	// Datapath returns a reference to the datapath implementation.
 	Datapath() datapath.Datapath
diff --git a/pkg/endpointmanager/manager.go b/pkg/endpointmanager/manager.go
index f84202bb57..604ca30b05 100644
--- a/pkg/endpointmanager/manager.go
+++ b/pkg/endpointmanager/manager.go
@@ -486,12 +486,8 @@ func (mgr *EndpointManager) AddEndpoint(owner regeneration.Owner, ep *endpoint.E
 	if err != nil {
 		return err
 	}
+	owner.SendNotification(monitorAPI.EndpointCreateMessage(ep))
 
-	repr, err := monitorAPI.EndpointCreateRepr(ep)
-	// Ignore endpoint creation if EndpointCreateRepr != nil
-	if err == nil {
-		owner.SendNotification(monitorAPI.AgentNotifyEndpointCreated, repr)
-	}
 	return nil
 }
 
diff --git a/pkg/envoy/server.go b/pkg/envoy/server.go
index 598d57e5f0..737200c703 100644
--- a/pkg/envoy/server.go
+++ b/pkg/envoy/server.go
@@ -29,6 +29,7 @@ import (
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/envoy/xds"
 	"github.com/cilium/cilium/pkg/lock"
+	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/policy/api"
@@ -68,10 +69,12 @@ var (
 )
 
 const (
+	adminClusterName      = "envoy-admin"
 	egressClusterName     = "egress-cluster"
 	egressTLSClusterName  = "egress-cluster-tls"
 	ingressClusterName    = "ingress-cluster"
 	ingressTLSClusterName = "ingress-cluster-tls"
+	metricsListenerName   = "envoy-prometheus-metrics-listener"
 	EnvoyTimeout          = 300 * time.Second // must be smaller than endpoint.EndpointGenerationTimeout
 )
 
@@ -360,10 +363,83 @@ func (s *XDSServer) getTcpFilterChainProto(clusterName string, filterName string
 	return chain
 }
 
-// AddListener adds a listener to a running Envoy proxy.
-func (s *XDSServer) AddListener(name string, kind policy.L7ParserType, port uint16, isIngress bool, mayUseOriginalSourceAddr bool, wg *completion.WaitGroup) {
-	log.Debugf("Envoy: %s AddListener %s (mayUseOriginalSourceAddr: %v)", kind, name, mayUseOriginalSourceAddr)
+// AddMetricsListener adds a prometheus metrics listener to Envoy.
+// We could do this in the bootstrap config, but then a failure to bind to the configured port
+// would fail starting Envoy.
+func (s *XDSServer) AddMetricsListener(port uint16, wg *completion.WaitGroup) {
+	if port == 0 {
+		return // 0 == disabled
+	}
+
+	log.WithField(logfields.Port, port).Debug("Envoy: AddMetricsListener")
+
+	s.addListener(metricsListenerName, port, func() *envoy_config_listener.Listener {
+		hcmConfig := &envoy_config_http.HttpConnectionManager{
+			StatPrefix: metricsListenerName,
+			HttpFilters: []*envoy_config_http.HttpFilter{{
+				Name: "envoy.router",
+			}},
+			StreamIdleTimeout: &duration.Duration{}, // 0 == disabled
+			RouteSpecifier: &envoy_config_http.HttpConnectionManager_RouteConfig{
+				RouteConfig: &envoy_config_route.RouteConfiguration{
+					VirtualHosts: []*envoy_config_route.VirtualHost{{
+						Name:    "prometheus_metrics_route",
+						Domains: []string{"*"},
+						Routes: []*envoy_config_route.Route{{
+							Match: &envoy_config_route.RouteMatch{
+								PathSpecifier: &envoy_config_route.RouteMatch_Prefix{Prefix: "/metrics"},
+							},
+							Action: &envoy_config_route.Route_Route{
+								Route: &envoy_config_route.RouteAction{
+									ClusterSpecifier: &envoy_config_route.RouteAction_Cluster{
+										Cluster: adminClusterName,
+									},
+									PrefixRewrite: "/stats/prometheus",
+								},
+							},
+						}},
+					}},
+				},
+			},
+		}
 
+		listenerConf := &envoy_config_listener.Listener{
+			Name: metricsListenerName,
+			Address: &envoy_config_core.Address{
+				Address: &envoy_config_core.Address_SocketAddress{
+					SocketAddress: &envoy_config_core.SocketAddress{
+						Protocol:      envoy_config_core.SocketAddress_TCP,
+						Address:       "::",
+						Ipv4Compat:    true,
+						PortSpecifier: &envoy_config_core.SocketAddress_PortValue{PortValue: uint32(port)},
+					},
+				},
+			},
+			FilterChains: []*envoy_config_listener.FilterChain{{
+				Filters: []*envoy_config_listener.Filter{{
+					Name: "envoy.http_connection_manager",
+					ConfigType: &envoy_config_listener.Filter_TypedConfig{
+						TypedConfig: toAny(hcmConfig),
+					},
+				}},
+			}},
+		}
+
+		return listenerConf
+	}, wg, func(err error) {
+		if err != nil {
+			log.WithField(logfields.Port, port).WithError(err).Debug("Envoy: Adding metrics listener failed")
+			// Remove the added listener in case of a failure
+			s.RemoveListener(metricsListenerName, nil)
+		} else {
+			log.WithField(logfields.Port, port).Info("Envoy: Listening for prometheus metrics")
+		}
+	})
+}
+
+// addListener either reuses an existing listener with 'name', or creates a new one.
+// 'listenerConf()' is only called if a new listener is being created.
+func (s *XDSServer) addListener(name string, port uint16, listenerConf func() *envoy_config_listener.Listener, wg *completion.WaitGroup, cb func(err error)) {
 	s.mutex.Lock()
 	listener := s.listeners[name]
 	if listener == nil {
@@ -390,6 +466,34 @@ func (s *XDSServer) AddListener(name string, kind policy.L7ParserType, port uint
 	}
 	listener.mutex.Unlock() // Listener locked again in callbacks below
 
+	s.listenerMutator.Upsert(ListenerTypeURL, name, listenerConf(), []string{"127.0.0.1"}, wg,
+		func(err error) {
+			// listener might have already been removed, so we can't look again
+			// but we still need to complete all the completions in case
+			// someone is still waiting!
+			listener.mutex.Lock()
+			if err == nil {
+				// Allow future users to not need to wait
+				listener.acked = true
+			} else {
+				// Prevent further reuse of a failed listener
+				listener.nacked = true
+			}
+			// Pass the completion result to all the additional waiters.
+			for _, waiter := range listener.waiters {
+				waiter.Complete(err)
+			}
+			listener.waiters = nil
+			listener.mutex.Unlock()
+
+			if cb != nil {
+				cb(err)
+			}
+		})
+	s.mutex.Unlock()
+}
+
+func (s *XDSServer) getListenerConf(name string, kind policy.L7ParserType, port uint16, isIngress bool, mayUseOriginalSourceAddr bool) *envoy_config_listener.Listener {
 	clusterName := egressClusterName
 	socketMark := int64(0xB00)
 	if isIngress {
@@ -462,35 +566,23 @@ func (s *XDSServer) AddListener(name string, kind policy.L7ParserType, port uint
 				EmitDynamicMetadata: true,
 			})))
 	}
+	return listenerConf
+}
 
-	s.listenerMutator.Upsert(ListenerTypeURL, name, listenerConf, []string{"127.0.0.1"}, wg,
-		func(err error) {
-			// listener might have already been removed, so we can't look again
-			// but we still need to complete all the completions in case
-			// someone is still waiting!
-			listener.mutex.Lock()
-			if err == nil {
-				// Allow future users to not need to wait
-				listener.acked = true
-			} else {
-				// Prevent further reuse of a failed listener
-				listener.nacked = true
-			}
-			// Pass the completion result to all the additional waiters.
-			for _, waiter := range listener.waiters {
-				waiter.Complete(err)
-			}
-			listener.waiters = nil
-			listener.mutex.Unlock()
-		})
-	s.mutex.Unlock()
+// AddListener adds a listener to a running Envoy proxy.
+func (s *XDSServer) AddListener(name string, kind policy.L7ParserType, port uint16, isIngress bool, mayUseOriginalSourceAddr bool, wg *completion.WaitGroup) {
+	log.Debugf("Envoy: %s AddListener %s (mayUseOriginalSourceAddr: %v)", kind, name, mayUseOriginalSourceAddr)
+
+	s.addListener(name, port, func() *envoy_config_listener.Listener {
+		return s.getListenerConf(name, kind, port, isIngress, mayUseOriginalSourceAddr)
+	}, wg, nil)
 }
 
 // RemoveListener removes an existing Envoy Listener.
 func (s *XDSServer) RemoveListener(name string, wg *completion.WaitGroup) xds.AckingResourceMutatorRevertFunc {
-	log.Debugf("Envoy: removeListener %s", name)
+	log.Debugf("Envoy: RemoveListener %s", name)
 
-	var listenerRevertFunc func(*completion.Completion)
+	var listenerRevertFunc xds.AckingResourceMutatorRevertFunc
 
 	s.mutex.Lock()
 	listener, ok := s.listeners[name]
@@ -837,6 +929,27 @@ func createBootstrap(filePath string, nodeId, cluster string, xdsSock, egressClu
 					},
 					Http2ProtocolOptions: &envoy_config_core.Http2ProtocolOptions{},
 				},
+				{
+					Name:                 adminClusterName,
+					ClusterDiscoveryType: &envoy_config_cluster.Cluster_Type{Type: envoy_config_cluster.Cluster_STATIC},
+					ConnectTimeout:       &duration.Duration{Seconds: connectTimeout, Nanos: 0},
+					LbPolicy:             envoy_config_cluster.Cluster_ROUND_ROBIN,
+					LoadAssignment: &envoy_config_endpoint.ClusterLoadAssignment{
+						ClusterName: adminClusterName,
+						Endpoints: []*envoy_config_endpoint.LocalityLbEndpoints{{
+							LbEndpoints: []*envoy_config_endpoint.LbEndpoint{{
+								HostIdentifier: &envoy_config_endpoint.LbEndpoint_Endpoint{
+									Endpoint: &envoy_config_endpoint.Endpoint{
+										Address: &envoy_config_core.Address{
+											Address: &envoy_config_core.Address_Pipe{
+												Pipe: &envoy_config_core.Pipe{Path: adminPath}},
+										},
+									},
+								},
+							}},
+						}},
+					},
+				},
 			},
 		},
 		DynamicResources: &envoy_config_bootstrap.Bootstrap_DynamicResources{
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/agent/agent.go b/pkg/monitor/agent/agent.go
index 42f040e51a..28952f24a2 100644
--- a/pkg/monitor/agent/agent.go
+++ b/pkg/monitor/agent/agent.go
@@ -29,6 +29,7 @@ import (
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/monitor/agent/consumer"
 	"github.com/cilium/cilium/pkg/monitor/agent/listener"
+	"github.com/cilium/cilium/pkg/monitor/api"
 	"github.com/cilium/cilium/pkg/monitor/payload"
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/perf"
@@ -117,6 +118,19 @@ func (a *Agent) SendEvent(typ int, event interface{}) error {
 
 	a.notifyAgentEvent(typ, event)
 
+	// marshal notifications into JSON format for legacy listeners
+	if typ == api.MessageTypeAgent {
+		msg, ok := event.(api.AgentNotifyMessage)
+		if !ok {
+			return errors.New("unexpected event type for MessageTypeAgent")
+		}
+		var err error
+		event, err = msg.ToJSON()
+		if err != nil {
+			return fmt.Errorf("unable to JSON encode agent notification: %w", err)
+		}
+	}
+
 	var buf bytes.Buffer
 	if err := buf.WriteByte(byte(typ)); err != nil {
 		return fmt.Errorf("unable to initialize buffer: %w", err)
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/monitor/api/types.go b/pkg/monitor/api/types.go
index e047101189..c9882e2504 100644
--- a/pkg/monitor/api/types.go
+++ b/pkg/monitor/api/types.go
@@ -170,12 +170,33 @@ func TraceObservationPoint(obsPoint uint8) string {
 	return fmt.Sprintf("%d", obsPoint)
 }
 
-// AgentNotify is a notification from the agent
+// AgentNotify is a notification from the agent. The notification is stored
+// in its JSON-encoded representation
 type AgentNotify struct {
 	Type AgentNotification
 	Text string
 }
 
+// AgentNotify is a notification from the agent. It is similar to AgentNotify,
+// but the notification is an unencoded struct. See the *Message constructors
+// in this package for possible values.
+type AgentNotifyMessage struct {
+	Type         AgentNotification
+	Notification interface{}
+}
+
+// ToJSON encodes a AgentNotifyMessage to its JSON-based AgentNotify representation
+func (m *AgentNotifyMessage) ToJSON() (AgentNotify, error) {
+	repr, err := json.Marshal(m.Notification)
+	if err != nil {
+		return AgentNotify{}, err
+	}
+	return AgentNotify{
+		Type: m.Type,
+		Text: string(repr),
+	}, nil
+}
+
 // AgentNotification specifies the type of agent notification
 type AgentNotification uint32
 
@@ -240,29 +261,32 @@ type PolicyUpdateNotification struct {
 	RuleCount int      `json:"rule_count"`
 }
 
-// PolicyUpdateRepr returns string representation of monitor notification
-func PolicyUpdateRepr(numRules int, labels []string, revision uint64) (string, error) {
+// PolicyUpdateMessage constructs an agent notification message for policy updates
+func PolicyUpdateMessage(numRules int, labels []string, revision uint64) AgentNotifyMessage {
 	notification := PolicyUpdateNotification{
 		Labels:    labels,
 		Revision:  revision,
 		RuleCount: numRules,
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyPolicyUpdated,
+		Notification: notification,
+	}
 }
 
-// PolicyDeleteRepr returns string representation of monitor notification
-func PolicyDeleteRepr(deleted int, labels []string, revision uint64) (string, error) {
+// PolicyDeleteMessage constructs an agent notification message for policy deletion
+func PolicyDeleteMessage(deleted int, labels []string, revision uint64) AgentNotifyMessage {
 	notification := PolicyUpdateNotification{
 		Labels:    labels,
 		Revision:  revision,
 		RuleCount: deleted,
 	}
-	repr, err := json.Marshal(notification)
 
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyPolicyDeleted,
+		Notification: notification,
+	}
 }
 
 // EndpointRegenNotification structures regeneration notification
@@ -272,20 +296,23 @@ type EndpointRegenNotification struct {
 	Error  string   `json:"error,omitempty"`
 }
 
-// EndpointRegenRepr returns string representation of monitor notification
-func EndpointRegenRepr(e notifications.RegenNotificationInfo, err error) (string, error) {
+// EndpointRegenMessage constructs an agent notification message for endpoint regeneration
+func EndpointRegenMessage(e notifications.RegenNotificationInfo, err error) AgentNotifyMessage {
 	notification := EndpointRegenNotification{
 		ID:     e.GetID(),
 		Labels: e.GetOpLabels(),
 	}
 
+	typ := AgentNotifyEndpointRegenerateSuccess
 	if err != nil {
 		notification.Error = err.Error()
+		typ = AgentNotifyEndpointRegenerateFail
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         typ,
+		Notification: notification,
+	}
 }
 
 // EndpointCreateNotification structures the endpoint create notification
@@ -295,8 +322,8 @@ type EndpointCreateNotification struct {
 	Namespace string `json:"namespace,omitempty"`
 }
 
-// EndpointCreateRepr returns string representation of monitor notification
-func EndpointCreateRepr(e notifications.RegenNotificationInfo) (string, error) {
+// EndpointCreateMessage constructs an agent notification message for endpoint creation
+func EndpointCreateMessage(e notifications.RegenNotificationInfo) AgentNotifyMessage {
 	notification := EndpointCreateNotification{
 		EndpointRegenNotification: EndpointRegenNotification{
 			ID:     e.GetID(),
@@ -306,9 +333,10 @@ func EndpointCreateRepr(e notifications.RegenNotificationInfo) (string, error) {
 		Namespace: e.GetK8sNamespace(),
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyEndpointCreated,
+		Notification: notification,
+	}
 }
 
 // EndpointDeleteNotification structures the an endpoint delete notification
@@ -318,8 +346,8 @@ type EndpointDeleteNotification struct {
 	Namespace string `json:"namespace,omitempty"`
 }
 
-// EndpointDeleteRepr returns string representation of monitor notification
-func EndpointDeleteRepr(e notifications.RegenNotificationInfo) (string, error) {
+// EndpointDeleteMessage constructs an agent notification message for endpoint deletion
+func EndpointDeleteMessage(e notifications.RegenNotificationInfo) AgentNotifyMessage {
 	notification := EndpointDeleteNotification{
 		EndpointRegenNotification: EndpointRegenNotification{
 			ID:     e.GetID(),
@@ -329,9 +357,10 @@ func EndpointDeleteRepr(e notifications.RegenNotificationInfo) (string, error) {
 		Namespace: e.GetK8sNamespace(),
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyEndpointDeleted,
+		Notification: notification,
+	}
 }
 
 // IPCacheNotification structures ipcache change notifications
@@ -348,9 +377,9 @@ type IPCacheNotification struct {
 	PodName    string `json:"pod-name,omitempty"`
 }
 
-// IPCacheNotificationRepr returns string representation of monitor notification
-func IPCacheNotificationRepr(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
-	encryptKey uint8, namespace, podName string) (string, error) {
+// IPCacheUpsertedMessage constructs an agent notification message for ipcache upsertions
+func IPCacheUpsertedMessage(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
+	encryptKey uint8, namespace, podName string) AgentNotifyMessage {
 	notification := IPCacheNotification{
 		CIDR:        cidr,
 		Identity:    id,
@@ -362,9 +391,30 @@ func IPCacheNotificationRepr(cidr string, id uint32, oldID *uint32, hostIP net.I
 		PodName:     podName,
 	}
 
-	repr, err := json.Marshal(notification)
+	return AgentNotifyMessage{
+		Type:         AgentNotifyIPCacheUpserted,
+		Notification: notification,
+	}
+}
+
+// IPCacheDeletedMessage constructs an agent notification message for ipcache deletions
+func IPCacheDeletedMessage(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
+	encryptKey uint8, namespace, podName string) AgentNotifyMessage {
+	notification := IPCacheNotification{
+		CIDR:        cidr,
+		Identity:    id,
+		OldIdentity: oldID,
+		HostIP:      hostIP,
+		OldHostIP:   oldHostIP,
+		EncryptKey:  encryptKey,
+		Namespace:   namespace,
+		PodName:     podName,
+	}
 
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyIPCacheDeleted,
+		Notification: notification,
+	}
 }
 
 // TimeNotification structures agent start notification
@@ -372,13 +422,16 @@ type TimeNotification struct {
 	Time string `json:"time"`
 }
 
-// TimeRepr returns string representation of monitor notification
-func TimeRepr(t time.Time) (string, error) {
+// AgentStartMessage constructs an agent notification message when the agent starts
+func StartMessage(t time.Time) AgentNotifyMessage {
 	notification := TimeNotification{
 		Time: t.String(),
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyStart,
+		Notification: notification,
+	}
 }
 
 // ServiceUpsertNotificationAddr is part of ServiceUpsertNotification
@@ -401,13 +454,13 @@ type ServiceUpsertNotification struct {
 	Namespace string `json:"namespace,,omitempty"`
 }
 
-// ServiceUpsertRepr returns string representation of monitor notification
-func ServiceUpsertRepr(
+// ServiceUpsertMessage constructs an agent notification message for service upserts
+func ServiceUpsertMessage(
 	id uint32,
 	frontend ServiceUpsertNotificationAddr,
 	backends []ServiceUpsertNotificationAddr,
 	svcType, svcTrafficPolicy, svcName, svcNamespace string,
-) (string, error) {
+) AgentNotifyMessage {
 	notification := ServiceUpsertNotification{
 		ID:            id,
 		Frontend:      frontend,
@@ -417,8 +470,11 @@ func ServiceUpsertRepr(
 		Name:          svcName,
 		Namespace:     svcNamespace,
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyServiceUpserted,
+		Notification: notification,
+	}
 }
 
 // ServiceDeleteNotification structures service delete notifications
@@ -426,15 +482,16 @@ type ServiceDeleteNotification struct {
 	ID uint32 `json:"id"`
 }
 
-// ServiceDeleteRepr returns string representation of monitor notification
-func ServiceDeleteRepr(
-	id uint32,
-) (string, error) {
+// ServiceDeleteMessage constructs an agent notification message for service deletions
+func ServiceDeleteMessage(id uint32) AgentNotifyMessage {
 	notification := ServiceDeleteNotification{
 		ID: id,
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyServiceDeleted,
+		Notification: notification,
+	}
 }
 
 const (
diff --git a/pkg/option/config.go b/pkg/option/config.go
index 2c10a2cb85..045065eb19 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -154,6 +154,9 @@ const (
 	// EnvoyLog sets the path to a separate Envoy log file, if any
 	EnvoyLog = "envoy-log"
 
+	// ProxyPrometheusPort specifies the port to serve Cilium host proxy metrics on.
+	ProxyPrometheusPort = "proxy-prometheus-port"
+
 	// FixedIdentityMapping is the key-value for the fixed identity mapping
 	// which allows to use reserved label for fixed identities
 	FixedIdentityMapping = "fixed-identity-mapping"
@@ -266,6 +269,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"
 
@@ -913,6 +919,7 @@ var HelpFlagSections = []FlagsSection{
 			HTTPRetryCount,
 			HTTPRetryTimeout,
 			ProxyConnectTimeout,
+			ProxyPrometheusPort,
 			SidecarIstioProxyImage,
 		},
 	},
@@ -1439,6 +1446,9 @@ type DaemonConfig struct {
 	// connection attempt to have timed out.
 	ProxyConnectTimeout int
 
+	// ProxyPrometheusPort specifies the port to serve Envoy metrics on.
+	ProxyPrometheusPort int
+
 	// BPFCompilationDebug specifies whether to compile BPF programs compilation
 	// debugging enabled.
 	BPFCompilationDebug bool
@@ -1744,6 +1754,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 +2347,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)
@@ -2404,6 +2418,7 @@ func (c *DaemonConfig) Populate() {
 	c.PrependIptablesChains = viper.GetBool(PrependIptablesChainsName)
 	c.PrometheusServeAddr = viper.GetString(PrometheusServeAddr)
 	c.ProxyConnectTimeout = viper.GetInt(ProxyConnectTimeout)
+	c.ProxyPrometheusPort = viper.GetInt(ProxyPrometheusPort)
 	c.BlacklistConflictingRoutes = viper.GetBool(BlacklistConflictingRoutes)
 	c.ReadCNIConfiguration = viper.GetString(ReadCNIConfiguration)
 	c.RestoreState = viper.GetBool(Restore)
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)
 	}
 }
 
diff --git a/pkg/proxy/envoyproxy.go b/pkg/proxy/envoyproxy.go
index d04e6e1dec..ed318fdcc7 100644
--- a/pkg/proxy/envoyproxy.go
+++ b/pkg/proxy/envoyproxy.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/envoy"
+	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/revert"
 )
@@ -41,6 +42,12 @@ func createEnvoyRedirect(r *Redirect, stateDir string, xdsServer *envoy.XDSServe
 	envoyOnce.Do(func() {
 		// Start Envoy on first invocation
 		envoyProxy = envoy.StartEnvoy(stateDir, option.Config.EnvoyLogPath, 0)
+
+		if option.Config.ProxyPrometheusPort < 0 || option.Config.ProxyPrometheusPort > 65535 {
+			log.WithField(logfields.Port, option.Config.ProxyPrometheusPort).Error("Envoy: Invalid configured proxy-prometheus-port")
+		} else if option.Config.ProxyPrometheusPort != 0 {
+			xdsServer.AddMetricsListener(uint16(option.Config.ProxyPrometheusPort), wg)
+		}
 	})
 
 	l := r.listener
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 1219220ce5..e0039bd1fb 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -64,7 +64,7 @@ type healthServer interface {
 
 // monitorNotify is used to send update notifications to the monitor
 type monitorNotify interface {
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 }
 
 type svcInfo struct {
@@ -928,17 +928,13 @@ func (s *Service) notifyMonitorServiceUpsert(frontend lb.L3n4AddrID, backends []
 		be = append(be, b)
 	}
 
-	repr, err := monitorAPI.ServiceUpsertRepr(id, fe, be, string(svcType), string(svcTrafficPolicy), svcName, svcNamespace)
-	if err == nil {
-		s.monitorNotify.SendNotification(monitorAPI.AgentNotifyServiceUpserted, repr)
-	}
+	msg := monitorAPI.ServiceUpsertMessage(id, fe, be, string(svcType), string(svcTrafficPolicy), svcName, svcNamespace)
+	s.monitorNotify.SendNotification(msg)
 }
 
 func (s *Service) notifyMonitorServiceDelete(id lb.ID) {
 	if s.monitorNotify != nil {
-		if repr, err := monitorAPI.ServiceDeleteRepr(uint32(id)); err == nil {
-			s.monitorNotify.SendNotification(monitorAPI.AgentNotifyServiceDeleted, repr)
-		}
+		s.monitorNotify.SendNotification(monitorAPI.ServiceDeleteMessage(uint32(id)))
 	}
 }
 


Test patch:
diff --git a/daemon/cmd/daemon_test.go b/daemon/cmd/daemon_test.go
index 6a566f3ff6..0c11bd185d 100644
--- a/daemon/cmd/daemon_test.go
+++ b/daemon/cmd/daemon_test.go
@@ -61,7 +61,7 @@ type DaemonSuite struct {
 	OnGetPolicyRepository  func() *policy.Repository
 	OnQueueEndpointBuild   func(ctx context.Context, epID uint64) (func(), error)
 	OnGetCompilationLock   func() *lock.RWMutex
-	OnSendNotification     func(typ monitorAPI.AgentNotification, text string) error
+	OnSendNotification     func(typ monitorAPI.AgentNotifyMessage) error
 	OnGetCIDRPrefixLengths func() ([]int, []int)
 
 	// Metrics
@@ -254,9 +254,9 @@ func (ds *DaemonSuite) GetCompilationLock() *lock.RWMutex {
 	panic("GetCompilationLock should not have been called")
 }
 
-func (ds *DaemonSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (ds *DaemonSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	if ds.OnSendNotification != nil {
-		return ds.OnSendNotification(typ, text)
+		return ds.OnSendNotification(msg)
 	}
 	panic("SendNotification should not have been called")
 }
diff --git a/images/cilium-test/Dockerfile b/images/cilium-test/Dockerfile
new file mode 100644
index 0000000000..bede335d17
--- /dev/null
+++ b/images/cilium-test/Dockerfile
@@ -0,0 +1,47 @@
+# syntax=docker/dockerfile:1.1-experimental
+
+# Copyright 2020 Authors of Cilium
+# SPDX-License-Identifier: Apache-2.0
+
+ARG CILIUM_BUILDER_IMAGE=docker.io/cilium/cilium-builder-dev:8241b166e9576cb45990442509f2128ccbba1978
+ARG UBUNTU_IMAGE=docker.io/library/ubuntu:20.04@sha256:8bce67040cd0ae39e0beb55bcb976a824d9966d2ac8d2e4bf6119b45505cee64
+
+FROM ${UBUNTU_IMAGE} as rootfs
+ARG TARGETPLATFORM
+
+COPY images/cilium-test/install-deps.sh /tmp/install-deps.sh
+RUN /tmp/install-deps.sh
+
+COPY images/cilium-test/install-helm.sh /tmp/install-helm.sh
+RUN /tmp/install-helm.sh "${TARGETPLATFORM}"
+
+FROM --platform=linux/amd64 ${CILIUM_BUILDER_IMAGE} as builder
+
+RUN mkdir -p /out/linux/amd64/usr/local/bin /out/linux/arm64/usr/local/bin
+
+WORKDIR /go/src/github.com/cilium/cilium/images/cilium-test
+
+RUN --mount=type=bind,readwrite,target=/go/src/github.com/cilium/cilium --mount=target=/root/.cache,type=cache --mount=target=/go/pkg/mod,type=cache \
+  go build -o /out/linux/amd64/usr/local/bin/ginkgo github.com/onsi/ginkgo/ginkgo
+
+RUN --mount=type=bind,readwrite,target=/go/src/github.com/cilium/cilium --mount=target=/root/.cache,type=cache --mount=target=/go/pkg/mod,type=cache \
+  env GOARCH=arm64 CC=aarch64-linux-gnu-gcc \
+    go build -o /out/linux/arm64/usr/local/bin/ginkgo github.com/onsi/ginkgo/ginkgo
+
+WORKDIR /go/src/github.com/cilium/cilium/test
+
+RUN --mount=type=bind,readwrite,target=/go/src/github.com/cilium/cilium --mount=target=/root/.cache,type=cache --mount=target=/go/pkg/mod,type=cache \
+  /out/linux/amd64/usr/local/bin/ginkgo build ./ && mv test.test /out/linux/amd64/usr/local/bin/cilium-test
+
+RUN --mount=type=bind,readwrite,target=/go/src/github.com/cilium/cilium --mount=target=/root/.cache,type=cache --mount=target=/go/pkg/mod,type=cache \
+  env GOARCH=arm64 CC=aarch64-linux-gnu-gcc \
+    /out/linux/amd64/usr/local/bin/ginkgo build ./ && mv test.test /out/linux/arm64/usr/local/bin/cilium-test
+
+FROM scratch
+ARG TARGETPLATFORM
+LABEL maintainer="maintainer@cilium.io"
+COPY --from=rootfs / /
+COPY --from=builder /out/${TARGETPLATFORM} /
+COPY test /usr/local/src/cilium/test
+COPY install/kubernetes /usr/local/src/cilium/install/kubernetes
+COPY images/cilium-test/cilium-test-gke.sh /usr/local/bin/cilium-test-gke.sh
diff --git a/images/cilium-test/Dockerfile.dockerignore b/images/cilium-test/Dockerfile.dockerignore
new file mode 100644
index 0000000000..f6ea417ea4
--- /dev/null
+++ b/images/cilium-test/Dockerfile.dockerignore
@@ -0,0 +1,59 @@
+# Copyright 2020 Authors of Cilium
+# SPDX-License-Identifier: Apache-2.0
+
+# since this is image is built with root context, most of the files
+# need to be excluded for faster builds and to avoid spoiling build
+# cache due to unchecked files (like configs or random binaries)
+*
+
+# must-have toplevel files
+!/Makefile*
+!/go.sum
+!/go.mod
+!/VERSION
+# TODO: this is currently a make dependency, but should be removed
+# in the future
+!/Dockerfile
+
+# directories
+!/.git
+!/api
+!/bpf
+!/bugtool
+!/cilium
+!/cilium-health
+!/contrib/packaging/docker
+!/daemon
+!/envoy
+!/pkg
+!/plugins/cilium-cni
+!/proxylib
+!/vendor
+
+!/test
+!/install/kubernetes
+!/images/cilium-test
+/images/cilium-test/Dockerfile
+
+tests/cilium-files
+test/test_results*
+test/.vagrant
+test/tmp.yaml
+test/*_manifest.yaml
+test/*.xml
+test/*.json
+test/*.log
+test/bpf/_results
+test/cilium-[0-9a-f]*.yaml
+test/*tmp
+test/cilium-istioctl
+
+test/k8sT/manifests/cnp-second-namespaces.yaml
+test/cilium.conf.ginkgo
+
+test/gke/cluster-name
+test/gke/cluster-uri
+test/gke/cluster-version
+test/gke/gke-kubeconfig
+test/gke/resize-kubeconfig
+test/gke/registry-adder.yaml
diff --git a/images/cilium-test/cilium-test-gke.sh b/images/cilium-test/cilium-test-gke.sh
new file mode 100755
index 0000000000..bf114fc5fa
--- /dev/null
+++ b/images/cilium-test/cilium-test-gke.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+# Copyright 2017-2020 Authors of Cilium
+# SPDX-License-Identifier: Apache-2.0
+
+set -o xtrace
+set -o errexit
+set -o pipefail
+set -o nounset
+
+nodes() {
+  kubectl get nodes --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}'
+}
+
+until [ "$(nodes | wc -l)" -eq  2 ] ; do sleep 5 ; done
+
+index=1
+for node in $(nodes) ; do
+  kubectl label --overwrite node "${node}" "cilium.io/ci-node=k8s${index}"
+  index=$((index+1))
+done
+
+kubectl create ns cilium || true
+
+CILIUM_IMAGE="${1}"
+CILIUM_OPERATOR_IMAGE="${2}"
+
+export CILIUM_IMAGE CILIUM_OPERATOR_IMAGE
+
+shift 2
+
+CNI_INTEGRATION=gke
+
+K8S_VERSION="$(kubectl version -o json |  jq -r '(.serverVersion.major + "." + (.serverVersion.minor | scan("[0-9]+")))' | sed 's/"//g')"
+
+export CNI_INTEGRATION K8S_VERSION
+
+cd /usr/local/src/cilium/test
+
+cilium-test \
+  -test.v \
+  -ginkgo.v \
+  -ginkgo.noColor \
+  -ginkgo.focus="K8s*" \
+  -cilium.provision=false \
+  -cilium.kubeconfig="${KUBECONFIG}" \
+  -cilium.image="${CILIUM_IMAGE}" \
+  -cilium.operator-image="${CILIUM_OPERATOR_IMAGE}" \
+  -cilium.passCLIEnvironment=true
diff --git a/images/cilium-test/go.mod b/images/cilium-test/go.mod
new file mode 100644
index 0000000000..de1273497a
--- /dev/null
+++ b/images/cilium-test/go.mod
@@ -0,0 +1,5 @@
+module github.com/cilium/cilium/images/cilium-test
+
+go 1.15
+
+require github.com/onsi/ginkgo v1.14.0 // indirect
diff --git a/images/cilium-test/go.sum b/images/cilium-test/go.sum
new file mode 100644
index 0000000000..7658413437
--- /dev/null
+++ b/images/cilium-test/go.sum
@@ -0,0 +1,50 @@
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/images/cilium-test/install-deps.sh b/images/cilium-test/install-deps.sh
new file mode 100755
index 0000000000..b5c49cc7bf
--- /dev/null
+++ b/images/cilium-test/install-deps.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Copyright 2017-2020 Authors of Cilium
+# SPDX-License-Identifier: Apache-2.0
+
+set -o xtrace
+set -o errexit
+set -o pipefail
+set -o nounset
+
+ubuntu_packages=(
+  apt-transport-https
+  gawk
+  ca-certificates
+  curl
+  gnupg
+  grep
+  jq
+  sed
+)
+
+google_packages=(
+  google-cloud-sdk
+  kubectl
+)
+
+export DEBIAN_FRONTEND=noninteractive
+
+apt-get update
+
+ln -fs /usr/share/zoneinfo/UTC /etc/localtime
+
+apt-get install -y --no-install-recommends "${ubuntu_packages[@]}"
+
+echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
+curl --fail --show-error --silent https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
+
+apt-get update
+
+apt-get install -y --no-install-recommends "${google_packages[@]}"
diff --git a/images/cilium-test/install-helm.sh b/images/cilium-test/install-helm.sh
new file mode 100755
index 0000000000..c9cfa89f39
--- /dev/null
+++ b/images/cilium-test/install-helm.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Copyright 2017-2020 Authors of Cilium
+# SPDX-License-Identifier: Apache-2.0
+
+set -o xtrace
+set -o errexit
+set -o pipefail
+set -o nounset
+
+HELM_VERSION=3.3.0
+
+TARGETPLATFORM="${1}"
+
+target="$(echo "${TARGETPLATFORM}" | tr / -)"
+
+curl --fail --show-error --silent --location \
+     "https://get.helm.sh/helm-v${HELM_VERSION}-${target}.tar.gz" \
+   --output /tmp/helm.tgz
+
+tar -xf /tmp/helm.tgz -C /tmp
+
+ls -laR /tmp
+
+mv "/tmp/${target}/helm" /usr/local/bin
+
+rm -rf /tmp/helm.tgz "/tmp/${target}"
diff --git a/pkg/endpoint/endpoint_test.go b/pkg/endpoint/endpoint_test.go
index 49507c9586..d39275196b 100644
--- a/pkg/endpoint/endpoint_test.go
+++ b/pkg/endpoint/endpoint_test.go
@@ -68,7 +68,7 @@ type EndpointSuite struct {
 	OnQueueEndpointBuild      func(ctx context.Context, epID uint64) (func(), error)
 	OnRemoveFromEndpointQueue func(epID uint64)
 	OnGetCompilationLock      func() *lock.RWMutex
-	OnSendNotification        func(typ monitorAPI.AgentNotification, text string) error
+	OnSendNotification        func(msg monitorAPI.AgentNotifyMessage) error
 
 	// Metrics
 	collectors []prometheus.Collector
@@ -110,7 +110,7 @@ func (s *EndpointSuite) GetCompilationLock() *lock.RWMutex {
 	return nil
 }
 
-func (s *EndpointSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *EndpointSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/endpoint/redirect_test.go b/pkg/endpoint/redirect_test.go
index 1a7669a8af..7197287688 100644
--- a/pkg/endpoint/redirect_test.go
+++ b/pkg/endpoint/redirect_test.go
@@ -114,7 +114,7 @@ func (d *DummyOwner) GetCIDRPrefixLengths() (s6, s4 []int) {
 }
 
 // SendNotification does nothing.
-func (d *DummyOwner) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (d *DummyOwner) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/endpointmanager/manager_test.go b/pkg/endpointmanager/manager_test.go
index 566d9bfc86..9a78b0f5e0 100644
--- a/pkg/endpointmanager/manager_test.go
+++ b/pkg/endpointmanager/manager_test.go
@@ -83,7 +83,7 @@ func (s *EndpointManagerSuite) GetCIDRPrefixLengths() (s6, s4 []int) {
 	return nil, nil
 }
 
-func (s *EndpointManagerSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *EndpointManagerSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/envoy/envoy_test.go b/pkg/envoy/envoy_test.go
index 83772aa735..929f37f189 100644
--- a/pkg/envoy/envoy_test.go
+++ b/pkg/envoy/envoy_test.go
@@ -30,6 +30,7 @@ import (
 	"github.com/cilium/cilium/pkg/envoy/xds"
 	"github.com/cilium/cilium/pkg/flowdebug"
 	"github.com/cilium/cilium/pkg/identity"
+	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/proxy/accesslog"
@@ -77,6 +78,7 @@ func (s *EnvoySuite) TestEnvoy(c *C) {
 		c.Skip("skipping envoy unit test; CILIUM_ENABLE_ENVOY_UNIT_TEST not set")
 	}
 
+	logging.ConfigureLogLevel(true) // Use 'true' for debugging
 	flowdebug.Enable()
 
 	stateLogDir, err := ioutil.TempDir("", "envoy_go_test")
@@ -89,10 +91,18 @@ func (s *EnvoySuite) TestEnvoy(c *C) {
 	StartAccessLogServer(stateLogDir, xdsServer, &dummyEndpointInfoRegistry{})
 
 	// launch debug variant of the Envoy proxy
-	envoyProxy := StartEnvoy(stateLogDir, filepath.Join(stateLogDir, "cilium-envoy.log"), 42)
+	envoyProxy := StartEnvoy(stateLogDir, filepath.Join(stateLogDir, "cilium-envoy.log"), 0)
 	c.Assert(envoyProxy, NotNil)
 	log.Debug("started Envoy")
 
+	log.Debug("adding metrics listener")
+	xdsServer.AddMetricsListener(9095, s.waitGroup)
+
+	err = s.waitForProxyCompletion()
+	c.Assert(err, IsNil)
+	log.Debug("completed adding metrics listener")
+	s.waitGroup = completion.NewWaitGroup(ctx)
+
 	log.Debug("adding listener1")
 	xdsServer.AddListener("listener1", policy.ParserTypeHTTP, 8081, true, false, s.waitGroup)
 
@@ -172,7 +182,7 @@ func (s *EnvoySuite) TestEnvoyNACK(c *C) {
 
 	err = s.waitForProxyCompletion()
 	c.Assert(err, Not(IsNil))
-	c.Assert(err, checker.DeepEquals, &xds.ProxyError{Err: xds.ErrNackReceived, Detail: "Error adding/updating listener(s) listener:22: cannot bind '[::]:22': Address already in use"})
+	c.Assert(err, checker.DeepEquals, &xds.ProxyError{Err: xds.ErrNackReceived, Detail: "Error adding/updating listener(s) listener:22: cannot bind '[::]:22': Address already in use\n"})
 
 	s.waitGroup = completion.NewWaitGroup(ctx)
 	// Remove listener1
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..e23f2b7854 100644
--- a/pkg/fqdn/dnsproxy/proxy_test.go
+++ b/pkg/fqdn/dnsproxy/proxy_test.go
@@ -90,7 +90,7 @@ func (s *DNSProxyTestSuite) GetCIDRPrefixLengths() (s6, s4 []int) {
 	return nil, nil
 }
 
-func (s *DNSProxyTestSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *DNSProxyTestSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
@@ -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/monitor/api/types_test.go b/pkg/monitor/api/types_test.go
index 72ba1dc713..457d1cd7c3 100644
--- a/pkg/monitor/api/types_test.go
+++ b/pkg/monitor/api/types_test.go
@@ -64,7 +64,7 @@ func testEqualityEndpoint(got, expected string, c *C) {
 	c.Assert(gotStruct, checker.DeepEquals, expectedStruct)
 }
 
-func (s *MonitorAPISuite) TestRulesRepr(c *C) {
+func (s *MonitorAPISuite) TestPolicyUpdateMessage(c *C) {
 	rules := api.Rules{
 		&api.Rule{
 			Labels: labels.LabelArray{
@@ -82,27 +82,32 @@ func (s *MonitorAPISuite) TestRulesRepr(c *C) {
 	for _, r := range rules {
 		labels = append(labels, r.Labels.GetModel()...)
 	}
-	repr, err := PolicyUpdateRepr(len(rules), labels, 1)
 
+	msg := PolicyUpdateMessage(len(rules), labels, 1)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"labels":["unspec:key1=value1","unspec:key2=value2"],"revision":1,"rule_count":2}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyUpdated)
+	testEqualityRules(repr.Text, `{"labels":["unspec:key1=value1","unspec:key2=value2"],"revision":1,"rule_count":2}`, c)
 }
 
-func (s *MonitorAPISuite) TestRulesReprEmpty(c *C) {
-	repr, err := PolicyUpdateRepr(0, []string{}, 1)
-
+func (s *MonitorAPISuite) TestEmptyPolicyUpdateMessage(c *C) {
+	msg := PolicyUpdateMessage(0, []string{}, 1)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"revision":1,"rule_count":0}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyUpdated)
+	testEqualityRules(repr.Text, `{"revision":1,"rule_count":0}`, c)
 }
 
-func (s *MonitorAPISuite) TestPolicyDeleteRepr(c *C) {
+func (s *MonitorAPISuite) TestPolicyDeleteMessage(c *C) {
 	lab := labels.LabelArray{
 		labels.NewLabel("key1", "value1", labels.LabelSourceUnspec),
 	}
 
-	repr, err := PolicyDeleteRepr(1, lab.GetModel(), 2)
+	msg := PolicyDeleteMessage(1, lab.GetModel(), 2)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"labels":["unspec:key1=value1"],"revision":2,"rule_count":1}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyDeleted)
+	testEqualityRules(repr.Text, `{"labels":["unspec:key1=value1"],"revision":2,"rule_count":1}`, c)
 }
 
 type RegenError struct{}
@@ -131,24 +136,29 @@ func (MockEndpoint) GetK8sNamespace() string {
 	return ""
 }
 
-func (s *MonitorAPISuite) TestEndpointRegenRepr(c *C) {
+func (s *MonitorAPISuite) TestEndpointRegenMessage(c *C) {
 	e := MockEndpoint{}
 	rerr := RegenError{}
 
-	repr, err := EndpointRegenRepr(e, rerr)
+	msg := EndpointRegenMessage(e, rerr)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityEndpoint(repr, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"],"error":"RegenError"}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyEndpointRegenerateFail)
+	testEqualityEndpoint(repr.Text, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"],"error":"RegenError"}`, c)
 
-	repr, err = EndpointRegenRepr(e, nil)
+	msg = EndpointRegenMessage(e, nil)
+	repr, err = msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityEndpoint(repr, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"]}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyEndpointRegenerateSuccess)
+	testEqualityEndpoint(repr.Text, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"]}`, c)
 }
 
-func (s *MonitorAPISuite) TestTimeRepr(c *C) {
+func (s *MonitorAPISuite) TestStartMessage(c *C) {
 	t := time.Now()
 
-	repr, err := TimeRepr(t)
-
+	msg := StartMessage(t)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	c.Assert(repr, Equals, fmt.Sprintf(`{"time":"%s"}`, t.String()))
+	c.Assert(repr.Type, Equals, AgentNotifyStart)
+	c.Assert(repr.Text, Equals, fmt.Sprintf(`{"time":"%s"}`, t.String()))
 }
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)