internal/http3: document possible issue in Darwin when using dual-stack binding For golang/go#78737 Change-Id: I1112c6ec5fcfe22513b6d80c792806056a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/782080 Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/http3/transport.go b/internal/http3/transport.go index c0a4620..52d9f7c 100644 --- a/internal/http3/transport.go +++ b/internal/http3/transport.go
@@ -106,6 +106,31 @@ func (tr *transport) initEndpoint() (err error) { tr.mu.Lock() defer tr.mu.Unlock() + // This might cause rare issues on Darwin. Unlike Linux, Darwin kernel + // seems to have the following behaviors: + // - After closing a UDP socket, the port that was bound to the socket + // might not be immediately usable again. + // - When doing IPv6 dual-stack binding (e.g., bind to ":0"), it will + // happily bind the IPv6 port, even when the IPv4 port is unavailable. + // + // When both of these are combined, in practice, it is possible for the + // following to happen: + // 1. Transport binds ":0", creating a dual-stack IPv6 UDP socket. + // Everything works as expected. + // 2. At some point, CloseIdleConnections is called and the socket is + // closed. + // 3. Soon after, a new dial is started, and a new dual-stack IPv6 socket + // is coincidentally assigned the same port as the previous socket. + // 4. If the IPv4 port is still unavailable, Darwin's permissive binding + // behavior will cause us to have a socket that silently is unable to + // receive packets on its IPv4 address. + // 5. If the dial target is an IPv4 address, transport will be able to send + // packets to the target, but will be unable to receive its reply. + // + // TransportOpts.ListenQUIC can technically be configured to avoid + // dual-stack binding to avoid this issue, and high socket churn is + // probably uncommon for regular use cases. However, finding a workaround + // for this eventually would be ideal. if tr.endpoint == nil { tr.endpoint, err = tr.listenQUIC(":0", tr.config) }