Skip to content

Commit 977359f

Browse files
authored
Fix role conflict resolution (#71)
1 parent 8689e71 commit 977359f

File tree

4 files changed

+174
-16
lines changed

4 files changed

+174
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ as WebRTC multiplexes traffic on a single socket but PRs are welcomed
3030
```elixir
3131
def deps do
3232
[
33-
{:ex_ice, "~> 0.10.0"}
33+
{:ex_ice, "~> 0.10.1"}
3434
]
3535
end
3636
```

lib/ex_ice/priv/ice_agent.ex

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,7 +1521,10 @@ defmodule ExICE.Priv.ICEAgent do
15211521

15221522
## BINDING RESPONSE HANDLING ##
15231523
defp handle_conn_check_response(ice_agent, local_cand, src_ip, src_port, msg) do
1524-
{%{pair_id: pair_id}, conn_checks} = Map.pop!(ice_agent.conn_checks, msg.transaction_id)
1524+
{%{pair_id: pair_id, raw_req: raw_req}, conn_checks} =
1525+
Map.pop!(ice_agent.conn_checks, msg.transaction_id)
1526+
1527+
{:ok, req} = Message.decode(raw_req)
15251528
ice_agent = %__MODULE__{ice_agent | conn_checks: conn_checks}
15261529
conn_check_pair = Map.fetch!(ice_agent.checklist, pair_id)
15271530

@@ -1539,7 +1542,7 @@ defmodule ExICE.Priv.ICEAgent do
15391542
if symmetric?(ice_agent, local_cand.base.socket, {src_ip, src_port}, conn_check_pair) do
15401543
case msg.type.class do
15411544
:success_response -> handle_conn_check_success_response(ice_agent, conn_check_pair, msg)
1542-
:error_response -> handle_conn_check_error_response(ice_agent, conn_check_pair, msg)
1545+
:error_response -> handle_conn_check_error_response(ice_agent, conn_check_pair, req, msg)
15431546
end
15441547
else
15451548
cc_local_cand = Map.fetch!(ice_agent.local_cands, conn_check_pair.local_cand_id)
@@ -1609,14 +1612,14 @@ defmodule ExICE.Priv.ICEAgent do
16091612
end
16101613
end
16111614

1612-
defp handle_conn_check_error_response(ice_agent, conn_check_pair, msg) do
1615+
defp handle_conn_check_error_response(ice_agent, conn_check_pair, req, resp) do
16131616
# We only authenticate role conflict as it changes our state.
16141617
# We don't add message-integrity to bad request and unauthenticated errors
16151618
# so we also don't expect to receive it.
16161619
# In the worst case scenario, we won't allow for the connection.
1617-
case Message.get_attribute(msg, ErrorCode) do
1620+
case Message.get_attribute(resp, ErrorCode) do
16181621
{:ok, %ErrorCode{code: 487}} ->
1619-
handle_role_conflict_error_response(ice_agent, conn_check_pair, msg)
1622+
handle_role_conflict_error_response(ice_agent, conn_check_pair, req, resp)
16201623

16211624
other ->
16221625
Logger.debug(
@@ -1634,15 +1637,39 @@ defmodule ExICE.Priv.ICEAgent do
16341637
end
16351638
end
16361639

1637-
defp handle_role_conflict_error_response(ice_agent, conn_check_pair, msg) do
1638-
case authenticate_msg(msg, ice_agent.remote_pwd) do
1640+
defp handle_role_conflict_error_response(ice_agent, conn_check_pair, req, resp) do
1641+
case authenticate_msg(resp, ice_agent.remote_pwd) do
16391642
:ok ->
1640-
new_role = if ice_agent.role == :controlling, do: :controlled, else: :controlling
1643+
{:ok, role} = get_role_attribute(req)
16411644

1642-
Logger.debug("""
1643-
Conn check failed due to role conflict. Changing our role to: #{new_role}, \
1644-
recomputing pair priorities, regenerating tiebreaker and rescheduling conn check \
1645-
""")
1645+
ice_agent =
1646+
case {role, ice_agent.role} do
1647+
{%ICEControlled{}, :controlling} ->
1648+
# seems that we've already switched
1649+
ice_agent
1650+
1651+
{%ICEControlling{}, :controlled} ->
1652+
# seems that we've already switched
1653+
ice_agent
1654+
1655+
_ ->
1656+
new_role = if ice_agent.role == :controlling, do: :controlled, else: :controlling
1657+
1658+
Logger.debug("""
1659+
Conn check failed due to role conflict. Changing our role to: #{new_role}, \
1660+
recomputing pair priorities, regenerating tiebreaker and rescheduling conn check \
1661+
""")
1662+
1663+
tiebreaker = generate_tiebreaker()
1664+
checklist = recompute_pair_prios(ice_agent)
1665+
1666+
%__MODULE__{
1667+
ice_agent
1668+
| role: new_role,
1669+
checklist: checklist,
1670+
tiebreaker: tiebreaker
1671+
}
1672+
end
16461673

16471674
conn_check_pair = %CandidatePair{
16481675
conn_check_pair
@@ -1651,8 +1678,8 @@ defmodule ExICE.Priv.ICEAgent do
16511678
}
16521679

16531680
checklist = Map.replace!(ice_agent.checklist, conn_check_pair.id, conn_check_pair)
1654-
tiebreaker = generate_tiebreaker()
1655-
%__MODULE__{ice_agent | role: new_role, checklist: checklist, tiebreaker: tiebreaker}
1681+
1682+
%__MODULE__{ice_agent | checklist: checklist}
16561683

16571684
{:error, reason} ->
16581685
Logger.debug(

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule ExICE.MixProject do
22
use Mix.Project
33

4-
@version "0.10.0"
4+
@version "0.10.1"
55
@source_url "https://github.com/elixir-webrtc/ex_ice"
66

77
def project do

test/priv/ice_agent_test.exs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule ExICE.Priv.ICEAgentTest do
6363
end
6464

6565
@remote_cand ExICE.Candidate.new(:host, address: {192, 168, 0, 2}, port: 8445)
66+
@remote_cand2 ExICE.Candidate.new(:host, address: {192, 168, 0, 3}, port: 8445)
6667

6768
describe "unmarshal_remote_candidate/1" do
6869
test "with correct candidate" do
@@ -644,6 +645,62 @@ defmodule ExICE.Priv.ICEAgentTest do
644645
assert_bad_request_error_response(socket, request)
645646
end
646647

648+
test "with role conflict", %{ice_agent: ice_agent, remote_cand: remote_cand} do
649+
[socket] = ice_agent.sockets
650+
651+
binding_request = fn tiebreaker ->
652+
Message.new(%Type{class: :request, method: :binding}, [
653+
%Username{value: "#{ice_agent.local_ufrag}:someufrag"},
654+
%Priority{priority: 1234},
655+
%ICEControlling{tiebreaker: tiebreaker},
656+
%UseCandidate{}
657+
])
658+
|> Message.with_integrity(ice_agent.local_pwd)
659+
|> Message.with_fingerprint()
660+
end
661+
662+
# feed binding request with higher tiebreaker
663+
request = binding_request.(ice_agent.tiebreaker + 1)
664+
raw_request = Message.encode(request)
665+
666+
new_ice_agent =
667+
ICEAgent.handle_udp(
668+
ice_agent,
669+
socket,
670+
remote_cand.address,
671+
remote_cand.port,
672+
raw_request
673+
)
674+
675+
# agent should switch its role and send success response
676+
assert new_ice_agent.role == :controlled
677+
assert new_ice_agent.tiebreaker == ice_agent.tiebreaker
678+
assert packet = Transport.Mock.recv(socket)
679+
assert {:ok, msg} = ExSTUN.Message.decode(packet)
680+
assert msg.type == %ExSTUN.Message.Type{class: :success_response, method: :binding}
681+
682+
# feed binding request with smaller tiebreaker
683+
request = binding_request.(ice_agent.tiebreaker - 1)
684+
raw_request = Message.encode(request)
685+
686+
new_ice_agent =
687+
ICEAgent.handle_udp(
688+
ice_agent,
689+
socket,
690+
remote_cand.address,
691+
remote_cand.port,
692+
raw_request
693+
)
694+
695+
# agent shouldn't switch its role and should send 487 error response
696+
assert new_ice_agent.role == :controlling
697+
assert new_ice_agent.tiebreaker == ice_agent.tiebreaker
698+
assert packet = Transport.Mock.recv(socket)
699+
assert {:ok, msg} = ExSTUN.Message.decode(packet)
700+
assert msg.type == %ExSTUN.Message.Type{class: :error_response, method: :binding}
701+
assert {:ok, %ErrorCode{code: 487, reason: ""}} = Message.get_attribute(msg, ErrorCode)
702+
end
703+
647704
test "without username", %{ice_agent: ice_agent, remote_cand: remote_cand} do
648705
[socket] = ice_agent.sockets
649706

@@ -1157,6 +1214,80 @@ defmodule ExICE.Priv.ICEAgentTest do
11571214
assert new_pair.responses_received == pair.responses_received + 1
11581215
end
11591216

1217+
test "role conflict error response" do
1218+
ice_agent =
1219+
ICEAgent.new(
1220+
controlling_process: self(),
1221+
role: :controlling,
1222+
if_discovery_module: IfDiscovery.Mock,
1223+
transport_module: Transport.Mock
1224+
)
1225+
|> ICEAgent.set_remote_credentials("someufrag", "somepwd")
1226+
|> ICEAgent.gather_candidates()
1227+
|> ICEAgent.add_remote_candidate(@remote_cand)
1228+
1229+
[socket] = ice_agent.sockets
1230+
1231+
# trigger check
1232+
ice_agent = ICEAgent.handle_ta_timeout(ice_agent)
1233+
[pair1] = Map.values(ice_agent.checklist)
1234+
req1 = read_binding_request(socket, ice_agent.remote_pwd)
1235+
1236+
# Add the second candidate and trigger another check.
1237+
# We add candidate after generating the first check to be sure
1238+
# it is related to @remote_cand2.
1239+
ice_agent = ICEAgent.add_remote_candidate(ice_agent, @remote_cand2)
1240+
ice_agent = ICEAgent.handle_ta_timeout(ice_agent)
1241+
[pair2] = Map.values(ice_agent.checklist) |> Enum.reject(&(&1.id == pair1.id))
1242+
req2 = read_binding_request(socket, ice_agent.remote_pwd)
1243+
1244+
# reply to the first check with role conflict error response
1245+
resp =
1246+
Message.new(req1.transaction_id, %Type{class: :error_response, method: :binding}, [
1247+
%ErrorCode{code: 487}
1248+
])
1249+
|> Message.with_integrity(ice_agent.remote_pwd)
1250+
|> Message.with_fingerprint()
1251+
|> Message.encode()
1252+
1253+
new_ice_agent =
1254+
ICEAgent.handle_udp(
1255+
ice_agent,
1256+
socket,
1257+
@remote_cand.address,
1258+
@remote_cand.port,
1259+
resp
1260+
)
1261+
1262+
# assert that the agent changed its role and tiebreaker
1263+
assert new_ice_agent.role != ice_agent.role
1264+
assert new_ice_agent.tiebreaker != ice_agent.tiebreaker
1265+
assert Map.fetch!(new_ice_agent.checklist, pair1.id).state == :waiting
1266+
1267+
# reply to the second check with role conflict error response
1268+
resp =
1269+
Message.new(req2.transaction_id, %Type{class: :error_response, method: :binding}, [
1270+
%ErrorCode{code: 487}
1271+
])
1272+
|> Message.with_integrity(new_ice_agent.remote_pwd)
1273+
|> Message.with_fingerprint()
1274+
|> Message.encode()
1275+
1276+
new_ice_agent2 =
1277+
ICEAgent.handle_udp(
1278+
new_ice_agent,
1279+
socket,
1280+
@remote_cand2.address,
1281+
@remote_cand2.port,
1282+
resp
1283+
)
1284+
1285+
# assert that agent didn't switch its role and tiebreaker
1286+
assert new_ice_agent2.role == new_ice_agent.role
1287+
assert new_ice_agent2.tiebreaker == new_ice_agent.tiebreaker
1288+
assert Map.fetch!(new_ice_agent2.checklist, pair2.id).state == :waiting
1289+
end
1290+
11601291
test "unauthenticated error response", %{ice_agent: ice_agent, remote_cand: remote_cand} do
11611292
[socket] = ice_agent.sockets
11621293
[pair] = Map.values(ice_agent.checklist)

0 commit comments

Comments
 (0)