diff --git a/lib/ex_webrtc/dtls_transport.ex b/lib/ex_webrtc/dtls_transport.ex index e93935e0..9734005a 100644 --- a/lib/ex_webrtc/dtls_transport.ex +++ b/lib/ex_webrtc/dtls_transport.ex @@ -13,8 +13,8 @@ defmodule ExWebRTC.DTLSTransport do @doc false @spec start_link(ExICE.ICEAgent.opts(), GenServer.server()) :: GenServer.on_start() - def start_link(ice_config, peer_connection \\ self()) do - GenServer.start_link(__MODULE__, [ice_config, peer_connection]) + def start_link(ice_config, ice_module \\ ICEAgent) do + GenServer.start_link(__MODULE__, [ice_config, ice_module, self()]) end @doc false @@ -42,19 +42,18 @@ defmodule ExWebRTC.DTLSTransport do end @impl true - def init([ice_config, peer_connection]) do + def init([ice_config, ice_module, owner]) do # temporary hack to generate certs - {:ok, cert_client} = ExDTLS.start_link(client_mode: true, dtls_srtp: true) - {:ok, cert} = ExDTLS.get_cert(cert_client) - {:ok, pkey} = ExDTLS.get_pkey(cert_client) - {:ok, fingerprint} = ExDTLS.get_cert_fingerprint(cert_client) - :ok = ExDTLS.stop(cert_client) + dtls = ExDTLS.init(client_mode: true, dtls_srtp: true) + cert = ExDTLS.get_cert(dtls) + pkey = ExDTLS.get_pkey(dtls) + fingerprint = ExDTLS.get_cert_fingerprint(dtls) - {:ok, ice_agent} = ICEAgent.start_link(:controlled, ice_config) + {:ok, ice_agent} = ice_module.start_link(:controlled, ice_config) srtp = ExLibSRTP.new() state = %{ - peer_connection: peer_connection, + owner: owner, ice_agent: ice_agent, ice_state: nil, buffered_packets: nil, @@ -63,7 +62,7 @@ defmodule ExWebRTC.DTLSTransport do fingerprint: fingerprint, srtp: srtp, dtls_state: :new, - client: nil, + dtls: nil, mode: nil } @@ -81,17 +80,17 @@ defmodule ExWebRTC.DTLSTransport do end @impl true - def handle_call({:start_dtls, mode}, _from, %{client: nil} = state) + def handle_call({:start_dtls, mode}, _from, %{dtls: nil} = state) when mode in [:active, :passive] do - {:ok, client} = - ExDTLS.start_link( + dtls = + ExDTLS.init( client_mode: mode == :active, dtls_srtp: true, pkey: state.pkey, cert: state.cert ) - state = %{state | client: client, mode: mode} + state = %{state | dtls: dtls, mode: mode} {:reply, :ok, state} end @@ -118,8 +117,25 @@ defmodule ExWebRTC.DTLSTransport do end @impl true - def handle_info({:ex_dtls, _from, msg}, state) do - state = handle_dtls(msg, state) + def handle_info( + :dtls_timeout, + %{ice_state: ice_state, buffered_packets: buffered_packets} = state + ) do + case ExDTLS.handle_timeout(state.dtls) do + {:retransmit, packets, timeout} when ice_state in [:connected, :completed] -> + ICEAgent.send_data(state.ice_agent, packets) + Process.send_after(self(), :dtls_timeout, timeout) + + {:retransmit, ^buffered_packets, timeout} -> + # we got DTLS packets from the other side but + # we haven't established ICE connection yet so + # packets to retransmit have to be the same as dtls_buffered_packets + Process.send_after(self(), :dtls_timeout, timeout) + + :ok -> + :ok + end + {:noreply, state} end @@ -130,36 +146,44 @@ defmodule ExWebRTC.DTLSTransport do # forward everything, except for data, to peer connection process case msg do {:data, _data} -> :ok - _other -> send(state.peer_connection, ice_msg) + _other -> send(state.owner, ice_msg) end {:noreply, state} end + @impl true + def handle_info(msg, state) do + Logger.debug("DTLSTransport received unexpected message: #{inspect(msg)}") + {:noreply, state} + end + defp handle_ice({:data, <> = data}, state) when f in 20..64 do - case ExDTLS.process(state.client, data) do - {:handshake_packets, packets} when state.ice_state in [:connected, :completed] -> + case ExDTLS.handle_data(state.dtls, data) do + {:handshake_packets, packets, timeout} when state.ice_state in [:connected, :completed] -> :ok = ICEAgent.send_data(state.ice_agent, packets) + Process.send_after(self(), :dtls_timeout, timeout) %{state | dtls_state: :connecting} - {:handshake_packets, packets} -> + {:handshake_packets, packets, timeout} -> Logger.debug(""" Generated local DTLS packets but ICE is not in the connected or completed state yet. We will send those packets once ICE is ready. """) + Process.send_after(self(), :dtls_timeout, timeout) %{state | dtls_state: :connecting, buffered_packets: packets} - {:handshake_finished, keying_material, packets} -> + {:handshake_finished, _, remote_keying_material, profile, packets} -> Logger.debug("DTLS handshake finished") ICEAgent.send_data(state.ice_agent, packets) # TODO: validate fingerprint - state = setup_srtp(state, keying_material) + state = setup_srtp(state, remote_keying_material, profile) %{state | dtls_state: :connected} - {:handshake_finished, keying_material} -> + {:handshake_finished, _, remote_keying_material, profile} -> Logger.debug("DTLS handshake finished") - state = setup_srtp(state, keying_material) + state = setup_srtp(state, remote_keying_material, profile) %{state | dtls_state: :connected} :handshake_want_read -> @@ -172,7 +196,7 @@ defmodule ExWebRTC.DTLSTransport do case ExLibSRTP.unprotect(state.srtp, data) do {:ok, payload} -> # TODO: temporarily, everything goes to peer connection process - send(state.peer_connection, {:rtp_data, payload}) + send(state.owner, {:rtp_data, payload}) {:error, reason} -> Logger.warning("Failed to decrypt SRTP, reason: #{inspect(reason)}") @@ -194,7 +218,8 @@ defmodule ExWebRTC.DTLSTransport do when new_state in [:connected, :completed] do state = if state.mode == :active do - {:ok, packets} = ExDTLS.do_handshake(state.client) + {packets, timeout} = ExDTLS.do_handshake(state.dtls) + Process.send_after(self(), :dtls_timeout, timeout) :ok = ICEAgent.send_data(state.ice_agent, packets) %{state | dtls_state: :connecting} else @@ -221,28 +246,13 @@ defmodule ExWebRTC.DTLSTransport do defp handle_ice(_msg, state), do: state - defp handle_dtls({:retransmit, packets}, %{ice_state: ice_state} = state) - when ice_state in [:connected, :completed] do - ICEAgent.send_data(state.ice_agent, packets) - state - end - - defp handle_dtls({:retransmit, packets}, %{buffered_packets: packets} = state) do - # we got DTLS packets from the other side but - # we haven't established ICE connection yet so - # packets to retransmit have to be the same as dtls_buffered_packets - state - end - - defp setup_srtp(state, keying_material) do - {_local_material, remote_material, profile} = keying_material - + defp setup_srtp(state, remote_keying_material, profile) do {:ok, crypto_profile} = ExLibSRTP.Policy.crypto_profile_from_dtls_srtp_protection_profile(profile) policy = %ExLibSRTP.Policy{ ssrc: :any_inbound, - key: remote_material, + key: remote_keying_material, rtp: crypto_profile, rtcp: crypto_profile } diff --git a/mix.exs b/mix.exs index 4a4f7d19..478fd53b 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule ExWebRTC.MixProject do [ {:ex_sdp, "~> 0.13"}, {:ex_ice, "~> 0.1"}, - {:ex_dtls, "~> 0.13"}, + {:ex_dtls, "~> 0.14"}, {:ex_libsrtp, "~> 0.6"}, {:ex_rtp, "~> 0.2"}, {:ex_rtcp, "~> 0.1"}, diff --git a/mix.lock b/mix.lock index 630598c3..12c45908 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.34", "b0fbb4fd333ee7e9babc07e9573796850759cd12796fcf2fec59cf0031cbaad9", [:mix], [], "hexpm", "cc0d7a6f2367e4504867b4ec38ceee24e89ee6bca9c7b94a6d940f54aba2e8d5"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, - "ex_dtls": {:hex, :ex_dtls, "0.13.0", "4d7631eefc19a8820d4f79883f379ff2ad642976bda55493d4ec4e5d10d6c078", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3ece30967006ec12a4088e60514cb08847814fba8b8a21aca3862e5d1fd4a6bc"}, + "ex_dtls": {:hex, :ex_dtls, "0.14.0", "f2e589a24396599551c6b142c3a6a7a55f7fa772c90716888ed42bf3c994cb2d", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "7fa79815d8dbcee3b1464c3590527fb85fae9592b6e092f2554c5974e2c5847a"}, "ex_ice": {:hex, :ex_ice, "0.1.0", "2653c884872d8769cf9fc655c74002a63ed6c21be1b3c2badfa42bdc74de2355", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "e2539a321f87f31997ba974d532d00511e5828f2f113b550b1ef6aa799dd2ffe"}, "ex_libsrtp": {:hex, :ex_libsrtp, "0.6.0", "d96cd7fc1780157614f0bf47d31587e5eab953b43067f4885849f8177ec452a9", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "e9ce8a507a658f7e2df72fae82a4b3ba0a056c175f0bc490e79ab03058e094d5"}, "ex_rctp": {:git, "https://github.com/elixir-webrtc/ex_rtcp.git", "c0cf2b7f995e34d13cee4cbb228376a55700fb6a", []}, @@ -31,7 +31,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, - "req": {:hex, :req, "0.4.4", "a17b6bec956c9af4f08b5d8e8a6fc6e4edf24ccc0ac7bf363a90bba7a0f0138c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2618c0493444fee927d12073afb42e9154e766b3f4448e1011f0d3d551d1a011"}, + "req": {:hex, :req, "0.4.5", "2071bbedd280f107b9e33e1ddff2beb3991ec1ae06caa2cca2ab756393d8aca5", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dd23e9c7303ddeb2dee09ff11ad8102cca019e38394456f265fb7b9655c64dd8"}, "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, "shmex": {:hex, :shmex, "0.5.0", "7dc4fb1a8bd851085a652605d690bdd070628717864b442f53d3447326bcd3e8", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "b67bb1e22734758397c84458dbb746519e28eac210423c267c7248e59fc97bdc"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/test/ex_webrtc/dtls_transport_test.exs b/test/ex_webrtc/dtls_transport_test.exs new file mode 100644 index 00000000..9763fc9e --- /dev/null +++ b/test/ex_webrtc/dtls_transport_test.exs @@ -0,0 +1,143 @@ +defmodule ExWebRTC.DTLSTransportTest do + use ExUnit.Case, async: true + + alias ExWebRTC.DTLSTransport + + defmodule FakeICEAgent do + use GenServer + + def start_link(_mode, config) do + GenServer.start_link(__MODULE__, {self(), config}) + end + + def send_data(ice_agent, data) do + GenServer.cast(ice_agent, {:send_data, data}) + end + + def send_dtls(ice_agent, data) do + GenServer.cast(ice_agent, {:send_dtls, data}) + end + + @impl true + def init({dtls, tester: tester}), + do: {:ok, %{dtls: dtls, tester: tester}} + + @impl true + def handle_cast({:send_data, data}, state) do + send(state.tester, {:fake_ice, data}) + {:noreply, state} + end + + @impl true + def handle_cast({:send_dtls, data}, state) do + send(state.dtls, {:ex_ice, self(), data}) + {:noreply, state} + end + end + + setup do + assert {:ok, dtls} = DTLSTransport.start_link([tester: self()], FakeICEAgent) + ice = DTLSTransport.get_ice_agent(dtls) + assert is_pid(ice) + + %{dtls: dtls, ice: ice} + end + + test "forwards non-data ICE messages", %{ice: ice} do + message = :connected + + FakeICEAgent.send_dtls(ice, message) + assert_receive {:ex_ice, _from, ^message} + + FakeICEAgent.send_dtls(ice, {:data, <<1, 2, 3>>}) + refute_receive {:ex_ice, _from, _msg} + end + + test "cannot send data when handshake not finished", %{dtls: dtls} do + DTLSTransport.send_data(dtls, <<1, 2, 3>>) + + refute_receive {:fake_ice, _data} + end + + test "cannot start dtls more than once", %{dtls: dtls} do + assert :ok = DTLSTransport.start_dtls(dtls, :passive) + assert {:error, :already_started} = DTLSTransport.start_dtls(dtls, :passive) + end + + test "initiates DTLS handshake when in active mode", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :active) + + FakeICEAgent.send_dtls(ice, :connected) + + assert_receive {:fake_ice, packets} + assert is_binary(packets) + end + + test "won't initiate DTLS handshake when in passive mode", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :passive) + + FakeICEAgent.send_dtls(ice, :connected) + + refute_receive({:fake_ice, _msg}) + end + + test "will retransmit after initiating handshake", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :active) + + FakeICEAgent.send_dtls(ice, :connected) + + assert_receive {:fake_ice, _packets} + assert_receive {:fake_ice, _retransmited}, 1100 + end + + test "will buffer packets and send when connected", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :passive) + + remote_dtls = ExDTLS.init(client_mode: true, dtls_srtp: true) + {packets, _timeout} = ExDTLS.do_handshake(remote_dtls) + + FakeICEAgent.send_dtls(ice, {:data, packets}) + refute_receive {:fake_ice, _packets} + + FakeICEAgent.send_dtls(ice, :connected) + assert_receive {:fake_ice, packets} + assert is_binary(packets) + end + + test "finishes handshake in active mode", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :active) + remote_dtls = ExDTLS.init(client_mode: false, dtls_srtp: true) + + FakeICEAgent.send_dtls(ice, :connected) + + assert :ok = check_handshake(dtls, ice, remote_dtls) + end + + test "finishes handshake in passive mode", %{dtls: dtls, ice: ice} do + :ok = DTLSTransport.start_dtls(dtls, :passive) + FakeICEAgent.send_dtls(ice, :connected) + + remote_dtls = ExDTLS.init(client_mode: true, dtls_srtp: true) + {packets, _timeout} = ExDTLS.do_handshake(remote_dtls) + FakeICEAgent.send_dtls(ice, {:data, packets}) + + assert :ok == check_handshake(dtls, ice, remote_dtls) + end + + defp check_handshake(dtls, ice, remote_dtls) do + assert_receive {:fake_ice, packets} + + case ExDTLS.handle_data(remote_dtls, packets) do + {:handshake_packets, packets, _timeout} -> + FakeICEAgent.send_dtls(ice, {:data, packets}) + check_handshake(dtls, ice, remote_dtls) + + {:handshake_finished, _, _, _, packets} -> + FakeICEAgent.send_dtls(ice, {:data, packets}) + :ok + + {:handshake_finished, _, _, _} -> + :ok + end + end +end diff --git a/test/configuration_test.exs b/test/ex_webrtc/peer_connection/configuration_test.exs similarity index 100% rename from test/configuration_test.exs rename to test/ex_webrtc/peer_connection/configuration_test.exs diff --git a/test/peer_connection/demuxer_test.exs b/test/ex_webrtc/peer_connection/demuxer_test.exs similarity index 100% rename from test/peer_connection/demuxer_test.exs rename to test/ex_webrtc/peer_connection/demuxer_test.exs diff --git a/test/peer_connection_test.exs b/test/ex_webrtc/peer_connection_test.exs similarity index 100% rename from test/peer_connection_test.exs rename to test/ex_webrtc/peer_connection_test.exs