diff mbox series

[ovs-dev,RFC,ovn] Add CoPP (Control Plane Protection).

Message ID 1570695613-26065-1-git-send-email-dceara@redhat.com
State Superseded
Headers show
Series [ovs-dev,RFC,ovn] Add CoPP (Control Plane Protection). | expand

Commit Message

Dumitru Ceara Oct. 10, 2019, 8:20 a.m. UTC
Add new 'Copp' (Control plane protection) table to OVN Northbound DB:
- this stores mappings between control plane protocol names and meters
  that should be used to rate limit controller-destined traffic for
  those protocols.

Add new 'copp' columns to the following OVN Northbound DB tables:
- Logical_Switch
- Logical_Switch_Port
- Logical_Router
- Logical_Router_Port

This allows defining control plane policies with different
granularities. For example a user can decide to enforce a general
policy for the logical switch but at the same time configure a
different policy on some of the ports of the logical switch.
Control plane protocol policies applied to a logical port take
precedence over the ones defined at logical switch level. For
logical routers and logical router ports we take the same approach.

Add a new 'controller_meter' column to OVN Southbound Logical_Flow
table. This stores an optional string which should correspond to
the Meter that must be used for rate limiting controller actions
generated by packets hitting the flow.

Add CLI commands in 'ovn-nbctl' to allow the user to manage Control
Plane Protection Policies at different levels (logical switch,
logical router, logical port).

Add a new 'ctrl_meter_id' field to 'struct ovn_flow' to be used for
applying meters to flows that trigger controller actions.

Add a new 'ofctrl_add_flow_meter' function to create a new 'ovn_flow'
with an attached controller meter.

Change ofctrl_check_and_add_flow to allow specifying a meter ID for
packets that are punted to controller.

Change consider_logical_flow to parse controller_meter from the logical
flow and use it when building openflow entries.

Add a new 'ctrl_meter_id' field to 'struct ovnact_encode_params' to be
used when encoding controller actions from logical flow actions.

Change the ovn-northd implementation to set the new 'controller_meter'
field for flows that need to punt packets to ovn-controller. For some
protocols (ARP/ND/DNS) install two sets of flows:
- one flow with a lower priority for the whole datapath using the
  per-datapath CoPP policy.
- one flow per port with a higher priority than the per-datapath one if
  there is a per-port CoPP defined for the given port.

Post-RFC remaining items:
- add autotests for CoPP

Reported-at: https://mail.openvswitch.org/pipermail/ovs-dev/2019-September/362732.html
CC: Han Zhou <hzhou8@ebay.com>
CC: Numan Siddique <nusiddiq@redhat.com>
Signed-off-by: Dumitru Ceara <dceara@redhat.com>
---
 controller/lflow.c        |  39 ++-
 controller/ofctrl.c       |  17 +-
 controller/ofctrl.h       |   9 +-
 controller/physical.c     |   3 +-
 include/ovn/actions.h     |   3 +-
 lib/actions.c             | 104 ++----
 lib/automake.mk           |   2 +
 lib/copp.c                |  78 +++++
 lib/copp.h                |  55 +++
 northd/ovn-northd.c       | 838 ++++++++++++++++++++++++++++++++--------------
 ovn-nb.ovsschema          |  24 +-
 ovn-nb.xml                |  91 +++++
 ovn-sb.ovsschema          |   6 +-
 ovn-sb.xml                |   6 +
 tests/ovn.at              |   3 +-
 utilities/ovn-nbctl.8.xml |  94 ++++++
 utilities/ovn-nbctl.c     | 364 ++++++++++++++++++++
 17 files changed, 1406 insertions(+), 330 deletions(-)
 create mode 100644 lib/copp.c
 create mode 100644 lib/copp.h

Comments

Han Zhou Oct. 18, 2019, 1:53 a.m. UTC | #1
On Thu, Oct 10, 2019 at 1:20 AM Dumitru Ceara <dceara@redhat.com> wrote:
>
> Add new 'Copp' (Control plane protection) table to OVN Northbound DB:
> - this stores mappings between control plane protocol names and meters
>   that should be used to rate limit controller-destined traffic for
>   those protocols.
>
> Add new 'copp' columns to the following OVN Northbound DB tables:
> - Logical_Switch
> - Logical_Switch_Port
> - Logical_Router
> - Logical_Router_Port
>
> This allows defining control plane policies with different
> granularities. For example a user can decide to enforce a general
> policy for the logical switch but at the same time configure a
> different policy on some of the ports of the logical switch.
> Control plane protocol policies applied to a logical port take
> precedence over the ones defined at logical switch level. For
> logical routers and logical router ports we take the same approach.
>
> Add a new 'controller_meter' column to OVN Southbound Logical_Flow
> table. This stores an optional string which should correspond to
> the Meter that must be used for rate limiting controller actions
> generated by packets hitting the flow.
>
> Add CLI commands in 'ovn-nbctl' to allow the user to manage Control
> Plane Protection Policies at different levels (logical switch,
> logical router, logical port).
>
> Add a new 'ctrl_meter_id' field to 'struct ovn_flow' to be used for
> applying meters to flows that trigger controller actions.
>
> Add a new 'ofctrl_add_flow_meter' function to create a new 'ovn_flow'
> with an attached controller meter.
>
> Change ofctrl_check_and_add_flow to allow specifying a meter ID for
> packets that are punted to controller.
>
> Change consider_logical_flow to parse controller_meter from the logical
> flow and use it when building openflow entries.
>
> Add a new 'ctrl_meter_id' field to 'struct ovnact_encode_params' to be
> used when encoding controller actions from logical flow actions.
>
> Change the ovn-northd implementation to set the new 'controller_meter'
> field for flows that need to punt packets to ovn-controller. For some
> protocols (ARP/ND/DNS) install two sets of flows:
> - one flow with a lower priority for the whole datapath using the
>   per-datapath CoPP policy.
> - one flow per port with a higher priority than the per-datapath one if
>   there is a per-port CoPP defined for the given port.
>
> Post-RFC remaining items:
> - add autotests for CoPP
>
> Reported-at:
https://mail.openvswitch.org/pipermail/ovs-dev/2019-September/362732.html
> CC: Han Zhou <hzhou8@ebay.com>
> CC: Numan Siddique <nusiddiq@redhat.com>
> Signed-off-by: Dumitru Ceara <dceara@redhat.com>

Thanks Dumitru for the RFC. I didn't review in too much detail but just
some quick feedback.
Overall, it is great that this approach addresses rate limit in a generic
way.
It is easy to understand the meters applied to router/switch because each
lflow belongs to a datapath.
However, could you describe more about how the meters for a
logical_switch_port or logical_router_port is defined? Is it applied when
the specific port is the ingress port, or egress port, or both?
Regarding the attacking problem we have discussed before, when there is
attack from a single src through a single router port, even if we enforce a
dedicated meter on the router port level, e.g. for ARP resolve, then all
ARP resolving through that router port would still be impacted. Did you
consider flow based meters to lower the impact in that situation?
In addition, I suggest to split such big change to smaller incremental ones
when you submit the formal patch, if possible. For example, rate limit for
each protocol can be a separate patch, which would make the review easier
and some patches can be merged before the whole change is completely merged.

Thanks,
Han
Dumitru Ceara Oct. 18, 2019, 8:27 a.m. UTC | #2
On Fri, Oct 18, 2019 at 3:53 AM Han Zhou <zhouhan@gmail.com> wrote:
>
>
>
> On Thu, Oct 10, 2019 at 1:20 AM Dumitru Ceara <dceara@redhat.com> wrote:
> >
> > Add new 'Copp' (Control plane protection) table to OVN Northbound DB:
> > - this stores mappings between control plane protocol names and meters
> >   that should be used to rate limit controller-destined traffic for
> >   those protocols.
> >
> > Add new 'copp' columns to the following OVN Northbound DB tables:
> > - Logical_Switch
> > - Logical_Switch_Port
> > - Logical_Router
> > - Logical_Router_Port
> >
> > This allows defining control plane policies with different
> > granularities. For example a user can decide to enforce a general
> > policy for the logical switch but at the same time configure a
> > different policy on some of the ports of the logical switch.
> > Control plane protocol policies applied to a logical port take
> > precedence over the ones defined at logical switch level. For
> > logical routers and logical router ports we take the same approach.
> >
> > Add a new 'controller_meter' column to OVN Southbound Logical_Flow
> > table. This stores an optional string which should correspond to
> > the Meter that must be used for rate limiting controller actions
> > generated by packets hitting the flow.
> >
> > Add CLI commands in 'ovn-nbctl' to allow the user to manage Control
> > Plane Protection Policies at different levels (logical switch,
> > logical router, logical port).
> >
> > Add a new 'ctrl_meter_id' field to 'struct ovn_flow' to be used for
> > applying meters to flows that trigger controller actions.
> >
> > Add a new 'ofctrl_add_flow_meter' function to create a new 'ovn_flow'
> > with an attached controller meter.
> >
> > Change ofctrl_check_and_add_flow to allow specifying a meter ID for
> > packets that are punted to controller.
> >
> > Change consider_logical_flow to parse controller_meter from the logical
> > flow and use it when building openflow entries.
> >
> > Add a new 'ctrl_meter_id' field to 'struct ovnact_encode_params' to be
> > used when encoding controller actions from logical flow actions.
> >
> > Change the ovn-northd implementation to set the new 'controller_meter'
> > field for flows that need to punt packets to ovn-controller. For some
> > protocols (ARP/ND/DNS) install two sets of flows:
> > - one flow with a lower priority for the whole datapath using the
> >   per-datapath CoPP policy.
> > - one flow per port with a higher priority than the per-datapath one if
> >   there is a per-port CoPP defined for the given port.
> >
> > Post-RFC remaining items:
> > - add autotests for CoPP
> >
> > Reported-at: https://mail.openvswitch.org/pipermail/ovs-dev/2019-September/362732.html
> > CC: Han Zhou <hzhou8@ebay.com>
> > CC: Numan Siddique <nusiddiq@redhat.com>
> > Signed-off-by: Dumitru Ceara <dceara@redhat.com>
>
> Thanks Dumitru for the RFC. I didn't review in too much detail but just some quick feedback.

Hi Han,

Thanks for having a look.

> Overall, it is great that this approach addresses rate limit in a generic way.
> It is easy to understand the meters applied to router/switch because each lflow belongs to a datapath.
> However, could you describe more about how the meters for a logical_switch_port or logical_router_port is defined? Is it applied when the specific port is the ingress port, or egress port, or both?

One of the things the RFC misses is thorough documentation. I will add
that in the next version.
The meters (regardless if they're defined globally for the
switch/router or on a port) are applied only for traffic that hits a
flow with action=controller(..). So it's ingress traffic in the
logical topology. There might be cases when the packets injected by
controller will hit flows that send them back to a controller
(sometimes different). One example that comes to mind is a GARP
originated for a logical router port that reaches another logical
router port which might or might not be on the same controller. In
this case too, because the packet hits a flow with
action=controller(..), the metering will be performed.

> Regarding the attacking problem we have discussed before, when there is attack from a single src through a single router port, even if we enforce a dedicated meter on the router port level, e.g. for ARP resolve, then all ARP resolving through that router port would still be impacted. Did you consider flow based meters to lower the impact in that situation?

Agreed, in case of an attack on a metered port, all incoming ARP
traffic on that port will be metered potentially affecting legit
packets. It's a first step.
What I had in mind for the next step is to allow finer grain rate
limiting for protocols where this is applicable (e.g., ARP) by
splitting the openflow in two parts:
- first hash the packets
- then rate limit per hash bucket

So a flow like:

table=X, match="arp", action="controller(<encode_put_arp>)"

Would be changed to (assuming 16 hash buckets):

table=X,   match="arp", action="multipath(symmetric_l3, modulo_n, 16,
0, NXM_NX_REGX[]), resubmit(X+1)"
table=X+1, match="arp, regX=0",
action="controller(meter_id=arp_meter_bucket0, <encode_put_arp>)"
table=X+1, match="arp, regX=1",
action="controller(meter_id=arp_meter_bucket1, <encode_put_arp>)"
[..]
table=X+1, match="arp, regX=15",
action="controller(meter_id=arp_meter_bucket16, <encode_put_arp>)"

But this is quite a big change and I'd like to at least have some
generic simple rate limiting in place first.

> In addition, I suggest to split such big change to smaller incremental ones when you submit the formal patch, if possible. For example, rate limit for each protocol can be a separate patch, which would make the review easier and some patches can be merged before the whole change is completely merged.

Right, I kept adding stuff and had the impression that it's not such a
big change but it definitely requires splitting now. Will do.

Thanks,
Dumitru

>
> Thanks,
> Han
>
diff mbox series

Patch

diff --git a/controller/lflow.c b/controller/lflow.c
index e3ed20c..3705a95 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -538,6 +538,26 @@  update_conj_id_ofs(uint32_t *conj_id_ofs, uint32_t n_conjs)
     return true;
 }
 
+static void
+lflow_parse_ctrl_meter(const struct sbrec_logical_flow *lflow,
+                       struct ovn_extend_table *meter_table,
+                       uint32_t *meter_id)
+{
+    *meter_id = NX_CTLR_NO_METER;
+
+    if (lflow->controller_meter) {
+        *meter_id = ovn_extend_table_assign_id(meter_table,
+                                               lflow->controller_meter,
+                                               lflow->header_.uuid);
+        if (*meter_id == EXT_TABLE_ID_INVALID) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Unable to assign id for meter: %s",
+                         lflow->controller_meter);
+            return;
+        }
+    }
+}
+
 static bool
 consider_logical_flow(
     struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
@@ -671,6 +691,12 @@  consider_logical_flow(
         return true;
     }
 
+    /* Parse any meter to be used if this flow should punt packets to
+     * controller.
+     */
+    uint32_t ctrl_meter_id = NX_CTLR_NO_METER;
+    lflow_parse_ctrl_meter(lflow, meter_table, &ctrl_meter_id);
+
     /* Encode OVN logical actions into OpenFlow. */
     uint64_t ofpacts_stub[1024 / 8];
     struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
@@ -688,6 +714,7 @@  consider_logical_flow(
         .output_ptable = output_ptable,
         .mac_bind_ptable = OFTABLE_MAC_BINDING,
         .mac_lookup_ptable = OFTABLE_MAC_LOOKUP,
+        .ctrl_meter_id = ctrl_meter_id,
     };
     ovnacts_encode(ovnacts.data, ovnacts.size, &ep, &ofpacts);
     ovnacts_free(ovnacts.data, ovnacts.size);
@@ -719,9 +746,10 @@  consider_logical_flow(
             }
         }
         if (!m->n) {
-            ofctrl_add_flow(flow_table, ptable, lflow->priority,
-                            lflow->header_.uuid.parts[0], &m->match, &ofpacts,
-                            &lflow->header_.uuid);
+            ofctrl_add_flow_meter(flow_table, ptable, lflow->priority,
+                                  lflow->header_.uuid.parts[0], &m->match,
+                                  &ofpacts, &lflow->header_.uuid,
+                                  ctrl_meter_id);
         } else {
             uint64_t conj_stubs[64 / 8];
             struct ofpbuf conj;
@@ -736,8 +764,9 @@  consider_logical_flow(
                 dst->clause = src->clause;
                 dst->n_clauses = src->n_clauses;
             }
-            ofctrl_add_flow(flow_table, ptable, lflow->priority, 0, &m->match,
-                            &conj, &lflow->header_.uuid);
+            ofctrl_add_flow_meter(flow_table, ptable, lflow->priority, 0,
+                                  &m->match, &conj, &lflow->header_.uuid,
+                                  ctrl_meter_id);
             ofpbuf_uninit(&conj);
         }
     }
diff --git a/controller/ofctrl.c b/controller/ofctrl.c
index 3131baf..9b1b72a 100644
--- a/controller/ofctrl.c
+++ b/controller/ofctrl.c
@@ -67,6 +67,7 @@  struct ovn_flow {
     struct ofpact *ofpacts;
     size_t ofpacts_len;
     uint64_t cookie;
+    uint32_t ctrl_meter_id; /* Meter to be used for controller actions. */
 };
 
 static uint32_t ovn_flow_match_hash(const struct ovn_flow *);
@@ -655,6 +656,7 @@  ofctrl_check_and_add_flow(struct ovn_desired_flow_table *flow_table,
                           uint64_t cookie, const struct match *match,
                           const struct ofpbuf *actions,
                           const struct uuid *sb_uuid,
+                          uint32_t meter_id,
                           bool log_duplicate_flow)
 {
     struct ovn_flow *f = xmalloc(sizeof *f);
@@ -667,6 +669,7 @@  ofctrl_check_and_add_flow(struct ovn_desired_flow_table *flow_table,
     f->match_hmap_node.hash = ovn_flow_match_hash(f);
     f->uuid_hindex_node.hash = uuid_hash(&f->sb_uuid);
     f->cookie = cookie;
+    f->ctrl_meter_id = meter_id;
 
     ovn_flow_log(f, "ofctrl_add_flow");
 
@@ -718,8 +721,18 @@  ofctrl_add_flow(struct ovn_desired_flow_table *desired_flows,
                 const struct match *match, const struct ofpbuf *actions,
                 const struct uuid *sb_uuid)
 {
+    ofctrl_add_flow_meter(desired_flows, table_id, priority, cookie,
+                          match, actions, sb_uuid, NX_CTLR_NO_METER);
+}
+
+void
+ofctrl_add_flow_meter(struct ovn_desired_flow_table *desired_flows,
+                      uint8_t table_id, uint16_t priority, uint64_t cookie,
+                      const struct match *match, const struct ofpbuf *actions,
+                      const struct uuid *sb_uuid, uint32_t meter_id)
+{
     ofctrl_check_and_add_flow(desired_flows, table_id, priority, cookie,
-                              match, actions, sb_uuid, true);
+                              match, actions, sb_uuid, meter_id, true);
 }
 
 /* ovn_flow. */
@@ -745,6 +758,7 @@  ofctrl_dup_flow(struct ovn_flow *src)
     dst->sb_uuid = src->sb_uuid;
     dst->match_hmap_node.hash = src->match_hmap_node.hash;
     dst->uuid_hindex_node.hash = uuid_hash(&src->sb_uuid);
+    dst->ctrl_meter_id = src->ctrl_meter_id;
     return dst;
 }
 
@@ -760,6 +774,7 @@  ovn_flow_lookup(struct hmap *flow_table, const struct ovn_flow *target,
                              flow_table) {
         if (f->table_id == target->table_id
             && f->priority == target->priority
+            && f->ctrl_meter_id == target->ctrl_meter_id
             && minimatch_equal(&f->match, &target->match)) {
             if (!cmp_sb_uuid || uuid_equals(&target->sb_uuid, &f->sb_uuid)) {
                 return f;
diff --git a/controller/ofctrl.h b/controller/ofctrl.h
index 1e9ac16..d0f9ca3 100644
--- a/controller/ofctrl.h
+++ b/controller/ofctrl.h
@@ -70,6 +70,12 @@  void ofctrl_add_flow(struct ovn_desired_flow_table *, uint8_t table_id,
                      const struct match *, const struct ofpbuf *ofpacts,
                      const struct uuid *);
 
+void ofctrl_add_flow_meter(struct ovn_desired_flow_table *desired_flows,
+                           uint8_t table_id, uint16_t priority,
+                           uint64_t cookie, const struct match *match,
+                           const struct ofpbuf *actions,
+                           const struct uuid *sb_uuid, uint32_t meter_id);
+
 void ofctrl_remove_flows(struct ovn_desired_flow_table *, const struct uuid *);
 
 void ovn_desired_flow_table_init(struct ovn_desired_flow_table *);
@@ -80,7 +86,8 @@  void ofctrl_check_and_add_flow(struct ovn_desired_flow_table *,
                                uint8_t table_id, uint16_t priority,
                                uint64_t cookie, const struct match *,
                                const struct ofpbuf *ofpacts,
-                               const struct uuid *, bool log_duplicate_flow);
+                               const struct uuid *, uint32_t meter_id,
+                               bool log_duplicate_flow);
 
 bool ofctrl_is_connected(void);
 void ofctrl_set_probe_interval(int probe_interval);
diff --git a/controller/physical.c b/controller/physical.c
index 6e606d3..9b8cf42 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -809,7 +809,8 @@  put_local_common_flows(uint32_t dp_key, uint32_t port_key,
         put_resubmit(OFTABLE_LOG_TO_PHY, ofpacts_p);
         put_stack(MFF_IN_PORT, ofpact_put_STACK_POP(ofpacts_p));
         ofctrl_check_and_add_flow(flow_table, OFTABLE_SAVE_INPORT, 100, 0,
-                                  &match, ofpacts_p, hc_uuid, false);
+                                  &match, ofpacts_p, hc_uuid,
+                                  NX_CTLR_NO_METER, false);
     }
 }
 
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 4e2f4d2..7bcea03 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -338,7 +338,6 @@  struct ovnact_controller_event {
     int event_type;   /* controller event type */
     struct ovnact_gen_option *options;
     size_t n_options;
-    char *meter;
 };
 
 /* OVNACT_BIND_VPORT. */
@@ -641,6 +640,8 @@  struct ovnact_encode_params {
                                    resubmit. */
     uint8_t mac_lookup_ptable;  /* OpenFlow table for
                                    'lookup_arp'/'lookup_nd' to resubmit. */
+    uint32_t ctrl_meter_id;     /* Meter to be used if the resulting flow
+                                   sends packets to controller. */
 };
 
 void ovnacts_encode(const struct ovnact[], size_t ovnacts_len,
diff --git a/lib/actions.c b/lib/actions.c
index c8c9cc5..d259b96 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -105,10 +105,10 @@  encode_finish_controller_op(size_t ofs, struct ofpbuf *ofpacts)
 }
 
 static void
-encode_controller_op(enum action_opcode opcode, struct ofpbuf *ofpacts)
+encode_controller_op(enum action_opcode opcode, uint32_t meter_id,
+                     struct ofpbuf *ofpacts)
 {
-    size_t ofs = encode_start_controller_op(opcode, false, NX_CTLR_NO_METER,
-                                            ofpacts);
+    size_t ofs = encode_start_controller_op(opcode, false, meter_id, ofpacts);
     encode_finish_controller_op(ofs, ofpacts);
 }
 
@@ -1273,9 +1273,6 @@  format_TRIGGER_EVENT(const struct ovnact_controller_event *event,
 {
     ds_put_format(s, "trigger_event(event = \"%s\"",
                   event_to_string(event->event_type));
-    if (event->meter) {
-        ds_put_format(s, ", meter = \"%s\"", event->meter);
-    }
     for (const struct ovnact_gen_option *o = event->options;
          o < &event->options[event->n_options]; o++) {
         ds_put_cstr(s, ", ");
@@ -1301,7 +1298,7 @@  encode_nested_actions(const struct ovnact_nest *on,
      * packet to ARP or NA and then send the packet and actions back to the
      * switch inside an OFPT_PACKET_OUT message. */
     size_t oc_offset = encode_start_controller_op(opcode, false,
-                                                  NX_CTLR_NO_METER, ofpacts);
+                                                  ep->ctrl_meter_id, ofpacts);
     ofpacts_put_openflow_actions(inner_ofpacts.data, inner_ofpacts.size,
                                  ofpacts, OFP13_VERSION);
     encode_finish_controller_op(oc_offset, ofpacts);
@@ -1344,10 +1341,10 @@  encode_ICMP6(const struct ovnact_nest *on,
 
 static void
 encode_IGMP(const struct ovnact_null *a OVS_UNUSED,
-            const struct ovnact_encode_params *ep OVS_UNUSED,
+            const struct ovnact_encode_params *ep,
             struct ofpbuf *ofpacts)
 {
-    encode_controller_op(ACTION_OPCODE_IGMP, ofpacts);
+    encode_controller_op(ACTION_OPCODE_IGMP, ep->ctrl_meter_id, ofpacts);
 }
 
 static void
@@ -1420,24 +1417,11 @@  encode_event_empty_lb_backends_opts(struct ofpbuf *ofpacts,
 
 static void
 encode_TRIGGER_EVENT(const struct ovnact_controller_event *event,
-                     const struct ovnact_encode_params *ep OVS_UNUSED,
+                     const struct ovnact_encode_params *ep,
                      struct ofpbuf *ofpacts)
 {
-    uint32_t meter_id = NX_CTLR_NO_METER;
-    size_t oc_offset;
-
-    if (event->meter) {
-        meter_id = ovn_extend_table_assign_id(ep->meter_table, event->meter,
-                                              ep->lflow_uuid);
-        if (meter_id == EXT_TABLE_ID_INVALID) {
-            VLOG_WARN("Unable to assign id for trigger meter: %s",
-                      event->meter);
-            return;
-        }
-    }
-
-    oc_offset = encode_start_controller_op(ACTION_OPCODE_EVENT, false,
-                                           meter_id, ofpacts);
+    size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_EVENT, false,
+                                                  ep->ctrl_meter_id, ofpacts);
     ovs_be32 ofs = htonl(event->event_type);
     ofpbuf_put(ofpacts, &ofs, sizeof ofs);
 
@@ -1573,6 +1557,7 @@  format_PUT_ND(const struct ovnact_put_mac_bind *put_mac, struct ds *s)
 
 static void
 encode_put_mac(const struct ovnact_put_mac_bind *put_mac,
+               const struct ovnact_encode_params *ep,
                enum mf_field_id ip_field, enum action_opcode opcode,
                struct ofpbuf *ofpacts)
 {
@@ -1582,31 +1567,31 @@  encode_put_mac(const struct ovnact_put_mac_bind *put_mac,
         { expr_resolve_field(&put_mac->mac), MFF_ETH_SRC }
     };
     encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
-    encode_controller_op(opcode, ofpacts);
+    encode_controller_op(opcode, ep->ctrl_meter_id, ofpacts);
     encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
 }
 
 static void
 encode_PUT_ARP(const struct ovnact_put_mac_bind *put_mac,
-               const struct ovnact_encode_params *ep OVS_UNUSED,
+               const struct ovnact_encode_params *ep,
                struct ofpbuf *ofpacts)
 {
-    encode_put_mac(put_mac, MFF_REG0, ACTION_OPCODE_PUT_ARP, ofpacts);
+    encode_put_mac(put_mac, ep, MFF_REG0, ACTION_OPCODE_PUT_ARP, ofpacts);
 }
 
 static void
 encode_PUT_ND(const struct ovnact_put_mac_bind *put_mac,
-              const struct ovnact_encode_params *ep OVS_UNUSED,
+              const struct ovnact_encode_params *ep,
               struct ofpbuf *ofpacts)
 {
-    encode_put_mac(put_mac, MFF_XXREG0, ACTION_OPCODE_PUT_ND, ofpacts);
+    encode_put_mac(put_mac, ep, MFF_XXREG0, ACTION_OPCODE_PUT_ND, ofpacts);
 }
 
 static void
 ovnact_put_mac_bind_free(struct ovnact_put_mac_bind *put_mac OVS_UNUSED)
 {
 }
-
+
 static void format_lookup_mac(const struct ovnact_lookup_mac_bind *lookup_mac,
                               struct ds *s, const char *name)
 {
@@ -1858,27 +1843,12 @@  parse_trigger_event(struct action_context *ctx,
                                      sizeof *event->options);
         }
 
-        if (lexer_match_id(ctx->lexer, "meter")) {
-            if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
-                return;
-            }
-            /* If multiple meters are given, use the most recent. */
-            if (ctx->lexer->token.type == LEX_T_STRING &&
-                strlen(ctx->lexer->token.s)) {
-                free(event->meter);
-                event->meter = xstrdup(ctx->lexer->token.s);
-            } else if (ctx->lexer->token.type != LEX_T_STRING) {
-                lexer_syntax_error(ctx->lexer, "expecting string");
-                return;
-            }
-            lexer_get(ctx->lexer);
-        } else {
-            struct ovnact_gen_option *o = &event->options[event->n_options++];
-            memset(o, 0, sizeof *o);
-            parse_gen_opt(ctx, o,
-                    &ctx->pp->controller_event_opts->event_opts[event_type],
-                    event_to_string(event_type));
-            }
+        struct ovnact_gen_option *o = &event->options[event->n_options++];
+        memset(o, 0, sizeof *o);
+        parse_gen_opt(ctx, o,
+                      &ctx->pp->controller_event_opts->event_opts[event_type],
+                      event_to_string(event_type));
+
         if (ctx->lexer->error) {
             return;
         }
@@ -1899,7 +1869,6 @@  static void
 ovnact_controller_event_free(struct ovnact_controller_event *event)
 {
     free_gen_options(event->options, event->n_options);
-    free(event->meter);
 }
 
 static void
@@ -2096,13 +2065,13 @@  encode_put_dhcpv6_option(const struct ovnact_gen_option *o,
 
 static void
 encode_PUT_DHCPV4_OPTS(const struct ovnact_put_opts *pdo,
-                       const struct ovnact_encode_params *ep OVS_UNUSED,
+                       const struct ovnact_encode_params *ep,
                        struct ofpbuf *ofpacts)
 {
     struct mf_subfield dst = expr_resolve_field(&pdo->dst);
 
     size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_PUT_DHCP_OPTS,
-                                                  true, NX_CTLR_NO_METER,
+                                                  true, ep->ctrl_meter_id,
                                                   ofpacts);
     nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
     ovs_be32 ofs = htonl(dst.ofs);
@@ -2128,13 +2097,13 @@  encode_PUT_DHCPV4_OPTS(const struct ovnact_put_opts *pdo,
 
 static void
 encode_PUT_DHCPV6_OPTS(const struct ovnact_put_opts *pdo,
-                       const struct ovnact_encode_params *ep OVS_UNUSED,
+                       const struct ovnact_encode_params *ep,
                        struct ofpbuf *ofpacts)
 {
     struct mf_subfield dst = expr_resolve_field(&pdo->dst);
 
     size_t oc_offset = encode_start_controller_op(
-        ACTION_OPCODE_PUT_DHCPV6_OPTS, true, NX_CTLR_NO_METER, ofpacts);
+        ACTION_OPCODE_PUT_DHCPV6_OPTS, true, ep->ctrl_meter_id, ofpacts);
     nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
     ovs_be32 ofs = htonl(dst.ofs);
     ofpbuf_put(ofpacts, &ofs, sizeof ofs);
@@ -2223,13 +2192,13 @@  format_DNS_LOOKUP(const struct ovnact_dns_lookup *dl, struct ds *s)
 
 static void
 encode_DNS_LOOKUP(const struct ovnact_dns_lookup *dl,
-                  const struct ovnact_encode_params *ep OVS_UNUSED,
+                  const struct ovnact_encode_params *ep,
                   struct ofpbuf *ofpacts)
 {
     struct mf_subfield dst = expr_resolve_field(&dl->dst);
 
     size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_DNS_LOOKUP,
-                                                  true, NX_CTLR_NO_METER,
+                                                  true, ep->ctrl_meter_id,
                                                   ofpacts);
     nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
     ovs_be32 ofs = htonl(dst.ofs);
@@ -2388,13 +2357,14 @@  encode_put_nd_ra_option(const struct ovnact_gen_option *o,
 
 static void
 encode_PUT_ND_RA_OPTS(const struct ovnact_put_opts *po,
-                      const struct ovnact_encode_params *ep OVS_UNUSED,
+                      const struct ovnact_encode_params *ep,
                       struct ofpbuf *ofpacts)
 {
     struct mf_subfield dst = expr_resolve_field(&po->dst);
 
-    size_t oc_offset = encode_start_controller_op(
-        ACTION_OPCODE_PUT_ND_RA_OPTS, true, NX_CTLR_NO_METER, ofpacts);
+    size_t oc_offset =
+        encode_start_controller_op(ACTION_OPCODE_PUT_ND_RA_OPTS, true,
+                                   ep->ctrl_meter_id, ofpacts);
     nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
     ovs_be32 ofs = htonl(dst.ofs);
     ofpbuf_put(ofpacts, &ofs, sizeof ofs);
@@ -2669,15 +2639,15 @@  format_OVNFIELD_LOAD(const struct ovnact_load *load , struct ds *s)
 
 static void
 encode_OVNFIELD_LOAD(const struct ovnact_load *load,
-            const struct ovnact_encode_params *ep OVS_UNUSED,
+            const struct ovnact_encode_params *ep,
             struct ofpbuf *ofpacts)
 {
     const struct ovn_field *f = ovn_field_from_name(load->dst.symbol->name);
     switch (f->id) {
     case OVN_ICMP4_FRAG_MTU: {
-        size_t oc_offset = encode_start_controller_op(
-            ACTION_OPCODE_PUT_ICMP4_FRAG_MTU, true, NX_CTLR_NO_METER,
-            ofpacts);
+        size_t oc_offset =
+            encode_start_controller_op(ACTION_OPCODE_PUT_ICMP4_FRAG_MTU, true,
+                                       ep->ctrl_meter_id, ofpacts);
         ofpbuf_put(ofpacts, &load->imm.value.be16_int, sizeof(ovs_be16));
         encode_finish_controller_op(oc_offset, ofpacts);
         break;
@@ -2781,7 +2751,7 @@  encode_BIND_VPORT(const struct ovnact_bind_vport *vp,
     };
     encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
     size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_BIND_VPORT,
-                                                  false, NX_CTLR_NO_METER,
+                                                  false, ep->ctrl_meter_id,
                                                   ofpacts);
     ovs_be32 vp_key = htonl(vport_key);
     ofpbuf_put(ofpacts, &vp_key, sizeof(ovs_be32));
diff --git a/lib/automake.mk b/lib/automake.mk
index 0c8245c..7ba7ca0 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -9,6 +9,8 @@  lib_libovn_la_SOURCES = \
 	lib/actions.c \
 	lib/chassis-index.c \
 	lib/chassis-index.h \
+	lib/copp.c \
+	lib/copp.h \
 	lib/ovn-dirs.h \
 	lib/expr.c \
 	lib/extend-table.h \
diff --git a/lib/copp.c b/lib/copp.c
new file mode 100644
index 0000000..7f3b25c
--- /dev/null
+++ b/lib/copp.c
@@ -0,0 +1,78 @@ 
+/* Copyright (c) 2019, Red Hat, Inc.
+ *
+ * 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.
+ */
+
+#include <config.h>
+#include <stdlib.h>
+
+#include "openvswitch/shash.h"
+#include "smap.h"
+#include "lib/ovn-nb-idl.h"
+#include "lib/copp.h"
+
+static char *copp_proto_names[COPP_PROTO_MAX] = {
+    [COPP_ARP]           = "arp",
+    [COPP_ARP_RESOLVE]   = "arp-resolve",
+    [COPP_DHCPV4_OPTS]   = "dhcpv4-opts",
+    [COPP_DHCPV6_OPTS]   = "dhcpv6-opts",
+    [COPP_DNS]           = "dns",
+    [COPP_EVENT_ELB]     = "event-elb",
+    [COPP_ICMP4_ERR]     = "icmp4-error",
+    [COPP_ICMP6_ERR]     = "icmp6-error",
+    [COPP_IGMP]          = "igmp",
+    [COPP_ND_NA]         = "nd-na",
+    [COPP_ND_NS]         = "nd-ns",
+    [COPP_ND_NS_RESOLVE] = "nd-ns-resolve",
+    [COPP_ND_RA_OPTS]    = "nd-ra-opts",
+    [COPP_TCP_RESET]     = "tcp-reset",
+};
+
+const char *
+get_copp_proto_name(enum copp_proto proto)
+{
+    if (proto >= COPP_PROTO_MAX) {
+        return "<Invalid control protocol ID>";
+    }
+    return copp_proto_names[proto];
+}
+
+const char *
+get_copp_meter(enum copp_proto proto, const struct nbrec_copp *copp,
+               const struct shash *meter_groups)
+{
+    if (!copp || proto >= COPP_PROTO_MAX) {
+        return NULL;
+    }
+
+    const char *meter = smap_get(&copp->meters, copp_proto_names[proto]);
+
+    if (meter && shash_find(meter_groups, meter)) {
+        return meter;
+    }
+
+    return NULL;
+}
+
+const char *
+get_port_copp_meter(enum copp_proto proto, const struct nbrec_copp *port_copp,
+                    const struct nbrec_copp *dp_copp,
+                    const struct shash *meter_groups)
+{
+    const char *meter = get_copp_meter(proto, port_copp, meter_groups);
+
+    if (!meter) {
+        return get_copp_meter(proto, dp_copp, meter_groups);
+    }
+    return meter;
+}
diff --git a/lib/copp.h b/lib/copp.h
new file mode 100644
index 0000000..5c31d5f
--- /dev/null
+++ b/lib/copp.h
@@ -0,0 +1,55 @@ 
+/* Copyright (c) 2019, Red Hat, Inc.
+ *
+ * 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.
+ */
+
+#ifndef OVN_COPP_H
+#define OVN_COPP_H 1
+
+/*
+ * Control plane protection - metered actions.
+ */
+enum copp_proto {
+    COPP_PROTO_FIRST,
+    COPP_ARP = COPP_PROTO_FIRST,
+    COPP_ARP_RESOLVE,
+    COPP_DHCPV4_OPTS,
+    COPP_DHCPV6_OPTS,
+    COPP_DNS,
+    COPP_EVENT_ELB,
+    COPP_ICMP4_ERR,
+    COPP_ICMP6_ERR,
+    COPP_IGMP,
+    COPP_ND_NA,
+    COPP_ND_NS,
+    COPP_ND_NS_RESOLVE,
+    COPP_ND_RA_OPTS,
+    COPP_TCP_RESET,
+    COPP_PROTO_MAX,
+    COPP_PROTO_INVALID = COPP_PROTO_MAX,
+};
+
+struct nbrec_copp;
+
+const char *get_copp_proto_name(enum copp_proto);
+
+const char *get_copp_meter(enum copp_proto proto,
+                           const struct nbrec_copp *copp,
+                           const struct shash *meter_groups);
+
+const char *get_port_copp_meter(enum copp_proto proto,
+                                const struct nbrec_copp *port_copp,
+                                const struct nbrec_copp *dp_copp,
+                                const struct shash *meter_groups);
+
+#endif /* lib/copp.h */
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index e41c9d7..2b55f0f 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -29,6 +29,7 @@ 
 #include "openvswitch/json.h"
 #include "ovn/lex.h"
 #include "lib/chassis-index.h"
+#include "lib/copp.h"
 #include "lib/ip-mcast-index.h"
 #include "lib/mcast-group-index.h"
 #include "lib/ovn-l7.h"
@@ -3333,6 +3334,7 @@  struct ovn_lflow {
     char *match;
     char *actions;
     char *stage_hint;
+    char *ctrl_meter;
     const char *where;
 };
 
@@ -3353,13 +3355,14 @@  ovn_lflow_equal(const struct ovn_lflow *a, const struct ovn_lflow *b)
             && a->stage == b->stage
             && a->priority == b->priority
             && !strcmp(a->match, b->match)
-            && !strcmp(a->actions, b->actions));
+            && !strcmp(a->actions, b->actions)
+            && nullable_string_is_equal(a->ctrl_meter, b->ctrl_meter));
 }
 
 static void
 ovn_lflow_init(struct ovn_lflow *lflow, struct ovn_datapath *od,
                enum ovn_stage stage, uint16_t priority,
-               char *match, char *actions, char *stage_hint,
+               char *match, char *actions, char *ctrl_meter, char *stage_hint,
                const char *where)
 {
     lflow->od = od;
@@ -3368,6 +3371,7 @@  ovn_lflow_init(struct ovn_lflow *lflow, struct ovn_datapath *od,
     lflow->match = match;
     lflow->actions = actions;
     lflow->stage_hint = stage_hint;
+    lflow->ctrl_meter = ctrl_meter;
     lflow->where = where;
 }
 
@@ -3376,36 +3380,44 @@  static void
 ovn_lflow_add_at(struct hmap *lflow_map, struct ovn_datapath *od,
                  enum ovn_stage stage, uint16_t priority,
                  const char *match, const char *actions,
-                 const char *stage_hint, const char *where)
+                 const char *ctrl_meter, const char *stage_hint,
+                 const char *where)
 {
     ovs_assert(ovn_stage_to_datapath_type(stage) == ovn_datapath_get_type(od));
 
     struct ovn_lflow *lflow = xmalloc(sizeof *lflow);
     ovn_lflow_init(lflow, od, stage, priority,
                    xstrdup(match), xstrdup(actions),
+                   nullable_xstrdup(ctrl_meter),
                    nullable_xstrdup(stage_hint), where);
     hmap_insert(lflow_map, &lflow->hmap_node, ovn_lflow_hash(lflow));
 }
 
 /* Adds a row with the specified contents to the Logical_Flow table. */
 #define ovn_lflow_add_with_hint(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, \
-                                ACTIONS, STAGE_HINT) \
+                                ACTIONS, CTRL_METER, STAGE_HINT) \
     ovn_lflow_add_at(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS, \
-                     STAGE_HINT, OVS_SOURCE_LOCATOR)
+                     CTRL_METER, STAGE_HINT, OVS_SOURCE_LOCATOR)
 
 #define ovn_lflow_add(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS) \
     ovn_lflow_add_with_hint(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, \
-                            ACTIONS, NULL)
+                            ACTIONS, NULL, NULL)
+
+#define ovn_lflow_add_ctrl(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS, \
+                           CTRL_METER) \
+    ovn_lflow_add_with_hint(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, \
+                            ACTIONS, CTRL_METER, NULL)
 
 static struct ovn_lflow *
 ovn_lflow_find(struct hmap *lflows, struct ovn_datapath *od,
                enum ovn_stage stage, uint16_t priority,
-               const char *match, const char *actions, uint32_t hash)
+               const char *match, const char *actions, const char *ctrl_meter,
+               uint32_t hash)
 {
     struct ovn_lflow target;
     ovn_lflow_init(&target, od, stage, priority,
                    CONST_CAST(char *, match), CONST_CAST(char *, actions),
-                   NULL, NULL);
+                   CONST_CAST(char *, ctrl_meter), NULL, NULL);
 
     struct ovn_lflow *lflow;
     HMAP_FOR_EACH_WITH_HASH (lflow, hmap_node, hash, lflows) {
@@ -3424,6 +3436,7 @@  ovn_lflow_destroy(struct hmap *lflows, struct ovn_lflow *lflow)
         free(lflow->match);
         free(lflow->actions);
         free(lflow->stage_hint);
+        free(lflow->ctrl_meter);
         free(lflow);
     }
 }
@@ -4257,11 +4270,7 @@  build_empty_lb_event_flow(struct ovn_datapath *od, struct hmap *lflows,
     }
 
     struct ds match = DS_EMPTY_INITIALIZER;
-    char *meter = "", *action;
-
-    if (meter_groups && shash_find(meter_groups, "event-elb")) {
-        meter = "event-elb";
-    }
+    char *action;
 
     if (addr_family == AF_INET) {
         ds_put_format(&match, "ip4.dst == %s && %s",
@@ -4275,13 +4284,16 @@  build_empty_lb_event_flow(struct ovn_datapath *od, struct hmap *lflows,
                       port);
     }
     action = xasprintf("trigger_event(event = \"%s\", "
-                       "meter = \"%s\", vip = \"%s\", "
+                       "vip = \"%s\", "
                        "protocol = \"%s\", "
                        "load_balancer = \"" UUID_FMT "\");",
                        event_to_string(OVN_EVENT_EMPTY_LB_BACKENDS),
-                       meter, node->key, lb->protocol,
+                       node->key, lb->protocol,
                        UUID_ARGS(&lb->header_.uuid));
-    ovn_lflow_add(lflows, od, pl, 130, ds_cstr(&match), action);
+
+    const struct nbrec_copp *copp = (od->nbr ? od->nbr->copp : od->nbs->copp);
+    ovn_lflow_add_ctrl(lflows, od, pl, 130, ds_cstr(&match), action,
+                       get_copp_meter(COPP_EVENT_ELB, copp, meter_groups));
     ds_destroy(&match);
     free(action);
 }
@@ -4417,8 +4429,9 @@  build_acl_log(struct ds *actions, const struct nbrec_acl *acl)
 
 static void
 build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
-                       enum ovn_stage stage, struct nbrec_acl *acl,
-                       struct ds *extra_match, struct ds *extra_actions)
+                       struct shash *meter_groups, enum ovn_stage stage,
+                       struct nbrec_acl *acl, struct ds *extra_match,
+                       struct ds *extra_actions)
 {
     struct ds match = DS_EMPTY_INITIALIZER;
     struct ds actions = DS_EMPTY_INITIALIZER;
@@ -4434,8 +4447,11 @@  build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
                   "eth.dst <-> eth.src; ip4.dst <-> ip4.src; "
                   "tcp_reset { outport <-> inport; %s };",
                   ingress ? "output;" : "next(pipeline=ingress,table=0);");
-    ovn_lflow_add(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET + 10,
-                  ds_cstr(&match), ds_cstr(&actions));
+    ovn_lflow_add_ctrl(lflows, od, stage,
+                       acl->priority + OVN_ACL_PRI_OFFSET + 10,
+                       ds_cstr(&match), ds_cstr(&actions),
+                       get_copp_meter(COPP_TCP_RESET, od->nbs->copp,
+                                      meter_groups));
     ds_clear(&match);
     ds_clear(&actions);
     build_acl_log(&actions, acl);
@@ -4447,8 +4463,11 @@  build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
                   "eth.dst <-> eth.src; ip6.dst <-> ip6.src; "
                   "tcp_reset { outport <-> inport; %s };",
                   ingress ? "output;" : "next(pipeline=ingress,table=0);");
-    ovn_lflow_add(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET + 10,
-                  ds_cstr(&match), ds_cstr(&actions));
+    ovn_lflow_add_ctrl(lflows, od, stage,
+                       acl->priority + OVN_ACL_PRI_OFFSET + 10,
+                       ds_cstr(&match), ds_cstr(&actions),
+                       get_copp_meter(COPP_TCP_RESET, od->nbs->copp,
+                                      meter_groups));
 
     /* IP traffic */
     ds_clear(&match);
@@ -4465,8 +4484,10 @@  build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
                   "eth.dst <-> eth.src; ip4.dst <-> ip4.src; "
                   "icmp4 { outport <-> inport; %s };",
                   ingress ? "output;" : "next(pipeline=ingress,table=0);");
-    ovn_lflow_add(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET,
-                  ds_cstr(&match), ds_cstr(&actions));
+    ovn_lflow_add_ctrl(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET,
+                       ds_cstr(&match), ds_cstr(&actions),
+                       get_copp_meter(COPP_ICMP4_ERR, od->nbs->copp,
+                                      meter_groups));
     ds_clear(&match);
     ds_clear(&actions);
     build_acl_log(&actions, acl);
@@ -4475,22 +4496,25 @@  build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
     }
     ds_put_format(&match, "ip6 && (%s)", acl->match);
     if (extra_actions->length > 0) {
-        ds_put_format(&actions, "%s ", extra_actions->string);
+    ds_put_format(&actions, "%s ", extra_actions->string);
     }
     ds_put_format(&actions, "reg0 = 0; icmp6 { "
                   "eth.dst <-> eth.src; ip6.dst <-> ip6.src; "
                   "outport <-> inport; %s };",
                   ingress ? "output;" : "next(pipeline=ingress,table=0);");
-    ovn_lflow_add(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET,
-                  ds_cstr(&match), ds_cstr(&actions));
+    ovn_lflow_add_ctrl(lflows, od, stage, acl->priority + OVN_ACL_PRI_OFFSET,
+                       ds_cstr(&match), ds_cstr(&actions),
+                       get_copp_meter(COPP_ICMP6_ERR, od->nbs->copp,
+                                      meter_groups));
 
     ds_destroy(&match);
     ds_destroy(&actions);
 }
 
 static void
-consider_acl(struct hmap *lflows, struct ovn_datapath *od,
-             struct nbrec_acl *acl, bool has_stateful)
+consider_acl(struct hmap *lflows, struct shash *meter_groups,
+             struct ovn_datapath *od, struct nbrec_acl *acl,
+             bool has_stateful)
 {
     bool ingress = !strcmp(acl->direction, "from-lport") ? true :false;
     enum ovn_stage stage = ingress ? S_SWITCH_IN_ACL : S_SWITCH_OUT_ACL;
@@ -4510,7 +4534,7 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
             ovn_lflow_add_with_hint(lflows, od, stage,
                                     acl->priority + OVN_ACL_PRI_OFFSET,
                                     acl->match, ds_cstr(&actions),
-                                    stage_hint);
+                                    NULL, stage_hint);
             ds_destroy(&actions);
         } else {
             struct ds match = DS_EMPTY_INITIALIZER;
@@ -4539,7 +4563,7 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
                                     acl->priority + OVN_ACL_PRI_OFFSET,
                                     ds_cstr(&match),
                                     ds_cstr(&actions),
-                                    stage_hint);
+                                    NULL, stage_hint);
 
             /* Match on traffic in the request direction for an established
              * connection tracking entry that has not been marked for
@@ -4559,7 +4583,7 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
             ovn_lflow_add_with_hint(lflows, od, stage,
                                     acl->priority + OVN_ACL_PRI_OFFSET,
                                     ds_cstr(&match), ds_cstr(&actions),
-                                    stage_hint);
+                                    NULL, stage_hint);
 
             ds_destroy(&match);
             ds_destroy(&actions);
@@ -4579,8 +4603,8 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
             ds_put_cstr(&match,
                         "(!ct.est || (ct.est && ct_label.blocked == 1))");
             if (!strcmp(acl->action, "reject")) {
-                build_reject_acl_rules(od, lflows, stage, acl, &match,
-                                       &actions);
+                build_reject_acl_rules(od, lflows, meter_groups, stage, acl,
+                                       &match, &actions);
             } else {
                 ds_put_format(&match, " && (%s)", acl->match);
                 build_acl_log(&actions, acl);
@@ -4605,8 +4629,8 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
             ds_put_cstr(&match, "ct.est && ct_label.blocked == 0");
             ds_put_cstr(&actions, "ct_commit(ct_label=1/1); ");
             if (!strcmp(acl->action, "reject")) {
-                build_reject_acl_rules(od, lflows, stage, acl, &match,
-                                       &actions);
+                build_reject_acl_rules(od, lflows, meter_groups, stage, acl,
+                                       &match, &actions);
             } else {
                 ds_put_format(&match, " && (%s)", acl->match);
                 build_acl_log(&actions, acl);
@@ -4620,8 +4644,8 @@  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
              * so a "reject/drop" ACL is simply the "reject/drop"
              * logical flow action in all cases. */
             if (!strcmp(acl->action, "reject")) {
-                build_reject_acl_rules(od, lflows, stage, acl, &match,
-                                       &actions);
+                build_reject_acl_rules(od, lflows, meter_groups, stage, acl,
+                                       &match, &actions);
             } else {
                 build_acl_log(&actions, acl);
                 ds_put_cstr(&actions, "/* drop */");
@@ -4701,7 +4725,7 @@  build_port_group_lswitches(struct northd_context *ctx, struct hmap *pgs,
 
 static void
 build_acls(struct ovn_datapath *od, struct hmap *lflows,
-           struct hmap *port_groups)
+           struct hmap *port_groups, struct shash *meter_groups)
 {
     bool has_stateful = has_stateful_acl(od);
 
@@ -4802,13 +4826,14 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows,
     /* Ingress or Egress ACL Table (Various priorities). */
     for (size_t i = 0; i < od->nbs->n_acls; i++) {
         struct nbrec_acl *acl = od->nbs->acls[i];
-        consider_acl(lflows, od, acl, has_stateful);
+        consider_acl(lflows, meter_groups, od, acl, has_stateful);
     }
     struct ovn_port_group *pg;
     HMAP_FOR_EACH (pg, key_node, port_groups) {
         if (ovn_port_group_ls_find(pg, &od->nbs->header_.uuid)) {
             for (size_t i = 0; i < pg->nb_pg->n_acls; i++) {
-                consider_acl(lflows, od, pg->nb_pg->acls[i], has_stateful);
+                consider_acl(lflows, meter_groups, od, pg->nb_pg->acls[i],
+                             has_stateful);
             }
         }
     }
@@ -5143,6 +5168,273 @@  build_lrouter_groups(struct hmap *ports, struct ovs_list *lr_list)
     }
 }
 
+/*
+ * Ingress table 17: Priority 100 & 101 flows to punt IGMP packets to
+ * controller. If called for a specific 'op' it will install a per port
+ * flow only if there's a custom policy for IGMP in 'copp'.
+ */
+static void
+build_lswitch_igmp_flow(struct ovn_datapath *od, const struct ovn_port *op,
+                        uint16_t priority, bool flood_reports,
+                        const struct nbrec_copp *copp, struct hmap *lflows,
+                        const struct shash *meter_groups)
+{
+    const char *meter = get_copp_meter(COPP_IGMP, copp, meter_groups);
+
+    struct ds match_port = DS_EMPTY_INITIALIZER;
+    struct ds match      = DS_EMPTY_INITIALIZER;
+    struct ds actions    = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match_port, "inport == %s && ", op->json_key);
+    }
+
+    if (flood_reports) {
+        ds_put_cstr(&actions,
+                    "clone { "
+                        "outport = \""MC_MROUTER_STATIC"\"; "
+                        "output; "
+                    "};");
+    }
+    ds_put_cstr(&actions, "igmp;");
+
+    if (!op || meter) {
+        ds_put_format(&match, "%s ip4 && ip.proto == 2", ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
+                           ds_cstr(&match), ds_cstr(&actions), meter);
+    }
+
+    ds_destroy(&match_port);
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/*
+ * Table 14: DNS Lookup priority 100 & 101 flows. If called for a specific
+ * 'op' it will install a per port lookup flow only if there's a custom
+ * policy for DNS in 'copp'.
+ */
+static void
+build_lswitch_dns_lookup_flow(struct ovn_datapath *od,
+                              const struct ovn_port *op, uint16_t priority,
+                              const struct nbrec_copp *copp,
+                              struct hmap *lflows, struct shash *meter_groups)
+{
+    const char *meter = get_copp_meter(COPP_DNS, copp, meter_groups);
+
+    struct ds match_port = DS_EMPTY_INITIALIZER;
+    struct ds match      = DS_EMPTY_INITIALIZER;
+    struct ds actions    = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match_port, "inport == %s && ", op->json_key);
+    }
+
+    if (!op || meter) {
+        ds_put_format(&match, "%s udp.dst == 53", ds_cstr(&match_port));
+        ds_put_format(&actions,
+                      REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;");
+        ovn_lflow_add_ctrl(lflows, od, S_SWITCH_IN_DNS_LOOKUP, priority,
+                           ds_cstr(&match), ds_cstr(&actions), meter);
+    }
+
+    ds_destroy(&match_port);
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/*
+ * Table 15: DNS Response priority 100 flows.
+ */
+static void
+build_lswitch_dns_reply_flows(struct ovn_datapath *od, struct hmap *lflows)
+{
+    const char *match = "udp.dst == 53 && "REGBIT_DNS_LOOKUP_RESULT;
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100, match,
+                  "eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
+                  "udp.dst = udp.src; udp.src = 53; outport = inport; "
+                  "flags.loopback = 1; output;");
+
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100, match,
+                  "eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
+                  "udp.dst = udp.src; udp.src = 53; outport = inport; "
+                  "flags.loopback = 1; output;");
+}
+
+/*
+ * Ingress table 11: ARP/ND responder flows used to bind vports.
+ */
+static void
+build_lswitch_vport_bind_flows(struct ovn_port *op, struct hmap *lflows,
+                               struct hmap *ports,
+                               struct shash *meter_groups OVS_UNUSED)
+{
+    /* Handle
+     *  - GARPs for virtual ip which belong to a logical port
+     *    of type 'virtual' and bind that port.
+     *
+     *  - ARP reply from the virtual ip which belongs to a logical
+     *    port of type 'virtual' and bind that port.
+     * */
+    ovs_be32 ip;
+    const char *virtual_ip = smap_get(&op->nbsp->options,
+                                      "virtual-ip");
+    const char *virtual_parents = smap_get(&op->nbsp->options,
+                                           "virtual-parents");
+    if (!virtual_ip || !virtual_parents ||
+        !ip_parse(virtual_ip, &ip)) {
+        return;
+    }
+
+    struct ds match   = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    char *tokstr = xstrdup(virtual_parents);
+    char *save_ptr = NULL;
+    char *vparent;
+    for (vparent = strtok_r(tokstr, ",", &save_ptr); vparent != NULL;
+         vparent = strtok_r(NULL, ",", &save_ptr)) {
+        struct ovn_port *vp = ovn_port_find(ports, vparent);
+        if (!vp || vp->od != op->od) {
+            /* vparent name should be valid and it should belong
+             * to the same logical switch. */
+            continue;
+        }
+
+        ds_clear(&match);
+        ds_put_format(&match, "inport == \"%s\" && "
+                      "!is_chassis_resident(%s) && "
+                      "((arp.op == 1 && arp.spa == %s && "
+                      "arp.tpa == %s) || (arp.op == 2 && "
+                      "arp.spa == %s))",
+                      vparent, op->json_key, virtual_ip, virtual_ip,
+                      virtual_ip);
+        ds_clear(&actions);
+        ds_put_format(&actions,
+                      "bind_vport(%s, inport); "
+                      "next;",
+                      op->json_key);
+        ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+                      ds_cstr(&match), ds_cstr(&actions));
+    }
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+    free(tokstr);
+}
+
+/*
+ * Ingress table 11: ARP/ND responder flows (Priority 50 & 100) for replying
+ * to ARP requests.
+ */
+static void
+build_lswitch_arp_responder(struct ovn_port *op, struct hmap *lflows,
+                            struct shash *meter_groups OVS_UNUSED)
+{
+    struct ds match   = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    for (size_t i = 0; i < op->n_lsp_addrs; i++) {
+        for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
+            ds_clear(&match);
+            ds_put_format(&match, "arp.tpa == %s && arp.op == 1",
+                          op->lsp_addrs[i].ipv4_addrs[j].addr_s);
+            ds_clear(&actions);
+            ds_put_format(&actions,
+                          "eth.dst = eth.src; "
+                          "eth.src = %s; "
+                          "arp.op = 2; /* ARP reply */ "
+                          "arp.tha = arp.sha; "
+                          "arp.sha = %s; "
+                          "arp.tpa = arp.spa; "
+                          "arp.spa = %s; "
+                          "outport = inport; "
+                          "flags.loopback = 1; "
+                          "output;",
+                          op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
+                          op->lsp_addrs[i].ipv4_addrs[j].addr_s);
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+                          ds_cstr(&match), ds_cstr(&actions));
+
+            /* Do not reply to an ARP request from the port that owns
+             * the address (otherwise a DHCP client that ARPs to check
+             * for a duplicate address will fail).  Instead, forward
+             * it the usual way.
+             *
+             * (Another alternative would be to simply drop the packet.
+             * If everything is working as it is configured, then this
+             * would produce equivalent results, since no one should
+             * reply to the request.  But ARPing for one's own IP
+             * address is intended to detect situations where the
+             * network is not working as configured, so dropping the
+             * request would frustrate that intent.) */
+            ds_put_format(&match, " && inport == %s", op->json_key);
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+                          ds_cstr(&match), "next;");
+        }
+    }
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/*
+ * Ingress table 11: ARP/ND responder flows (Priority 50 & 100) for replying
+ * to NS requests.
+ */
+static void
+build_lswitch_nd_responder(struct ovn_port *op, struct hmap *lflows,
+                           struct shash *meter_groups)
+{
+    const char *meter = get_port_copp_meter(COPP_ND_NA, op->nbsp->copp,
+                                            op->od->nbs->copp, meter_groups);
+    struct ds match   = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    for (size_t i = 0; i < op->n_lsp_addrs; i++) {
+        /* For ND solicitations, we need to listen for both the
+         * unicast IPv6 address and its all-nodes multicast address,
+         * but always respond with the unicast IPv6 address. */
+        for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
+                          op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                          op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
+                          op->lsp_addrs[i].ipv6_addrs[j].addr_s);
+
+            ds_clear(&actions);
+            ds_put_format(&actions,
+                          "%s { "
+                          "eth.src = %s; "
+                          "ip6.src = %s; "
+                          "nd.target = %s; "
+                          "nd.tll = %s; "
+                          "outport = inport; "
+                          "flags.loopback = 1; "
+                          "output; "
+                          "};",
+                          !strcmp(op->nbsp->type, "router") ?
+                                "nd_na_router" : "nd_na",
+                          op->lsp_addrs[i].ea_s,
+                          op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                          op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                          op->lsp_addrs[i].ea_s);
+            ovn_lflow_add_ctrl(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+                               ds_cstr(&match), ds_cstr(&actions), meter);
+
+            /* Do not reply to a solicitation from the port that owns
+             * the address (otherwise DAD detection will fail). */
+            ds_put_format(&match, " && inport == %s", op->json_key);
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+                          ds_cstr(&match), "next;");
+        }
+    }
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
 static void
 build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                     struct hmap *port_groups, struct hmap *lflows,
@@ -5166,7 +5458,7 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         build_pre_acls(od, lflows);
         build_pre_lb(od, lflows, meter_groups);
         build_pre_stateful(od, lflows);
-        build_acls(od, lflows, port_groups);
+        build_acls(od, lflows, port_groups, meter_groups);
         build_qos(od, lflows);
         build_lb(od, lflows);
         build_stateful(od, lflows);
@@ -5219,53 +5511,8 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         if (!strcmp(op->nbsp->type, "virtual")) {
-            /* Handle
-             *  - GARPs for virtual ip which belongs to a logical port
-             *    of type 'virtual' and bind that port.
-             *
-             *  - ARP reply from the virtual ip which belongs to a logical
-             *    port of type 'virtual' and bind that port.
-             * */
-            ovs_be32 ip;
-            const char *virtual_ip = smap_get(&op->nbsp->options,
-                                              "virtual-ip");
-            const char *virtual_parents = smap_get(&op->nbsp->options,
-                                                   "virtual-parents");
-            if (!virtual_ip || !virtual_parents ||
-                !ip_parse(virtual_ip, &ip)) {
-                continue;
-            }
-
-            char *tokstr = xstrdup(virtual_parents);
-            char *save_ptr = NULL;
-            char *vparent;
-            for (vparent = strtok_r(tokstr, ",", &save_ptr); vparent != NULL;
-                 vparent = strtok_r(NULL, ",", &save_ptr)) {
-                struct ovn_port *vp = ovn_port_find(ports, vparent);
-                if (!vp || vp->od != op->od) {
-                    /* vparent name should be valid and it should belong
-                     * to the same logical switch. */
-                    continue;
-                }
-
-                ds_clear(&match);
-                ds_put_format(&match, "inport == \"%s\" && "
-                              "!is_chassis_resident(%s) && "
-                              "((arp.op == 1 && arp.spa == %s && "
-                              "arp.tpa == %s) || (arp.op == 2 && "
-                              "arp.spa == %s))",
-                              vparent, op->json_key, virtual_ip, virtual_ip,
-                              virtual_ip);
-                ds_clear(&actions);
-                ds_put_format(&actions,
-                    "bind_vport(%s, inport); "
-                    "next;",
-                    op->json_key);
-                ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
-                              ds_cstr(&match), ds_cstr(&actions));
-            }
-
-            free(tokstr);
+            /* Also handle binging of virtual ports. */
+            build_lswitch_vport_bind_flows(op, lflows, ports, meter_groups);
         } else {
             /*
              * Add ARP/ND reply flows if either the
@@ -5282,83 +5529,8 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                 continue;
             }
 
-            for (size_t i = 0; i < op->n_lsp_addrs; i++) {
-                for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
-                    ds_clear(&match);
-                    ds_put_format(&match, "arp.tpa == %s && arp.op == 1",
-                                op->lsp_addrs[i].ipv4_addrs[j].addr_s);
-                    ds_clear(&actions);
-                    ds_put_format(&actions,
-                        "eth.dst = eth.src; "
-                        "eth.src = %s; "
-                        "arp.op = 2; /* ARP reply */ "
-                        "arp.tha = arp.sha; "
-                        "arp.sha = %s; "
-                        "arp.tpa = arp.spa; "
-                        "arp.spa = %s; "
-                        "outport = inport; "
-                        "flags.loopback = 1; "
-                        "output;",
-                        op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
-                        op->lsp_addrs[i].ipv4_addrs[j].addr_s);
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
-                                ds_cstr(&match), ds_cstr(&actions));
-
-                    /* Do not reply to an ARP request from the port that owns
-                     * the address (otherwise a DHCP client that ARPs to check
-                     * for a duplicate address will fail).  Instead, forward
-                     * it the usual way.
-                     *
-                     * (Another alternative would be to simply drop the packet.
-                     * If everything is working as it is configured, then this
-                     * would produce equivalent results, since no one should
-                     * reply to the request.  But ARPing for one's own IP
-                     * address is intended to detect situations where the
-                     * network is not working as configured, so dropping the
-                     * request would frustrate that intent.) */
-                    ds_put_format(&match, " && inport == %s", op->json_key);
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
-                                ds_cstr(&match), "next;");
-                }
-
-                /* For ND solicitations, we need to listen for both the
-                 * unicast IPv6 address and its all-nodes multicast address,
-                 * but always respond with the unicast IPv6 address. */
-                for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
-                    ds_clear(&match);
-                    ds_put_format(&match,
-                            "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s);
-
-                    ds_clear(&actions);
-                    ds_put_format(&actions,
-                            "%s { "
-                            "eth.src = %s; "
-                            "ip6.src = %s; "
-                            "nd.target = %s; "
-                            "nd.tll = %s; "
-                            "outport = inport; "
-                            "flags.loopback = 1; "
-                            "output; "
-                            "};",
-                            !strcmp(op->nbsp->type, "router") ?
-                                "nd_na_router" : "nd_na",
-                            op->lsp_addrs[i].ea_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ea_s);
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
-                                ds_cstr(&match), ds_cstr(&actions));
-
-                    /* Do not reply to a solicitation from the port that owns
-                     * the address (otherwise DAD detection will fail). */
-                    ds_put_format(&match, " && inport == %s", op->json_key);
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
-                                ds_cstr(&match), "next;");
-                }
-            }
+            build_lswitch_arp_responder(op, lflows, meter_groups);
+            build_lswitch_nd_responder(op, lflows, meter_groups);
         }
     }
 
@@ -5421,9 +5593,14 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                                       op->json_key);
                     }
 
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS,
-                                  100, ds_cstr(&match),
-                                  ds_cstr(&options_action));
+                    ovn_lflow_add_ctrl(lflows, op->od,
+                                       S_SWITCH_IN_DHCP_OPTIONS,
+                                       100, ds_cstr(&match),
+                                       ds_cstr(&options_action),
+                                       get_port_copp_meter(COPP_DHCPV4_OPTS,
+                                                           op->nbsp->copp,
+                                                           op->od->nbs->copp,
+                                                           meter_groups));
                     ds_clear(&match);
                     /* Allow ip4.src = OFFER_IP and
                      * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
@@ -5495,8 +5672,14 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                                       op->json_key);
                     }
 
-                    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                  ds_cstr(&match), ds_cstr(&options_action));
+                    ovn_lflow_add_ctrl(lflows, op->od,
+                                       S_SWITCH_IN_DHCP_OPTIONS, 100,
+                                       ds_cstr(&match),
+                                       ds_cstr(&options_action),
+                                       get_port_copp_meter(COPP_DHCPV6_OPTS,
+                                                           op->nbsp->copp,
+                                                           op->od->nbs->copp,
+                                                           meter_groups));
 
                     /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
                      * put_dhcpv6_opts action is successful */
@@ -5519,28 +5702,26 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
            continue;
         }
 
-        struct ds action = DS_EMPTY_INITIALIZER;
+        /* Build flows that punt DNS queries to controller for this datapath.
+         * Priority 100. A flow with higher priority might be added for one
+         * or more ports in the logical switch if a different control plane
+         * protection policy is defined per port. */
+        build_lswitch_dns_lookup_flow(od, NULL, 100, od->nbs->copp, lflows,
+                                      meter_groups);
 
-        ds_clear(&match);
-        ds_put_cstr(&match, "udp.dst == 53");
-        ds_put_format(&action,
-                      REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 100,
-                      ds_cstr(&match), ds_cstr(&action));
-        ds_clear(&action);
-        ds_put_cstr(&match, " && "REGBIT_DNS_LOOKUP_RESULT);
-        ds_put_format(&action, "eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
-                      "udp.dst = udp.src; udp.src = 53; outport = inport; "
-                      "flags.loopback = 1; output;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
-                      ds_cstr(&match), ds_cstr(&action));
-        ds_clear(&action);
-        ds_put_format(&action, "eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
-                      "udp.dst = udp.src; udp.src = 53; outport = inport; "
-                      "flags.loopback = 1; output;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
-                      ds_cstr(&match), ds_cstr(&action));
-        ds_destroy(&action);
+        /* Build DNS response flows. */
+        build_lswitch_dns_reply_flows(od, lflows);
+    }
+
+    /* Logical switch ingress table 14: DNS lookup (per port). */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (!op->nbsp || !ls_has_dns_records(op->od->nbs)) {
+            continue;
+        }
+
+        /* Build per-port flows that punt DNS queries to controller. */
+        build_lswitch_dns_lookup_flow(od, op, 101, op->nbsp->copp, lflows,
+                                      meter_groups);
     }
 
     /* Ingress table 12 and 13: DHCP options and response, by default goto
@@ -5623,18 +5804,11 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         struct mcast_switch_info *mcast_sw_info = &od->mcast_info.sw;
 
         if (mcast_sw_info->enabled) {
-            ds_clear(&actions);
-            if (mcast_sw_info->flood_reports) {
-                ds_put_cstr(&actions,
-                            "clone { "
-                                "outport = \""MC_MROUTER_STATIC"\"; "
-                                "output; "
-                            "};");
-            }
             ds_put_cstr(&actions, "igmp;");
             /* Punt IGMP traffic to controller. */
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, 100,
-                          "ip4 && ip.proto == 2", ds_cstr(&actions));
+            build_lswitch_igmp_flow(od, NULL, 100,
+                                    mcast_sw_info->flood_reports,
+                                    od->nbs->copp, lflows, meter_groups);
 
             /* Flood all IP multicast traffic destined to 224.0.0.X to all
              * ports - RFC 4541, section 2.1.2, item 2.
@@ -5721,7 +5895,20 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
 
     /* Ingress table 17: Destination lookup, unicast handling (priority 50), */
     HMAP_FOR_EACH (op, key_node, ports) {
-        if (!op->nbsp || lsp_is_external(op->nbsp)) {
+        if (!op->nbsp) {
+            continue;
+        }
+
+        struct mcast_switch_info *mcast_sw_info = &op->od->mcast_info.sw;
+        if (mcast_sw_info->enabled) {
+            /* Punt IGMP traffic to controller. Higher priority (101) flow
+             * if there's a custom copp policy for this port for IGMP.
+             */
+            build_lswitch_igmp_flow(od, op, 101, mcast_sw_info->flood_reports,
+                                    op->nbsp->copp, lflows, meter_groups);
+        }
+
+        if (lsp_is_external(op->nbsp)) {
             continue;
         }
 
@@ -6486,6 +6673,114 @@  copy_ra_to_sb(struct ovn_port *op, const char *address_mode)
     smap_destroy(&options);
 }
 
+/* Logical router ingress table 2 LEARN_NEIGHBOR from ARP/ND packets. If
+ * called for a specific 'op' it will install per port flows only if there's
+ * a custom policy for ARP/ND in 'copp'.
+ */
+static void
+build_lrouter_learn_neigh_flows(struct ovn_datapath *od,
+                                const struct ovn_port *op, uint16_t priority,
+                                const struct nbrec_copp *copp,
+                                struct hmap *lflows,
+                                const struct shash *meter_groups)
+{
+    const char *meter;
+
+    struct ds match_port = DS_EMPTY_INITIALIZER;
+    struct ds match      = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match_port, "inport == %s && ", op->json_key);
+    }
+
+    meter = get_copp_meter(COPP_ARP, copp, meter_groups);
+    if (!op || meter) {
+        ds_put_format(&match, "%s arp", ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, priority,
+                           ds_cstr(&match),
+                           "put_arp(inport, arp.spa, arp.sha); next;",
+                           meter);
+    }
+
+    meter = get_copp_meter(COPP_ND_NA, copp, meter_groups);
+    if (!op || meter) {
+        ds_clear(&match);
+        ds_put_format(&match, "%s nd_na", ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, priority,
+                           ds_cstr(&match),
+                           "put_nd(inport, nd.target, nd.tll); next;",
+                           meter);
+    }
+
+    meter = get_copp_meter(COPP_ND_NS, copp, meter_groups);
+    if (!op || meter) {
+        ds_clear(&match);
+        ds_put_format(&match, "%s nd_ns", ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, priority,
+                           ds_cstr(&match),
+                           "put_nd(inport, ip6.src, nd.sll); next;",
+                           meter);
+    }
+
+    ds_destroy(&match_port);
+    ds_destroy(&match);
+}
+
+/*
+ * Logical router ingress table 13: ARP/NS request flows.  If called for a
+ * specific 'op' it will install per port flows only if there's a custom
+ * policy for ARP/NS resolve in 'copp'.
+ */
+static void
+build_lrouter_arp_nd_ns_flows(struct ovn_datapath *od,
+                              const struct ovn_port *op, uint16_t priority,
+                              const struct nbrec_copp *copp,
+                              struct hmap *lflows,
+                              const struct shash *meter_groups)
+{
+    const char *meter;
+
+    struct ds match_port = DS_EMPTY_INITIALIZER;
+    struct ds match      = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match_port, "inport == %s && ", op->json_key);
+    }
+
+    meter = get_copp_meter(COPP_ARP_RESOLVE, copp, meter_groups);
+    if (!op || meter) {
+        ds_put_format(&match, "%s eth.dst == 00:00:00:00:00:00",
+                      ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_ARP_REQUEST, priority,
+                           ds_cstr(&match),
+                           "arp { "
+                           "eth.dst = ff:ff:ff:ff:ff:ff; "
+                           "arp.spa = reg1; "
+                           "arp.tpa = reg0; "
+                           "arp.op = 1; " /* ARP request */
+                           "output; "
+                           "};",
+                           meter);
+    }
+
+    meter = get_copp_meter(COPP_ND_NS_RESOLVE, copp, meter_groups);
+    if (!op || meter) {
+        ds_clear(&match);
+        ds_put_format(&match, "%s eth.dst == 00:00:00:00:00:00",
+                      ds_cstr(&match_port));
+        ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_ARP_REQUEST, priority,
+                           ds_cstr(&match),
+                           "nd_ns { "
+                           "nd.target = xxreg0; "
+                           "output; "
+                           "};",
+                           meter);
+    }
+
+    ds_destroy(&match_port);
+    ds_destroy(&match);
+}
+
 static void
 build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     struct hmap *lflows, struct shash *meter_groups)
@@ -6595,20 +6890,14 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 0, "1",
                       REGBIT_SKIP_LOOKUP_NEIGHBOR" = 1; next;");
 
-        /* Flows for LEARN_NEIGHBOR. */
         /* Skip Neighbor learning if not required. */
         ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 100,
                       REGBIT_SKIP_LOOKUP_NEIGHBOR" == 1 || "
                       REGBIT_LOOKUP_NEIGHBOR_RESULT" == 1", "next;");
 
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "arp", "put_arp(inport, arp.spa, arp.sha); next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "nd_na", "put_nd(inport, nd.target, nd.tll); next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "nd_ns", "put_nd(inport, ip6.src, nd.sll); next;");
+        /* Flows for LEARN_NEIGHBOR. */
+        build_lrouter_learn_neigh_flows(od, NULL, 90, od->nbr->copp, lflows,
+                                        meter_groups);
     }
 
     HMAP_FOR_EACH (op, key_node, ports) {
@@ -6616,6 +6905,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
+        /* Port specific flows for LEARN_NEIGHBOR. Give higher priority to
+         * per port flows as they have different metering configurations.
+         */
+        build_lrouter_learn_neigh_flows(op->od, op, 91,
+                                        op->nbrp->copp, lflows, meter_groups);
+
         /* Check if we need to learn mac-binding from ARP requests. */
         for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
             ds_clear(&match);
@@ -6744,8 +7039,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           "ip.ttl = 255; "
                           "next; };",
                           op->lrp_networks.ipv4_addrs[i].addr_s);
-            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
-                          ds_cstr(&match), ds_cstr(&actions));
+            ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
+                               ds_cstr(&match), ds_cstr(&actions),
+                               get_port_copp_meter(COPP_ICMP4_ERR,
+                                                   op->nbrp->copp,
+                                                   op->od->nbr->copp,
+                                                   meter_groups));
         }
 
         /* ARP reply.  These flows reply to ARP requests for the router's own
@@ -6988,8 +7287,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                                      "icmp4.type = 3; "
                                      "icmp4.code = 3; "
                                      "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
-                              ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_ICMP4_ERR,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
 
                 ds_clear(&match);
                 ds_put_format(&match,
@@ -6999,8 +7302,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                          "eth.dst <-> eth.src; "
                          "ip4.dst <-> ip4.src; "
                          "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
-                              ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_TCP_RESET,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
 
                 ds_clear(&match);
                 ds_put_format(&match,
@@ -7013,8 +7320,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                          "icmp4.type = 3; "
                          "icmp4.code = 2; "
                          "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 70,
-                              ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 70,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_ICMP4_ERR,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
             }
         }
 
@@ -7149,8 +7460,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                                      "eth.dst <-> eth.src; "
                                      "ip6.dst <-> ip6.src; "
                                      "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
-                          ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_TCP_RESET,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
 
                 ds_clear(&match);
                 ds_put_format(&match,
@@ -7163,8 +7478,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                          "icmp6.type = 1; "
                          "icmp6.code = 4; "
                          "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
-                              ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_ICMP6_ERR,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
 
                 ds_clear(&match);
                 ds_put_format(&match,
@@ -7177,8 +7496,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                          "icmp6.type = 1; "
                          "icmp6.code = 3; "
                          "next; };";
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 70,
-                              ds_cstr(&match), action);
+                ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 70,
+                                   ds_cstr(&match), action,
+                                   get_port_copp_meter(COPP_ICMP6_ERR,
+                                                       op->nbrp->copp,
+                                                       op->od->nbr->copp,
+                                                       meter_groups));
             }
         }
 
@@ -7209,8 +7532,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           "icmp6.code = 0; /* TTL exceeded in transit */ "
                           "next; };",
                           op->lrp_networks.ipv6_addrs[i].addr_s);
-            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
-                          ds_cstr(&match), ds_cstr(&actions));
+            ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
+                               ds_cstr(&match), ds_cstr(&actions),
+                               get_port_copp_meter(COPP_ICMP6_ERR,
+                                                   op->nbrp->copp,
+                                                   op->od->nbr->copp,
+                                                   meter_groups));
         }
     }
 
@@ -7822,9 +8149,15 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         if (add_rs_response_flow) {
+
             ds_put_cstr(&actions, "); next;");
-            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_ND_RA_OPTIONS, 50,
-                          ds_cstr(&match), ds_cstr(&actions));
+            ovn_lflow_add_ctrl(lflows, op->od, S_ROUTER_IN_ND_RA_OPTIONS, 50,
+                               ds_cstr(&match), ds_cstr(&actions),
+                               get_port_copp_meter(COPP_ND_RA_OPTS,
+                                                   op->nbrp->copp,
+                                                   op->od->nbr->copp,
+                                                   meter_groups));
+
             ds_clear(&actions);
             ds_clear(&match);
             ds_put_format(&match, "inport == %s && ip6.dst == ff02::2 && "
@@ -8383,8 +8716,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     rp->lrp_networks.ea_s,
                     rp->lrp_networks.ipv4_addrs[0].addr_s,
                     gw_mtu - 18);
-                ovn_lflow_add(lflows, od, S_ROUTER_IN_LARGER_PKTS, 50,
-                              ds_cstr(&match), ds_cstr(&actions));
+                ovn_lflow_add_ctrl(lflows, od, S_ROUTER_IN_LARGER_PKTS, 50,
+                                   ds_cstr(&match), ds_cstr(&actions),
+                                   get_port_copp_meter(COPP_ICMP4_ERR,
+                                                       rp->nbrp->copp,
+                                                       rp->od->nbr->copp,
+                                                       meter_groups));
             }
         }
     }
@@ -8478,24 +8815,25 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           ds_cstr(&match), ds_cstr(&actions));
         }
 
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
-                      "eth.dst == 00:00:00:00:00:00",
-                      "arp { "
-                      "eth.dst = ff:ff:ff:ff:ff:ff; "
-                      "arp.spa = reg1; "
-                      "arp.tpa = reg0; "
-                      "arp.op = 1; " /* ARP request */
-                      "output; "
-                      "};");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
-                      "eth.dst == 00:00:00:00:00:00",
-                      "nd_ns { "
-                      "nd.target = xxreg0; "
-                      "output; "
-                      "};");
+        /* Per logical-router flows to generate ARP/NS requests. */
+        build_lrouter_arp_nd_ns_flows(od, NULL, 100, od->nbr->copp, lflows,
+                                      meter_groups);
         ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 0, "1", "output;");
     }
 
+    /*
+     * Local router ingress table 13: ARP request.
+     */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (!op->nbrp) {
+            continue;
+        }
+
+        /* Per logical-router-port flows to generate ARP/NS requests. */
+        build_lrouter_arp_nd_ns_flows(op->od, op, 101, op->nbrp->copp,
+                                      lflows, meter_groups);
+    }
+
     /* Logical router egress table 1: Delivery (priority 100-110).
      *
      * Priority 100 rules deliver packets to enabled logical ports.
@@ -8575,7 +8913,8 @@  build_lflows(struct northd_context *ctx, struct hmap *datapaths,
             = !strcmp(sbflow->pipeline, "ingress") ? P_IN : P_OUT;
         struct ovn_lflow *lflow = ovn_lflow_find(
             &lflows, od, ovn_stage_build(dp_type, pipeline, sbflow->table_id),
-            sbflow->priority, sbflow->match, sbflow->actions, sbflow->hash);
+            sbflow->priority, sbflow->match, sbflow->actions,
+            sbflow->controller_meter, sbflow->hash);
         if (lflow) {
             ovn_lflow_destroy(&lflows, lflow);
         } else {
@@ -8594,6 +8933,7 @@  build_lflows(struct northd_context *ctx, struct hmap *datapaths,
         sbrec_logical_flow_set_priority(sbflow, lflow->priority);
         sbrec_logical_flow_set_match(sbflow, lflow->match);
         sbrec_logical_flow_set_actions(sbflow, lflow->actions);
+        sbrec_logical_flow_set_controller_meter(sbflow, lflow->ctrl_meter);
 
         /* Trim the source locator lflow->where, which looks something like
          * "ovn/northd/ovn-northd.c:1234", down to just the part following the
@@ -10004,6 +10344,8 @@  main(int argc, char *argv[])
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_logical_flow_col_priority);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_logical_flow_col_match);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_logical_flow_col_actions);
+    add_column_noalert(ovnsb_idl_loop.idl,
+                       &sbrec_logical_flow_col_controller_meter);
 
     ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_multicast_group);
     add_column_noalert(ovnsb_idl_loop.idl,
diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index 2c87cbb..ab7272d 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
-    "version": "5.16.0",
-    "cksum": "923459061 23095",
+    "version": "5.17.0",
+    "cksum": "204873632 24164",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -26,6 +26,14 @@ 
                 "ipsec": {"type": "boolean"}},
             "maxRows": 1,
             "isRoot": true},
+        "Copp": {
+            "columns": {
+                "meters": {
+                    "type": {"key": "string",
+                             "value": "string",
+                             "min": 0,
+                             "max": "unlimited"}}},
+            "isRoot": true},
         "Logical_Switch": {
             "columns": {
                 "name": {"type": "string"},
@@ -54,6 +62,9 @@ 
                                          "refType": "weak"},
                                   "min": 0,
                                   "max": "unlimited"}},
+                "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp",
+                                          "refType": "weak"},
+                                  "min": 0, "max": 1}},
                 "other_config": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}},
@@ -108,6 +119,9 @@ 
                                      "refType": "strong"},
                              "min": 0,
                              "max": 1}},
+                "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp",
+                                          "refType": "weak"},
+                                  "min": 0, "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
@@ -265,6 +279,9 @@ 
                                                   "refType": "weak"},
                                            "min": 0,
                                            "max": "unlimited"}},
+                "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp",
+                                          "refType": "weak"},
+                                  "min": 0, "max": 1}},
                 "options": {
                      "type": {"key": "string",
                               "value": "string",
@@ -303,6 +320,9 @@ 
                 "ipv6_ra_configs": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}},
+                "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp",
+                                          "refType": "weak"},
+                                  "min": 0, "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 1504f8f..c844f2d 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -147,6 +147,65 @@ 
     </group>
   </table>
 
+  <table name="Copp" title="Control plane protection">
+    <p>
+      This table is used to define control plane protection policies, i.e.,
+      associate entries from table <ref table="Meter"/> to control protocol
+      names.
+    </p>
+    <column name="meters" key="arp">
+      Rate limiting meter for ARP packets (request/reply) used for learning
+      neighbors.
+    </column>
+    <column name="meters" key="arp-resolve">
+      Rate limiting meter for packets that require resolving the next-hop
+      (through ARP).
+    </column>
+    <column name="meters" key="dhcpv4-opts">
+      Rate limiting meter for packets that require adding DHCPv4 options.
+    </column>
+    <column name="meters" key="dhcpv6-opts">
+      Rate limiting meter for packets that require adding DHCPv6 options.
+    </column>
+    <column name="meters" key="dns">
+      Rate limiting meter for DNS query packets that need to be replied to.
+    </column>
+    <column name="meters" key="event-elb">
+      Rate limiting meter for empty load balancer events.
+    </column>
+    <column name="meters" key="icmp4-error">
+      Rate limiting meter for packets that require replying with an ICMP
+      error.
+    </column>
+    <column name="meters" key="icmp6-error">
+      Rate limiting meter for packets that require replying with an ICMPv6
+      error.
+    </column>
+    <column name="meters" key="igmp">
+      Rate limiting meter for IGMP packets.
+    </column>
+    <column name="meters" key="nd-na">
+      Rate limiting meter for ND neighbor advertisement packets used for
+      learning neighbors.
+    </column>
+    <column name="meters" key="nd-ns">
+      Rate limiting meter for ND neighbor solicitation packets used for
+      learning neighbors.
+    </column>
+    <column name="meters" key="nd-ns-resolve">
+      Rate limiting meter for packets that require resolving the next-hop
+      (through ND).
+    </column>
+    <column name="meters" key="nd-ra-opts">
+      Rate limiting meter for packets that require adding ND router
+      advertisement options.
+    </column>
+    <column name="meters" key="tcp-reset">
+      Rate limiting meter for packets that require replying with TCP RST
+      packet.
+    </column>
+  </table>
+
   <table name="Logical_Switch" title="L2 logical switch">
     <p>
       Each row represents one L2 logical switch.
@@ -347,6 +406,14 @@ 
       </column>
     </group>
 
+    <column name="copp">
+      <p>
+        The control plane protection policy from table <ref table="Copp"/>
+        used for metering packets sent to <code>ovn-controller</code> from
+        ports of this logical switch.
+      </p>
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
@@ -1136,6 +1203,14 @@ 
       </column>
     </group>
 
+    <column name="copp">
+      <p>
+        The control plane protection policy from table <ref table="Copp"/>
+        used for metering packets sent to <code>ovn-controller</code> from
+        this logical port.
+      </p>
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         <p>
@@ -1563,6 +1638,14 @@ 
       </column>
     </group>
 
+    <column name="copp">
+      <p>
+        The control plane protection policy from table <ref table="Copp"/>
+        used for metering packets sent to <code>ovn-controller</code> from
+        logical ports of this router.
+      </p>
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
@@ -2077,6 +2160,14 @@ 
       </column>
     </group>
 
+    <column name="copp">
+      <p>
+        The control plane protection policy from table <ref table="Copp"/>
+        used for metering packets sent to <code>ovn-controller</code> from
+        this logical port.
+      </p>
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index 5c013b1..5895d89 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Southbound",
-    "version": "2.5.0",
-    "cksum": "1257419092 20387",
+    "version": "2.6.0",
+    "cksum": "3178216375 20519",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -86,6 +86,8 @@ 
                                               "maxInteger": 65535}}},
                 "match": {"type": "string"},
                 "actions": {"type": "string"},
+                "controller_meter": {"type": {"key": {"type": "string"},
+                                     "min": 0, "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn-sb.xml b/ovn-sb.xml
index e5fb51a..58b3bfd 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2100,6 +2100,12 @@  tcp.flags = RST;
       </dl>
     </column>
 
+    <column name="controller_meter">
+      The name of the meter in table <ref table="Meter"/> to be used for
+      all packets that the logical flow might send to
+      <code>ovn-controller</code>.
+    </column>
+
     <column name="external_ids" key="stage-name">
       Human-readable name for this flow's stage in the pipeline.
     </column>
diff --git a/tests/ovn.at b/tests/ovn.at
index 0b292a3..aa30a64 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2540,8 +2540,7 @@  trigger_event(event = "empty_lb_backends", vip = "10.0.0.1:80", protocol = "tcp"
     encodes as controller(userdata=00.00.00.0f.00.00.00.00.00.00.00.00.00.01.00.0b.31.30.2e.30.2e.30.2e.31.3a.38.30.00.02.00.03.74.63.70.00.03.00.24.31.32.33.34.35.36.37.38.2d.61.62.63.64.2d.39.38.37.36.2d.66.65.64.63.2d.31.31.31.31.39.66.38.65.37.64.36.63)
 
 trigger_event(event = "empty_lb_backends", meter="event-elb" vip = "10.0.0.1:80", protocol = "tcp", load_balancer = "12345678-abcd-9876-fedc-11119f8e7d6c");
-    formats as trigger_event(event = "empty_lb_backends", meter = "event-elb", vip = "10.0.0.1:80", protocol = "tcp", load_balancer = "12345678-abcd-9876-fedc-11119f8e7d6c");
-    encodes as controller(userdata=00.00.00.0f.00.00.00.00.00.00.00.00.00.01.00.0b.31.30.2e.30.2e.30.2e.31.3a.38.30.00.02.00.03.74.63.70.00.03.00.24.31.32.33.34.35.36.37.38.2d.61.62.63.64.2d.39.38.37.36.2d.66.65.64.63.2d.31.31.31.31.39.66.38.65.37.64.36.63,meter_id=5)
+    Syntax error at `meter' expecting empty_lb_backends option name.
 
 # Testing invalid vip results in extra error messages from socket-util.c
 trigger_event(event = "empty_lb_backends", vip = "10.0.0.1:80", protocol = "sctp", load_balancer = "12345678-abcd-9876-fedc-11119f8e7d6c");
diff --git a/utilities/ovn-nbctl.8.xml b/utilities/ovn-nbctl.8.xml
index fd75c0e..effbbce 100644
--- a/utilities/ovn-nbctl.8.xml
+++ b/utilities/ovn-nbctl.8.xml
@@ -949,6 +949,100 @@ 
       </dd>
     </dl>
 
+    <h1> Control Plane Protection Policy commands</h1>
+
+    <dl>
+      <dt><code>ls-copp-add</code> <var>switch</var> <var>proto</var>
+      <var>meter</var></dt>
+      <dd>
+        Adds the control <code>proto</code> to <code>meter</code> mapping
+        to the <code>switch</code> control plane protection policy. If no
+        policy exists yet, it creates one. If a mapping already existed for
+        <code>proto</code>, this will overwrite it.
+      </dd>
+
+      <dt><code>ls-copp-del</code> <var>switch</var> [<var>proto</var>]</dt>
+      <dd>
+        Removes the control <code>proto</code> mapping from the
+        <code>switch</code> control plane protection policy. If
+        <code>proto</code> is not specified, the whole control plane
+        protection policy is destroyed.
+      </dd>
+
+      <dt><code>ls-copp-list</code> <var>switch</var></dt>
+      <dd>
+        Display the current control plane protection policy for
+        <code>switch</code>.
+      </dd>
+
+      <dt><code>lsp-copp-add</code> <var>proto</var> <var>proto</var>
+      <var>meter</var></dt>
+      <dd>
+        Adds the control <code>proto</code> to <code>meter</code> mapping
+        to the <code>port</code> control plane protection policy. If no
+        policy exists yet, it creates one. If a mapping already existed for
+        <code>proto</code>, this will overwrite it.
+      </dd>
+
+      <dt><code>lsp-copp-del</code> <var>port</var> [<var>proto</var>]</dt>
+      <dd>
+        Removes the control <code>proto</code> mapping from the
+        <code>port</code> control plane protection policy. If
+        <code>proto</code> is not specified, the whole control plane
+        protection policy is destroyed.
+      </dd>
+      <dt><code>lsp-copp-list</code> <var>port</var></dt>
+      <dd>
+        Display the current control plane protection policy for
+        <code>port</code>.
+      </dd>
+
+      <dt><code>lr-copp-add</code> <var>router</var> <var>proto</var>
+      <var>meter</var></dt>
+      <dd>
+        Adds the control <code>proto</code> to <code>meter</code> mapping
+        to the <code>router</code> control plane protection policy. If no
+        policy exists yet, it creates one. If a mapping already existed for
+        <code>proto</code>, this will overwrite it.
+      </dd>
+
+      <dt><code>lr-copp-del</code> <var>router</var> [<var>proto</var>]</dt>
+      <dd>
+        Removes the control <code>proto</code> mapping from the
+        <code>router</code> control plane protection policy. If
+        <code>proto</code> is not specified, the whole control plane
+        protection policy is destroyed.
+      </dd>
+
+      <dt><code>lr-copp-list</code> <var>router</var></dt>
+      <dd>
+        Display the current control plane protection policy for
+        <code>router</code>.
+      </dd>
+
+      <dt><code>lrp-copp-add</code> <var>proto</var> <var>proto</var>
+      <var>meter</var></dt>
+      <dd>
+        Adds the control <code>proto</code> to <code>meter</code> mapping
+        to the <code>port</code> control plane protection policy. If no
+        policy exists yet, it creates one. If a mapping already existed for
+        <code>proto</code>, this will overwrite it.
+      </dd>
+
+      <dt><code>lrp-copp-del</code> <var>port</var> [<var>proto</var>]</dt>
+      <dd>
+        Removes the control <code>proto</code> mapping from the
+        <code>port</code> control plane protection policy. If
+        <code>proto</code> is not specified, the whole control plane
+        protection policy is destroyed.
+      </dd>
+      <dt><code>lrp-copp-list</code> <var>port</var></dt>
+      <dd>
+        Display the current control plane protection policy for
+        <code>port</code>.
+      </dd>
+    </dl>
+
     <h1>Database Commands</h1>
     <p>These commands query and modify the contents of <code>ovsdb</code> tables.
     They are a slight abstraction of the <code>ovsdb</code> interface and
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index a89a9cb..c766278 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -27,6 +27,7 @@ 
 #include "jsonrpc.h"
 #include "openvswitch/json.h"
 #include "lib/acl-log.h"
+#include "lib/copp.h"
 #include "lib/ovn-nb-idl.h"
 #include "lib/ovn-util.h"
 #include "packets.h"
@@ -4797,6 +4798,347 @@  nbctl_lr_route_list(struct ctl_context *ctx)
     free(ipv6_routes);
 }
 
+static char *
+copp_proto_validate(const char *proto_name)
+{
+    for (size_t i = COPP_PROTO_FIRST; i < COPP_PROTO_MAX; i++) {
+        if (!strcmp(proto_name, get_copp_proto_name(i))) {
+            return NULL;
+        }
+    }
+
+    struct ds usage = DS_EMPTY_INITIALIZER;
+
+    ds_put_cstr(&usage, "Invalid control protocol. Allowed values: ");
+    for (size_t i = COPP_PROTO_FIRST; i < COPP_PROTO_MAX; i++) {
+        ds_put_format(&usage, "%s, ", get_copp_proto_name(i));
+    }
+    ds_chomp(&usage, ' ');
+    ds_chomp(&usage, ',');
+    ds_put_cstr(&usage, ".");
+
+    char *usage_str = xstrdup(ds_cstr(&usage));
+    ds_destroy(&usage);
+    return usage_str;
+}
+
+static const struct nbrec_copp *
+copp_add_meter(struct ctl_context *ctx, const struct nbrec_copp *copp,
+               const char *proto_name, const char *meter)
+{
+    if (!copp) {
+        copp = nbrec_copp_insert(ctx->txn);
+    }
+
+    struct smap meters;
+    smap_init(&meters);
+    smap_clone(&meters, &copp->meters);
+    smap_replace(&meters, proto_name, meter);
+    nbrec_copp_set_meters(copp, &meters);
+    smap_destroy(&meters);
+
+    return copp;
+}
+
+static void
+copp_del_meter(const struct nbrec_copp *copp, const char *proto_name)
+{
+    if (!copp) {
+        return;
+    }
+
+    if (proto_name) {
+        if (smap_get(&copp->meters, proto_name)) {
+            struct smap meters;
+            smap_init(&meters);
+            smap_clone(&meters, &copp->meters);
+            smap_remove(&meters, proto_name);
+            nbrec_copp_set_meters(copp, &meters);
+            smap_destroy(&meters);
+        }
+    } else {
+        nbrec_copp_delete(copp);
+    }
+}
+
+static void
+copp_list(struct ctl_context *ctx, const struct nbrec_copp *copp)
+{
+    if (!copp) {
+        return;
+    }
+
+    struct smap_node *node;
+
+    SMAP_FOR_EACH (node, &copp->meters) {
+        ds_put_format(&ctx->output, "%s: %s\n", node->key, node->value);
+    }
+}
+
+static void
+nbctl_ls_copp_add(struct ctl_context *ctx)
+{
+    const char *ls_name = ctx->argv[1];
+    const char *proto_name = ctx->argv[2];
+    const char *meter = ctx->argv[3];
+
+    char *error = copp_proto_validate(proto_name);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_logical_switch *ls = NULL;
+    error = ls_by_name_or_uuid(ctx, ls_name, true, &ls);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_copp *copp =
+        copp_add_meter(ctx, ls->copp, proto_name, meter);
+    nbrec_logical_switch_set_copp(ls, copp);
+}
+
+static void
+nbctl_ls_copp_del(struct ctl_context *ctx)
+{
+    const char *ls_name = ctx->argv[1];
+    const char *proto_name = NULL;
+    char *error;
+
+    if (ctx->argc == 3) {
+        proto_name = ctx->argv[2];
+        error = copp_proto_validate(proto_name);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+    }
+
+    const struct nbrec_logical_switch *ls = NULL;
+    error = ls_by_name_or_uuid(ctx, ls_name, true, &ls);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_del_meter(ls->copp, proto_name);
+}
+
+static void
+nbctl_ls_copp_list(struct ctl_context *ctx)
+{
+    const char *ls_name = ctx->argv[1];
+
+    const struct nbrec_logical_switch *ls = NULL;
+    char *error = ls_by_name_or_uuid(ctx, ls_name, true, &ls);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_list(ctx, ls->copp);
+}
+
+static void
+nbctl_lsp_copp_add(struct ctl_context *ctx)
+{
+    const char *lsp_name = ctx->argv[1];
+    const char *proto_name = ctx->argv[2];
+    const char *meter = ctx->argv[3];
+
+    char *error = copp_proto_validate(proto_name);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_logical_switch_port *lsp = NULL;
+    error = lsp_by_name_or_uuid(ctx, lsp_name, true, &lsp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_copp *copp =
+        copp_add_meter(ctx, lsp->copp, proto_name, meter);
+    nbrec_logical_switch_port_set_copp(lsp, copp);
+}
+
+static void
+nbctl_lsp_copp_del(struct ctl_context *ctx)
+{
+    const char *lsp_name = ctx->argv[1];
+    const char *proto_name = NULL;
+    char *error;
+
+    if (ctx->argc == 3) {
+        proto_name = ctx->argv[2];
+        error = copp_proto_validate(proto_name);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+    }
+
+    const struct nbrec_logical_switch_port *lsp = NULL;
+    error = lsp_by_name_or_uuid(ctx, lsp_name, true, &lsp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_del_meter(lsp->copp, proto_name);
+}
+
+static void
+nbctl_lsp_copp_list(struct ctl_context *ctx)
+{
+    const char *lsp_name = ctx->argv[1];
+
+    const struct nbrec_logical_switch_port *lsp = NULL;
+    char *error = lsp_by_name_or_uuid(ctx, lsp_name, true, &lsp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_list(ctx, lsp->copp);
+}
+
+static void
+nbctl_lr_copp_add(struct ctl_context *ctx)
+{
+    const char *lr_name = ctx->argv[1];
+    const char *proto_name = ctx->argv[2];
+    const char *meter = ctx->argv[3];
+
+    char *error = copp_proto_validate(proto_name);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_logical_router *lr = NULL;
+    error = lr_by_name_or_uuid(ctx, lr_name, true, &lr);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_copp *copp =
+        copp_add_meter(ctx, lr->copp, proto_name, meter);
+    nbrec_logical_router_set_copp(lr, copp);
+}
+
+static void
+nbctl_lr_copp_del(struct ctl_context *ctx)
+{
+    const char *lr_name = ctx->argv[1];
+    const char *proto_name = NULL;
+    char *error;
+
+    if (ctx->argc == 3) {
+        proto_name = ctx->argv[2];
+        error = copp_proto_validate(proto_name);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+    }
+
+    const struct nbrec_logical_router *lr = NULL;
+    error = lr_by_name_or_uuid(ctx, lr_name, true, &lr);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_del_meter(lr->copp, proto_name);
+}
+
+static void
+nbctl_lr_copp_list(struct ctl_context *ctx)
+{
+    const char *lr_name = ctx->argv[1];
+
+    const struct nbrec_logical_router *lr = NULL;
+    char *error = lr_by_name_or_uuid(ctx, lr_name, true, &lr);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_list(ctx, lr->copp);
+}
+
+static void
+nbctl_lrp_copp_add(struct ctl_context *ctx)
+{
+    const char *lrp_name = ctx->argv[1];
+    const char *proto_name = ctx->argv[2];
+    const char *meter = ctx->argv[3];
+
+    char *error = copp_proto_validate(proto_name);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_logical_router_port *lrp = NULL;
+    error = lrp_by_name_or_uuid(ctx, lrp_name, true, &lrp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const struct nbrec_copp *copp =
+        copp_add_meter(ctx, lrp->copp, proto_name, meter);
+    nbrec_logical_router_port_set_copp(lrp, copp);
+}
+
+static void
+nbctl_lrp_copp_del(struct ctl_context *ctx)
+{
+    const char *lrp_name = ctx->argv[1];
+    const char *proto_name = NULL;
+    char *error;
+
+    if (ctx->argc == 3) {
+        proto_name = ctx->argv[2];
+        error = copp_proto_validate(proto_name);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+    }
+
+    const struct nbrec_logical_router_port *lrp = NULL;
+    error = lrp_by_name_or_uuid(ctx, lrp_name, true, &lrp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_del_meter(lrp->copp, proto_name);
+}
+
+static void
+nbctl_lrp_copp_list(struct ctl_context *ctx)
+{
+    const char *lrp_name = ctx->argv[1];
+
+    const struct nbrec_logical_router_port *lrp = NULL;
+    char *error = lrp_by_name_or_uuid(ctx, lrp_name, true, &lrp);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    copp_list(ctx, lrp->copp);
+}
+
 static void
 verify_connections(struct ctl_context *ctx)
 {
@@ -5724,6 +6066,28 @@  static const struct ctl_command_syntax nbctl_commands[] = {
     {"dhcp-options-get-options", 1, 1, "DHCP_OPT_UUID", NULL,
      nbctl_dhcp_options_get_options, NULL, "", RO },
 
+    /* Control plane protection commands */
+    {"ls-copp-add", 3, 3, "SWITCH PROTO METER", NULL, nbctl_ls_copp_add, NULL,
+       "", RW},
+    {"ls-copp-del", 1, 2, "SWITCH [PROTO]", NULL, nbctl_ls_copp_del, NULL,
+       "", RW},
+    {"ls-copp-list", 1, 1, "SWITCH", NULL, nbctl_ls_copp_list, NULL, "", RO},
+    {"lsp-copp-add", 3, 3, "PORT PROTO METER", NULL, nbctl_lsp_copp_add, NULL,
+       "", RW},
+    {"lsp-copp-del", 1, 2, "PORT [PROTO]", NULL, nbctl_lsp_copp_del, NULL,
+       "", RW},
+    {"lsp-copp-list", 1, 1, "PORT", NULL, nbctl_lsp_copp_list, NULL, "", RO},
+    {"lr-copp-add", 3, 3, "ROUTER PROTO METER", NULL, nbctl_lr_copp_add, NULL,
+       "", RW},
+    {"lr-copp-del", 1, 2, "ROUTER [PROTO]", NULL, nbctl_lr_copp_del, NULL,
+       "", RW},
+    {"lr-copp-list", 1, 1, "ROUTER", NULL, nbctl_lr_copp_list, NULL, "", RO},
+    {"lrp-copp-add", 3, 3, "PORT PROTO METER", NULL, nbctl_lrp_copp_add, NULL,
+       "", RW},
+    {"lrp-copp-del", 1, 2, "PORT [PROTO]", NULL, nbctl_lrp_copp_del, NULL,
+       "", RW},
+    {"lrp-copp-list", 1, 1, "PORT", NULL, nbctl_lrp_copp_list, NULL, "", RO},
+
     /* Connection commands. */
     {"get-connection", 0, 0, "", pre_connection, cmd_get_connection, NULL, "", RO},
     {"del-connection", 0, 0, "", pre_connection, cmd_del_connection, NULL, "", RW},