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

@typedoc """
Mapping function used instead of STUN server. Maps local ip addresses into public ones.
These public addresses are then used to create server reflexive candidates.

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
and NAT use 1 to 1 port mapping.
"""
@type map_to_nat_ip() :: (: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 +84,16 @@ 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`.
* `map_to_nat_ip` - Mapping function used instead of STUN server. Maps
local ip addresses into public ones.
These public addresses are then used to create server reflexive candidates.

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
and NAT use 1 to 1 port mapping.
"""
@type opts() :: [
role: role() | nil,
Expand All @@ -88,7 +111,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,
map_to_nat_ip: map_to_nat_ip() | nil
]

@doc """
Expand Down
22 changes: 19 additions & 3 deletions lib/ex_ice/priv/ice_agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule ExICE.Priv.ICEAgent do
ConnCheckHandler,
Gatherer,
IfDiscovery,
NATMapper,
Transport,
Utils
}
Expand Down Expand Up @@ -98,7 +99,8 @@ defmodule ExICE.Priv.ICEAgent do
selected_candidate_pair_changes: 0,
# binding requests that failed to pass checks required to assign them to specific candidate pair
# e.g. missing required attributes, role conflict, authentication, etc.
unmatched_requests: 0
unmatched_requests: 0,
map_to_nat_ip: nil
]

@spec unmarshal_remote_candidate(String.t()) :: {:ok, Candidate.t()} | {:error, term()}
Expand Down Expand Up @@ -165,7 +167,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,
map_to_nat_ip: opts[:map_to_nat_ip]
}
end

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

ice_agent = %__MODULE__{ice_agent | local_preferences: local_preferences}

srflx_cands =
NATMapper.create_srflx_candidates(
host_cands,
ice_agent.map_to_nat_ip,
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
73 changes: 73 additions & 0 deletions lib/ex_ice/priv/nat_mapper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule ExICE.Priv.NATMapper do
@moduledoc false

require Logger

alias ExICE.ICEAgent
alias ExICE.Priv.Candidate

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

def create_srflx_candidates(host_cands, map_to_nat_ip, local_preferences) do
{cands, _external_ips} =
Enum.reduce(host_cands, {[], []}, fn host_cand, {cands, external_ips} ->
external_ip = map_to_nat_ip.(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)}")

{[cand | cands], [external_ip | external_ips]}
else
{cands, external_ips}
end
end)

cands
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)}"
)
end
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 "NAT mapping" do
alias ExICE.Priv.Candidate

@ipv4 {10, 10, 10, 10}
@ipv6 {0, 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 repeats itself" 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"}],
map_to_nat_ip: 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, map_to_nat_ip) do
%ICEAgent{gathering_state: :complete} =
ICEAgent.new(
controlling_process: self(),
role: :controlled,
transport_module: Transport.Mock,
if_discovery_module: discovery_module,
map_to_nat_ip: map_to_nat_ip
)
|> 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