Skip to content

Commit 0cbfc51

Browse files
committed
Handle DTLS close_notify alert
1 parent 598dc4e commit 0cbfc51

File tree

12 files changed

+402
-66
lines changed

12 files changed

+402
-66
lines changed

examples/whip_whep/lib/whip_whep/forwarder.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ defmodule WhipWhep.Forwarder do
9494
@impl true
9595
def handle_info({:ex_webrtc, pc, {:connection_state_change, :failed}}, state) do
9696
Logger.info("Output peer connection (#{inspect(pc)}) state change: failed. Removing.")
97-
:ok = PeerConnection.close(pc)
97+
:ok = PeerConnection.stop(pc)
9898
pending_outputs = List.delete(state.pending_outputs, pc)
9999
outputs = Map.delete(state.outputs, pc)
100100
state = %{state | pending_outputs: pending_outputs, outputs: outputs}

lib/ex_webrtc/dtls_transport.ex

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ defmodule ExWebRTC.DTLSTransport do
101101
GenServer.cast(dtls_transport, {:set_packet_loss, packet_loss})
102102
end
103103

104+
@spec close(dtls_transport()) :: :ok
105+
def close(dtls_transport) do
106+
GenServer.call(dtls_transport, :close)
107+
end
108+
104109
@spec stop(dtls_transport()) :: :ok
105110
def stop(dtls_transport) do
106111
GenServer.stop(dtls_transport)
@@ -146,7 +151,7 @@ defmodule ExWebRTC.DTLSTransport do
146151
state = %{state | ice_connected: true}
147152

148153
if state.mode == :active do
149-
{packets, timeout} = ExDTLS.do_handshake(state.dtls)
154+
{:ok, packets, timeout} = ExDTLS.do_handshake(state.dtls)
150155
Process.send_after(self(), :dtls_timeout, timeout)
151156
:ok = do_send(state, packets)
152157
state = update_dtls_state(state, :connecting)
@@ -158,7 +163,7 @@ defmodule ExWebRTC.DTLSTransport do
158163
end
159164

160165
@impl true
161-
def handle_call(:set_ice_connected, _from, state) do
166+
def handle_call(:set_ice_connected, _from, %{dtls_state: :connecting} = state) do
162167
state = %{state | ice_connected: true}
163168

164169
if state.buffered_local_packets do
@@ -171,6 +176,17 @@ defmodule ExWebRTC.DTLSTransport do
171176
end
172177
end
173178

179+
@impl true
180+
def handle_call(:set_ice_connected, _from, state) do
181+
Logger.debug("""
182+
Setting ice connected in unexpected DTLS state: #{state.dtls_state}. \
183+
DTLS handshake won't be performed.\
184+
""")
185+
186+
state = %{state | ice_connected: true}
187+
{:reply, :ok, state}
188+
end
189+
174190
@impl true
175191
def handle_call(:get_certs_info, _from, state) do
176192
local_cert_info = %{
@@ -239,6 +255,19 @@ defmodule ExWebRTC.DTLSTransport do
239255
{:reply, {:error, :already_started}, state}
240256
end
241257

258+
@impl true
259+
def handle_call(:close, _from, %{state: :closed} = state) do
260+
{:reply, :ok, state}
261+
end
262+
263+
@impl true
264+
def handle_call(:close, _from, state) do
265+
{:ok, packets} = ExDTLS.disconnect(state.dtls)
266+
:ok = do_send(state, packets)
267+
state = update_dtls_state(state, :closed, notify: false)
268+
{:reply, :ok, state}
269+
end
270+
242271
@impl true
243272
def handle_cast({:send_rtp, data}, %{dtls_state: :connected, ice_connected: true} = state) do
244273
case ExLibSRTP.protect(state.out_srtp, data) do
@@ -327,6 +356,14 @@ defmodule ExWebRTC.DTLSTransport do
327356
{:ok, state} ->
328357
{:noreply, state}
329358

359+
{:error, :peer_closed_for_writing} ->
360+
# See W3C WebRTC sec. 5.5.1
361+
# peer_closed_for_writing is returned when the remote side
362+
# sends close_notify alert
363+
ExDTLS.disconnect(state.dtls)
364+
state = update_dtls_state(state, :closed)
365+
{:noreply, state}
366+
330367
{:error, _reason} ->
331368
# See W3C WebRTC sec. 5.5.
332369
state = update_dtls_state(state, :failed)
@@ -345,6 +382,12 @@ defmodule ExWebRTC.DTLSTransport do
345382
Logger.debug("Stopping DTLSTransport with reason: #{inspect(reason)}")
346383
end
347384

385+
defp handle_ice_data({:data, _data}, %{dtls_state: dtls_state} = state)
386+
when dtls_state in [:failed, :closed] do
387+
Logger.debug("Received DTLS packets in state #{dtls_state}. Ignoring.")
388+
{:ok, state}
389+
end
390+
348391
defp handle_ice_data({:data, data}, %{dtls: nil} = state) do
349392
# received DTLS data from remote peer before receiving an
350393
# SDP answer and initializing the DTLS Transport, will buffer the data
@@ -470,11 +513,16 @@ defmodule ExWebRTC.DTLSTransport do
470513
:ok
471514
end
472515

473-
defp update_dtls_state(%{dtls_state: dtls_state} = state, dtls_state), do: state
516+
defp update_dtls_state(state, dtls_state, otps \\ [])
517+
defp update_dtls_state(%{dtls_state: dtls_state} = state, dtls_state, _opts), do: state
474518

475-
defp update_dtls_state(state, new_dtls_state) do
519+
defp update_dtls_state(state, new_dtls_state, opts) do
476520
Logger.debug("Changing DTLS state: #{state.dtls_state} -> #{new_dtls_state}")
477-
notify(state.owner, {:state_change, new_dtls_state})
521+
522+
if opts[:notify] != false do
523+
notify(state.owner, {:state_change, new_dtls_state})
524+
end
525+
478526
%{state | dtls_state: new_dtls_state}
479527
end
480528

lib/ex_webrtc/ice_transport.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule ExWebRTC.ICETransport do
33

44
# module implementing this behaviour
55
@type t() :: module()
6-
@type state() :: :checking | :connected | :completed | :failed
6+
@type state() :: :checking | :connected | :completed | :failed | :closed
77

88
@callback start_link(Keyword.t()) :: {:ok, pid()}
99
@callback on_data(pid(), pid()) :: :ok
@@ -19,6 +19,7 @@ defmodule ExWebRTC.ICETransport do
1919
@callback set_role(pid(), ExICE.ICEAgent.role()) :: :ok
2020
@callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok
2121
@callback get_stats(pid()) :: map()
22+
@callback close(pid()) :: :ok
2223
@callback stop(pid()) :: :ok
2324
end
2425

@@ -58,5 +59,7 @@ defmodule ExWebRTC.DefaultICETransport do
5859
@impl true
5960
defdelegate get_stats(pid), to: ICEAgent
6061
@impl true
62+
def close(_pid), do: :ok
63+
@impl true
6164
defdelegate stop(pid), to: ICEAgent
6265
end

lib/ex_webrtc/peer_connection.ex

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ defmodule ExWebRTC.PeerConnection do
6363
6464
For the exact meaning, refer to the [RTCDtlsTransport: state property](https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state)
6565
"""
66-
@type dtls_transport_state() :: :new | :connecting | :connected | :failed
66+
@type dtls_transport_state() :: :new | :connecting | :connected | :closed | :failed
6767

6868
@typedoc """
6969
Possible signaling states.
@@ -105,8 +105,8 @@ defmodule ExWebRTC.PeerConnection do
105105
| {:track_muted, MediaStreamTrack.id()}
106106
| {:track_ended, MediaStreamTrack.id()}
107107
| {:data, DataChannel.ref(), binary()}
108-
| {:rtp, MediaStreamTrack.id(), String.t() | nil, ExRTP.Packet.t()}}
109-
| {:rtcp, [{MediaStreamTrack.id() | nil, ExRTCP.Packet.packet()}]}
108+
| {:rtp, MediaStreamTrack.id(), String.t() | nil, ExRTP.Packet.t()}
109+
| {:rtcp, [{MediaStreamTrack.id() | nil, ExRTCP.Packet.packet()}]}}
110110

111111
#### NON-MDN-API ####
112112

@@ -472,7 +472,7 @@ defmodule ExWebRTC.PeerConnection do
472472
peer_connection(),
473473
RTPTransceiver.kind() | MediaStreamTrack.t(),
474474
direction: RTPTransceiver.direction()
475-
) :: {:ok, RTPTransceiver.t()}
475+
) :: {:ok, RTPTransceiver.t()} | {:error, term()}
476476
def add_transceiver(peer_connection, kind_or_track, options \\ []) do
477477
GenServer.call(peer_connection, {:add_transceiver, kind_or_track, options})
478478
end
@@ -507,7 +507,8 @@ defmodule ExWebRTC.PeerConnection do
507507
508508
For more information, refer to the [RTCPeerConnection: addTrack() method](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack).
509509
"""
510-
@spec add_track(peer_connection(), MediaStreamTrack.t()) :: {:ok, RTPSender.t()}
510+
@spec add_track(peer_connection(), MediaStreamTrack.t()) ::
511+
{:ok, RTPSender.t()} | {:error, term()}
511512
def add_track(peer_connection, track) do
512513
GenServer.call(peer_connection, {:add_track, track})
513514
end
@@ -574,11 +575,20 @@ defmodule ExWebRTC.PeerConnection do
574575
@doc """
575576
Closes the PeerConnection.
576577
577-
This function kills the `peer_connection` process.
578+
This function doest not kill the `peer_connection` process.
579+
If you want to stop the `peer_connection` process, see `stop/1`.
578580
For more information, refer to the [RTCPeerConnection: close() method](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/close).
579581
"""
580582
@spec close(peer_connection()) :: :ok
581583
def close(peer_connection) do
584+
GenServer.call(peer_connection, :close)
585+
end
586+
587+
@doc """
588+
Closes and stops PeerConnection process.
589+
"""
590+
@spec stop(peer_connection()) :: :ok
591+
def stop(peer_connection) do
582592
GenServer.stop(peer_connection)
583593
end
584594

@@ -1067,6 +1077,14 @@ defmodule ExWebRTC.PeerConnection do
10671077

10681078
if Code.ensure_loaded?(ExSCTP) do
10691079
@impl true
1080+
def handle_call(
1081+
{:create_data_channel, _label, _opts},
1082+
_from,
1083+
%{connection_state: :closed} = state
1084+
) do
1085+
{:reply, {:error, :closed}, state}
1086+
end
1087+
10701088
def handle_call({:create_data_channel, label, opts}, _from, state) do
10711089
ordered = Keyword.get(opts, :ordered, true)
10721090
lifetime = Keyword.get(opts, :max_packet_life_time)
@@ -1075,7 +1093,7 @@ defmodule ExWebRTC.PeerConnection do
10751093

10761094
with true <- byte_size(label) < 65_535,
10771095
true <- lifetime == nil or max_rtx == nil do
1078-
{events, channel, sctp_transport} =
1096+
{:ok, events, channel, sctp_transport} =
10791097
SCTPTransport.add_channel(
10801098
state.sctp_transport,
10811099
label,
@@ -1271,9 +1289,17 @@ defmodule ExWebRTC.PeerConnection do
12711289
{:reply, stats, state}
12721290
end
12731291

1292+
@impl true
1293+
def handle_call(:close, _from, state) do
1294+
Logger.debug("Closing peer connection")
1295+
# don't emit state change events
1296+
state = do_close(state, notify: false)
1297+
{:reply, :ok, state}
1298+
end
1299+
12741300
@impl true
12751301
def handle_cast({:send_rtp, track_id, packet, opts}, %{conn_state: conn_state} = state)
1276-
when conn_state != :failed do
1302+
when conn_state not in [:failed, :closed] do
12771303
rtx? = Keyword.get(opts, :rtx?, false)
12781304

12791305
# TODO: iterating over transceivers is not optimal
@@ -1331,7 +1357,7 @@ defmodule ExWebRTC.PeerConnection do
13311357

13321358
@impl true
13331359
def handle_cast({:send_pli, track_id, rid}, %{conn_state: conn_state} = state)
1334-
when conn_state != :failed do
1360+
when conn_state not in [:failed, :closed] do
13351361
state.transceivers
13361362
|> Enum.with_index()
13371363
|> Enum.find(fn {tr, _idx} -> tr.receiver.track.id == track_id end)
@@ -1365,7 +1391,7 @@ defmodule ExWebRTC.PeerConnection do
13651391
if Code.ensure_loaded?(ExSCTP) do
13661392
@impl true
13671393
def handle_cast({:send_data, channel_ref, data_type, data}, %{conn_state: conn_state} = state)
1368-
when conn_state != :failed do
1394+
when conn_state not in [:failed, :closed] do
13691395
{events, sctp_transport} =
13701396
SCTPTransport.send(state.sctp_transport, channel_ref, data_type, data)
13711397

@@ -1432,12 +1458,17 @@ defmodule ExWebRTC.PeerConnection do
14321458

14331459
next_conn_state = next_conn_state(state.ice_state, new_dtls_state)
14341460

1435-
state =
1436-
%{state | dtls_state: new_dtls_state}
1437-
|> update_conn_state(next_conn_state)
1438-
|> maybe_connect_sctp()
1461+
if next_conn_state == :closed and state.conn_state != :closed do
1462+
state = do_close(state)
1463+
{:noreply, state}
1464+
else
1465+
state =
1466+
%{state | dtls_state: new_dtls_state}
1467+
|> update_conn_state(next_conn_state)
1468+
|> maybe_connect_sctp()
14391469

1440-
{:noreply, state}
1470+
{:noreply, state}
1471+
end
14411472
end
14421473

14431474
@impl true
@@ -1955,15 +1986,24 @@ defmodule ExWebRTC.PeerConnection do
19551986
defp next_signaling_state(:have_remote_pranswer, :remote, :answer), do: {:ok, :stable}
19561987
defp next_signaling_state(:have_remote_pranswer, _, _), do: {:error, :invalid_state}
19571988

1958-
defp update_signaling_state(%{signaling_state: signaling_state} = state, signaling_state),
1959-
do: state
1989+
defp update_signaling_state(state, signaling_state, opts \\ [])
1990+
1991+
defp update_signaling_state(
1992+
%{signaling_state: signaling_state} = state,
1993+
signaling_state,
1994+
_opts
1995+
),
1996+
do: state
19601997

1961-
defp update_signaling_state(state, new_signaling_state) do
1998+
defp update_signaling_state(state, new_signaling_state, opts) do
19621999
Logger.debug(
19632000
"Changing PeerConnection signaling state state: #{state.signaling_state} -> #{new_signaling_state}"
19642001
)
19652002

1966-
notify(state.owner, {:signaling_state_change, new_signaling_state})
2003+
if opts[:notify] != false do
2004+
notify(state.owner, {:signaling_state_change, new_signaling_state})
2005+
end
2006+
19672007
%{state | signaling_state: new_signaling_state}
19682008
end
19692009

@@ -2192,12 +2232,15 @@ defmodule ExWebRTC.PeerConnection do
21922232
defp get_last_answer(%{current_remote_desc: {:answer, desc}}), do: desc
21932233

21942234
# TODO support :disconnected state - our ICE doesn't provide disconnected state for now
2195-
# TODO support :closed state
21962235
# the order of these clauses is important
21972236
defp next_conn_state(ice_state, dtls_state)
21982237

2199-
defp next_conn_state(ice_state, dtls_state) when ice_state == :failed or dtls_state == :failed,
2200-
do: :failed
2238+
# Give closed precedence over failed.
2239+
# Failed connection can be restarted.
2240+
# Closed connection can't be reused.
2241+
defp next_conn_state(:closed, _dtls_state), do: :closed
2242+
2243+
defp next_conn_state(_ice_state, :closed), do: :closed
22012244

22022245
defp next_conn_state(:failed, _dtls_state), do: :failed
22032246

@@ -2212,11 +2255,16 @@ defmodule ExWebRTC.PeerConnection do
22122255
defp next_conn_state(ice_state, :connected) when ice_state in [:connected, :completed],
22132256
do: :connected
22142257

2215-
defp update_conn_state(%{conn_state: conn_state} = state, conn_state), do: state
2258+
defp update_conn_state(state, conn_state, opts \\ [])
2259+
defp update_conn_state(%{conn_state: conn_state} = state, conn_state, _opts), do: state
22162260

2217-
defp update_conn_state(state, new_conn_state) do
2261+
defp update_conn_state(state, new_conn_state, opts) do
22182262
Logger.debug("Changing PeerConnection state: #{state.conn_state} -> #{new_conn_state}")
2219-
notify(state.owner, {:connection_state_change, new_conn_state})
2263+
2264+
if opts[:notify] != false do
2265+
notify(state.owner, {:connection_state_change, new_conn_state})
2266+
end
2267+
22202268
%{state | conn_state: new_conn_state}
22212269
end
22222270

@@ -2413,6 +2461,23 @@ defmodule ExWebRTC.PeerConnection do
24132461
%SessionDescription{type: type, sdp: to_string(sdp)}
24142462
end
24152463

2464+
defp do_close(state, opts \\ []) do
2465+
transceivers = Enum.map(state.transceivers, &RTPTransceiver.stop(&1))
2466+
sctp_transport = SCTPTransport.close_abruptly(state.sctp_transport)
2467+
:ok = DTLSTransport.close(state.dtls_transport)
2468+
:ok = state.ice_transport.close(state.ice_pid)
2469+
2470+
%{
2471+
state
2472+
| ice_state: :closed,
2473+
dtls_state: :closed,
2474+
transceivers: transceivers,
2475+
sctp_transport: sctp_transport
2476+
}
2477+
|> update_signaling_state(:closed, opts)
2478+
|> update_conn_state(:closed, opts)
2479+
end
2480+
24162481
defp generate_ssrcs(state) do
24172482
generate_ssrcs(state.transceivers, Map.keys(state.demuxer.ssrc_to_mid))
24182483
end

lib/ex_webrtc/rtp_transceiver.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ defmodule ExWebRTC.RTPTransceiver do
469469

470470
@doc false
471471
@spec stop(transceiver(), (-> term())) :: transceiver()
472-
def stop(transceiver, on_track_ended) do
472+
def stop(transceiver, on_track_ended \\ fn -> :ok end) do
473473
transceiver =
474474
if transceiver.stopping,
475475
do: transceiver,

0 commit comments

Comments
 (0)