Skip to content

Commit a4f930b

Browse files
authored
Add host_to_srflx_ip_mapper option (#87)
1 parent e92cfcf commit a4f930b

File tree

5 files changed

+295
-3
lines changed

5 files changed

+295
-3
lines changed

lib/ex_ice/ice_agent.ex

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

50+
@typedoc """
51+
Function called for each host candidate to derive a new "fabricated" srflx candidate from it.
52+
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
53+
there should be no srflx one.
54+
55+
Note that each returned IP address must be unique.
56+
If the mapping function repeatedly returns the same address,
57+
it will be ignored, and only one server reflexive candidate will be created.
58+
59+
This function is meant to be used for server implementations where the public addresses are well known.
60+
NAT uses 1 to 1 port mapping and using STUN server for discovering public IP is undesirable
61+
(e.g. because of unknown response time).
62+
"""
63+
@type host_to_srflx_ip_mapper() :: (:inet.ip_address() -> :inet.ip_address() | nil)
64+
5065
@typedoc """
5166
ICE Agent configuration options.
5267
All notifications are by default sent to a process that spawns `ExICE`.
@@ -71,6 +86,9 @@ defmodule ExICE.ICEAgent do
7186
* `on_connection_state_change` - where to send connection state change notifications. Defaults to a process that spawns `ExICE`.
7287
* `on_data` - where to send data. Defaults to a process that spawns `ExICE`.
7388
* `on_new_candidate` - where to send new candidates. Defaults to a process that spawns `ExICE`.
89+
* `host_to_srflx_ip_mapper` - function called for each host candidate to derive a new "fabricated" srflx candidate from it.
90+
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
91+
there should be no srflx one.
7492
"""
7593
@type opts() :: [
7694
role: role() | nil,
@@ -88,7 +106,8 @@ defmodule ExICE.ICEAgent do
88106
on_gathering_state_change: pid() | nil,
89107
on_connection_state_change: pid() | nil,
90108
on_data: pid() | nil,
91-
on_new_candidate: pid() | nil
109+
on_new_candidate: pid() | nil,
110+
host_to_srflx_ip_mapper: host_to_srflx_ip_mapper() | nil
92111
]
93112

94113
@doc """

lib/ex_ice/priv/gatherer.ex

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule ExICE.Priv.Gatherer do
22
@moduledoc false
33

4+
alias ExICE.ICEAgent
45
alias ExICE.Priv.{Candidate, Transport, Utils}
56
alias ExSTUN.Message
67
alias ExSTUN.Message.Type
@@ -139,6 +140,104 @@ defmodule ExICE.Priv.Gatherer do
139140
end
140141
end
141142

143+
@spec fabricate_srflx_candidates([Candidate.Host.t()], ICEAgent.host_to_srflx_ip_mapper(), %{
144+
:inet.ip_address() => non_neg_integer()
145+
}) :: [Candidate.Srflx.t()]
146+
def fabricate_srflx_candidates(_host_cands, nil, _local_preferences) do
147+
[]
148+
end
149+
150+
def fabricate_srflx_candidates(host_cands, host_to_srflx_ip_mapper, local_preferences) do
151+
do_fabricate_srflx_candidates(
152+
host_cands,
153+
host_to_srflx_ip_mapper,
154+
local_preferences,
155+
[],
156+
[]
157+
)
158+
end
159+
160+
defp do_fabricate_srflx_candidates(
161+
[],
162+
_host_to_srflx_ip_mapper,
163+
_local_preferences,
164+
srflx_cands,
165+
_external_ips
166+
) do
167+
srflx_cands
168+
end
169+
170+
defp do_fabricate_srflx_candidates(
171+
[host_cand | rest],
172+
host_to_srflx_ip_mapper,
173+
local_preferences,
174+
srflx_cands,
175+
external_ips
176+
) do
177+
external_ip = host_to_srflx_ip_mapper.(host_cand.base.address)
178+
179+
if valid_external_ip?(external_ip, host_cand.base.address, external_ips) do
180+
priority =
181+
Candidate.priority!(local_preferences, host_cand.base.address, :srflx)
182+
183+
cand =
184+
Candidate.Srflx.new(
185+
address: external_ip,
186+
port: host_cand.base.port,
187+
base_address: host_cand.base.address,
188+
base_port: host_cand.base.port,
189+
priority: priority,
190+
transport_module: host_cand.base.transport_module,
191+
socket: host_cand.base.socket
192+
)
193+
194+
Logger.debug("New srflx candidate from NAT mapping: #{inspect(cand)}")
195+
196+
do_fabricate_srflx_candidates(
197+
rest,
198+
host_to_srflx_ip_mapper,
199+
local_preferences,
200+
[cand | srflx_cands],
201+
[external_ip | external_ips]
202+
)
203+
else
204+
do_fabricate_srflx_candidates(
205+
rest,
206+
host_to_srflx_ip_mapper,
207+
local_preferences,
208+
srflx_cands,
209+
external_ips
210+
)
211+
end
212+
end
213+
214+
defp valid_external_ip?(external_ip, host_ip, external_ips) do
215+
same_type? = :inet.is_ipv4_address(external_ip) == :inet.is_ipv4_address(host_ip)
216+
217+
cond do
218+
host_ip == external_ip ->
219+
log_warning(host_ip, external_ip, "external IP is the same as local IP")
220+
false
221+
222+
not :inet.is_ip_address(external_ip) or not same_type? ->
223+
log_warning(host_ip, external_ip, "not valid IP address")
224+
false
225+
226+
external_ip in external_ips ->
227+
log_warning(host_ip, external_ip, "address already in use")
228+
false
229+
230+
true ->
231+
true
232+
end
233+
end
234+
235+
defp log_warning(host_ip, external_ip, reason),
236+
do:
237+
Logger.warning(
238+
"Ignoring NAT mapping: #{inspect(host_ip)} to #{inspect(external_ip)}, #{inspect(reason)}"
239+
)
240+
142241
defp loopback_if?({_int_name, int}) do
143242
:loopback in int[:flags]
144243
end

lib/ex_ice/priv/ice_agent.ex

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ defmodule ExICE.Priv.ICEAgent do
9090
stun_servers: [],
9191
turn_servers: [],
9292
resolved_turn_servers: [],
93+
host_to_srflx_ip_mapper: nil,
9394
# stats
9495
bytes_sent: 0,
9596
bytes_received: 0,
@@ -165,7 +166,8 @@ defmodule ExICE.Priv.ICEAgent do
165166
local_ufrag: local_ufrag,
166167
local_pwd: local_pwd,
167168
stun_servers: stun_servers,
168-
turn_servers: turn_servers
169+
turn_servers: turn_servers,
170+
host_to_srflx_ip_mapper: opts[:host_to_srflx_ip_mapper]
169171
}
170172
end
171173

@@ -324,12 +326,25 @@ defmodule ExICE.Priv.ICEAgent do
324326

325327
ice_agent = %__MODULE__{ice_agent | local_preferences: local_preferences}
326328

329+
srflx_cands =
330+
Gatherer.fabricate_srflx_candidates(
331+
host_cands,
332+
ice_agent.host_to_srflx_ip_mapper,
333+
ice_agent.local_preferences
334+
)
335+
327336
ice_agent =
328337
Enum.reduce(host_cands, ice_agent, fn host_cand, ice_agent ->
329338
add_local_cand(ice_agent, host_cand)
330339
end)
331340

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

test/priv/gatherer_test.exs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,90 @@ defmodule ExICE.Priv.GathererTest do
100100
assert port in port_range
101101
end
102102
end
103+
104+
describe "host to prefabricated srflx mapper" do
105+
@ipv4 {10, 10, 10, 10}
106+
@ipv6 {64_512, 0, 0, 0, 0, 0, 0, 1}
107+
@invalid_ip :invalid_ip
108+
109+
@ipv4_filter &:inet.is_ipv4_address(&1)
110+
111+
test "adds srflx candidate" do
112+
ip_filter = fn
113+
{192, 168, 0, 2} -> false
114+
_ -> true
115+
end
116+
117+
{local_preferences, host_cands} = setup_gatherer(ip_filter)
118+
119+
assert [%Candidate.Srflx{base: %{address: @ipv4}}] =
120+
Gatherer.fabricate_srflx_candidates(
121+
host_cands,
122+
fn _ip -> @ipv4 end,
123+
local_preferences
124+
)
125+
end
126+
127+
test "creates only one candidate if external ip is repeated" do
128+
{local_preferences, host_cands} = setup_gatherer(@ipv4_filter)
129+
130+
assert [%Candidate.Srflx{base: %{address: @ipv4}}] =
131+
Gatherer.fabricate_srflx_candidates(
132+
host_cands,
133+
fn _ip -> @ipv4 end,
134+
local_preferences
135+
)
136+
end
137+
138+
test "ignores one to one mapping" do
139+
{local_preferences, host_cands} = setup_gatherer(@ipv4_filter)
140+
141+
assert [] ==
142+
Gatherer.fabricate_srflx_candidates(
143+
host_cands,
144+
fn ip -> ip end,
145+
local_preferences
146+
)
147+
end
148+
149+
test "ignores if ip types is not the same" do
150+
{local_preferences, host_cands} = setup_gatherer(@ipv4_filter)
151+
152+
assert [] ==
153+
Gatherer.fabricate_srflx_candidates(
154+
host_cands,
155+
fn _ip -> @ipv6 end,
156+
local_preferences
157+
)
158+
end
159+
160+
test "ignores when function returns nil value" do
161+
{local_preferences, host_cands} = setup_gatherer(@ipv4_filter)
162+
163+
assert [] ==
164+
Gatherer.fabricate_srflx_candidates(
165+
host_cands,
166+
fn _ip -> nil end,
167+
local_preferences
168+
)
169+
end
170+
171+
test "ignores when function returns invalid value" do
172+
{local_preferences, host_cands} = setup_gatherer(@ipv4_filter)
173+
174+
assert [] ==
175+
Gatherer.fabricate_srflx_candidates(
176+
host_cands,
177+
fn _ip -> @invalid_ip end,
178+
local_preferences
179+
)
180+
end
181+
182+
defp setup_gatherer(ip_filter) do
183+
gatherer = Gatherer.new(IfDiscovery.Mock, Transport.Mock, ip_filter, [0])
184+
assert {:ok, sockets} = Gatherer.open_sockets(gatherer)
185+
186+
Gatherer.gather_host_candidates(gatherer, %{}, sockets)
187+
end
188+
end
103189
end

test/priv/ice_agent_test.exs

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

2634+
describe "host to prefabricated srflx mapper" do
2635+
alias ExICE.Priv.Candidate
2636+
2637+
@ipv4 {10, 10, 10, 10}
2638+
2639+
test "adds srflx candidate" do
2640+
ice_agent =
2641+
%ICEAgent{gathering_state: :complete} =
2642+
ICEAgent.new(
2643+
controlling_process: self(),
2644+
role: :controlled,
2645+
transport_module: Transport.Mock,
2646+
if_discovery_module: IfDiscovery.MockSingle,
2647+
host_to_srflx_ip_mapper: fn _ip -> @ipv4 end
2648+
)
2649+
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
2650+
|> ICEAgent.gather_candidates()
2651+
2652+
assert [%Candidate.Srflx{base: %{address: @ipv4}}] = srflx_candidates(ice_agent)
2653+
2654+
assert_receive {:ex_ice, _pid, {:new_candidate, host_cand}}
2655+
assert_receive {:ex_ice, _pid, {:new_candidate, srflx_cand}}
2656+
2657+
assert host_cand =~ "typ host"
2658+
assert srflx_cand =~ "typ srflx"
2659+
end
2660+
2661+
test "works with STUN enabled" do
2662+
ice_agent =
2663+
ICEAgent.new(
2664+
controlling_process: self(),
2665+
role: :controlled,
2666+
transport_module: Transport.Mock,
2667+
if_discovery_module: IfDiscovery.MockSingle,
2668+
ice_servers: [%{urls: "stun:192.168.0.3:19302"}],
2669+
host_to_srflx_ip_mapper: fn _ip -> @ipv4 end
2670+
)
2671+
|> ICEAgent.set_remote_credentials("remoteufrag", "remotepwd")
2672+
|> ICEAgent.gather_candidates()
2673+
2674+
[%Candidate.Srflx{base: %{address: @ipv4, port: srflx_port}}] = srflx_candidates(ice_agent)
2675+
2676+
[socket] = ice_agent.sockets
2677+
2678+
# assert no transactions are started until handle_ta_timeout is called
2679+
assert nil == Transport.Mock.recv(socket)
2680+
2681+
# assert ice agent started gathering transaction by sending a binding request
2682+
ice_agent = ICEAgent.handle_ta_timeout(ice_agent)
2683+
assert packet = Transport.Mock.recv(socket)
2684+
assert {:ok, req} = ExSTUN.Message.decode(packet)
2685+
assert req.type.class == :request
2686+
assert req.type.method == :binding
2687+
2688+
resp =
2689+
Message.new(req.transaction_id, %Type{class: :success_response, method: :binding}, [
2690+
%XORMappedAddress{address: @ipv4, port: srflx_port}
2691+
])
2692+
|> Message.encode()
2693+
2694+
ice_agent = ICEAgent.handle_udp(ice_agent, socket, @stun_ip, @stun_port, resp)
2695+
2696+
# assert there isn't new srflx candidate
2697+
assert [%Candidate.Srflx{}] = srflx_candidates(ice_agent)
2698+
end
2699+
2700+
defp srflx_candidates(ice_agent) do
2701+
ice_agent.local_cands
2702+
|> Map.values()
2703+
|> Enum.filter(&(&1.base.type == :srflx))
2704+
end
2705+
end
2706+
26342707
test "relay ice_transport_policy" do
26352708
ice_agent =
26362709
ICEAgent.new(

0 commit comments

Comments
 (0)