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