aioice icon indicating copy to clipboard operation
aioice copied to clipboard

Finer local candidate address selection

Open sebastianriese opened this issue 6 years ago • 11 comments

Currently all addresses on all interfaces are automatically selected as candidates by gather_candidates and the loopback device is only discarded based on the addresses bound to it. While this does work, it may not be desirable for all users, who for example have redundant links where one link is unfit for bulk transfers of data negotiated via ICE (e.g. a volume-limited mobile link). Other addresses should perhaps be ignored from the start because they have no routes to the public internet (e.g. a VPN link to some intranet). This information is not available at the level of aioice but may be available to an application (e.g. via configuration or tight integration with the target system).

Some kind of interface with finer granularity for selecting candidates would enable the use of such information. Probably, one could simply manually modify or set Connection.local_candidates to achieve this, but this feels wrong, since I would consider the member variable private by default. If this is the preferred interface, this should be documented (and the issue closed).

I would argue for additional methods for setting the local candidates:

  • by explicitly adding IPs,
  • by specifying interfaces whose addresses to add,
  • ...

I would implement the changes, but wanted to discuss the preferred API first.

sebastianriese avatar Mar 14 '18 16:03 sebastianriese

It feels as though this could be achieved with some optional keyword arguments to gather_candidates. The IPv4 / Ipv6 flags which are currently in the constructor would for example be better off as arguments to gather_candidates.

I'm not too fond of the idea of explicitly adding IP addresses, but restricting the interfaces (either as a whitelist or a blacklist) sounds interesting.

jlaine avatar Mar 14 '18 18:03 jlaine

Maybe we could have a look a what existing ICE libraries (like nice or pjnath) to see how they handle it

jlaine avatar Mar 14 '18 18:03 jlaine

Yes, explicitly listing IPs also seems not quite right (and can be achieved anyway by setting local candidates manually, if an application really needs to do this). Using a keyword argument for restricting interfaces sounds very convincing to me.

sebastianriese avatar Mar 14 '18 20:03 sebastianriese

This might also be implemented "backwards", i.e. on each candidate expose which interface it came from, and provide a way to delete candidates from a Connection. The workflow is then:

  • call gather_candidates()
  • iterate through all candidates, inspect their source interface, delete candidates not needed
  • call connect()

twisteroidambassador avatar Jun 26 '18 13:06 twisteroidambassador

@twisteroidambassador I'm not sure that's the right way to go, I think it will complicate adding "full trickle" ICE support, i.e. gathering candidates in an asynchronous fashion.

FYI we currently support "half trickle", i.e. we gather all local candidates in one go, but you can add remote candidates using Connection.add_remote_candidate.

jlaine avatar Jun 26 '18 14:06 jlaine

By the way I'm still waiting for someone to provide some "prior art" on how other ICE libraries handle local candidate selection. So far the options I see involve additional keyword arguments:

  • adding an interface keyword argument, like interfaces=['eth0', 'eth1']

  • adding a candidate_filter keyword argument which would take a callable that returns True or False when given a candidate. While this option gives a lot of flexibility, ICE Candidates don't carry the explicit name of the network interface, so if you just want to restrict which network interfaces are used, it will be tricky.

jlaine avatar Jun 26 '18 14:06 jlaine

Based on a brief look at their documentations (and I can't say I'm fluent in C), pjnath does not seem to provide a way to selectively include host candidates. libnice can either discover all addresses automatically, or use a list of IP addresses explicitly provided.

Arguably, the goal of ICE is to discover possible network routes and establish a connection without prior knowledge of the network topology and available interfaces / addresses, so the use case of fine-grained host candidate selection is debatable: addresses that have no connectivity to the peer / internet will naturally be ignored.

On the other hand, there are privacy implications of telling the peer all your local IP addresses, especially with WebRTC. An IETF draft discusses this problem, and provides several modes of behavior that may be worthy of implementation: https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-09

twisteroidambassador avatar Jun 27 '18 02:06 twisteroidambassador

Thanks @twisteroidambassador that draft is very interesting.

As far as I understand mode 2 could be implemented with a restriction on which interfaces are picked up, though I'm not too sure what "default route" means if you have both ipv4 and ipv6 addresses.

Mode 3 is more problematic as it means discarding the host candidates after gathering the server reflexive candidates, which doesn't play nice with trickle ICE..

jlaine avatar Jun 27 '18 08:06 jlaine

Also of interest:

https://github.com/jitsi/ice4j/blob/master/doc/configuration.md

https://wiki.mozilla.org/Media/WebRTC/Privacy

jlaine avatar Jun 27 '18 09:06 jlaine

The "default route interface" can be identified by examining the routing table, or perhaps more often, by creating a datagram socket, connecting to an Internet address (no actual packet is sent), and examining the resultant bind address (i.e. getsockname). https://github.com/twisteroidambassador/aiostun/blob/2d1c13218d231603ff7754a4ea03f53fdcd64537/aiostun/ip.py#L20

In the context of ICE, the IP address of the STUN / TURN server can be used, assuming they are on the Internet. In case of a dual-stack host, perhaps do the same thing with both an IPv4 and a n IPv6 address.

When Chrome does something like Mode 3, they bind to 0.0.0.0 and collect server reflexive / relay candidates from there. https://www.w3.org/2011/04/webrtc/wiki/images/d/da/WebRTC_IP_Address_Privacy.pdf

Also interesting, libnice only use one datagram socket (bound to 0.0.0.0) per component, shared among all candidates related to the component. https://github.com/libnice/libnice/blob/master/docs/design.txt

twisteroidambassador avatar Jun 27 '18 14:06 twisteroidambassador

Creating the datagram socket to determine the default route is a neat trick, will keep that in mind.

Binding to 0.0.0.0 is something I've tried previously but I seem to recall ending up with strange results depending on the IPv4/IPv6 mixture.

jlaine avatar Jun 27 '18 22:06 jlaine