Skip to content

Commit a110e16

Browse files
authored
Add support for aggressive nomination in controlling agent (#73)
1 parent b58acee commit a110e16

16 files changed

+494
-65
lines changed

lib/ex_ice/candidate.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ defmodule ExICE.Candidate do
9393
@spec new(type(), Keyword.t()) :: t()
9494
def new(type, config) when type in [:host, :srflx, :prflx, :relay] do
9595
transport = Keyword.get(config, :transport, :udp)
96-
97-
priority = config[:priority] || ExICE.Priv.Candidate.priority(type)
9896
address = Keyword.fetch!(config, :address)
9997

10098
%__MODULE__{
@@ -104,7 +102,7 @@ defmodule ExICE.Candidate do
104102
base_port: config[:base_port],
105103
foundation: ExICE.Priv.Candidate.foundation(type, address, nil, transport),
106104
port: Keyword.fetch!(config, :port),
107-
priority: priority,
105+
priority: Keyword.fetch!(config, :priority),
108106
transport: transport,
109107
type: type
110108
}

lib/ex_ice/ice_agent.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ defmodule ExICE.ICEAgent do
6161
* `ice_transport_policy` - candidate types to be used.
6262
* `all` - all ICE candidates will be considered (default).
6363
* `relay` - only relay candidates will be considered.
64+
* `aggressive_nomination` - whether to use aggressive nomination from RFC 5245.
65+
ExICE aims to implement RFC 8445, which removes aggressive nomination.
66+
In particular, RFC 8445 assumes that data can be sent on any valid pair (there is no need for nomination).
67+
While this behavior is supported by most of the implementations, some of them still require
68+
a pair to be nominated by the controlling agent before they can start sending data.
69+
Defaults to false.
6470
* `on_gathering_state_change` - where to send gathering state change notifications. Defaults to a process that spawns `ExICE`.
6571
* `on_connection_state_change` - where to send connection state change notifications. Defaults to a process that spawns `ExICE`.
6672
* `on_data` - where to send data. Defaults to a process that spawns `ExICE`.
@@ -78,6 +84,7 @@ defmodule ExICE.ICEAgent do
7884
}
7985
],
8086
ice_transport_policy: :all | :relay,
87+
aggressive_nomination: boolean(),
8188
on_gathering_state_change: pid() | nil,
8289
on_connection_state_change: pid() | nil,
8390
on_data: pid() | nil,

lib/ex_ice/priv/candidate.ex

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,25 @@ defmodule ExICE.Priv.Candidate do
2727
@callback send_data(t(), :inet.ip_address(), :inet.port_number(), binary()) ::
2828
{:ok, t()} | {:error, term(), t()}
2929

30-
@spec priority(type()) :: integer()
31-
def priority(type) do
30+
@spec priority!(%{:inet.ip_address() => non_neg_integer()}, :inet.ip_address(), type()) ::
31+
non_neg_integer()
32+
def priority!(local_preferences, base_address, type) do
33+
local_preference = Map.fetch!(local_preferences, base_address)
34+
do_priority(local_preference, type)
35+
end
36+
37+
@spec priority(%{:inet.ip_address() => non_neg_integer()}, :inet.ip_address(), type()) ::
38+
{%{:inet.ip_address() => non_neg_integer()}, non_neg_integer()}
39+
def priority(local_preferences, base_address, type) do
40+
local_preference =
41+
Map.get(local_preferences, base_address) || generate_local_preference(local_preferences)
42+
43+
local_preferences = Map.put(local_preferences, base_address, local_preference)
44+
45+
{local_preferences, do_priority(local_preference, type)}
46+
end
47+
48+
defp do_priority(local_preference, type) do
3249
type_preference =
3350
case type do
3451
:host -> 126
@@ -37,14 +54,25 @@ defmodule ExICE.Priv.Candidate do
3754
:relay -> 0
3855
end
3956

40-
# That's not fully correct as according to RFC 8445 sec. 5.1.2.1 we should:
41-
# * use value of 65535 when there is only one IP address
42-
# * use different values when there are multiple IP addresses
43-
local_preference = 65_535
44-
4557
2 ** 24 * type_preference + 2 ** 8 * local_preference + 2 ** 0 * (256 - 1)
4658
end
4759

60+
defp generate_local_preference(local_preferences, attempts \\ 200)
61+
62+
defp generate_local_preference(_local_preferences, 0),
63+
do: raise("Couldn't generate local preference")
64+
65+
defp generate_local_preference(local_preferences, attempts) do
66+
# this should give us a number from 0 to 2**16-1
67+
<<pref::16>> = :crypto.strong_rand_bytes(2)
68+
69+
if Map.has_key?(local_preferences, pref) do
70+
generate_local_preference(local_preferences, attempts - 1)
71+
else
72+
pref
73+
end
74+
end
75+
4876
@spec foundation(type(), :inet.ip_address() | String.t(), :inet.ip_address() | nil, atom()) ::
4977
integer()
5078
def foundation(type, ip, stun_turn_ip, transport) do

lib/ex_ice/priv/candidate_base.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,14 @@ defmodule ExICE.Priv.CandidateBase do
3535
transport = :udp
3636
address = Keyword.fetch!(config, :address)
3737

38-
priority = config[:priority] || Candidate.priority(type)
39-
4038
%__MODULE__{
4139
id: Utils.id(),
4240
address: address,
4341
base_address: config[:base_address],
4442
base_port: config[:base_port],
4543
foundation: Candidate.foundation(type, address, nil, transport),
4644
port: Keyword.fetch!(config, :port),
47-
priority: priority,
45+
priority: Keyword.fetch!(config, :priority),
4846
transport: transport,
4947
transport_module: Keyword.get(config, :transport_module, ExICE.Priv.Transport.UDP),
5048
socket: Keyword.fetch!(config, :socket),

lib/ex_ice/priv/conn_check_handler/controlled.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,12 @@ defmodule ExICE.Priv.ConnCheckHandler.Controlled do
203203

204204
%ICEAgent{ice_agent | selected_pair_id: pair.id}
205205
else
206+
Logger.debug("Not selecting a new pair as it has lower priority.")
206207
ice_agent
207208
end
208209

209210
true ->
210-
Logger.debug("Not selecting a new pair as it has lower priority or has the same id")
211+
Logger.debug("Not selecting a new pair as it has the same id")
211212
ice_agent
212213
end
213214
end

lib/ex_ice/priv/conn_check_handler/controlling.ex

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,51 @@ defmodule ExICE.Priv.ConnCheckHandler.Controlling do
9797
def update_nominated_flag(ice_agent, _pair_id, false), do: ice_agent
9898

9999
@impl true
100-
def update_nominated_flag(%ICEAgent{eoc: true} = ice_agent, pair_id, true) do
100+
def update_nominated_flag(
101+
%ICEAgent{eoc: eoc, aggressive_nomination: aggressive_nomination} = ice_agent,
102+
pair_id,
103+
true
104+
)
105+
when (aggressive_nomination == false and eoc == true) or aggressive_nomination == true do
101106
Logger.debug("Nomination succeeded. Selecting pair: #{inspect(pair_id)}")
102107

103108
pair = Map.fetch!(ice_agent.checklist, pair_id)
104109
pair = %CandidatePair{pair | nominate?: false, nominated?: true}
105110
ice_agent = put_in(ice_agent.checklist[pair.id], pair)
106111

112+
ice_agent =
113+
cond do
114+
ice_agent.selected_pair_id == nil ->
115+
Logger.debug("Selecting pair: #{pair_id}")
116+
%ICEAgent{ice_agent | selected_pair_id: pair.id}
117+
118+
ice_agent.selected_pair_id != nil and pair.id != ice_agent.selected_pair_id ->
119+
selected_pair = Map.fetch!(ice_agent.checklist, ice_agent.selected_pair_id)
120+
121+
if pair.priority >= selected_pair.priority do
122+
Logger.debug("""
123+
Selecting new pair with higher priority. \
124+
New pair: #{pair_id}, old pair: #{ice_agent.selected_pair_id}.\
125+
""")
126+
127+
%ICEAgent{ice_agent | selected_pair_id: pair.id}
128+
else
129+
Logger.debug("Not selecting a new pair as it has lower priority.")
130+
ice_agent
131+
end
132+
133+
true ->
134+
Logger.debug("Not selecting a new pair as it has the same id")
135+
ice_agent
136+
end
137+
107138
# the controlling agent could nominate only when eoc was set
108139
# and checklist finished
109-
unless Checklist.finished?(ice_agent.checklist) do
140+
if not ice_agent.aggressive_nomination and not Checklist.finished?(ice_agent.checklist) do
110141
Logger.warning("Nomination succeeded but checklist hasn't finished.")
111142
end
112143

113-
ice_agent = %ICEAgent{ice_agent | nominating?: {false, nil}, selected_pair_id: pair.id}
114-
ICEAgent.change_connection_state(ice_agent, :completed)
144+
%ICEAgent{ice_agent | nominating?: {false, nil}}
115145
end
116146

117147
defp resolve_pair(ice_agent, pair) do

lib/ex_ice/priv/gatherer.ex

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,17 @@ defmodule ExICE.Priv.Gatherer do
8484
end)
8585
end
8686

87-
@spec gather_host_candidates(t(), [Transport.socket()]) :: [Candidate.t()]
88-
def gather_host_candidates(gatherer, sockets) do
89-
Enum.map(sockets, &create_new_host_candidate(gatherer, &1))
87+
@spec gather_host_candidates(t(), %{:inet.ip_address() => non_neg_integer()}, [
88+
Transport.socket()
89+
]) :: [Candidate.t()]
90+
def gather_host_candidates(gatherer, local_preferences, sockets) do
91+
{local_preferences, cands} =
92+
Enum.reduce(sockets, {local_preferences, []}, fn socket, {local_preferences, cands} ->
93+
{local_preferences, cand} = create_new_host_candidate(gatherer, local_preferences, socket)
94+
{local_preferences, [cand | cands]}
95+
end)
96+
97+
{local_preferences, Enum.reverse(cands)}
9098
end
9199

92100
@spec gather_srflx_candidate(t(), integer(), Transport.socket(), ExSTUN.URI.t()) ::
@@ -155,21 +163,24 @@ defmodule ExICE.Priv.Gatherer do
155163
Keyword.get_values(int, :addr)
156164
end
157165

158-
defp create_new_host_candidate(gatherer, socket) do
166+
defp create_new_host_candidate(gatherer, local_preferences, socket) do
159167
{:ok, {sock_ip, sock_port}} = gatherer.transport_module.sockname(socket)
160168

169+
{local_preferences, priority} = Candidate.priority(local_preferences, sock_ip, :host)
170+
161171
cand =
162172
Candidate.Host.new(
163173
address: sock_ip,
164174
port: sock_port,
165175
base_address: sock_ip,
166176
base_port: sock_port,
177+
priority: priority,
167178
transport_module: gatherer.transport_module,
168179
socket: socket
169180
)
170181

171182
Logger.debug("New candidate: #{inspect(cand)}")
172183

173-
cand
184+
{local_preferences, cand}
174185
end
175186
end

0 commit comments

Comments
 (0)