Hi Ales,

Sorry about our lack of engagement on this. I'll be working on this in the
coming weeks.

Erlon

On Wed, Nov 26, 2025 at 4:29 AM Ales Musil <[email protected]> wrote:

>
>
> On Thu, Oct 16, 2025 at 3:27 PM Frode Nordahl <[email protected]> wrote:
>
>> Hello all,
>>
>> Please review my comments in-line.
>>
>> On 9/29/25 08:16, Ales Musil wrote:
>> >
>> >
>> > On Fri, Sep 26, 2025 at 9:15 PM Erlon Rodrigues Cruz <
>> [email protected] <mailto:[email protected]>> wrote:
>> >
>> >     Hi Dumitru,
>> >
>> >
>> >     On Fri, Sep 26, 2025 at 4:16 AM Dumitru Ceara <[email protected]
>> <mailto:[email protected]>> wrote:
>> >
>> >         On 9/25/25 6:48 PM, Erlon Rodrigues Cruz wrote:
>> >          > Hey folks, thanks for taking the time to look at this.
>> >          >
>> >
>> >         Hi Erlon, Brian,
>> >
>> >          > I brought this up in our meeting today and wanted to bring
>> this here as
>> >          > well. The implementation of the parser on northd is meant to
>> be temporary.
>> >          > This is so we can ensure that we can backport this fix.
>> Another important
>> >          > point to mention is that we've previously discussed and
>> agreed on a
>> >          > temporary solution on northd, which led us to provide this
>> version to our
>> >          > customer. They have thoroughly tested it and are very
>> confident with the
>> >          > results. Changing our approach now would require them to
>> redo all that
>> >          > testing, which is completely out of line with their roadmap.
>> >          >
>> >
>> >         I just wanted to make sure I understand the expectations.  In
>> the
>> >         community meeting you said:
>> >
>> >         "<erlon> We depend on the official version of OVN, and we're
>> offering a
>> >         hotfix package with the patch on top of it. However, every hot
>> fix you
>> >         provide must also be available upstream to ensure there are no
>> >         regressions when customers upgrade."
>> >
>> >         https://libera.catirclogs.org/openvswitch/2025-09-25#38774158 <
>> https://libera.catirclogs.org/openvswitch/2025-09-25#38774158>;
>> >
>> >         My understanding was that it's OK for you if this patch is
>> accepted on
>> >         the main upstream ovn branch so you have an accepted commit to
>> cherry
>> >         pick and backport in your downstream.  That is, that it
>> wouldn't have to
>> >         be backported to stable branches upstream.
>> >
>> >
>> > Hi Erlon and Dumitru,
>> >
>> > my 2c on this whole story.
>> >
>> >
>> >     We would also want this to be backported to some previous stable
>> versions.
>> >
>> >
>> > Once again I would like to say what I said previously during the
>> > discussions, this patch 2/2 wasn't considered as safe to backport.
>> > During our discussion we were willing to allow the 1/2 (the register
>> > definitions) to be backported to unblock the workaround. There was
>> > even a pushback on the register definition backport in a sense that
>> > we shouldn't expose internals on stable branches. The aim was
>> > to find a solution that might be backportable. In my mind that would
>> > be the ovn-controller version as it is way simpler (we still need to
>> > see the final version). Also AFAICT we wouldn't need the register
>> > definitions going with the ovn-controller solution.
>> >
>> >     The idea is that we work as close as possible with upstream (master
>> or
>> >     stable) versions. Having it backported to the OVN stable branches,
>> guarantees
>> >     that we can also upgrade our stable package versions to take
>> benefit of any
>> >     future stable release fixes. Carrying/maintaining the patch-of-tree
>> is also possible
>> >     but it's less desirable.
>> >
>> >         This commit would live temporarily, only on main, and would be
>> removed
>> >         by the series you plan to work on next.  I also assume that the
>> follow
>> >         up series, with the parsing in ovn-controller as suggested by
>> Ales,
>> >         would be backported to all stable branches upstream too,
>> ensuring that
>> >         when your customers upgrade from the hotfixed version to the
>> officially
>> >         released 2x.0y.z one there's no breakage.
>> >
>> >
>> >     So, following the optimal path, the commits would leave temporarily
>> on main,
>> >     permanently on the stable branches, and would cease existing as
>> soon as
>> >     we can land the ovn-controller version.
>> >
>> >
>> > Considering the above, the optimal path cannot happen in my opinion.
>> > We shouldn't have anything temporary solution on stable branches;
>> > whatever  feature lands in a stable branch (and this is a feature),
>> > needs to have some guarantees.
>> >
>> >     In the practical scenario, assuming we
>> >     can get both series (current [on northd] and ovn-controller version
>> ) before the
>> >     26.03 cut, we would backport the northd patches from  25.09, 24.03
>> and 22.03.
>> >
>> >
>> > So talking about the 26.03 timeline there is plenty of time to go
>> > without the need for any temporary solutions. We could have
>> > a permanent one in a form of ovn-controller version before the
>> > 26.03 release. If you don't have enough time to invest into
>> > ovn-controller solution we might be able to help with the
>> > development.
>>
>> This discussion appears to have reached some level of temperature, and I
>> think this it is fair to state that in a stressful day parts of this may
>> have been attempts at defending the work already done taken one step too
>> far.
>>
>> For us the bulk of the value of the work done here so far was finding a
>> working approach that allowed all the testing done directly and indirectly
>> by our end user.  We also achieved containing the fix in a single project,
>> which simplifies delivery.
>>
>> There is credit due for this work which should end up in the committed
>> solution regardless of which parts of the original submission remains in
>> the next iteartion, let's find the path to complete this work.
>>
>> While it would have been amazing if we got detailed insights [0] at the
>> beginning of the journey, it is likely that these insights came as part of
>> the journey. It's not obvious until it is.
>>
>> We accept your generous offer to consider backport of the controller
>> based solution back to 22.03 [1] and will commence on this path.
>>
>> I'll defer to Erlon and / or Brian for further discussions on the path
>> forward.
>>
>> 0:
>> https://mail.openvswitch.org/pipermail/ovs-dev/2025-September/426314.html
>> 1:
>> https://mail.openvswitch.org/pipermail/ovs-dev/2025-September/426503.html
>>
>> --
>> Frode Nordahl
>>
>
> Hi all,
>
> we haven't heard anything here in a while. We would like to use the
> solution as well, so my question is pretty straight forward, would
> you be able to send another revision based on the symtab suggestion
> by the end of the year? If you are fine with that I can "take over"
> and post a patch properly crediting you for the work that you have
> done on the testing side of things. Would that be a feasible option?
> Please let us know your opinion so we can adjust our schedule
> accordingly.
>
> Thanks,
> Ales
>
>
>>
>> >
>> >     Having the ovn-controller version backported will depend on how
>> fast we can get
>> >     that to work as reliably as we have in the current version and how
>> intrusive that
>> >     work is. The bottom line is that, as you mentioned, we need to make
>> sure that
>> >     users coming from the patch versions don't break.
>> >
>> >
>> >
>> > To sum up, I'm very hesitant to put my name behind the northd
>> > approach even on main. The backport however is unfortunately
>> > hard nack from my side.
>> >
>> > Ales
>> >
>> >
>> >     Erlon
>> >
>> >
>> >         Am I reading this correctly?
>> >
>> >          > Brian is currently working on the v5 version of the patch,
>> which includes
>> >          > all the suggested fixes except for the parser change. Once
>> we have that, we
>> >          > can start working on the next series, where we will remove
>> the northd
>> >          > approach and implement it on the controller.
>> >          >
>> >          > @ales, please let me know if that works for you.
>> >          >
>> >          > Erlon
>> >          >
>> >
>> >         Regards,
>> >         Dumitru
>> >
>> >          > On Tue, Sep 16, 2025 at 5:34 PM Brian Haley <
>> [email protected] <mailto:[email protected]>> wrote:
>> >          >
>> >          >> Hi Ales,
>> >          >>
>> >          >> Responding for Erlon since he is out this week, comments
>> inline.
>> >          >>
>> >          >> On 9/16/25 1:41 AM, Ales Musil via dev wrote:
>> >          >>> On Wed, Sep 3, 2025 at 4:07 PM Erlon R. Cruz <
>> [email protected] <mailto:[email protected]>>
>> >          >> wrote:
>> >          >>>
>> >          >>>> At the current state, OVN can not handle UDP fragmented
>> traffic
>> >          >>>> for ACLs in the userspace. 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. One way
>> to solve that
>> >          >>>> is to use the ct_udp field to match on ct.new <
>> http://ct.new> connections. With the
>> >          >>>> current state, OVN will get the CMS rules and write them
>> verbatim
>> >          >>>> to SB, which will generate similar rules in OVS. So, this
>> workaround
>> >          >>>> replaces L4 protocol matches with connection tracking
>> equivalents.
>> >          >>>> For example:
>> >          >>>>
>> >          >>>>     "outport == "server" && udp && udp.dst == 4242"
>> >          >>>> becomes:
>> >          >>>>     "outport == "server" && ct.new <http://ct.new> &&
>> udp && ct_udp.dst == 4242"
>> >          >>>>
>> >          >>>> We made this behavior to be optional and disabled by
>> default. In
>> >          >>>> order to enable it, the user needs to set an NB_Global
>> flag:
>> >          >>>>
>> >          >>>> ovn-nbctl set NB_Global .
>> options:acl_udp_ct_translation=true
>> >          >>>>
>> >          >>>> Signed-off-by: Erlon R. Cruz <[email protected]
>> <mailto:[email protected]>>
>> >          >>>> ---
>> >          >>>>
>> >          >>>
>> >          >>> Hi Erlon,
>> >          >>>
>> >          >>> I was thinking about this and I still don't like the fact
>> that we
>> >          >>> add a custom parser into northd while we have a functional
>> and very
>> >          >>> much tested parser in ovn-controller. I would propose the
>> following
>> >          >>> solution. When the acl translation is set to true we could
>> add
>> >          >>> a special external_id to SB logical_flow. This would serve
>> as an
>> >          >>> indication for ovn-controller. Now to do the replacement
>> we would
>> >          >>> need a second symtab that would basically do the correct
>> replacement
>> >          >>> e.g.
>> >          >>>
>> >          >>> expr_symtab_add_predicate(symtab, "tcp", "ct_proto == 6");
>> >          >>> expr_symtab_add_field(symtab, "tcp.src", MFF_CT_TP_DST,
>> "tcp", false);
>> >          >>>
>> >          >>> for all supported protocols (we should support replacement
>> of all of
>> >          >>> them). And by simply using the second symtab in
>> >          >>> "convert_match_to_expr()", when appropriate, we would have
>> the
>> >          >>> replacement done for "free" by the current expression
>> parser without
>> >          >>> any significant change.
>> >          >>
>> >          >> I will have to look at this closer as I'm not super
>> familiar with the
>> >          >> code in question.
>> >          >>
>> >          >> Thanks for the review,
>> >          >>
>> >          >> -Brian
>> >          >>
>> >          >>>
>> >          >>>
>> >          >>>>   NEWS                      |   4 +
>> >          >>>>   northd/en-global-config.c |   5 +
>> >          >>>>   northd/northd.c           | 239 +++++++++++++++++++++-
>> >          >>>>   ovn-nb.xml                |  11 +
>> >          >>>>   tests/automake.mk <http://automake.mk>         |   4 +-
>> >          >>>>   tests/system-ovn.at <http://system-ovn.at>       | 420
>> >          >> ++++++++++++++++++++++++++++++++++++++
>> >          >>>>   tests/tcp_simple.py       | 338
>> ++++++++++++++++++++++++++++++
>> >          >>>>   tests/udp_client.py       | 113 ++++++++++
>> >          >>>>   8 files changed, 1129 insertions(+), 5 deletions(-)
>> >          >>>>   create mode 100755 tests/tcp_simple.py
>> >          >>>>   create mode 100755 tests/udp_client.py
>> >          >>>>
>> >          >>>> diff --git a/NEWS b/NEWS
>> >          >>>> index 932e173af..96aa6afda 100644
>> >          >>>> --- a/NEWS
>> >          >>>> +++ b/NEWS
>> >          >>>> @@ -31,6 +31,10 @@ OVN v25.09.0 - xxx xx xxxx
>> >          >>>>        with stateless ACL to work with load balancer.
>> >          >>>>      - Added new ovn-nbctl command 'pg-get-ports' to get
>> the ports
>> >          >> assigned
>> >          >>>> to
>> >          >>>>        the port group.
>> >          >>>> +   - Added new NB_Global option 'acl_udp_ct_translation'
>> to control
>> >          >>>> whether
>> >          >>>> +     stateful ACL rules that match on UDP port fields
>> are rewritten to
>> >          >> use
>> >          >>>> +     connection tracking fields to properly handle IP
>> fragments.
>> >          >> Default
>> >          >>>> is
>> >          >>>> +     false.
>> >          >>>>      - The ovn-controller option
>> ovn-ofctrl-wait-before-clear is no
>> >          >> longer
>> >          >>>>        supported.  It will be ignored if used.
>> ovn-controller will
>> >          >>>>        automatically care about proper delay before
>> clearing lflow.
>> >          >>>> diff --git a/northd/en-global-config.c
>> b/northd/en-global-config.c
>> >          >>>> index 76046c265..48586c69b 100644
>> >          >>>> --- a/northd/en-global-config.c
>> >          >>>> +++ b/northd/en-global-config.c
>> >          >>>> @@ -650,6 +650,11 @@ check_nb_options_out_of_sync(
>> >          >>>>           return true;
>> >          >>>>       }
>> >          >>>>
>> >          >>>> +    if (config_out_of_sync(&nb->options,
>> &config_data->nb_options,
>> >          >>>> +                           "acl_udp_ct_translation",
>> false)) {
>> >          >>>> +        return true;
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>>       return false;
>> >          >>>>   }
>> >          >>>>
>> >          >>>> diff --git a/northd/northd.c b/northd/northd.c
>> >          >>>> index e0a329a17..2eb6e4924 100644
>> >          >>>> --- a/northd/northd.c
>> >          >>>> +++ b/northd/northd.c
>> >          >>>> @@ -71,6 +71,7 @@
>> >          >>>>   #include "uuid.h"
>> >          >>>>   #include "ovs-thread.h"
>> >          >>>>   #include "openvswitch/vlog.h"
>> >          >>>> +#include <ctype.h>
>> >          >>>>
>> >          >>>>   VLOG_DEFINE_THIS_MODULE(northd);
>> >          >>>>
>> >          >>>> @@ -88,6 +89,12 @@ static bool use_common_zone = false;
>> >          >>>>    * Otherwise, it will avoid using it.  The default is
>> true. */
>> >          >>>>   static bool use_ct_inv_match = true;
>> >          >>>>
>> >          >>>> +/* If this option is 'true' northd will rewrite stateful
>> ACL rules that
>> >          >>>> match
>> >          >>>> + * on UDP port fields to use connection tracking fields
>> to properly
>> >          >>>> handle IP
>> >          >>>> + * fragments. By default this option is set to 'false'.
>> >          >>>> + */
>> >          >>>> +static bool acl_udp_ct_translation = false;
>> >          >>>> +
>> >          >>>>   /* 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.
>> >          >>>> @@ -6789,6 +6796,201 @@
>> build_acl_sample_default_flows(const struct
>> >          >>>> ovn_datapath *od,
>> >          >>>>                     "next;", lflow_ref);
>> >          >>>>   }
>> >          >>>>
>> >          >>>> +/* Port extraction result structure */
>> >          >>>> +struct port_extract_result {
>> >          >>>> +    bool found;
>> >          >>>> +    char *operator;  /* "==", ">=", "<=" */
>> >          >>>> +    char port_value[16];
>> >          >>>> +};
>> >          >>>> +
>> >          >>>> +/* Extracts port number from a match string with support
>> for exact
>> >          >>>> matches and
>> >          >>>> + * ranges.
>> >          >>>> + * Examples of match strings and extracted values:
>> >          >>>> + * - "udp.dst == 4242" -> operator="==",
>> port_value="4242"
>> >          >>>> + * - "udp.dst >= 3002" -> operator=">=",
>> port_value="3002"
>> >          >>>> + * - "udp.dst <= 3006" -> operator="<=",
>> port_value="3006"
>> >          >>>> + * - "outport == \"server\" && udp && udp.dst == 4242" ->
>> >          >> operator="==",
>> >          >>>> + *    port_value="4242"
>> >          >>>> + *
>> >          >>>> + * Fills the caller-allocated result struct with
>> extracted values.
>> >          >>>> + * Returns true if port was extracted, false otherwise.
>> >          >>>> + */
>> >          >>>> +static bool
>> >          >>>> +extract_port_value(const char *match_str, const char
>> *field,
>> >          >>>> +                   struct port_extract_result *result)
>> >          >>>> +{
>> >          >>>> +    char *str_copy = xstrdup(match_str);
>> >          >>>> +    char *token;
>> >          >>>> +    char *saveptr;
>> >          >>>> +
>> >          >>>> +    /* Initialize result struct */
>> >          >>>> +    if (result) {
>> >          >>>> +        result->found = false;
>> >          >>>> +        result->operator = NULL;
>> >          >>>> +        result->port_value[0] = '\0';
>> >          >>>> +    } else {
>> >          >>>> +        return false;
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>> +    /* Tokenize by && to find the specific field
>> condition */
>> >          >>>> +    token = strtok_r(str_copy, "&&", &saveptr);
>> >          >>>> +
>> >          >>>> +    while (token) {
>> >          >>>> +        /* Skip leading spaces */
>> >          >>>> +        while (*token == ' ') {
>> >          >>>> +            token++;
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        /* Check if this token contains our field */
>> >          >>>> +        if (strstr(token, field)) {
>> >          >>>> +            char *field_pos = strstr(token, field);
>> >          >>>> +            field_pos += strlen(field);
>> >          >>>> +
>> >          >>>> +            /* Skip spaces */
>> >          >>>> +            while (*field_pos == ' ') {
>> >          >>>> +                field_pos++;
>> >          >>>> +            }
>> >          >>>> +
>> >          >>>> +            /* Determine the operator and extract port */
>> >          >>>> +            if (strncmp(field_pos, ">=", 2) == 0) {
>> >          >>>> +                result->operator = ">=";
>> >          >>>> +                field_pos += 2;
>> >          >>>> +            } else if (strncmp(field_pos, "<=", 2) == 0)
>> {
>> >          >>>> +                result->operator = "<=";
>> >          >>>> +                field_pos += 2;
>> >          >>>> +            } else if (strncmp(field_pos, "==", 2) == 0)
>> {
>> >          >>>> +                result->operator = "==";
>> >          >>>> +                field_pos += 2;
>> >          >>>> +            } else {
>> >          >>>> +                /* No recognized operator found */
>> >          >>>> +                token = strtok_r(NULL, "&&", &saveptr);
>> >          >>>> +                continue;
>> >          >>>> +            }
>> >          >>>> +
>> >          >>>> +            /* Skip spaces after operator */
>> >          >>>> +            while (*field_pos == ' ') {
>> >          >>>> +                field_pos++;
>> >          >>>> +            }
>> >          >>>> +
>> >          >>>> +            /* Extract the port number */
>> >          >>>> +            size_t i = 0;
>> >          >>>> +            while (*field_pos && isdigit(*field_pos) &&
>> >          >>>> +                   i < (sizeof(result->port_value) - 1))
>> {
>> >          >>>> +                result->port_value[i++] = *field_pos++;
>> >          >>>> +            }
>> >          >>>> +            result->port_value[i] = '\0';
>> >          >>>> +
>> >          >>>> +            if (i > 0) {
>> >          >>>> +                result->found = true;
>> >          >>>> +                break;
>> >          >>>> +            }
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        token = strtok_r(NULL, "&&", &saveptr);
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>> +    free(str_copy);
>> >          >>>> +    return result->found;
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +/* This function implements a workaround for stateful
>> ACLs with UDP
>> >          >>>> matches
>> >          >>>> + * that need to handle IP fragments properly. The issue
>> is that UDP L4
>> >          >>>> headers
>> >          >>>> + * are only present in the first fragment of a
>> fragmented packet.
>> >          >>>> Subsequent
>> >          >>>> + * fragments don't have L4 headers, so they won't match
>> ACL rules that
>> >          >>>> look for
>> >          >>>> + * UDP fields.
>> >          >>>> + *
>> >          >>>> + * The workaround replaces UDP protocol matches with
>> connection
>> >          >> tracking
>> >          >>>> + * equivalents. For example:
>> >          >>>> + *   "outport == "server" && udp && udp.dst == 4242"
>> >          >>>> + * becomes:
>> >          >>>> + *   "outport == "server" && udp && ct.new <
>> http://ct.new> && ct_udp.dst == 4242"
>> >          >>>> + */
>> >          >>>> +static char *
>> >          >>>> +rewrite_match_for_fragments(const char *match_str)
>> >          >>>> +{
>> >          >>>> +    VLOG_DBG("rewrite_match_for_fragments called with:
>> %s", match_str);
>> >          >>>> +    struct ds new_match = DS_EMPTY_INITIALIZER;
>> >          >>>> +    bool has_udp = false;
>> >          >>>> +    bool has_udp_dst = false;
>> >          >>>> +    bool has_udp_src = false;
>> >          >>>> +
>> >          >>>> +    char *str_copy = xstrdup(match_str);
>> >          >>>> +    char *token;
>> >          >>>> +    char *saveptr;
>> >          >>>> +
>> >          >>>> +    /* First token */
>> >          >>>> +    token = strtok_r(str_copy, "&&", &saveptr);
>> >          >>>> +
>> >          >>>> +    while (token) {
>> >          >>>> +        /* Skip leading spaces */
>> >          >>>> +        while (*token == ' ') {
>> >          >>>> +            token++;
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        /* Check what kind of token this is */
>> >          >>>> +        if (strstr(token, "udp") && !strstr(token,
>> "udp.")) {
>> >          >>>> +            /* This is the UDP protocol marker */
>> >          >>>> +            has_udp = true;
>> >          >>>> +        } else if (strstr(token, "udp.dst")) {
>> >          >>>> +            /* This is a UDP destination port condition
>> */
>> >          >>>> +            has_udp_dst = true;
>> >          >>>> +        } else if (strstr(token, "udp.src")) {
>> >          >>>> +            /* This is a UDP source port condition */
>> >          >>>> +            has_udp_src = true;
>> >          >>>> +        } else {
>> >          >>>> +            /* This is a non-UDP condition, keep it */
>> >          >>>> +            if (new_match.length > 0) {
>> >          >>>> +                ds_put_cstr(&new_match, " && ");
>> >          >>>> +            }
>> >          >>>> +            ds_put_cstr(&new_match, token);
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        /* Get next token */
>> >          >>>> +        token = strtok_r(NULL, "&&", &saveptr);
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>> +    /* Free the string copy */
>> >          >>>> +    free(str_copy);
>> >          >>>> +
>> >          >>>> +    /* If we found UDP, always preserve it */
>> >          >>>> +    if (has_udp) {
>> >          >>>> +        if (new_match.length > 0) {
>> >          >>>> +            ds_put_cstr(&new_match, " && ");
>> >          >>>> +        }
>> >          >>>> +        ds_put_cstr(&new_match, "udp");
>> >          >>>> +
>> >          >>>> +        /* Handle destination port conditions */
>> >          >>>> +        if (has_udp_dst) {
>> >          >>>> +            struct port_extract_result dst_result;
>> >          >>>> +            if (extract_port_value(match_str, "udp.dst",
>> &dst_result))
>> >          >> {
>> >          >>>> +                ds_put_format(&new_match, " &&
>> ct_udp.dst %s %s",
>> >          >>>> +                              dst_result.operator,
>> >          >> dst_result.port_value);
>> >          >>>> +            }
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        /* Handle source port conditions */
>> >          >>>> +        if (has_udp_src) {
>> >          >>>> +            struct port_extract_result src_result;
>> >          >>>> +            if (extract_port_value(match_str, "udp.src",
>> &src_result))
>> >          >> {
>> >          >>>> +                ds_put_format(&new_match, " &&
>> ct_udp.src %s %s",
>> >          >>>> +                              src_result.operator,
>> >          >> src_result.port_value);
>> >          >>>> +            }
>> >          >>>> +        }
>> >          >>>> +
>> >          >>>> +        /* Add !ct.inv condition */
>> >          >>>> +        if (new_match.length > 0) {
>> >          >>>> +            ds_put_cstr(&new_match, " && ");
>> >          >>>> +        }
>> >          >>>> +        ds_put_cstr(&new_match, "!ct.inv && ct_proto ==
>> 17");
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>> +    /* Return the result */
>> >          >>>> +    char *result = xstrdup(ds_cstr(&new_match));
>> >          >>>> +    ds_destroy(&new_match);
>> >          >>>> +    VLOG_DBG("rewrite_match_for_fragments returning:
>> %s", result);
>> >          >>>> +    return result;
>> >          >>>> +}
>> >          >>>> +
>> >          >>>>   static void
>> >          >>>>   consider_acl(struct lflow_table *lflows, const struct
>> ovn_datapath
>> >          >> *od,
>> >          >>>>                const struct nbrec_acl *acl, bool
>> has_stateful,
>> >          >>>> @@ -6802,6 +7004,8 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>       enum ovn_stage stage;
>> >          >>>>       enum acl_observation_stage obs_stage;
>> >          >>>>
>> >          >>>> +    VLOG_DBG("consider_acl: ingress=%d, acl=%s",
>> ingress, acl->match);
>> >          >>>> +
>> >          >>>>       if (ingress && smap_get_bool(&acl->options,
>> "apply-after-lb",
>> >          >> false))
>> >          >>>> {
>> >          >>>>           stage = S_SWITCH_IN_ACL_AFTER_LB_EVAL;
>> >          >>>>           obs_stage = ACL_OBS_FROM_LPORT_AFTER_LB;
>> >          >>>> @@ -6839,6 +7043,23 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>           match_tier_len = match->length;
>> >          >>>>       }
>> >          >>>>
>> >          >>>> +    /* Check if this ACL has L4 matches that need
>> fragment handling */
>> >          >>>> +    bool has_udp_match = strstr(acl->match, "udp") !=
>> NULL;
>> >          >>>> +
>> >          >>>> +    char *modified_match = NULL;
>> >          >>>> +    /* For stateful ACLs with L4 matches, rewrite the
>> match string to
>> >          >>>> handle
>> >          >>>> +     * fragments, but only if acl_udp_ct_translation is
>> enabled */
>> >          >>>> +    if (has_stateful && has_udp_match &&
>> acl_udp_ct_translation) {
>> >          >>>> +        modified_match =
>> rewrite_match_for_fragments(acl->match);
>> >          >>>> +
>> >          >>>> +        VLOG_DBG("Rewriting ACL match for L4 fragment
>> handling: "
>> >          >>>> +                "original='%s' modified='%s'",
>> acl->match,
>> >          >>>> modified_match);
>> >          >>>> +    }
>> >          >>>> +
>> >          >>>> +    /* Use the original or modified match string based
>> on whether UDP
>> >          >> L4
>> >          >>>> +     * matches were detected */
>> >          >>>> +    const char *match_to_use = modified_match ?
>> modified_match :
>> >          >>>> acl->match;
>> >          >>>> +
>> >          >>>>       if (!has_stateful
>> >          >>>>           || !strcmp(acl->action, "pass")
>> >          >>>>           || !strcmp(acl->action, "allow-stateless")) {
>> >          >>>> @@ -6877,7 +7098,7 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>            */
>> >          >>>>           ds_truncate(match, match_tier_len);
>> >          >>>>           ds_put_format(match, REGBIT_ACL_HINT_ALLOW_NEW
>> " == 1 &&
>> >          >> (%s)",
>> >          >>>> -                      acl->match);
>> >          >>>> +                      match_to_use);
>> >          >>>>
>> >          >>>>           ds_truncate(actions, log_verdict_len);
>> >          >>>>
>> >          >>>> @@ -6922,7 +7143,7 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>           ds_truncate(match, match_tier_len);
>> >          >>>>           ds_truncate(actions, log_verdict_len);
>> >          >>>>           ds_put_format(match, REGBIT_ACL_HINT_ALLOW " ==
>> 1 && (%s)",
>> >          >>>> -                      acl->match);
>> >          >>>> +                      match_to_use);
>> >          >>>>           if (acl->label || acl->sample_est) {
>> >          >>>>               ds_put_cstr(actions,
>> REGBIT_CONNTRACK_COMMIT" = 1; ");
>> >          >>>>           }
>> >          >>>> @@ -6944,7 +7165,7 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>            * connection, then we can simply reject/drop
>> it. */
>> >          >>>>           ds_truncate(match, match_tier_len);
>> >          >>>>           ds_put_cstr(match, REGBIT_ACL_HINT_DROP " ==
>> 1");
>> >          >>>> -        ds_put_format(match, " && (%s)", acl->match);
>> >          >>>> +        ds_put_format(match, " && (%s)", match_to_use);
>> >          >>>>
>> >          >>>>           ds_truncate(actions, log_verdict_len);
>> >          >>>>
>> >          >>>> @@ -6968,7 +7189,7 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>            */
>> >          >>>>           ds_truncate(match, match_tier_len);
>> >          >>>>           ds_put_cstr(match, REGBIT_ACL_HINT_BLOCK " ==
>> 1");
>> >          >>>> -        ds_put_format(match, " && (%s)", acl->match);
>> >          >>>> +        ds_put_format(match, " && (%s)", match_to_use);
>> >          >>>>
>> >          >>>>           ds_truncate(actions, log_verdict_len);
>> >          >>>>
>> >          >>>> @@ -6983,6 +7204,12 @@ consider_acl(struct lflow_table
>> *lflows, const
>> >          >>>> struct ovn_datapath *od,
>> >          >>>>                                   ds_cstr(match),
>> ds_cstr(actions),
>> >          >>>>                                   &acl->header_,
>> lflow_ref);
>> >          >>>>       }
>> >          >>>> +
>> >          >>>> +    /* Free the modified match string if it was created
>> */
>> >          >>>> +    if (modified_match) {
>> >          >>>> +        free(modified_match);
>> >          >>>> +    }
>> >          >>>> +    VLOG_DBG("consider_acl done");
>> >          >>>>   }
>> >          >>>>
>> >          >>>>   static void
>> >          >>>> @@ -19172,6 +19399,10 @@ ovnnb_db_run(struct northd_input
>> *input_data,
>> >          >>>>       use_common_zone =
>> smap_get_bool(input_data->nb_options,
>> >          >>>> "use_common_zone",
>> >          >>>>                                       false);
>> >          >>>>
>> >          >>>> +    acl_udp_ct_translation =
>> smap_get_bool(input_data->nb_options,
>> >          >>>> +
>>  "acl_udp_ct_translation",
>> >          >>>> +                                       false);
>> >          >>>> +
>> >          >>>>       vxlan_mode = input_data->vxlan_mode;
>> >          >>>>
>> >          >>>>       build_datapaths(input_data->synced_lses,
>> >          >>>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>> >          >>>> index 3f4398afb..498b6c993 100644
>> >          >>>> --- a/ovn-nb.xml
>> >          >>>> +++ b/ovn-nb.xml
>> >          >>>> @@ -429,6 +429,17 @@
>> >          >>>>           </p>
>> >          >>>>         </column>
>> >          >>>>
>> >          >>>> +      <column name="options"
>> key="acl_udp_ct_translation">
>> >          >>>> +        <p>
>> >          >>>> +          If set to <code>true</code>,
>> <code>ovn-northd</code> will
>> >          >>>> rewrite
>> >          >>>> +          stateful ACL rules that match on UDP port
>> fields to use
>> >          >>>> connection
>> >          >>>> +          tracking fields (ct_udp.dst, ct_udp.src) to
>> properly handle
>> >          >> IP
>> >          >>>> +          fragments. This ensures that fragmented UDP
>> packets
>> >          >>>> +          match ACLs correctly. By default this option
>> is set to
>> >          >>>> +          <code>false</code>.
>> >          >>>> +        </p>
>> >          >>>> +      </column>
>> >          >>>> +
>> >          >>>>         <column name="options"
>> key="enable_chassis_nb_cfg_update">
>> >          >>>>           <p>
>> >          >>>>             If set to <code>false</code>, ovn-controllers
>> will no longer
>> >          >>>> update
>> >          >>>> diff --git a/tests/automake.mk <http://automake.mk>
>> b/tests/automake.mk <http://automake.mk>
>> >          >>>> index adfa19503..edb370b99 100644
>> >          >>>> --- a/tests/automake.mk <http://automake.mk>
>> >          >>>> +++ b/tests/automake.mk <http://automake.mk>
>> >          >>>> @@ -333,7 +333,9 @@ CHECK_PYFILES = \
>> >          >>>>          tests/check_acl_log.py \
>> >          >>>>          tests/scapy-server.py \
>> >          >>>>          tests/client.py \
>> >          >>>> -       tests/server.py
>> >          >>>> +       tests/server.py \
>> >          >>>> +       tests/udp_client.py \
>> >          >>>> +       tests/tcp_simple.py
>> >          >>>>
>> >          >>>>   EXTRA_DIST += $(CHECK_PYFILES)
>> >          >>>>   PYCOV_CLEAN_FILES += $(CHECK_PYFILES:.py=.py,cover)
>> .coverage
>> >          >>>> diff --git a/tests/system-ovn.at <http://system-ovn.at>
>> b/tests/system-ovn.at <http://system-ovn.at>
>> >          >>>> index 8e356df6f..805e471df 100644
>> >          >>>> --- a/tests/system-ovn.at <http://system-ovn.at>
>> >          >>>> +++ b/tests/system-ovn.at <http://system-ovn.at>
>> >          >>>> @@ -18252,6 +18252,7 @@
>> OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query
>> >          >> port
>> >          >>>> patch-.*/d
>> >          >>>>   AT_CLEANUP
>> >          >>>>   ])
>> >          >>>>
>> >          >>>> +<<<<<<< HEAD
>> >          >>>>   OVN_FOR_EACH_NORTHD([
>> >          >>>>   AT_SETUP([dynamic-routing - EVPN])
>> >          >>>>   AT_KEYWORDS([dynamic-routing])
>> >          >>>> @@ -18484,3 +18485,422 @@
>> OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query
>> >          >>>> port patch-.*/d
>> >          >>>>   /connection dropped.*/d"])
>> >          >>>>   AT_CLEANUP
>> >          >>>>   ])
>> >          >>>> +
>> >          >>>> +OVN_FOR_EACH_NORTHD([
>> >          >>>> +AT_SETUP([LB correctly handles fragmented traffic])
>> >          >>>> +AT_KEYWORDS([ovnlb])
>> >          >>>> +
>> >          >>>> +CHECK_CONNTRACK()
>> >          >>>> +CHECK_CONNTRACK_NAT()
>> >          >>>> +
>> >          >>>> +ovn_start
>> >          >>>> +OVS_TRAFFIC_VSWITCHD_START()
>> >          >>>> +ADD_BR([br-int])
>> >          >>>> +ADD_BR([br-ext])
>> >          >>>> +
>> >          >>>> +# Logical network:
>> >          >>>> +# 2 logical switches "public" (192.168.1.0/24 <
>> http://192.168.1.0/24>) and "internal" (
>> >          >>>> 172.16.1.0/24 <http://172.16.1.0/24>)
>> >          >>>> +# connected to a router lr.
>> >          >>>> +# internal has a server.
>> >          >>>> +# client is connected through localnet.
>> >          >>>> +
>> >          >>>> +check ovs-ofctl add-flow br-ext action=normal
>> >          >>>> +# 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 \
>> >          >>>> +        -- set Open_vSwitch .
>> >          >>>> external-ids:ovn-bridge-mappings=phynet:br-ext
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +# Start ovn-controller
>> >          >>>> +start_daemon ovn-controller
>> >          >>>> +
>> >          >>>> +# Set the minimal fragment size for userspace DP.
>> >          >>>> +# Note that this call will fail for system DP as this
>> setting is not
>> >          >>>> supported there.
>> >          >>>> +ovs-appctl dpctl/ipf-set-min-frag v4 500
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lr-add lr
>> >          >>>> +check ovn-nbctl ls-add internal
>> >          >>>> +check ovn-nbctl ls-add public
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lrp-add lr lr-pub 00:00:01:01:02:03
>> 192.168.1.1/24 <http://192.168.1.1/24>
>> >          >>>> +check ovn-nbctl lsp-add  public pub-lr -- set
>> Logical_Switch_Port
>> >          >> pub-lr \
>> >          >>>> +    type=router options:router-port=lr-pub
>> >          >> addresses=\"00:00:01:01:02:03\"
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lrp-add lr lr-internal 00:00:01:01:02:04
>> 172.16.1.1/24 <http://172.16.1.1/24>
>> >          >>>> +check ovn-nbctl lsp-add internal internal-lr -- set
>> Logical_Switch_Port
>> >          >>>> internal-lr \
>> >          >>>> +    type=router options:router-port=lr-internal
>> >          >>>> addresses=\"00:00:01:01:02:04\"
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lsp-add public ln_port \
>> >          >>>> +                -- lsp-set-addresses ln_port unknown \
>> >          >>>> +                -- lsp-set-type ln_port localnet \
>> >          >>>> +                -- lsp-set-options ln_port
>> network_name=phynet
>> >          >>>> +
>> >          >>>> +ADD_NAMESPACES(client)
>> >          >>>> +ADD_VETH(client, client, br-ext, "192.168.1.2/24 <
>> http://192.168.1.2/24>",
>> >          >> "f0:00:00:01:02:03", \
>> >          >>>> +         "192.168.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 <
>> http://172.16.1.2/24>",
>> >          >> "f0:00:0f:01:02:03", \
>> >          >>>> +         "172.16.1.1")
>> >          >>>> +check ovn-nbctl lsp-add internal server \
>> >          >>>> +-- lsp-set-addresses server "f0:00:0f:01:02:03
>> 172.16.1.2"
>> >          >>>> +
>> >          >>>> +check ovn-nbctl set logical_router lr options:chassis=hv1
>> >          >>>> +
>> >          >>>> +AT_DATA([client.py], [dnl
>> >          >>>> +import socket
>> >          >>>> +
>> >          >>>> +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
>> >          >>>> +sock.sendto(b"x" * 1000, ("172.16.1.20", 4242))
>> >          >>>> +])
>> >          >>>> +
>> >          >>>> +test_fragmented_traffic() {
>> >          >>>> +    check ovn-nbctl --wait=hv sync
>> >          >>>> +
>> >          >>>> +    check ovs-appctl dpctl/flush-conntrack
>> >          >>>> +
>> >          >>>> +    NETNS_DAEMONIZE([server], [nc -l -u 172.16.1.2 4242
>> > /dev/null],
>> >          >>>> [server.pid])
>> >          >>>> +
>> >          >>>> +    # Collect ICMP packets on client side
>> >          >>>> +    NETNS_START_TCPDUMP([client], [-U -i client -vnne
>> udp],
>> >          >>>> [tcpdump-client])
>> >          >>>> +
>> >          >>>> +    # Collect UDP packets on server side
>> >          >>>> +    NETNS_START_TCPDUMP([server], [-U -i server -vnne
>> 'udp and
>> >          >> ip[[6:2]]
>> >          >>>>> 0 and not ip[[6]] = 64'], [tcpdump-server])
>> >          >>>> +
>> >          >>>> +    NS_CHECK_EXEC([client], [$PYTHON3 ./client.py])
>> >          >>>> +    OVS_WAIT_UNTIL([test "$(cat tcpdump-server.tcpdump |
>> wc -l)" =
>> >          >> "4"])
>> >          >>>> +
>> >          >>>> +    kill $(cat tcpdump-client.pid) $(cat
>> tcpdump-server.pid) $(cat
>> >          >>>> server.pid)
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +AS_BOX([LB on router without port and protocol])
>> >          >>>> +check ovn-nbctl lb-add lb1 172.16.1.20 172.16.1.2
>> >          >>>> +check ovn-nbctl lr-lb-add lr lb1
>> >          >>>> +
>> >          >>>> +test_fragmented_traffic
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lr-lb-del lr
>> >          >>>> +check ovn-nbctl lb-del lb1
>> >          >>>> +
>> >          >>>> +AS_BOX([LB on router with port and protocol])
>> >          >>>> +check ovn-nbctl lb-add lb1 172.16.1.20:4242 <
>> http://172.16.1.20:4242> 172.16.1.2:4242 <http://172.16.1.2:4242> udp
>> >          >>>> +check ovn-nbctl lr-lb-add lr lb1
>> >          >>>> +
>> >          >>>> +test_fragmented_traffic
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lr-lb-del lr
>> >          >>>> +check ovn-nbctl lb-del lb1
>> >          >>>> +
>> >          >>>> +AS_BOX([LB on switch without port and protocol])
>> >          >>>> +check ovn-nbctl lb-add lb1 172.16.1.20 172.16.1.2
>> >          >>>> +check ovn-nbctl ls-lb-add public lb1
>> >          >>>> +
>> >          >>>> +test_fragmented_traffic
>> >          >>>> +
>> >          >>>> +check ovn-nbctl ls-lb-del public
>> >          >>>> +check ovn-nbctl lb-del lb1
>> >          >>>> +
>> >          >>>> +AS_BOX([LB on switch witho port and protocol])
>> >          >>>> +check ovn-nbctl lb-add lb1 172.16.1.20:4242 <
>> http://172.16.1.20:4242> 172.16.1.2:4242 <http://172.16.1.2:4242> udp
>> >          >>>> +check ovn-nbctl ls-lb-add public lb1
>> >          >>>> +
>> >          >>>> +test_fragmented_traffic
>> >          >>>> +
>> >          >>>> +check ovn-nbctl ls-lb-del public
>> >          >>>> +check ovn-nbctl lb-del lb1
>> >          >>>> +
>> >          >>>> +OVN_CLEANUP_CONTROLLER([hv1])
>> >          >>>> +
>> >          >>>> +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
>> >          >>>> +])
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +OVN_FOR_EACH_NORTHD([
>> >          >>>> +AT_SETUP([ACL UDP: dual direction with fragmentation])
>> >          >>>> +AT_KEYWORDS([ovnacl])
>> >          >>>> +
>> >          >>>> +CHECK_CONNTRACK()
>> >          >>>> +
>> >          >>>> +ovn_start
>> >          >>>> +OVS_TRAFFIC_VSWITCHD_START()
>> >          >>>> +ADD_BR([br-int])
>> >          >>>> +ADD_BR([br-ext])
>> >          >>>> +
>> >          >>>> +# Logical network:
>> >          >>>> +# 2 logical switches "public" (192.168.1.0/24 <
>> http://192.168.1.0/24>) and "internal" (
>> >          >>>> 172.16.1.0/24 <http://172.16.1.0/24>)
>> >          >>>> +# connected to a router lr. internal has a server.
>> client and server
>> >          >> are
>> >          >>>> +# connected through localnet.
>> >          >>>> +
>> >          >>>> +check ovs-ofctl add-flow br-ext action=normal
>> >          >>>> +# 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 \
>> >          >>>> +        -- set Open_vSwitch .
>> >          >>>> external-ids:ovn-bridge-mappings=phynet:br-ext
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +# Start ovn-controller
>> >          >>>> +start_daemon ovn-controller
>> >          >>>> +check sleep 3
>> >          >>>> +
>> >          >>>> +# Set the minimal fragment size for userspace DP.
>> >          >>>> +# Note that this call will fail for system DP as this
>> setting is not
>> >          >>>> supported there.
>> >          >>>> +ovs-appctl dpctl/ipf-set-min-frag v4 500
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lr-add lr
>> >          >>>> +check ovn-nbctl ls-add internal
>> >          >>>> +check ovn-nbctl ls-add public
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lrp-add lr lr-pub 00:00:01:01:02:03
>> 192.168.1.1/24 <http://192.168.1.1/24>
>> >          >>>> +check ovn-nbctl lsp-add  public pub-lr -- set
>> Logical_Switch_Port
>> >          >> pub-lr \
>> >          >>>> +    type=router options:router-port=lr-pub
>> >          >> addresses=\"00:00:01:01:02:03\"
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lrp-add lr lr-internal 00:00:01:01:02:04
>> 172.16.1.1/24 <http://172.16.1.1/24>
>> >          >>>> +check ovn-nbctl lsp-add internal internal-lr -- set
>> Logical_Switch_Port
>> >          >>>> internal-lr \
>> >          >>>> +    type=router options:router-port=lr-internal
>> >          >>>> addresses=\"00:00:01:01:02:04\"
>> >          >>>> +
>> >          >>>> +check ovn-nbctl lsp-add public ln_port \
>> >          >>>> +                -- lsp-set-addresses ln_port unknown \
>> >          >>>> +                -- lsp-set-type ln_port localnet \
>> >          >>>> +                -- lsp-set-options ln_port
>> network_name=phynet
>> >          >>>> +
>> >          >>>> +ADD_NAMESPACES(client)
>> >          >>>> +ADD_VETH(client, client, br-int, "172.16.1.3/24 <
>> http://172.16.1.3/24>",
>> >          >> "f0:00:00:01:02:03", \
>> >          >>>> +         "172.16.1.1")
>> >          >>>> +check ovn-nbctl lsp-add internal client \
>> >          >>>> +-- lsp-set-addresses client "f0:00:00:01:02:03
>> 172.16.1.3"
>> >          >>>> +NS_EXEC([client], [ip l set dev client mtu 900])
>> >          >>>> +NS_EXEC([client], [ip addr add 127.0.0.1/24 <
>> http://127.0.0.1/24> dev lo])
>> >          >>>> +NS_EXEC([client], [ip link set up dev lo])
>> >          >>>> +NS_EXEC([client], [ip a])
>> >          >>>> +
>> >          >>>> +ADD_NAMESPACES(server)
>> >          >>>> +ADD_VETH(server, server, br-int, "172.16.1.2/24 <
>> http://172.16.1.2/24>",
>> >          >> "f0:00:0f:01:02:03", \
>> >          >>>> +         "172.16.1.1")
>> >          >>>> +check ovn-nbctl lsp-add internal server \
>> >          >>>> +-- lsp-set-addresses server "f0:00:0f:01:02:03
>> 172.16.1.2"
>> >          >>>> +NS_EXEC([server], [ip addr add 127.0.0.1/24 <
>> http://127.0.0.1/24> dev lo])
>> >          >>>> +NS_EXEC([server], [ip link set up dev lo])
>> >          >>>> +NS_EXEC([server], [ip a])
>> >          >>>> +NS_CHECK_EXEC([server], [ip route add 192.168.1.0/24 <
>> http://192.168.1.0/24> via 172.16.1.1
>> >          >> dev
>> >          >>>> server])
>> >          >>>> +
>> >          >>>> +check ovn-nbctl set logical_router lr options:chassis=hv1
>> >          >>>> +
>> >          >>>> +dump_ovs_info() {
>> >          >>>> +    echo ====== ovs_info $1 ======
>> >          >>>> +    ovs-ofctl show br-int
>> >          >>>> +    echo && echo
>> >          >>>> +    echo =======ovn-nbctl show========
>> >          >>>> +    ovn-nbctl show
>> >          >>>> +    echo && echo
>> >          >>>> +    echo ========ACL===========
>> >          >>>> +    ovn-nbctl list ACL
>> >          >>>> +    echo ========Logical_Flow===========
>> >          >>>> +    ovn-sbctl list Logical_Flow
>> >          >>>> +    echo && echo
>> >          >>>> +    echo ========lflow-list===========
>> >          >>>> +    ovn-sbctl lflow-list
>> >          >>>> +    echo && echo
>> >          >>>> +    echo ============br-int===========
>> >          >>>> +    ovs-ofctl dump-flows -O OpenFlow15 br-int
>> >          >>>> +    echo && echo
>> >          >>>> +    echo ============br-int===========
>> >          >>>> +    ovs-ofctl -O OpenFlow15 dump-flows br-int | grep -E
>> >          >>>> "ct_tp|ct_state|ct_nw_proto" | head -10
>> >          >>>> +    echo && echo
>> >          >>>> +    echo ============br-ext===========
>> >          >>>> +    ovs-ofctl dump-flows br-ext
>> >          >>>> +    echo && echo
>> >          >>>> +
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +dump_data_plane_flows() {
>> >          >>>> +    echo ====== dataplane_flows $1 ======
>> >          >>>> +    echo ============dp flow-table==============
>> >          >>>> +    ovs-appctl dpctl/dump-flows
>> >          >>>> +    echo =======================================
>> >          >>>> +    echo ============dp flow-table -m===========
>> >          >>>> +    ovs-appctl dpctl/dump-flows -m
>> >          >>>> +    echo =======================================
>> >          >>>> +
>> >          >>>> +    ovs-appctl dpctl/dump-flows -m > flows-m.txt
>> >          >>>> +    pmd=$(cat flows-m.txt | awk -F ': ' '/flow-dump from
>> >          >> pmd/{print$2}')
>> >          >>>> +    for ufid in $(awk -F ',' '/ufid:/{print$1}'
>> flows-m.txt); do
>> >          >>>> +        grep ^$ufid flows-m.txt
>> >          >>>> +        echo ========= ofproto/detrace $1 $ufid
>> =============
>> >          >>>> +        ovs-appctl ofproto/detrace $ufid pmd=$pmd
>> >          >>>> +        echo =======================================
>> >          >>>> +    done
>> >          >>>> +
>> >          >>>> +    echo ============conntrack-table============
>> >          >>>> +    ovs-appctl dpctl/dump-conntrack
>> >          >>>> +    echo =======================================
>> >          >>>> +
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +test_tcp_traffic() {
>> >          >>>> +    local src_port=${1:-8080}
>> >          >>>> +    local dst_port=${2:-8080}
>> >          >>>> +    local payload_bytes=${3:-10000}
>> >          >>>> +
>> >          >>>> +    check ovn-nbctl --wait=hv sync
>> >          >>>> +    check ovs-appctl dpctl/flush-conntrack
>> >          >>>> +
>> >          >>>> +    # Start TCP server in background
>> >          >>>> +    NETNS_DAEMONIZE([server], [tcp_simple.py --mode
>> server --bind-ip
>> >          >>>> 172.16.1.2 -p $dst_port], [server.pid])
>> >          >>>> +
>> >          >>>> +    # Give server time to start
>> >          >>>> +    sleep 1
>> >          >>>> +
>> >          >>>> +    # Collect only inbound TCP packets on client and
>> server sides
>> >          >>>> +    NETNS_START_TCPDUMP([client], [-U -i client -Q in
>> -nn -e -q tcp],
>> >          >>>> [tcpdump-tcp-client])
>> >          >>>> +    NETNS_START_TCPDUMP([server], [-U -i server -Q in
>> -nn -e -q tcp],
>> >          >>>> [tcpdump-tcp-server])
>> >          >>>> +
>> >          >>>> +    # Run TCP client test and capture output
>> >          >>>> +    NS_EXEC([client], [tcp_simple.py --mode client -s
>> 172.16.1.3 -d
>> >          >>>> 172.16.1.2 -p $dst_port -B $payload_bytes -n 5 -I 0.2 >
>> >          >>>> tcp_client_output.log 2>&1])
>> >          >>>> +
>> >          >>>> +    sleep 1
>> >          >>>> +    dump_data_plane_flows [tcp]
>> >          >>>> +
>> >          >>>> +    # Wait for client to complete and check success
>> >          >>>> +    OVS_WAIT_UNTIL([test -f tcp_client_output.log])
>> >          >>>> +    OVS_WAIT_UNTIL([grep -q "Client summary:"
>> tcp_client_output.log])
>> >          >>>> +
>> >          >>>> +    # Verify client reported success
>> >          >>>> +    if ! grep -q "success=0" tcp_client_output.log; then
>> >          >>>> +        echo "TCP client test failed - checking output:"
>> >          >>>> +        cat tcp_client_output.log
>> >          >>>> +        AT_FAIL_IF([true])
>> >          >>>> +    fi
>> >          >>>> +
>> >          >>>> +    # Clean up
>> >          >>>> +    kill $(cat tcpdump-client.pid) $(cat
>> tcpdump-server.pid) $(cat
>> >          >>>> server.pid) 2>/dev/null || true
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +test_fragmented_udp_traffic() {
>> >          >>>> +    local src_port=${1:-5353}
>> >          >>>> +    local dst_port=${2:-4242}
>> >          >>>> +
>> >          >>>> +    check ovn-nbctl --wait=hv sync
>> >          >>>> +    check ovs-appctl dpctl/flush-conntrack
>> >          >>>> +
>> >          >>>> +    NETNS_DAEMONIZE([server], [nc -l -u 172.16.1.2
>> $dst_port >
>> >          >>>> /dev/null], [server.pid])
>> >          >>>> +    NETNS_DAEMONIZE([client], [nc -l -u 172.16.1.3
>> $src_port >
>> >          >>>> /dev/null], [client.pid])
>> >          >>>> +
>> >          >>>> +    # Collect only inbound UDP packets on client and
>> server sides
>> >          >>>> +    NETNS_START_TCPDUMP([client], \
>> >          >>>> +      [-U -i client -Q in -nn -e -q udp],
>> [tcpdump-client])
>> >          >>>> +    NETNS_START_TCPDUMP([server], \
>> >          >>>> +      [-U -i server -Q in -nn -e -q udp],
>> [tcpdump-server])
>> >          >>>> +
>> >          >>>> +    NS_EXEC([client], [udp_client.py -s 172.16.1.3 -d
>> 172.16.1.2 -S
>> >          >>>> $src_port -D $dst_port -M 900 -B 1500 -i client -n 4 -I
>> 0.5])
>> >          >>>> +    NS_EXEC([server], [udp_client.py -s 172.16.1.2 -d
>> 172.16.1.3 -S
>> >          >>>> $dst_port -D $src_port -M 900 -B 1500 -i server -n 4 -I
>> 0.5])
>> >          >>>> +
>> >          >>>> +    sleep 1
>> >          >>>> +    dump_data_plane_flows [udp]
>> >          >>>> +
>> >          >>>> +    OVS_WAIT_UNTIL([test "$(cat tcpdump-server.tcpdump |
>> wc -l)" =
>> >          >> "8"])
>> >          >>>> +    OVS_WAIT_UNTIL([test "$(cat tcpdump-client.tcpdump |
>> wc -l)" =
>> >          >> "8"])
>> >          >>>> +
>> >          >>>> +    kill $(cat tcpdump-client.pid) $(cat
>> tcpdump-server.pid) $(cat
>> >          >>>> server.pid) $(cat client.pid)
>> >          >>>> +}
>> >          >>>> +
>> >          >>>> +ovn-appctl -t ovn-northd vlog/set debug
>> >          >>>> +ovn-appctl -t ovn-controller vlog/set debug
>> >          >>>> +ovn-appctl vlog/set file:dbg
>> >          >>>> +ovs-appctl vlog/set dpif:dbg ofproto:dbg conntrack:dbg
>> >          >>>> +
>> >          >>>> +check ovn-nbctl set NB_Global .
>> options:acl_udp_ct_translation=true
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv acl-del internal
>> >          >>>> +
>> >          >>>> +# Create port group with both client and server to
>> trigger conjunction
>> >          >>>> flows
>> >          >>>> +client_uuid=$(ovn-nbctl --bare --columns=_uuid find
>> logical_switch_port
>> >          >>>> name=client)
>> >          >>>> +server_uuid=$(ovn-nbctl --bare --columns=_uuid find
>> logical_switch_port
>> >          >>>> name=server)
>> >          >>>> +check ovn-nbctl pg-add internal_vms $client_uuid
>> $server_uuid
>> >          >>>> +
>> >          >>>> +# dump_ovs_info
>> >          >>>> +
>> >          >>>> +# Use port group in ACL rules to trigger conjunction
>> flow generation
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms
>> from-lport 1002 "inport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.dst == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms
>> from-lport 1002 "inport
>> >          >> ==
>> >          >>>> @internal_vms && ip4" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms
>> from-lport 1002 "inport
>> >          >> ==
>> >          >>>> @internal_vms && ip6" allow-related
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst == 22"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst == 123"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst == 1666"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst == 9090"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst ==
>> >          >> 40004"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst ==
>> >          >> 51204"
>> >          >>>> allow-related
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.dst >= 3002
>> >          >> &&
>> >          >>>> tcp.dst <=13002" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp && udp.dst >= 5002
>> >          >> &&
>> >          >>>> udp.dst <=10010" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && tcp && tcp.src >= 3002
>> >          >> &&
>> >          >>>> tcp.src <=13002" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp && udp.src >= 5002
>> >          >> &&
>> >          >>>> udp.src <=10010" allow-related
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp && udp.dst == 123"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp && udp.dst == 5060"
>> >          >>>> allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && udp && udp.dst ==
>> >          >> 40004"
>> >          >>>> allow-related
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +check # Add the drop rule using port group
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && ip4 && ip4.src == 0.0.0.0/0 <
>> http://0.0.0.0/0> && icmp4" allow-related
>> >          >>>> +check ovn-nbctl --wait=hv acl-add internal_vms to-lport
>> 1002 "outport
>> >          >> ==
>> >          >>>> @internal_vms && arp" allow-related
>> >          >>>> +
>> >          >>>> +check ovn-nbctl --wait=hv --log --severity=info acl-add
>> internal_vms
>> >          >>>> to-lport 100 "outport == @internal_vms" drop
>> >          >>>> +
>> >          >>>> +AS_BOX([Testing ACL: test_fragmented_udp_traffic])
>> >          >>>> +dump_ovs_info test_fragmented_udp_traffic
>> >          >>>> +test_fragmented_udp_traffic 5060 5060
>> >          >>>> +
>> >          >>>> +AS_BOX([Testing ACL: test_tcp_traffic])
>> >          >>>> +dump_ovs_info test_tcp_traffic
>> >          >>>> +test_tcp_traffic 9090 9090
>> >          >>>> +
>> >          >>>> +AS_BOX([Testing ACL:
>> test_fragmented_udp_traffic_inside_range])
>> >          >>>> +dump_ovs_info test_fragmented_udp_traffic_inside_range
>> >          >>>> +test_fragmented_udp_traffic 5005 5005
>> >          >>>> +
>> >          >>>> +AS_BOX([Testing ACL: test_tcp_traffic_range])
>> >          >>>> +dump_ovs_info test_tcp_traffic_range
>> >          >>>> +test_tcp_traffic 3003 3003
>> >          >>>> +
>> >          >>>> +# Reset the option back to default
>> >          >>>> +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
>> >          >>>> +/WARN|netdev@ovs-netdev: execute.*/d
>> >          >>>> +/dpif|WARN|system@ovs-system: execute.*/d
>> >          >>>> +"])
>> >          >>>> +AT_CLEANUP
>> >          >>>> +])
>> >          >>>> +
>> >          >>>> diff --git a/tests/tcp_simple.py b/tests/tcp_simple.py
>> >          >>>> new file mode 100755
>> >          >>>> index 000000000..c5bbed08f
>> >          >>>> --- /dev/null
>> >          >>>> +++ b/tests/tcp_simple.py
>> >          >>>> @@ -0,0 +1,338 @@
>> >          >>>> +#!/usr/bin/env python3
>> >          >>>> +"""
>> >          >>>> +Simple TCP client/server for testing network
>> connectivity and data
>> >          >>>> integrity.
>> >          >>>> +
>> >          >>>> +This module provides TCP echo server and client
>> functionality for
>> >          >> testing
>> >          >>>> +network connections, data transmission, and MD5 checksum
>> verification.
>> >          >>>> +"""
>> >          >>>> +import socket
>> >          >>>> +import time
>> >          >>>> +import argparse
>> >          >>>> +import sys
>> >          >>>> +import hashlib
>> >          >>>> +import random
>> >          >>>> +
>> >          >>>> +DEFAULT_SRC_IP = "0.0.0.0"
>> >          >>>> +DEFAULT_DST_IP = "0.0.0.0"
>> >          >>>> +DEFAULT_PORT = 8080
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +def calculate_md5(data):
>> >          >>>> +    """Calculate MD5 checksum of data."""
>> >          >>>> +    if isinstance(data, str):
>> >          >>>> +        data = data.encode('utf-8')
>> >          >>>> +    return hashlib.md5(data).hexdigest()
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +def generate_random_bytes(size):
>> >          >>>> +    """Generate random bytes that are valid UTF-8."""
>> >          >>>> +    # Generate random printable ASCII characters
>> (32-126) which are
>> >          >> valid
>> >          >>>> UTF-8
>> >          >>>> +    return bytes([random.randint(32, 126) for _ in
>> range(size)])
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +class TCPServer:
>> >          >>>> +    """Simple TCP echo server using standard sockets."""
>> >          >>>> +
>> >          >>>> +    def __init__(self, bind_ip, port):
>> >          >>>> +        self.bind_ip = bind_ip
>> >          >>>> +        self.port = port
>> >          >>>> +        self.running = False
>> >          >>>> +        self.server_socket = None
>> >          >>>> +
>> >          >>>> +    def start(self):
>> >          >>>> +        """Start the TCP server."""
>> >          >>>> +        exit_code = 0
>> >          >>>> +        self.running = True
>> >          >>>> +        self.server_socket =
>> socket.socket(socket.AF_INET,
>> >          >>>> socket.SOCK_STREAM)
>> >          >>>> +        self.server_socket.setsockopt(socket.SOL_SOCKET,
>> >          >>>> +
>> socket.SO_REUSEADDR, 1)
>> >          >>>> +
>> >          >>>> +        try:
>> >          >>>> +            self.server_socket.bind((self.bind_ip,
>> self.port))
>> >          >>>> +            self.server_socket.listen(5)
>> >          >>>> +            print(f"TCP Server listening on
>> >          >> {self.bind_ip}:{self.port}")
>> >          >>>> +
>> >          >>>> +            while self.running:
>> >          >>>> +                try:
>> >          >>>> +                    client_socket, client_addr =
>> >          >>>> self.server_socket.accept()
>> >          >>>> +                    print(f"Connection from
>> >          >>>> {client_addr[0]}:{client_addr[1]}")
>> >          >>>> +
>> >          >>>> +                    # Handle client directly
>> (single-threaded)
>> >          >>>> +                    self._handle_client(client_socket,
>> client_addr)
>> >          >>>> +
>> >          >>>> +                except socket.error as e:
>> >          >>>> +                    if self.running:
>> >          >>>> +                        print(f"Socket error: {e}")
>> >          >>>> +
>> >          >>>> +        except OSError as e:
>> >          >>>> +            if e.errno == 99:  # Cannot assign requested
>> address
>> >          >>>> +                print(f"Error: Cannot bind to
>> >          >> {self.bind_ip}:{self.port}")
>> >          >>>> +            elif e.errno == 98:  # Address already in use
>> >          >>>> +                print(f"Error: Port {self.port} is
>> already in use.")
>> >          >>>> +            else:
>> >          >>>> +                print(f"Error binding to
>> {self.bind_ip}:{self.port}:
>> >          >> {e}")
>> >          >>>> +        except KeyboardInterrupt:
>> >          >>>> +            print("\nServer shutting down...")
>> >          >>>> +            exit_code = 0
>> >          >>>> +        except Exception as e:
>> >          >>>> +            print(f"Unexpected server error: {e}")
>> >          >>>> +            exit_code = 1
>> >          >>>> +        finally:
>> >          >>>> +            self.running = False
>> >          >>>> +            if self.server_socket:
>> >          >>>> +                self.server_socket.close()
>> >          >>>> +        sys.exit(exit_code)
>> >          >>>> +
>> >          >>>> +    def _handle_client(self, client_socket, client_addr):
>> >          >>>> +        """Handle individual client connection."""
>> >          >>>> +        total_bytes_received = 0
>> >          >>>> +        try:
>> >          >>>> +            while self.running:
>> >          >>>> +                data = client_socket.recv(4096)
>> >          >>>> +                if not data:
>> >          >>>> +                    break
>> >          >>>> +                client_socket.send(data)
>> >          >>>> +                total_bytes_received += len(data)
>> >          >>>> +
>> >          >>>> +            print(f"Total bytes received:
>> {total_bytes_received}")
>> >          >>>> +        except socket.error as e:
>> >          >>>> +            print(f"Client
>> {client_addr[0]}:{client_addr[1]} error:
>> >          >> {e}")
>> >          >>>> +        finally:
>> >          >>>> +            client_socket.close()
>> >          >>>> +            print(f"Connection closed with
>> >          >>>> {client_addr[0]}:{client_addr[1]}")
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +class TCPClient:
>> >          >>>> +    """Simple TCP client using standard sockets."""
>> >          >>>> +
>> >          >>>> +    def __init__(self, src_ip, dst_ip, dst_port):
>> >          >>>> +        self.src_ip = src_ip
>> >          >>>> +        self.dst_ip = dst_ip
>> >          >>>> +        self.dst_port = dst_port
>> >          >>>> +
>> >          >>>> +    def connect_and_send(self, data_len, iterations=1,
>> interval=0.1,
>> >          >>>> +                         unique_data=False):
>> >          >>>> +        """Connect to server and send data."""
>> >          >>>> +        print(f"TCP Client connecting to
>> >          >> {self.dst_ip}:{self.dst_port}")
>> >          >>>> +
>> >          >>>> +        success_count = 0
>> >          >>>> +        correct_responses = 0
>> >          >>>> +
>> >          >>>> +        # Generate data once if not using unique data
>> per iteration
>> >          >>>> +        shared_data_bytes = None
>> >          >>>> +        shared_md5 = None
>> >          >>>> +        if not unique_data:
>> >          >>>> +            shared_data_bytes =
>> generate_random_bytes(data_len)
>> >          >>>> +            shared_md5 = calculate_md5(shared_data_bytes)
>> >          >>>> +            print(f"Generated shared buffer:
>> {len(shared_data_bytes)}
>> >          >>>> bytes, "
>> >          >>>> +                  f"MD5: {shared_md5}")
>> >          >>>> +
>> >          >>>> +        return_code = 0
>> >          >>>> +        for i in range(iterations):
>> >          >>>> +            iteration_result = self._process_iteration(
>> >          >>>> +                i, data_len, unique_data,
>> shared_data_bytes,
>> >          >> shared_md5)
>> >          >>>> +            if iteration_result['success']:
>> >          >>>> +                success_count += 1
>> >          >>>> +                if iteration_result['checksum_match']:
>> >          >>>> +                    correct_responses += 1
>> >          >>>> +            else:
>> >          >>>> +                return_code = 1
>> >          >>>> +
>> >          >>>> +            if i < iterations - 1:
>> >          >>>> +                time.sleep(interval)
>> >          >>>> +
>> >          >>>> +        print(f"Client completed:
>> {success_count}/{iterations} "
>> >          >>>> +              f"successful connections")
>> >          >>>> +        print(f"MD5 checksum verification: "
>> >          >>>> +              f"{correct_responses}/{success_count}
>> correct")
>> >          >>>> +
>> >          >>>> +        return_code = 0 if (correct_responses ==
>> >          >>>> +                           success_count and
>> success_count > 0) else 1
>> >          >>>> +        return return_code
>> >          >>>> +
>> >          >>>> +    def _process_iteration(self, iteration_idx,
>> data_len, unique_data,
>> >          >>>> +                          shared_data_bytes, shared_md5):
>> >          >>>> +        """Process a single iteration of the client
>> test."""
>> >          >>>> +        try:
>> >          >>>> +            # Create socket and connect
>> >          >>>> +            client_socket = socket.socket(socket.AF_INET,
>> >          >>>> +
>> socket.SOCK_STREAM)
>> >          >>>> +
>> >          >>>> +            # Bind to specific source IP if provided
>> >          >>>> +            if self.src_ip != "0.0.0.0":
>> >          >>>> +                client_socket.bind((self.src_ip, 0))
>> >          >>>> +
>> >          >>>> +            client_socket.connect((self.dst_ip,
>> self.dst_port))
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Connected to
>> >          >> server")
>> >          >>>> +
>> >          >>>> +            # Prepare data and calculate MD5 checksum
>> >          >>>> +            if unique_data:
>> >          >>>> +                data_bytes =
>> generate_random_bytes(data_len)
>> >          >>>> +                original_md5 = calculate_md5(data_bytes)
>> >          >>>> +                print(f"Iteration {iteration_idx + 1}:
>> Generated unique
>> >          >>>> data, "
>> >          >>>> +                      f"MD5: {original_md5}")
>> >          >>>> +            else:
>> >          >>>> +                data_bytes = shared_data_bytes
>> >          >>>> +                original_md5 = shared_md5
>> >          >>>> +                print(f"Iteration {iteration_idx + 1}:
>> Using shared
>> >          >>>> buffer, "
>> >          >>>> +                      f"MD5: {original_md5}")
>> >          >>>> +
>> >          >>>> +            client_socket.send(data_bytes)
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}: Sent
>> >          >> {len(data_bytes)}
>> >          >>>> "
>> >          >>>> +                  f"bytes")
>> >          >>>> +
>> >          >>>> +            # Receive echo response
>> >          >>>> +            response =
>> self._receive_response(client_socket,
>> >          >>>> len(data_bytes))
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Received
>> >          >>>> {len(response)} "
>> >          >>>> +                  f"bytes")
>> >          >>>> +
>> >          >>>> +            # Calculate MD5 of received data
>> >          >>>> +            received_md5 = calculate_md5(response)
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Received MD5: "
>> >          >>>> +                  f"{received_md5}")
>> >          >>>> +
>> >          >>>> +            # Verify checksum
>> >          >>>> +            checksum_match = original_md5 == received_md5
>> >          >>>> +
>> >          >>>> +            if checksum_match:
>> >          >>>> +                print(f"Iteration {iteration_idx + 1}: ✓
>> MD5 checksum "
>> >          >>>> +                      f"verified correctly")
>> >          >>>> +            else:
>> >          >>>> +                print(f"Iteration {iteration_idx + 1}: ✗
>> MD5 checksum "
>> >          >>>> +                      f"mismatch!")
>> >          >>>> +
>> >          >>>> +            client_socket.close()
>> >          >>>> +            return {'success': True, 'checksum_match':
>> checksum_match}
>> >          >>>> +
>> >          >>>> +        except ConnectionRefusedError:
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Connection refused
>> >          >> to "
>> >          >>>> +                  f"{self.dst_ip}:{self.dst_port}")
>> >          >>>> +            return {'success': False, 'checksum_match':
>> False}
>> >          >>>> +        except socket.timeout:
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Connection timeout
>> >          >> to "
>> >          >>>> +                  f"{self.dst_ip}:{self.dst_port}")
>> >          >>>> +            return {'success': False, 'checksum_match':
>> False}
>> >          >>>> +        except OSError as os_error:
>> >          >>>> +            self._handle_os_error(iteration_idx,
>> os_error)
>> >          >>>> +            return {'success': False, 'checksum_match':
>> False}
>> >          >>>> +        except socket.error as sock_error:
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Socket error:
>> >          >>>> {sock_error}")
>> >          >>>> +            return {'success': False, 'checksum_match':
>> False}
>> >          >>>> +        except Exception as general_error:
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Unexpected error: "
>> >          >>>> +                  f"{general_error}")
>> >          >>>> +            return {'success': False, 'checksum_match':
>> False}
>> >          >>>> +
>> >          >>>> +    def _receive_response(self, client_socket,
>> bytes_to_receive):
>> >          >>>> +        """Receive response data from server."""
>> >          >>>> +        response = b""
>> >          >>>> +        while len(response) < bytes_to_receive:
>> >          >>>> +            chunk = client_socket.recv(
>> >          >>>> +                min(4096, bytes_to_receive -
>> len(response)))
>> >          >>>> +            if not chunk:
>> >          >>>> +                break
>> >          >>>> +            response += chunk
>> >          >>>> +        return response
>> >          >>>> +
>> >          >>>> +    def _handle_os_error(self, iteration_idx, os_error):
>> >          >>>> +        """Handle OS-specific errors."""
>> >          >>>> +        if os_error.errno == 99:  # Cannot assign
>> requested address
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Cannot bind to
>> >          >> source
>> >          >>>> IP "
>> >          >>>> +                  f"{self.src_ip}")
>> >          >>>> +        elif os_error.errno == 101:  # Network is
>> unreachable
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Network unreachable
>> >          >> to
>> >          >>>> "
>> >          >>>> +                  f"{self.dst_ip}:{self.dst_port}")
>> >          >>>> +        else:
>> >          >>>> +            print(f"Iteration {iteration_idx + 1}:
>> Network error:
>> >          >>>> {os_error}")
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +def main():
>> >          >>>> +    """Main function to parse arguments and run TCP
>> client or
>> >          >> server."""
>> >          >>>> +    parser = argparse.ArgumentParser(
>> >          >>>> +        description="Simple TCP client/server for
>> testing",
>> >          >>>> +
>> formatter_class=argparse.RawDescriptionHelpFormatter,
>> >          >>>> +        epilog="""
>> >          >>>> +COMMON OPTIONS:
>> >          >>>> +  --mode {client,server}  Run in client or server mode
>> (required)
>> >          >>>> +  -p, --port PORT         TCP port (default: 8080)
>> >          >>>> +
>> >          >>>> +CLIENT MODE OPTIONS:
>> >          >>>> +  -s, --src-ip IP         Source IPv4 address (default:
>> 0.0.0.0)
>> >          >>>> +  -d, --dst-ip IP         Destination IPv4 address
>> (default: 0.0.0.0)
>> >          >>>> +  -n, --iterations N      Number of connections to make
>> (default: 1)
>> >          >>>> +  -I, --interval SECS     Seconds between connections
>> (default: 0.1)
>> >          >>>> +  -B, --payload-bytes N   Total TCP payload bytes
>> (minimum: 500)
>> >          >>>> +  --unique-data           Generate unique random data
>> for each
>> >          >> iteration
>> >          >>>> +
>> >          >>>> +SERVER MODE OPTIONS:
>> >          >>>> +  --bind-ip IP            Server bind IP address
>> (default: 172.16.1.2)
>> >          >>>> +
>> >          >>>> +""")
>> >          >>>> +
>> >          >>>> +    # Mode selection (required)
>> >          >>>> +    parser.add_argument("--mode", choices=['client',
>> 'server'],
>> >          >>>> required=True,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +
>> >          >>>> +    # Common arguments
>> >          >>>> +    parser.add_argument("-p", "--port", type=int,
>> default=DEFAULT_PORT,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +
>> >          >>>> +    # Client mode arguments
>> >          >>>> +    parser.add_argument("-B", "--payload-bytes",
>> type=int,
>> >          >> default=None,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +    parser.add_argument("-s", "--src-ip",
>> default=DEFAULT_SRC_IP,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +    parser.add_argument("-d", "--dst-ip",
>> default=DEFAULT_DST_IP,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +    parser.add_argument("-I", "--interval", type=float,
>> default=0.1,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +    parser.add_argument("-n", "--iterations", type=int,
>> default=1,
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +    parser.add_argument("--unique-data",
>> action="store_true",
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +
>> >          >>>> +    # Server mode arguments
>> >          >>>> +    parser.add_argument("--bind-ip", default="0.0.0.0",
>> >          >>>> +                       help=argparse.SUPPRESS)
>> >          >>>> +
>> >          >>>> +    args = parser.parse_args()
>> >          >>>> +
>> >          >>>> +    # Validate arguments
>> >          >>>> +    if args.port < 1 or args.port > 65535:
>> >          >>>> +        print(f"Error: Port {args.port} is out of valid
>> range
>> >          >> (1-65535)")
>> >          >>>> +        sys.exit(1)
>> >          >>>> +
>> >          >>>> +    if args.mode == 'client':
>> >          >>>> +        if args.iterations < 1:
>> >          >>>> +            print(f"Error: Iterations must be at least
>> 1, "
>> >          >>>> +                  f"got {args.iterations}")
>> >          >>>> +            sys.exit(1)
>> >          >>>> +        if args.interval < 0:
>> >          >>>> +            print(f"Error: Interval cannot be negative, "
>> >          >>>> +                  f"got {args.interval}")
>> >          >>>> +            sys.exit(1)
>> >          >>>> +        if args.payload_bytes is not None and
>> args.payload_bytes < 500:
>> >          >>>> +            print(f"Error: Payload bytes must be at
>> least 500, "
>> >          >>>> +                  f"got {args.payload_bytes}")
>> >          >>>> +            sys.exit(1)
>> >          >>>> +
>> >          >>>> +    if args.mode == 'server':
>> >          >>>> +        # Run server
>> >          >>>> +        server = TCPServer(args.bind_ip, args.port)
>> >          >>>> +        server.start()
>> >          >>>> +    elif args.mode == 'client':
>> >          >>>> +        # Run client
>> >          >>>> +        client = TCPClient(args.src_ip, args.dst_ip,
>> args.port)
>> >          >>>> +        success = client.connect_and_send(
>> >          >>>> +            max(0, int(args.payload_bytes)),
>> args.iterations,
>> >          >>>> +            args.interval, args.unique_data)
>> >          >>>> +
>> >          >>>> +        # Summary
>> >          >>>> +        print(f"Client summary:
>> payload_bytes={args.payload_bytes} "
>> >          >>>> +              f"iterations={args.iterations}
>> success={success}")
>> >          >>>> +
>> >          >>>> +        sys.exit(success)
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +if __name__ == "__main__":
>> >          >>>> +    main()
>> >          >>>> diff --git a/tests/udp_client.py b/tests/udp_client.py
>> >          >>>> new file mode 100755
>> >          >>>> index 000000000..cc67275f8
>> >          >>>> --- /dev/null
>> >          >>>> +++ b/tests/udp_client.py
>> >          >>>> @@ -0,0 +1,113 @@
>> >          >>>> +#!/usr/bin/env python3
>> >          >>>> +"""
>> >          >>>> +Scapy UDP client with MTU/payload control for testing
>> fragmented UDP
>> >          >>>> traffic.
>> >          >>>> +
>> >          >>>> +This module provides functionality to send fragmented
>> UDP packets using
>> >          >>>> Scapy,
>> >          >>>> +allowing control over MTU, payload size, and
>> fragmentation behavior.
>> >          >>>> +"""
>> >          >>>> +import time
>> >          >>>> +import argparse
>> >          >>>> +import sys
>> >          >>>> +from scapy.all import IP, UDP, Raw, send, fragment
>> >          >>>> +
>> >          >>>> +# Defaults for same logical switch topology
>> >          >>>> +DEFAULT_SRC_IP = "172.16.1.3"
>> >          >>>> +DEFAULT_DST_IP = "172.16.1.2"
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +def read_iface_mtu(ifname: str):
>> >          >>>> +    """Return MTU of interface or None if not
>> available."""
>> >          >>>> +    try:
>> >          >>>> +        path = f"/sys/class/net/{ifname}/mtu"
>> >          >>>> +        with open(path, "r", encoding="utf-8") as f:
>> >          >>>> +            return int(f.read().strip())
>> >          >>>> +    except Exception:
>> >          >>>> +        return None
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +def main():
>> >          >>>> +    """Main function to parse arguments and send
>> fragmented UDP
>> >          >>>> packets."""
>> >          >>>> +    parser = argparse.ArgumentParser(
>> >          >>>> +        description="Scapy UDP client with MTU/payload
>> control")
>> >          >>>> +    parser.add_argument("-M", "--mtu", type=int,
>> default=None,
>> >          >>>> +        help="Target MTU; payload ~ mtu-28 (IPv4+UDP).")
>> >          >>>> +    parser.add_argument("-B", "--payload-bytes",
>> type=int,
>> >          >> default=None,
>> >          >>>> +        help="Total UDP payload bytes (override
>> default).")
>> >          >>>> +    parser.add_argument("-S", "--sport", type=int,
>> default=4242,
>> >          >>>> +        help="UDP source port")
>> >          >>>> +    parser.add_argument("-D", "--dport", type=int,
>> default=4242,
>> >          >>>> +        help="UDP destination port")
>> >          >>>> +    parser.add_argument("-s", "--src-ip",
>> default=DEFAULT_SRC_IP,
>> >          >>>> +        help="Source IPv4 address")
>> >          >>>> +    parser.add_argument("-d", "--dst-ip",
>> default=DEFAULT_DST_IP,
>> >          >>>> +        help="Destination IPv4 address")
>> >          >>>> +    parser.add_argument("-i", "--iface",
>> default="client",
>> >          >>>> +        help="Egress interface (default: 'client').")
>> >          >>>> +    parser.add_argument("-I", "--interval", type=float,
>> default=0.1,
>> >          >>>> +        help="Seconds between sends.")
>> >          >>>> +    parser.add_argument("-n", "--iterations", type=int,
>> default=1,
>> >          >>>> +        help="Number of datagrams to send.")
>> >          >>>> +    args = parser.parse_args()
>> >          >>>> +
>> >          >>>> +    # Derive fragment size: for link MTU M, fragsize
>> should typically
>> >          >> be
>> >          >>>> M-20
>> >          >>>> +    # (IP header)
>> >          >>>> +    if args.mtu is not None:
>> >          >>>> +        fragsize = max(68, int(args.mtu) - 20)
>> >          >>>> +    else:
>> >          >>>> +        fragsize = 1480  # default for 1500 MTU
>> >          >>>> +
>> >          >>>> +    # Build payload; size can be user-controlled or
>> synthesized to
>> >          >> ensure
>> >          >>>> +    # fragmentation
>> >          >>>> +    if args.payload_bytes is not None:
>> >          >>>> +        payload_len = max(0, int(args.payload_bytes))
>> >          >>>> +        payload = b"x" * payload_len
>> >          >>>> +    elif args.mtu is not None:
>> >          >>>> +        # Ensure payload exceeds fragsize-8 (UDP header)
>> so we actually
>> >          >>>> +        # fragment.
>> >          >>>> +        base_len = max(0, int(args.mtu) - 28)
>> >          >>>> +        min_len_to_fragment = max(0, fragsize - 8 + 1)
>> >          >>>> +        payload_len = (base_len if base_len >
>> min_len_to_fragment else
>> >          >>>> +                       fragsize * 2)
>> >          >>>> +        payload = b"x" * payload_len
>> >          >>>> +    else:
>> >          >>>> +        # No explicit size; synthesize a payload big
>> enough to
>> >          >> fragment at
>> >          >>>> +        # default fragsize
>> >          >>>> +        payload_len = fragsize * 2
>> >          >>>> +        payload = b"x" * payload_len
>> >          >>>> +
>> >          >>>> +    # Construct full IP/UDP packet and fragment it
>> explicitly
>> >          >>>> +    ip_layer = IP(src=args.src_ip, dst=args.dst_ip)
>> >          >>>> +    udp_layer = UDP(sport=args.sport, dport=args.dport)
>> >          >>>> +    full_packet = ip_layer / udp_layer /
>> Raw(load=payload)
>> >          >>>> +    frags = fragment(full_packet, fragsize=fragsize)
>> >          >>>> +
>> >          >>>> +    total_fragments_per_send = len(frags)
>> >          >>>> +    for _ in range(max(0, args.iterations)):
>> >          >>>> +        try:
>> >          >>>> +            send(frags, iface=args.iface,
>> return_packets=True,
>> >          >>>> verbose=False)
>> >          >>>> +        except OSError as e:
>> >          >>>> +            # Errno 90: Message too long (likely iface
>> MTU < chosen
>> >          >> --mtu)
>> >          >>>> +            if getattr(e, "errno", None) == 90:
>> >          >>>> +                iface_mtu = read_iface_mtu(args.iface)
>> >          >>>> +                mtu_note = (f"iface_mtu={iface_mtu}" if
>> iface_mtu is
>> >          >> not
>> >          >>>> None
>> >          >>>> +                            else "iface_mtu=unknown")
>> >          >>>> +                print("ERROR: packet exceeds interface
>> MTU. "
>> >          >>>> +                    f"iface={args.iface} {mtu_note}
>> chosen_mtu="
>> >          >>>> +                    f"{args.mtu if args.mtu is not None
>> else 1500} "
>> >          >>>> +                    f"fragsize={fragsize}. Set --mtu to
>> the interface
>> >          >>>> MTU.",
>> >          >>>> +                    file=sys.stderr)
>> >          >>>> +                sys.exit(2)
>> >          >>>> +            raise
>> >          >>>> +        time.sleep(args.interval)
>> >          >>>> +
>> >          >>>> +    # Summary
>> >          >>>> +    mtu_print = args.mtu if args.mtu is not None else
>> 1500
>> >          >>>> +    total_frags = total_fragments_per_send * max(0,
>> args.iterations)
>> >          >>>> +    print(f"payload_bytes={payload_len} mtu={mtu_print}
>> >          >>>> fragsize={fragsize} "
>> >          >>>> +
>> f"fragments_per_datagram={total_fragments_per_send} "
>> >          >>>> +        f"total_fragments_sent={total_frags}")
>> >          >>>> +
>> >          >>>> +
>> >          >>>> +if __name__ == "__main__":
>> >          >>>> +    main()
>> >          >>>> +    sys.exit(0)
>> >          >>>> --
>> >          >>>> 2.43.0
>> >          >>>>
>> >          >>>>
>> >          >>> Thanks,
>> >          >>> Ales
>> >          >>> _______________________________________________
>> >          >>> dev mailing list
>> >          >>> [email protected] <mailto:[email protected]>
>> >          >>> https://mail.openvswitch.org/mailman/listinfo/ovs-dev <
>> https://mail.openvswitch.org/mailman/listinfo/ovs-dev>
>> >          >>
>> >          >>
>> >          > _______________________________________________
>> >          > dev mailing list
>> >          > [email protected] <mailto:[email protected]>
>> >          > https://mail.openvswitch.org/mailman/listinfo/ovs-dev <
>> 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