Skip to content

Commit 393849d

Browse files
authored
Fix checklist pruning and adding valid pair (#11)
1 parent 193a38c commit 393849d

File tree

3 files changed

+95
-92
lines changed

3 files changed

+95
-92
lines changed

lib/checklist.ex

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ defmodule ExICE.Checklist do
99

1010
@spec get_next_pair(t()) :: CandidatePair.t()
1111
def get_next_pair(checklist) do
12+
# FIXME correctly handle frozen pairs, according to sec 6.1.4.2
1213
checklist
13-
|> Enum.filter(fn {_id, pair} -> pair.state == :waiting end)
14+
|> Enum.filter(fn {_id, pair} -> pair.state in [:frozen, :waiting] end)
1415
|> Enum.max_by(fn {_id, pair} -> pair.priority end, fn -> {nil, nil} end)
1516
|> elem(1)
1617
end
@@ -52,7 +53,7 @@ defmodule ExICE.Checklist do
5253

5354
@spec waiting?(t()) :: boolean()
5455
def waiting?(checklist) do
55-
Enum.any?(checklist, fn {_id, pair} -> pair.state == :waiting end)
56+
Enum.any?(checklist, fn {_id, pair} -> pair.state in [:frozen, :waiting] end)
5657
end
5758

5859
@spec in_progress?(t()) :: boolean()
@@ -74,42 +75,18 @@ defmodule ExICE.Checklist do
7475

7576
@spec prune(t()) :: t()
7677
def prune(checklist) do
77-
# We sort the checklist as follows:
78-
# * first in flight or done pairs
79-
# * next waiting pairs
80-
# * in both categories sort by priority
81-
# because uniq_by keeps first occurence, we will
82-
# always drop newly added pair if the same pair
83-
# is already in flight or done.
84-
# That's not fully compliant with the RFC 8838
85-
# as sec 10 says:
86-
#
87-
# The agent prunes redundant pairs by following
88-
# the rules in Section 6.1.2.4 of [RFC8445] but
89-
# checks existing pairs only if they have a state
90-
# of Waiting or Frozen;
91-
#
92-
#
93-
# but the code is much easier if we guarantee
94-
# there are no duplicate pairs in the checklist.
95-
# Also, we should always pick pairs with local
96-
# host candidates over local srflx candidates
97-
# so it should be okay?
98-
99-
checklist =
100-
checklist
101-
|> Enum.sort_by(
102-
fn {_id, p} ->
103-
pair_state = if p.state in [:waiting, :frozen], do: 0, else: 1
104-
{pair_state, p.priority}
105-
end,
106-
:desc
107-
)
78+
# This is done according to RFC 8838 sec. 10
79+
{waiting, in_flight_or_done} =
80+
Enum.split_with(checklist, fn {_id, p} -> p.state in [:waiting, :frozen] end)
81+
82+
waiting =
83+
waiting
84+
|> Enum.sort_by(fn {_id, p} -> p.priority end, :desc)
10885
|> Enum.uniq_by(fn {_id, p} ->
10986
{p.local_cand.base_address, p.local_cand.base_port, p.remote_cand}
11087
end)
11188

112-
Map.new(checklist)
89+
Map.new(waiting ++ in_flight_or_done)
11390
end
11491

11592
@spec timeout_pairs(t(), [integer()]) :: t()

lib/ice_agent.ex

Lines changed: 83 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -842,23 +842,51 @@ defmodule ExICE.ICEAgent do
842842
defp handle_gathering_transaction_success_response(_socket, _src_ip, _src_port, msg, state) do
843843
t = Map.fetch!(state.gathering_transactions, msg.transaction_id)
844844

845-
{:ok, %XORMappedAddress{address: address, port: port}} =
845+
{:ok, %XORMappedAddress{address: xor_addr, port: xor_port}} =
846846
Message.get_attribute(msg, XORMappedAddress)
847847

848-
c =
849-
Candidate.new(
850-
:srflx,
851-
address,
852-
port,
853-
t.host_cand.address,
854-
t.host_cand.port,
855-
t.host_cand.socket
856-
)
848+
case find_cand(state.local_cands, xor_addr, xor_port) do
849+
nil ->
850+
c =
851+
Candidate.new(
852+
:srflx,
853+
xor_addr,
854+
xor_port,
855+
t.host_cand.address,
856+
t.host_cand.port,
857+
t.host_cand.socket
858+
)
859+
860+
Logger.debug("New srflx candidate: #{inspect(c)}")
861+
send(state.controlling_process, {:ex_ice, self(), {:new_candidate, Candidate.marshal(c)}})
862+
add_srflx_cand(c, state)
863+
864+
cand ->
865+
Logger.debug("""
866+
Not adding srflx candidate as we already have a candidate with the same address.
867+
Candidate: #{inspect(cand)}
868+
""")
869+
end
870+
|> update_in([:gathering_transactions, t.t_id], fn t -> %{t | state: :complete} end)
871+
end
857872

858-
Logger.debug("New srflx candidate: #{inspect(c)}")
873+
defp handle_gathering_transaction_error_response(_socket, _src_ip, _src_port, msg, state) do
874+
t = Map.fetch!(state.gathering_transactions, msg.transaction_id)
859875

860-
send(state.controlling_process, {:ex_ice, self(), {:new_candidate, Candidate.marshal(c)}})
876+
error_code =
877+
case Message.get_attribute(msg, ErrorCode) do
878+
{:ok, error_code} -> error_code
879+
_other -> nil
880+
end
881+
882+
Logger.debug(
883+
"Gathering transaction failed, t_id: #{msg.transaction_id}, reason: #{inspect(error_code)}"
884+
)
885+
886+
update_in(state, [:gathering_transactions, t.t_id], fn t -> %{t | state: :failed} end)
887+
end
861888

889+
defp add_srflx_cand(c, state) do
862890
# replace address and port with candidate base
863891
# and prune the checklist - see sec. 6.1.2.4
864892
local_cand = %Candidate{c | address: c.base_address, port: c.base_port}
@@ -884,56 +912,15 @@ defmodule ExICE.ICEAgent do
884912
Logger.debug("New candidate pairs: #{inspect(added_pairs)}")
885913
end
886914

887-
state
888-
|> update_in([:local_cands], fn local_cands -> [c | local_cands] end)
889-
|> update_in([:gathering_transactions, t.t_id], fn t -> %{t | state: :complete} end)
890-
|> update_in([:checklist], fn _ -> checklist end)
891-
end
892-
893-
defp handle_gathering_transaction_error_response(_socket, _src_ip, _src_port, msg, state) do
894-
t = Map.fetch!(state.gathering_transactions, msg.transaction_id)
895-
896-
error_code =
897-
case Message.get_attribute(msg, ErrorCode) do
898-
{:ok, error_code} -> error_code
899-
_other -> nil
900-
end
901-
902-
Logger.debug(
903-
"Gathering transaction failed, t_id: #{msg.transaction_id}, reason: #{inspect(error_code)}"
904-
)
905-
906-
update_in(state, [:gathering_transactions, t.t_id], fn t -> %{t | state: :failed} end)
915+
%{state | checklist: checklist, local_cands: [c | state.local_cands]}
907916
end
908917

909918
# Adds valid pair according to sec 7.2.5.3.2
910919
# TODO sec. 7.2.5.3.3
911920
# The agent MUST set the states for all other Frozen candidate pairs in
912921
# all checklists with the same foundation to Waiting.
913-
defp add_valid_pair(
914-
valid_pair,
915-
conn_check_pair,
916-
%CandidatePair{valid?: true} = checklist_pair,
917-
%{role: :controlling} = state
918-
)
919-
when are_pairs_equal(valid_pair, checklist_pair) do
920-
# valid pair is already in the checklist and it is
921-
# marked as valid so this cannot be our first conn check on it -
922-
# this means that nominate? flag has to be set
923-
true = checklist_pair.nominate?
924-
conn_check_pair = %CandidatePair{conn_check_pair | state: :succeeded}
925-
checklist_pair = %CandidatePair{checklist_pair | state: :succeeded}
926-
927-
checklist =
928-
state.checklist
929-
|> Map.replace!(checklist_pair.id, checklist_pair)
930-
|> Map.replace!(conn_check_pair.id, conn_check_pair)
931-
932-
state = %{state | checklist: checklist}
933-
{checklist_pair.id, state}
934-
end
935-
936-
# check against valid_pair == conn_check_pair before
922+
#
923+
# Check against valid_pair == conn_check_pair before
937924
# checking against valid_pair == checklist_pair as
938925
# the second condition is always true if the first one is
939926
defp add_valid_pair(valid_pair, conn_check_pair, _, state)
@@ -957,6 +944,41 @@ defmodule ExICE.ICEAgent do
957944
{conn_check_pair.id, state}
958945
end
959946

947+
defp add_valid_pair(
948+
valid_pair,
949+
conn_check_pair,
950+
%CandidatePair{valid?: true} = checklist_pair,
951+
state
952+
)
953+
when are_pairs_equal(valid_pair, checklist_pair) do
954+
Logger.debug("""
955+
New valid pair: #{checklist_pair.id} \
956+
resulted from conn check on pair: #{conn_check_pair.id} \
957+
but there is already such a pair in the checklist marked as valid.
958+
Should this ever happen after we don't add redundant srflx candidates?
959+
Checklist pair: #{checklist_pair.id}.
960+
""")
961+
962+
# if we get here, don't update discovered_pair_id and succeeded_pair_id of
963+
# the checklist pair as they are already set
964+
conn_check_pair = %CandidatePair{
965+
conn_check_pair
966+
| state: :succeeded,
967+
succeeded_pair_id: conn_check_pair.id,
968+
discovered_pair_id: checklist_pair.id
969+
}
970+
971+
checklist_pair = %CandidatePair{checklist_pair | state: :succeeded}
972+
973+
checklist =
974+
state.checklist
975+
|> Map.replace!(checklist_pair.id, checklist_pair)
976+
|> Map.replace!(conn_check_pair.id, conn_check_pair)
977+
978+
state = %{state | checklist: checklist}
979+
{checklist_pair.id, state}
980+
end
981+
960982
defp add_valid_pair(valid_pair, conn_check_pair, checklist_pair, state)
961983
when are_pairs_equal(valid_pair, checklist_pair) do
962984
Logger.debug("""
@@ -1150,7 +1172,7 @@ defmodule ExICE.ICEAgent do
11501172
%CandidatePair{} = pair ->
11511173
Logger.debug("Trying to nominate pair: #{inspect(pair.id)}")
11521174
pair = %CandidatePair{pair | nominate?: true}
1153-
put_in(state, [:checklist, pair.id], pair)
1175+
state = put_in(state, [:checklist, pair.id], pair)
11541176
state = %{state | nominating?: {true, pair.id}}
11551177
pair = Map.fetch!(state.checklist, pair.succeeded_pair_id)
11561178
pair = %CandidatePair{pair | state: :waiting, nominate?: true}
@@ -1160,7 +1182,10 @@ defmodule ExICE.ICEAgent do
11601182
nil ->
11611183
# TODO revisit this
11621184
# should we check if state.state == :in_progress?
1163-
Logger.debug("No pairs for nomination. ICE failed.")
1185+
Logger.debug("""
1186+
No pairs for nomination. ICE failed. #{inspect(state.checklist, pretty: true)}
1187+
""")
1188+
11641189
change_connection_state(:failed, state)
11651190
end
11661191
end

questions.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
and pruned if redundant - see section 6.1.2.4
66
5. Is it possible to connect ice outside docker with ice inside docker?
77
6. Is it possible for initial pair state to be different than :waiting when we have only one checklist?
8+
Yes, consider scenario where remote peer sends us two the same srflx candidates that differ in ports only. Remote can obtain two srflx candidates when it has multiple local interfaces or has docker bridges on its system. RFC doesnt seem to say we should filter out "redundant" candidates.
89
7. Is it possible not to prune some srflx candidate - sec 6.1.2.4?
910
8. Is it possible to receive binding response to binding request with USE-CANDIDATE that will result in creating
1011
a new valid pair? sec 7.2.5.3.4

0 commit comments

Comments
 (0)