Open
Conversation
Before closing a socket, walk every relay candidate and gathering
transaction bound to it, invoke ExTURN.Client.close/1, and ship the
returned Refresh(lifetime=0) datagram on the original socket. Without
this, a TURN server (notably Cloudflare Realtime TURN) keeps the
5-tuple allocated until TTL expires; a future Allocate from the same
source port is then rejected with 437 Allocation Mismatch (RFC 5766
§6.2, RFC 8656 §6.2), gathering completes with no typ relay candidate,
and ICE fails.
Make the teardown run on abrupt parent death too. PeerConnection in
ex_webrtc 0.16 does not trap exits; if its DTLSTransport child crashes
(e.g. unifex_parse_arg when DTLS never negotiated), the linked cascade
kills ICE before PeerConnection can call ice_transport.close. Trap
exits in ICEAgent's init and propagate non-:normal EXITs as {:stop,
reason, state} so terminate/2 always runs the close path. :normal
EXITs (from short-lived children like gatherer worker processes) stay
noreply.
Transport.Mock in test support keeps closed sockets in the ETS table
with state: :closed so tests can assert what the agent sent on the
close path; setup_socket / open_ephemeral transparently reuse the slot
on re-open.
Depends on the matching ExTURN.Client.close/1 addition; pinned to that
commit via git dep until an ex_turn release ships.
Verified end-to-end against Cloudflare Realtime TURN via
ex_turn_cloudflare_repro: 20/20 iterations emit typ relay with zero
437s on narrow port-range cycling (was 0/20 without the fix, 437 on
iteration 1).
The handle_info({:EXIT, _, reason}, state) clause was uncovered: gen_server
intercepts EXITs from the parent and runs terminate/2 directly, so the
existing parent-death test never reached the clause. Add a test that links
a non-parent process to the agent and lets it exit abnormally, which is
the only path that actually drives the {:stop, reason, state} return.
Drop the case fallback in terminate/2; init/1 always returns a state map
with :ice_agent, so the _ -> :ok branch was unreachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ExTURN.Client.close/1 emits no logs and the transport's send/4 returns
{:error, _} silently, so a failed Refresh leaves no breadcrumb — exactly
the failure mode that triggers 437 Allocation Mismatch on the next port
reuse. Surface the error at warning level instead of swallowing it.
Add a Transport.Mock.fail_send/2 hook so the test can drive a real
allocation to :allocated, force the close-path send to return :enotconn,
and assert on the captured log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
|
Rationale behind my changes to your branch:
Comments are welcome:) |
joaothallis
approved these changes
May 5, 2026
Karolk99
approved these changes
May 7, 2026
Comment on lines
+2489
to
+2496
| defp release_turn_allocation(ice_agent, socket, client) do | ||
| with {:send, turn_addr, data, _client} <- ExTURN.Client.close(client) do | ||
| case ice_agent.transport_module.send(socket, turn_addr, data) do | ||
| :ok -> :ok | ||
| {:error, reason} -> Logger.debug("Couldn't send deallocate request, reason: #{reason}") | ||
| end | ||
| end | ||
| end |
Contributor
There was a problem hiding this comment.
NITPICK
It looks nice (if ExTURN.close/1 returns {:ok, state} it automatically returns), but on the other hand it would be better to always return the same value.
Suggested change
| defp release_turn_allocation(ice_agent, socket, client) do | |
| with {:send, turn_addr, data, _client} <- ExTURN.Client.close(client) do | |
| case ice_agent.transport_module.send(socket, turn_addr, data) do | |
| :ok -> :ok | |
| {:error, reason} -> Logger.debug("Couldn't send deallocate request, reason: #{reason}") | |
| end | |
| end | |
| end | |
| defp release_turn_allocation(ice_agent, socket, client) do | |
| with {:send, turn_addr, data, _client} <- ExTURN.Client.close(client) do | |
| :ok <- ice_agent.transport_module.send(socket, turn_addr, data) do | |
| :ok | |
| else | |
| {:ok, _state} -> :ok | |
| {:error, reason} -> Logger.debug("Couldn't send deallocate request, reason: #{reason}") | |
| end | |
| end | |
| end |
| with {:send, turn_addr, data, _client} <- ExTURN.Client.close(client) do | ||
| case ice_agent.transport_module.send(socket, turn_addr, data) do | ||
| :ok -> :ok | ||
| {:error, reason} -> Logger.debug("Couldn't send deallocate request, reason: #{reason}") |
Contributor
There was a problem hiding this comment.
If that can cause problems for the user, maybe info would be better.
Karolk99
reviewed
May 7, 2026
Contributor
Karolk99
left a comment
There was a problem hiding this comment.
We should look more closely at whether we need to close TURN allocations in cases when handle_terminate won't be called (when PeerConnection is closed with a different reason than :normal)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Use
ExTURN.Client.close/1to sendRefresh(lifetime=0)and delete the present allocation.Resolves #100
ref: elixir-webrtc/ex_turn#10