aiortc / aioice

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

Finer local candidate address selection #2

Open sebastianriese opened 6 years ago

sebastianriese commented 6 years ago

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:

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

jlaine commented 6 years ago

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 commented 6 years ago

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

sebastianriese commented 6 years ago

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.

twisteroidambassador commented 6 years ago

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:

jlaine commented 6 years ago

@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 commented 6 years ago

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:

twisteroidambassador commented 6 years ago

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

jlaine commented 6 years ago

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 commented 6 years ago

Also of interest:

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

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

twisteroidambassador commented 6 years ago

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

jlaine commented 6 years ago

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.