Skip to content

Add optional port_range to restrict host candidate local ports - #112

Open
junkerderprovinz wants to merge 2 commits into
aiortc:mainfrom
junkerderprovinz:feat/host-candidate-port-range
Open

Add optional port_range to restrict host candidate local ports#112
junkerderprovinz wants to merge 2 commits into
aiortc:mainfrom
junkerderprovinz:feat/host-candidate-port-range

Conversation

@junkerderprovinz

Copy link
Copy Markdown

This adds an optional port_range keyword argument to Connection, so callers can pin the local UDP ports used for host candidates to a fixed inclusive (min_port, max_port) range instead of letting the OS pick an ephemeral port.

This is the feature requested in #47 and supersedes the now stale and conflicting #63, which I used as a starting point. It is rebased on current main and addresses the review feedback from that thread.

Use cases (from #47 and #63):

  • Setting up static port forwarding on a NAT (or via UPnP) so the peer stays reachable without relying solely on STUN.
  • Publishing a fixed, known set of ports in advance, for example when running inside a container.

Behavior:

  • When port_range is None (the default), an ephemeral port is used exactly as before, so existing callers are unaffected.
  • When port_range is set, each host candidate socket is bound by trying the ports in the range in ascending order until a free one is found. If the whole range is exhausted the bind fails like any other host bind failure, so that address simply yields no host candidate (this reuses the existing OSError handling in get_component_candidates).
  • The range is validated at construction time; an invalid range raises ValueError.
  • Only host candidate binding is affected. STUN and TURN local ports and the mDNS port (5353) are left untouched.

Addressing the review comments on #63:

  • "Many calls to create_datagram_endpoint need wrapping": only the host candidate bind in get_component_candidates uses an ephemeral local port. mDNS binds explicitly to 5353, and the STUN and TURN paths do not set a local address, so the change is confined to a single bind site. The server reflexive candidate reuses the host socket, so it inherits the restricted port automatically.
  • "Docstring should be explicit that these are local ports": the docstring now states the range applies to the local UDP ports of host candidates and does not affect STUN, TURN or mDNS.
  • "Need an explicit test plus a resource exhaustion test": both are included, see below.

API note: the 2022 review suggested ephemeral_ports: Optional[Iterable[int]]. I went with a port_range two-tuple because the concrete use cases (static NAT forwarding, container port publishing) are naturally contiguous ranges, it is trivial to validate, and it matches the port range option found in comparable ICE stacks. The internal binding loop does not depend on the shape, so if you prefer the Iterable[int] form I am happy to switch.

Tests (tests/test_ice.py):

  • test_invalid_port_range: an out of bounds or inverted range raises ValueError.
  • test_gather_candidates_port_range: every gathered host candidate is bound within the configured range.
  • test_gather_candidates_port_range_exhausted: when the range is fully occupied, no host candidate is produced. The tests mock host address discovery to stay on loopback and remain hermetic.

Add an optional `port_range` keyword argument to `Connection`, allowing
callers to restrict the local UDP ports used for host candidates to an
inclusive `(min_port, max_port)` range. This is useful when the local
ports must be known in advance, for example to set up static port
forwarding on a NAT or to publish a fixed set of ports for a container.

When `port_range` is set, each host candidate socket is bound by trying
ports in the range in ascending order until a free one is found; if the
whole range is exhausted an `OSError` is raised (and handled like any
other bind failure). When `port_range` is `None`, an ephemeral port is
used exactly as before.

The range is validated at construction time and a `ValueError` is raised
for invalid input. Only host candidate binding is affected; STUN, TURN
and mDNS local ports are left untouched.

Fixes aiortc#47.
@PMohanJ

PMohanJ commented Jul 29, 2026

Copy link
Copy Markdown

The min_port should be >= 1024 as 0-1023 are system reserved ports. Either increase the lower bound to 1024 with a warning log or reject the provided port range; I'd prefer the latter.

Also I don't think this is going to be merged anytime soon since there's already a PR with similar changes; let's wait for few days, hopefully someone would come in for rescue.

Per review: raise the port_range lower bound from 1 to 1024 so the
system-reserved ports (0-1023) cannot be requested. Invalid ranges are
rejected with a ValueError rather than clamped. Docstring and tests updated.
@junkerderprovinz

Copy link
Copy Markdown
Author

Good call, done. The range now has to be within 1024-65535, and an out-of-range or inverted range is rejected with a ValueError rather than clamped, as you preferred. Docstring and the tests are updated to match; ruff, mypy and the tests are green.

No rush on the merge, understood. I'll leave it here for whenever it fits, and if the other PR lands first that's fine too.

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.

2 participants