Skip to content

test: rework unused_port_tcp_udp to try random candidate ports#5427

Draft
Watson1978 wants to merge 1 commit into
fluent:masterfrom
Watson1978:fix-unused-port-tcp-udp
Draft

test: rework unused_port_tcp_udp to try random candidate ports#5427
Watson1978 wants to merge 1 commit into
fluent:masterfrom
Watson1978:fix-unused-port-tcp-udp

Conversation

@Watson1978

@Watson1978 Watson1978 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Which issue(s) this PR fixes:
Fixes #

What this PR does / why we need it:
unused_port_tcp_udp() collected 200 TCP ephemeral ports at once and returned the first one that could also be bound for UDP.
On Windows, Hyper-V/WinNAT/HNS reserve "excluded port ranges" inside the ephemeral port range (49152-65535), the reservations are managed separately for TCP and UDP, and binding a port inside such a range fails with EACCES instead of EADDRINUSE.
The TCP ephemeral allocator assigns ports sequentially and knows nothing about UDP exclusions, so the 200 candidates form a contiguous block that can land entirely inside a UDP-only excluded range. When that happens, every candidate fails the UDP bind check and test setup fails with:

  RuntimeError: can't find unused port
  test/helper.rb:94:in `unused_port_tcp_udp'
  test/helper.rb:81:in `unused_port'
  test/plugin/test_in_forward.rb:22:in `setup'

This was captured on a windows-2025 GitHub Actions runner whose UDP exclusion ranges (netsh interface ipv4 show excludedportrange protocol=udp) were 54315-54414 and 54415-54514: exactly 200 consecutive ports, while the TCP exclusions were elsewhere (49691-49890).

Instead, draw random candidate ports from a fixed high range and verify that each can be bound for both TCP and UDP, trying another candidate on failure. Because the candidates are independent draws, reserved or in-use ports only matter through their overall fraction of the range: contiguous reservation blocks cannot defeat the search, and the chance of exhausting the retry limit is negligible. The range is kept wide (10001 ports) so that reservation blocks, which come in units of about 100-200 ports, can only ever cover a small fraction of it.

This also removes the need to hold 200 sockets at once, which the old implementation capped because of file descriptor limits, and the checks now bind both protocols explicitly on the same IPv4 wildcard address.

Docs Changes:
N/A

Release Note:
N/A

@Watson1978 Watson1978 force-pushed the fix-unused-port-tcp-udp branch from 0910773 to 1cc8953 Compare July 12, 2026 10:41
unused_port_tcp_udp() collected 200 TCP ephemeral ports at once and
returned the first one that could also be bound for UDP. On Windows,
Hyper-V/WinNAT/HNS reserve "excluded port ranges" inside the ephemeral
port range (49152-65535), the reservations are managed separately for
TCP and UDP, and binding a port inside such a range fails with EACCES
instead of EADDRINUSE. The TCP ephemeral allocator assigns ports
sequentially and knows nothing about UDP exclusions, so the 200
candidates form a contiguous block that can land entirely inside a
UDP-only excluded range. When that happens, every candidate fails the
UDP bind check and test setup fails with:

  RuntimeError: can't find unused port
  test/helper.rb:94:in `unused_port_tcp_udp'
  test/helper.rb:81:in `unused_port'
  test/plugin/test_in_forward.rb:22:in `setup'

This was captured on a windows-2025 GitHub Actions runner whose UDP
exclusion ranges (netsh interface ipv4 show excludedportrange
protocol=udp) were 54315-54414 and 54415-54514: exactly 200
consecutive ports, while the TCP exclusions were elsewhere
(49691-49890).

Instead, draw random candidate ports from a fixed high range and
verify that each can be bound for both TCP and UDP, trying another
candidate on failure. Because the candidates are independent draws,
reserved or in-use ports only matter through their overall fraction
of the range: contiguous reservation blocks cannot defeat the search,
and the chance of exhausting the retry limit is negligible. The range
is kept wide (10001 ports) so that reservation blocks, which come in
units of about 100-200 ports, can only ever cover a small fraction of
it.

This also removes the need to hold 200 sockets at once, which the old
implementation capped because of file descriptor limits, and the
checks now bind both protocols explicitly on the same IPv4 wildcard
address.

Only the protocol: :all path changes; unused_port_tcp() and
unused_port_udp() used by the :tcp/:tls/:udp paths are unchanged.

Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
@Watson1978 Watson1978 force-pushed the fix-unused-port-tcp-udp branch from 1cc8953 to ff8d8b5 Compare July 12, 2026 14:05
@Watson1978 Watson1978 changed the title test: rework unused_port_tcp_udp to probe one UDP-proposed port at a time test: rework unused_port_tcp_udp to try random candidate ports Jul 12, 2026
@Watson1978 Watson1978 added CI Test/CI issues backport to v1.19 We will backport this fix to the LTS branch labels Jul 12, 2026
@Watson1978 Watson1978 added this to the v1.20.0 milestone Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport to v1.19 We will backport this fix to the LTS branch CI Test/CI issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant