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
