Repository: cilium/cilium
Pull request #12941 (2020-08-20 14:58:04)

images/cilium-test: New test suite image
This is to be used with GKE test cluster operator that will be
introduce in the due course. The operator will be in charge of
running the tests, hence there needs to be an image with all
test binaries and assets included.


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..f21e8032df 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -59,6 +59,7 @@ cilium-agent [flags]
       --disable-iptables-feeder-rules strings         Chains to ignore when installing feeder rules.
       --egress-masquerade-interfaces string           Limit egress masquerading to interface selector
       --enable-auto-protect-node-port-range           Append NodePort range to net.ipv4.ip_local_reserved_ports if it overlaps with ephemeral port range (net.ipv4.ip_local_port_range) (default true)
+      --enable-bandwidth-manager                      Enable BPF bandwidth manager
       --enable-bpf-clock-probe                        Enable BPF clock source probing for more efficient tick retrieval
       --enable-bpf-masquerade                         Masquerade packets from endpoints leaving the host with BPF instead of iptables
       --enable-endpoint-health-checking               Enable connectivity health checking between virtual endpoints (default true)
diff --git a/Documentation/cmdref/cilium_bpf.md b/Documentation/cmdref/cilium_bpf.md
index 38e4c247e1..94e8d45b2d 100644
--- a/Documentation/cmdref/cilium_bpf.md
+++ b/Documentation/cmdref/cilium_bpf.md
@@ -25,6 +25,7 @@ Direct access to local BPF maps
 ### SEE ALSO
 
 * [cilium](../cilium)	 - CLI
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
 * [cilium bpf ct](../cilium_bpf_ct)	 - Connection tracking tables
 * [cilium bpf endpoint](../cilium_bpf_endpoint)	 - Local endpoint map
 * [cilium bpf fs](../cilium_bpf_fs)	 - BPF filesystem mount
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth.md b/Documentation/cmdref/cilium_bpf_bandwidth.md
new file mode 100644
index 0000000000..ad1780d98f
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth.md
@@ -0,0 +1,29 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth
+
+BPF datapath bandwidth settings
+
+### Synopsis
+
+BPF datapath bandwidth settings
+
+### Options
+
+```
+  -h, --help   help for bandwidth
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf](../cilium_bpf)	 - Direct access to local BPF maps
+* [cilium bpf bandwidth list](../cilium_bpf_bandwidth_list)	 - List BPF datapath bandwidth settings
+
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth_list.md b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
new file mode 100644
index 0000000000..4c91646689
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
@@ -0,0 +1,33 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth list
+
+List BPF datapath bandwidth settings
+
+### Synopsis
+
+List BPF datapath bandwidth settings
+
+```
+cilium bpf bandwidth list [flags]
+```
+
+### Options
+
+```
+  -h, --help            help for list
+  -o, --output string   json| jsonpath='{}'
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
+
diff --git a/Documentation/gettingstarted/bandwidth-manager.rst b/Documentation/gettingstarted/bandwidth-manager.rst
new file mode 100644
index 0000000000..dcd7e5bf13
--- /dev/null
+++ b/Documentation/gettingstarted/bandwidth-manager.rst
@@ -0,0 +1,144 @@
+.. only:: not (epub or latex or html)
+
+    WARNING: You are looking at unreleased Cilium documentation.
+    Please use the official rendered version released here:
+    https://docs.cilium.io
+
+.. _bandwidth-manager:
+
+************************
+Bandwidth Manager (beta)
+************************
+
+This guide explains how to configure Cilium's bandwidth manager to
+optimize TCP and UDP workloads and efficiently rate limit individual Pods
+if needed through the help of EDT (Earliest Departure Time) and eBPF.
+
+The bandwidth manager does not rely on CNI chaining and is natively integrated
+into Cilium instead. Hence, it does not make use of the `bandwidth CNI
+<https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping>`_
+plugin. Due to scalability concerns in particular for multi-queue network
+interfaces, it is not recommended to use the bandwidth CNI plugin which is
+based on TBF (Token Bucket Filter) instead of EDT.
+
+Cilium's bandwidth manager supports the ``kubernetes.io/egress-bandwidth`` Pod
+annotation which is enforced on egress at the native host networking devices.
+The bandwidth enforcement is supported for direct routing as well as tunneling
+mode in Cilium.
+
+The ``kubernetes.io/ingress-bandwidth`` annotation is not supported and also not
+recommended to use. Limiting bandwidth happens natively at the egress point of
+networking devices in order to reduce or pace bandwidth usage on the wire.
+Enforcing at ingress would add yet another layer of buffer queueing right in the
+critical fast-path of a node via ``ifb`` device where ingress traffic first needs
+to be redirected to the ``ifb``'s egress point in order to perform shaping before
+traffic can go up the stack. At this point traffic has already occupied the
+bandwidth usage on the wire, and the node has already spent resources on
+processing the packet. ``kubernetes.io/ingress-bandwidth`` annotation is ignored
+by Cilium's bandwidth manager.
+
+.. note::
+
+   Bandwidth Manager requires a v5.0.x or more recent Linux kernel.
+
+.. include:: k8s-install-download-release.rst
+
+The Cilium bandwidth manager is enabled by default for new deployments via Helm:
+
+.. parsed-literal::
+
+   helm install cilium |CHART_RELEASE| \\
+     --namespace kube-system
+
+The option for Helm is controllable through ``global.bandwidthManager`` with a
+possible setting of ``true`` (default) and ``false``.
+
+The native host networking devices are auto detected as native devices which have
+the default route on the host or have Kubernetes InternalIP or ExternalIP assigned.
+InternalIP is preferred over ExternalIP if both exist. To change and manually specify
+the devices, set their names in the ``global.devices`` helm option, e.g.
+``global.devices='{eth0,eth1,eth2}'``. Each listed device has to be named the same
+on all Cilium managed nodes.
+
+Verify that the Cilium Pod has come up correctly:
+
+.. code:: bash
+
+    kubectl -n kube-system get pods -l k8s-app=cilium
+    NAME                READY     STATUS    RESTARTS   AGE
+    cilium-crf7f        1/1       Running   0          10m
+
+In order to verify whether the bandwidth manager feature has been enabled in Cilium,
+the ``cilium status`` CLI command provides visibility through the ``BandwidthManager``
+info line. It also dumps a list of devices on which the egress bandwidth limitation
+is enforced:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep BandwidthManager
+    BandwidthManager:       EDT with BPF   [eth0]
+
+Assuming we have a multi-node cluster, in the next step, we deploy a netperf Pod on
+the node named foobar. The following example deployment yaml limits the egress
+bandwidth of the netperf Pod on the node's physical device:
+
+.. parsed-literal::
+
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: netperf
+    spec:
+      selector:
+        matchLabels:
+          run: netperf
+      replicas: 1
+      template:
+        metadata:
+          labels:
+            run: netperf
+          annotations:
+            kubernetes.io/egress-bandwidth: "10M"
+        spec:
+          nodeName: foobar
+          containers:
+          - name: netperf
+            image: cilium/netperf
+            ports:
+            - containerPort: 12865
+
+Once up and running, ``netperf`` client can be invoked from a different node in the
+cluster using the Pod's IP (in this case ``10.217.0.254``) directly. The test streaming
+direction is from the netperf deployment towards the client, hence ``TCP_MAERTS``:
+
+.. parsed-literal::
+
+  netperf -t TCP_MAERTS -H 10.217.0.254
+  MIGRATED TCP MAERTS TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 10.217.0.254 () port 0 AF_INET
+  Recv   Send    Send
+  Socket Socket  Message  Elapsed
+  Size   Size    Size     Time     Throughput
+  bytes  bytes   bytes    secs.    10^6bits/sec
+
+   87380  16384  16384    10.00       9.56
+
+As can be seen, egress traffic of the netperf Pod has been limited to 10Mbit per second.
+
+In order to introspect current endpoint bandwidth settings from BPF side, the following
+command can be run:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium bpf bandwidth list
+    IDENTITY   EGRESS BANDWIDTH (BitsPerSec)
+    491        10M
+
+Each Pod is represented in Cilium as an :ref:`endpoint` which has an identity. The above
+identity can then be correlated with the ``cilium endpoint list`` command.
+
+Limitations
+###########
+
+    * Bandwidth enforcement currently does not work in combination with L7 Cilium Network Policies.
+      In case they select the Pod at egress, then the bandwidth enforcement will be disabled for
+      those Pods.
diff --git a/Documentation/gettingstarted/index.rst b/Documentation/gettingstarted/index.rst
index 1efe35f23e..ede8a1a8a4 100644
--- a/Documentation/gettingstarted/index.rst
+++ b/Documentation/gettingstarted/index.rst
@@ -63,6 +63,7 @@ Advanced Networking
    encryption
    host-services
    kubeproxy-free
+   bandwidth-manager
    kata
    ipam
 
diff --git a/Documentation/spelling_wordlist.txt b/Documentation/spelling_wordlist.txt
index 55aceb4ec3..b3c2cb2b82 100644
--- a/Documentation/spelling_wordlist.txt
+++ b/Documentation/spelling_wordlist.txt
@@ -92,6 +92,7 @@ Lund
 Lx
 Majkowski
 Marek
+Mbit
 Meetup
 Mellanox
 Memcached
@@ -504,6 +505,7 @@ netdevice
 netdevices
 netdevsim
 netfilter
+netperf
 netsec
 netvsc
 newproto
diff --git a/api/v1/models/bandwidth_manager.go b/api/v1/models/bandwidth_manager.go
new file mode 100644
index 0000000000..9e97e198a9
--- /dev/null
+++ b/api/v1/models/bandwidth_manager.go
@@ -0,0 +1,51 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// Copyright 2017-2020 Authors of Cilium
+// SPDX-License-Identifier: Apache-2.0
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+)
+
+// BandwidthManager Status of bandwidth manager
+//
+// +k8s:deepcopy-gen=true
+//
+// swagger:model BandwidthManager
+type BandwidthManager struct {
+
+	// devices
+	Devices []string `json:"devices"`
+
+	// Is bandwidth manager enabled
+	Enabled bool `json:"enabled,omitempty"`
+}
+
+// Validate validates this bandwidth manager
+func (m *BandwidthManager) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *BandwidthManager) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *BandwidthManager) UnmarshalBinary(b []byte) error {
+	var res BandwidthManager
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/api/v1/models/status_response.go b/api/v1/models/status_response.go
index 3427f45523..6688e2379f 100644
--- a/api/v1/models/status_response.go
+++ b/api/v1/models/status_response.go
@@ -22,6 +22,9 @@ import (
 // swagger:model StatusResponse
 type StatusResponse struct {
 
+	// Status of bandwidth manager
+	BandwidthManager *BandwidthManager `json:"bandwidth-manager,omitempty"`
+
 	// Status of BPF maps
 	BpfMaps *BPFMapStatus `json:"bpf-maps,omitempty"`
 
@@ -78,6 +81,10 @@ type StatusResponse struct {
 func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	var res []error
 
+	if err := m.validateBandwidthManager(formats); err != nil {
+		res = append(res, err)
+	}
+
 	if err := m.validateBpfMaps(formats); err != nil {
 		res = append(res, err)
 	}
@@ -144,6 +151,24 @@ func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
+func (m *StatusResponse) validateBandwidthManager(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.BandwidthManager) { // not required
+		return nil
+	}
+
+	if m.BandwidthManager != nil {
+		if err := m.BandwidthManager.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("bandwidth-manager")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (m *StatusResponse) validateBpfMaps(formats strfmt.Registry) error {
 
 	if swag.IsZero(m.BpfMaps) { // not required
diff --git a/api/v1/models/zz_generated.deepcopy.go b/api/v1/models/zz_generated.deepcopy.go
index 08ba3e693f..12a8edcc90 100644
--- a/api/v1/models/zz_generated.deepcopy.go
+++ b/api/v1/models/zz_generated.deepcopy.go
@@ -49,6 +49,27 @@ func (in *BPFMapStatus) DeepCopy() *BPFMapStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BandwidthManager) DeepCopyInto(out *BandwidthManager) {
+	*out = *in
+	if in.Devices != nil {
+		in, out := &in.Devices, &out.Devices
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BandwidthManager.
+func (in *BandwidthManager) DeepCopy() *BandwidthManager {
+	if in == nil {
+		return nil
+	}
+	out := new(BandwidthManager)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CIDRPolicy) DeepCopyInto(out *CIDRPolicy) {
 	*out = *in
@@ -732,6 +753,11 @@ func (in *RequestResponseStatistics) DeepCopy() *RequestResponseStatistics {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StatusResponse) DeepCopyInto(out *StatusResponse) {
 	*out = *in
+	if in.BandwidthManager != nil {
+		in, out := &in.BandwidthManager, &out.BandwidthManager
+		*out = new(BandwidthManager)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.BpfMaps != nil {
 		in, out := &in.BpfMaps, &out.BpfMaps
 		*out = new(BPFMapStatus)
diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml
index c32a1e2a27..92fbaa8dca 100644
--- a/api/v1/openapi.yaml
+++ b/api/v1/openapi.yaml
@@ -1597,6 +1597,9 @@ definitions:
       proxy:
         description: Status of proxy
         "$ref": "#/definitions/ProxyStatus"
+      bandwidth-manager:
+        description: Status of bandwidth manager
+        "$ref": "#/definitions/BandwidthManager"
       masquerading:
         description: Status of masquerading
         "$ref": "#/definitions/Masquerading"
@@ -2380,6 +2383,20 @@ definitions:
       forwarded:
         description: Number of messages forwarded
         type: integer
+  BandwidthManager:
+    description: |-
+      Status of bandwidth manager
+
+      +k8s:deepcopy-gen=true
+    type: object
+    properties:
+      enabled:
+        description: Is bandwidth manager enabled
+        type: boolean
+      devices:
+        type: array
+        items:
+          type: string
   Masquerading:
     description: |-
       Status of masquerading
diff --git a/api/v1/server/embedded_spec.go b/api/v1/server/embedded_spec.go
index e52d56536d..a3ec578731 100644
--- a/api/v1/server/embedded_spec.go
+++ b/api/v1/server/embedded_spec.go
@@ -1432,6 +1432,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -3286,6 +3302,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
@@ -5105,6 +5125,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -7240,6 +7276,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
diff --git a/bpf/Makefile b/bpf/Makefile
index a0c355d764..d3ca769e43 100644
--- a/bpf/Makefile
+++ b/bpf/Makefile
@@ -56,6 +56,8 @@ LB_OPTIONS = \
 	-DENABLE_IPV4:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_SRC_RANGE_CHECK \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER:-DENABLE_SRC_RANGE_CHECK
 
 # These options are intended to max out the BPF program complexity. it is load
 # tested as well.
diff --git a/bpf/bpf_alignchecker.c b/bpf/bpf_alignchecker.c
index dcbcb89dbe..95c70c81d9 100644
--- a/bpf/bpf_alignchecker.c
+++ b/bpf/bpf_alignchecker.c
@@ -81,6 +81,8 @@ int main(void)
 	DECLARE(struct, lb_affinity_match, iter);
 	DECLARE(struct, lb4_src_range_key, iter);
 	DECLARE(struct, lb6_src_range_key, iter);
+	DECLARE(struct, edt_id, iter);
+	DECLARE(struct, edt_info, iter);
 
 	return 0;
 }
diff --git a/bpf/bpf_host.c b/bpf/bpf_host.c
index aa30cbe003..dab48a5455 100644
--- a/bpf/bpf_host.c
+++ b/bpf/bpf_host.c
@@ -26,6 +26,7 @@
 #endif
 
 #include "lib/common.h"
+#include "lib/edt.h"
 #include "lib/arp.h"
 #include "lib/maps.h"
 #include "lib/ipv6.h"
@@ -883,6 +884,10 @@ int from_netdev(struct __ctx_buff *ctx)
 __section("from-host")
 int from_host(struct __ctx_buff *ctx)
 {
+	/* Traffic from the host ns going through cilium_host device must
+	 * not be subject to EDT rate-limiting.
+	 */
+	edt_set_aggregate(ctx, 0);
 	return handle_netdev(ctx, true);
 }
 
@@ -927,14 +932,22 @@ int to_netdev(struct __ctx_buff *ctx __maybe_unused)
 		ret = DROP_UNKNOWN_L3;
 		break;
 	}
-
-
 out:
 	if (IS_ERR(ret))
 		return send_drop_notify_error(ctx, src_id, ret, CTX_ACT_DROP,
 					      METRIC_EGRESS);
 #endif /* ENABLE_HOST_FIREWALL */
 
+#if defined(ENABLE_BANDWIDTH_MANAGER)
+	ret = edt_sched_departure(ctx);
+	/* No send_drop_notify_error() here given we're rate-limiting. */
+	if (ret == CTX_ACT_DROP) {
+		update_metrics(ctx_full_len(ctx), METRIC_EGRESS,
+			       -DROP_EDT_HORIZON);
+		return ret;
+	}
+#endif
+
 #if defined(ENABLE_NODEPORT) && \
 	(!defined(ENABLE_DSR) || \
 	 (defined(ENABLE_DSR) && defined(ENABLE_DSR_HYBRID)) || \
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 332d51e224..1f044f9e86 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -16,6 +16,7 @@
 #include "lib/config.h"
 #include "lib/maps.h"
 #include "lib/arp.h"
+#include "lib/edt.h"
 #include "lib/ipv6.h"
 #include "lib/ipv4.h"
 #include "lib/icmp6.h"
@@ -801,6 +802,7 @@ int handle_xgress(struct __ctx_buff *ctx)
 	int ret;
 
 	bpf_clear_meta(ctx);
+	edt_set_aggregate(ctx, LXC_ID);
 
 	send_trace_notify(ctx, TRACE_FROM_LXC, SECLABEL, 0, 0, 0, 0,
 			  TRACE_PAYLOAD_LEN);
diff --git a/bpf/include/bpf/ctx/skb.h b/bpf/include/bpf/ctx/skb.h
index c821347d45..204b15295a 100644
--- a/bpf/include/bpf/ctx/skb.h
+++ b/bpf/include/bpf/ctx/skb.h
@@ -68,6 +68,12 @@ ctx_full_len(const struct __sk_buff *ctx)
 	return ctx->len;
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct __sk_buff *ctx)
+{
+	return ctx->wire_len;
+}
+
 static __always_inline __maybe_unused void
 ctx_store_meta(struct __sk_buff *ctx, const __u32 off, __u32 data)
 {
diff --git a/bpf/include/bpf/ctx/xdp.h b/bpf/include/bpf/ctx/xdp.h
index 48cd35d0d6..df8c300db4 100644
--- a/bpf/include/bpf/ctx/xdp.h
+++ b/bpf/include/bpf/ctx/xdp.h
@@ -251,6 +251,12 @@ ctx_full_len(const struct xdp_md *ctx)
 	return ctx_data_end(ctx) - ctx_data(ctx);
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct xdp_md *ctx)
+{
+	return ctx_full_len(ctx);
+}
+
 struct bpf_elf_map __section_maps cilium_xdp_scratch = {
 	.type		= BPF_MAP_TYPE_PERCPU_ARRAY,
 	.size_key	= sizeof(int),
diff --git a/bpf/include/linux/bpf.h b/bpf/include/linux/bpf.h
index f13e3e6f21..bc5e109b57 100644
--- a/bpf/include/linux/bpf.h
+++ b/bpf/include/linux/bpf.h
@@ -3590,19 +3590,12 @@ struct __sk_buff {
 	/* ... here. */
 
 	__u32 data_meta;
-
-#if 0
-	/* TODO: compiled out for now since this cases verifier breakage
-	 * on older kernels resulting in invalid bpf_context access. Needs
-	 * follow-up investigation.
-	 */
 	__bpf_md_ptr(struct bpf_flow_keys *, flow_keys);
 	__u64 tstamp;
 	__u32 wire_len;
 	__u32 gso_segs;
 	__bpf_md_ptr(struct bpf_sock *, sk);
 	__u32 gso_size;
-#endif
 };
 
 struct bpf_tunnel_key {
@@ -3927,7 +3920,12 @@ struct bpf_sock_addr {
 	__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.
 				 * Stored in network byte order.
 				 */
-#if 0 /* TODO, see comment in __sk_buff */
+
+	/* TODO: compiled out for now since this cases verifier breakage
+	 * on older kernels resulting in invalid bpf_context access. Needs
+	 * follow-up investigation.
+	 */
+#if 0
 	__bpf_md_ptr(struct bpf_sock *, sk);
 #endif
 };
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index fbbdc190bf..7e741b745b 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -208,6 +208,17 @@ struct endpoint_info {
 	__u32		pad[4];
 };
 
+struct edt_id {
+	__u64		id;
+};
+
+struct edt_info {
+	__u64		bps;
+	__u64		t_last;
+	__u64		t_horizon_drop;
+	__u64		pad[4];
+};
+
 struct remote_endpoint_info {
 	__u32		sec_label;
 	__u32		tunnel_endpoint;
@@ -347,11 +358,11 @@ enum {
 #define DROP_UNUSED8		-159 /* unused */
 #define DROP_NO_TUNNEL_ENDPOINT -160
 #define DROP_UNUSED9		-161 /* unused */
-#define DROP_UNUSED10		-162 /* unused */
-#define DROP_UNKNOWN_CT			-163
-#define DROP_HOST_UNREACHABLE		-164
+#define DROP_EDT_HORIZON	-162
+#define DROP_UNKNOWN_CT		-163
+#define DROP_HOST_UNREACHABLE	-164
 #define DROP_NO_CONFIG		-165
-#define DROP_UNSUPPORTED_L2		-166
+#define DROP_UNSUPPORTED_L2	-166
 #define DROP_NAT_NO_MAPPING	-167
 #define DROP_NAT_UNSUPP_PROTO	-168
 #define DROP_NO_FIB		-169
diff --git a/bpf/lib/edt.h b/bpf/lib/edt.h
new file mode 100644
index 0000000000..c00ebeda0b
--- /dev/null
+++ b/bpf/lib/edt.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2020 Authors of Cilium */
+
+#ifndef __EDT_H_
+#define __EDT_H_
+
+#include "common.h"
+#include "time.h"
+#include "maps.h"
+
+#ifdef ENABLE_BANDWIDTH_MANAGER
+static __always_inline void edt_set_aggregate(struct __ctx_buff *ctx,
+					      __u32 aggregate)
+{
+	/* 16 bit as current used aggregate, and preserved in host ns. */
+	ctx->queue_mapping = aggregate;
+}
+
+static __always_inline __u32 edt_get_aggregate(struct __ctx_buff *ctx)
+{
+	__u32 aggregate = ctx->queue_mapping;
+
+	/* We need to reset queue mapping here such that new mapping will
+	 * be performed based on skb hash. See netdev_pick_tx().
+	 */
+	ctx->queue_mapping = 0;
+
+	return aggregate;
+}
+
+static __always_inline int edt_sched_departure(struct __ctx_buff *ctx)
+{
+	__u64 delay, now, t, t_next;
+	struct edt_id aggregate;
+	struct edt_info *info;
+	__u16 proto;
+
+	if (!validate_ethertype(ctx, &proto))
+		return CTX_ACT_OK;
+	if (proto != bpf_htons(ETH_P_IP) &&
+	    proto != bpf_htons(ETH_P_IPV6))
+		return CTX_ACT_OK;
+
+	aggregate.id = edt_get_aggregate(ctx);
+	if (!aggregate.id)
+		return CTX_ACT_OK;
+
+	info = map_lookup_elem(&THROTTLE_MAP, &aggregate);
+	if (!info)
+		return CTX_ACT_OK;
+
+	now = ktime_get_ns();
+	t = ctx->tstamp;
+	if (t < now)
+		t = now;
+	delay = ((__u64)ctx_wire_len(ctx)) * NSEC_PER_SEC / info->bps;
+	t_next = READ_ONCE(info->t_last) + delay;
+	if (t_next <= t) {
+		WRITE_ONCE(info->t_last, t);
+		return CTX_ACT_OK;
+	}
+	/* FQ implements a drop horizon, see also 39d010504e6b ("net_sched:
+	 * sch_fq: add horizon attribute"). However, we explicitly need the
+	 * drop horizon here to i) avoid having t_last messed up and ii) to
+	 * potentially allow for per aggregate control.
+	 */
+	if (t_next - now >= info->t_horizon_drop)
+		return CTX_ACT_DROP;
+	WRITE_ONCE(info->t_last, t_next);
+	ctx->tstamp = t_next;
+	return CTX_ACT_OK;
+}
+#else
+static __always_inline void
+edt_set_aggregate(struct __ctx_buff *ctx __maybe_unused,
+		  __u32 aggregate __maybe_unused)
+{
+}
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+#endif /* __EDT_H_ */
diff --git a/bpf/lib/maps.h b/bpf/lib/maps.h
index 9cbb9ddadd..bfc21f442f 100644
--- a/bpf/lib/maps.h
+++ b/bpf/lib/maps.h
@@ -42,6 +42,17 @@ struct bpf_elf_map __section_maps POLICY_CALL_MAP = {
 };
 #endif /* SKIP_POLICY_MAP */
 
+#ifdef ENABLE_BANDWIDTH_MANAGER
+struct bpf_elf_map __section_maps THROTTLE_MAP = {
+	.type		= BPF_MAP_TYPE_HASH,
+	.size_key	= sizeof(struct edt_id),
+	.size_value	= sizeof(struct edt_info),
+	.pinning	= PIN_GLOBAL_NS,
+	.max_elem	= THROTTLE_MAP_SIZE,
+	.flags		= BPF_F_NO_PREALLOC,
+};
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+
 /* Map to link endpoint id to per endpoint cilium_policy map */
 #ifdef SOCKMAP
 struct bpf_elf_map __section_maps EP_POLICY_MAP = {
diff --git a/bpf/lib/overloadable_skb.h b/bpf/lib/overloadable_skb.h
index 7a563f872d..7b66a722e1 100644
--- a/bpf/lib/overloadable_skb.h
+++ b/bpf/lib/overloadable_skb.h
@@ -9,11 +9,11 @@ bpf_clear_meta(struct __sk_buff *ctx)
 {
 	__u32 zero = 0;
 
-	ctx->cb[0] = zero;
-	ctx->cb[1] = zero;
-	ctx->cb[2] = zero;
-	ctx->cb[3] = zero;
-	ctx->cb[4] = zero;
+	WRITE_ONCE(ctx->cb[0], zero);
+	WRITE_ONCE(ctx->cb[1], zero);
+	WRITE_ONCE(ctx->cb[2], zero);
+	WRITE_ONCE(ctx->cb[3], zero);
+	WRITE_ONCE(ctx->cb[4], zero);
 }
 
 /**
diff --git a/bpf/node_config.h b/bpf/node_config.h
index 7d0598ef12..d1cac7e9c9 100644
--- a/bpf/node_config.h
+++ b/bpf/node_config.h
@@ -91,6 +91,8 @@ DEFINE_IPV6(HOST_IP, 0xbe, 0xef, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0xa, 0x
 #define LB4_AFFINITY_MAP test_cilium_lb4_affinity
 #define LB6_AFFINITY_MAP test_cilium_lb6_affinity
 #define LB_AFFINITY_MATCH_MAP test_cilium_lb_affinity_match
+#define THROTTLE_MAP test_cilium_throttle
+#define THROTTLE_MAP_SIZE 65536
 #define ENABLE_ARP_RESPONDER
 #define TUNNEL_ENDPOINT_MAP_SIZE 65536
 #define ENDPOINTS_MAP_SIZE 65536
diff --git a/bugtool/cmd/configuration.go b/bugtool/cmd/configuration.go
index 8939cdf0b1..f37590c45a 100644
--- a/bugtool/cmd/configuration.go
+++ b/bugtool/cmd/configuration.go
@@ -54,10 +54,13 @@ func defaultCommands(confDir string, cmdDir string, k8sPods []string) []string {
 		"ip -6 n",
 		"ss -t -p -a -i -s",
 		"ss -u -p -a -i -s",
+		"tc qdisc show",
+		"tc -d -s qdisc show",
 		"uname -a",
 		"top -b -n 1",
 		"uptime",
 		"dmesg --time-format=iso",
+		"sysctl -a",
 		"bpftool map show",
 		"bpftool prog show",
 		// LB and CT map for debugging services; using bpftool for a reliable dump
@@ -231,6 +234,7 @@ func copyCiliumInfoCommands(cmdDir string, k8sPods []string) []string {
 		"cilium metrics list",
 		"cilium fqdn cache list",
 		"cilium config",
+		"cilium bpf bandwidth list",
 		"cilium bpf tunnel list",
 		"cilium bpf lb list",
 		"cilium bpf endpoint list",
diff --git a/cilium/cmd/bpf_bandwidth.go b/cilium/cmd/bpf_bandwidth.go
new file mode 100644
index 0000000000..d374e8f8ad
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth.go
@@ -0,0 +1,29 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// bpfBandwidthCmd represents the bpf_bandwidth command
+var bpfBandwidthCmd = &cobra.Command{
+	Use:   "bandwidth",
+	Short: "BPF datapath bandwidth settings",
+}
+
+func init() {
+	bpfCmd.AddCommand(bpfBandwidthCmd)
+}
diff --git a/cilium/cmd/bpf_bandwidth_list.go b/cilium/cmd/bpf_bandwidth_list.go
new file mode 100644
index 0000000000..80d4656b80
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth_list.go
@@ -0,0 +1,104 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+
+	"github.com/spf13/cobra"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+var bpfBandwidthListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List BPF datapath bandwidth settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		common.RequireRootPrivilege("cilium bpf bandwidth list")
+
+		bpfBandwidthList := make(map[string][]string)
+		if err := bwmap.ThrottleMap.Dump(bpfBandwidthList); err != nil {
+			fmt.Fprintf(os.Stderr, "error dumping contents of map: %s\n", err)
+			os.Exit(1)
+		}
+
+		if command.OutputJSON() {
+			if err := command.PrintOutput(bpfBandwidthList); err != nil {
+				fmt.Fprintf(os.Stderr, "error getting output of map in JSON: %s\n", err)
+				os.Exit(1)
+			}
+			return
+		}
+
+		listBandwidth(bpfBandwidthList)
+	},
+}
+
+func listBandwidth(bpfBandwidthList map[string][]string) {
+	if len(bpfBandwidthList) == 0 {
+		fmt.Fprintf(os.Stderr, "No entries found.\n")
+		return
+	}
+
+	const (
+		labelsIDTitle   = "IDENTITY"
+		labelsBandwidth = "EGRESS BANDWIDTH (BitsPerSec)"
+	)
+
+	w := tabwriter.NewWriter(os.Stdout, 5, 0, 3, ' ', 0)
+	fmt.Fprintf(w, "%s\t%s\n", labelsIDTitle, labelsBandwidth)
+
+	const numColumns = 2
+	rows := [][numColumns]string{}
+
+	for key, value := range bpfBandwidthList {
+		bps, _ := strconv.Atoi(value[0])
+		bps *= 8
+		quantity := resource.NewQuantity(int64(bps), resource.DecimalSI)
+		rows = append(rows, [numColumns]string{key, quantity.String()})
+	}
+
+	sort.Slice(rows, func(i, j int) bool {
+		for k := 0; k < numColumns; k++ {
+			c := strings.Compare(rows[i][k], rows[j][k])
+
+			if c != 0 {
+				return c < 0
+			}
+		}
+
+		return false
+	})
+
+	for _, r := range rows {
+		fmt.Fprintf(w, "%s\t%s\n", r[0], r[1])
+	}
+
+	w.Flush()
+}
+
+func init() {
+	bpfBandwidthCmd.AddCommand(bpfBandwidthListCmd)
+	command.AddJSONOutput(bpfBandwidthListCmd)
+}
diff --git a/contrib/coccinelle/const.cocci b/contrib/coccinelle/const.cocci
index 5d1e6515ef..53a6e66fdf 100644
--- a/contrib/coccinelle/const.cocci
+++ b/contrib/coccinelle/const.cocci
@@ -43,6 +43,7 @@ position p;
       when != &x->z
       when != e = x
       when != WRITE_ONCE(x->z, ...)
+      when != WRITE_ONCE(x->z[...], ...)
       when != f(..., x, ...)
       // Special case for the addr field since it's an array in several
       // structures.
diff --git a/daemon/cmd/daemon.go b/daemon/cmd/daemon.go
index ebdd28349d..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..38b4ced626 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -520,6 +520,9 @@ func init() {
 	flags.Bool(option.EnableSVCSourceRangeCheck, true, "Enable check of service source ranges (currently, only for LoadBalancer)")
 	option.BindEnv(option.EnableSVCSourceRangeCheck)
 
+	flags.Bool(option.EnableBandwidthManager, false, "Enable BPF bandwidth manager")
+	option.BindEnv(option.EnableBandwidthManager)
+
 	flags.String(option.NodePortMode, option.NodePortModeSNAT, "BPF NodePort mode (\"snat\", \"dsr\", \"hybrid\")")
 	option.BindEnv(option.NodePortMode)
 
@@ -1393,11 +1396,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/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index bdc25c8d74..eae40a6a8f 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -400,6 +400,7 @@ data:
   iptables-lock-timeout: {{ .Values.global.iptablesLockTimeout | quote }}
 {{- end }}
   auto-direct-node-routes: {{ .Values.global.autoDirectNodeRoutes | quote }}
+  enable-bandwidth-manager: {{ .Values.global.bandwidthManager | quote }}
 {{- if .Values.global.nativeRoutingCIDR }}
   native-routing-cidr: {{ .Values.global.nativeRoutingCIDR }}
 {{- end }}
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 847b2b13e8..5da17a6448 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -335,6 +335,10 @@ global:
   # hostFirewall enables the enforcement of host policies in the BPF datapath
   hostFirewall: false
 
+  # bandwidthManager enables BBR and FQ + EDT + BPF based rate-limiting for
+  # Pods that are annotated with kubernetes.io/egress-bandwidth.
+  bandwidthManager: true
+
   # kubeProxyReplacement enables kube-proxy replacement in Cilium BPF datapath
   kubeProxyReplacement: "probe"
 
diff --git a/install/kubernetes/experimental-install.yaml b/install/kubernetes/experimental-install.yaml
index c5a984e5d8..c94fa95ea2 100644
--- a/install/kubernetes/experimental-install.yaml
+++ b/install/kubernetes/experimental-install.yaml
@@ -125,6 +125,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/install/kubernetes/quick-install.yaml b/install/kubernetes/quick-install.yaml
index a34b267659..6209c36cfb 100644
--- a/install/kubernetes/quick-install.yaml
+++ b/install/kubernetes/quick-install.yaml
@@ -111,6 +111,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/pkg/bandwidth/bandwidth.go b/pkg/bandwidth/bandwidth.go
new file mode 100644
index 0000000000..74f1159f46
--- /dev/null
+++ b/pkg/bandwidth/bandwidth.go
@@ -0,0 +1,164 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bandwidth
+
+import (
+	"github.com/cilium/cilium/pkg/datapath/linux/probes"
+	"github.com/cilium/cilium/pkg/logging"
+	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/sysctl"
+	"github.com/sirupsen/logrus"
+	"github.com/vishvananda/netlink"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+const (
+	subsystem = "bandwidth-manager"
+
+	// EgressBandwidth is the K8s Pod annotation.
+	EgressBandwidth = "kubernetes.io/egress-bandwidth"
+	// IngressBandwidth is the K8s Pod annotation.
+	IngressBandwidth = "kubernetes.io/ingress-bandwidth"
+)
+
+var log = logging.DefaultLogger.WithField(logfields.LogSubsys, subsystem)
+
+func GetBytesPerSec(bandwidth string) (uint64, error) {
+	res, err := resource.ParseQuantity(bandwidth)
+	if err != nil {
+		return 0, err
+	}
+	return uint64(res.Value() / 8), err
+}
+
+func ProbeBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if _, err := sysctl.Read("net.core.default_qdisc"); err != nil {
+		log.Warn("BPF bandwidth manager could not read procfs. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	kernelGood := false
+	if h := probes.NewProbeManager().GetHelpers("sched_cls"); h != nil {
+		// We at least need 5.0 kernel for native TCP EDT integration.
+		// Below helper is available for 5.0 kernels and onwards.
+		if _, ok := h["bpf_sk_release"]; ok {
+			kernelGood = true
+		}
+	}
+	if !kernelGood {
+		log.Warn("BPF bandwidth manager needs kernel 5.0 or newer. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+}
+
+func InitBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if len(option.Config.Devices) == 0 {
+		log.Warn("BPF bandwidth manager could not detect host devices. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	log.Info("Setting up BPF bandwidth manager")
+
+	if _, err := bwmap.ThrottleMap.OpenOrCreate(); err != nil {
+		log.WithError(err).Fatal("Failed to access ThrottleMap")
+	}
+
+	type setting struct {
+		name string
+		val  string
+	}
+	baseSettings := []setting{
+		{"net.core.netdev_max_backlog", "1000"},
+		{"net.core.somaxconn", "4096"},
+		{"net.core.default_qdisc", "fq"},
+		{"net.ipv4.tcp_congestion_control", "bbr"},
+		{"net.ipv4.tcp_max_syn_backlog", "4096"},
+	}
+	for _, s := range baseSettings {
+		log.WithFields(logrus.Fields{
+			logfields.SysParamName:  s.name,
+			logfields.SysParamValue: s.val,
+		}).Info("Setting sysctl")
+		if err := sysctl.Write(s.name, s.val); err != nil {
+			log.WithError(err).WithFields(logrus.Fields{
+				logfields.SysParamName:  s.name,
+				logfields.SysParamValue: s.val,
+			}).Fatal("Failed to set sysctl needed by BPF bandwidth manager.")
+		}
+	}
+
+	for _, device := range option.Config.Devices {
+		link, err := netlink.LinkByName(device)
+		if err != nil {
+			log.WithError(err).WithField("device", device).Warn("Link does not exist")
+			continue
+		}
+		// We strictly want to avoid a down/up cycle on the device at
+		// runtime, so given we've changed the default qdisc to FQ, we
+		// need to reset the root qdisc, and then set up MQ which will
+		// automatically get FQ leaf qdiscs (given it's been default).
+		qdisc := &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "noqueue",
+		}
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			log.WithError(err).WithField("device", device).
+				Fatalf("Cannot replace root Qdisc to %s", qdisc.QdiscType)
+		}
+		qdisc = &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "mq",
+		}
+		which := "mq with fq leafs"
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			// No MQ support, so just replace to FQ directly.
+			fq := &netlink.Fq{
+				QdiscAttrs: netlink.QdiscAttrs{
+					LinkIndex: link.Attrs().Index,
+					Parent:    netlink.HANDLE_ROOT,
+				},
+				Pacing: 1,
+			}
+			// At this point there is nothing we can do about
+			// it if we fail here, so hard bail out.
+			if err = netlink.QdiscReplace(fq); err != nil {
+				log.WithError(err).WithField("device", device).
+					Fatalf("Cannot replace root Qdisc to %s", fq.Type())
+			}
+			which = "fq"
+		}
+		log.WithField("device", device).Infof("Setting qdisc to %s", which)
+	}
+}
diff --git a/pkg/bandwidth/doc.go b/pkg/bandwidth/doc.go
new file mode 100644
index 0000000000..9dd2769508
--- /dev/null
+++ b/pkg/bandwidth/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bandwidth provides efficient EDT-based rate-limiting.
+package bandwidth
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 819bcc613a..6607cfedc4 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -400,6 +400,17 @@ func FormatStatusResponse(w io.Writer, sr *models.StatusResponse, sd StatusDetai
 		}
 	}
 
+	if sr.BandwidthManager != nil {
+		var status string
+		if !sr.BandwidthManager.Enabled {
+			status = "Disabled"
+		} else {
+			status = fmt.Sprintf("EDT with BPF\t[%s]",
+				strings.Join(sr.BandwidthManager.Devices, ", "))
+		}
+		fmt.Fprintf(w, "BandwidthManager:\t%s\n", status)
+	}
+
 	if sr.Masquerading != nil {
 		var status string
 		if !sr.Masquerading.Enabled {
diff --git a/pkg/datapath/alignchecker/alignchecker.go b/pkg/datapath/alignchecker/alignchecker.go
index 6f9e3b1eff..f75deaddc8 100644
--- a/pkg/datapath/alignchecker/alignchecker.go
+++ b/pkg/datapath/alignchecker/alignchecker.go
@@ -19,6 +19,7 @@ import (
 
 	check "github.com/cilium/cilium/pkg/alignchecker"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/eppolicymap"
 	"github.com/cilium/cilium/pkg/maps/eventsmap"
@@ -86,6 +87,8 @@ func CheckStructAlignments(path string) error {
 		"lb_affinity_val":   {reflect.TypeOf(lbmap.AffinityValue{})},
 		"lb4_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey4{})},
 		"lb6_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey6{})},
+		"edt_id":            {reflect.TypeOf(bwmap.EdtId{})},
+		"edt_info":          {reflect.TypeOf(bwmap.EdtInfo{})},
 	}
 	if err := check.CheckStructAlignments(path, toCheck, true); err != nil {
 		return err
diff --git a/pkg/datapath/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/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..be33770400 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -266,6 +266,9 @@ const (
 	// conflicting marks.
 	EnableIdentityMark = "enable-identity-mark"
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager = "enable-bandwidth-manager"
+
 	// LibDir enables the directory path to store runtime build environment
 	LibDir = "lib-dir"
 
@@ -1744,6 +1747,9 @@ type DaemonConfig struct {
 	// features in BPF datapath
 	KubeProxyReplacement string
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager bool
+
 	// EnableExternalIPs enables implementation of k8s services with externalIPs in datapath
 	EnableExternalIPs bool
 
@@ -2334,6 +2340,7 @@ func (c *DaemonConfig) Populate() {
 	c.EnableAutoProtectNodePortRange = viper.GetBool(EnableAutoProtectNodePortRange)
 	c.KubeProxyReplacement = viper.GetString(KubeProxyReplacement)
 	c.EnableSessionAffinity = viper.GetBool(EnableSessionAffinity)
+	c.EnableBandwidthManager = viper.GetBool(EnableBandwidthManager)
 	c.EnableHostFirewall = viper.GetBool(EnableHostFirewall)
 	c.EncryptInterface = viper.GetString(EncryptInterface)
 	c.EncryptNode = viper.GetBool(EncryptNode)
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index ca8f8e8f78..6f2ba5e73b 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -346,10 +346,9 @@ func (l4 *L4Filter) ToMapState(policyOwner PolicyOwner, direction trafficdirecti
 //
 // The caller is responsible for making sure the same identity is not
 // present in both 'added' and 'deleted'.
-func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 	log.WithFields(logrus.Fields{
 		logfields.EndpointSelector: selector,
-		logfields.PolicyID:         selections,
 		logfields.AddedPolicyID:    added,
 		logfields.DeletedPolicyID:  deleted,
 	}).Debug("identities selected by L4Filter updated")
diff --git a/pkg/policy/selectorcache.go b/pkg/policy/selectorcache.go
index 3a500046e2..f413948eea 100644
--- a/pkg/policy/selectorcache.go
+++ b/pkg/policy/selectorcache.go
@@ -106,7 +106,7 @@ type CachedSelectionUser interface {
 	//
 	// The caller is responsible for making sure the same identity is not
 	// present in both 'added' and 'deleted'.
-	IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity)
+	IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity)
 }
 
 // identitySelector is the internal interface for all selectors in the
@@ -378,7 +378,7 @@ type fqdnSelector struct {
 func (f *fqdnSelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range f.users {
 		// pass 'f' to the user as '*fqdnSelector'
-		user.IdentitySelectionUpdated(f, f.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(f, added, deleted)
 	}
 }
 
@@ -437,7 +437,7 @@ type labelIdentitySelector struct {
 func (l *labelIdentitySelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range l.users {
 		// pass 'l' to the user as '*labelIdentitySelector'
-		user.IdentitySelectionUpdated(l, l.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(l, added, deleted)
 	}
 }
 
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/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)