Skip to content

Commit

Permalink
Use new, struct-based ex_dtls; add tests (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
LVala authored Nov 21, 2023
1 parent d258b94 commit daa448a
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 47 deletions.
98 changes: 54 additions & 44 deletions lib/ex_webrtc/dtls_transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -63,7 +62,7 @@ defmodule ExWebRTC.DTLSTransport do
fingerprint: fingerprint,
srtp: srtp,
dtls_state: :new,
client: nil,
dtls: nil,
mode: nil
}

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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, <<f, _rest::binary>> = 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 ->
Expand All @@ -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)}")
Expand All @@ -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
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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", []},
Expand All @@ -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"},
Expand Down
143 changes: 143 additions & 0 deletions test/ex_webrtc/dtls_transport_test.exs
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit daa448a

Please sign in to comment.