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
