ninenines / gun

HTTP/1.1, HTTP/2, Websocket client (and more) for Erlang/OTP.
ISC License
891 stars 232 forks source link

Implement RFC8305 Happy Eyeballs for dual stack IPv4 and IPv6 hosts #188

Open nroi opened 5 years ago

nroi commented 5 years ago

I've been looking at different Erlang HTTP client libraries, and haven't found a single one which supports happy eyeballs. Using IPv6 exclusively is still not an option since many hosts don't support it yet, and using IPv4 exclusively is also less than ideal: Many ISPs don't even give out new IPv4s to new customers and instead implement something like "Dual Stack Lite", which means that IPv6 works more reliably than IPv4 for those customers.

Some links:

essen commented 5 years ago

This can be implemented on top of Gun, so I would suggest working on a reference implementation and then we can see about including it directly in Gun.

essen commented 4 years ago

Looked a bit more and it should be done in gun:open or in a new function wrapping it. First try IPv6 and then after a timeout try IPv4 and then picks up the first one that's up. Then select this one and return the info to the client so it can use the same on future attempts. Or maybe use an ets table for Gun to manage it on its own.

Not sure this implements the whole of RFC6555 but that's a start. Would love a proof of concept PR.

nroi commented 4 years ago

Keep in mind that RFC6555 has been obsoleted RFC8305. The algorithm is more complex than what you described, but I agree that an easier implementation of dual stack support is also good to have.

I've started to implement a work-in-progress sort-of prototype of RFC8305 in Elixir (https://github.com/nroi/eyepatch), it aims to be usable for many HTTP client libraries and I have tested it with hackney. The project is far from done, but I would like to share some insights:

I just glossed over the test directory in gun and it seems that gun currently doesn't have integration tests where connections and DNS resolutions are made to an external server, is that correct? Because that would be another argument for implementing just a simple version of dual stack support: adding many untested lines of code for a feature that is more of a nice-to-have than a necessity doesn't seem like a good idea.

essen commented 4 years ago

Gun just uses gen_tcp/inet which has those DNS related tests and I suspect a proper implementation of this RFC should sit there.

Or at least replicate a lot of the connect functionality, even more than I did in current Gun master which now separates domain lookup from connect and TLS handshake. Basically we could extend domain lookup to get all the records the RFC requires and then extend the connect to eventually try a second connection and do so asynchronously but that's quite a large amount of work so I was just thinking it'd be far easier to do it in front of Gun.

And because of how gen_tcp/inet is structured, it'd be far far easier to just try on inet6 first and after a timeout try inet (while leaving inet6 trying to connect) and just pick the first winner. Even if it doesn't implement the RFC fully. And it's unclear to me how much better a full implementation of the RFC would be compared to just doing this. Probably not much.

essen commented 4 years ago

By the way, related, gen_tcp:connect might itself benefit from sorting IP addresses, I've seen it with a [{127,0,0,1},{127,0,0,1}] list, implying it attempts to connect twice in some cases.

I'm also curious how the RFC behaves on Windows. In Erlang's case gen_tcp:connect will end up trying to connect 3 times to the given IP, and that is a Windows behavior. Don't know if that can be disabled without tweaking the registry.

essen commented 4 years ago

In case anyone really needing this comes upon this ticket, I have no plans to implement it at this time but would be willing to for a paid customer. Please see https://ninenines.eu/services/ for general information about that.

dch commented 3 years ago

I finally needed this, and it turns out @stolen has already implemented it years ago at https://github.com/yandex/inet64_tcp so I forked it, tidied it up for rebar3, and it works as promised. The magic bits are in here - https://github.com/skunkwerks/inet64_tcp/blob/main/src/inet64_tcp.erl#L72-L96

essen commented 3 years ago

I think happy eyeballs you must do queries concurrently, and then connects concurrently as well (after some wait time). First connect wins.

There's also https://github.com/hrefhref/gun/commit/c5d577e4b88b6a401ea15111d81240fb674b6717 as an attempt to deal with both 4 and 6.