Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http3: client always prefers IPv4 #3755

Open
kgersen opened this issue Apr 3, 2023 · 4 comments · May be fixed by #3770
Open

http3: client always prefers IPv4 #3755

kgersen opened this issue Apr 3, 2023 · 4 comments · May be fixed by #3770

Comments

@kgersen
Copy link

kgersen commented Apr 3, 2023

Because of a bug of ResolveUDPAddr, when trying to connect to a dual stacked host (= host with both ipv6 and ipv4 addresses), quic-go client always uses IPv4 instead of prefering IPv6 if available.
net/http (both versions 1.x and 2.0) doesn't behave like that. It prefers IPv6 if available.

As a workaround we're forced to explicitly resolve names before using quic-go http3 client.

demo of the issue: https://github.com/kgersen/h3dial
running on a dual stack machine:

QUIC-GO:
  connected to 172.67.9.235:443
  got status:  200 OK
  proto:  HTTP/3.0
net/http:
  connected to  [2606:4700:10::6816:926]:443
  got status:  200 OK
  proto:  HTTP/2.0
@marten-seemann
Copy link
Member

Am I right to assume that this is due to this line?

quic-go/http3/roundtrip.go

Lines 270 to 272 in c9ae152

func (r *RoundTripper) makeDialer() func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
return func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)

How does net/http get around the bug that you linked?

@kgersen
Copy link
Author

kgersen commented Apr 3, 2023

afaik net/http doesn't use ResolveUDPAddr but Dial/DialContext
I have not looked much further.

@marten-seemann
Copy link
Member

Very interesting. I've played around with the h3dial a bit, thank you for providing such a great example code!

Using net.DialUDP doesn't work, as it requires a net.UDPAddr, and we'd have to call net.ResolveUDPAddr to resolve the address. Using net.Dial("udp", addr) seems to fix the problem though. I'll add a comment to the code, since this is not really obvious.


This makes me wonder how this actually supposed to work though. DNS resolution will (in general) return a list of A and AAAA records. It seems like net.Dial now picks the first-best (?) AAAA record, whereas net.ResolveUDPAddr picks the A record. But what if that address doesn't work? net.Dial falls back to the other addresses when using TCP, but since UDP doesn't have a handshake, this doesn't work for UDP.
It seems like we'd have to replicate that kind of behavior on the QUIC layer, but there's no way to get access to the list of DNS records.

@kgersen
Copy link
Author

kgersen commented Apr 13, 2023

The problem is forResolve which states "IPv4 is preferred, unless addr contains an IPv6 literal".
In the Go std lib, it is used by 3 functions: ResolveUDPAddr, ResolveUDPAddr and ResolveIPAddr. So as long as they don't fix forResolve, don't use any of these 3 functions (or any code that depend on these).

The Happy Eyeballs used by net/http (FallbackDelay field of net.Dialer and default opt-in of 300ms) is, imho, a thing of the past and shouldn't be used in quic-go. It's from the early days of IPv6. Nowadays one can assume that if IPv6 works then it works ...Having redundancy with IPv4 should be opt-in (that's only my opinion, others may disagree) not opt-out like it is now in net/http.

The QUIC layer should not be aware of anything DNS since it could be used for DNS traffic.

The HTTP/3 layer should use eventually the new DNS records (HTTPS RRs) but that's is unrelated to this IPv4/IPv6 issue.

The good way is to assume, given a target host.domain with both AAAA and A addresses then use the machine policy ( see https://www.rfc-editor.org/rfc/rfc6724#section-10.3 ) which is GUA IPv6 first then IPv4 then ULA IPv6. It's in the stdlib ( https://cs.opensource.google/go/go/+/master:src/net/addrselect.go) and used by the Go resolver (at least on Unix systems).

It's 2023 we should safely assume that machines with 'broken or misconfigured' IPv6 stack should fail and the code, by default, should not try to work by falling back to IPv4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants