@ -46,6 +46,7 @@ import (
"tailscale.com/net/sockstats"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/tstime"
"tailscale.com/util/multierr"
"tailscale.com/util/multierr"
@ -497,11 +498,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
tr . DisableCompression = true
tr . DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the
// (mis)use httptrace to extract the underlying net.Conn from the
// transport. We make exactly 1 request using this transport, so
// transport. The transport handles 101 Switching Protocols correctly,
// there will be exactly 1 GotConn call. Additionally, the
// such that the Conn will not be reused or kept alive by the transport
// transport handles 101 Switching Protocols correctly, such that
// once the response has been handed back from RoundTrip.
// the Conn will not be reused or kept alive by the transport once
// the response has been handed back from RoundTrip.
//
//
// In theory, the machinery of net/http should make it such that
// In theory, the machinery of net/http should make it such that
// the trace callback happens-before we get the response, but
// the trace callback happens-before we get the response, but
@ -517,10 +516,16 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
// unexpected EOFs...), and we're bound to forget someday and
// unexpected EOFs...), and we're bound to forget someday and
// introduce a protocol optimization at a higher level that starts
// introduce a protocol optimization at a higher level that starts
// eagerly transmitting from the server.
// eagerly transmitting from the server.
connCh := make ( chan net . Conn , 1 )
var lastConn syncs . AtomicValue [ net . Conn ]
trace := httptrace . ClientTrace {
trace := httptrace . ClientTrace {
// Even though we only make a single HTTP request which should
// require a single connection, the context (with the attached
// trace configuration) might be used by our custom dialer to
// make other HTTP requests (e.g. BootstrapDNS). We only care
// about the last connection made, which should be the one to
// the control server.
GotConn : func ( info httptrace . GotConnInfo ) {
GotConn : func ( info httptrace . GotConnInfo ) {
connCh <- info . Conn
lastConn. Store ( info . Conn )
} ,
} ,
}
}
ctx = httptrace . WithClientTrace ( ctx , & trace )
ctx = httptrace . WithClientTrace ( ctx , & trace )
@ -548,11 +553,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
// is still a read buffer attached to it within resp.Body. So, we
// is still a read buffer attached to it within resp.Body. So, we
// must direct I/O through resp.Body, but we can still use the
// must direct I/O through resp.Body, but we can still use the
// underlying net.Conn for stuff like deadlines.
// underlying net.Conn for stuff like deadlines.
var switchedConn net . Conn
switchedConn := lastConn . Load ( )
select {
case switchedConn = <- connCh :
default :
}
if switchedConn == nil {
if switchedConn == nil {
resp . Body . Close ( )
resp . Body . Close ( )
return nil , fmt . Errorf ( "httptrace didn't provide a connection" )
return nil , fmt . Errorf ( "httptrace didn't provide a connection" )