On Thu, Feb 12, 2026 at 1:07 PM Dumitru Ceara <[email protected]> wrote:

> On 2/12/26 3:01 PM, Erlon R. Cruz wrote:
> > At the current state, OVN can not handle fragmented traffic for ACLs
> > in the userspace datapath (DPDK). Just like in the case of LB
> > (commit 20a96b9), the kernel DP will try to reassemble the fragments
> > during CT lookup, however userspace won't reassemble them.
> >
> > This patch allows OVN to handle fragmented traffic by defining a
> > translation table on southbound that leverages OpenFlow connection
> > tracking capabilities. When a stateful flow is created on NB, we add
> > a hint in the flow. This hint will be read in SB and if the
> > connection tracking is set to be used, SB will use the alternative
> > translation table that will use the connection tracking information.
> >
> > This approach should not change the current behavior and it's only
> > enabled if acl_ct_translation is set:
> >
> > ovn-nbctl set NB_Global . options:acl_ct_translation=true
> >
> > Signed-off-by: Erlon R. Cruz <[email protected]>
> > ---
>
> Hi Erlon,
>
> Thanks for the new revision!
>
> I think we're close to getting this patch merged but I have some
> comments, most of them easy to fix, but one more important one about the
> lflow syncing that I think will require a v5.
>
> > v2: Rebased on current upstream main, removed external python
> > code traffic generators, added scenario tests for: dhcp, negative
> > udp and ovn rule generation, documentation and many code clean ups.
> > DHCP traffic is being dropped for some reason. Still need to figure
> > that out for v3.
> > v3: Rebased on current upstream main, clean ups. Fix DHCP/broadcast
> > bug
> > v4: Rebase, code cleanup
> > ---
> >  NEWS                         |  12 +-
> >  controller/lflow.c           |  23 +-
> >  include/ovn/logical-fields.h |   3 +
> >  lib/logical-fields.c         |  67 ++++++
> >  northd/en-global-config.c    |   5 +
> >  northd/lflow-mgr.c           |  63 ++++--
> >  northd/lflow-mgr.h           |   4 +-
> >  northd/northd.c              |  50 ++++-
> >  ovn-nb.xml                   |  23 ++
> >  tests/ovn.at                 |  75 +++++++
> >  tests/system-ovn.at          | 396 +++++++++++++++++++++++++++++++++++
> >  11 files changed, 690 insertions(+), 31 deletions(-)
> >
> > diff --git a/NEWS b/NEWS
> > index bb550fe59..61de2c696 100644
> > --- a/NEWS
> > +++ b/NEWS
> > @@ -98,9 +98,15 @@ Post v25.09.0
> >       reserving an unused IP from the backend's subnet. This change
> allows
> >       using LRP IPs directly, eliminating the need to reserve additional
> IPs
> >       per backend port.
> > -  - Add "distributed" option for load balancer, that forces traffic to
> be
> > -    routed only to backend instances running locally on the same chassis
> > -    it arrives on.
> > +   - Add "distributed" option for load balancer, that forces traffic to
> be
> > +     routed only to backend instances running locally on the same
> chassis
> > +     it arrives on.
> > +   - Fixed support for fragmented traffic in the userspace datapath.
> Added the
> > +     "acl_ct_translation" NB_Global option to enable connection tracking
> > +     based L4 field translation for stateful ACLs. When enabled allows
> proper
> > +     handling of IP fragmentation in userspace datapaths. This option
> breaks
> > +     hardware offloading and is disabled by default.
> > +
> >
> >  OVN v25.09.0 - xxx xx xxxx
> >  --------------------------
> > diff --git a/controller/lflow.c b/controller/lflow.c
> > index b6be5c630..95da1095c 100644
> > --- a/controller/lflow.c
> > +++ b/controller/lflow.c
> > @@ -51,10 +51,19 @@ COVERAGE_DEFINE(consider_logical_flow);
> >  /* Contains "struct expr_symbol"s for fields supported by OVN lflows. */
> >  static struct shash symtab;
> >
> > +/* Alternative symbol table for ACL CT translation.
> > + * This symbol table maps L4 port fields (tcp/udp/sctp) to their
> connection
> > + * tracking equivalents (ct_tp_src/ct_tp_dst with ct_proto predicates).
> > + * This allows matching on all IP fragments (not just the first
> fragment)
> > + * so that all fragments can be matched based on the connection
> tracking state.
> > + */
> > +static struct shash acl_ct_symtab;
> > +
> >  void
> >  lflow_init(void)
> >  {
> >      ovn_init_symtab(&symtab);
> > +    ovn_init_acl_ct_symtab(&acl_ct_symtab);
> >  }
> >
> >  struct lookup_port_aux {
> > @@ -1001,7 +1010,15 @@ convert_match_to_expr(const struct
> sbrec_logical_flow *lflow,
> >                       lflow->match);
> >          return NULL;
> >      }
> > -    struct expr *e = expr_parse_string(lex_str_get(&match_s), &symtab,
> > +
> > +    /* Check if this logical flow requires ACL CT translation.
> > +     * If the tags contains "acl_ct_translation"="true", we use the
> > +     * alternative symbol table that maps L4 fields (tcp/udp/sctp ports)
> > +     * to their CT equivalents. */
> > +    bool ct_trans = smap_get_bool(&lflow->tags, "acl_ct_translation",
> false);
> > +    struct shash *symtab_to_use = ct_trans ? &acl_ct_symtab : &symtab;
> > +
> > +    struct expr *e = expr_parse_string(lex_str_get(&match_s),
> symtab_to_use,
> >                                         addr_sets, port_groups,
> &addr_sets_ref,
> >                                         &port_groups_ref,
> >                                         ldp->datapath->tunnel_key,
> > @@ -1033,7 +1050,7 @@ convert_match_to_expr(const struct
> sbrec_logical_flow *lflow,
> >              e = expr_combine(EXPR_T_AND, e, *prereqs);
> >              *prereqs = NULL;
> >          }
> > -        e = expr_annotate(e, &symtab, &error);
> > +        e = expr_annotate(e, symtab_to_use, &error);
> >      }
> >      if (error) {
> >          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> > @@ -2062,6 +2079,8 @@ lflow_destroy(void)
> >  {
> >      expr_symtab_destroy(&symtab);
> >      shash_destroy(&symtab);
> > +    expr_symtab_destroy(&acl_ct_symtab);
> > +    shash_destroy(&acl_ct_symtab);
> >  }
> >
> >  bool
> > diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> > index 028f5aef7..baed0fa23 100644
> > --- a/include/ovn/logical-fields.h
> > +++ b/include/ovn/logical-fields.h
> > @@ -278,6 +278,9 @@ const char *event_to_string(enum
> ovn_controller_event event);
> >  int string_to_event(const char *s);
> >  const struct ovn_field *ovn_field_from_name(const char *name);
> >
> > +void ovn_init_acl_ct_symtab(struct shash *symtab);
> > +void expr_symtab_remove(struct shash *symtab, const char *name);
> > +
> >  /* OVN CT label values
> >   * ===================
> >   * These are specific ct.label bit values OVN uses to track different
> types
> > diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> > index c8bddcdc5..334a7e244 100644
> > --- a/lib/logical-fields.c
> > +++ b/lib/logical-fields.c
> > @@ -451,3 +451,70 @@ ovn_field_from_name(const char *name)
> >
> >      return shash_find_data(&ovnfield_by_name, name);
> >  }
> > +
> > +/* Removes the symbol with 'name' from 'symtab', freeing its memory. */
> > +void
> > +expr_symtab_remove(struct shash *symtab, const char *name)
>
> This could be static, it's only needed in this file.
>
> > +{
> > +    struct expr_symbol *symbol = shash_find_and_delete(symtab, name);
> > +    if (symbol) {
> > +        free(symbol->name);
> > +        free(symbol->prereqs);
> > +        free(symbol->predicate);
> > +        free(symbol);
> > +    }
> > +}
> > +
> > +/* Initialize a symbol table for ACL CT translation.
> > + * This creates an alternative symbol table that maps L4 port fields
> > + * (tcp/udp/sctp) to their connection tracking equivalents. This allows
> > + * matching on all IP fragments (not just the first) by using CT state
> > + * which is available for all fragments. */
> > +void
> > +ovn_init_acl_ct_symtab(struct shash *acl_symtab)
> > +{
> > +    /* Initialize with the standard symbol table. */
> > +    ovn_init_symtab(acl_symtab);
> > +
> > +    /* Remove the original tcp/udp/sctp symbols that we will override.
> */
> > +    expr_symtab_remove(acl_symtab, "tcp.src");
> > +    expr_symtab_remove(acl_symtab, "tcp.dst");
> > +    expr_symtab_remove(acl_symtab, "tcp");
> > +    expr_symtab_remove(acl_symtab, "udp.src");
> > +    expr_symtab_remove(acl_symtab, "udp.dst");
> > +    expr_symtab_remove(acl_symtab, "udp");
> > +    expr_symtab_remove(acl_symtab, "sctp.src");
> > +    expr_symtab_remove(acl_symtab, "sctp.dst");
> > +    expr_symtab_remove(acl_symtab, "sctp");
> > +
> > +    /* Add ct_proto field - CT original direction protocol. Used in the
> > +     * tcp/udp/sctp predicate expansions below. */
> > +    expr_symtab_add_field(acl_symtab, "ct_proto", MFF_CT_NW_PROTO,
> > +                          "ct.trk", false);
> > +
> > +    /* Override TCP protocol and port fields to use CT equivalents.
> > +     * When "tcp" is used as a predicate, it expands to "ct_proto == 6"
> > +     * instead of "ip.proto == 6". */
> > +    expr_symtab_add_predicate(acl_symtab, "tcp",
> > +                              "ct.trk && !ct.inv && ct_proto == 6");
> > +    expr_symtab_add_field(acl_symtab, "tcp.src", MFF_CT_TP_SRC,
> > +                          "tcp", false);
> > +    expr_symtab_add_field(acl_symtab, "tcp.dst", MFF_CT_TP_DST,
> > +                          "tcp", false);
> > +
> > +    /* Override UDP protocol and port fields */
> > +    expr_symtab_add_predicate(acl_symtab, "udp",
> > +                              "ct.trk && !ct.inv && ct_proto == 17");
> > +    expr_symtab_add_field(acl_symtab, "udp.src", MFF_CT_TP_SRC,
> > +                          "udp", false);
> > +    expr_symtab_add_field(acl_symtab, "udp.dst", MFF_CT_TP_DST,
> > +                          "udp", false);
> > +
> > +    /* Override SCTP protocol and port fields */
> > +    expr_symtab_add_predicate(acl_symtab, "sctp",
> > +                              "ct.trk && !ct.inv && ct_proto == 132");
> > +    expr_symtab_add_field(acl_symtab, "sctp.src", MFF_CT_TP_SRC,
> > +                          "sctp", false);
> > +    expr_symtab_add_field(acl_symtab, "sctp.dst", MFF_CT_TP_DST,
> > +                          "sctp", false);
> > +}
> > diff --git a/northd/en-global-config.c b/northd/en-global-config.c
> > index 2556b2888..a6f37b0c3 100644
> > --- a/northd/en-global-config.c
> > +++ b/northd/en-global-config.c
> > @@ -717,6 +717,11 @@ check_nb_options_out_of_sync(
> >          return true;
> >      }
> >
> > +    if (config_out_of_sync(&nb->options, &config_data->nb_options,
> > +                           "acl_ct_translation", false)) {
> > +        return true;
> > +    }
> > +
> >      return false;
> >  }
> >
> > diff --git a/northd/lflow-mgr.c b/northd/lflow-mgr.c
> > index f21024903..faf2d47ef 100644
> > --- a/northd/lflow-mgr.c
> > +++ b/northd/lflow-mgr.c
> > @@ -56,7 +56,8 @@ static struct ovn_lflow *do_ovn_lflow_add(
> >      const char *actions, const char *io_port,
> >      const char *ctrl_meter,
> >      const struct ovsdb_idl_row *stage_hint,
> > -    const char *where, const char *flow_desc);
> > +    const char *where, const char *flow_desc,
> > +    bool acl_ct_translation);
> >
> >
> >  static struct ovs_mutex *lflow_hash_lock(const struct hmap *lflow_table,
> > @@ -184,6 +185,7 @@ struct ovn_lflow {
> >      struct ovn_dp_group *dpg;    /* Link to unique Sb datapath group. */
> >      const char *where;
> >      const char *flow_desc;
> > +    bool acl_ct_translation;     /* Use CT-based L4 field translation.
> */
> >
> >      struct uuid sb_uuid;         /* SB DB row uuid, specified by
> northd. */
> >      struct ovs_list referenced_by;  /* List of struct lflow_ref_node. */
> > @@ -725,16 +727,17 @@ lflow_ref_sync_lflows(struct lflow_ref *lflow_ref,
> >   * then it may corrupt the hmap.  Caller should ensure thread safety
> >   * for such scenarios.
> >   */
> > -static void
> > +static struct ovn_lflow *
> >  lflow_table_add_lflow__(struct lflow_table *lflow_table,
> > -                       const struct ovn_synced_datapath *sdp,
> > -                       const unsigned long *dp_bitmap, size_t
> dp_bitmap_len,
> > -                       const struct ovn_stage *stage, uint16_t priority,
> > -                       const char *match, const char *actions,
> > -                       const char *io_port, const char *ctrl_meter,
> > -                       const struct ovsdb_idl_row *stage_hint,
> > -                       const char *where, const char *flow_desc,
> > -                       struct lflow_ref *lflow_ref)
> > +                        const struct ovn_synced_datapath *sdp,
> > +                        const unsigned long *dp_bitmap, size_t
> dp_bitmap_len,
> > +                        const struct ovn_stage *stage, uint16_t
> priority,
> > +                        const char *match, const char *actions,
> > +                        const char *io_port, const char *ctrl_meter,
> > +                        const struct ovsdb_idl_row *stage_hint,
> > +                        const char *where, const char *flow_desc,
> > +                        bool acl_ct_translation,
> > +                        struct lflow_ref *lflow_ref)
> >      OVS_EXCLUDED(fake_hash_mutex)
> >  {
> >      struct ovs_mutex *hash_lock;
> > @@ -755,7 +758,8 @@ lflow_table_add_lflow__(struct lflow_table
> *lflow_table,
> >                           sdp ? sparse_array_len(&sdp->dps->dps_array)
> >                               : dp_bitmap_len,
> >                           hash, stage, priority, match, actions,
> > -                         io_port, ctrl_meter, stage_hint, where,
> flow_desc);
> > +                         io_port, ctrl_meter, stage_hint, where,
> flow_desc,
> > +                         acl_ct_translation);
> >
> >      if (lflow_ref) {
> >          struct lflow_ref_node *lrn =
> > @@ -798,6 +802,7 @@ lflow_table_add_lflow__(struct lflow_table
> *lflow_table,
> >      ovn_dp_group_add_with_reference(lflow, sdp, dp_bitmap,
> dp_bitmap_len);
> >
> >      lflow_hash_unlock(hash_lock);
> > +    return lflow;
>
> This function's return value is never used.
>
> >  }
> >
> >  void
> > @@ -815,7 +820,8 @@ lflow_table_add_lflow(struct lflow_table_add_args
> *args)
> >                              args->dp_bitmap_len, args->stage,
> args->priority,
> >                              args->match, args->actions, args->io_port,
> >                              args->ctrl_meter, args->stage_hint,
> args->where,
> > -                            args->flow_desc, args->lflow_ref);
> > +                            args->flow_desc, args->acl_ct_translation,
> > +                            args->lflow_ref);
> >  }
> >
> >  struct ovn_dp_group *
> > @@ -945,6 +951,7 @@ ovn_lflow_init(struct ovn_lflow *lflow,
> >      lflow->stage_hint = stage_hint;
> >      lflow->ctrl_meter = ctrl_meter;
> >      lflow->flow_desc = flow_desc;
> > +    lflow->acl_ct_translation = false;
> >      lflow->dpg = NULL;
> >      lflow->where = where;
> >      lflow->sb_uuid = sbuuid;
> > @@ -1039,7 +1046,8 @@ do_ovn_lflow_add(struct lflow_table *lflow_table,
> size_t dp_bitmap_len,
> >                   uint16_t priority, const char *match, const char
> *actions,
> >                   const char *io_port, const char *ctrl_meter,
> >                   const struct ovsdb_idl_row *stage_hint,
> > -                 const char *where, const char *flow_desc)
> > +                 const char *where, const char *flow_desc,
> > +                 bool acl_ct_translation)
> >      OVS_REQUIRES(fake_hash_mutex)
> >  {
> >      struct ovn_lflow *old_lflow;
> > @@ -1053,6 +1061,7 @@ do_ovn_lflow_add(struct lflow_table *lflow_table,
> size_t dp_bitmap_len,
> >      if (old_lflow) {
> >          dynamic_bitmap_realloc(&old_lflow->dpg_bitmap, dp_bitmap_len);
> >          if (old_lflow->sync_state != LFLOW_STALE) {
> > +            old_lflow->acl_ct_translation = acl_ct_translation;
> >              return old_lflow;
> >          }
> >          sbuuid = old_lflow->sb_uuid;
> > @@ -1069,6 +1078,7 @@ do_ovn_lflow_add(struct lflow_table *lflow_table,
> size_t dp_bitmap_len,
> >                     nullable_xstrdup(ctrl_meter),
> >                     ovn_lflow_hint(stage_hint), where,
> >                     flow_desc, sbuuid);
> > +    lflow->acl_ct_translation = acl_ct_translation;
>
> This should be in ovn_lflow_init().
>
> >
> >      if (parallelization_state != STATE_USE_PARALLELIZATION) {
> >          hmap_insert(&lflow_table->entries, &lflow->hmap_node, hash);
> > @@ -1122,12 +1132,20 @@ sync_lflow_to_sb(struct ovn_lflow *lflow,
> >          sbrec_logical_flow_set_match(sbflow, lflow->match);
> >          sbrec_logical_flow_set_actions(sbflow, lflow->actions);
> >          sbrec_logical_flow_set_flow_desc(sbflow, lflow->flow_desc);
> > -        if (lflow->io_port) {
> > +
> > +        /* Set tags for io_port and/or acl_ct_translation if needed. */
> > +        if (lflow->io_port || lflow->acl_ct_translation) {
> >              struct smap tags = SMAP_INITIALIZER(&tags);
> > -            smap_add(&tags, "in_out_port", lflow->io_port);
> > +            if (lflow->io_port) {
> > +                smap_add(&tags, "in_out_port", lflow->io_port);
> > +            }
> > +            if (lflow->acl_ct_translation) {
> > +                smap_add(&tags, "acl_ct_translation", "true");
> > +            }
> >              sbrec_logical_flow_set_tags(sbflow, &tags);
> >              smap_destroy(&tags);
> >          }
>
> I think the following is a bit more readable:
>
> struct smap tags = SMAP_INITIALIZER(&tags);
>
> if (lflow->io_port) {
>     smap_add(&tags, "acl_ct_translation", "true");
> }
>
> if (lflow->acl_ct_translation) {
>     smap_add(&tags, "acl_ct_translation", "true");
> }
>
> if (!smap_is_empty(&tags)) {
>     sbrec_logical_flow_set_tags(sbflow, &tags);
> }
> smap_destroy(&tags);
>
> > +
> >          sbrec_logical_flow_set_controller_meter(sbflow,
> lflow->ctrl_meter);
> >
> >          /* Trim the source locator lflow->where, which looks something
> like
> > @@ -1193,6 +1211,21 @@ sync_lflow_to_sb(struct ovn_lflow *lflow,
> >                  }
> >              }
> >          }
> > +
> > +        /* Update acl_ct_translation marker in tags if needed.
> > +         * This must be outside ovn_internal_version_changed check
> because
> > +         * the option can be enabled/disabled at runtime. */
> > +        bool cur_has_ct_trans = smap_get_bool(&sbflow->tags,
> > +                                              "acl_ct_translation",
> false);
> > +        if (lflow->acl_ct_translation != cur_has_ct_trans) {
> > +            if (lflow->acl_ct_translation) {
> > +                sbrec_logical_flow_update_tags_setkey(
> > +                    sbflow, "acl_ct_translation", "true");
> > +            } else {
> > +                sbrec_logical_flow_update_tags_delkey(
> > +                    sbflow, "acl_ct_translation");
> > +            }
> > +        }
>
> It's kind of weird that this is needed.
>
> All new lflows have the tag set, if needed.  I think the actual problem
> is when we lookup flows in ovn_lflow_find() we ignore the
> acl_ct_translation value and we match even if that field doesn't match.
>
> I think the correct way of doing things is make ovn_lflow_find() aware
> of acl_ct_translation, so whenever the value changes for a "desired
> lflow" we'll just recreate it.
>
> Then we wouldn't have to update here and we wouldn't need this
> complicated explanation in the comment above either.
>
> Would that work?
>
> >      }
> >
> >      if (lflow->dp) {
> > diff --git a/northd/lflow-mgr.h b/northd/lflow-mgr.h
> > index 4a1655c35..84d0b3e67 100644
> > --- a/northd/lflow-mgr.h
> > +++ b/northd/lflow-mgr.h
> > @@ -24,6 +24,7 @@
> >  struct ovsdb_idl_txn;
> >  struct ovn_datapath;
> >  struct ovsdb_idl_row;
> > +struct ovn_lflow;
> >
> >  /* lflow map which stores the logical flows. */
> >  struct lflow_table {
> > @@ -87,12 +88,13 @@ struct lflow_table_add_args {
> >      const char *flow_desc;
> >      struct lflow_ref *lflow_ref;
> >      const char *where;
> > +    bool acl_ct_translation;
> >  };
> >
> >  void lflow_table_add_lflow(struct lflow_table_add_args *args);
> >
> > -
> >  #define WITH_HINT(HINT) .stage_hint = HINT
> > +#define WITH_CT_TRANSLATION(CT_TRANS) .acl_ct_translation = (CT_TRANS)
> >  /* The IN_OUT_PORT argument 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
> > diff --git a/northd/northd.c b/northd/northd.c
> > index 6d9c67821..a0bc0a789 100644
> > --- a/northd/northd.c
> > +++ b/northd/northd.c
> > @@ -83,12 +83,18 @@ static bool check_lsp_is_up;
> >  static bool install_ls_lb_from_router;
> >
> >  /* Use common zone for SNAT and DNAT if this option is set to "true". */
> > -static bool use_common_zone = false;
> > +static bool use_common_zone;
>
> While I like this change, it's unrelated.  It shouldn't be part of this
> patch.
>
> >
> >  /* If this option is 'true' northd will make use of ct.inv match fields.
> >   * Otherwise, it will avoid using it.  The default is true. */
> >  static bool use_ct_inv_match = true;
> >
> > +/* If this option is 'true' northd will flag the related ACL flows to
> use
> > + * connection tracking fields to properly handle IP fragments. By
> default this
> > + * option is set to 'false'.
> > + */
> > +static bool acl_ct_translation = false;
> > +
>

This and use_common_zone slipped through the cracks. Please let me know if
there's something else other than those and I add to a v6.


> In line with the above, we could skip `= false`, it's in .bss so it will
> be zeroed at startup.  However in OVN we don't really do that so let's
> leave it explicit as you did here.
>
> >  /* If this option is 'true' northd will implicitly add a lowest-priority
> >   * drop rule in the ACL stage of logical switches that have at least one
> >   * ACL.
> > @@ -6301,11 +6307,19 @@ build_ls_stateful_rec_pre_acls(
> >                        "(udp && udp.src == 546 && udp.dst == 547)",
> "next;",
> >                        lflow_ref);
> >
> > -        /* Do not send multicast packets to conntrack. */
> > -        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 110, "eth.mcast",
> > -                      "next;", lflow_ref);
> > -        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
> "eth.mcast",
> > -                      "next;", lflow_ref);
> > +        /* Do not send multicast packets to conntrack unless ACL CT
> > +         * translation is enabled.  When translation is active, L4 port
> > +         * fields are rewritten to their CT equivalents (e.g. udp.dst ->
> > +         * ct_udp.dst), which requires ct.trk to be set.  Skipping CT
> > +         * for multicast would leave ct.trk unset and cause all
> > +         * CT-translated ACL matches to fail for multicast traffic
> > +         * (including DHCP). */
> > +        if (!acl_ct_translation) {
> > +            ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
> "eth.mcast",
> > +                          "next;", lflow_ref);
> > +            ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
> "eth.mcast",
> > +                          "next;", lflow_ref);
> > +        }
> >
> >          /* Ingress and Egress Pre-ACL Table (Priority 100).
> >           *
> > @@ -7281,6 +7295,11 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >          match_tier_len = match->length;
> >      }
> >
> > +    /* Check if this ACL needs CT translation for fragment handling.
> > +     * All stateful ACLs are marked when the option is enabled; the
> actual
> > +     * translation only affects L4 port fields in ovn-controller. */
> > +    bool needs_ct_trans = has_stateful && acl_ct_translation;
> > +
> >      if (!has_stateful
> >          || !strcmp(acl->action, "pass")
> >          || !strcmp(acl->action, "allow-stateless")) {
> > @@ -7298,6 +7317,7 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >
> >          ds_put_cstr(actions, "next;");
> >          ds_put_format(match, "(%s)", acl->match);
> > +        /* Stateless ACLs don't need CT translation. */
> >          ovn_lflow_add(lflows, od, stage, priority, ds_cstr(match),
> >                        ds_cstr(actions), lflow_ref,
> WITH_HINT(&acl->header_));
> >          return;
> > @@ -7367,7 +7387,9 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >          }
> >          ds_put_cstr(actions, "next;");
> >          ovn_lflow_add(lflows, od, stage, priority, ds_cstr(match),
> > -                      ds_cstr(actions), lflow_ref,
> WITH_HINT(&acl->header_));
> > +                      ds_cstr(actions), lflow_ref,
> > +                      WITH_HINT(&acl->header_),
> > +                      WITH_CT_TRANSLATION(needs_ct_trans));
> >
> >          /* Match on traffic in the request direction for an established
> >           * connection tracking entry that has not been marked for
> > @@ -7397,7 +7419,9 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >          }
> >          ds_put_cstr(actions, "next;");
> >          ovn_lflow_add(lflows, od, stage, priority, ds_cstr(match),
> > -                      ds_cstr(actions), lflow_ref,
> WITH_HINT(&acl->header_));
> > +                      ds_cstr(actions), lflow_ref,
> > +                      WITH_HINT(&acl->header_),
> > +                      WITH_CT_TRANSLATION(needs_ct_trans));
> >      } else if (!strcmp(acl->action, "drop")
> >                 || !strcmp(acl->action, "reject")) {
> >          if (acl->network_function_group) {
> > @@ -7423,7 +7447,9 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >                                        obs_stage);
> >          ds_put_cstr(actions, "next;");
> >          ovn_lflow_add(lflows, od, stage, priority, ds_cstr(match),
> > -                      ds_cstr(actions), lflow_ref,
> WITH_HINT(&acl->header_));
> > +                      ds_cstr(actions), lflow_ref,
> > +                      WITH_HINT(&acl->header_),
> > +                      WITH_CT_TRANSLATION(needs_ct_trans));
> >          /* For an existing connection without ct_mark.blocked set, we've
> >           * encountered a policy change. ACLs previously allowed
> >           * this connection and we committed the connection tracking
> > @@ -7449,7 +7475,9 @@ consider_acl(struct lflow_table *lflows, const
> struct ovn_datapath *od,
> >                        "ct_commit { ct_mark.blocked = 1; "
> >                        "ct_label.obs_point_id = %"PRIu32"; }; next;",
> obs_pid);
> >          ovn_lflow_add(lflows, od, stage, priority, ds_cstr(match),
> > -                      ds_cstr(actions), lflow_ref,
> WITH_HINT(&acl->header_));
> > +                      ds_cstr(actions), lflow_ref,
> > +                      WITH_HINT(&acl->header_),
> > +                      WITH_CT_TRANSLATION(needs_ct_trans));
> >      }
> >  }
> >
> > @@ -20696,6 +20724,8 @@ ovnnb_db_run(struct northd_input *input_data,
> >
> >      use_ct_inv_match = smap_get_bool(input_data->nb_options,
> >                                       "use_ct_inv_match", true);
> > +    acl_ct_translation = smap_get_bool(input_data->nb_options,
> > +                                       "acl_ct_translation", false);
> >
> >      /* deprecated, use --event instead */
> >      controller_event_en = smap_get_bool(input_data->nb_options,
> > diff --git a/ovn-nb.xml b/ovn-nb.xml
> > index aab091883..8ea0e0267 100644
> > --- a/ovn-nb.xml
> > +++ b/ovn-nb.xml
> > @@ -327,6 +327,29 @@
> >          </p>
> >        </column>
> >
> > +      <column name="options" key="acl_ct_translation">
> > +        <p>
> > +          If set to <code>true</code>, <code>ovn-northd</code> will
> enable
> > +          connection tracking based L4 field translation for stateful
> ACLs.
> > +          When enabled, ACL matches on L4 port fields (tcp/udp/sctp) use
> > +          connection tracking state instead of packet headers. This
> allows
> > +          proper handling of IP fragmentation in userspace datapaths
> (e.g.,
> > +          DPDK) where fragments are not automatically reassembled during
> > +          connection tracking lookup.
> > +        </p>
> > +        <p>
> > +          <em>Important:</em> Enabling this option breaks hardware
> offloading
> > +          of flows. It also disables the multicast conntrack bypass,
> which
> > +          means multicast traffic (including DHCP) will go through
> connection
> > +          tracking. This may have a performance impact on
> multicast-heavy
> > +          workloads. Only enable this option if you need to handle
> fragmented
> > +          traffic with stateful ACLs in userspace datapaths.
> > +        </p>
> > +        <p>
> > +          The default value is <code>false</code>.
> > +        </p>
> > +      </column>
> > +
> >        <column name="options" key="default_acl_drop">
> >          <p>
> >            If set to <code>true</code>., <code>ovn-northd</code> will
> > diff --git a/tests/ovn.at b/tests/ovn.at
> > index 802e6d0da..0f63e110b 100644
> > --- a/tests/ovn.at
> > +++ b/tests/ovn.at
> > @@ -44594,3 +44594,78 @@ check ovn-nbctl --wait=hv lsp-set-type down_ext
> localnet
> >  OVN_CLEANUP([hv1],[hv2])
> >  AT_CLEANUP
> >  ])
> > +
> > +OVN_FOR_EACH_NORTHD([
> > +AT_SETUP([ACL CT translation - Rules])
> > +AT_KEYWORDS([acl_ct_translation_rules])
> > +ovn_start
> > +
> > +check ovn-nbctl ls-add ls1
> > +
> > +check ovn-nbctl lsp-add ls1 lp1 \
> > +    -- lsp-set-addresses lp1 "f0:00:00:00:00:01 10.0.0.1"
> > +check ovn-nbctl lsp-add ls1 lp2 \
> > +    -- lsp-set-addresses lp2 "f0:00:00:00:00:02 10.0.0.2"
> > +
> > +net_add n1
> > +sim_add hv1
> > +
> > +as hv1
> > +ovs-vsctl add-br br-phys
> > +ovn_attach n1 br-phys 192.168.0.1
> > +ovs-vsctl -- add-port br-int hv1-vif1 -- \
> > +    set interface hv1-vif1 external-ids:iface-id=lp1
> > +ovs-vsctl -- add-port br-int hv1-vif2 -- \
> > +    set interface hv1-vif2 external-ids:iface-id=lp2
>
> Please add "check" in front of the ovs-vsctl commands.
>
> > +
> > +# Get the OF table numbers for ACL evaluation stages
>
> Comments should be sentences and end with period.  This applies to all
> comments in this test.
>
> > +acl_in_eval=$(ovn-debug lflow-stage-to-oftable ls_in_acl_eval)
> > +acl_out_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
> > +
> > +# Create port group and add ACLs with TCP/UDP matches
> > +lp1_uuid=$(fetch_column nb:logical_switch_port _uuid name=lp1)
> > +lp2_uuid=$(fetch_column nb:logical_switch_port _uuid name=lp2)
> > +check ovn-nbctl pg-add pg1 $lp1_uuid $lp2_uuid
> > +
> > +check ovn-nbctl acl-add pg1 from-lport 1002 \
> > +    "inport == @pg1 && ip4 && tcp && tcp.dst == 80" allow-related
> > +check ovn-nbctl acl-add pg1 from-lport 1002 \
> > +    "inport == @pg1 && ip4 && udp && udp.dst == 53" allow-related
> > +check ovn-nbctl acl-add pg1 to-lport 1002 \
> > +    "outport == @pg1 && ip4 && tcp && tcp.src == 80" allow-related
> > +check ovn-nbctl acl-add pg1 to-lport 1002 \
> > +    "outport == @pg1 && ip4 && udp && udp.src == 53" allow-related
> > +check ovn-nbctl --wait=hv --log acl-add pg1 to-lport 100 "outport ==
> @pg1" drop
> > +
> > +# Verify lflows do NOT have acl_ct_translation tag when option is
> disabled (default)
> > +check_row_count Logical_Flow 0 'tags:acl_ct_translation="true"'
> > +
> > +# Verify OpenFlows use packet header fields (tcp.dst, udp.dst) not CT
> fields
> > +# When CT translation is OFF, we should see tp_dst matches in ACL tables
> > +as hv1
> > +AT_CHECK([ovs-ofctl dump-flows br-int table=$acl_in_eval | grep -q
> "tp_dst=80"], [0])
> > +
> > +# Now enable ACL CT translation
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=true
> > +
> > +# Verify lflows now have acl_ct_translation tag in the tags column
> > +wait_row_count Logical_Flow 10 'tags:acl_ct_translation="true"'
> > +
> > +# Verify OpenFlows now use CT fields (ct_tp_dst) instead of packet
> headers
> > +# When CT translation is ON, we should see ct_tp_dst matches in ACL
> tables
> > +as hv1
> > +AT_CHECK([ovs-ofctl dump-flows br-int table=$acl_in_eval | grep -q
> "ct_tp_dst=80"], [0])
> > +
> > +# Now disable ACL CT translation again
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=false
> > +
> > +# Verify lflows no longer have acl_ct_translation tag
> > +wait_row_count Logical_Flow 0 'tags:acl_ct_translation="true"'
> > +
> > +# Verify OpenFlows reverted to packet header fields in ACL tables
> > +as hv1
> > +AT_CHECK([ovs-ofctl dump-flows br-int table=$acl_in_eval | grep -q
> "tp_dst=80"], [0])
> > +
> > +OVN_CLEANUP([hv1])
> > +AT_CLEANUP
> > +])
> > diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> > index c732af9b9..b76822103 100644
> > --- a/tests/system-ovn.at
> > +++ b/tests/system-ovn.at
> > @@ -20997,3 +20997,399 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query
> port patch-.*/d
> >  /connection dropped.*/d"])
> >  AT_CLEANUP
> >  ])
> > +
> > +OVN_FOR_EACH_NORTHD([
> > +AT_SETUP([ACL CT translation - UDP fragmentation])
> > +AT_KEYWORDS([acl_ct_translation_udp_fragmentation])
> > +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
> > +
> > +CHECK_CONNTRACK()
> > +
> > +ovn_start
> > +OVS_TRAFFIC_VSWITCHD_START()
> > +ADD_BR([br-int])
> > +
> > +# Set external-ids in br-int needed for ovn-controller
> > +check ovs-vsctl \
> > +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> > +        -- set Open_vSwitch .
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> > +        -- set bridge br-int fail-mode=secure
> other-config:disable-in-band=true
> > +
> > +# Start ovn-controller
> > +start_daemon ovn-controller
> > +
> > +# Set the minimal fragment size for userspace DP.
> > +ovs-appctl dpctl/ipf-set-min-frag v4 500
> > +
> > +check ovn-nbctl ls-add internal
> > +check ovn-nbctl lsp-add internal client \
> > +    -- lsp-set-addresses client "f0:00:00:01:02:03 172.16.1.3"
> > +check ovn-nbctl lsp-add internal server \
> > +    -- lsp-set-addresses server "f0:00:0f:01:02:03 172.16.1.2"
> > +
> > +ADD_NAMESPACES(client)
> > +ADD_VETH(client, client, br-int, "172.16.1.3/24", "f0:00:00:01:02:03",
> \
> > +         "172.16.1.1")
> > +NS_EXEC([client], [ip l set dev client mtu 900])
> > +
> > +ADD_NAMESPACES(server)
> > +ADD_VETH(server, server, br-int, "172.16.1.2/24", "f0:00:0f:01:02:03",
> \
> > +         "172.16.1.1")
> > +NS_EXEC([server], [ip l set dev server mtu 900])
> > +
> > +# Create data file for traffic testing (8000 bytes will be fragmented
> with MTU 900)
> > +printf %8000s > datafile
> > +
> > +# Create port group with both client and server
> > +client_uuid=$(fetch_column nb:logical_switch_port _uuid name=client)
> > +server_uuid=$(fetch_column nb:logical_switch_port _uuid name=server)
> > +check ovn-nbctl pg-add internal_vms $client_uuid $server_uuid
> > +
> > +# ACL rules - allow outgoing traffic
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4 && udp" allow-related
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4" allow-related
> > +
> > +# ACL rules - allow incoming UDP on specific ports
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && ip4 && udp && udp.dst == 5060" allow-related
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && ip4 && udp && udp.dst == 5061" allow-related
> > +
> > +# Allow ARP
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && arp" allow-related
> > +
> > +# Drop rule
> > +check ovn-nbctl --log --severity=info acl-add internal_vms to-lport 100
> "outport == @internal_vms" drop
> > +
> > +# Enable ACL CT translation for fragmentation handling
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=true
> > +
> > +check ovn-nbctl --wait=hv sync
> > +check ovs-appctl dpctl/flush-conntrack
> > +
> > +# Start tcpdump to capture IP fragments on both sides
> > +NETNS_START_TCPDUMP([server], [-U -i server -Q in -nn ip and
> '(ip[[6:2]] & 0x3fff != 0)'], [tcpdump-udp-server])
> > +NETNS_START_TCPDUMP([client], [-U -i client -Q in -nn ip and
> '(ip[[6:2]] & 0x3fff != 0)'], [tcpdump-udp-client])
> > +
> > +# Start UDP listeners on both sides
> > +NETNS_DAEMONIZE([server], [nc -l -u 172.16.1.2 5060 > udp_server.rcvd],
> [server.pid])
> > +NETNS_DAEMONIZE([client], [nc -l -u 172.16.1.3 5061 > udp_client.rcvd],
> [client.pid])
> > +
> > +# Client sends to server (will be fragmented due to MTU 900)
> > +NS_CHECK_EXEC([client], [cat datafile | nc -w 1 -u 172.16.1.2 5060],
> [0], [ignore], [ignore])
> > +
> > +# Server sends to client (will be fragmented due to MTU 900)
> > +NS_CHECK_EXEC([server], [cat datafile | nc -w 1 -u 172.16.1.3 5061],
> [0], [ignore], [ignore])
> > +
> > +OVS_WAIT_UNTIL([test -s udp_server.rcvd])
> > +OVS_WAIT_UNTIL([test -s udp_client.rcvd])
> > +
> > +# Verify both sides received data
> > +check cmp datafile udp_server.rcvd
> > +check cmp datafile udp_client.rcvd
> > +
> > +# Verify IP fragments were received on both sides (at least 5 fragments
> confirms fragmentation)
> > +OVS_WAIT_UNTIL([test $(cat tcpdump-udp-server.tcpdump | wc -l) -ge 5])
> > +OVS_WAIT_UNTIL([test $(cat tcpdump-udp-client.tcpdump | wc -l) -ge 5])
> > +
> > +OVN_CLEANUP_CONTROLLER([hv1])
> > +OVN_CLEANUP_NORTHD
> > +
> > +as
> > +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> > +/connection dropped.*/d
> > +/WARN|netdev@ovs-netdev: execute.*/d
> > +/dpif|WARN|system@ovs-system: execute.*/d
> > +"])
> > +AT_CLEANUP
> > +])
> > +
> > +OVN_FOR_EACH_NORTHD([
> > +AT_SETUP([ACL CT translation - TCP traffic])
> > +AT_KEYWORDS([acl_ct_translation_tcp_fragmentation])
> > +
> > +CHECK_CONNTRACK()
> > +
> > +ovn_start
> > +OVS_TRAFFIC_VSWITCHD_START()
> > +ADD_BR([br-int])
> > +
> > +# Set external-ids in br-int needed for ovn-controller
> > +check ovs-vsctl \
> > +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> > +        -- set Open_vSwitch .
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> > +        -- set bridge br-int fail-mode=secure
> other-config:disable-in-band=true
> > +
> > +# Start ovn-controller
> > +start_daemon ovn-controller
> > +
> > +check ovn-nbctl ls-add internal
> > +check ovn-nbctl lsp-add internal client \
> > +    -- lsp-set-addresses client "f0:00:00:01:02:03 172.16.1.3"
> > +check ovn-nbctl lsp-add internal server \
> > +    -- lsp-set-addresses server "f0:00:0f:01:02:03 172.16.1.2"
> > +
> > +ADD_NAMESPACES(client)
> > +ADD_VETH(client, client, br-int, "172.16.1.3/24", "f0:00:00:01:02:03",
> \
> > +         "172.16.1.1")
> > +
> > +ADD_NAMESPACES(server)
> > +ADD_VETH(server, server, br-int, "172.16.1.2/24", "f0:00:0f:01:02:03",
> \
> > +         "172.16.1.1")
> > +
> > +# Create data file for traffic testing
> > +printf %8000s > datafile
> > +
> > +# Create port group with both client and server
> > +client_uuid=$(fetch_column nb:logical_switch_port _uuid name=client)
> > +server_uuid=$(fetch_column nb:logical_switch_port _uuid name=server)
> > +check ovn-nbctl pg-add internal_vms $client_uuid $server_uuid
> > +
> > +# ACL rules - allow outgoing traffic
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4 && tcp" allow-related
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4" allow-related
> > +
> > +# ACL rules - allow incoming TCP on specific ports
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && ip4 && tcp && tcp.dst == 8080" allow-related
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && ip4 && tcp && tcp.dst == 8081" allow-related
> > +
> > +# Allow ARP
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && arp" allow-related
> > +
> > +# Drop rule
> > +check ovn-nbctl --log --severity=info acl-add internal_vms to-lport 100
> "outport == @internal_vms" drop
> > +
> > +# Enable ACL CT translation
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=true
> > +
> > +check ovn-nbctl --wait=hv sync
> > +check ovs-appctl dpctl/flush-conntrack
> > +
> > +# Test client -> server direction
> > +NETNS_DAEMONIZE([server], [nc -l 172.16.1.2 8080 > tcp_server.rcvd],
> [server.pid])
> > +NS_CHECK_EXEC([client], [nc -w 1 172.16.1.2 8080 < datafile], [0],
> [ignore], [ignore])
> > +
> > +OVS_WAIT_UNTIL([test -s tcp_server.rcvd])
> > +check cmp datafile tcp_server.rcvd
> > +
> > +# Clean up first direction
> > +kill $(cat server.pid) 2>/dev/null || true
> > +
> > +# Test server -> client direction
> > +NETNS_DAEMONIZE([client], [nc -l 172.16.1.3 8081 > tcp_client.rcvd],
> [client.pid])
> > +NS_CHECK_EXEC([server], [nc -w 1 172.16.1.3 8081 < datafile], [0],
> [ignore], [ignore])
> > +
> > +OVS_WAIT_UNTIL([test -s tcp_client.rcvd])
> > +check cmp datafile tcp_client.rcvd
> > +
> > +OVN_CLEANUP_CONTROLLER([hv1])
> > +OVN_CLEANUP_NORTHD
> > +
> > +as
> > +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> > +/connection dropped.*/d
> > +"])
> > +AT_CLEANUP
> > +])
> > +
> > +OVN_FOR_EACH_NORTHD([
> > +AT_SETUP([ACL CT translation - negative test])
> > +AT_KEYWORDS([acl_ct_translation_udp_fragmentation_negative])
> > +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
> > +
> > +CHECK_CONNTRACK()
> > +
> > +ovn_start
> > +OVS_TRAFFIC_VSWITCHD_START()
> > +ADD_BR([br-int])
> > +
> > +# Set external-ids in br-int needed for ovn-controller
> > +check ovs-vsctl \
> > +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> > +        -- set Open_vSwitch .
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> > +        -- set bridge br-int fail-mode=secure
> other-config:disable-in-band=true
> > +
> > +# Start ovn-controller
> > +start_daemon ovn-controller
> > +
> > +check ovn-nbctl ls-add internal
> > +check ovn-nbctl lsp-add internal client \
> > +    -- lsp-set-addresses client "f0:00:00:01:02:03 172.16.1.3"
> > +check ovn-nbctl lsp-add internal server \
> > +    -- lsp-set-addresses server "f0:00:0f:01:02:03 172.16.1.2"
> > +
> > +ADD_NAMESPACES(client)
> > +ADD_VETH(client, client, br-int, "172.16.1.3/24", "f0:00:00:01:02:03",
> \
> > +         "172.16.1.1")
> > +
> > +ADD_NAMESPACES(server)
> > +ADD_VETH(server, server, br-int, "172.16.1.2/24", "f0:00:0f:01:02:03",
> \
> > +         "172.16.1.1")
> > +
> > +# Create port group with both client and server
> > +client_uuid=$(fetch_column nb:logical_switch_port _uuid name=client)
> > +server_uuid=$(fetch_column nb:logical_switch_port _uuid name=server)
> > +check ovn-nbctl pg-add internal_vms $client_uuid $server_uuid
> > +
> > +# ACL rules - allow outgoing traffic
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4 && udp" allow-related
> > +check ovn-nbctl acl-add internal_vms from-lport 1002 "inport ==
> @internal_vms && ip4" allow-related
> > +
> > +# ACL rules - allow incoming UDP ONLY on port 5060 (NOT 4000)
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && ip4 && udp && udp.dst == 5060" allow-related
> > +
> > +# Allow ARP
> > +check ovn-nbctl acl-add internal_vms to-lport 1002 "outport ==
> @internal_vms && arp" allow-related
> > +
> > +# Drop rule - this should drop traffic on port 4000
> > +check ovn-nbctl --log --severity=info acl-add internal_vms to-lport 100
> "outport == @internal_vms" drop
> > +
> > +# Enable ACL CT translation
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=true
> > +
> > +check ovn-nbctl --wait=hv sync
> > +check ovs-appctl dpctl/flush-conntrack
> > +
> > +# Start tcpdump to capture any incoming packets on server
> > +NETNS_START_TCPDUMP([server], [-U -i server -Q in -nn udp port 4000],
> [tcpdump-drop-server])
> > +
> > +# Start UDP listener on server (on port 4000 which is NOT allowed by
> ACLs)
> > +NETNS_DAEMONIZE([server], [nc -l -u 172.16.1.2 4000 > udp_drop.rcvd],
> [drop_server.pid])
> > +
> > +# Client sends to server on disallowed port
> > +NS_CHECK_EXEC([client], [echo "test" | nc -w 1 -u 172.16.1.2 4000],
> [0], [ignore], [ignore])
> > +
> > +# Wait a bit for any packets to arrive
> > +sleep 2
> > +
> > +# Verify server did NOT receive any data (file should be empty or not
> exist)
> > +AT_CHECK([test ! -s udp_drop.rcvd], [0])
> > +
> > +# Verify tcpdump captured no packets (traffic was dropped by ACL)
> > +AT_CHECK([test $(cat tcpdump-drop-server.tcpdump | wc -l) -eq 0], [0])
> > +
> > +OVN_CLEANUP_CONTROLLER([hv1])
> > +OVN_CLEANUP_NORTHD
> > +
> > +as
> > +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> > +/connection dropped.*/d
> > +"])
> > +AT_CLEANUP
> > +])
> > +
> > +OVN_FOR_EACH_NORTHD([
> > +AT_SETUP([ACL CT translation with DHCP traffic])
> > +AT_KEYWORDS([acl_ct_translation_dhcp])
> > +AT_SKIP_IF([test $HAVE_DHCPD = no])
> > +AT_SKIP_IF([test $HAVE_DHCLIENT = no])
> > +
> > +CHECK_CONNTRACK()
> > +
> > +ovn_start
> > +OVS_TRAFFIC_VSWITCHD_START()
> > +ADD_BR([br-int])
> > +
> > +# Set external-ids in br-int needed for ovn-controller
> > +check ovs-vsctl \
> > +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> > +        -- set Open_vSwitch .
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> > +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> > +        -- set bridge br-int fail-mode=secure
> other-config:disable-in-band=true
> > +
> > +# Start ovn-controller
> > +start_daemon ovn-controller
> > +
> > +# Create logical switch
> > +check ovn-nbctl ls-add ls1
> > +
> > +# Create DHCP server port
> > +check ovn-nbctl lsp-add ls1 dhcp-server \
> > +    -- lsp-set-addresses dhcp-server "00:00:00:00:02:00 192.168.1.2"
> > +
> > +# Create DHCP client port
> > +check ovn-nbctl lsp-add ls1 dhcp-client \
> > +    -- lsp-set-addresses dhcp-client "00:00:00:00:02:01"
> > +
> > +# Add OVS ports
> > +ADD_NAMESPACES(server, client)
> > +ADD_VETH(server, server, br-int, "192.168.1.2/24", "00:00:00:00:02:00")
> > +ADD_VETH(client, client, br-int, "0.0.0.0/0", "00:00:00:00:02:01")
> > +
> > +# Bind logical ports to OVS ports
> > +check ovs-vsctl set Interface ovs-server
> external_ids:iface-id=dhcp-server
> > +check ovs-vsctl set Interface ovs-client
> external_ids:iface-id=dhcp-client
> > +
> > +# Create port group with both ports
> > +server_uuid=$(fetch_column nb:logical_switch_port _uuid
> name=dhcp-server)
> > +client_uuid=$(fetch_column nb:logical_switch_port _uuid
> name=dhcp-client)
> > +check ovn-nbctl pg-add dhcp_vms $server_uuid $client_uuid
> > +
> > +# Add ACL rules - allow outgoing traffic from both ports
> > +check ovn-nbctl acl-add dhcp_vms from-lport 1002 "inport == @dhcp_vms
> && ip4" allow-related
> > +check ovn-nbctl acl-add dhcp_vms from-lport 1002 "inport == @dhcp_vms
> && udp" allow-related
> > +
> > +# Add ACL rules for DHCP traffic (ports 67 and 68)
> > +check ovn-nbctl acl-add dhcp_vms to-lport 1002 "outport == @dhcp_vms &&
> ip4 && udp && udp.dst == 67" allow-related
> > +check ovn-nbctl acl-add dhcp_vms to-lport 1002 "outport == @dhcp_vms &&
> ip4 && udp && udp.dst == 68" allow-related
> > +
> > +# Allow ARP and broadcast
> > +check ovn-nbctl acl-add dhcp_vms to-lport 1002 "outport == @dhcp_vms &&
> arp" allow-related
> > +
> > +# Add drop rule
> > +check ovn-nbctl --log --severity=info acl-add dhcp_vms to-lport 100
> "outport == @dhcp_vms" drop
> > +
> > +# Enable ACL CT translation
> > +check ovn-nbctl --wait=hv set NB_Global .
> options:acl_ct_translation=true
> > +
> > +# Wait for flows to be installed
> > +check ovn-nbctl --wait=hv sync
> > +
> > +# Setup DHCP server configuration
> > +DHCP_TEST_DIR="$ovs_base/dhcp-test"
> > +mkdir -p $DHCP_TEST_DIR
> > +
> > +cat > $DHCP_TEST_DIR/dhcpd.conf <<EOF
> > +subnet 192.168.1.0 netmask 255.255.255.0 {
> > +  range 192.168.1.100 192.168.1.100;
> > +  option routers 192.168.1.2;
> > +  option broadcast-address 192.168.1.255;
> > +  default-lease-time 60;
> > +  max-lease-time 120;
> > +}
> > +EOF
> > +
> > +touch $DHCP_TEST_DIR/dhcpd.leases
> > +chown root:dhcpd $DHCP_TEST_DIR $DHCP_TEST_DIR/dhcpd.leases
> > +chmod 775 $DHCP_TEST_DIR
> > +chmod 664 $DHCP_TEST_DIR/dhcpd.leases
> > +
> > +# Start dhcpd as DHCP server in the server namespace
> > +NETNS_DAEMONIZE([server], [dhcpd -4 -f -cf $DHCP_TEST_DIR/dhcpd.conf
> server > $DHCP_TEST_DIR/dhcpd.log 2>&1], [dhcpd.pid])
> > +
> > +# Give dhcpd time to start
> > +sleep 1
> > +
> > +# Request IP via DHCP using dhclient
> > +NS_CHECK_EXEC([client], [dhclient -1 -v -lf
> $DHCP_TEST_DIR/dhclient.lease -pf $DHCP_TEST_DIR/dhclient.pid client], [0],
> [ignore], [ignore])
> > +# Register cleanup handler to kill dhclient when test exits
> > +on_exit 'kill $(cat $DHCP_TEST_DIR/dhclient.pid) 2>/dev/null || true'
> > +
> > +# Verify client got an IP address from DHCP
> > +NS_CHECK_EXEC([client], [ip addr show client | grep -q
> "192.168.1.100"], [0])
> > +
> > +OVN_CLEANUP_CONTROLLER([hv1])
> > +
> > +OVN_CLEANUP_NORTHD
> > +
> > +as
> > +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> > +/connection dropped.*/d
> > +"])
> > +AT_CLEANUP
> > +])
>
> Regards,
> Dumitru
>
>
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to