Skip to content

Add UDP GSO/GRO support (Linux) and --gsro switch#1925

Merged
bmah888 merged 12 commits intoesnet:masterfrom
gegles:gsro
Feb 18, 2026
Merged

Add UDP GSO/GRO support (Linux) and --gsro switch#1925
bmah888 merged 12 commits intoesnet:masterfrom
gegles:gsro

Conversation

@gegles
Copy link
Contributor

@gegles gegles commented Aug 8, 2025

UDP GSO/GRO Support - Refactored Implementation

This PR implements UDP Generic Segmentation Offload (GSO) and Generic Receive Offload (GRO) support for iperf3 on Linux, addressing all maintainer feedback with a cleaned-up implementation.

Changes

1. Feature Enablement (Disabled by Default)

  • Changed from --no-gsro to --gsro flag
  • GSO/GRO is now opt-in rather than opt-out
  • Default behavior unchanged for existing users

2. Code Refactoring

  • Eliminated ~84 lines of duplicated code
  • Unified packet processing loops in iperf_udp_send() and iperf_udp_recv()
  • Single code path handles both GSO/GRO-enabled and disabled cases
  • Improved maintainability and reduced error potential

3. Bug Fix: Missing Counters

  • Critical fix: Loss and jitter counters now work in ALL modes
  • Previously only calculated when GRO was enabled
  • Moved counter calculations outside #ifdef guards
  • Users without --gsro now get proper statistics

4. Debug Output Control

  • Gated GSO/GRO diagnostic messages behind test->debug flag
  • Cleaner output for production use
  • Debug info still available with -d flag

5. Documentation

  • Added --gsro flag to iperf3.1 man page
  • Documented GSO/GRO functionality and platform support

Testing

On 2 bare-metal Linux machines connected with 100Gbps NICs I get:

Normal mode (no GSO/GRO):

./src/iperf3 -c rapid10-100g -u -b 35G
Connecting to host rapid10-100g, port 5201
[  5] local 192.168.100.9 port 47526 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec   296 MBytes  2.48 Gbits/sec  214425  
[  5]   1.00-2.00   sec   294 MBytes  2.47 Gbits/sec  212846  
[  5]   2.00-3.00   sec   291 MBytes  2.44 Gbits/sec  210867  
[  5]   3.00-4.00   sec   291 MBytes  2.44 Gbits/sec  210949  
[  5]   4.00-5.00   sec   292 MBytes  2.45 Gbits/sec  211456  
[  5]   5.00-6.00   sec   296 MBytes  2.48 Gbits/sec  214033  
[  5]   6.00-7.00   sec   296 MBytes  2.49 Gbits/sec  214711  
[  5]   7.00-8.00   sec   296 MBytes  2.48 Gbits/sec  214328  
[  5]   8.00-9.00   sec   296 MBytes  2.48 Gbits/sec  214206  
[  5]   9.00-10.00  sec   296 MBytes  2.48 Gbits/sec  214480  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  2.88 GBytes  2.47 Gbits/sec  0.000 ms  0/2132301 (0%)  sender
[  5]   0.00-10.00  sec  2.88 GBytes  2.47 Gbits/sec  0.002 ms  0/2132301 (0%)  receiver

So 2.47 Gbits/sec max (even though 35G requested)

Now with --gsro enabled:

./src/iperf3 -c rapid10-100g -u -b 35G --gsro
Connecting to host rapid10-100g, port 5201
[  5] local 192.168.100.9 port 56515 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  4.08 GBytes  35.0 Gbits/sec  3024450  
[  5]   1.00-2.00   sec  4.07 GBytes  35.0 Gbits/sec  3021435  
[  5]   2.00-3.00   sec  4.07 GBytes  35.0 Gbits/sec  3021255  
[  5]   3.00-4.00   sec  4.07 GBytes  35.0 Gbits/sec  3021434  
[  5]   4.00-5.00   sec  4.07 GBytes  35.0 Gbits/sec  3021526  
[  5]   5.00-6.00   sec  4.07 GBytes  35.0 Gbits/sec  3021390  
[  5]   6.00-7.00   sec  4.07 GBytes  35.0 Gbits/sec  3021435  
[  5]   7.00-8.00   sec  4.07 GBytes  35.0 Gbits/sec  3021390  
[  5]   8.00-9.00   sec  4.07 GBytes  35.0 Gbits/sec  3019132  
[  5]   9.00-10.00  sec  4.08 GBytes  35.0 Gbits/sec  3023603  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  40.7 GBytes  35.0 Gbits/sec  0.000 ms  0/30217050 (0%)  sender
[  5]   0.00-10.00  sec  40.7 GBytes  35.0 Gbits/sec  0.000 ms  0/30217005 (0%)  receiver

Result: 35.0 Gbits/sec max throughput achieved. Beyond that some packet loss starts to happen due to CPU bound. On more modern machines more could be achieved.

Benefits

  • Reduced CPU overhead for high-throughput UDP tests
  • Improved throughput on supported hardware
  • Cleaner, more maintainable codebase
  • Proper statistics in all modes

@gegles
Copy link
Contributor Author

gegles commented Aug 12, 2025

FYI @bmah888 or @jefposkanzer, any way someone can review and hopefully merge this?

@gegles
Copy link
Contributor Author

gegles commented Aug 25, 2025

Is anybody able to review this? Who are the active/official maintainers for this project?

@gegles
Copy link
Contributor Author

gegles commented Sep 4, 2025

@marcosfsch, FYI (as I saw you're basing your work on this branch), I added a fix for the loss calculation...
Also you can see my updated test results in the Testing Done section of the description. thx!

src/iperf_api.c Outdated
break;
#endif
#if defined(HAVE_UDP_SEGMENT) || defined(HAVE_UDP_GRO)
case OPT_NO_GSRO:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have experience with using gso/gro, but since there are two separate options for setsockopt(), it may be better to also allow setting these options separately. This can be done by adding an optional_argument:
--no-gsro [<GSO>][/<GRO>], where GSO/GRO are boolean (0/1, T/F, E/N (enable/disable), etc.). The default will be that gso/gro will not be supported as it is implemented now (with the name of the option, this probably means they are set to true by default). For example of how to define and parse option with optional arguments see --cntl-ka (OPT_CNTL_KA).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidBar-On I hear what you’re saying, and I’m open to changing the option’s behavior. For example, I originally had two distinct options to explicitly enable or disable each feature (--no-gso and --no-gro). But then I started thinking: if the kernel supports them and they’re implemented correctly, why wouldn’t we want both enabled by default? The packets on the wire are identical, and the benefits—whether higher throughput or lower CPU usage—are significant.

That said, if the consensus is to provide more knobs and finer-grained control, I’m fine with that too.

src/iperf_api.c Outdated
test->settings->gso_bf_size = (test->settings->gso_bf_size / test->settings->gso_dg_size) * test->settings->gso_dg_size;
} else {
/* If gso_dg_size is 0 (unlimited bandwidth), use default UDP datagram size */
test->settings->gso_dg_size = 1472; /* Standard UDP payload size for Ethernet MTU */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using DEFAULT_UDP_BLKSIZE instead of 1472? If it is important that 1472 will be used, suggest to add #define DEFAULT_GSO_DG_BLKSIZE 1472.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch. I addressed it in 569ab0b

I removed the hard-coded 1472 and switched the GSO fallback to use the existing DEFAULT_UDP_BLKSIZE.

This avoids the magic number and keeps a conservative default that’s safer across common IPv4/IPv6 paths when MSS isn’t available. Also verified that when users specify
-l/--length, that value drives both the UDP block size and gso_dg_size; otherwise gso_dg_size follows the computed blksize, only falling back to DEFAULT_UDP_BLKSIZE in the unlimited (0) case.

@davidBar-On
Copy link
Contributor

Added some comments to the code, but I have a question: there are already 4 other PRs about GSO/GRO - #921, #1309, #1616, #1854. How is this PR different?

@gegles
Copy link
Contributor Author

gegles commented Sep 5, 2025

Added some comments to the code, but I have a question: there are already 4 other PRs about GSO/GRO - #921, #1309, #1616, #1854. How is this PR different?

Yes — I reviewed the prior related PRs. Most are quite old/stale, not rebased onto current master, and miss key pieces we’ve addressed here:

  • Missing both sides: Many touch GSO only (no GRO) or vice‑versa.
  • No feature gating: Lack proper Autoconf detection and Linux‑only guards.
  • No param exchange: Don’t propagate settings over the control channel; mix client policy into server.
  • Conflicts/regressions: Diverge from current UDP code paths and JSON schema.

This PR is up to date, rebased, and integrates:

  • Client‑driven --no-gsro, JSON param exchange, and safe defaults.
  • Per‑endpoint setsockopt with local fallback based on kernel support.
  • No magic numbers and unit tests passing.

If there’s a specific older PR with a feature we should carry forward, I’m happy to cross‑check and fold it in.

@gegles
Copy link
Contributor Author

gegles commented Sep 11, 2025

@davidBar-On Any further comment? or could this be approved/merged? ;-)

@gegles gegles requested a review from davidBar-On September 11, 2025 19:23
@davidBar-On
Copy link
Contributor

@davidBar-On Any further comment? or could this be approved/merged? ;-)

@gegles, I am not from the iperf3 maintenance team, so I don't know if or when this PR can be merged into the the mainline. As I wrote, my own opinion is that backward compatibility must be kept, even if ideally your approach is the better. However, this is only my private opinion and I cannot speak for the iperf3 team.

@swlars
Copy link
Contributor

swlars commented Oct 10, 2025

Hi! Thank you for the pull request. We've looked at it briefly, but we might take a little more time to evaluate this change, especially since it's so large and it's enabled by default. We like the performance increase, but we're concerned about the maintainability over time, so we'd like to take a closer look at it.

@gegles
Copy link
Contributor Author

gegles commented Oct 10, 2025

Hi! Thank you for the pull request. We've looked at it briefly, but we might take a little more time to evaluate this change, especially since it's so large and it's enabled by default. We like the performance increase, but we're concerned about the maintainability over time, so we'd like to take a closer look at it.

My pleasure! Yeah, totally understand. Please feel free to change/tweak anything or let me know how I can help. Ultimately, I see no reasons why it shouldn't be on by default wherever available, but I am also fine to reverse the logic if we want to take a more careful approach first... LMK.

@gegles
Copy link
Contributor Author

gegles commented Nov 17, 2025

@swlars, just FYI, I rebased on latest main (no changes, just a minor conflict resolution.

Any idea when you'll be able to review/merge this?

LMK. thx!

@gegles
Copy link
Contributor Author

gegles commented Dec 15, 2025

@swlars, just FYI, I rebased on latest main (no changes, just a minor conflict resolution.

Any idea when you'll be able to review/merge this?

LMK. thx!

@gegles
Copy link
Contributor Author

gegles commented Jan 7, 2026

@swlars, just FYI, I rebased on latest main.

Any idea when you'll be able to review/merge this?

LMK. thx!

marcosfsch and others added 4 commits January 26, 2026 10:02
This change adds first-class support for Linux UDP Generic Segmentation
Offload (GSO) and Generic Receive Offload (GRO) in iperf3.

At configure time, the build detects availability of the UDP_SEGMENT
and UDP_GRO socket options via <linux/udp.h> and enables code paths
accordingly. On capable systems, these features are now enabled by
default for UDP tests.

A new command-line flag, --no-gsro, allows users to disable GSO and GRO
even when supported by the kernel. Help text is included in usage_longstr.

Additional changes:
 - Updated iperf_settings to track GSO/GRO state and buffer/segment sizes.
 - Added a warning if the configured UDP block size exceeds the TCP MSS.
 - Ensured behavior is unchanged on systems without GSO/GRO support.

GSO can reduce CPU overhead on send by offloading UDP segmentation to
the kernel/NIC. GRO can reduce per-packet processing cost on receive by
coalescing incoming UDP segments. Together they can improve throughput
and efficiency in high-rate UDP tests on modern Linux systems.

# Conflicts:
#	src/iperf_api.c
#	src/iperf_api.h
Parse coalesced GRO payloads using the negotiated blksize stride and account loss/jitter per datagram. Avoids inflated “loss” when kernel GRO hints are unreliable. No change to GSO send behavior.
Use the existing DEFAULT_UDP_BLKSIZE for GSO datagram-size fallback instead of the literal 1472.

Rationale: avoids a magic number and keeps a conservative, widely safe default across IPv4/IPv6 when the control socket MSS is unavailable. In normal operation UDP blksize is derived from the control TCP MSS; this constant fallback is only used when MSS cannot be determined or when the computed gso_dg_size ends up 0 (unlimited case).

Behavior notes:

- If the user sets -l/--length, that value drives both UDP block size and gso_dg_size.

- Otherwise, gso_dg_size tracks the chosen blksize; it falls back to DEFAULT_UDP_BLKSIZE only when the computed value is 0.

Files:

- src/iperf_api.c: update two fallback sites

- src/iperf_client_api.c: update fallback site
…pts and applies locally

Client remains the source of truth for UDP GSO/GRO policy (including --no-gsro). During parameter exchange, the client now sends GSO/GRO flags and sizes in JSON, and the server simply consumes those values without recomputing.

Kernel capability gating stays local and authoritative: each endpoint attempts to enable GSO/GRO on its own sockets via setsockopt, and if the kernel rejects it, we log and flip the local flag off. This handles the case where only one side supports the feature (GSO for the sender, GRO for the receiver).

Backward compatibility: if talking to an older client that doesn’t send GSO fields, the server derives gso_dg_size from blksize and adjusts gso_bf_size, falling back to DEFAULT_UDP_BLKSIZE if zero. This preserves previous behavior without overriding explicit client intent.

Behavior details:

- --no-gsro on the client sends gso=0 and gro=0, so the server won’t try to enable them.

- If -l/--length is provided, blksize (and therefore gso_dg_size when enabled) follows that value; otherwise default logic applies.

Files: src/iperf_api.c (send_parameters adds gso/gro fields; get_parameters reads them and removes server-side recompute unless needed for compatibility).
@gegles
Copy link
Contributor Author

gegles commented Jan 26, 2026

@swlars, just FYI, I rebased on latest main.

Any idea when you'll be able to review/merge this?

LMK. thx!

@swlars
Copy link
Contributor

swlars commented Jan 26, 2026

Hi! Thanks for the pull request, we're going back and forth on whether to include this feature because we're concerned about the amount of support it might need in the future. We're not sure how we would test and maintain it, and while there's significant community support, our main focus is supporting ESnet and they currently have no need for it. Is there a specific use case you're considering?

Change from enabled-by-default with --no-gsro to disabled-by-default
with --gsro flag. This makes GSO/GRO opt-in rather than opt-out.

Changes:
- Rename --no-gsro to --gsro flag
- Change default initialization from enabled (1) to disabled (0)
- Update option handler to enable instead of disable
- Update help text to reflect new behavior
Add documentation for the --gsro flag in the manual page,
describing UDP GSO/GRO functionality and its benefits.
Refactor iperf_udp_send() and iperf_udp_recv() to use unified loops
that handle both GSO/GRO-enabled and disabled cases in a single code
path, eliminating code duplication.

Key changes:
- Configure loop parameters (dgram_sz, buf_sz) upfront based on
  GSO/GRO availability
- Use single unified loop for packet processing regardless of
  GSO/GRO state
- Restore loss counter increments and jitter computation outside
  #ifdef guards so they work when GSO/GRO is disabled
- Gate diagnostic output in iperf_udp_gso() and iperf_udp_gro()
  behind test->debug checks to prevent unwanted verbosity

This addresses maintainer feedback to eliminate separate code branches
and ensure counters work correctly in all configurations.

Tested:
- Normal UDP mode: jitter and loss counters working correctly
- --gsro mode: jitter and loss counters working correctly
- Debug output properly gated behind -d flag
@gegles
Copy link
Contributor Author

gegles commented Feb 10, 2026

Thanks for the detailed feedback! I've addressed all the recommendations:

Changes Made

1. Feature Enablement ✅

  • Switched from --no-gsro (enabled-by-default) to --gsro (disabled-by-default)
  • Renamed OPT_NO_GSRO to OPT_GSRO
  • Updated defaults: gso = 0 and gro = 0
  • Updated help text and all related comments

2. Code Refactoring ✅

Eliminated duplication by using unified loops as suggested:

  • iperf_udp_send(): Configure loop parameters upfront (dgram_sz, buf_sz), single loop processes both GSO-on and GSO-off cases
  • iperf_udp_recv(): Same pattern - configure params based on GRO availability, unified loop handles all cases
  • Result: 84 fewer lines, single code path for packet processing

3. Missing Counters ✅

This was a critical bug fix:

  • Loss counter increments and jitter computation now outside #ifdef guards
  • Previously only worked when GRO was enabled
  • Now works correctly in all configurations (tested both modes)

4. Debug Output Control ✅

  • Gated all iperf_printf() calls in iperf_udp_gso() and iperf_udp_gro() behind test->debug checks
  • No spam in normal operation
  • Messages appear with -d flag as expected

5. Documentation ✅

  • Added --gsro entry to iperf3.1 man page
  • Documented GSO/GRO functionality and Linux-only support

Testing

Tested on macOS within Linux containers (Ubuntu):

  • Normal mode (without --gsro): ~88.8 Gbps, 0% loss, jitter tracking working
  • --gsro mode: ~77.4 Gbps, 0% loss, jitter tracking working
  • Debug mode: GSO/GRO messages properly shown only with -d flag
  • Build clean, no regressions

Note: Testing was done in containerized environment. Final validation on real hardware with physical 10/40/100 Gbps NICs would be valuable before merge.

The refactoring closely follows your suggested pattern - unified loops with parameters configured upfront. Let me know if you'd like any adjustments!

@bmah888
Copy link
Contributor

bmah888 commented Feb 10, 2026

Wow @gegles you're fast! I just kicked off the GitHub Actions builds. Might be a few days before we get time to look at this, so if you don't get any feedback back right away we're not ignoring you.

Reject --gsro when used with -s (server mode) by returning
IECLIENTONLY, matching the pattern used by other client-only flags.
The server already receives GSO/GRO settings from the client via the
JSON parameter exchange, so passing --gsro on the server command line
has no effect. Update help text and man page accordingly.
@gegles
Copy link
Contributor Author

gegles commented Feb 10, 2026

FYI, I've now also made the option client-side only for clarity. but it automatically enables it on the server as well when set (and if available).

@gegles
Copy link
Contributor Author

gegles commented Feb 10, 2026

@swlars @bmah888,

On 2 bare-metal Linux machines connected with 100Gbps NICs I get:

Normal mode (no GSO/GRO):

./src/iperf3 -c rapid10-100g -u -b 35G
Connecting to host rapid10-100g, port 5201
[  5] local 192.168.100.9 port 47526 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec   296 MBytes  2.48 Gbits/sec  214425  
[  5]   1.00-2.00   sec   294 MBytes  2.47 Gbits/sec  212846  
[  5]   2.00-3.00   sec   291 MBytes  2.44 Gbits/sec  210867  
[  5]   3.00-4.00   sec   291 MBytes  2.44 Gbits/sec  210949  
[  5]   4.00-5.00   sec   292 MBytes  2.45 Gbits/sec  211456  
[  5]   5.00-6.00   sec   296 MBytes  2.48 Gbits/sec  214033  
[  5]   6.00-7.00   sec   296 MBytes  2.49 Gbits/sec  214711  
[  5]   7.00-8.00   sec   296 MBytes  2.48 Gbits/sec  214328  
[  5]   8.00-9.00   sec   296 MBytes  2.48 Gbits/sec  214206  
[  5]   9.00-10.00  sec   296 MBytes  2.48 Gbits/sec  214480  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  2.88 GBytes  2.47 Gbits/sec  0.000 ms  0/2132301 (0%)  sender
[  5]   0.00-10.00  sec  2.88 GBytes  2.47 Gbits/sec  0.002 ms  0/2132301 (0%)  receiver

So only 2.47 Gbits/sec max (even though 35G requested)

Now with --gsro enabled:

./src/iperf3 -c rapid10-100g -u -b 35G --gsro
Connecting to host rapid10-100g, port 5201
[  5] local 192.168.100.9 port 56515 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  4.08 GBytes  35.0 Gbits/sec  3024450  
[  5]   1.00-2.00   sec  4.07 GBytes  35.0 Gbits/sec  3021435  
[  5]   2.00-3.00   sec  4.07 GBytes  35.0 Gbits/sec  3021255  
[  5]   3.00-4.00   sec  4.07 GBytes  35.0 Gbits/sec  3021434  
[  5]   4.00-5.00   sec  4.07 GBytes  35.0 Gbits/sec  3021526  
[  5]   5.00-6.00   sec  4.07 GBytes  35.0 Gbits/sec  3021390  
[  5]   6.00-7.00   sec  4.07 GBytes  35.0 Gbits/sec  3021435  
[  5]   7.00-8.00   sec  4.07 GBytes  35.0 Gbits/sec  3021390  
[  5]   8.00-9.00   sec  4.07 GBytes  35.0 Gbits/sec  3019132  
[  5]   9.00-10.00  sec  4.08 GBytes  35.0 Gbits/sec  3023603  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  40.7 GBytes  35.0 Gbits/sec  0.000 ms  0/30217050 (0%)  sender
[  5]   0.00-10.00  sec  40.7 GBytes  35.0 Gbits/sec  0.000 ms  0/30217005 (0%)  receiver

Result: 35.0 Gbits/sec max throughput achieved. Beyond that some packet loss starts to happen due to CPU bound. On more modern machines more could be achieved.

@davidBar-On
Copy link
Contributor

I have added few minor comments to the code (but I didn't review the main functionality and did not try to test it, as I don't have the proper testing environment).

In addition, I think that at the client side, the --gsro option should always be available, regardless of defined(HAVE_UDP_SEGMENT) || defined(HAVE_UDP_GRO), and that the client should always send the option related values to the server. The reason is that the server may support these options, so even if the client doesn't support them it will allow to activate GSO/GRO on the server for the test.

If this is implemented, then the help message may reflect whether the client supports the options. Something like this (I removed the "(client-only option)", as it appears in the "Client specific:" section of the help message):

#if defined(HAVE_UDP_SEGMENT) || defined(HAVE_UDP_GRO)
                           "  --gsro                    enable UDP GSO/GRO on both client and server\n"
#else
                           "  --gsro                    enable UDP GSO/GRO on the server\n"
#endif

Address PR feedback to allow clients without GSO/GRO support to request
server-side enablement. Key changes:

- Remove conditional compilation guards around --gsro option
- Always define gso/gro fields in iperf_settings structure
- Always send/receive GSO/GRO parameters in JSON protocol
- Add warnings when --gsro requested but not supported locally
- Socket options only applied when HAVE_UDP_SEGMENT/HAVE_UDP_GRO defined

This allows a client compiled without GSO/GRO support to still use --gsro
to enable these features on a capable server, improving flexibility for
heterogeneous deployments.
@gegles
Copy link
Contributor Author

gegles commented Feb 12, 2026

Thanks for the detailed feedback! I've addressed the remaining items:

Changes Made

1. --gsro Now Available Regardless of Local Support

  • Option available on all clients, even without HAVE_UDP_SEGMENT/HAVE_UDP_GRO
  • Allows clients without local support to request server-side GSO/GRO
  • GSO/GRO fields always defined in iperf_settings structure
  • Parameters always sent/received in JSON for full client-server negotiation

2. User Warnings Added

When --gsro is used on a client without local support, warnings inform the user:

  • "--gsro requested but UDP GSO/GRO not supported on this client; will only be enabled on server if supported"
  • Different messages for partial support (GSO-only or GRO-only)

3. Backward Compatibility Maintained

  • Server handles missing GSO/GRO parameters from older clients
  • Default derivation logic preserved
  • Existing behavior unchanged when --gsro not used

Build & Test Results

✅ All unit tests pass (5/5)
✅ No compilation errors
✅ Net -22 lines of code (removed #ifdef guards)

The implementation maximizes flexibility for heterogeneous deployments where client and server may have different GSO/GRO capabilities.

Remove HAVE_UDP_SEGMENT/HAVE_UDP_GRO guards around:
- GSO parameter initialization in iperf_parse_arguments
- Buffer sizing logic in iperf_new_stream

These fields are now always defined, allowing client/server negotiation
regardless of local support. Guards remain only around actual socket
operations.

Also restore MAP_SHARED for mmap (was accidentally changed to MAP_PRIVATE
during GSO/GRO merge in b56475e), fixing zerocopy functionality that was
broken since PR esnet#1949.
- Remove conditional guards around --gsro help text to match option
  availability (option is always available regardless of local support)
- Remove debug printf statements from net.c (net layer doesn't log to
  console; returns error codes silently per existing pattern)
- Remove duplicate iperf.h include in net.c

This ensures --gsro appears in help on all systems and eliminates
console spam from GSO/GRO error paths.
Replace compile-time feature guards in headers with stub implementations
that return errors when features are unavailable:

- Remove #ifdef guards from net.h function declarations
- Reorganize net.h to group Nread_gro with read functions and
  Nwrite_gso with write functions
- Add stub implementations in net.c and iperf_udp.c that return errors
  and set gso/gro flags to 0 when features not supported
- Remove guards around ALL function calls (including in iperf_udp_connect)
  to ensure stubs run on platforms without GSO/GRO support
- Keep guards only around setsockopt calls using platform constants

Also fix:
- Unused variable warning: remove cnt from iperf_udp_recv
- Warning ordering: show platform support warnings only after
  client-only check passes (prevents confusing output with -s --gsro)

This fixes the critical bug where --gsro on macOS caused zero traffic
because ifdef guards prevented stubs from running, leaving gso=1 and
triggering the unsupported GSO path.

Benefits:
- Cleaner API (no preprocessor clutter in headers)
- Runtime feature detection via stubs
- Code works correctly on all platforms
- Better code organization and error messages
@gegles
Copy link
Contributor Author

gegles commented Feb 12, 2026

@davidBar-On @swlars @bmah888, beyond the adjustments suggested above, I've now also gone in and cleaned things up as much as possible. I've now drastically reduced and isolated the use of the HAVE_UDP_* guards.

Overall the changeset is now quite tight and focused.

I've done as much testing as I could on Linux (with GSO/GRO) and on macOS (no GSO/GRO).

@bmah888 bmah888 changed the title Add UDP GSO/GRO support (Linux) and --no-gsro switch Add UDP GSO/GRO support (Linux) and --gsro switch Feb 13, 2026
@bmah888
Copy link
Contributor

bmah888 commented Feb 14, 2026

Thanks for all the revisions! I agree this code is a lot cleaner (and the diff is smaller).

I had a chance to do some testing with the latest version of the PR. No changes to suggest at this point.

Most testing was done between two very old bare-metal x86_64 hosts running Ubuntu 22.04.5 LTS, with Mellanox Connect-X 6 cards connected at 100GE through an L3VPN on a Nokia 7750 SR2S:

  • --gsro with both IPv4 and IPv6 working as expected
  • The combination of --gsro --reverse works as expected
  • Multiple parallel streams (tested through --parallel 4) works as expected
  • Slightly less packet loss through 10Gbps with --gsro
  • Reproduced the result that at very high -b values (such as -b30g), using --gsro allows the sender to send faster, so the receiver gets more packets and greater aggregate throughput. But this comes at the cost of a higher loss rate.
  • Increasing the buffer size (i.e. -w1m) can reduce the packet loss rate. This is generally true regardless of whether GSO/GRO are used or not.
  • Manual page looks good, thanks for adding that.

On some separate VMs:

  • Testing between FreeBSD 14.3 (no GSO/GRO) and Ubuntu 22.04.5 LTS (GSO/GRO enabled) shows they interoperate correctly in both directions.

@bmah888 bmah888 self-assigned this Feb 14, 2026
@gegles
Copy link
Contributor Author

gegles commented Feb 14, 2026

Thanks for all the revisions! I agree this code is a lot cleaner (and the diff is smaller).

I had a chance to do some testing with the latest version of the PR. No changes to suggest at this point.

Most testing was done between two very old bare-metal x86_64 hosts running Ubuntu 22.04.5 LTS, with Mellanox Connect-X 6 cards connected at 100GE through an L3VPN on a Nokia 7750 SR2S:

  • --gsro with both IPv4 and IPv6 working as expected
  • The combination of --gsro --reverse works as expected
  • Multiple parallel streams (tested through --parallel 4) works as expected
  • Slightly less packet loss through 10Gbps with --gsro
  • Reproduced the result that at very high -b values (such as -b30g), using --gsro allows the sender to send faster, so the receiver gets more packets and greater aggregate throughput. But this comes at the cost of a higher loss rate.
  • Increasing the buffer size (i.e. -w1m) can reduce the packet loss rate. This is generally true regardless of whether GSO/GRO are used or not.
  • Manual page looks good, thanks for adding that.

On some separate VMs:

  • Testing between FreeBSD 14.3 (no GSO/GRO) and Ubuntu 22.04.5 LTS (GSO/GRO enabled) shows they interoperate correctly in both directions.

This is great news and good feedback...

But do you confirm that you see massive throughput improvements when GSO/GRO is available (on both sides)?

As I showed above, I go from 2Gbps max to 35Gbps with no packet loss ..... and that's on pretty old machines as well...

Indeed, if one tries to go above that, then often the sender can overwhelm the receiver and thus generate packet loss...

@bmah888
Copy link
Contributor

bmah888 commented Feb 14, 2026

This is great news and good feedback...

But do you confirm that you see massive throughput improvements when GSO/GRO is available (on both sides)?

As I showed above, I go from 2Gbps max to 35Gbps with no packet loss ..... and that's on pretty old machines as well...

Indeed, if one tries to go above that, then often the sender can overwhelm the receiver and thus generate packet loss...

Just for reference my test machines have 1 CPU, type "Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz". Also, the hosts are tuned with some of the tunings from fasterdata.es.net, although we've probably spent more effort tuning TCP as opposed to UDP.

With --gsro I can see an improvement in transmit rate which leads to increased receive rate, but there's also an increased loss. If I try to reproduce your results, the maximum sending rate I have without --gsro is about 20Gbps, not 2Gbps.

netlab-pt7:iperf% src/iperf3 --udp --client fc00::192:168:128:3 -b35g
Connecting to host fc00::192:168:128:3, port 5201
[  5] local fc00::192:168:127:3 port 46550 connected to fc00::192:168:128:3 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  2.28 GBytes  19.6 Gbits/sec  274274
[  5]   1.00-2.00   sec  2.31 GBytes  19.9 Gbits/sec  278096
[  5]   2.00-3.00   sec  2.33 GBytes  20.0 Gbits/sec  279976
[  5]   3.00-4.00   sec  2.34 GBytes  20.1 Gbits/sec  281458
[  5]   4.00-5.00   sec  2.33 GBytes  20.0 Gbits/sec  280650
[  5]   5.00-6.00   sec  2.33 GBytes  20.0 Gbits/sec  280420
[  5]   6.00-7.00   sec  2.34 GBytes  20.1 Gbits/sec  281200
[  5]   7.00-8.00   sec  2.33 GBytes  20.0 Gbits/sec  280516
[  5]   8.00-9.00   sec  2.34 GBytes  20.1 Gbits/sec  280830
[  5]   9.00-10.00  sec  2.33 GBytes  20.0 Gbits/sec  280546
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  23.3 GBytes  20.0 Gbits/sec  0.000 ms  0/2797966 (0%)  sender
[  5]   0.00-10.00  sec  23.3 GBytes  20.0 Gbits/sec  0.002 ms  869/2797966 (0.031%)  receiver

iperf Done.
netlab-pt7:iperf% src/iperf3 --udp --client fc00::192:168:128:3 -b35g --gsro
Connecting to host fc00::192:168:128:3, port 5201
[  5] local fc00::192:168:127:3 port 59353 connected to fc00::192:168:128:3 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  4.08 GBytes  35.0 Gbits/sec  490518
[  5]   1.00-2.00   sec  4.07 GBytes  35.0 Gbits/sec  490042
[  5]   2.00-3.00   sec  4.07 GBytes  35.0 Gbits/sec  490007
[  5]   3.00-4.00   sec  4.07 GBytes  35.0 Gbits/sec  490042
[  5]   4.00-5.00   sec  4.07 GBytes  35.0 Gbits/sec  490021
[  5]   5.00-6.00   sec  4.07 GBytes  35.0 Gbits/sec  490049
[  5]   6.00-7.00   sec  4.07 GBytes  35.0 Gbits/sec  490021
[  5]   7.00-8.00   sec  4.07 GBytes  35.0 Gbits/sec  490028
[  5]   8.00-9.00   sec  4.07 GBytes  35.0 Gbits/sec  490042
[  5]   9.00-10.00  sec  4.07 GBytes  35.0 Gbits/sec  490021
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  40.7 GBytes  35.0 Gbits/sec  0.000 ms  0/4900791 (0%)  sender
[  5]   0.00-10.00  sec  38.8 GBytes  33.3 Gbits/sec  0.003 ms  233265/4900791 (4.8%)  receiver

iperf Done.

So I can see that --gsro makes some differences but the effect is a little different from what you're seeing.

@bmah888
Copy link
Contributor

bmah888 commented Feb 18, 2026

I think we're good to go. I did another test with --json with expected results. I'm going to merge this in the next couple minutes.

As a follow-up I'm thinking of doing a couple of follow-up changes later, which include:

  1. Writing the values of test->settings->gso and/or test->settings->gro to the JSON
  2. Writing the availability of GSO/GRO support to the --version output. Key off of defined(HAVE_UDP_GRO) and or defined(HAVE_UDP_SEGMENT).

Thanks for the PR and for your patience in working with us on this!

@bmah888 bmah888 merged commit 4ffe24d into esnet:master Feb 18, 2026
7 checks passed
@gegles gegles deleted the gsro branch February 18, 2026 19:54
@gegles
Copy link
Contributor Author

gegles commented Feb 18, 2026

Thanks @bmah888, @swlars and @davidBar-On!!!

Very excited that will be officially in iperf3 and for others to start playing with it!

@gegles
Copy link
Contributor Author

gegles commented Feb 18, 2026

@bmah888 any idea as to when the next release will happen? ;-)

@bmah888
Copy link
Contributor

bmah888 commented Feb 18, 2026

@bmah888 any idea as to when the next release will happen? ;-)

Probably April/May timeframe. We need to be better about publicizing our release cadence, but we generally plan for two feature releases a year (the other release this year would be approximately October/November).

@davidBar-On
Copy link
Contributor

@bmah888 regarding:

As a follow-up I'm thinking of doing a couple of follow-up changes later, which include:

I suggest to also add the following changes:

  1. in iperf_udp_send(), change if (sp->test->debug) to if (sp->test->debug_level >= DEBUG_LEVEL_DEBUG). Otherwise, the message will be printed for any debug level and will shadow debug messages when e.g. level is DEBUG_LEVEL_INFO.
  2. In iperf_udp_recv(), restore the comments that were replaced by /* Loss/out-of-order accounting - now always executed */. I think they are important for future maintenance - explain why the related functionality is as it is.

@bmah888
Copy link
Contributor

bmah888 commented Feb 19, 2026

@bmah888 regarding:

As a follow-up I'm thinking of doing a couple of follow-up changes later, which include:

I suggest to also add the following changes:

1. in `iperf_udp_send()`, change `if (sp->test->debug)` to `if (sp->test->debug_level >= DEBUG_LEVEL_DEBUG)`.  Otherwise, the message will be printed for any debug level and will shadow debug messages when e.g. level is `DEBUG_LEVEL_INFO`.

2. In `iperf_udp_recv()`, restore the comments that were replaced by `/* Loss/out-of-order accounting - now always executed */`.  I think they are important for future maintenance - explain why the related functionality is as it is.

Hey @davidBar-On these are both good points. Adding them to my list. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants