Skip to content

Conversation

@manedurphy
Copy link
Contributor

@manedurphy manedurphy commented Apr 19, 2024

PLEASE NOTE the following text from the iperf3 license. Submitting a
pull request to the iperf3 repository constitutes "[making]
Enhancements available...publicly":

You are under no obligation whatsoever to provide any bug fixes, patches, or
upgrades to the features, functionality or performance of the source code
("Enhancements") to anyone; however, if you choose to make your Enhancements
available either publicly, or directly to Lawrence Berkeley National
Laboratory, without imposing a separate written license agreement for such
Enhancements, then you hereby grant the following license: a non-exclusive,
royalty-free perpetual license to install, use, modify, prepare derivative
works, incorporate into other computer software, distribute, and sublicense
such enhancements or derivative works thereof, in binary and source code form.

The complete iperf3 license is available in the LICENSE file in the
top directory of the iperf3 source tree.

  • Version of iperf3 (or development branch, such as master or
    3.1-STABLE) to which this pull request applies: master

  • Issues fixed (if any): Enable '-t X' option in server mode #354, Server-side limiting switches #615

  • Brief description of code changes (suitable for use as a commit message):
    These changes add the functionality as requested by the linked issues. Specifically, this functionality allows the server to determine the maximum duration of an iperf measurement. The implementation is similar to the --server-bitrate-limit flag, where the measurement is forcibly stopped when an interval has exceeded the server's configured bitrate. In this case, the measurement is forcibly stopped when the duration exceeds the server's configured time, which is the value of --server-time.

Example

From the dropdowns below, we can see that the server has enforced a measurement time of 3 seconds. The client attempts to run the measurement for 4 seconds. When the 3rd second is exceeded, the socket is forcibly closed.

server
./src/iperf3 -s --server-time 3
-----------------------------------------------------------
Server listening on 5201 (test #1)
-----------------------------------------------------------
Accepted connection from 127.0.0.1, port 44938
[  6] local 127.0.0.1 port 5201 connected to 127.0.0.1 port 44946
[ ID] Interval           Transfer     Bitrate
[  6]   0.00-1.00   sec  3.08 GBytes  26.5 Gbits/sec                  (omitted)
[  6]   1.00-2.00   sec  3.35 GBytes  28.8 Gbits/sec                  (omitted)
[  6]   2.00-3.00   sec  3.18 GBytes  27.3 Gbits/sec                  (omitted)
[  6]   0.00-1.00   sec  3.20 GBytes  27.5 Gbits/sec
[  6]   1.00-2.00   sec  3.34 GBytes  28.7 Gbits/sec
iperf3: error - select failed: Bad file descriptor
-----------------------------------------------------------
Server listening on 5201 (test #2)
-----------------------------------------------------------
client
./src/iperf3 --client 127.0.0.1 --port 5201 -t 4 --omit 3
Connecting to host 127.0.0.1, port 5201
[  5] local 127.0.0.1 port 44946 connected to 127.0.0.1 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  3.08 GBytes  26.5 Gbits/sec    0   1.06 MBytes       (omitted)
[  5]   1.00-2.00   sec  3.35 GBytes  28.8 Gbits/sec    0   1.06 MBytes       (omitted)
[  5]   2.00-3.00   sec  3.15 GBytes  27.1 Gbits/sec    0   1.06 MBytes       (omitted)
[  5]   0.00-1.00   sec  3.20 GBytes  27.5 Gbits/sec    0   1.12 MBytes
[  5]   1.00-2.00   sec  3.34 GBytes  28.7 Gbits/sec    0   1.25 MBytes
[  5]   1.00-2.00   sec  3.34 GBytes  28.7 Gbits/sec    0   1.25 MBytes
iperf3: error - control socket has closed unexpectedly
client_json
{
        "start":        {
                "connected":    [{
                                "socket":       5,
                                "local_host":   "127.0.0.1",
                                "local_port":   53314,
                                "remote_host":  "127.0.0.1",
                                "remote_port":  5201
                        }],
                "version":      "iperf 3.16+",
                "system_info":  "Linux windows-nexus 5.15.146.1-microsoft-standard-WSL2 #1 SMP Thu Jan 11 04:09:03 UTC 2024 x86_64",
                "timestamp":    {
                        "time": "Thu, 18 Apr 2024 04:38:09 GMT",
                        "timesecs":     1713415089
                },
                "connecting_to":        {
                        "host": "127.0.0.1",
                        "port": 5201
                },
                "cookie":       "wtuhwwetnqayseccmqno6dutzjf3zly3op5p",
                "tcp_mss_default":      32768,
                "target_bitrate":       0,
                "fq_rate":      0,
                "sock_bufsize": 0,
                "sndbuf_actual":        16384,
                "rcvbuf_actual":        131072,
                "test_start":   {
                        "protocol":     "TCP",
                        "num_streams":  1,
                        "blksize":      131072,
                        "omit": 3,
                        "duration":     4,
                        "bytes":        0,
                        "blocks":       0,
                        "reverse":      0,
                        "tos":  0,
                        "target_bitrate":       0,
                        "bidir":        0,
                        "fqrate":       0,
                        "interval":     1
                }
        },
        "intervals":    [{
                        "streams":      [{
                                        "socket":       5,
                                        "start":        0,
                                        "end":  1.001066,
                                        "seconds":      1.0010659694671631,
                                        "bytes":        3578265600,
                                        "bits_per_second":      28595642717.968742,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  51,
                                        "rttvar":       5,
                                        "pmtu": 65535,
                                        "omitted":      true,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        0,
                                "end":  1.001066,
                                "seconds":      1.0010659694671631,
                                "bytes":        3578265600,
                                "bits_per_second":      28595642717.968742,
                                "retransmits":  0,
                                "omitted":      true,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        1.001066,
                                        "end":  2.001046,
                                        "seconds":      0.99997997283935547,
                                        "bytes":        3452698624,
                                        "bits_per_second":      27622142185.078888,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  88,
                                        "rttvar":       67,
                                        "pmtu": 65535,
                                        "omitted":      true,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        1.001066,
                                "end":  2.001046,
                                "seconds":      0.99997997283935547,
                                "bytes":        3452698624,
                                "bits_per_second":      27622142185.078888,
                                "retransmits":  0,
                                "omitted":      true,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        2.001046,
                                        "end":  3.001049,
                                        "seconds":      1.0000029802322388,
                                        "bytes":        3500015616,
                                        "bits_per_second":      28000041481.373692,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  63,
                                        "rttvar":       17,
                                        "pmtu": 65535,
                                        "omitted":      true,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        2.001046,
                                "end":  3.001049,
                                "seconds":      1.0000029802322388,
                                "bytes":        3500015616,
                                "bits_per_second":      28000041481.373692,
                                "retransmits":  0,
                                "omitted":      true,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        0.012641,
                                        "end":  0.988366,
                                        "seconds":      1.0010069608688354,
                                        "bytes":        3449421824,
                                        "bits_per_second":      27567615082.364941,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  58,
                                        "rttvar":       7,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        0.012641,
                                "end":  0.988366,
                                "seconds":      1.0010069608688354,
                                "bytes":        3449421824,
                                "bits_per_second":      27567615082.364941,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        0.988366,
                                        "end":  1.988398,
                                        "seconds":      1.0000319480895996,
                                        "bytes":        3414818816,
                                        "bits_per_second":      27317677780.382618,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  61,
                                        "rttvar":       9,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        0.988366,
                                "end":  1.988398,
                                "seconds":      1.0000319480895996,
                                "bytes":        3414818816,
                                "bits_per_second":      27317677780.382618,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        0.988366,
                                        "end":  1.988398,
                                        "seconds":      1.0000319480895996,
                                        "bytes":        3414818816,
                                        "bits_per_second":      27317677780.382618,
                                        "retransmits":  0,
                                        "snd_cwnd":     2029973,
                                        "snd_wnd":      3145088,
                                        "rtt":  61,
                                        "rttvar":       9,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        0.988366,
                                "end":  1.988398,
                                "seconds":      1.0000319480895996,
                                "bytes":        3414818816,
                                "bits_per_second":      27317677780.382618,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }],
        "end":  {
        },
        "error":        "control socket has closed unexpectedly"
}

Implementation Details

With this implementation, the server overrides the value of test->duration with the value that was passed in via the --server-time flag. We can see from the drop downs that the client's experience is similar to when the bitrate limit is exceeded with the error: iperf3: error - control socket has closed unexpectedly. However, the server side is less friendly, with an error of iperf3: error - select failed: Bad file descriptor. I can already envision myself encountering this error and not knowing why I am getting it, as well as forgetting that I had set the --server-time flag to begin with.

Another thing to note is that nothing is stopping the user from setting a high value for --omit. Given that this feature is meant to protect the server, is it reasonable to include a --server-omit option to enforce a maximum value?

Update (04/18/2024)

Since the first commit, I've introduced a solution for the error iperf3: error - select failed: Bad file descriptor which provides a more friendly experience. Let's consider the scenario where the --server-time flag has been set on the server to a value of 3:

  1. The client connects to the server
  2. The server accepts the connection, receives the client's cookie, and sends the client that value of 3 for the server time
  3. When the client's timers are created, it compares its duration to the server's duration of 3. If the client's duration is greater than the server's, then the server's value is selected for the timer.
  4. The same process occurs when the server's timers are created.

The JSON output also has a new field which specifies the server's value for duration: server_duration

client_json
{
        "start":        {
                "connected":    [{
                                "socket":       5,
                                "local_host":   "127.0.0.1",
                                "local_port":   36906,
                                "remote_host":  "127.0.0.1",
                                "remote_port":  5201
                        }],
                "version":      "iperf 3.16+",
                "system_info":  "Linux windows-nexus 5.15.146.1-microsoft-standard-WSL2 #1 SMP Thu Jan 11 04:09:03 UTC 2024 x86_64",
                "timestamp":    {
                        "time": "Mon, 22 Apr 2024 02:23:53 GMT",
                        "timesecs":     1713752633
                },
                "connecting_to":        {
                        "host": "127.0.0.1",
                        "port": 5201
                },
                "cookie":       "m4n5p26auofjvyda2x2ba56kexzsly7mz5k7",
                "tcp_mss_default":      32768,
                "target_bitrate":       5000000,
                "fq_rate":      0,
                "sock_bufsize": 0,
                "sndbuf_actual":        16384,
                "rcvbuf_actual":        131072,
                "test_start":   {
                        "protocol":     "TCP",
                        "num_streams":  1,
                        "blksize":      131072,
                        "omit": 0,
                        "duration":     6,
                        "server_duration":      3,
                        "bytes":        0,
                        "blocks":       0,
                        "reverse":      0,
                        "tos":  0,
                        "target_bitrate":       5000000,
                        "bidir":        0,
                        "fqrate":       0,
                        "interval":     1
                }
        },
        "intervals":    [{
                        "streams":      [{
                                        "socket":       5,
                                        "start":        0,
                                        "end":  1.000087,
                                        "seconds":      1.0000870227813721,
                                        "bytes":        655360,
                                        "bits_per_second":      5242423.78970069,
                                        "retransmits":  0,
                                        "snd_cwnd":     654830,
                                        "snd_wnd":      1375232,
                                        "rtt":  38,
                                        "rttvar":       24,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        0,
                                "end":  1.000087,
                                "seconds":      1.0000870227813721,
                                "bytes":        655360,
                                "bits_per_second":      5242423.78970069,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        1.000087,
                                        "end":  2.000084,
                                        "seconds":      0.99999701976776123,
                                        "bytes":        655360,
                                        "bits_per_second":      5242895.6250465661,
                                        "retransmits":  0,
                                        "snd_cwnd":     654830,
                                        "snd_wnd":      2684928,
                                        "rtt":  45,
                                        "rttvar":       22,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        1.000087,
                                "end":  2.000084,
                                "seconds":      0.99999701976776123,
                                "bytes":        655360,
                                "bits_per_second":      5242895.6250465661,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }, {
                        "streams":      [{
                                        "socket":       5,
                                        "start":        2.000084,
                                        "end":  3.000148,
                                        "seconds":      1.0000640153884888,
                                        "bytes":        655360,
                                        "bits_per_second":      5242544.39648379,
                                        "retransmits":  0,
                                        "snd_cwnd":     654830,
                                        "snd_wnd":      3112448,
                                        "rtt":  62,
                                        "rttvar":       44,
                                        "pmtu": 65535,
                                        "omitted":      false,
                                        "sender":       true
                                }],
                        "sum":  {
                                "start":        2.000084,
                                "end":  3.000148,
                                "seconds":      1.0000640153884888,
                                "bytes":        655360,
                                "bits_per_second":      5242544.39648379,
                                "retransmits":  0,
                                "omitted":      false,
                                "sender":       true
                        }
                }],
        "end":  {
                "streams":      [{
                                "sender":       {
                                        "socket":       5,
                                        "start":        0,
                                        "end":  3.000148,
                                        "seconds":      3.000148,
                                        "bytes":        1966080,
                                        "bits_per_second":      5242621.36401271,
                                        "retransmits":  0,
                                        "max_snd_cwnd": 654830,
                                        "max_snd_wnd":  3112448,
                                        "max_rtt":      62,
                                        "min_rtt":      38,
                                        "mean_rtt":     48,
                                        "sender":       true
                                },
                                "receiver":     {
                                        "socket":       5,
                                        "start":        0,
                                        "end":  3.000258,
                                        "seconds":      3.000148,
                                        "bytes":        1966080,
                                        "bits_per_second":      5242429.1510930061,
                                        "sender":       true
                                }
                        }],
                "sum_sent":     {
                        "start":        0,
                        "end":  3.000148,
                        "seconds":      3.000148,
                        "bytes":        1966080,
                        "bits_per_second":      5242621.36401271,
                        "retransmits":  0,
                        "sender":       true
                },
                "sum_received": {
                        "start":        0,
                        "end":  3.000258,
                        "seconds":      3.000258,
                        "bytes":        1966080,
                        "bits_per_second":      5242429.1510930061,
                        "sender":       true
                },
                "cpu_utilization_percent":      {
                        "host_total":   100.87966904530634,
                        "host_user":    100.8797023673657,
                        "host_system":  0,
                        "remote_total": 0.037496475331318856,
                        "remote_user":  0.037496475331318856,
                        "remote_system":        0
                },
                "sender_tcp_congestion":        "cubic",
                "receiver_tcp_congestion":      "cubic"
        }
}

Update (06/05/2024)

A valid question was brought up by @TheRealDJ: What happens when the server has this feature but the client doesn't?. With the current implementation, there is no backwards compatability. Further discussions will need to be had in order to handle this case properly.

Update (05/17/2025)

A new implementation has been made to address the backwards compatibility issue mentioned above.

Implementation Details

The server has a new flag, --server-max-duration, represents the maximum permitted duration for an iperf3 test in seconds. When the client initiates a test, it sends its desired duration to the server. The server then compares the client's requested duration with its own maximum duration. If the client's requested duration exceeds the server's maximum, the server rejects the test and sends an error message back to the client. Since older client's do not have the newly added IEMAXSERVERDURATIONEXCEEDED error, the handler for int_errno falls into the default case. The output for the server and both outputs for the client are shown below.

server
-----------------------------------------------------------
Server listening on 5201 (test #1)
-----------------------------------------------------------
iperf3: error - client's max duration exceeds the server's maximum permitted duration
client (new)
Connecting to host 127.0.0.1, port 5201
iperf3: error - client's max duration exceeds the server's maximum permitted duration
client (old)
Connecting to host 127.0.0.1, port 5201
iperf3: error - int_errno=37: No locks available

Questions

Is this output acceptable? The error message is not very user friendly, but I'm not sure of a way to relay more information when the error message is based on the int_errno value. Ideally, the server and the client should exchange information on their respective versions, but since that functionality does not currently exist, I am uncertain of the best way to handle this.

Update (05/19/2025)

@TheRealDJ mentioned that the sum of the client's duration and omits must not exceed the server's maximum duration. This change has been implemented.

@manedurphy manedurphy marked this pull request as draft April 19, 2024 00:16
@manedurphy manedurphy marked this pull request as ready for review April 19, 2024 00:27
@manedurphy
Copy link
Contributor Author

Update (05/22/2025)

@bmah888 expressed concern over the value for the added OPT_SERVER_MAX_DURATION, as it is approaching a value close to those of other short-hand flags. The way I see it, these should be okay to increment as is up until ASCII 47, as these values are unlikely to be used for flags. We can also increase it to 128 now and never think about it again. I'll leave that decision up to Bruce. My only concern left for the current implementation is the user-unfriendly message when the iperf3 client's version is older. Is this a known limitation that users are familiar enough with to know what to do when then encounter the message iperf3: error - int_errno=37: No locks available which provides the error code from the server that the client does not have a case for? And if this is a well-known limitation and we can merge this change as is, is there room for an enhancement in the future? The way that I would approach this is for the client and server to exchange their respective versions, and when the server encounters an error for a feature that the client doesn't have awareness of, it will send a special code which indicates that the error has to do with a feature on a newer version of iperf3..... or something like that.

@bmah888
Copy link
Contributor

bmah888 commented May 30, 2025

Thanks for the new version of the PR!

I'll have to play around with happens w.r.t. interoperability between different versions of software. Ideally yes the control protocol would allow the client and server to exchange version numbers or API versions and behave in some compatible manner accordingly. If I was going to redesign iperf3 that'd be a part of the control protocol, for sure. I'm haven't thought about what's the right thing to do in this situation.

@manedurphy
Copy link
Contributor Author

manedurphy commented Jul 3, 2025

@bmah888 have you had some time to consider the approach we should take here given the constraints? @davidBar-On your input on this would also be appreciated.

It appears that it has always been an issue that an iperf3 client could run into a case where it cannot provide more context for what happened on the server. Here is the output for a 3.8.1 client that is trying to exceed the duration of the --server-max-duration flag:

iperf3 -c 127.0.0.1 -t 10
Connecting to host 127.0.0.1, port 5201
iperf3: error -

So while a more sophisticated approach for providing more context to older versions of the client would be ideal, I'm not sure that it is absolutely necessary given the historical context. At least not necessary to move forward with this feature. An enhancement for this can be added to the code base separately.

@manedurphy manedurphy requested a review from davidBar-On July 3, 2025 17:56
@manedurphy manedurphy requested a review from davidBar-On July 4, 2025 17:48
@bmah888
Copy link
Contributor

bmah888 commented Jul 11, 2025

@bmah888 have you had some time to consider the approach we should take here given the constraints? @davidBar-On your input on this would also be appreciated.

It appears that it has always been an issue that an iperf3 client could run into a case where it cannot provide more context for what happened on the server. Here is the output for a 3.8.1 client that is trying to exceed the duration of the --server-max-duration flag:

iperf3 -c 127.0.0.1 -t 10
Connecting to host 127.0.0.1, port 5201
iperf3: error -

So while a more sophisticated approach for providing more context to older versions of the client would be ideal, I'm not sure that it is absolutely necessary given the historical context. At least not necessary to move forward with this feature. An enhancement for this can be added to the code base separately.

I agree with you here...iperf3 wasn't designed to support perfect forward/backward compatibility between arbitrary versions of code. It was intended for deployments within a single administrative domain (or at least between cooperating administrative domains), where the requirements for compatibility are lessened a bit. In this case all we're missing is the ability to display a meaningful error message to the end-user on the client side.

@bmah888
Copy link
Contributor

bmah888 commented Jul 11, 2025

I've been testing this and it seems to be working as designed.

One thing just occurred to me that if you run the server as iperf3 --server --server-max-duration, I can still force a really long test to run by doing something like iperf3 --client iperf3.example.com --bytes 1T or something like that (in other words, have my client specify a number of bytes or a number of blocks to transmit, both of which override the time duration). I'm not sure how to handle these cases, short of adding more limiting options.

@manedurphy
Copy link
Contributor Author

manedurphy commented Jul 14, 2025

@bmah888 I'm open to adding those protections. I think having a flag per protection is fine since the goal is to give the administrator of the iperf3 server more control over preventing a single client's ability to flood traffic. If I have a 10 Gbps server that is supposed to allow some pool of 30 devices to test their 1 Gbps speeds, then I can better ensure that no device is able to hog the server's bandwidth by:

  1. Setting --server-max-bitrate to 1Gb/1s.
  2. Setting --max-server-duration to 15 since that's plenty of time to verify speed. The client can set --omit 5 to account for TCP ramp up and --time 10 for the test duration.
  3. This would mean the theoretical max bytes transferred would be 1875000000 bytes, to which I could set --server-max-bytes to protect the server from any device that wants to run a longer test. This would mean that devices with slower bit rates could run for longer than the configured --server-max-duration, but would never be able to exceed the maximum configured number of bytes that the server will accept via the --server-max-bytes flag.

For me, personally, as an iperf3 server administrator, I care the most for my server's availability to meaningfully serve as many clients as possible, and preventing clients from dominating the server's resources. If someone wants to run a long iperf3 client with --bitrate 1Kb/1s --bytes 1875000000, that's fine with me because it won't interfere with other devices.

I'd want to know if these additional changes should be implemented as part of these changes, or if I should raise them in a separate PR. I'm open to either, but tend to prefer raising separate PRs for separation of concern for the PR template.

@manedurphy manedurphy requested a review from bmah888 July 14, 2025 21:11
@davidBar-On
Copy link
Contributor

davidBar-On commented Jul 15, 2025

... as an iperf3 server administrator, I care the most for my server's availability to meaningfully serve as many clients as possible, and preventing clients from dominating the server's resources.

I assume that means that the duration that a specific client is using the server is the most important limit. In this case, instead of limiting bytes/blocks, at the beginning of the test the server can start a timer that will expire after the --server-max-duration and will allow the server to terminate the test.

Actually, the server already does something similar, in create_server_timers(). Based on the test duration test->duration + test->omit + grace_period. If that timer expires, server_timer_proc() is called and it terminates the test (I have submitted PR #1914 to enhance this function error handling). The suggested approach is that when --server-max-duration is set, the timer will be set to the minimum between the parameter value and the value currently used for the timer. Something like this:

        max_duration = test->duration + test->omit + grace_period;
        if (test->max_server_duration > 0 && test->max_server_duration < max_duration) {
            max_duration = test->max_server_duration;
        }
        test->timer = tmr_create(&now, server_timer_proc, cd, max_duration * SEC_TO_US, 0);

@bmah888
Copy link
Contributor

bmah888 commented Aug 15, 2025

For me, personally, as an iperf3 server administrator, I care the most for my server's availability to meaningfully serve as many clients as possible, and preventing clients from dominating the server's resources. If someone wants to run a long iperf3 client with --bitrate 1Kb/1s --bytes 1875000000, that's fine with me because it won't interfere with other devices.

Thanks @manedurphy! Herein we're running into a difference in use cases between what iperf3 was originally designed for, and what people would (quite reasonably) like it to do. :-) iperf3 was originally designed to test between two specific hosts (i.e. client and server) at a time. In the networking environment it was designed for, it's very possible that the end hosts, rather than the network, are the bottlenecks to performance. So only one test per server process can be running at once (and ideally only one test per host at a time), to avoid interference between tests. There was also a general assumption that the client and server were loosely affiliated, or at least co-operating.

This is very different from people who want to run large-scale iperf3 speed-test type services that serve dozens or hundreds of potentially unknown clients, some of which might need some guardrails. I understand that the PR we're discussing is an attempt to adapt iperf3 to this use case, which is great. I'm just trying to poke (in a friendly way, or at least I'm trying!) at the solution to see what else might be needed to fit the requirements.

I appreciate wanting to support (server-side) limits on parameters as well, although I am not sure how these options should interact, particularly when the client side can only specify one of the ending criteria. So...my question back to you is do you think this PR is still useful as-is? I haven't quite made up my mind on this. (I see where @davidBar-On has some feedback also, which I haven't read yet.)

@manedurphy
Copy link
Contributor Author

@bmah888 I appreciate the additional context for iperf3's original design and use case. You are correct, this feature stems from a use case of large-scale speed testing. We have pools of 10Gb/s, 40Gb/s, and 100Gb/s servers that are assigned to physical locations for serving iperf3 clients that reside in those areas. We have 1 client process that interacts with 1 server process as iperf3 was designed to do, but we cannot have only 1 server process per host due to our scale. Depending on the location, it can be either tens or hundreds of iperf3 server processes on a single host, each of which interact with a single client process. I believe that this server-protecting feature falls in line with the --server-bitrate-max feature in iperf/#999, which I think has more usefulness when the client and server are not affiliated as does the proposed --server-max-duration.

@davidBar-On your proposed change of using the value of --server-max-duration to set the value of the timer actually falls more in line with the original implementation that @TheRealDJ and I discussed. However, I believe that the server should not allow the client to run at all when it can determine that it is destined to fail based on the client's input parameters. The same can be (conditionally) said for the --server-bitrate-max flag in the sense that if the server has this flag set to 1Gbit/1 and the client has --bitrate set to 2Gbit/1, then the server should reject the test because it knows that it will exceed its configured maximum, and it should provide a message to the client which makes it clear why the test was rejected. In the cases where the server cannot determine this in advance, such as when the --bitrate in the client is not set, then the server should ideally send a user-friendly message to the client. As of now, there is a nice message on the server, iperf3: error - total required bandwidth is larger than server limit, but the client has no visibility into what went wrong, iperf3: error - control socket has closed unexpectedly. I believe the enhancement in iperf/#1914 intends to do the same, where the server would print iperf3: error - server test duration expired - test is terminated by the server, but the client would still have no visibility into what went wrong. I'm bringing up the --server-bitrate-max here because I believe it's usefulness in protecting the server aligns with the purpose of the proposed --server-duration-max, and I believe that all server-protecting flags, current and future, should follow the same set of guidelines for implementation.

Guidelines:

  • When the server has a protection flag set, like --server-bitrate-limit, --server-max-duration, or --server-max-bytes, etc, it should be prepared to evaluate and react to the client's respective input parameters: --bitrate, --time, bytes, etc.
  • Should the server determine that the client's input parameters would effectively violate the protections it has set, then it should reject the test with a user-friendly message to the client which describes why the test was rejected:
    • client's requested bit rate exceeds the server's maximum permitted limit
    • client's requested duration exceeds the server's maximum permitted limit
    • client's requested bytes exceed the server's maximum permitted limit
  • Should the server not be able to determine by the client's parameters that the test will violate the server's protections, then the server should stop the test upon violation and send a user-friendly message to the client to explain why the test was stopped:
    • client's bit rate exceeded the server's maximum permitted limit
    • client's bytes exceeded the server's maximum permitted limit

Since it is always possible for the server to determine if the client's duration will violate it's protection, it will either reject or permit with success. For other protections such as bit rate or bytes, the server may not be able to determine if its protections will be violated, and will have to react accordingly.

@davidBar-On
Copy link
Contributor

@manedurphy, thanks for the detailed response. In general, I fully agree with what you wrote.

I believe that the server should not allow the client to run at all when it can determine that it is destined to fail based on the client's input parameters. The same can be (conditionally) said for the --server-bitrate-max flag ...

I agree that both checks should be done , both when receiving the client's parameters and during the test and that the parameters check should be added regarding --server-bitrate-max.

... then the server should ideally send a user-friendly message to the client. As of now, there is a nice message on the server, iperf3: error - total required bandwidth is larger than server limit, but the client has no visibility into what went wrong, iperf3: error - control socket has closed unexpectedly ...

I believe that I found a reasonable solution (actually a workaround) to this problem in PR #1909. Once this PR (or a variant of it) will be merged, the client will (also) print the correct error message. The idea is that if the server finds a problem with a parameter received from the client, it will send the error code as the next State to client (after making sure that no State and error code have the same value). When the client receive the "State", instead of just printing Received unknown state, it suggests that maybe this is an error code and prints Received unknown state <state code> from server, which may be the error: <the error string>.

@manedurphy
Copy link
Contributor Author

@davidBar-On I was thinking of following the same pattern to convey the message. I've pasted a snippet below, but I'll open a new PR which has both the error handling and input validation for the sake of keeping this PR focused on handling the time parameter.

snippet
// Check if average transfer rate was exceeded (condition set in the callback routines)
if (test->bitrate_limit_exceeded) {
    i_errno = IETOTALRATE;
    if (iperf_set_send_state(test, SERVER_ERROR) != 0) {
        cleanup_server(test);
        return -1;
    }

    err = htonl(i_errno);
    if (Nwrite(test->ctrl_sck, (char*) &err, sizeof(err), Ptcp) < 0) {
        cleanup_server(test);
        i_errno = IECTRLWRITE;
        return -1;
    }
    err = htonl(errno);
    if (Nwrite(test->ctrl_sck, (char*) &err, sizeof(err), Ptcp) < 0) {
        cleanup_server(test);
        i_errno = IECTRLWRITE;
        return -1;
    }

    cleanup_server(test);
    return -1;
}

My latest push here includes writing errno as per your requested change.

@bmah888
Copy link
Contributor

bmah888 commented Oct 3, 2025

@manedurphy thanks for your patience in dealing with feedback on this PR! I'm pretty happy with this in the state it's in, except there's a merge conflict with master caused by an unrelated PR. Would you be up for fixing that merge conflict on your branch? I can merge it then.

Also I noticed your PR #1931, you probably want that in as well before we do iperf-3.20, right?

@manedurphy manedurphy marked this pull request as draft October 3, 2025 20:55
@manedurphy manedurphy marked this pull request as ready for review October 3, 2025 21:19
@manedurphy
Copy link
Contributor Author

I think all of the PRs that were asked for 3.20 have already been merged, so this and #1931 can be merged whenever you feel they are ready. I've rebased both, but think I'll have to rebase again when one of them is merged.

Copy link
Contributor

@bmah888 bmah888 left a comment

Choose a reason for hiding this comment

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

Looks good, quick testing seems to do the right thing.

I'm intending to do a squash and merge on the whole thing since it's more or less hitting the master branch as a single body of work.

@bmah888 bmah888 merged commit e4503c8 into esnet:master Oct 3, 2025
7 checks passed
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.

3 participants