aiortc / aioice

asyncio-based Interactive Connectivity Establishment (RFC 5245)
BSD 3-Clause "New" or "Revised" License
106 stars 52 forks source link

Fallback to STUN Server if TURN connection failes #16

Open TanjaBayer opened 4 years ago

TanjaBayer commented 4 years ago

Hi,

we have a setup where sometimes a STUN server needs to be used (in a company network) and sometimes a TURN server needs to be used (e.g. mobile Hotspot), so we define a STUN server and a TURN server at the same time.

The problem with this setup is, that if we are in the network which blocks the TURN connection, the whole ICE connection failes, because the TURN creat_turn_endpoint raises an error:

Error handling request
Traceback (most recent call last):
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aiohttp/web_protocol.py", line 418, in start
    resp = await task
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aiohttp/web_app.py", line 458, in _handle
    resp = await handler(request)
  File "/home/tanja/git/cidaas/id-card-utility-backend/server.py", line 104, in offer
    await pc.setLocalDescription(answer)
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aiortc/rtcpeerconnection.py", line 666, in setLocalDescription
    await self.__gather()
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aiortc/rtcpeerconnection.py", line 865, in __gather
    await asyncio.gather(*coros)
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aiortc/rtcicetransport.py", line 174, in gather
    await self._connection.gather_candidates()
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/ice.py", line 362, in gather_candidates
    addresses=addresses)
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/ice.py", line 749, in get_component_candidates
    transport=self.turn_transport)
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/turn.py", line 301, in create_turn_endpoint
    await transport._connect()
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/turn.py", line 272, in _connect
    self.__relayed_address = await self.__inner_protocol.connect()
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/turn.py", line 80, in connect
    response, _ = await self.request(request)
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/turn.py", line 173, in request
    return await transaction.run()
  File "/home/tanja/venvs/cidaas/id-services-aggregator/lib/python3.7/site-packages/aioice/stun.py", line 250, in run
    return await self.__future
aioice.exceptions.TransactionTimeout: STUN transaction timed out

A quick fixe for overcoming this issue was to catch the error and to continue with the other servers: In turn.py create_turn_endpoint I added:

    try:
        await transport._connect()
    except exceptions.TransactionTimeout as e:
        logging.error(e)
        return None, None
    return transport, protocol

And if the protocol is None, the canditate is just not added: In ice.py get_component_candidates

 _, protocol = await turn.create_turn_endpoint(
                lambda: StunProtocol(self),
                server_addr=self.turn_server,
                username=self.turn_username,
                password=self.turn_password,
                ssl=self.turn_ssl,
                transport=self.turn_transport,
            )
            if not protocol is None:
                protocol = cast(StunProtocol, protocol)

                self._protocols.append(protocol)

                # add relayed candidate
                candidate_address = protocol.transport.get_extra_info("sockname")
                related_address = protocol.transport.get_extra_info("related_address")
                protocol.local_candidate = Candidate(
                    foundation=candidate_foundation("relay", "udp", candidate_address[0]),
                    component=component,
                    transport="udp",
                    priority=candidate_priority(component, "relay"),
                    host=candidate_address[0],
                    port=candidate_address[1],
                    type="relay",
                    related_address=related_address[0],
                    related_port=related_address[1],
                )
                candidates.append(protocol.local_candidate)

But in my opinion that is a dirty fix, as we first need to run into an timeout before beeing able to continue. Do you have any ideas on how to implement this in a proper way? I would also be happy to help with that.

lgrahl commented 4 years ago

Linking https://github.com/aiortc/aiortc/issues/232

As briefly explained in that issue, I'd propose to fan out gathering. Pseudocode:

tasks = []
for ice_server in ice_servers:
    tasks.append(gather_srflx(ice_server, timeout=...))
    if ice_server.is_turn:
        tasks.append(create_turn_endpoint(ice_server, timeout=...))
results = asyncio.gather(tasks, return_exceptions=True)
<log exceptions, append resulting candidates, only fail if there are no candidates>
TanjaBayer commented 4 years ago

Thanks for the suggestion. I'll look into how to correctly do that in the code and provide a PR if I manage it