diff mbox series

[ovs-dev,3/4] ovn-northd: Populate in_out_port in logical_flow table's tags.

Message ID 20210716000718.3168742-4-hzhou@ovn.org
State Accepted
Headers show
Series Avoid parsing non-local lflows with the help of tags in SB. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes fail github build: failed

Commit Message

Han Zhou July 16, 2021, 12:07 a.m. UTC
Populate the in_out_port tag for logical switch pipeline flows wherever
possible.

Signed-off-by: Han Zhou <hzhou@ovn.org>
---
 northd/ovn-northd.c  | 272 ++++++++++++++----------
 northd/ovn_northd.dl | 495 +++++++++++++++++++++++++------------------
 tests/ovn-northd.at  |  21 ++
 3 files changed, 470 insertions(+), 318 deletions(-)
diff mbox series

Patch

diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index ff81bf540..534bb9f97 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -4213,6 +4213,7 @@  struct ovn_lflow {
     uint16_t priority;
     char *match;
     char *actions;
+    char *io_port;
     char *stage_hint;
     const char *where;
 };
@@ -4248,7 +4249,7 @@  ovn_lflow_equal(const struct ovn_lflow *a, const struct ovn_datapath *od,
 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 *io_port, char *stage_hint,
                const char *where)
 {
     hmapx_init(&lflow->od_group);
@@ -4257,6 +4258,7 @@  ovn_lflow_init(struct ovn_lflow *lflow, struct ovn_datapath *od,
     lflow->priority = priority;
     lflow->match = match;
     lflow->actions = actions;
+    lflow->io_port = io_port;
     lflow->stage_hint = stage_hint;
     lflow->where = where;
 }
@@ -4274,7 +4276,7 @@  static struct hashrow_locks lflow_locks;
 static void
 do_ovn_lflow_add(struct hmap *lflow_map, struct ovn_datapath *od,
                  uint32_t hash, enum ovn_stage stage, uint16_t priority,
-                 const char *match, const char *actions,
+                 const char *match, const char *actions, const char *io_port,
                  const struct ovsdb_idl_row *stage_hint,
                  const char *where)
 {
@@ -4297,6 +4299,7 @@  do_ovn_lflow_add(struct hmap *lflow_map, struct ovn_datapath *od,
      * one datapath in a group, so it could be hashed correctly. */
     ovn_lflow_init(lflow, NULL, stage, priority,
                    xstrdup(match), xstrdup(actions),
+                   io_port ? xstrdup(io_port) : NULL,
                    ovn_lflow_hint(stage_hint), where);
     hmapx_add(&lflow->od_group, od);
     hmap_insert_fast(lflow_map, &lflow->hmap_node, hash);
@@ -4306,7 +4309,7 @@  do_ovn_lflow_add(struct hmap *lflow_map, struct ovn_datapath *od,
 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 *match, const char *actions, const char *io_port,
                  const struct ovsdb_idl_row *stage_hint, const char *where)
 {
     ovs_assert(ovn_stage_to_datapath_type(stage) == ovn_datapath_get_type(od));
@@ -4321,11 +4324,11 @@  ovn_lflow_add_at(struct hmap *lflow_map, struct ovn_datapath *od,
     if (use_logical_dp_groups && use_parallel_build) {
         lock_hash_row(&lflow_locks, hash);
         do_ovn_lflow_add(lflow_map, od, hash, stage, priority, match,
-                         actions, stage_hint, where);
+                         actions, io_port, stage_hint, where);
         unlock_hash_row(&lflow_locks, hash);
     } else {
         do_ovn_lflow_add(lflow_map, od, hash, stage, priority, match,
-                         actions, stage_hint, where);
+                         actions, io_port, stage_hint, where);
     }
 }
 
@@ -4333,11 +4336,27 @@  ovn_lflow_add_at(struct hmap *lflow_map, struct ovn_datapath *od,
 #define ovn_lflow_add_with_hint(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, \
                                 ACTIONS, STAGE_HINT) \
     ovn_lflow_add_at(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS, \
-                     STAGE_HINT, OVS_SOURCE_LOCATOR)
+                     NULL, STAGE_HINT, OVS_SOURCE_LOCATOR)
+
+/* This macro is similar to ovn_lflow_add_with_hint, except that it requires
+ * the IN_OUT_PORT argument, which tells the lport name that appears in the
+ * MATCH, which helps ovn-controller to bypass lflows parsing when the lport is
+ * not local to the chassis. The critiera of the lport to be added using this
+ * argument:
+ *
+ * - For ingress pipeline, the lport that is used to match "inport".
+ * - For egress pipeline, the lport that is used to match "outport".
+ *
+ * For now, only LS pipelines should use this macro.  */
+#define ovn_lflow_add_with_lport_and_hint(LFLOW_MAP, OD, STAGE, PRIORITY, \
+                                          MATCH, ACTIONS, IN_OUT_PORT, \
+                                          STAGE_HINT) \
+    ovn_lflow_add_at(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS, \
+                     IN_OUT_PORT, STAGE_HINT, OVS_SOURCE_LOCATOR)
 
 #define ovn_lflow_add(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS) \
     ovn_lflow_add_at(LFLOW_MAP, OD, STAGE, PRIORITY, MATCH, ACTIONS, \
-                     NULL, OVS_SOURCE_LOCATOR)
+                     NULL, NULL, OVS_SOURCE_LOCATOR)
 
 static struct ovn_lflow *
 ovn_lflow_find(const struct hmap *lflows, const struct ovn_datapath *od,
@@ -4363,6 +4382,7 @@  ovn_lflow_destroy(struct hmap *lflows, struct ovn_lflow *lflow)
         hmapx_destroy(&lflow->od_group);
         free(lflow->match);
         free(lflow->actions);
+        free(lflow->io_port);
         free(lflow->stage_hint);
         free(lflow);
     }
@@ -4532,8 +4552,10 @@  build_port_security_nd(struct ovn_port *op, struct hmap *lflows,
                 ds_chomp(&match, ',');
                 ds_put_cstr(&match, "}");
             }
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND,
-                                    90, ds_cstr(&match), "next;", stage_hint);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                              S_SWITCH_IN_PORT_SEC_ND, 90,
+                                              ds_cstr(&match), "next;",
+                                              op->key, stage_hint);
         }
 
         if (ps->n_ipv6_addrs || no_ip) {
@@ -4542,15 +4564,18 @@  build_port_security_nd(struct ovn_port *op, struct hmap *lflows,
                           op->json_key, ps->ea_s);
             build_port_security_ipv6_nd_flow(&match, ps->ea, ps->ipv6_addrs,
                                              ps->n_ipv6_addrs);
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND,
-                                    90, ds_cstr(&match), "next;", stage_hint);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                              S_SWITCH_IN_PORT_SEC_ND, 90,
+                                              ds_cstr(&match), "next;",
+                                              op->key, stage_hint);
         }
     }
 
     ds_clear(&match);
     ds_put_format(&match, "inport == %s && (arp || nd)", op->json_key);
-    ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND, 80,
-                            ds_cstr(&match), "drop;", stage_hint);
+    ovn_lflow_add_with_lport_and_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND,
+                                      80, ds_cstr(&match), "drop;", op->key,
+                                      stage_hint);
     ds_destroy(&match);
 }
 
@@ -4602,9 +4627,10 @@  build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
                               " && ip4.dst == 255.255.255.255"
                               " && udp.src == 68 && udp.dst == 67",
                               op->json_key, ps->ea_s);
-                ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                        ds_cstr(&dhcp_match), "next;",
-                                        stage_hint);
+                ovn_lflow_add_with_lport_and_hint(lflows, op->od, stage, 90,
+                                                  ds_cstr(&dhcp_match),
+                                                  "next;", op->key,
+                                                  stage_hint);
                 ds_destroy(&dhcp_match);
                 ds_put_format(&match, "inport == %s && eth.src == %s"
                               " && ip4.src == {", op->json_key,
@@ -4643,9 +4669,9 @@  build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
             ds_chomp(&match, ' ');
             ds_chomp(&match, ',');
             ds_put_cstr(&match, "}");
-            ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                    ds_cstr(&match), "next;",
-                                    stage_hint);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od, stage, 90,
+                                              ds_cstr(&match), "next;",
+                                              op->key, stage_hint);
             ds_destroy(&match);
         }
 
@@ -4659,11 +4685,11 @@  build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
                               " && eth.src == %s"
                               " && ip6.src == ::"
                               " && ip6.dst == ff02::/16"
-                              " && icmp6.type == {131, 135, 143}", op->json_key,
-                              ps->ea_s);
-                ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                        ds_cstr(&dad_match), "next;",
-                                        stage_hint);
+                              " && icmp6.type == {131, 135, 143}",
+                              op->json_key, ps->ea_s);
+                ovn_lflow_add_with_lport_and_hint(lflows, op->od, stage, 90,
+                                                  ds_cstr(&dad_match), "next;",
+                                                  op->key, stage_hint);
                 ds_destroy(&dad_match);
             }
             ds_put_format(&match, "%s == %s && %s == %s",
@@ -4671,9 +4697,9 @@  build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
                           pipeline == P_IN ? "eth.src" : "eth.dst", ps->ea_s);
             build_port_security_ipv6_flow(pipeline, &match, ps->ea,
                                           ps->ipv6_addrs, ps->n_ipv6_addrs);
-            ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                    ds_cstr(&match), "next;",
-                                    stage_hint);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od, stage, 90,
+                                              ds_cstr(&match), "next;",
+                                              op->key, stage_hint);
             ds_destroy(&match);
         }
 
@@ -4681,8 +4707,8 @@  build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
                                 port_direction, op->json_key,
                                 pipeline == P_IN ? "eth.src" : "eth.dst",
                                 ps->ea_s);
-        ovn_lflow_add_with_hint(lflows, op->od, stage, 80, match, "drop;",
-                                stage_hint);
+        ovn_lflow_add_with_lport_and_hint(lflows, op->od, stage, 80, match,
+                                          "drop;", op->key, stage_hint);
         free(match);
     }
 
@@ -5003,9 +5029,9 @@  build_lswitch_input_port_sec_op(
         ds_put_format(actions, "set_queue(%s); ", queue_id);
     }
     ds_put_cstr(actions, "next;");
-    ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2, 50,
-                            ds_cstr(match), ds_cstr(actions),
-                            &op->nbsp->header_);
+    ovn_lflow_add_with_lport_and_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2,
+                                      50, ds_cstr(match), ds_cstr(actions),
+                                      op->key, &op->nbsp->header_);
 
     if (op->nbsp->n_port_security) {
         build_port_security_ip(P_IN, op, lflows, &op->nbsp->header_);
@@ -5039,16 +5065,18 @@  build_lswitch_learn_fdb_op(
         ds_put_format(match, "inport == %s", op->json_key);
         ds_put_format(actions, REGBIT_LKUP_FDB
                       " = lookup_fdb(inport, eth.src); next;");
-        ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 100,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbsp->header_);
+        ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                          S_SWITCH_IN_LOOKUP_FDB, 100,
+                                          ds_cstr(match), ds_cstr(actions),
+                                          op->key, &op->nbsp->header_);
 
         ds_put_cstr(match, " && "REGBIT_LKUP_FDB" == 0");
         ds_clear(actions);
         ds_put_cstr(actions, "put_fdb(inport, eth.src); next;");
-        ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PUT_FDB, 100,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbsp->header_);
+        ovn_lflow_add_with_lport_and_hint(lflows, op->od, S_SWITCH_IN_PUT_FDB,
+                                          100, ds_cstr(match),
+                                          ds_cstr(actions), op->key,
+                                          &op->nbsp->header_);
     }
 }
 
@@ -5098,13 +5126,15 @@  build_lswitch_output_port_sec_op(struct ovn_port *op,
                 }
             }
             ds_put_cstr(actions, "output;");
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2,
-                                    50, ds_cstr(match), ds_cstr(actions),
-                                    &op->nbsp->header_);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                              S_SWITCH_OUT_PORT_SEC_L2, 50,
+                                              ds_cstr(match), ds_cstr(actions),
+                                              op->key, &op->nbsp->header_);
         } else {
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2,
-                                    150, ds_cstr(match), "drop;",
-                                    &op->nbsp->header_);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                              S_SWITCH_OUT_PORT_SEC_L2, 150,
+                                              ds_cstr(match), "drop;", op->key,
+                                              &op->nbsp->header_);
         }
 
         if (op->nbsp->n_port_security) {
@@ -5146,12 +5176,12 @@  skip_port_from_conntrack(struct ovn_datapath *od, struct ovn_port *op,
 
     ds_put_format(&match_in, "ip && inport == %s", op->json_key);
     ds_put_format(&match_out, "ip && outport == %s", op->json_key);
-    ovn_lflow_add_with_hint(lflows, od, in_stage, priority,
-                            ds_cstr(&match_in), "next;",
-                            &op->nbsp->header_);
-    ovn_lflow_add_with_hint(lflows, od, out_stage, priority,
-                            ds_cstr(&match_out), "next;",
-                            &op->nbsp->header_);
+    ovn_lflow_add_with_lport_and_hint(lflows, od, in_stage, priority,
+                                      ds_cstr(&match_in), "next;", op->key,
+                                      &op->nbsp->header_);
+    ovn_lflow_add_with_lport_and_hint(lflows, od, out_stage, priority,
+                                      ds_cstr(&match_out), "next;", op->key,
+                                      &op->nbsp->header_);
 
     ds_destroy(&match_in);
     ds_destroy(&match_out);
@@ -5674,7 +5704,8 @@  build_reject_acl_rules(struct ovn_datapath *od, struct hmap *lflows,
                   "outport <-> inport; %s };", next_action);
     ovn_lflow_add_with_hint(lflows, od, stage,
                             acl->priority + OVN_ACL_PRI_OFFSET,
-                            ds_cstr(&match), ds_cstr(&actions), stage_hint);
+                            ds_cstr(&match), ds_cstr(&actions),
+                            stage_hint);
 
     free(next_action);
     ds_destroy(&match);
@@ -6047,9 +6078,10 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows,
                               "&& ip4.src == %s && udp && udp.src == 67 "
                               "&& udp.dst == 68", od->nbs->ports[i]->name,
                               server_mac, server_id);
-                ovn_lflow_add_with_hint(
+                ovn_lflow_add_with_lport_and_hint(
                     lflows, od, S_SWITCH_OUT_ACL, 34000, ds_cstr(&match),
-                    dhcp_actions, &od->nbs->ports[i]->dhcpv4_options->header_);
+                    dhcp_actions, od->nbs->ports[i]->name,
+                    &od->nbs->ports[i]->dhcpv4_options->header_);
             }
         }
 
@@ -6073,9 +6105,9 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows,
                               "&& ip6.src == %s && udp && udp.src == 547 "
                               "&& udp.dst == 546", od->nbs->ports[i]->name,
                               server_mac, server_ip);
-                ovn_lflow_add_with_hint(
+                ovn_lflow_add_with_lport_and_hint(
                     lflows, od, S_SWITCH_OUT_ACL, 34000, ds_cstr(&match),
-                    dhcp6_actions,
+                    dhcp6_actions, od->nbs->ports[i]->name,
                     &od->nbs->ports[i]->dhcpv6_options->header_);
             }
         }
@@ -6829,7 +6861,7 @@  build_lswitch_rport_arp_req_flows(struct ovn_port *op,
 static void
 build_dhcpv4_options_flows(struct ovn_port *op,
                            struct lport_addresses *lsp_addrs,
-                           const char *json_key, bool is_external,
+                           struct ovn_port *inport, bool is_external,
                            struct hmap *lflows)
 {
     struct ds match = DS_EMPTY_INITIALIZER;
@@ -6846,18 +6878,17 @@  build_dhcpv4_options_flows(struct ovn_port *op,
                 &match, "inport == %s && eth.src == %s && "
                 "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
                 "udp.src == 68 && udp.dst == 67",
-                json_key, lsp_addrs->ea_s);
+                inport->json_key, lsp_addrs->ea_s);
 
             if (is_external) {
                 ds_put_format(&match, " && is_chassis_resident(%s)",
                               op->json_key);
             }
 
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                    ds_cstr(&match),
-                                    ds_cstr(&options_action),
-                                    &op->nbsp->dhcpv4_options->header_);
+            ovn_lflow_add_with_lport_and_hint(
+                lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS, 100, ds_cstr(&match),
+                ds_cstr(&options_action), inport->key,
+                &op->nbsp->dhcpv4_options->header_);
             ds_clear(&match);
             /* Allow ip4.src = OFFER_IP and
              * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
@@ -6870,18 +6901,17 @@  build_dhcpv4_options_flows(struct ovn_port *op,
             ds_put_format(
                 &match, "inport == %s && eth.src == %s && "
                 "%s && udp.src == 68 && udp.dst == 67",
-                json_key, lsp_addrs->ea_s, ds_cstr(&ipv4_addr_match));
+                inport->json_key, lsp_addrs->ea_s, ds_cstr(&ipv4_addr_match));
 
             if (is_external) {
                 ds_put_format(&match, " && is_chassis_resident(%s)",
                               op->json_key);
             }
 
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                    ds_cstr(&match),
-                                    ds_cstr(&options_action),
-                                    &op->nbsp->dhcpv4_options->header_);
+            ovn_lflow_add_with_lport_and_hint(
+                lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS, 100, ds_cstr(&match),
+                ds_cstr(&options_action), inport->key,
+                &op->nbsp->dhcpv4_options->header_);
             ds_clear(&match);
 
             /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
@@ -6890,18 +6920,17 @@  build_dhcpv4_options_flows(struct ovn_port *op,
                 &match, "inport == %s && eth.src == %s && "
                 "ip4 && udp.src == 68 && udp.dst == 67"
                 " && "REGBIT_DHCP_OPTS_RESULT,
-                json_key, lsp_addrs->ea_s);
+                inport->json_key, lsp_addrs->ea_s);
 
             if (is_external) {
                 ds_put_format(&match, " && is_chassis_resident(%s)",
                               op->json_key);
             }
 
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_SWITCH_IN_DHCP_RESPONSE, 100,
-                                    ds_cstr(&match),
-                                    ds_cstr(&response_action),
-                                    &op->nbsp->dhcpv4_options->header_);
+            ovn_lflow_add_with_lport_and_hint(
+                lflows, op->od, S_SWITCH_IN_DHCP_RESPONSE, 100,
+                ds_cstr(&match), ds_cstr(&response_action), inport->key,
+                &op->nbsp->dhcpv4_options->header_);
             ds_destroy(&options_action);
             ds_destroy(&response_action);
             ds_destroy(&ipv4_addr_match);
@@ -6914,7 +6943,7 @@  build_dhcpv4_options_flows(struct ovn_port *op,
 static void
 build_dhcpv6_options_flows(struct ovn_port *op,
                            struct lport_addresses *lsp_addrs,
-                           const char *json_key, bool is_external,
+                           struct ovn_port *inport, bool is_external,
                            struct hmap *lflows)
 {
     struct ds match = DS_EMPTY_INITIALIZER;
@@ -6930,27 +6959,25 @@  build_dhcpv6_options_flows(struct ovn_port *op,
                 &match, "inport == %s && eth.src == %s"
                 " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
                 " udp.dst == 547",
-                json_key, lsp_addrs->ea_s);
+                inport->json_key, lsp_addrs->ea_s);
 
             if (is_external) {
                 ds_put_format(&match, " && is_chassis_resident(%s)",
                               op->json_key);
             }
 
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                    ds_cstr(&match),
-                                    ds_cstr(&options_action),
-                                    &op->nbsp->dhcpv6_options->header_);
+            ovn_lflow_add_with_lport_and_hint(
+                lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS, 100, ds_cstr(&match),
+                ds_cstr(&options_action), inport->key,
+                &op->nbsp->dhcpv6_options->header_);
 
             /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
              * put_dhcpv6_opts action is successful */
             ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_SWITCH_IN_DHCP_RESPONSE, 100,
-                                    ds_cstr(&match),
-                                    ds_cstr(&response_action),
-                                    &op->nbsp->dhcpv6_options->header_);
+            ovn_lflow_add_with_lport_and_hint(
+                lflows, op->od, S_SWITCH_IN_DHCP_RESPONSE, 100,
+                ds_cstr(&match), ds_cstr(&response_action), inport->key,
+                &op->nbsp->dhcpv6_options->header_);
             ds_destroy(&options_action);
             ds_destroy(&response_action);
             break;
@@ -6979,10 +7006,10 @@  build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
                         port->json_key,
                         op->lsp_addrs[i].ea_s, op->json_key,
                         rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_EXTERNAL_PORT,
-                                            100, ds_cstr(&match), "drop;",
-                                            &op->nbsp->header_);
+                    ovn_lflow_add_with_lport_and_hint(
+                        lflows, op->od, S_SWITCH_IN_EXTERNAL_PORT, 100,
+                        ds_cstr(&match), "drop;", port->key,
+                        &op->nbsp->header_);
                 }
                 for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv6_addrs; l++) {
                     ds_clear(&match);
@@ -6995,10 +7022,10 @@  build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
                         rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
                         rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
                         rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_EXTERNAL_PORT, 100,
-                                            ds_cstr(&match), "drop;",
-                                            &op->nbsp->header_);
+                    ovn_lflow_add_with_lport_and_hint(
+                        lflows, op->od, S_SWITCH_IN_EXTERNAL_PORT, 100,
+                        ds_cstr(&match), "drop;", port->key,
+                        &op->nbsp->header_);
                 }
 
                 ds_clear(&match);
@@ -7009,10 +7036,11 @@  build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
                     port->json_key,
                     op->lsp_addrs[i].ea_s, rp->lsp_addrs[k].ea_s,
                     op->json_key);
-                ovn_lflow_add_with_hint(lflows, op->od,
-                                        S_SWITCH_IN_EXTERNAL_PORT,
-                                        100, ds_cstr(&match), "drop;",
-                                        &op->nbsp->header_);
+                ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                                  S_SWITCH_IN_EXTERNAL_PORT,
+                                                  100, ds_cstr(&match),
+                                                  "drop;", port->key,
+                                                  &op->nbsp->header_);
             }
         }
     }
@@ -7118,9 +7146,10 @@  build_lswitch_arp_nd_responder_skip_local(struct ovn_port *op,
             (!strcmp(op->nbsp->type, "vtep"))) {
             ds_clear(match);
             ds_put_format(match, "inport == %s", op->json_key);
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP,
-                                    100, ds_cstr(match), "next;",
-                                    &op->nbsp->header_);
+            ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                              S_SWITCH_IN_ARP_ND_RSP, 100,
+                                              ds_cstr(match), "next;", op->key,
+                                              &op->nbsp->header_);
         }
     }
 }
@@ -7177,10 +7206,11 @@  build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
                     "bind_vport(%s, inport); "
                     "next;",
                     op->json_key);
-                ovn_lflow_add_with_hint(lflows, op->od,
-                                        S_SWITCH_IN_ARP_ND_RSP, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &vp->nbsp->header_);
+                ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                                  S_SWITCH_IN_ARP_ND_RSP, 100,
+                                                  ds_cstr(match),
+                                                  ds_cstr(actions), vparent,
+                                                  &vp->nbsp->header_);
             }
 
             free(tokstr);
@@ -7243,10 +7273,11 @@  build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
                      * 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_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 100,
-                                            ds_cstr(match), "next;",
-                                            &op->nbsp->header_);
+                    ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                                      S_SWITCH_IN_ARP_ND_RSP,
+                                                      100, ds_cstr(match),
+                                                      "next;", op->key,
+                                                      &op->nbsp->header_);
                 }
 
                 /* For ND solicitations, we need to listen for both the
@@ -7285,10 +7316,11 @@  build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
                     /* 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_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 100,
-                                            ds_cstr(match), "next;",
-                                            &op->nbsp->header_);
+                    ovn_lflow_add_with_lport_and_hint(lflows, op->od,
+                                                      S_SWITCH_IN_ARP_ND_RSP,
+                                                      100, ds_cstr(match),
+                                                      "next;", op->key,
+                                                      &op->nbsp->header_);
                 }
             }
         }
@@ -7428,17 +7460,17 @@  build_lswitch_dhcp_options_and_response(struct ovn_port *op,
                 for (size_t j = 0; j < op->od->n_localnet_ports; j++) {
                     build_dhcpv4_options_flows(
                         op, &op->lsp_addrs[i],
-                        op->od->localnet_ports[j]->json_key, is_external,
+                        op->od->localnet_ports[j], is_external,
                         lflows);
                     build_dhcpv6_options_flows(
                         op, &op->lsp_addrs[i],
-                        op->od->localnet_ports[j]->json_key, is_external,
+                        op->od->localnet_ports[j], is_external,
                         lflows);
                 }
             } else {
-                build_dhcpv4_options_flows(op, &op->lsp_addrs[i], op->json_key,
+                build_dhcpv4_options_flows(op, &op->lsp_addrs[i], op,
                                            is_external, lflows);
-                build_dhcpv6_options_flows(op, &op->lsp_addrs[i], op->json_key,
+                build_dhcpv6_options_flows(op, &op->lsp_addrs[i], op,
                                            is_external, lflows);
             }
         }
@@ -12849,6 +12881,12 @@  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);
+        if (lflow->io_port) {
+            struct smap tags = SMAP_INITIALIZER(&tags);
+            smap_add(&tags, "in_out_port", lflow->io_port);
+            sbrec_logical_flow_set_tags(sbflow, &tags);
+            smap_destroy(&tags);
+        }
 
         /* Trim the source locator lflow->where, which looks something like
          * "ovn/northd/ovn-northd.c:1234", down to just the part following the
diff --git a/northd/ovn_northd.dl b/northd/ovn_northd.dl
index ceeabe6f3..f4689bf02 100644
--- a/northd/ovn_northd.dl
+++ b/northd/ovn_northd.dl
@@ -1635,6 +1635,34 @@  relation Flow(
     external_ids:     Map<string,string>
 )
 
+relation FlowWithInOutPort(f: Flow, ioPort: string)
+
+relation TaggedFlow(
+    logical_datapath: uuid,
+    stage:            Stage,
+    priority:         integer,
+    __match:          string,
+    actions:          string,
+    tags:             Map<string,string>,
+    external_ids:     Map<string,string>
+)
+TaggedFlow(
+    .logical_datapath = f.logical_datapath,
+    .stage            = f.stage,
+    .priority         = f.priority,
+    .__match          = f.__match,
+    .actions          = f.actions,
+    .tags             = map_empty(),
+    .external_ids     = f.external_ids) :- Flow[f].
+TaggedFlow(
+    .logical_datapath = f.logical_datapath,
+    .stage            = f.stage,
+    .priority         = f.priority,
+    .__match          = f.__match,
+    .actions          = f.actions,
+    .tags             = [ "in_out_port" -> ioPort ],
+    .external_ids     = f.external_ids) :- FlowWithInOutPort(f, ioPort).
+
 /* If this option is 'true' northd will combine logical flows that differ by
  * logical datapath only by creating a datapath group. */
 relation UseLogicalDatapathGroups[bool]
@@ -1651,6 +1679,7 @@  relation AggregatedFlow (
     priority:          integer,
     __match:           string,
     actions:           string,
+    tags:              Map<string,string>,
     external_ids:      Map<string,string>
 )
 AggregatedFlow(.logical_datapaths = g.to_set(),
@@ -1658,17 +1687,19 @@  AggregatedFlow(.logical_datapaths = g.to_set(),
                .priority = priority,
                .__match = __match,
                .actions = actions,
+               .tags = tags,
                .external_ids = external_ids) :-
-    Flow(logical_datapath, stage, priority, __match, actions, external_ids),
-    var g = logical_datapath.group_by((stage, priority, __match, actions, external_ids)),
+    TaggedFlow(logical_datapath, stage, priority, __match, actions, tags, external_ids),
+    var g = logical_datapath.group_by((stage, priority, __match, actions, tags, external_ids)),
     UseLogicalDatapathGroups[true].
 AggregatedFlow(.logical_datapaths = set_singleton(logical_datapath),
                .stage = stage,
                .priority = priority,
                .__match = __match,
                .actions = actions,
+               .tags = tags,
                .external_ids = external_ids) :-
-    Flow(logical_datapath, stage, priority, __match, actions, external_ids),
+    TaggedFlow(logical_datapath, stage, priority, __match, actions, tags, external_ids),
     UseLogicalDatapathGroups[false].
 
 for (f in AggregatedFlow()) {
@@ -1685,6 +1716,7 @@  for (f in AggregatedFlow()) {
             .priority         = f.priority,
             .__match          = f.__match,
             .actions          = f.actions,
+            .tags             = f.tags,
             .external_ids     = external_ids)
     } else {
         var group_uuid = hash128(f.logical_datapaths) in {
@@ -1697,6 +1729,7 @@  for (f in AggregatedFlow()) {
                 .priority         = f.priority,
                 .__match          = f.__match,
                 .actions          = f.actions,
+                .tags             = f.tags,
                 .external_ids     = external_ids);
             sb::Out_Logical_DP_Group(._uuid = group_uuid, .datapaths = f.logical_datapaths)
         }
@@ -1893,35 +1926,43 @@  for (&SwitchPort(.lsp = lsp@&nb::Logical_Switch_Port{.__type = "router"},
      * as the icmp request went through the logical router
      * on hostA, not hostB. This would only work with
      * distributed conntrack state across all chassis. */
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_IN_PRE_ACL(),
-         .priority         = 110,
-         .__match          = "ip && inport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid));
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_OUT_PRE_ACL(),
-         .priority         = 110,
-         .__match          = "ip && outport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_IN_PRE_ACL(),
+             .priority         = 110,
+             .__match          = "ip && inport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name);
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_OUT_PRE_ACL(),
+             .priority         = 110,
+             .__match          = "ip && outport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 for (&SwitchPort(.lsp = lsp@&nb::Logical_Switch_Port{.__type = "localnet"},
                  .json_name = lsp_name,
                  .sw = &Switch{._uuid = ls_uuid, .has_stateful_acl = true})) {
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_IN_PRE_ACL(),
-         .priority         = 110,
-         .__match          = "ip && inport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid));
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_OUT_PRE_ACL(),
-         .priority         = 110,
-         .__match          = "ip && outport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_IN_PRE_ACL(),
+             .priority         = 110,
+             .__match          = "ip && inport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name);
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_OUT_PRE_ACL(),
+             .priority         = 110,
+             .__match          = "ip && outport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 for (&Switch(._uuid = ls_uuid, .has_stateful_acl = true)) {
@@ -2015,18 +2056,22 @@  for (&Switch(._uuid = ls_uuid)) {
 
 for (&SwitchPort(.lsp = lsp, .json_name = lsp_name, .sw = &Switch{._uuid = ls_uuid}))
 if (lsp.__type == "router" or lsp.__type == "localnet") {
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_IN_PRE_LB(),
-         .priority         = 110,
-         .__match          = "ip && inport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid));
-    Flow(.logical_datapath = ls_uuid,
-         .stage            = s_SWITCH_OUT_PRE_LB(),
-         .priority         = 110,
-         .__match          = "ip && outport == ${lsp_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_IN_PRE_LB(),
+             .priority         = 110,
+             .__match          = "ip && inport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name);
+    FlowWithInOutPort(
+        Flow{.logical_datapath = ls_uuid,
+             .stage            = s_SWITCH_OUT_PRE_LB(),
+             .priority         = 110,
+             .__match          = "ip && outport == ${lsp_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 relation HasEventElbMeter(has_meter: bool)
@@ -2736,15 +2781,17 @@  for (SwitchPortDHCPv4Options(.port = &SwitchPort{.lsp = lsp, .sw = sw},
     (Some{var server_id}, Some{var server_mac}, Some{var lease_time}) =
         (options.get("server_id"), options.get("server_mac"), options.get("lease_time")) in
     var has_stateful = sw.has_stateful_acl or sw.has_lb_vip in
-    Flow(.logical_datapath = sw._uuid,
-         .stage            = s_SWITCH_OUT_ACL(),
-         .priority         = 34000,
-         .__match          = "outport == ${json_string_escape(lsp.name)} "
-                             "&& eth.src == ${server_mac} "
-                             "&& ip4.src == ${server_id} && udp && udp.src == 67 "
-                             "&& udp.dst == 68",
-         .actions          = if (has_stateful) "ct_commit; next;" else "next;",
-         .external_ids     = stage_hint(dhcpv4_options._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = sw._uuid,
+             .stage            = s_SWITCH_OUT_ACL(),
+             .priority         = 34000,
+             .__match          = "outport == ${json_string_escape(lsp.name)} "
+                                 "&& eth.src == ${server_mac} "
+                                 "&& ip4.src == ${server_id} && udp && udp.src == 67 "
+                                 "&& udp.dst == 68",
+             .actions          = if (has_stateful) "ct_commit; next;" else "next;",
+             .external_ids     = stage_hint(dhcpv4_options._uuid)},
+        lsp.name)
 }
 
 for (SwitchPortDHCPv6Options(.port = &SwitchPort{.lsp = lsp, .sw = sw},
@@ -2756,15 +2803,17 @@  for (SwitchPortDHCPv6Options(.port = &SwitchPort{.lsp = lsp, .sw = sw},
     /* Get the link local IP of the DHCPv6 server from the
      * server MAC. */
     var has_stateful = sw.has_stateful_acl or sw.has_lb_vip in
-    Flow(.logical_datapath = sw._uuid,
-         .stage            = s_SWITCH_OUT_ACL(),
-         .priority         = 34000,
-         .__match          = "outport == ${json_string_escape(lsp.name)} "
-                             "&& eth.src == ${server_mac} "
-                             "&& ip6.src == ${server_ip} && udp && udp.src == 547 "
-                             "&& udp.dst == 546",
-         .actions          = if (has_stateful) "ct_commit; next;" else "next;",
-         .external_ids     = stage_hint(dhcpv6_options._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = sw._uuid,
+             .stage            = s_SWITCH_OUT_ACL(),
+             .priority         = 34000,
+             .__match          = "outport == ${json_string_escape(lsp.name)} "
+                                 "&& eth.src == ${server_mac} "
+                                 "&& ip6.src == ${server_ip} && udp && udp.src == 547 "
+                                 "&& udp.dst == 546",
+             .actions          = if (has_stateful) "ct_commit; next;" else "next;",
+             .external_ids     = stage_hint(dhcpv6_options._uuid)},
+        lsp.name)
 }
 
 relation QoSAction(qos: uuid, key_action: string, value_action: integer)
@@ -3095,12 +3144,14 @@  for (&SwitchPort(.lsp = lsp, .sw = sw, .json_name = json_name, .ps_eth_addresses
                 None -> "next;",
                 Some{id} -> "set_queue(${id}); next;"
             } in
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_IN_PORT_SEC_L2(),
-             .priority         = 50,
-             .__match          = __match,
-             .actions          = actions,
-             .external_ids     = stage_hint(lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_IN_PORT_SEC_L2(),
+                 .priority         = 50,
+                 .__match          = __match,
+                 .actions          = actions,
+                 .external_ids     = stage_hint(lsp._uuid)},
+            lsp.name)
     }
 }
 
@@ -3130,12 +3181,14 @@  for (SwitchPortPSAddresses(.port = port@&SwitchPort{.sw = sw}, .ps_addrs = ps)
                          " && ip4.src == 0.0.0.0"
                          " && ip4.dst == 255.255.255.255"
                          " && udp.src == 68 && udp.dst == 67" in {
-            Flow(.logical_datapath = sw._uuid,
-                 .stage            = s_SWITCH_IN_PORT_SEC_IP(),
-                 .priority         = 90,
-                 .__match          = dhcp_match,
-                 .actions          = "next;",
-                 .external_ids     = stage_hint(port.lsp._uuid))
+            FlowWithInOutPort(
+                Flow{.logical_datapath = sw._uuid,
+                     .stage            = s_SWITCH_IN_PORT_SEC_IP(),
+                     .priority         = 90,
+                     .__match          = dhcp_match,
+                     .actions          = "next;",
+                     .external_ids     = stage_hint(port.lsp._uuid)},
+                port.lsp.name)
         };
         var addrs = {
             var addrs = vec_empty();
@@ -3153,12 +3206,14 @@  for (SwitchPortPSAddresses(.port = port@&SwitchPort{.sw = sw}, .ps_addrs = ps)
             "inport == ${port.json_name} && eth.src == ${ps.ea} && ip4.src == {" ++
             addrs.join(", ") ++ "}" in
         {
-            Flow(.logical_datapath = sw._uuid,
-                 .stage         = s_SWITCH_IN_PORT_SEC_IP(),
-                 .priority         = 90,
-                 .__match          = __match,
-                 .actions          = "next;",
-                 .external_ids     = stage_hint(port.lsp._uuid))
+            FlowWithInOutPort(
+                Flow{.logical_datapath = sw._uuid,
+                     .stage         = s_SWITCH_IN_PORT_SEC_IP(),
+                     .priority         = 90,
+                     .__match          = __match,
+                     .actions          = "next;",
+                     .external_ids     = stage_hint(port.lsp._uuid)},
+                port.lsp.name)
         }
     };
     if (ps.ipv6_addrs.len() > 0) {
@@ -3178,22 +3233,26 @@  for (SwitchPortPSAddresses(.port = port@&SwitchPort{.sw = sw}, .ps_addrs = ps)
         var __match = "inport == ${port.json_name} && eth.src == ${ps.ea}" ++
                       build_port_security_ipv6_flow(Ingress, ps.ea, ps.ipv6_addrs) in
         {
-            Flow(.logical_datapath = sw._uuid,
-                 .stage            = s_SWITCH_IN_PORT_SEC_IP(),
-                 .priority         = 90,
-                 .__match          = __match,
-                 .actions          = "next;",
-                 .external_ids     = stage_hint(port.lsp._uuid))
+            FlowWithInOutPort(
+                Flow{.logical_datapath = sw._uuid,
+                     .stage            = s_SWITCH_IN_PORT_SEC_IP(),
+                     .priority         = 90,
+                     .__match          = __match,
+                     .actions          = "next;",
+                     .external_ids     = stage_hint(port.lsp._uuid)},
+                port.lsp.name)
         }
     };
     var __match = "inport == ${port.json_name} && eth.src == ${ps.ea} && ip" in
     {
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_IN_PORT_SEC_IP(),
-             .priority         = 80,
-             .__match          = __match,
-             .actions          = "drop;",
-             .external_ids     = stage_hint(port.lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_IN_PORT_SEC_IP(),
+                 .priority         = 80,
+                 .__match          = __match,
+                 .actions          = "drop;",
+                 .external_ids     = stage_hint(port.lsp._uuid)},
+            port.lsp.name)
     }
 }
 
@@ -3236,32 +3295,38 @@  for (SwitchPortPSAddresses(.port = port@&SwitchPort{.sw = sw}, .ps_addrs = ps)
                     prefix
                 }
             } in {
-                Flow(.logical_datapath = sw._uuid,
-                     .stage            = s_SWITCH_IN_PORT_SEC_ND(),
-                     .priority         = 90,
-                     .__match          = __match,
-                     .actions          = "next;",
-                     .external_ids     = stage_hint(port.lsp._uuid))
+                FlowWithInOutPort(
+                    Flow{.logical_datapath = sw._uuid,
+                         .stage            = s_SWITCH_IN_PORT_SEC_ND(),
+                         .priority         = 90,
+                         .__match          = __match,
+                         .actions          = "next;",
+                         .external_ids     = stage_hint(port.lsp._uuid)},
+                    port.lsp.name)
             }
         };
         if (not ps.ipv6_addrs.is_empty() or no_ip) {
             var __match = "inport == ${port.json_name} && eth.src == ${ps.ea}" ++
                           build_port_security_ipv6_nd_flow(ps.ea, ps.ipv6_addrs) in
             {
-                Flow(.logical_datapath = sw._uuid,
-                     .stage            = s_SWITCH_IN_PORT_SEC_ND(),
-                     .priority         = 90,
-                     .__match          = __match,
-                     .actions          = "next;",
-                     .external_ids     = stage_hint(port.lsp._uuid))
+                FlowWithInOutPort(
+                    Flow{.logical_datapath = sw._uuid,
+                         .stage            = s_SWITCH_IN_PORT_SEC_ND(),
+                         .priority         = 90,
+                         .__match          = __match,
+                         .actions          = "next;",
+                         .external_ids     = stage_hint(port.lsp._uuid)},
+                    port.lsp.name)
             }
         };
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_IN_PORT_SEC_ND(),
-             .priority         = 80,
-             .__match          = "inport == ${port.json_name} && (arp || nd)",
-             .actions          = "drop;",
-             .external_ids     = stage_hint(port.lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_IN_PORT_SEC_ND(),
+                 .priority         = 80,
+                 .__match          = "inport == ${port.json_name} && (arp || nd)",
+                 .actions          = "drop;",
+                 .external_ids     = stage_hint(port.lsp._uuid)},
+            port.lsp.name)
     }
 }
 
@@ -3289,12 +3354,14 @@  for (&SwitchPort(.lsp = lsp, .sw = sw, .json_name = json_name)
      if lsp.is_enabled() and
         (lsp.__type == "localnet" or lsp.__type == "vtep"))
 {
-    Flow(.logical_datapath = sw._uuid,
-         .stage            = s_SWITCH_IN_ARP_ND_RSP(),
-         .priority         = 100,
-         .__match          = "inport == ${json_name}",
-         .actions          = "next;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = sw._uuid,
+             .stage            = s_SWITCH_IN_ARP_ND_RSP(),
+             .priority         = 100,
+             .__match          = "inport == ${json_name}",
+             .actions          = "next;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 function lsp_is_up(lsp: Intern<nb::Logical_Switch_Port>): bool = {
@@ -3310,14 +3377,16 @@  function lsp_is_up(lsp: Intern<nb::Logical_Switch_Port>): bool = {
  *  - ARP reply from the virtual ip which belongs to a logical
  *    port of type 'virtual' and bind that port.
  * */
- Flow(.logical_datapath = sp.sw._uuid,
-      .stage            = s_SWITCH_IN_ARP_ND_RSP(),
-      .priority         = 100,
-      .__match          = "inport == ${vp.json_name} && "
-                          "((arp.op == 1 && arp.spa == ${virtual_ip} && arp.tpa == ${virtual_ip}) || "
-                          "(arp.op == 2 && arp.spa == ${virtual_ip}))",
-      .actions          = "bind_vport(${sp.json_name}, inport); next;",
-      .external_ids     = stage_hint(lsp._uuid)) :-
+ FlowWithInOutPort(
+     Flow{.logical_datapath = sp.sw._uuid,
+          .stage            = s_SWITCH_IN_ARP_ND_RSP(),
+          .priority         = 100,
+          .__match          = "inport == ${vp.json_name} && "
+                              "((arp.op == 1 && arp.spa == ${virtual_ip} && arp.tpa == ${virtual_ip}) || "
+                              "(arp.op == 2 && arp.spa == ${virtual_ip}))",
+          .actions          = "bind_vport(${sp.json_name}, inport); next;",
+          .external_ids     = stage_hint(lsp._uuid)},
+     vp.lsp.name) :-
     sp in &SwitchPort(.lsp = lsp@&nb::Logical_Switch_Port{.__type = "virtual"}),
     Some{var virtual_ip} = lsp.options.get("virtual-ip"),
     Some{var virtual_parents} = lsp.options.get("virtual-parents"),
@@ -3373,12 +3442,14 @@  for (CheckLspIsUp[check_lsp_is_up]) {
              * detect situations where the network is not working as
              * configured, so dropping the request would frustrate that
              * intent.) */
-            Flow(.logical_datapath = sw._uuid,
-                 .stage            = s_SWITCH_IN_ARP_ND_RSP(),
-                 .priority         = 100,
-                 .__match          = __match ++ " && inport == ${json_name}",
-                 .actions          = "next;",
-                 .external_ids     = stage_hint(lsp._uuid))
+            FlowWithInOutPort(
+                Flow{.logical_datapath = sw._uuid,
+                     .stage            = s_SWITCH_IN_ARP_ND_RSP(),
+                     .priority         = 100,
+                     .__match          = __match ++ " && inport == ${json_name}",
+                     .actions          = "next;",
+                     .external_ids     = stage_hint(lsp._uuid)},
+                lsp.name)
         }
     }
 }
@@ -3451,12 +3522,14 @@  for (SwitchPortIPv6Address(.port = &SwitchPort{.lsp = lsp, .json_name = json_nam
 
         /* Do not reply to a solicitation from the port that owns the
          * address (otherwise DAD detection will fail). */
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_IN_ARP_ND_RSP(),
-             .priority         = 100,
-             .__match          = __match ++ " && inport == ${json_name}",
-             .actions          = "next;",
-             .external_ids     = stage_hint(lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_IN_ARP_ND_RSP(),
+                 .priority         = 100,
+                 .__match          = __match ++ " && inport == ${json_name}",
+                 .actions          = "next;",
+                 .external_ids     = stage_hint(lsp._uuid)},
+            lsp.name)
     }
 }
 
@@ -4032,15 +4105,17 @@  for (IgmpSwitchMulticastGroup(.address = address, .switch = sw)) {
  * chassis, drop ARP requests arriving on localnet ports from X's Ethernet
  * address, if the ARP request is asking to translate the IP address of a
  * router port on LS. */
-Flow(.logical_datapath = sp.sw._uuid,
-     .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
-     .priority         = 100,
-     .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
-                          "eth.src == ${lp_addr.ea} && "
-                          "!is_chassis_resident(${sp.json_name}) && "
-                          "arp.tpa == ${rp_addr.addr} && arp.op == 1"),
-     .actions          = "drop;",
-     .external_ids     = stage_hint(sp.lsp._uuid)) :-
+FlowWithInOutPort(
+    Flow{.logical_datapath = sp.sw._uuid,
+         .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
+         .priority         = 100,
+         .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
+                              "eth.src == ${lp_addr.ea} && "
+                              "!is_chassis_resident(${sp.json_name}) && "
+                              "arp.tpa == ${rp_addr.addr} && arp.op == 1"),
+         .actions          = "drop;",
+         .external_ids     = stage_hint(sp.lsp._uuid)},
+    localnet_port.1) :-
     sp in &SwitchPort(),
     sp.lsp.__type == "external",
     var localnet_port = FlatMap(sp.sw.localnet_ports),
@@ -4048,16 +4123,18 @@  Flow(.logical_datapath = sp.sw._uuid,
     rp in &SwitchPort(.sw = sp.sw),
     rp.lsp.__type == "router",
     SwitchPortIPv4Address(.port = rp, .addr = rp_addr).
-Flow(.logical_datapath = sp.sw._uuid,
-     .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
-     .priority         = 100,
-     .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
-                          "eth.src == ${lp_addr.ea} && "
-                          "!is_chassis_resident(${sp.json_name}) && "
-                          "nd_ns && ip6.dst == {${rp_addr.addr}, ${rp_addr.solicited_node()}} && "
-                          "nd.target == ${rp_addr.addr}"),
-     .actions          = "drop;",
-     .external_ids     = stage_hint(sp.lsp._uuid)) :-
+FlowWithInOutPort(
+    Flow{.logical_datapath = sp.sw._uuid,
+         .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
+         .priority         = 100,
+         .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
+                              "eth.src == ${lp_addr.ea} && "
+                              "!is_chassis_resident(${sp.json_name}) && "
+                              "nd_ns && ip6.dst == {${rp_addr.addr}, ${rp_addr.solicited_node()}} && "
+                              "nd.target == ${rp_addr.addr}"),
+         .actions          = "drop;",
+         .external_ids     = stage_hint(sp.lsp._uuid)},
+    localnet_port.1) :-
     sp in &SwitchPort(),
     sp.lsp.__type == "external",
     var localnet_port = FlatMap(sp.sw.localnet_ports),
@@ -4065,15 +4142,17 @@  Flow(.logical_datapath = sp.sw._uuid,
     rp in &SwitchPort(.sw = sp.sw),
     rp.lsp.__type == "router",
     SwitchPortIPv6Address(.port = rp, .addr = rp_addr).
-Flow(.logical_datapath = sp.sw._uuid,
-     .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
-     .priority         = 100,
-     .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
-                          "eth.src == ${lp_addr.ea} && "
-                          "eth.dst == ${ea} && "
-                          "!is_chassis_resident(${sp.json_name})"),
-     .actions          = "drop;",
-     .external_ids     = stage_hint(sp.lsp._uuid)) :-
+FlowWithInOutPort(
+    Flow{.logical_datapath = sp.sw._uuid,
+         .stage            = s_SWITCH_IN_EXTERNAL_PORT(),
+         .priority         = 100,
+         .__match          = ("inport == ${json_string_escape(localnet_port.1)} && "
+                              "eth.src == ${lp_addr.ea} && "
+                              "eth.dst == ${ea} && "
+                              "!is_chassis_resident(${sp.json_name})"),
+         .actions          = "drop;",
+         .external_ids     = stage_hint(sp.lsp._uuid)},
+    localnet_port.1) :-
     sp in &SwitchPort(),
     sp.lsp.__type == "external",
     var localnet_port = FlatMap(sp.sw.localnet_ports),
@@ -4450,18 +4529,22 @@  for (&Switch(._uuid = ls_uuid)) {
          .external_ids     = map_empty())
 }
 
-Flow(.logical_datapath = ls_uuid,
-     .stage = s_SWITCH_IN_LOOKUP_FDB(),
-     .priority = 100,
-     .__match = "inport == ${sp.json_name}",
-     .actions = "$[rEGBIT_LKUP_FDB()} = lookup_fdb(inport, eth.src); next;",
-     .external_ids = stage_hint(lsp_uuid)),
-Flow(.logical_datapath = ls_uuid,
-     .stage = s_SWITCH_IN_LOOKUP_FDB(),
-     .priority = 100,
-     .__match = "inport == ${sp.json_name} && ${rEGBIT_LKUP_FDB()} == 0",
-     .actions = "put_fdb(inport, eth.src); next;",
-     .external_ids = stage_hint(lsp_uuid)) :-
+FlowWithInOutPort(
+    Flow{.logical_datapath = ls_uuid,
+         .stage = s_SWITCH_IN_LOOKUP_FDB(),
+         .priority = 100,
+         .__match = "inport == ${sp.json_name}",
+         .actions = "$[rEGBIT_LKUP_FDB()} = lookup_fdb(inport, eth.src); next;",
+         .external_ids = stage_hint(lsp_uuid)},
+    sp.lsp.name),
+FlowWithInOutPort(
+    Flow{.logical_datapath = ls_uuid,
+         .stage = s_SWITCH_IN_LOOKUP_FDB(),
+         .priority = 100,
+         .__match = "inport == ${sp.json_name} && ${rEGBIT_LKUP_FDB()} == 0",
+         .actions = "put_fdb(inport, eth.src); next;",
+         .external_ids = stage_hint(lsp_uuid)},
+    sp.lsp.name) :-
     LogicalSwitchPortWithUnknownAddress(ls_uuid, lsp_uuid),
     sp in &SwitchPort(.lsp = &nb::Logical_Switch_Port{._uuid = lsp_uuid, .__type = ""},
                       .ps_addresses = vec_empty()).
@@ -4489,12 +4572,14 @@  Flow(.logical_datapath = ls_uuid,
  *
  * Priority 150 rules drop packets to disabled logical ports, so that they
  * don't even receive multicast or broadcast packets. */
-Flow(.logical_datapath = sw._uuid,
-     .stage            = s_SWITCH_OUT_PORT_SEC_L2(),
-     .priority         = 50,
-     .__match          = __match,
-     .actions          = queue_action ++ "output;",
-     .external_ids     = stage_hint(lsp._uuid)) :-
+FlowWithInOutPort(
+    Flow{.logical_datapath = sw._uuid,
+         .stage            = s_SWITCH_OUT_PORT_SEC_L2(),
+         .priority         = 50,
+         .__match          = __match,
+         .actions          = queue_action ++ "output;",
+         .external_ids     = stage_hint(lsp._uuid)},
+    lsp.name) :-
     &SwitchPort(.sw = sw, .lsp = lsp, .json_name = json_name, .ps_eth_addresses = ps_eth_addresses),
     lsp.is_enabled(),
     lsp.__type != "external",
@@ -4512,12 +4597,14 @@  Flow(.logical_datapath = sw._uuid,
 
 for (&SwitchPort(.lsp = lsp, .json_name = json_name, .sw = sw)
      if not lsp.is_enabled() and lsp.__type != "external") {
-    Flow(.logical_datapath = sw._uuid,
-         .stage            = s_SWITCH_OUT_PORT_SEC_L2(),
-         .priority         = 150,
-         .__match          = "outport == {$json_name}",
-         .actions          = "drop;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = sw._uuid,
+             .stage            = s_SWITCH_OUT_PORT_SEC_L2(),
+             .priority         = 150,
+             .__match          = "outport == {$json_name}",
+             .actions          = "drop;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 for (SwitchPortPSAddresses(.port = &SwitchPort{.lsp = lsp, .json_name = json_name, .sw = sw},
@@ -4544,30 +4631,36 @@  for (SwitchPortPSAddresses(.port = &SwitchPort{.lsp = lsp, .json_name = json_nam
         var __match =
             "outport == ${json_name} && eth.dst == ${ps.ea} && ip4.dst == {255.255.255.255, 224.0.0.0/4, " ++
             addrs.join(", ") ++ "}" in
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
-             .priority         = 90,
-             .__match          = __match,
-             .actions          = "next;",
-             .external_ids     = stage_hint(lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
+                 .priority         = 90,
+                 .__match          = __match,
+                 .actions          = "next;",
+                 .external_ids     = stage_hint(lsp._uuid)},
+            lsp.name)
     };
     if (ps.ipv6_addrs.len() > 0) {
         var __match = "outport == ${json_name} && eth.dst == ${ps.ea}" ++
                       build_port_security_ipv6_flow(Egress, ps.ea, ps.ipv6_addrs) in
-        Flow(.logical_datapath = sw._uuid,
-             .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
-             .priority         = 90,
-             .__match          = __match,
-             .actions          = "next;",
-             .external_ids     = stage_hint(lsp._uuid))
+        FlowWithInOutPort(
+            Flow{.logical_datapath = sw._uuid,
+                 .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
+                 .priority         = 90,
+                 .__match          = __match,
+                 .actions          = "next;",
+                 .external_ids     = stage_hint(lsp._uuid)},
+            lsp.name)
     };
     var __match = "outport == ${json_name} && eth.dst == ${ps.ea} && ip" in
-    Flow(.logical_datapath = sw._uuid,
-         .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
-         .priority         = 80,
-         .__match          = __match,
-         .actions          = "drop;",
-         .external_ids     = stage_hint(lsp._uuid))
+    FlowWithInOutPort(
+        Flow{.logical_datapath = sw._uuid,
+             .stage            = s_SWITCH_OUT_PORT_SEC_IP(),
+             .priority         = 80,
+             .__match          = __match,
+             .actions          = "drop;",
+             .external_ids     = stage_hint(lsp._uuid)},
+        lsp.name)
 }
 
 /* Logical router ingress table ADMISSION: Admission control framework. */
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 5dc910f13..1496a72e5 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -4654,3 +4654,24 @@  AT_CHECK([grep "lr_out_snat" lr0flows | sort], [0], [dnl
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- Add tags to logical flows])
+ovn_start
+
+check ovn-nbctl \
+    -- ls-add sw0 \
+    -- lb-add lb0 10.0.0.10:80 10.0.0.4:8080 \
+    -- ls-lb-add sw0 lb0
+
+check ovn-nbctl lsp-add sw0 lsp0 \
+    -- lsp-set-addresses lsp0 "00:00:00:00:ff:01 1.2.3.4" \
+    -- lsp-set-port-security lsp0 "00:00:00:00:ff:01 1.2.3.4"
+
+check ovn-nbctl --wait=sb sync
+AT_CHECK([ovn-sbctl --columns=tags list logical_flow | grep lsp0 -c], [0], [dnl
+9
+])
+
+AT_CLEANUP
+])