On Windows, an iperf2 UDP server (-s -u) cannot deliver Server Report ACKs to any client after the first one disconnects. The server-side output shows "recvfrom failed: Connection reset by peer" and all subsequent clients receive "WARNING: did not receive ack of last datagram after 10 tries". This affects both daemon mode (-D) and foreground mode with persistent servers.
The root cause is a well-documented Windows platform behavior (Microsoft KB 263823): when a UDP socket sends a packet to a closed destination port, Windows delivers the resulting ICMP "Port Unreachable" as WSAECONNRESET (error 10054) on the next recvfrom() call. The standard fix — WSAIoctl(SIO_UDP_CONNRESET) — is applied by Go, Rust (tokio/mio), .NET, pjsip, and many other networking libraries, but is missing from iperf2.
-s -u (foreground), -s -u -D (daemon), all -P counts, all bandwidths# On Windows — start persistent UDP server
iperf.exe -s -u -p 5201 -D
# On Linux/macOS — run client twice
iperf -c <windows-ip> -u -p 5201 -t 3 -b 1M # Run 1: Server Report received ✓
iperf -c <windows-ip> -u -p 5201 -t 3 -b 1M # Run 2: WARNING: did not receive ack ✗
Both clients receive Server Reports with valid jitter/loss statistics.
WARNING: did not receive ack of last datagram after 10 triesrecvfrom failed: Connection reset by peerTested with 12 consecutive clients against a persistent daemon, varying parameters:
| Parameters | Server Reports received | WARNINGs |
|---|---|---|
| -P 4 -b 1M (3 runs) | 0 | 12 |
| -P 2 -b 2M (3 runs) | 0 | 6 |
| -P 1 -b 4M (3 runs) | 0 | 3 |
| -P 4 -b 500K (3 runs) | 0 | 12 |
100% ACK failure after the first client, independent of stream count and bandwidth.
WSAECONNRESET (10054) on the server's UDP socket — this does NOT happen on Linux/macOS, where ICMP errors on UDP sockets are silently ignoredrecvfrom() call (for client #2), the stale WSAECONNRESET is returned instead of dataIn include/util.h, the FATALUDPREADERR macro on Windows treats ALL errors except WSAEWOULDBLOCK as fatal:
// Windows — WSAECONNRESET (10054) is treated as fatal
#define FATALUDPREADERR(errno) (((errno = WSAGetLastError()) != WSAEWOULDBLOCK))
// Unix — correctly excludes transient errors (EAGAIN, EWOULDBLOCK, EINTR)
#define FATALUDPREADERR(errno) ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINTR))
When WSAECONNRESET hits, Server.cpp sets peerclose = true and the server thread exits. The socket is permanently poisoned.
Microsoft documents this behavior in KB 263823 and provides SIO_UDP_CONNRESET (available since Windows XP) to disable it:
BOOL bNewBehavior = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(sock, SIO_UDP_CONNRESET, &bNewBehavior, sizeof(bNewBehavior),
NULL, 0, &dwBytesReturned, NULL, NULL);
| Project | Fix |
|---|---|
Go net package |
WSAIoctl(SIO_UDP_CONNRESET) on all UDP sockets (golang/go#5834) |
Rust mio / tokio |
WSAIoctl(SIO_UDP_CONNRESET) (tokio#2017) |
| .NET runtime | Applied by default on all UDP sockets |
| pjsip | WSAIoctl(SIO_UDP_CONNRESET) (pjsip#1197) |
| Dart/Flutter | WSAIoctl(SIO_UDP_CONNRESET) (flutter#155823) |
Two complementary changes:
Add WSAIoctl(SIO_UDP_CONNRESET) after UDP socket creation in Listener.cpp and Client.cpp.
include/headers.h — add <mstcpip.h> or define the constant:
#ifdef WIN32
// ... existing includes ...
#ifndef SIO_UDP_CONNRESET
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#endif
#endif
src/Listener.cpp — after mSettings->mSock = ListenSocket; (before SetSocketOptions):
#ifdef WIN32
if (isUDP(mSettings)) {
// Disable Windows WSAECONNRESET on UDP sockets (KB 263823).
// Without this, ICMP Port Unreachable from a disconnected client
// poisons the socket, causing all subsequent recvfrom() calls to
// fail with WSAECONNRESET (error 10054).
BOOL bNewBehavior = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(ListenSocket, SIO_UDP_CONNRESET, &bNewBehavior,
sizeof(bNewBehavior), NULL, 0, &dwBytesReturned, NULL, NULL);
}
#endif
src/Client.cpp — same pattern after mSettings->mSock = mySocket;.
include/util.h — update the Windows macro:
// Before:
#define FATALUDPREADERR(errno) (((errno = WSAGetLastError()) != WSAEWOULDBLOCK))
// After:
#define FATALUDPREADERR(errno) (((errno = WSAGetLastError()) != WSAEWOULDBLOCK) && (errno != WSAECONNRESET))
This ensures that even if SIO_UDP_CONNRESET is not applied (e.g., older Windows SDK), the server thread survives the transient error.
SIO_UDP_CONNRESET only suppresses spurious ICMP-triggered errors that are silently ignored on all other platforms