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. > 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
