diff --git a/lib/dialer/public.go b/lib/dialer/public.go index d9fd8f11b..8326e5626 100644 --- a/lib/dialer/public.go +++ b/lib/dialer/public.go @@ -83,37 +83,15 @@ func dialContextWithFallback(ctx context.Context, fallback proxy.ContextDialer, return dialerConn{conn, newDialerAddr(network, addr)}, nil } - ctx, cancel := context.WithCancel(ctx) - defer cancel() - var proxyConn, fallbackConn net.Conn - var proxyErr, fallbackErr error - proxyDone := make(chan struct{}) - fallbackDone := make(chan struct{}) - go func() { - proxyConn, proxyErr = dialer.DialContext(ctx, network, addr) - l.Debugf("Dialing proxy result %s %s: %v %v", network, addr, proxyConn, proxyErr) - if proxyErr == nil { - proxyConn = dialerConn{proxyConn, newDialerAddr(network, addr)} + proxyDialFudgeAddress := func(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := dialer.DialContext(ctx, network, addr) + if err != nil { + return nil, err } - close(proxyDone) - }() - go func() { - fallbackConn, fallbackErr = fallback.DialContext(ctx, network, addr) - l.Debugf("Dialing fallback result %s %s: %v %v", network, addr, fallbackConn, fallbackErr) - close(fallbackDone) - }() - <-proxyDone - if proxyErr == nil { - go func() { - <-fallbackDone - if fallbackErr == nil { - _ = fallbackConn.Close() - } - }() - return proxyConn, nil + return dialerConn{conn, newDialerAddr(network, addr)}, err } - <-fallbackDone - return fallbackConn, fallbackErr + + return dialTwicePreferFirst(ctx, proxyDialFudgeAddress, fallback.DialContext, "proxy", "fallback", network, addr) } // DialContext dials via context and/or directly, depending on how it is configured. @@ -125,19 +103,86 @@ func DialContext(ctx context.Context, network, addr string) (net.Conn, error) { // DialContextReusePort tries dialing via proxy if a proxy is configured, and falls back to // a direct connection reusing the port from the connections registry, if no proxy is defined, or connecting via proxy -// fails. If the context has a timeout, the timeout might be applied twice. +// fails. It also in parallel dials without reusing the port, just in case reusing the port affects routing decisions badly. func DialContextReusePort(ctx context.Context, network, addr string) (net.Conn, error) { - dialer := &net.Dialer{ - Control: ReusePortControl, + // If proxy is configured, there is no point trying to reuse listen addresses. + if proxy.FromEnvironment() != proxy.Direct { + return DialContext(ctx, network, addr) } + localAddrInterface := registry.Get(network, tcpAddrLess) - if localAddrInterface != nil { - if addr, ok := localAddrInterface.(*net.TCPAddr); !ok { - return nil, errUnexpectedInterfaceType - } else { - dialer.LocalAddr = addr + if localAddrInterface == nil { + // Nothing listening, nothing to reuse. + return DialContext(ctx, network, addr) + } + + laddr, ok := localAddrInterface.(*net.TCPAddr) + if !ok { + return nil, errUnexpectedInterfaceType + } + + // Dial twice, once reusing the listen address, another time not reusing it, just in case reusing the address + // influences routing and we fail to reach our destination. + dialer := net.Dialer{ + Control: ReusePortControl, + LocalAddr: laddr, + } + return dialTwicePreferFirst(ctx, dialer.DialContext, (&net.Dialer{}).DialContext, "reuse", "non-reuse", network, addr) +} + +type dialFunc func(ctx context.Context, network, address string) (net.Conn, error) + +func dialTwicePreferFirst(ctx context.Context, first, second dialFunc, firstName, secondName, network, address string) (net.Conn, error) { + // Delay second dial by some time. + sleep := time.Second + if deadline, ok := ctx.Deadline(); ok { + timeout := time.Until(deadline) + if timeout > 0 { + sleep = timeout / 3 } } - return dialContextWithFallback(ctx, dialer, network, addr) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + var firstConn, secondConn net.Conn + var firstErr, secondErr error + firstDone := make(chan struct{}) + secondDone := make(chan struct{}) + go func() { + firstConn, firstErr = first(ctx, network, address) + l.Debugf("Dialing %s result %s %s: %v %v", firstName, network, address, firstConn, firstErr) + close(firstDone) + }() + go func() { + select { + case <-firstDone: + if firstErr == nil { + // First succeeded, no point doing anything in second + secondErr = errors.New("didn't dial") + close(secondDone) + return + } + case <-ctx.Done(): + secondErr = ctx.Err() + close(secondDone) + return + case <-time.After(sleep): + } + secondConn, secondErr = second(ctx, network, address) + l.Debugf("Dialing %s result %s %s: %v %v", secondName, network, address, secondConn, secondErr) + close(secondDone) + }() + <-firstDone + if firstErr == nil { + go func() { + <-secondDone + if secondConn != nil { + _ = secondConn.Close() + } + }() + return firstConn, firstErr + } + <-secondDone + return secondConn, secondErr }