On Fri, Sep 26, 2025 at 9:15 PM Erlon Rodrigues Cruz <[email protected]> wrote:
> Hi Dumitru, > > > On Fri, Sep 26, 2025 at 4:16 AM Dumitru Ceara <[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; >> >> 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. > > 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]> >> 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]> >> >> 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 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 && 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]> >> >>>> --- >> >>>> >> >>> >> >>> 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 | 4 +- >> >>>> tests/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 && 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 b/tests/automake.mk >> >>>> index adfa19503..edb370b99 100644 >> >>>> --- a/tests/automake.mk >> >>>> +++ b/tests/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 b/tests/system-ovn.at >> >>>> index 8e356df6f..805e471df 100644 >> >>>> --- a/tests/system-ovn.at >> >>>> +++ b/tests/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) and "internal" ( >> >>>> 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 >> >>>> +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 >> >>>> +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", >> >> "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", >> >> "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 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 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) and "internal" ( >> >>>> 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 >> >>>> +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 >> >>>> +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", >> >> "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 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", >> >> "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 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 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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] >> >>> https://mail.openvswitch.org/mailman/listinfo/ovs-dev >> >> >> >> >> > _______________________________________________ >> > dev mailing list >> > [email protected] >> > https://mail.openvswitch.org/mailman/listinfo/ovs-dev >> >> _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
