Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
21 changes: 20 additions & 1 deletion lib/ex_ice/ice_agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ defmodule ExICE.ICEAgent do
"""
@type ip_filter() :: (:inet.ip_address() -> boolean)

@typedoc """
Function called for each host candidate to derive a new "fabricated" srflx candidate from it.
This function takes host's ip as an argument and should return srflx's ip as a result or nil if for given host candidate
there should be no srflx one.

Note that each returned IP address must be unique.
If the mapping function repeatedly returns the same address,
it will be ignored, and only one server reflexive candidate will be created.

This function is meant to be used for server implementations where the public addresses are well known.
NAT uses 1 to 1 port mapping and using STUN server for discovering public IP is undesirable
(e.g. because of unknown response time).
"""
@type host_to_srflx_ip_mapper() :: (:inet.ip_address() -> :inet.ip_address() | nil)

@typedoc """
ICE Agent configuration options.
All notifications are by default sent to a process that spawns `ExICE`.
Expand All @@ -71,6 +86,9 @@ defmodule ExICE.ICEAgent do
* `on_connection_state_change` - where to send connection state change notifications. Defaults to a process that spawns `ExICE`.
* `on_data` - where to send data. Defaults to a process that spawns `ExICE`.
* `on_new_candidate` - where to send new candidates. Defaults to a process that spawns `ExICE`.
* `host_to_srflx_ip_mapper` - function called for each host candidate to derive a new "fabricated" srflx candidate from it.
This function takes host's ip as an argument and should return srflx's ip as a result or nil if for given host candidate
there should be no srflx one.
"""
@type opts() :: [
role: role() | nil,
Expand All @@ -88,7 +106,8 @@ defmodule ExICE.ICEAgent do
on_gathering_state_change: pid() | nil,
on_connection_state_change: pid() | nil,
on_data: pid() | nil,
on_new_candidate: pid() | nil
on_new_candidate: pid() | nil,
host_to_srflx_ip_mapper: host_to_srflx_ip_mapper() | nil
]

@doc """
Expand Down
99 changes: 99 additions & 0 deletions lib/ex_ice/priv/gatherer.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ExICE.Priv.Gatherer do
@moduledoc false

alias ExICE.ICEAgent
alias ExICE.Priv.{Candidate, Transport, Utils}
alias ExSTUN.Message
alias ExSTUN.Message.Type
Expand Down Expand Up @@ -139,6 +140,104 @@ defmodule ExICE.Priv.Gatherer do
end
end

@spec fabricate_srflx_candidates([Candidate.Host.t()], ICEAgent.host_to_srflx_ip_mapper(), %{
:inet.ip_address() => non_neg_integer()
}) :: [Candidate.Srflx.t()]
def fabricate_srflx_candidates(_host_cands, nil, _local_preferences) do
[]
end

def fabricate_srflx_candidates(host_cands, host_to_srflx_ip_mapper, local_preferences) do
do_fabricate_srflx_candidates(
host_cands,
host_to_srflx_ip_mapper,
local_preferences,
[],
[]
)
end

defp do_fabricate_srflx_candidates(
[],
_host_to_srflx_ip_mapper,
_local_preferences,
srflx_cands,
_external_ips
) do
srflx_cands
end

defp do_fabricate_srflx_candidates(
[host_cand | rest],
host_to_srflx_ip_mapper,
local_preferences,
srflx_cands,
external_ips
) do
external_ip = host_to_srflx_ip_mapper.(host_cand.base.address)

if valid_external_ip?(external_ip, host_cand.base.address, external_ips) do
priority =
Candidate.priority!(local_preferences, host_cand.base.address, :srflx)

cand =
Candidate.Srflx.new(
address: external_ip,
port: host_cand.base.port,
base_address: host_cand.base.address,
base_port: host_cand.base.port,
priority: priority,
transport_module: host_cand.base.transport_module,
socket: host_cand.base.socket
)

Logger.debug("New srflx candidate from NAT mapping: #{inspect(cand)}")

do_fabricate_srflx_candidates(
rest,
host_to_srflx_ip_mapper,
local_preferences,
[cand | srflx_cands],
[external_ip | external_ips]
)
else
do_fabricate_srflx_candidates(
rest,
host_to_srflx_ip_mapper,
local_preferences,
srflx_cands,
external_ips
)
end
end

defp valid_external_ip?(external_ip, host_ip, external_ips) do
same_type? = :inet.is_ipv4_address(external_ip) == :inet.is_ipv4_address(host_ip)

cond do
host_ip == external_ip ->
log_warning(host_ip, external_ip, "external IP is the same as local IP")
false

not :inet.is_ip_address(external_ip) or not same_type? ->
log_warning(host_ip, external_ip, "not valid IP address")
false

external_ip in external_ips ->
log_warning(host_ip, external_ip, "address already in use")
false

true ->
true
end
end

defp log_warning(host_ip, external_ip, reason),
do:
Logger.warning(
"Ignoring NAT mapping: #{inspect(host_ip)} to #{inspect(external_ip)}, #{inspect(reason)}"
)

defp loopback_if?({_int_name, int}) do
:loopback in int[:flags]
end
Expand Down
19 changes: 17 additions & 2 deletions lib/ex_ice/priv/ice_agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ defmodule ExICE.Priv.ICEAgent do
stun_servers: [],
turn_servers: [],
resolved_turn_servers: [],
host_to_srflx_ip_mapper: nil,
# stats
bytes_sent: 0,
bytes_received: 0,
Expand Down Expand Up @@ -165,7 +166,8 @@ defmodule ExICE.Priv.ICEAgent do
local_ufrag: local_ufrag,
local_pwd: local_pwd,
stun_servers: stun_servers,
turn_servers: turn_servers
turn_servers: turn_servers,
host_to_srflx_ip_mapper: opts[:host_to_srflx_ip_mapper]
}
end

Expand Down Expand Up @@ -324,12 +326,25 @@ defmodule ExICE.Priv.ICEAgent do

ice_agent = %__MODULE__{ice_agent | local_preferences: local_preferences}

srflx_cands =
Gatherer.fabricate_srflx_candidates(
host_cands,
ice_agent.host_to_srflx_ip_mapper,
ice_agent.local_preferences
)

ice_agent =
Enum.reduce(host_cands, ice_agent, fn host_cand, ice_agent ->
add_local_cand(ice_agent, host_cand)
end)

for %cand_mod{} = cand <- host_cands do
ice_agent =
Enum.reduce(srflx_cands, ice_agent, fn cand, ice_agent ->
# don't pair reflexive candidate, it should be pruned anyway - see sec. 6.1.2.4
put_in(ice_agent.local_cands[cand.base.id], cand)
end)

for %cand_mod{} = cand <- host_cands ++ srflx_cands do
notify(ice_agent.on_new_candidate, {:new_candidate, cand_mod.marshal(cand)})
end

Expand Down
121 changes: 121 additions & 0 deletions test/priv/ice_agent_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2631,6 +2631,127 @@ defmodule ExICE.Priv.ICEAgentTest do
end
end

describe "host to prefabricated srflx mapper" do
alias ExICE.Priv.Candidate

@ipv4 {10, 10, 10, 10}
@ipv6 {64_512, 0, 0, 0, 0, 0, 0, 1}
@invalid_ip :invalid_ip

test "adds srflx candidate" do
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, all tests except for the first one and the last one could be moved to gatherer_test.exs

ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @ipv4 end)

assert [%Candidate.Srflx{base: %{address: @ipv4}}] = srflx_candidates(ice_agent)

assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
assert_receive {:ex_ice, _pid, {:new_candidate, srflx_cand}}

assert host_cand =~ "typ host"
assert srflx_cand =~ "typ srflx"
end

test "creates only one candidate if external ip is repeated" do
ice_agent = spawn_ice_agent(IfDiscovery.MockMulti, fn _ip -> @ipv4 end)

assert [%Candidate.Srflx{base: %{address: @ipv4}}] = srflx_candidates(ice_agent)

assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand_2}}
assert_receive {:ex_ice, _pid, {:new_candidate, srflx_cand}}

assert host_cand =~ "typ host"
assert host_cand_2 =~ "typ host"
assert srflx_cand =~ "typ srflx"
end

test "ignores one to one mapping" do
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn ip -> ip end)

assert [] == srflx_candidates(ice_agent)

assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
refute_receive {:ex_ice, _pid, {:new_candidate, _srflx_cand}}

assert host_cand =~ "typ host"
end

test "ignores if ip types is not the same" do
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @ipv6 end)

assert [] == srflx_candidates(ice_agent)
end

test "ignores when function returns nil value" do
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> nil end)

assert [] == srflx_candidates(ice_agent)
end

test "ignores when function returns invalid value" do
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @invalid_ip end)

assert [] == srflx_candidates(ice_agent)
end

test "works with STUN enabled" do
ice_agent =
ICEAgent.new(
controlling_process: self(),
role: :controlled,
transport_module: Transport.Mock,
if_discovery_module: IfDiscovery.MockSingle,
ice_servers: [%{urls: "stun:192.168.0.3:19302"}],
host_to_srflx_ip_mapper: fn _ip -> @ipv4 end
)
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
|> ICEAgent.gather_candidates()

[%Candidate.Srflx{base: %{address: @ipv4, port: srflx_port}}] = srflx_candidates(ice_agent)

[socket] = ice_agent.sockets

# assert no transactions are started until handle_ta_timeout is called
assert nil == Transport.Mock.recv(socket)

# assert ice agent started gathering transaction by sending a binding request
ice_agent = ICEAgent.handle_ta_timeout(ice_agent)
assert packet = Transport.Mock.recv(socket)
assert {:ok, req} = ExSTUN.Message.decode(packet)
assert req.type.class == :request
assert req.type.method == :binding

resp =
Message.new(req.transaction_id, %Type{class: :success_response, method: :binding}, [
%XORMappedAddress{address: @ipv4, port: srflx_port}
])
|> Message.encode()

ice_agent = ICEAgent.handle_udp(ice_agent, socket, @stun_ip, @stun_port, resp)

# assert there isn't new srflx candidate
assert [%Candidate.Srflx{}] = srflx_candidates(ice_agent)
end

defp spawn_ice_agent(discovery_module, host_to_srflx_ip_mapper) do
%ICEAgent{gathering_state: :complete} =
ICEAgent.new(
controlling_process: self(),
role: :controlled,
transport_module: Transport.Mock,
if_discovery_module: discovery_module,
host_to_srflx_ip_mapper: host_to_srflx_ip_mapper
)
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
|> ICEAgent.gather_candidates()
end

defp srflx_candidates(ice_agent) do
ice_agent.local_cands
|> Map.values()
|> Enum.filter(&(&1.base.type == :srflx))
end
end

test "relay ice_transport_policy" do
ice_agent =
ICEAgent.new(
Expand Down
Loading