Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use new, struct-based ex_dtls; add tests #17

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

@mickel8 mickel8 Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be perfect we should define a behavior for ice_module but I don't insist

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe let's let it be for now, especially considering that DTLSTransport uses exactly one function from ICEAgent API.

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
LVala marked this conversation as resolved.
Show resolved Hide resolved
{: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
132 changes: 132 additions & 0 deletions test/ex_webrtc/dtls_transport_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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__, config)
end

def send_data(ice_agent, data) do
GenServer.cast(ice_agent, {:send_data, data})
end

@impl true
def init(tester: tester), do: {:ok, tester}

@impl true
def handle_cast({:send_data, data}, tester) do
send(tester, {:fake_ice, data})
{:noreply, tester}
end
end

setup do
assert {:ok, dtls} = DTLSTransport.start_link([tester: self()], FakeICEAgent)

%{dtls: dtls}
end

test "forwards non-data ICE messages", %{dtls: dtls} do
message = "test message"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better send some signaling message like connection state change albo gathering state change


send_ice(dtls, message)
assert_receive {:ex_ice, _from, ^message}

send_ice(dtls, {: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} do
:ok = DTLSTransport.start_dtls(dtls, :active)

send_ice(dtls, :connected)

assert_receive {:fake_ice, packets}
assert is_binary(packets)
end

test "won't initiate DTLS handshake when in passive mode", %{dtls: dtls} do
:ok = DTLSTransport.start_dtls(dtls, :passive)

send_ice(dtls, :connected)

refute_receive({:fake_ice, _msg})
end

test "will retransmit after initiating handshake", %{dtls: dtls} do
:ok = DTLSTransport.start_dtls(dtls, :active)

send_ice(dtls, :connected)

assert_receive {:fake_ice, _packets}
assert_receive {:fake_ice, _retransmited}, 1200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert packets and retransmitted are the same

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But they are not the same tho.

end

test "will buffer packets and send when connected", %{dtls: dtls} do
:ok = DTLSTransport.start_dtls(dtls, :passive)

remote_dtls = ExDTLS.init(client_mode: true, dtls_srtp: true)
{packets, _timeout} = ExDTLS.do_handshake(remote_dtls)

send_ice(dtls, {:data, packets})
refute_receive {:fake_ice, _packets}

send_ice(dtls, :connected)
assert_receive {:fake_ice, packets}
assert is_binary(packets)
end

test "finishes handshake in actice mode", %{dtls: dtls} do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test "finishes handshake in actice mode", %{dtls: dtls} do
test "finishes handshake in active mode", %{dtls: dtls} do

:ok = DTLSTransport.start_dtls(dtls, :active)
remote_dtls = ExDTLS.init(client_mode: false, dtls_srtp: true)

send_ice(dtls, :connected)

assert :ok = check_handshake(dtls, remote_dtls)
end

test "finishes handshake in passive mode", %{dtls: dtls} do
:ok = DTLSTransport.start_dtls(dtls, :passive)
send_ice(dtls, :connected)

remote_dtls = ExDTLS.init(client_mode: true, dtls_srtp: true)
{packets, _timeout} = ExDTLS.do_handshake(remote_dtls)
send_ice(dtls, {:data, packets})

assert :ok == check_handshake(dtls, remote_dtls)
end

defp check_handshake(dtls, remote_dtls) do
assert_receive {:fake_ice, packets}

case ExDTLS.handle_data(remote_dtls, packets) do
{:handshake_packets, packets, _timeout} ->
send_ice(dtls, {:data, packets})
check_handshake(dtls, remote_dtls)

{:handshake_finished, _, _, _, packets} ->
send_ice(dtls, {:data, packets})
:ok

{:handshake_finished, _, _, _} ->
:ok
end
end

defp send_ice(dtls, msg), do: send(dtls, {:ex_ice, "dummy_pid", msg})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this accept ice_pid instead i.e

Suggested change
defp send_ice(dtls, msg), do: send(dtls, {:ex_ice, "dummy_pid", msg})
defp send_ice(ice_agent, msg), do: send(ice_agent, {:send, msg})

end
File renamed without changes.
Loading