Skip to content

udp: fix server reply address for multi-address interfaces - #2077

Open
Aniketnegi03 wants to merge 1 commit into
esnet:masterfrom
Aniketnegi03:udp-bind-server-to-client-address
Open

Aniketnegi03 wants to merge 1 commit into
esnet:masterfrom
Aniketnegi03:udp-bind-server-to-client-address

Conversation

@Aniketnegi03

@Aniketnegi03 Aniketnegi03 commented Sep 13, 2026 •

Copy link
Copy Markdown

A UDP test against a server that has more than one address (IPv4 or IPv6) on the outgoing interface fails: the server replies from a different address than the one the client sent its datagrams to, the client's connected socket discards those replies, and the client eventually dies with

_**_iperf3: error - unable to read from stream socket:
        Resource temporarily unavailable_**_

The server's UDP socket is created by netannounce() with no local address unless -B was given, so it is bound to the wildcard address and has no source address of its own. iperf_udp_accept() then connect(2)s it to the client, at which point the kernel has to choose a source address and does so with a route lookup towards the client. That yields the interface's primary address, which need not be the address the client used. TCP has been unaffected because accept(2) inherits the full address pair of the connection.

The client uses the same server address for the control connection and for the data streams, so the local address of the control connection is the address the client expects replies from. Bind the UDP socket to it and leave the kernel nothing to guess. If that address cannot be determined or cannot be bound, fall back to the previous wildcard bind.

As an effect the server now also reports the correct local address for UDP streams in its human-readable and JSON output.

Fixes #2059

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): #2059

* Brief description of code changes (suitable for use as a commit message):     
  Introduce `iperf_udp_announce()` which binds the server-side UDP data         
  socket to the local address of the control connection rather than the         
  wildcard address, ensuring the kernel uses the correct source address         
  when connect(2)ing to the client.

A UDP test against a server that has more than one address on the
outgoing interface fails: the server replies from a different address
than the one the client sent its datagrams to, the client's connected
socket discards those replies, and the client eventually dies with

    iperf3: error - unable to read from stream socket:
            Resource temporarily unavailable

The server's UDP socket is created by netannounce() with no local
address unless -B was given, so it is bound to the wildcard address and
has no source address of its own.  iperf_udp_accept() then connect(2)s
it to the client, at which point the kernel has to choose a source
address and does so with a route lookup towards the client.  That yields
the interface's primary address, which need not be the address the
client used.  TCP is unaffected because accept(2) inherits the full
address pair of the connection.

The client uses the same server address for the control connection and
for the data streams, so the local address of the control connection is
the address the client expects replies from.  Bind the UDP socket to it
and leave the kernel nothing to guess.  If that address cannot be
determined or cannot be bound, fall back to the previous wildcard bind,
so the change can only ever restore the old behaviour, never do worse.

As a side effect the server now also reports the correct local address
for UDP streams in its human-readable and JSON output.

Fixes esnet#2059
@Aniketnegi03
Aniketnegi03 force-pushed the udp-bind-server-to-client-address branch from 0cb6523 to 4319beb Compare September 13, 2026 19:05
@bmah888

bmah888 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR!

@davidBar-On

Copy link
Copy Markdown
Contributor

@Aniketnegi03 I am not from the iperf3 maintenance team and I did not try running the PR code. However, I am not sure I understand the fix, and I will appreciate it if you can briefly explain it.

What I don't understand is that the main change is done for Create a new "listening" socket to replace the one we were using before.. I.e. for creating the new listener socket for the next stream. However, I thought that the problem is in connect(). That is, that the connect() is done to different server IP address than the address the "connect message" was received from. Isn't this the case? Thanks.

@Aniketnegi03

Aniketnegi03 commented Sep 15, 2026 •

Copy link
Copy Markdown
Author

@davidBar-On, thanks for taking the time to review - let me explain in brief.

You're right that connect() is where the wrong source address gets chosen. Might you already know the reason underneath it:

> /* __ip4_datagram_connect(), net/ipv4/datagram.c */
> if (!inet->inet_saddr) inet->inet_saddr = fl4->saddr;  /* -> 172.16.0.101 */

So when the socket has no source address of its own, the kernel fills one in from a route lookup toward the client. That lookup returns the route's preferred source - the interface's primary address - which need not be the address the client actually contacted (or on which the connect message was received). The problem isn't that the right address is unavailable; it's that we let the route table choose instead of replying from the address the connect message arrived on.

Regarding "Create a new "Listening" socket to replace the one we were using before" - I think you read that as the patch only fixing the next stream's socket. It isn't: the socket serving the current stream was already bound with the local address by iperf_udp_listen(), when it was first set up to listen.

The ordering makes this clearer. After the TCP handshake and parameter exchange (get_parameters()), the server calls protocol->listen() -> iperf_udp_listen(), which creates and binds the UDP socket - and only then does it send CREATE_STREAMS to the client, which is what prompts the client to send its UDP connect message. So the socket is bound before the probe can arrive:

Just an example to help get the context:

Server has primary 192.168.1.101 and secondary 192.168.1.102
Client is 192.1681.1 and contacts the iperf server at secondary, 192.168.1.102

--- issue case ---                      --- after fix ---
socket()                      = 5       socket()                       = 5
bind(5, "::")                 = 0       bind(5, "::ffff:192.168.1.102") = 0
recvfrom(5, <CONNECT_MSG>)    = 4       recvfrom(5, <CONNECT_MSG>)     = 4
connect(5, ...192.168.1.1)     = 0       connect(5, ...192.168.1.1)      = 0
  ^ kernel picks 192.168.1.101             ^ source already set, nothing to pick

This is also why the fix can't go in connect() itself: there is no source argument (connect(fd, dst, len)), and it can't be corrected afterwards either - netannounce() has already bound the socket to the wildcard. The source has to be right before connect() runs, i.e. at socket creation.

Once that socket is promoted to stream 1's data socket, a connected UDP socket only receives from its peer, so it can no longer hear the next client - hence the new listening socket in iperf_udp_accept(), which needs the same binding and so calls iperf_udp_announce() too. That covers streams 2..N.
In a single-stream test that second socket is created and then closed unused, so the hunk we have discussed does nothing there - incase of single-stream iperf_udp_listen() change is helping only.


This branch has not been deployed

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

iperf 3.21: UDP server with multiple IP aliases may send replies from unexpected addresses

3 participants