Skip to content

Support multiple STUN and TURN servers - #113

Open
Mohit-Ak wants to merge 1 commit into
aiortc:mainfrom
Mohit-Ak:multiple-stun-turn-servers
Open

Support multiple STUN and TURN servers#113
Mohit-Ak wants to merge 1 commit into
aiortc:mainfrom
Mohit-Ak:multiple-stun-turn-servers

Conversation

@Mohit-Ak

@Mohit-Ak Mohit-Ak commented Aug 5, 2026

Copy link
Copy Markdown

Closes #3.

Connection accepts a single STUN server and a single TURN server, spread over six scalar arguments (turn_server, turn_username, turn_password, turn_ssl, turn_transport). Applications with more than one server have no way to pass them. Downstream, aiortc drops everything after the first — connection_kwargs() in rtcicetransport.py has literal # only a single STUN server is supported / # only a single TURN server is supported comments and continues past the rest — so an RTCConfiguration listing a primary and a fallback TURN server silently uses only the primary.

What this does

Adds stun_servers and turn_servers, taking StunServer and TurnServer objects:

conn = Connection(
    ice_controlling=True,
    stun_servers=[StunServer(address=("stun1.example.org", 3478))],
    turn_servers=[
        TurnServer(address=("turn1.example.org", 3478), username="u", password="p"),
        TurnServer(address=("turn2.example.org", 5349), username="u", password="p",
                   ssl=True, transport="tcp"),
    ],
)

On the API shape — you noted in #3 that the turn_XXX parameters were proliferating and that multiple-server support "could actually clean this up a little if done right". Bundling the per-server settings into a dataclass is my attempt at that: adding a second TURN server doesn't mean six more arguments, and each server carries its own transport, TLS flag and credentials rather than sharing one global set. If you'd rather see plain tuples/dicts, or a different split between the two classes, I'm happy to rework it — the gathering side is independent of that choice.

get_component_candidates() now iterates both lists: each STUN server is queried from every IPv4 host protocol, and each TURN server gets its own allocation. These already ran concurrently through asyncio.wait(..., timeout=timeout), so one slow or broken server still can't hold up the others — that part needed no change. Server-reflexive candidates duplicating one already obtained are dropped, since several STUN servers normally observe the same public address; the discarded protocol is closed rather than leaked.

Backwards compatibility

The singular arguments keep working and are folded into the plural form internally. stun_server and turn_server remain readable as properties returning the first entry, so existing code that reads them is unaffected. Passing both forms of the same argument raises ValueError instead of silently ignoring one.

I checked this against the real downstream consumer rather than assuming: released aiortc 1.15.0, unmodified, still builds a Connection from its own singular connection_kwargs() output and completes an offer with candidates gathered against this branch.

Testing

  • 10 new tests; 114 pass, 0 fail (104 before this change).
  • Multi-server behaviour is verified against real servers, not mocks — the suite's own run_turn_server() is started twice to get genuinely distinct servers: two TURN servers yield two distinct relayed candidates; mixed UDP/TCP transports both allocate; a server with bad credentials doesn't stop a healthy one; a STUN server that fails DNS doesn't stop the other; STUN and TURN combine to give host/srflx/relay.
  • RED check: restricting the new gathering loops back to the first server (self.turn_servers[:1]) fails test_connect_with_multiple_turn_servers and ..._mixed_transports with AssertionError: 1 != 2, so the tests genuinely pin the new behaviour.
  • Deduplication is asserted directly: two STUN servers observing the same address produce one server-reflexive candidate per IPv4 host protocol, not one per server.
  • ruff check --diff, ruff format --diff and mypy examples src tests are all clean, matching the lint job.

Not included

The aiortc side is a separate change in a different repo and needs a released aioice to depend on, so I've left it out. Once this lands I'm happy to follow up there with the connection_kwargs() change that stops discarding the extra servers.

Connection accepted a single STUN server and a single TURN server, spread
over six scalar arguments. Applications with more than one server had no
way to pass them, and aiortc drops every server after the first when it
builds its kwargs, with a comment noting only one is supported.

Add `stun_servers` and `turn_servers`, taking StunServer and TurnServer
objects. Bundling the turn_* arguments into a dataclass also addresses the
concern raised in aiortc#3 about the proliferation of turn_XXX parameters: a
second TURN server no longer means six more arguments.

Candidate gathering now iterates over both lists, so each STUN server is
queried from every IPv4 host protocol and each TURN server gets its own
allocation. These already ran concurrently through asyncio.wait, so a slow
or broken server still cannot hold up the others. Server-reflexive
candidates that duplicate one already obtained are dropped, since several
STUN servers usually observe the same public address.

The singular arguments continue to work and are folded into the plural
form. stun_server and turn_server remain readable as properties returning
the first entry. Passing both the singular and plural form of the same
argument raises ValueError rather than silently ignoring one.
@Mohit-Ak
Mohit-Ak force-pushed the multiple-stun-turn-servers branch from 9aa9e8f to e37efe4 Compare August 5, 2026 03:32
@Mohit-Ak
Mohit-Ak marked this pull request as ready for review August 10, 2026 14:11
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.

support multiple stun and turn servers

1 participant