Skip to content

Conversation

@gegles
Copy link

@gegles gegles commented Aug 8, 2025

License

This code is distributed under the terms of the BSD license, as per the LICENSE file in the iperf3 source tree.

Summary

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

Description

Addresses #1831

This change adds first-class support for Linux UDP GSO (Generic Segmentation Offload) and GRO (Generic Receive Offload) in iperf3. At configure time we detect the availability of the socket options and wire them into the UDP data path; at runtime the feature is enabled by default when the kernel supports it, with an opt-out CLI switch.

Highlights

  • Build-time detection
    • Autoconf checks for <linux/udp.h> constants UDP_SEGMENT and UDP_GRO and defines HAVE_UDP_SEGMENT / HAVE_UDP_GRO accordingly.
  • Runtime control & settings
    • New iperf settings fields (in struct iperf_settings) to track GSO/GRO enablement and buffer/segment sizes (with sane maxima).
    • New CLI flag --no-gsro to disable both GSO and GRO when they would otherwise be on by default; help text is included in usage_longstr.
  • Client heuristics & guardrails
    • UDP block size selection logic is clarified; we also warn when the configured UDP block size exceeds the TCP control socket’s MSS (to help avoid excessive fragmentation / drops).

Why it matters

On modern NICs/kernels, GSO lets iperf3 hand larger UDP payloads to the kernel, which segments them efficiently, reducing CPU overhead on send; GRO coalesces received UDP segments, reducing per-packet processing on receive. Together they can materially increase achievable throughput and lower CPU utilization during high-rate UDP tests on capable Linux systems.

User-visible changes

  • New option: --no-gsro (disable default GSO/GRO enablement).
  • Help/manpage: long usage text includes the new switch.
  • Warnings: when UDP block size is bigger than the TCP MSS, users now get a clear warning.

Platform notes

  • Linux only (gated by presence of UDP_SEGMENT / UDP_GRO). Other platforms are unaffected.

Backwards compatibility

  • Safe by default: On systems without the kernel features, nothing changes. Where supported, GSO/GRO auto-enable, but can be disabled with --no-gsro.
  • No protocol changes to the control channel or JSON schema.

Testing done

  • Configure-time: verified correct detection toggles on a recent Linux kernel (checks define HAVE_UDP_SEGMENT/HAVE_UDP_GRO as expected).
  • Runtime smoke tests (UDP):
    • Baseline vs. --no-gsro comparison to confirm feature gating.
    • High-rate send/receive to validate improved CPU behavior and absence of regressions in throughput reporting.
    • Confirmed new help string is printed and that the MSS/UDP block size warning triggers when applicable.

On a 100Gbps NIC link between 2 test machines I am able to send and receive with no loss at around 24Gbps:

iperf3 -c rapid10-100g -u -b 24G
Connecting to host rapid10-100g, port 5201
GSO (1448)
GRO
[  5] local 192.168.100.9 port 52407 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  2.80 GBytes  24.0 Gbits/sec  2073825  
[  5]   1.00-2.00   sec  2.79 GBytes  24.0 Gbits/sec  2071935  
[  5]   2.00-3.00   sec  2.79 GBytes  24.0 Gbits/sec  2071800  
[  5]   3.00-4.00   sec  2.79 GBytes  24.0 Gbits/sec  2071845  
[  5]   4.00-5.00   sec  2.79 GBytes  24.0 Gbits/sec  2071800  
[  5]   5.00-6.00   sec  2.79 GBytes  24.0 Gbits/sec  2071845  
[  5]   6.00-7.00   sec  2.79 GBytes  24.0 Gbits/sec  2071800  
[  5]   7.00-8.00   sec  2.79 GBytes  24.0 Gbits/sec  2071800  
[  5]   8.00-9.00   sec  2.79 GBytes  24.0 Gbits/sec  2071845  
[  5]   9.00-10.00  sec  2.79 GBytes  24.0 Gbits/sec  2071845  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  27.9 GBytes  24.0 Gbits/sec  0.000 ms  0/20720340 (0%)  sender
[  5]   0.00-10.00  sec  27.9 GBytes  24.0 Gbits/sec  0.000 ms  0/20720340 (0%)  receiver

If I try to send at max speed, I can send at around 48Gbps, but still only receive at 24Gbps:

iperf3 -c rapid10-100g -u -b 100G
Connecting to host rapid10-100g, port 5201
GSO (1448)
GRO
[  5] local 192.168.100.9 port 39941 connected to 192.168.100.10 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec  5.59 GBytes  47.9 Gbits/sec  4142340  
[  5]   1.00-2.00   sec  5.58 GBytes  47.9 Gbits/sec  4138470  
[  5]   2.00-3.00   sec  5.58 GBytes  48.0 Gbits/sec  4140900  
[  5]   3.00-4.00   sec  5.59 GBytes  48.0 Gbits/sec  4142163  
[  5]   4.00-5.00   sec  5.59 GBytes  48.0 Gbits/sec  4141572  
[  5]   5.00-6.00   sec  5.62 GBytes  48.2 Gbits/sec  4163985  
[  5]   6.00-7.00   sec  5.63 GBytes  48.4 Gbits/sec  4175865  
[  5]   7.00-8.00   sec  5.63 GBytes  48.4 Gbits/sec  4175013  
[  5]   8.00-9.00   sec  5.64 GBytes  48.4 Gbits/sec  4180137  
[  5]   9.00-10.00  sec  5.64 GBytes  48.4 Gbits/sec  4179960  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  56.1 GBytes  48.2 Gbits/sec  0.000 ms  0/41580405 (0%)  sender
[  5]   0.00-10.00  sec  28.6 GBytes  24.5 Gbits/sec  0.000 ms  20370110/41565660 (49%)  receiver

Documentation & follow-ups

  • Manpage: add --no-gsro to src/iperf3.1 options list.
  • Release notes: suggest a short note under “Enhancements: UDP” for Linux GSO/GRO availability and the opt-out flag.

@gegles
Copy link
Author

gegles commented Aug 12, 2025

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

@gegles
Copy link
Author

gegles commented Aug 25, 2025

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

@gegles
Copy link
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!

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

test->settings->socket_bufsize = j_p->valueint;
if ((j_p = iperf_cJSON_GetObjectItemType(j, "len", cJSON_Number)) != NULL)
test->settings->blksize = j_p->valueint;
#ifdef HAVE_UDP_SEGMENT
Copy link
Contributor

Choose a reason for hiding this comment

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

Since get_parameters() is run by the Server and --no-gsro is a Client only parameter, then there should be no related logic here. Instead, all the related values should be sent in send_parameters() (by the Client), and get_parameters() will just get them and set the related variables.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for pointing this out. Addressed in e63ff17.
I moved the GSO/GRO policy decisions out of get_parameters() and into the client side:

  • The client now includes GSO/GRO intent and sizes in send_parameters(). The server’s get_parameters() just reads those values and doesn’t recompute from blksize.
  • Each endpoint independently attempts to enable the feature on its own sockets. If the local kernel rejects it (e.g., setsockopt(UDP_SEGMENT/UDP_GRO) fails), we log and disable that feature locally. This correctly handles the asymmetric case (sender uses GSO, receiver uses GRO) and
    kernel capability differences.
  • For compatibility with older clients that don’t send GSO fields, the server derives gso_dg_size from blksize as a fallback to preserve prior behavior. If the client sends explicit values (including --no-gsro sending gso=0/gro=0), the server honors them as-is.

This keeps --no-gsro a client-only knob, avoids server-side policy decisions, and still respects local kernel capabilities at runtime. Build and unit tests pass locally.

@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
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
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
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
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!

marcosfsch and others added 4 commits December 15, 2025 14:40
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
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!

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.

4 participants