Skip to content

Commit 8248126

Browse files
committed
Allow for defining public ips for 1:1 NAT
1 parent e92cfcf commit 8248126

File tree

4 files changed

+238
-4
lines changed

4 files changed

+238
-4
lines changed

lib/ex_ice/ice_agent.ex

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ defmodule ExICE.ICEAgent do
4747
"""
4848
@type ip_filter() :: (:inet.ip_address() -> boolean)
4949

50+
@typedoc """
51+
Mapping function used instead of STUN server. Maps local ip addresses into public ones.
52+
These public addresses are then used to create server reflexive candidates.
53+
54+
Note that each returned IP address must be unique.
55+
If the mapping function repeatedly returns the same address,
56+
it will be ignored, and only one server reflexive candidate will be created.
57+
58+
This function is meant to be used for server implementations where the public addresses are well known
59+
and NAT use 1 to 1 port mapping.
60+
"""
61+
@type map_to_nat_ip() :: (:inet.ip_address() -> :inet.ip_address() | nil)
62+
5063
@typedoc """
5164
ICE Agent configuration options.
5265
All notifications are by default sent to a process that spawns `ExICE`.
@@ -71,6 +84,16 @@ defmodule ExICE.ICEAgent do
7184
* `on_connection_state_change` - where to send connection state change notifications. Defaults to a process that spawns `ExICE`.
7285
* `on_data` - where to send data. Defaults to a process that spawns `ExICE`.
7386
* `on_new_candidate` - where to send new candidates. Defaults to a process that spawns `ExICE`.
87+
* `map_to_nat_ip` - Mapping function used instead of STUN server. Maps
88+
local ip addresses into public ones.
89+
These public addresses are then used to create server reflexive candidates.
90+
91+
Note that each returned IP address must be unique.
92+
If the mapping function repeatedly returns the same address,
93+
it will be ignored, and only one server reflexive candidate will be created.
94+
95+
This function is meant to be used for server implementations where the public addresses are well known
96+
and NAT use 1 to 1 port mapping.
7497
"""
7598
@type opts() :: [
7699
role: role() | nil,
@@ -88,7 +111,8 @@ defmodule ExICE.ICEAgent do
88111
on_gathering_state_change: pid() | nil,
89112
on_connection_state_change: pid() | nil,
90113
on_data: pid() | nil,
91-
on_new_candidate: pid() | nil
114+
on_new_candidate: pid() | nil,
115+
map_to_nat_ip: map_to_nat_ip() | nil
92116
]
93117

94118
@doc """

lib/ex_ice/priv/ice_agent.ex

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule ExICE.Priv.ICEAgent do
1010
ConnCheckHandler,
1111
Gatherer,
1212
IfDiscovery,
13+
NATMapper,
1314
Transport,
1415
Utils
1516
}
@@ -98,7 +99,8 @@ defmodule ExICE.Priv.ICEAgent do
9899
selected_candidate_pair_changes: 0,
99100
# binding requests that failed to pass checks required to assign them to specific candidate pair
100101
# e.g. missing required attributes, role conflict, authentication, etc.
101-
unmatched_requests: 0
102+
unmatched_requests: 0,
103+
map_to_nat_ip: nil
102104
]
103105

104106
@spec unmarshal_remote_candidate(String.t()) :: {:ok, Candidate.t()} | {:error, term()}
@@ -165,7 +167,8 @@ defmodule ExICE.Priv.ICEAgent do
165167
local_ufrag: local_ufrag,
166168
local_pwd: local_pwd,
167169
stun_servers: stun_servers,
168-
turn_servers: turn_servers
170+
turn_servers: turn_servers,
171+
map_to_nat_ip: opts[:map_to_nat_ip]
169172
}
170173
end
171174

@@ -324,12 +327,25 @@ defmodule ExICE.Priv.ICEAgent do
324327

325328
ice_agent = %__MODULE__{ice_agent | local_preferences: local_preferences}
326329

330+
srflx_cands =
331+
NATMapper.create_srflx_candidates(
332+
host_cands,
333+
ice_agent.map_to_nat_ip,
334+
ice_agent.local_preferences
335+
)
336+
327337
ice_agent =
328338
Enum.reduce(host_cands, ice_agent, fn host_cand, ice_agent ->
329339
add_local_cand(ice_agent, host_cand)
330340
end)
331341

332-
for %cand_mod{} = cand <- host_cands do
342+
ice_agent =
343+
Enum.reduce(srflx_cands, ice_agent, fn cand, ice_agent ->
344+
# don't pair reflexive candidate, it should be pruned anyway - see sec. 6.1.2.4
345+
put_in(ice_agent.local_cands[cand.base.id], cand)
346+
end)
347+
348+
for %cand_mod{} = cand <- host_cands ++ srflx_cands do
333349
notify(ice_agent.on_new_candidate, {:new_candidate, cand_mod.marshal(cand)})
334350
end
335351

lib/ex_ice/priv/nat_mapper.ex

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
defmodule ExICE.Priv.NATMapper do
2+
@moduledoc false
3+
4+
require Logger
5+
6+
alias ExICE.ICEAgent
7+
alias ExICE.Priv.Candidate
8+
9+
@spec create_srflx_candidates([Candidate.Host.t()], ICEAgent.map_to_nat_ip(), %{
10+
:inet.ip_address() => non_neg_integer()
11+
}) :: [Candidate.Srflx.t()]
12+
def create_srflx_candidates(_host_cands, nil, _local_preferences) do
13+
[]
14+
end
15+
16+
def create_srflx_candidates(host_cands, map_to_nat_ip, local_preferences) do
17+
{cands, _external_ips} =
18+
Enum.reduce(host_cands, {[], []}, fn host_cand, {cands, external_ips} ->
19+
external_ip = map_to_nat_ip.(host_cand.base.address)
20+
21+
if valid_external_ip?(external_ip, host_cand.base.address, external_ips) do
22+
priority =
23+
Candidate.priority!(local_preferences, host_cand.base.address, :srflx)
24+
25+
cand =
26+
Candidate.Srflx.new(
27+
address: external_ip,
28+
port: host_cand.base.port,
29+
base_address: host_cand.base.address,
30+
base_port: host_cand.base.port,
31+
priority: priority,
32+
transport_module: host_cand.base.transport_module,
33+
socket: host_cand.base.socket
34+
)
35+
36+
Logger.debug("New srflx candidate from NAT mapping: #{inspect(cand)}")
37+
38+
{[cand | cands], [external_ip | external_ips]}
39+
else
40+
{cands, external_ips}
41+
end
42+
end)
43+
44+
cands
45+
end
46+
47+
defp valid_external_ip?(external_ip, host_ip, external_ips) do
48+
same_type? = :inet.is_ipv4_address(external_ip) == :inet.is_ipv4_address(host_ip)
49+
50+
cond do
51+
host_ip == external_ip ->
52+
log_warning(host_ip, external_ip, "external IP is the same as local IP")
53+
false
54+
55+
not :inet.is_ip_address(external_ip) or not same_type? ->
56+
log_warning(host_ip, external_ip, "not valid IP address")
57+
false
58+
59+
external_ip in external_ips ->
60+
log_warning(host_ip, external_ip, "address already in use")
61+
false
62+
63+
true ->
64+
true
65+
end
66+
end
67+
68+
defp log_warning(host_ip, external_ip, reason),
69+
do:
70+
Logger.warning(
71+
"Ignoring NAT mapping: #{inspect(host_ip)} to #{inspect(external_ip)}, #{inspect(reason)}"
72+
)
73+
end

test/priv/ice_agent_test.exs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,127 @@ defmodule ExICE.Priv.ICEAgentTest do
26312631
end
26322632
end
26332633

2634+
describe "NAT mapping" do
2635+
alias ExICE.Priv.Candidate
2636+
2637+
@ipv4 {10, 10, 10, 10}
2638+
@ipv6 {0, 0, 0, 0, 0, 0, 0, 1}
2639+
@invalid_ip :invalid_ip
2640+
2641+
test "adds srflx candidate" do
2642+
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @ipv4 end)
2643+
2644+
assert [%Candidate.Srflx{base: %{address: @ipv4}}] = srflx_candidates(ice_agent)
2645+
2646+
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
2647+
assert_receive {:ex_ice, _pid, {:new_candidate, srflx_cand}}
2648+
2649+
assert host_cand =~ "typ host"
2650+
assert srflx_cand =~ "typ srflx"
2651+
end
2652+
2653+
test "creates only one candidate if external ip repeats itself" do
2654+
ice_agent = spawn_ice_agent(IfDiscovery.MockMulti, fn _ip -> @ipv4 end)
2655+
2656+
assert [%Candidate.Srflx{base: %{address: @ipv4}}] = srflx_candidates(ice_agent)
2657+
2658+
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
2659+
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand_2}}
2660+
assert_receive {:ex_ice, _pid, {:new_candidate, srflx_cand}}
2661+
2662+
assert host_cand =~ "typ host"
2663+
assert host_cand_2 =~ "typ host"
2664+
assert srflx_cand =~ "typ srflx"
2665+
end
2666+
2667+
test "ignores one to one mapping" do
2668+
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn ip -> ip end)
2669+
2670+
assert [] == srflx_candidates(ice_agent)
2671+
2672+
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
2673+
refute_receive {:ex_ice, _pid, {:new_candidate, _srflx_cand}}
2674+
2675+
assert host_cand =~ "typ host"
2676+
end
2677+
2678+
test "ignores if ip types is not the same" do
2679+
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @ipv6 end)
2680+
2681+
assert [] == srflx_candidates(ice_agent)
2682+
end
2683+
2684+
test "ignores when function returns nil value" do
2685+
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> nil end)
2686+
2687+
assert [] == srflx_candidates(ice_agent)
2688+
end
2689+
2690+
test "ignores when function returns invalid value" do
2691+
ice_agent = spawn_ice_agent(IfDiscovery.MockSingle, fn _ip -> @invalid_ip end)
2692+
2693+
assert [] == srflx_candidates(ice_agent)
2694+
end
2695+
2696+
test "works with STUN enabled" do
2697+
ice_agent =
2698+
ICEAgent.new(
2699+
controlling_process: self(),
2700+
role: :controlled,
2701+
transport_module: Transport.Mock,
2702+
if_discovery_module: IfDiscovery.MockSingle,
2703+
ice_servers: [%{urls: "stun:192.168.0.3:19302"}],
2704+
map_to_nat_ip: fn _ip -> @ipv4 end
2705+
)
2706+
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
2707+
|> ICEAgent.gather_candidates()
2708+
2709+
[%Candidate.Srflx{base: %{address: @ipv4, port: srflx_port}}] = srflx_candidates(ice_agent)
2710+
2711+
[socket] = ice_agent.sockets
2712+
2713+
# assert no transactions are started until handle_ta_timeout is called
2714+
assert nil == Transport.Mock.recv(socket)
2715+
2716+
# assert ice agent started gathering transaction by sending a binding request
2717+
ice_agent = ICEAgent.handle_ta_timeout(ice_agent)
2718+
assert packet = Transport.Mock.recv(socket)
2719+
assert {:ok, req} = ExSTUN.Message.decode(packet)
2720+
assert req.type.class == :request
2721+
assert req.type.method == :binding
2722+
2723+
resp =
2724+
Message.new(req.transaction_id, %Type{class: :success_response, method: :binding}, [
2725+
%XORMappedAddress{address: @ipv4, port: srflx_port}
2726+
])
2727+
|> Message.encode()
2728+
2729+
ice_agent = ICEAgent.handle_udp(ice_agent, socket, @stun_ip, @stun_port, resp)
2730+
2731+
# assert there isn't new srflx candidate
2732+
assert [%Candidate.Srflx{}] = srflx_candidates(ice_agent)
2733+
end
2734+
2735+
defp spawn_ice_agent(discovery_module, map_to_nat_ip) do
2736+
%ICEAgent{gathering_state: :complete} =
2737+
ICEAgent.new(
2738+
controlling_process: self(),
2739+
role: :controlled,
2740+
transport_module: Transport.Mock,
2741+
if_discovery_module: discovery_module,
2742+
map_to_nat_ip: map_to_nat_ip
2743+
)
2744+
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
2745+
|> ICEAgent.gather_candidates()
2746+
end
2747+
2748+
defp srflx_candidates(ice_agent) do
2749+
ice_agent.local_cands
2750+
|> Map.values()
2751+
|> Enum.filter(&(&1.base.type == :srflx))
2752+
end
2753+
end
2754+
26342755
test "relay ice_transport_policy" do
26352756
ice_agent =
26362757
ICEAgent.new(

0 commit comments

Comments
 (0)