On 8/27/20 1:39 AM, Han Zhou wrote:
> Hi Dumitru,
> 
> Please see my comments below.
> 
> Thanks,
> Han

Hi Han,

Thanks for your review.

> 
> On Thu, Aug 20, 2020 at 4:19 AM Dumitru Ceara <[email protected]
> <mailto:[email protected]>> wrote:
>>
>> A new table is added to OVN_Northbound: Stateless_Filter. Users can
>> populate this table with records consisting of <priority, match>. These
>> records generate logical flows in the PRE_ACL stages of the logical
>> switch pipeline.
>>
>> Packets matching these flows will completely bypass connection tracking
>> for ACL purposes. In specific scenarios CMSs can predetermine which
>> traffic must be firewalled statefully or not, e.g., UDP vs TCP. However,
>> until now, if at least one stateful ACL (allow-related) is configured
>> on the switch, all traffic gets sent to connection tracking.
>> This induces a hit in performance when forwarding packets that don't
>> need stateful processing.
>>
>> New command line arguments are added to ovn-nbctl (stateless-filter-*)
>> to allow the users to interact with the Stateless_Filter table.
>>
>> Signed-off-by: Dumitru Ceara <[email protected]
> <mailto:[email protected]>>
>> ---
>> V2:
>> - address Numan's comments:
>>   - fix spacing in the logical flow match.
>>   - add a new table to the NB DB instead of using a config option on the
>>     logical switch.
>> - add ovn-nbctl CLI commands for the new table and also unit tests for
>>   them.
>> - reword the commit message.
>> NOTE: checkpatch.py will complain about lines lacking whitespacec around
>> operators in the ovn-nbctl help string but this is a false positive and
>> should be ignored.
>> ---
>>  NEWS                          |   3 +
>>  northd/ovn-northd.8.xml       |  20 ++++
>>  northd/ovn-northd.c           | 146 ++++++++++++++++++-----
>>  ovn-nb.ovsschema              |  26 ++++-
>>  ovn-nb.xml                    |  57 ++++++++-
>>  tests/ovn-nbctl.at <http://ovn-nbctl.at>            |  53 +++++++++
>>  tests/ovn-northd.at <http://ovn-northd.at>           | 263
> ++++++++++++++++++++++++++++++++++++++++++
>>  tests/system-common-macros.at <http://system-common-macros.at> |   8 ++
>>  tests/system-ovn.at <http://system-ovn.at>           | 113
> ++++++++++++++++++
>>  utilities/ovn-detrace.in <http://ovn-detrace.in>      |  12 ++
>>  utilities/ovn-nbctl.c         | 213 ++++++++++++++++++++++++++++++++--
>>  11 files changed, 871 insertions(+), 43 deletions(-)
>>
>> diff --git a/NEWS b/NEWS
>> index a1ce4e8..eedd091 100644
>> --- a/NEWS
>> +++ b/NEWS
>> @@ -11,6 +11,9 @@ Post-v20.06.0
>>       called Chassis_Private now contains the nb_cfg column which is
> updated
>>       by incrementing the value in the NB_Global table, CMSes relying on
>>       this mechanism should update their code to use this new table.
>> +   - Added support for bypassing connection tracking for ACL
> processing for
>> +     specific types of traffic through the user supplied Stateless_Filter
>> +     configuration.
>>
>>  OVN v20.06.0
>>  --------------------------
>> diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
>> index 989e364..1f89942 100644
>> --- a/northd/ovn-northd.8.xml
>> +++ b/northd/ovn-northd.8.xml
>> @@ -322,6 +322,16 @@
>>      </p>
>>
>>      <p>
>> +      For each record in table <code>Stateful_Filter</code> in the
>> +      <code>OVN_Northbound</code> database, a flow with
>> +      <code>priority + 1000</code> is added and sets <code>reg0[7] =
> 1</code>
>> +      for traffic that matches the condition in the <code>match</code>
>> +      column and advances to next table.  <code>reg0[7]</code> acts
> as a hint
>> +      for tables <code>Pre-Stateful</code> and <code>ACL</code> to avoid
>> +      sending this traffic to the connection tracker.
>> +    </p>
>> +
> 
> It seems documentation is missing for the flows that uses reg0[7] in
> Pre-Stateful and ACL stages.
> 

Ack, I'll add the documentation for ACL stage but in Pre-Stateful I
didn't use reg0[7] (REGBIT_SKIP_ACL_CT).

>> +    <p>
>>        This table also has a priority-110 flow with the match
>>        <code>eth.dst == <var>E</var></code> for all logical switch
>>        datapaths to move traffic to the next table. Where <var>E</var>
>> @@ -1383,6 +1393,16 @@ output;
>>      </p>
>>
>>      <p>
>> +      For each record in table <code>Stateful_Filter</code> in the
>> +      <code>OVN_Northbound</code> database, a flow with
>> +      <code>priority + 1000</code> is added and sets <code>reg0[7] =
> 1</code>
>> +      for traffic that matches the condition in the <code>match</code>
>> +      column and advances to next table.  <code>reg0[7]</code> acts
> as a hint
>> +      for tables <code>Pre-Stateful</code> and <code>ACL</code> to avoid
>> +      sending this traffic to the connection tracker.
>> +    </p>
>> +
>> +    <p>
>>        This table also has a priority-110 flow with the match
>>        <code>eth.src == <var>E</var></code> for all logical switch
>>        datapaths to move traffic to the next table. Where <var>E</var>
>> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
>> index 212de2f..b8f457b 100644
>> --- a/northd/ovn-northd.c
>> +++ b/northd/ovn-northd.c
>> @@ -211,6 +211,7 @@ enum ovn_stage {
>>  #define REGBIT_DNS_LOOKUP_RESULT "reg0[4]"
>>  #define REGBIT_ND_RA_OPTS_RESULT "reg0[5]"
>>  #define REGBIT_HAIRPIN           "reg0[6]"
>> +#define REGBIT_SKIP_ACL_CT       "reg0[7]"
>>
>>  /* Register definitions for switches and routers. */
>>
>> @@ -245,11 +246,11 @@ enum ovn_stage {
>>   * OVS register usage:
>>   *
>>   * Logical Switch pipeline:
>> - * +---------+-------------------------------------+
>> - * | R0      | REGBIT_{CONNTRACK/DHCP/DNS/HAIRPIN} |
>> - * +---------+-------------------------------------+
>> - * | R1 - R9 |              UNUSED                 |
>> - * +---------+-------------------------------------+
>> + * +---------+-------------------------------------------------+
>> + * | R0      | REGBIT_{CONNTRACK/DHCP/DNS/HAIRPIN/SKIP_ACL_CT} |
>> + * +---------+-------------------------------------------------+
>> + * | R1 - R9 |              UNUSED                             |
>> + * +---------+-------------------------------------------------+
>>   *
>>   * Logical Router pipeline:
>>   *
> +-----+--------------------------+---+-----------------+---+---------------+
>> @@ -4713,6 +4714,12 @@ has_stateful_acl(struct ovn_datapath *od)
>>      return false;
>>  }
>>
>> +static bool
>> +has_stateful_acl_bypass(struct ovn_datapath *od)
>> +{
>> +    return od->nbs->n_stateless_filters > 0;
>> +}
>> +
>>  static void
>>  build_lswitch_input_port_sec(struct hmap *ports, struct hmap *datapaths,
>>                               struct hmap *lflows)
>> @@ -4881,7 +4888,47 @@ skip_port_from_conntrack(struct ovn_datapath
> *od, struct ovn_port *op,
>>  }
>>
>>  static void
>> -build_pre_acls(struct ovn_datapath *od, struct hmap *lflows)
>> +build_stateless_filter(struct ovn_datapath *od,
>> +                       const struct nbrec_stateless_filter *filter,
>> +                       struct hmap *lflows)
>> +{
>> +    /* Stateless filters must be applied in both directions so that reply
>> +     * traffic bypasses conntrack too.
>> +     */
>> +    ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL,
>> +                            filter->priority + OVN_ACL_PRI_OFFSET,
>> +                            filter->match,
>> +                            REGBIT_SKIP_ACL_CT" = 1; next;",
>> +                            &filter->header_);
>> +    ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL,
>> +                            filter->priority + OVN_ACL_PRI_OFFSET,
>> +                            filter->match,
>> +                            REGBIT_SKIP_ACL_CT" = 1; next;",
>> +                            &filter->header_);
>> +}
>> +
>> +static void
>> +build_stateless_filters(struct ovn_datapath *od, struct hmap
> *port_groups,
>> +                        struct hmap *lflows)
>> +{
>> +    for (size_t i = 0; i < od->nbs->n_stateless_filters; i++) {
>> +        build_stateless_filter(od, od->nbs->stateless_filters[i],
> lflows);
>> +    }
>> +
>> +    struct ovn_port_group *pg;
>> +    HMAP_FOR_EACH (pg, key_node, port_groups) {
>> +        if (ovn_port_group_ls_find(pg, &od->nbs->header_.uuid)) {
>> +            for (size_t i = 0; i < pg->nb_pg->n_stateless_filters; i++) {
>> +                build_stateless_filter(od,
> pg->nb_pg->stateless_filters[i],
>> +                                       lflows);
>> +            }
>> +        }
>> +    }
>> +}
>> +
>> +static void
>> +build_pre_acls(struct ovn_datapath *od, struct hmap *port_groups,
>> +               struct hmap *lflows)
>>  {
>>      bool has_stateful = has_stateful_acl(od);
>>
>> @@ -4926,6 +4973,13 @@ build_pre_acls(struct ovn_datapath *od, struct
> hmap *lflows)
>>                        "nd || nd_rs || nd_ra || "
>>                        "(udp && udp.src == 546 && udp.dst == 547)",
> "next;");
>>
>> +        /* Ingress and Egress Pre-ACL Table (Stateless_Filter).
>> +         *
>> +         * If the logical switch is configured to bypass conntrack for
>> +         * specific types of traffic, skip conntrack for that traffic.
>> +         */
>> +        build_stateless_filters(od, port_groups, lflows);
>> +
>>          /* Ingress and Egress Pre-ACL Table (Priority 100).
>>           *
>>           * Regardless of whether the ACL is "from-lport" or "to-lport",
>> @@ -5260,7 +5314,8 @@ build_reject_acl_rules(struct ovn_datapath *od,
> struct hmap *lflows,
>>
>>  static void
>>  consider_acl(struct hmap *lflows, struct ovn_datapath *od,
>> -             struct nbrec_acl *acl, bool has_stateful)
>> +             struct nbrec_acl *acl, bool has_stateful,
>> +             bool has_stateful_bypass)
>>  {
>>      bool ingress = !strcmp(acl->direction, "from-lport") ? true :false;
>>      enum ovn_stage stage = ingress ? S_SWITCH_IN_ACL : S_SWITCH_OUT_ACL;
>> @@ -5285,7 +5340,19 @@ consider_acl(struct hmap *lflows, struct
> ovn_datapath *od,
>>              struct ds match = DS_EMPTY_INITIALIZER;
>>              struct ds actions = DS_EMPTY_INITIALIZER;
>>
>> -            /* Commit the connection tracking entry if it's a new
>> +            /* If traffic matched the acl-stateful-bypass rule, we don't
>> +             * need to commit the connection tracking entry.
>> +             */
>> +            if (has_stateful_bypass) {
>> +                ds_put_format(&match, "(" REGBIT_SKIP_ACL_CT "== 1 &&
> (%s)",
>> +                              acl->match);
>> +                build_acl_log(&actions, acl);
>> +                ds_put_format(&actions, "next;");
>> +                ds_clear(&match);
>> +                ds_clear(&actions);
> 
> It seems this whole "if" block is useless. The match and actions are set
> but then cleared without being used.
> 

Ooops, thanks for pointing it out. I'll remove it.

>> +            }
>> +
>> +            /* Otherwise commit the connection tracking entry if it's
> a new
>>               * connection that matches this ACL.  After this commit,
>>               * the reply traffic is allowed by a flow we create at
>>               * priority 65535, defined earlier.
>> @@ -5297,10 +5364,11 @@ consider_acl(struct hmap *lflows, struct
> ovn_datapath *od,
>>               * by ct_commit in the "stateful" stage) to indicate that the
>>               * connection should be allowed to resume.
>>               */
>> -            ds_put_format(&match, "((ct.new && !ct.est)"
>> -                                  " || (!ct.new && ct.est && !ct.rpl "
>> -                                       "&& ct_label.blocked == 1)) "
>> -                                  "&& (%s)", acl->match);
>> +            ds_put_format(&match, REGBIT_SKIP_ACL_CT " == 0 "
>> +                          "&& ((ct.new && !ct.est)"
>> +                          " || (!ct.new && ct.est && !ct.rpl "
>> +                               "&& ct_label.blocked == 1)) "
>> +                          "&& (%s)", acl->match);
>>              ds_put_cstr(&actions, REGBIT_CONNTRACK_COMMIT" = 1; ");
>>              build_acl_log(&actions, acl);
>>              ds_put_cstr(&actions, "next;");
>> @@ -5315,11 +5383,16 @@ consider_acl(struct hmap *lflows, struct
> ovn_datapath *od,
>>               * deletion.  There is no need to commit here, so we can just
>>               * proceed to the next table. We use this to ensure that this
>>               * connection is still allowed by the currently defined
>> -             * policy. Match untracked packets too. */
>> +             * policy. Match untracked packets too.
>> +             *
>> +             * This flow also allows traffic that matches the
>> +             * acl-stateful-bypass rule.
>> +             */
>>              ds_clear(&match);
>>              ds_clear(&actions);
>>              ds_put_format(&match,
>> -                          "(!ct.trk || (!ct.new && ct.est && !ct.rpl"
>> +                          "(" REGBIT_SKIP_ACL_CT " == 1 || !ct.trk || "
>> +                          "(!ct.new && ct.est && !ct.rpl"
>>                            " && ct_label.blocked == 0)) && (%s)",
>>                            acl->match);
> 
> Because of this lflow, each ACL is translated to 3 extra OVS flows (2
> before this patch). If large address set/port groups used in the ACL the
> cost can be huge. One way to optimize it could be introducing a new
> stage with a single logical flow to match (" REGBIT_SKIP_ACL_CT " == 1
> || !ct.trk ||  (!ct.new && ct.est && !ct.rpl && ct_label.blocked == 0),
> and set a new flag "NO_TRACK", and then in the current ACL table it only
> needs a single (extra) flow for each ACL: NO_TRACK == 1 && <acl match>.
> 
> Something similar can be done for "reject/drop" rules handling for the
> lflows with several (x '||' y) operators plus a "&&" with the real ACL
> match.
> 
> I am not 100% sure if a new stage worth it, but I think at least it is
> something to be considered.
> 

Sounds good to me, it does reduce the number of OVS flows. I'll change
it as you suggested.

>>
>> @@ -5346,7 +5419,7 @@ consider_acl(struct hmap *lflows, struct
> ovn_datapath *od,
>>              /* If the packet is not tracked or not part of an established
>>               * connection, then we can simply reject/drop it. */
>>              ds_put_cstr(&match,
>> -                        "(!ct.trk || !ct.est"
>> +                        "(" REGBIT_SKIP_ACL_CT " == 1 || !ct.trk ||
> !ct.est"
>>                          " || (ct.est && ct_label.blocked == 1))");
>>              if (!strcmp(acl->action, "reject")) {
>>                  build_reject_acl_rules(od, lflows, stage, acl, &match,
>> @@ -5373,7 +5446,8 @@ consider_acl(struct hmap *lflows, struct
> ovn_datapath *od,
>>               */
>>              ds_clear(&match);
>>              ds_clear(&actions);
>> -            ds_put_cstr(&match, "ct.est && ct_label.blocked == 0");
>> +            ds_put_cstr(&match, REGBIT_SKIP_ACL_CT " == 0 "
>> +                        "&& ct.est && ct_label.blocked == 0");
>>              ds_put_cstr(&actions, "ct_commit { ct_label.blocked = 1;
> }; ");
>>              if (!strcmp(acl->action, "reject")) {
>>                  build_reject_acl_rules(od, lflows, stage, acl, &match,
>> @@ -5478,6 +5552,7 @@ build_acls(struct ovn_datapath *od, struct hmap
> *lflows,
>>             struct hmap *port_groups)
>>  {
>>      bool has_stateful = has_stateful_acl(od);
>> +    bool has_stateful_bypass = has_stateful_acl_bypass(od);
>>
>>      /* Ingress and Egress ACL Table (Priority 0): Packets are allowed by
>>       * default.  A related rule at priority 1 is added below if there
>> @@ -5508,11 +5583,15 @@ build_acls(struct ovn_datapath *od, struct
> hmap *lflows,
>>           * Subsequent packets will hit the flow at priority 0 that just
>>           * uses "next;". */
>>          ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 1,
>> -                      "ip && (!ct.est || (ct.est && ct_label.blocked
> == 1))",
>> -                       REGBIT_CONNTRACK_COMMIT" = 1; next;");
>> +                      REGBIT_SKIP_ACL_CT " == 0 "
>> +                      "&& ip "
>> +                      "&& (!ct.est || (ct.est && ct_label.blocked ==
> 1))",
>> +                      REGBIT_CONNTRACK_COMMIT" = 1; next;");
>>          ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 1,
>> -                      "ip && (!ct.est || (ct.est && ct_label.blocked
> == 1))",
>> -                       REGBIT_CONNTRACK_COMMIT" = 1; next;");
>> +                      REGBIT_SKIP_ACL_CT " == 0 "
>> +                      "&& ip "
>> +                      "&& (!ct.est || (ct.est && ct_label.blocked ==
> 1))",
>> +                      REGBIT_CONNTRACK_COMMIT" = 1; next;");
>>
>>          /* Ingress and Egress ACL Table (Priority 65535).
>>           *
>> @@ -5522,10 +5601,14 @@ build_acls(struct ovn_datapath *od, struct
> hmap *lflows,
>>           *
>>           * This is enforced at a higher priority than ACLs can be
> defined. */
>>          ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
>> -                      "ct.inv || (ct.est && ct.rpl &&
> ct_label.blocked == 1)",
>> +                      REGBIT_SKIP_ACL_CT " == 0 "
>> +                      "&& (ct.inv "
>> +                           "|| (ct.est && ct.rpl && ct_label.blocked
> == 1))",
>>                        "drop;");
>>          ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
>> -                      "ct.inv || (ct.est && ct.rpl &&
> ct_label.blocked == 1)",
>> +                      REGBIT_SKIP_ACL_CT " == 0 "
>> +                      "&& (ct.inv "
>> +                           "|| (ct.est && ct.rpl && ct_label.blocked
> == 1))",
>>                        "drop;");
>>
>>          /* Ingress and Egress ACL Table (Priority 65535).
>> @@ -5538,11 +5621,13 @@ build_acls(struct ovn_datapath *od, struct
> hmap *lflows,
>>           *
>>           * This is enforced at a higher priority than ACLs can be
> defined. */
>>          ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
>> -                      "ct.est && !ct.rel && !ct.new && !ct.inv "
>> +                      REGBIT_SKIP_ACL_CT "== 0 "
>> +                      "&& ct.est && !ct.rel && !ct.new && !ct.inv "
>>                        "&& ct.rpl && ct_label.blocked == 0",
>>                        "next;");
>>          ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
>> -                      "ct.est && !ct.rel && !ct.new && !ct.inv "
>> +                      REGBIT_SKIP_ACL_CT "== 0 "
>> +                      "&& ct.est && !ct.rel && !ct.new && !ct.inv "
>>                        "&& ct.rpl && ct_label.blocked == 0",
>>                        "next;");
>>
>> @@ -5558,11 +5643,13 @@ build_acls(struct ovn_datapath *od, struct
> hmap *lflows,
>>           * related traffic such as an ICMP Port Unreachable through
>>           * that's generated from a non-listening UDP port.  */
>>          ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
>> -                      "!ct.est && ct.rel && !ct.new && !ct.inv "
>> +                      REGBIT_SKIP_ACL_CT "== 0 "
>> +                      "&& !ct.est && ct.rel && !ct.new && !ct.inv "
>>                        "&& ct_label.blocked == 0",
>>                        "next;");
>>          ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
>> -                      "!ct.est && ct.rel && !ct.new && !ct.inv "
>> +                      REGBIT_SKIP_ACL_CT "== 0 "
>> +                      "&& !ct.est && ct.rel && !ct.new && !ct.inv "
>>                        "&& ct_label.blocked == 0",
>>                        "next;");
>>
>> @@ -5578,13 +5665,14 @@ build_acls(struct ovn_datapath *od, struct
> hmap *lflows,
>>      /* Ingress or Egress ACL Table (Various priorities). */
>>      for (size_t i = 0; i < od->nbs->n_acls; i++) {
>>          struct nbrec_acl *acl = od->nbs->acls[i];
>> -        consider_acl(lflows, od, acl, has_stateful);
>> +        consider_acl(lflows, od, acl, has_stateful, has_stateful_bypass);
>>      }
>>      struct ovn_port_group *pg;
>>      HMAP_FOR_EACH (pg, key_node, port_groups) {
>>          if (ovn_port_group_ls_find(pg, &od->nbs->header_.uuid)) {
>>              for (size_t i = 0; i < pg->nb_pg->n_acls; i++) {
>> -                consider_acl(lflows, od, pg->nb_pg->acls[i],
> has_stateful);
>> +                consider_acl(lflows, od, pg->nb_pg->acls[i],
> has_stateful,
>> +                             has_stateful_bypass);
>>              }
>>          }
>>      }
>> @@ -6617,7 +6705,7 @@ build_lswitch_flows(struct hmap *datapaths,
> struct hmap *ports,
>>              continue;
>>          }
>>
>> -        build_pre_acls(od, lflows);
>> +        build_pre_acls(od, port_groups, lflows);
>>          build_pre_lb(od, lflows, meter_groups, lbs);
>>          build_pre_stateful(od, lflows);
>>          build_acls(od, lflows, port_groups);
>> diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
>> index 0c939b7..ef0121d 100644
>> --- a/ovn-nb.ovsschema
>> +++ b/ovn-nb.ovsschema
>> @@ -1,7 +1,7 @@
>>  {
>>      "name": "OVN_Northbound",
>> -    "version": "5.25.0",
>> -    "cksum": "1354137211 26116",
>> +    "version": "5.26.0",
>> +    "cksum": "1450952466 27225",
>>      "tables": {
>>          "NB_Global": {
>>              "columns": {
>> @@ -35,6 +35,12 @@
>>                                             "refType": "strong"},
>>                                     "min": 0,
>>                                     "max": "unlimited"}},
>> +                "stateless_filters": {
>> +                    "type": {"key": {"type": "uuid",
>> +                                     "refTable": "Stateless_Filter",
>> +                                     "refType": "strong"},
>> +                             "min": 0,
>> +                             "max": "unlimited"}},
>>                  "acls": {"type": {"key": {"type": "uuid",
>>                                            "refTable": "ACL",
>>                                            "refType": "strong"},
>> @@ -150,6 +156,12 @@
>>                                             "refType": "weak"},
>>                                     "min": 0,
>>                                     "max": "unlimited"}},
>> +                "stateless_filters": {
>> +                    "type": {"key": {"type": "uuid",
>> +                                     "refTable": "Stateless_Filter",
>> +                                     "refType": "strong"},
>> +                             "min": 0,
>> +                             "max": "unlimited"}},
>>                  "acls": {"type": {"key": {"type": "uuid",
>>                                            "refTable": "ACL",
>>                                            "refType": "strong"},
>> @@ -201,6 +213,16 @@
>>                      "type": {"key": "string", "value": "string",
>>                               "min": 0, "max": "unlimited"}}},
>>              "isRoot": false},
>> +        "Stateless_Filter": {
>> +            "columns": {
>> +                "priority": {"type": {"key": {"type": "integer",
>> +                                              "minInteger": 0,
>> +                                              "maxInteger": 32767}}},
>> +                "match": {"type": "string"},
>> +                "external_ids": {
>> +                    "type": {"key": "string", "value": "string",
>> +                             "min": 0, "max": "unlimited"}}},
>> +            "isRoot": false},
> 
> Is there any specific consideration that "direction" is not needed?
> 

Initially I had added direction too but found it a bit confusing to use.
My idea was that if traffic hits a stateless filter in one direction
then there's no point for replies to that type of traffic to be sent to
conntrack.

Not having a "direction" field does move a bit the responsibility on the
CMS to make sure that the "match" is true both for request and for reply
traffic.

Alternatively, we could add a "direction" field but then the CMS will
(probably always) have to define the stateless_filters for both directions.

I don't expect a lot of stateless filters to be configured and the ones
that will be configured should probably be quite generic. The only use
case I know of today is for ovn-k8s where all TCP traffic could be
firewalled in a stateless way while UDP would still need to go to conntrack.

I don't have a strong preference though so I can add a "direction" field
if you think that may become useful in the future.

>>          "ACL": {
>>              "columns": {
>>                  "name": {"type": {"key": {"type": "string",
>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>> index 9f3621d..ccb0cbc 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -271,9 +271,16 @@
>>        ip addresses.
>>      </column>
>>
>> -    <column name="acls">
>> -      Access control rules that apply to packets within the logical
> switch.
>> -    </column>
>> +    <group title="ACL processing">
>> +      <column name="acls">
>> +        Access control rules that apply to packets within the logical
> switch.
>> +      </column>
>> +
>> +      <column name="stateless_filters">
>> +        Stateless filters to bypass connection tracking that apply to
> packets
>> +        within the logical switch.
>> +      </column>
>> +    </group>
>>
>>      <column name="qos_rules">
>>        QoS marking and metering rules that apply to packets within the
>> @@ -1430,6 +1437,11 @@
>>        lswitches that the ports of the port group belong to.
>>      </column>
>>
>> +    <column name="stateless_filters">
>> +      Stateless filters to bypass connection tracking that apply to the
>> +      port_group.
>> +    </column>
>> +
>>      <group title="Common Columns">
>>        <column name="external_ids">
>>          See <em>External IDs</em> at the beginning of this document.
>> @@ -1589,6 +1601,45 @@
>>      </group>
>>    </table>
>>
>> +  <table name="Stateless_Filter" title="Filters to bypass ACL
> processing">
>> +    <p>
>> +      Each row in this table represents a rule to determine if
> traffic should
>> +      be processed in a stateless way in the ACL stage, without
> recirculating
>> +      through connection tracking, regardless of the type of ACL that
> is hit.
>> +
>> +      In normal operation, whenever an ACL associated to a Logical_Switch
>> +      has action <code>allow-related</code>, all IP traffic gets sent
>> +      to conntrack and related traffic is allowed.
>> +
>> +      If <ref column="match"/> is set to <code>E</code> all
>> +      <code>allow</code> and <code>allow-related</code> ACLs that match
> 
> Shall we simply say that all ACLs match the filter are considered
> stateless, regardless of the action? (even reject/drop would have some
> implication of stateful, so I think it would be better not mentioning
> the allow/allow-related to avoid confusion)
> 

Sure, sounds better indeed.

Thanks,
Dumitru

>> +      packets for which <code>E</code> is true are applied
>> +      in a stateless way, without recirculating through connection
> tracking.
>> +
>> +      This also implies that the CMS should add an explicit
> <code>allow</code>
>> +      ACL for return traffic, because return traffic will not go to
> conntrack
>> +      either so it has to be explicitly allowed.
>> +
>> +      This is useful when some specific types of traffic do not need
>> +      stateful processing.
>> +    </p>
>> +    <column name="priority">
>> +      The priority of the filter rule.  Rules with numerically higher
> priority
>> +      take precedence.
>> +    </column>
>> +    <column name="match">
>> +      The packets that the stateless filter should match, in the same
>> +      expression language used for the <ref column="match"
> table="Logical_Flow"
>> +      db="OVN_Southbound"/> column in the OVN Southbound database's
>> +      <ref table="Logical_Flow" db="OVN_Southbound"/> table.
>> +    </column>
>> +    <group title="Common Columns">
>> +      <column name="external_ids">
>> +        See <em>External IDs</em> at the beginning of this document.
>> +      </column>
>> +    </group>
>> +  </table>
>> +
>>    <table name="ACL" title="Access Control List (ACL) rule">
>>      <p>
>>        Each row in this table represents one ACL rule for a logical switch
>> diff --git a/tests/ovn-nbctl.at <http://ovn-nbctl.at>
> b/tests/ovn-nbctl.at <http://ovn-nbctl.at>
>> index 619051d..b55ee03 100644
>> --- a/tests/ovn-nbctl.at <http://ovn-nbctl.at>
>> +++ b/tests/ovn-nbctl.at <http://ovn-nbctl.at>
>> @@ -270,6 +270,59 @@ AT_CHECK([ovn-nbctl --type=port-group acl-add ls0
> to-lport 100 ip drop], [0], [i
>>
>>  dnl ---------------------------------------------------------------------
>>
>> +OVN_NBCTL_TEST([ovn_nbctl_stateless_filters], [Stateless_Filters], [
>> +ovn_nbctl_test_stateless_filters() {
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 300 udp])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 200 tcp])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 100 ip])
>> +   dnl Add duplicated Stateless_Filter
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 100 ip], [1], [],
> [stderr])
>> +   AT_CHECK([grep 'already existed' stderr], [0], [ignore])
>> +   AT_CHECK([ovn-nbctl $2 --may-exist stateless-filter-add $1 100 ip])
>> +
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-list $1], [0], [dnl
>> +  300 (udp)
>> +  200 (tcp)
>> +  100 (ip)
>> +])
>> +
>> +   dnl Delete all Stateless_Filters.
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-del $1])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-list $1], [0], [dnl
>> +])
>> +
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 300 udp])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 200 tcp])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-add $1 100 ip])
>> +
>> +   dnl Delete a single filter.
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-del $1 200 tcp])
>> +   AT_CHECK([ovn-nbctl $2 stateless-filter-list $1], [0], [dnl
>> +  300 (udp)
>> +  100 (ip)
>> +])
>> +}
>> +
>> +AT_CHECK([ovn-nbctl ls-add ls0])
>> +ovn_nbctl_test_stateless_filters ls0
>> +AT_CHECK([ovn-nbctl ls-add ls1])
>> +ovn_nbctl_test_stateless_filters ls1 --type=switch
>> +AT_CHECK([ovn-nbctl create port_group name=pg0], [0], [ignore])
>> +ovn_nbctl_test_stateless_filters pg0 --type=port-group
>> +
>> +dnl Test when port group doesn't exist
>> +AT_CHECK([ovn-nbctl --type=port-group stateless-filter-add pg1 100
> ip], [1], [], [dnl
>> +ovn-nbctl: pg1: port group name not found
>> +])
>> +
>> +dnl Test when same name exists in logical switches and portgroups
>> +AT_CHECK([ovn-nbctl create port_group name=ls0], [0], [ignore])
>> +AT_CHECK([ovn-nbctl stateless-filter-add ls0 100 ip], [1], [], [stderr])
>> +AT_CHECK([grep 'exists in both' stderr], [0], [ignore])
>> +AT_CHECK([ovn-nbctl --type=port-group stateless-filter-add ls0 100
> ip], [0], [ignore])])
>> +
>> +dnl ---------------------------------------------------------------------
>> +
>>  OVN_NBCTL_TEST([ovn_nbctl_qos], [QoS], [
>>  AT_CHECK([ovn-nbctl ls-add ls0])
>>  AT_CHECK([ovn-nbctl qos-add ls0 from-lport 600 tcp dscp=63])
>> diff --git a/tests/ovn-northd.at <http://ovn-northd.at>
> b/tests/ovn-northd.at <http://ovn-northd.at>
>> index 8344c7f..0cbc092 100644
>> --- a/tests/ovn-northd.at <http://ovn-northd.at>
>> +++ b/tests/ovn-northd.at <http://ovn-northd.at>
>> @@ -1781,3 +1781,266 @@ AT_CHECK([ovn-sbctl lflow-list | grep
> "ls_out_pre_lb.*priority=100" | grep reg0
>>  ])
>>
>>  AT_CLEANUP
>> +
>> +AT_SETUP([ovn -- ACL Stateful Bypass - Logical_Switch])
>> +ovn_start
>> +
>> +ovn-nbctl ls-add ls
>> +ovn-nbctl lsp-add ls lsp1
>> +ovn-nbctl lsp-set-addresses lsp1 00:00:00:00:00:01
>> +ovn-nbctl lsp-add ls lsp2
>> +ovn-nbctl lsp-set-addresses lsp2 00:00:00:00:00:02
>> +
>> +ovn-nbctl acl-add ls from-lport 3 "tcp" allow
>> +ovn-nbctl acl-add ls from-lport 2 "udp" allow-related
>> +ovn-nbctl acl-add ls from-lport 1 "ip" drop
>> +ovn-nbctl --wait=sb sync
>> +
>> +flow_eth='eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02'
>> +flow_ip='ip.ttl==64 && ip4.src == 42.42.42.1 && ip4.dst == 66.66.66.66'
>> +flow_tcp='tcp && tcp.dst == 80'
>> +flow_udp='udp && udp.dst == 80'
>> +
>> +# TCP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# Enable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-add ls 1 tcp
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should not go to conntrack anymore.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +output("lsp2");
>> +])
>> +
>> +# UDP packets still go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# Add a load balancer.
>> +ovn-nbctl lb-add lb-tcp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> tcp
>> +ovn-nbctl lb-add lb-udp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> udp
>> +ovn-nbctl ls-lb-add ls lb-tcp
>> +ovn-nbctl ls-lb-add ls lb-udp
>> +
>> +# Disable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-del ls
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# Enable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-add ls 1 tcp
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should go to conntrack for load balancing.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets still go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +AT_CLEANUP
>> +
>> +AT_SETUP([ovn -- ACL Stateful Bypass - Port_Group])
>> +ovn_start
>> +
>> +ovn-nbctl ls-add ls
>> +ovn-nbctl lsp-add ls lsp1
>> +ovn-nbctl lsp-set-addresses lsp1 00:00:00:00:00:01
>> +ovn-nbctl lsp-add ls lsp2
>> +ovn-nbctl lsp-set-addresses lsp2 00:00:00:00:00:02
>> +
>> +ovn-nbctl pg-add pg lsp1 lsp2
>> +ovn-nbctl acl-add pg from-lport 3 "tcp" allow
>> +ovn-nbctl acl-add pg from-lport 2 "udp" allow-related
>> +ovn-nbctl acl-add pg from-lport 1 "ip" drop
>> +ovn-nbctl --wait=sb sync
>> +
>> +flow_eth='eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02'
>> +flow_ip='ip.ttl==64 && ip4.src == 42.42.42.1 && ip4.dst == 66.66.66.66'
>> +flow_tcp='tcp && tcp.dst == 80'
>> +flow_udp='udp && udp.dst == 80'
>> +
>> +# TCP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# Enable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-add pg 1 tcp
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should not go to conntrack anymore.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +output("lsp2");
>> +])
>> +
>> +# UDP packets still go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_next(ct_state=new|trk) {
>> +        output("lsp2");
>> +    };
>> +};
>> +])
>> +
>> +# Add a load balancer.
>> +ovn-nbctl lb-add lb-tcp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> tcp
>> +ovn-nbctl lb-add lb-udp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> udp
>> +ovn-nbctl ls-lb-add ls lb-tcp
>> +ovn-nbctl ls-lb-add ls lb-udp
>> +
>> +# Disable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-del pg
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets should go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# Enable Stateful Bypass for TCP.
>> +ovn-nbctl stateless-filter-add pg 1 tcp
>> +ovn-nbctl --wait=sb sync
>> +
>> +# TCP packets should go to conntrack for load balancing.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> tcp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80,tcp_flags=0
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +# UDP packets still go to conntrack.
>> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
>> +AT_CHECK([ovn-trace --ct new --ct new --minimal ls "${flow}"], [0], [dnl
>> +#
> udp,reg14=0x1,vlan_tci=0x0000,dl_src=00:00:00:00:00:01,dl_dst=00:00:00:00:00:02,nw_src=42.42.42.1,nw_dst=66.66.66.66,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=0,tp_dst=80
>> +ct_next(ct_state=new|trk) {
>> +    ct_lb {
>> +        ct_next(ct_state=new|trk) {
>> +            output("lsp2");
>> +        };
>> +    };
>> +};
>> +])
>> +
>> +AT_CLEANUP
>> diff --git a/tests/system-common-macros.at
> <http://system-common-macros.at> b/tests/system-common-macros.at
> <http://system-common-macros.at>
>> index c8fa6f0..65904ed 100644
>> --- a/tests/system-common-macros.at <http://system-common-macros.at>
>> +++ b/tests/system-common-macros.at <http://system-common-macros.at>
>> @@ -234,6 +234,14 @@ m4_define([FORMAT_PING], [grep "transmitted" |
> sed 's/time.*ms$/time 0ms/'])
>>  #
>>  m4_define([STRIP_MONITOR_CSUM], [grep "csum:" | sed 's/csum:.*/csum:
> <skip>/'])
>>
>> +# FORMAT_CT_STATE([ip-addr])
>> +#
>> +# Strip content from the piped input which would differ from test to test
>> +# and limit the output to the rows containing 'ip-addr'. Don't strip
> state.
>> +#
>> +m4_define([FORMAT_CT_STATE],
>> +    [[grep "dst=$1" | sed -e 's/port=[0-9]*/port=<cleared>/g' -e
> 's/id=[0-9]*/id=<cleared>/g' | sort | uniq]])
>> +
>>  # FORMAT_CT([ip-addr])
>>  #
>>  # Strip content from the piped input which would differ from test to test
>> diff --git a/tests/system-ovn.at <http://system-ovn.at>
> b/tests/system-ovn.at <http://system-ovn.at>
>> index 40ba6e4..b1c890b 100644
>> --- a/tests/system-ovn.at <http://system-ovn.at>
>> +++ b/tests/system-ovn.at <http://system-ovn.at>
>> @@ -5397,3 +5397,116 @@ as
>>  OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
>>  /.*terminating with signal 15.*/d"])
>>  AT_CLEANUP
>> +
>> +AT_SETUP([ovn -- ACL Stateful Bypass + Load balancer])
>> +AT_SKIP_IF([test $HAVE_NC = no])
>> +AT_KEYWORDS([lb])
>> +AT_KEYWORDS([conntrack])
>> +ovn_start
>> +
>> +OVS_TRAFFIC_VSWITCHD_START()
>> +ADD_BR([br-int])
>> +
>> +# Set external-ids in br-int needed for ovn-controller
>> +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
>> +
>> +# Logical network:
>> +# One logical switch with a load balancer with one backend.
>> +# On the LS we add "allow" ACLs for TCP and "allow-related" ACLs for UDP.
>> +# The "allow-related" ACL normally forces all traffic to go to conntrack.
>> +# We enable ACL stateful bypass for TCP so TCP traffic should not be
>> +# sent to conntrack for ACLs (only for LB).
>> +
>> +ovn-nbctl ls-add ls
>> +ovn-nbctl lsp-add ls lsp1
>> +ovn-nbctl lsp-set-addresses lsp1 00:00:00:00:00:01
>> +ovn-nbctl lsp-add ls lsp2
>> +ovn-nbctl lsp-set-addresses lsp2 00:00:00:00:00:02
>> +
>> +ovn-nbctl acl-add ls from-lport 3 "tcp" allow
>> +ovn-nbctl acl-add ls from-lport 2 "udp" allow-related
>> +ovn-nbctl acl-add ls from-lport 1 "ip" drop
>> +
>> +ovn-nbctl lr-add rtr
>> +ovn-nbctl lrp-add rtr rtr-ls 00:00:00:00:01:00 42.42.42.254/24
> <http://42.42.42.254/24>
>> +ovn-nbctl lsp-add ls ls-rtr                       \
>> +    -- lsp-set-type ls-rtr router                 \
>> +    -- lsp-set-addresses ls-rtr 00:00:00:00:01:00 \
>> +    -- lsp-set-options ls-rtr router-port=rtr-ls
>> +
>> +# Add a load balancer.
>> +ovn-nbctl lb-add lb-tcp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> tcp
>> +ovn-nbctl lb-add lb-udp 66.66.66.66:80 <http://66.66.66.66:80>
> 42.42.42.2:8080 <http://42.42.42.2:8080> udp
>> +ovn-nbctl ls-lb-add ls lb-tcp
>> +ovn-nbctl ls-lb-add ls lb-udp
>> +
>> +# Enable Stateful Bypass for TCP.
>> +ovn-nbctl \
>> +    --id=@f1 create Stateless_Filter priority=1 match="tcp" -- \
>> +    set Logical_Switch ls stateless_filters='@f1'
>> +
>> +ADD_NAMESPACES(lsp1)
>> +ADD_VETH(lsp1, lsp1, br-int, "42.42.42.1/24 <http://42.42.42.1/24>",
> "00:00:00:00:00:01", \
>> +         "42.42.42.254")
>> +
>> +ADD_NAMESPACES(lsp2)
>> +ADD_VETH(lsp2, lsp2, br-int, "42.42.42.2/24 <http://42.42.42.2/24>",
> "00:00:00:00:00:02", \
>> +         "42.42.42.254")
>> +
>> +ovn-nbctl --wait=hv sync
>> +
>> +# Start a UDP server on lsp2.
>> +NETNS_DAEMONIZE([lsp2], [nc -l --no-shutdown -u 42.42.42.2 8080],
> [nc2.pid])
>> +
>> +# Start a UDP connection.
>> +NS_CHECK_EXEC([lsp1], [echo "foo" | nc --no-shutdown -u 66.66.66.66 80])
>> +
>> +# There should be 2 UDP conntrack entries:
>> +# - one for the allow-related ACL.
>> +# - one for the LB dnat.
>> +OVS_WAIT_UNTIL([test "$(ovs-appctl dpctl/dump-conntrack | grep udp |
> grep '42.42.42.1' -c)" = "2"])
>> +
>> +AT_CHECK([ovs-appctl dpctl/dump-conntrack |
> FORMAT_CT_STATE(42.42.42.1) | grep udp | \
>> +sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
>>
> +udp,orig=(src=42.42.42.1,dst=42.42.42.2,sport=<cleared>,dport=<cleared>),reply=(src=42.42.42.2,dst=42.42.42.1,sport=<cleared>,dport=<cleared>),zone=<cleared>
>>
> +udp,orig=(src=42.42.42.1,dst=66.66.66.66,sport=<cleared>,dport=<cleared>),reply=(src=42.42.42.2,dst=42.42.42.1,sport=<cleared>,dport=<cleared>),zone=<cleared>,labels=0x2
>> +])
>> +
>> +# Start a TCP server on lsp2.
>> +NETNS_DAEMONIZE([lsp2], [nc -l --no-shutdown 42.42.42.2 8080], [nc0.pid])
>> +
>> +# Start a TCP connection.
>> +NETNS_DAEMONIZE([lsp1], [nc --no-shutdown 66.66.66.66 80], [nc1.pid])
>> +
>> +OVS_WAIT_UNTIL([test "$(ovs-appctl dpctl/dump-conntrack | grep tcp |
> grep '42.42.42.1' -c)" = "1"])
>> +
>> +# There should be only one TCP conntrack entry, for the LB dnat.
>> +AT_CHECK([ovs-appctl dpctl/dump-conntrack |
> FORMAT_CT_STATE(42.42.42.1) | grep tcp | \
>> +sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
>>
> +tcp,orig=(src=42.42.42.1,dst=66.66.66.66,sport=<cleared>,dport=<cleared>),reply=(src=42.42.42.2,dst=42.42.42.1,sport=<cleared>,dport=<cleared>),zone=<cleared>,labels=0x2,protoinfo=(state=ESTABLISHED)
>> +])
>> +
>> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
>> +
>> +as ovn-sb
>> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
>> +
>> +as ovn-nb
>> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
>> +
>> +as northd
>> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
>> +
>> +as
>> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
>> +/connection dropped.*/d"])
>> +
>> +AT_CLEANUP
>> diff --git a/utilities/ovn-detrace.in <http://ovn-detrace.in>
> b/utilities/ovn-detrace.in <http://ovn-detrace.in>
>> index 4f8dd5f..343965d 100755
>> --- a/utilities/ovn-detrace.in <http://ovn-detrace.in>
>> +++ b/utilities/ovn-detrace.in <http://ovn-detrace.in>
>> @@ -232,6 +232,17 @@ class StaticRouteHintHandler(CookieHandlerByUUUID):
>>                      route.ip_prefix, route.nexthop, route.output_port,
>>                      route.policy))
>>
>> +class StatelessFilterHintHandler(CookieHandlerByUUUID):
>> +    def __init__(self, ovnnb_db):
>> +        super(StatelessFilterHintHandler, self).__init__(ovnnb_db,
>> +                                                        
> 'Stateless_Filter')
>> +
>> +    def print_record(self, s_filter):
>> +        output = 'Stateless_Filter: priority=%s, match=(%s)' % (
>> +            s_filter.priority,
>> +            s_filter.match.strip('"'))
>> +        print_h(output)
>> +
>>  class QoSHintHandler(CookieHandlerByUUUID):
>>      def __init__(self, ovnnb_db):
>>          super(QoSHintHandler, self).__init__(ovnnb_db, 'QoS')
>> @@ -254,6 +265,7 @@ class LogicalFlowHandler(CookieHandlerByUUUID):
>>              LoadBalancerHintHandler(ovnnb_db),
>>              NATHintHandler(ovnnb_db),
>>              StaticRouteHintHandler(ovnnb_db),
>> +            StatelessFilterHintHandler(ovnnb_db),
>>              QoSHintHandler(ovnnb_db),
>>          ]
>>
>> diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
>> index d7bb4b4..7716dcd 100644
>> --- a/utilities/ovn-nbctl.c
>> +++ b/utilities/ovn-nbctl.c
>> @@ -601,6 +601,17 @@ ACL commands:\n\
>>    acl-list {SWITCH | PORTGROUP}\n\
>>                              print ACLs for SWITCH\n\
>>  \n\
>> +Stateless filter commands:\n\
>> +  [--type={switch | port-group}] [--may-exist]\n\
>> +  stateless-filter-add {SWITCH | PORTGROUP} PRIORITY MATCH \n\
>> +                            add a stateless filter to SWITCH/PORTGROUP\n\
>> +  [--type={switch | port-group}]\n\
>> +  stateless-filter-del {SWITCH | PORTGROUP} [PRIORITY MATCH]\n\
>> +                            remove stateless filters from
> SWITCH/PORTGROUP\n\
>> +  [--type={switch | port-group}]\n\
>> +  stateless-filter-list {SWITCH | PORTGROUP}\n\
>> +                            print stateless filters for SWITCH\n\
>> +\n\
>>  QoS commands:\n\
>>    qos-add SWITCH DIRECTION PRIORITY MATCH [rate=RATE [burst=BURST]]
> [dscp=DSCP]\n\
>>                              add an QoS rule to SWITCH\n\
>> @@ -725,7 +736,8 @@ LB commands:\n\
>>    ls-lb-add SWITCH LB       add a load-balancer to SWITCH\n\
>>    ls-lb-del SWITCH [LB]     remove load-balancers from SWITCH\n\
>>    ls-lb-list SWITCH         print load-balancers\n\
>> -\n\
>> +\n\n",program_name, program_name);
>> +    printf("\
>>  DHCP Options commands:\n\
>>    dhcp-options-create CIDR [EXTERNAL_IDS]\n\
>>                             create a DHCP options row with CIDR\n\
>> @@ -743,8 +755,7 @@ Connection commands:\n\
>>    del-connection             delete the connections\n\
>>    [--inactivity-probe=MSECS]\n\
>>    set-connection TARGET...   set the list of connections to TARGET...\n\
>> -\n\n",program_name, program_name);
>> -    printf("\
>> +\n\
>>  SSL commands:\n\
>>    get-ssl                     print the SSL configuration\n\
>>    del-ssl                     delete the SSL configuration\n\
>> @@ -2021,9 +2032,9 @@ acl_cmp(const void *acl1_, const void *acl2_)
>>  }
>>
>>  static char * OVS_WARN_UNUSED_RESULT
>> -acl_cmd_get_pg_or_ls(struct ctl_context *ctx,
>> -                     const struct nbrec_logical_switch **ls,
>> -                     const struct nbrec_port_group **pg)
>> +cmd_get_pg_or_ls(struct ctl_context *ctx,
>> +                 const struct nbrec_logical_switch **ls,
>> +                 const struct nbrec_port_group **pg)
>>  {
>>      const char *opt_type = shash_find_data(&ctx->options, "--type");
>>      char *error;
>> @@ -2073,7 +2084,7 @@ nbctl_acl_list(struct ctl_context *ctx)
>>      const struct nbrec_acl **acls;
>>      size_t i;
>>
>> -    char *error = acl_cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>>      if (error) {
>>          ctx->error = error;
>>          return;
>> @@ -2173,7 +2184,7 @@ nbctl_acl_add(struct ctl_context *ctx)
>>      const struct nbrec_port_group *pg = NULL;
>>      const char *action = ctx->argv[5];
>>
>> -    char *error = acl_cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>>      if (error) {
>>          ctx->error = error;
>>          return;
>> @@ -2264,7 +2275,7 @@ nbctl_acl_del(struct ctl_context *ctx)
>>      const struct nbrec_logical_switch *ls = NULL;
>>      const struct nbrec_port_group *pg = NULL;
>>
>> -    char *error = acl_cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>>      if (error) {
>>          ctx->error = error;
>>          return;
>> @@ -2351,6 +2362,181 @@ nbctl_acl_del(struct ctl_context *ctx)
>>      }
>>  }
>>
>> +static int
>> +stateless_filter_cmp(const void *filter1_, const void *filter2_)
>> +{
>> +    const struct nbrec_stateless_filter *const *filter1p = filter1_;
>> +    const struct nbrec_stateless_filter *const *filter2p = filter2_;
>> +    const struct nbrec_stateless_filter *filter1 = *filter1p;
>> +    const struct nbrec_stateless_filter *filter2 = *filter2p;
>> +
>> +    if (filter1->priority != filter2->priority) {
>> +        return filter1->priority > filter2->priority ? -1 : 1;
>> +    } else {
>> +        return strcmp(filter1->match, filter2->match);
>> +    }
>> +}
>> +
>> +static void
>> +nbctl_stateless_filter_list(struct ctl_context *ctx)
>> +{
>> +    const struct nbrec_logical_switch *ls = NULL;
>> +    const struct nbrec_port_group *pg = NULL;
>> +    const struct nbrec_stateless_filter **filters;
>> +    size_t i;
>> +
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    if (error) {
>> +        ctx->error = error;
>> +        return;
>> +    }
>> +
>> +    size_t n_filters = pg ? pg->n_stateless_filters :
> ls->n_stateless_filters;
>> +    struct nbrec_stateless_filter **nb_filters = pg
>> +                                                 ? pg->stateless_filters
>> +                                                 : ls->stateless_filters;
>> +
>> +    filters = xmalloc(sizeof *filters * n_filters);
>> +    for (i = 0; i < n_filters; i++) {
>> +        filters[i] = nb_filters[i];
>> +    }
>> +
>> +    qsort(filters, n_filters, sizeof *filters, stateless_filter_cmp);
>> +
>> +    for (i = 0; i < n_filters; i++) {
>> +        const struct nbrec_stateless_filter *filter = filters[i];
>> +        ds_put_format(&ctx->output, "%5"PRId64" (%s)\n",
>> +                      filter->priority, filter->match);
>> +    }
>> +
>> +    free(filters);
>> +}
>> +
>> +static void
>> +nbctl_stateless_filter_add(struct ctl_context *ctx)
>> +{
>> +    const struct nbrec_logical_switch *ls = NULL;
>> +    const struct nbrec_port_group *pg = NULL;
>> +
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    if (error) {
>> +        ctx->error = error;
>> +        return;
>> +    }
>> +
>> +    int64_t priority;
>> +    error = parse_priority(ctx->argv[2], &priority);
>> +    if (error) {
>> +        ctx->error = error;
>> +        return;
>> +    }
>> +
>> +    /* Create the filter. */
>> +    struct nbrec_stateless_filter *filter =
>> +        nbrec_stateless_filter_insert(ctx->txn);
>> +    nbrec_stateless_filter_set_priority(filter, priority);
>> +    nbrec_stateless_filter_set_match(filter, ctx->argv[3]);
>> +
>> +    /* Check if same filter already exists for the ls/portgroup */
>> +    size_t n_filters = pg ? pg->n_stateless_filters :
> ls->n_stateless_filters;
>> +    struct nbrec_stateless_filter **filters = pg
>> +                                              ? pg->stateless_filters
>> +                                              : ls->stateless_filters;
>> +    for (size_t i = 0; i < n_filters; i++) {
>> +        if (!stateless_filter_cmp(&filters[i], &filter)) {
>> +            bool may_exist = shash_find(&ctx->options, "--may-exist")
> != NULL;
>> +            if (!may_exist) {
>> +                ctl_error(ctx,
>> +                          "Same filter already existed on ls or pg %s.",
>> +                          ctx->argv[1]);
>> +                return;
>> +            }
>> +            return;
>> +        }
>> +    }
>> +
>> +    /* Insert the filter into the logical switch/port group. */
>> +    struct nbrec_stateless_filter **new_filters =
>> +        xmalloc(sizeof *new_filters * (n_filters + 1));
>> +    nullable_memcpy(new_filters, filters, sizeof *new_filters *
> n_filters);
>> +    new_filters[n_filters] = filter;
>> +    if (pg) {
>> +        nbrec_port_group_verify_stateless_filters(pg);
>> +        nbrec_port_group_set_stateless_filters(pg, new_filters,
>> +                                               n_filters + 1);
>> +    } else {
>> +        nbrec_logical_switch_verify_stateless_filters(ls);
>> +        nbrec_logical_switch_set_stateless_filters(ls, new_filters,
>> +                                                   n_filters + 1);
>> +    }
>> +    free(new_filters);
>> +}
>> +
>> +static void
>> +nbctl_stateless_filter_del(struct ctl_context *ctx)
>> +{
>> +    const struct nbrec_logical_switch *ls = NULL;
>> +    const struct nbrec_port_group *pg = NULL;
>> +
>> +    char *error = cmd_get_pg_or_ls(ctx, &ls, &pg);
>> +    if (error) {
>> +        ctx->error = error;
>> +        return;
>> +    }
>> +
>> +    if (ctx->argc == 2) {
>> +        /* If priority and match are not specified, delete filters. */
>> +        if (pg) {
>> +            nbrec_port_group_verify_stateless_filters(pg);
>> +            nbrec_port_group_set_stateless_filters(pg, NULL, 0);
>> +        } else {
>> +            nbrec_logical_switch_verify_stateless_filters(ls);
>> +            nbrec_logical_switch_set_stateless_filters(ls, NULL, 0);
>> +        }
>> +        return;
>> +    }
>> +
>> +    int64_t priority;
>> +    error = parse_priority(ctx->argv[2], &priority);
>> +    if (error) {
>> +        ctx->error = error;
>> +        return;
>> +    }
>> +
>> +    if (ctx->argc == 3) {
>> +        ctl_error(ctx, "cannot specify priority without match");
>> +        return;
>> +    }
>> +
>> +    size_t n_filters = pg ? pg->n_stateless_filters :
> ls->n_stateless_filters;
>> +    struct nbrec_stateless_filter **filters = pg
>> +                                              ? pg->stateless_filters
>> +                                              : ls->stateless_filters;
>> +
>> +    /* Remove the matching rule. */
>> +    for (size_t i = 0; i < n_filters; i++) {
>> +        struct nbrec_stateless_filter *filter = filters[i];
>> +
>> +        if (priority == filter->priority
>> +            && !strcmp(ctx->argv[3], filter->match)) {
>> +            struct nbrec_stateless_filter **new_filters
>> +                = xmemdup(filters, sizeof *new_filters * n_filters);
>> +            new_filters[i] = filters[n_filters - 1];
>> +            if (pg) {
>> +                nbrec_port_group_verify_stateless_filters(pg);
>> +                nbrec_port_group_set_stateless_filters(pg, new_filters,
>> +                                                       n_filters - 1);
>> +            } else {
>> +                nbrec_logical_switch_verify_stateless_filters(ls);
>> +                nbrec_logical_switch_set_stateless_filters(ls,
> new_filters,
>> +                                                           n_filters
> - 1);
>> +            }
>> +            free(new_filters);
>> +            return;
>> +        }
>> +    }
>> +}
>> +
>>  static void
>>  nbctl_qos_list(struct ctl_context *ctx)
>>  {
>> @@ -6283,6 +6469,15 @@ static const struct ctl_command_syntax
> nbctl_commands[] = {
>>      { "acl-list", 1, 1, "{SWITCH | PORTGROUP}",
>>        NULL, nbctl_acl_list, NULL, "--type=", RO },
>>
>> +    /* stateless filter commands. */
>> +    { "stateless-filter-add", 3, 4, "{SWITCH | PORTGROUP} PRIORITY
> MATCH",
>> +      NULL, nbctl_stateless_filter_add, NULL,
>> +      "--may-exist,--type=", RW },
>> +    { "stateless-filter-del", 1, 4, "{SWITCH | PORTGROUP} [PRIORITY
> MATCH]",
>> +      NULL, nbctl_stateless_filter_del, NULL, "--type=", RW },
>> +    { "stateless-filter-list", 1, 1, "{SWITCH | PORTGROUP}",
>> +      NULL, nbctl_stateless_filter_list, NULL, "--type=", RO },
>> +
>>      /* qos commands. */
>>      { "qos-add", 5, 7,
>>        "SWITCH DIRECTION PRIORITY MATCH [rate=RATE [burst=BURST]]
> [dscp=DSCP]",
>> --
>> 1.8.3.1
>>
>> _______________________________________________
>> dev mailing list
>> [email protected] <mailto:[email protected]>
>> https://mail.openvswitch.org/mailman/listinfo/ovs-dev

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to