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

swarm: use happy eyeballs ranking for TCP dials #2573

Merged
merged 9 commits into from
Oct 19, 2023
45 changes: 45 additions & 0 deletions core/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package transport
import (
"context"
"errors"
"fmt"
"net"

"github.com/libp2p/go-libp2p/core/network"
Expand Down Expand Up @@ -124,3 +125,47 @@ type Upgrader interface {
// Upgrade upgrades the multiaddr/net connection into a full libp2p-transport connection.
Upgrade(ctx context.Context, t Transport, maconn manet.Conn, dir network.Direction, p peer.ID, scope network.ConnManagementScope) (CapableConn, error)
}

// DialUpdater provides updates on in progress dials.
type DialUpdater interface {
// DialWithUpdates dials a remote peer and provides updates on the passed channel.
DialWithUpdates(context.Context, ma.Multiaddr, peer.ID, chan<- DialUpdate) (CapableConn, error)
}

// DialUpdateKind indicates the type of DialUpdate event.
type DialUpdateKind int

const (
// UpdateKindDialFailed indicates dial failed.
UpdateKindDialFailed DialUpdateKind = iota
// UpdateKindDialSuccessful indicates dial succeeded.
UpdateKindDialSuccessful
// UpdateKindHandshakeProgressed indicates successful completion of the TCP 3-way
// handshake
UpdateKindHandshakeProgressed
)

func (k DialUpdateKind) String() string {
switch k {
case UpdateKindDialFailed:
return "DialFailed"
case UpdateKindDialSuccessful:
return "DialSuccessful"
case UpdateKindHandshakeProgressed:
return "UpdateKindHandshakeProgressed"
default:
return fmt.Sprintf("DialUpdateKind<Unknown-%d>", k)
}
}

// DialUpdate is used by DialUpdater to provide dial updates.
type DialUpdate struct {
// Kind is the kind of update event.
Kind DialUpdateKind
// Addr is the peer's address.
Addr ma.Multiaddr
// Conn is the resulting connection on success.
Conn CapableConn
// Err is the reason for dial failure.
Err error
}
105 changes: 84 additions & 21 deletions p2p/net/swarm/dial_ranker.go
sukunrt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,19 @@ func NoDelayDialRanker(addrs []ma.Multiaddr) []network.AddrDelay {
// 3. If a QUIC or WebTransport address is present, TCP addresses dials are delayed relative to the last QUIC dial:
// We prefer to end up with a QUIC connection. For public addresses, the delay introduced is 250ms (PublicTCPDelay),
// and for private addresses 30ms (PrivateTCPDelay).
// 4. For the TCP addresses we follow a strategy similar to QUIC with an optimisation for handling the long TCP
// handshake time described in 6. If both IPv6 TCP and IPv4 TCP addresses are present, we do a Happy Eyeballs
// style ranking. First dial the IPv6 TCP address with the lowest port. After this, dial the IPv4 TCP address
// with the lowest port delayed by 250ms (PublicTCPDelay) for public addresses, and 30ms (PrivateTCPDelay)
// for local addresses. After this we dial all the rest of the addresses delayed by 250ms (PublicTCPDelay) for
// public addresses, and 30ms (PrivateTCPDelay) for local addresses.
// 5. If only one of TCP IPv6 or TCP IPv4 addresses are present, dial the TCP address with the lowest port
// first. After this we dial the rest of the TCP addresses delayed by 250ms (PublicTCPDelay) for public
// addresses, and 30ms (PrivateTCPDelay) for local addresses.
// 6. When a TCP socket is connected and awaiting security and muxer upgrade, we stop new dials for 2*PrivateTCPDelay
// to allow for the upgrade to complete.
//
// We dial lowest ports first for QUIC addresses as they are more likely to be the listen port.
// We dial lowest ports first as they are more likely to be the listen port.
func DefaultDialRanker(addrs []ma.Multiaddr) []network.AddrDelay {
relay, addrs := filterAddrs(addrs, isRelayAddr)
pvt, addrs := filterAddrs(addrs, manet.IsPrivateAddr)
Expand Down Expand Up @@ -88,48 +99,100 @@ func DefaultDialRanker(addrs []ma.Multiaddr) []network.AddrDelay {
// addresses relative to direct addresses.
func getAddrDelay(addrs []ma.Multiaddr, tcpDelay time.Duration, quicDelay time.Duration,
offset time.Duration) []network.AddrDelay {
if len(addrs) == 0 {
return nil
}

sort.Slice(addrs, func(i, j int) bool { return score(addrs[i]) < score(addrs[j]) })

// If the first address is (QUIC, IPv6), make the second address (QUIC, IPv4).
happyEyeballs := false
if len(addrs) > 0 {
// addrs is now sorted by (Transport, IPVersion). Reorder addrs for happy eyeballs dialing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this sorting logic be part of score?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that'd be more readable. For this ranking we need to look at other addresses, so an IPv4 address gets higher score than other IPv6 addresses only if there are some IPv6 addresses present. I think the present implementation makes the logic explicit.

// For QUIC and TCP, if we have both IPv6 and IPv4 addresses, move the
// highest priority IPv4 address to the second position.
happyEyeballsQUIC := false
happyEyeballsTCP := false
// tcpStartIdx is the index of the first TCP Address
var tcpStartIdx int
{
i := 0
// If the first QUIC address is IPv6 move the first QUIC IPv4 address to second position
if isQUICAddr(addrs[0]) && isProtocolAddr(addrs[0], ma.P_IP6) {
for i := 1; i < len(addrs); i++ {
if isQUICAddr(addrs[i]) && isProtocolAddr(addrs[i], ma.P_IP4) {
// make IPv4 address the second element
if i > 1 {
a := addrs[i]
copy(addrs[2:], addrs[1:i])
for j := 1; j < len(addrs); j++ {
if isQUICAddr(addrs[j]) && isProtocolAddr(addrs[j], ma.P_IP4) {
// The first IPv4 address is at position j
// Move the jth element at position 1 shifting the affected elements
if j > 1 {
a := addrs[j]
copy(addrs[2:], addrs[1:j])
addrs[1] = a
}
happyEyeballs = true
happyEyeballsQUIC = true
i = j + 1
break
}
}
}

for tcpStartIdx = i; tcpStartIdx < len(addrs); tcpStartIdx++ {
if isProtocolAddr(addrs[tcpStartIdx], ma.P_TCP) {
break
}
}

// If the first TCP address is IPv6 move the first TCP IPv4 address to second position
if tcpStartIdx < len(addrs) && isProtocolAddr(addrs[tcpStartIdx], ma.P_IP6) {
for j := tcpStartIdx + 1; j < len(addrs); j++ {
if isProtocolAddr(addrs[j], ma.P_TCP) && isProtocolAddr(addrs[j], ma.P_IP4) {
// First TCP IPv4 address is at position j, move it to position tcpStartIdx+1
// which is the second priority TCP address
if j > tcpStartIdx+1 {
a := addrs[j]
copy(addrs[tcpStartIdx+2:], addrs[tcpStartIdx+1:j])
addrs[tcpStartIdx+1] = a
}
happyEyeballsTCP = true
break
}
}
}
}

res := make([]network.AddrDelay, 0, len(addrs))

var totalTCPDelay time.Duration
var tcpFirstDialDelay time.Duration
for i, addr := range addrs {
var delay time.Duration
switch {
case isQUICAddr(addr):
// For QUIC addresses we dial an IPv6 address, then after quicDelay an IPv4
// address, then after quicDelay we dial rest of the addresses.
sukunrt marked this conversation as resolved.
Show resolved Hide resolved
// We dial an IPv6 address, then after quicDelay an IPv4
// address, then after a further quicDelay we dial the rest of the addresses.
if i == 1 {
delay = quicDelay
}
if i > 1 && happyEyeballs {
delay = 2 * quicDelay
} else if i > 1 {
delay = quicDelay
if i > 1 {
// If we have happy eyeballs for QUIC, dials after the second position
// will be delayed by 2*quicDelay
if happyEyeballsQUIC {
delay = 2 * quicDelay
} else {
delay = quicDelay
}
}
totalTCPDelay = delay + tcpDelay
tcpFirstDialDelay = delay + tcpDelay
case isProtocolAddr(addr, ma.P_TCP):
delay = totalTCPDelay
// We dial an IPv6 address, then after tcpDelay an IPv4
// address, then after a further tcpDelay we dial the rest of the addresses.
if i == tcpStartIdx+1 {
delay = tcpDelay
}
if i > tcpStartIdx+1 {
// If we have happy eyeballs for TCP, dials after the second position
// will be delayed by 2*tcpDelay
if happyEyeballsTCP {
delay = 2 * tcpDelay
} else {
delay = tcpDelay
}
}
delay += tcpFirstDialDelay
}
res = append(res, network.AddrDelay{Addr: addr, Delay: offset + delay})
}
Expand Down
44 changes: 36 additions & 8 deletions p2p/net/swarm/dial_ranker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func TestDelayRankerTCPDelay(t *testing.T) {
t1 := ma.StringCast("/ip4/1.2.3.5/tcp/1/")
t1v6 := ma.StringCast("/ip6/1::2/tcp/1")
t2 := ma.StringCast("/ip4/1.2.3.4/tcp/2")
t3 := ma.StringCast("/ip4/1.2.3.4/tcp/3")

testCase := []struct {
name string
Expand All @@ -169,36 +170,63 @@ func TestDelayRankerTCPDelay(t *testing.T) {
}{
{
name: "quic-with-tcp-ip6-ip4",
addrs: []ma.Multiaddr{q1v1, q1v16, q2v16, q3v16, q2v1, t1, t2},
addrs: []ma.Multiaddr{q1v1, q1v16, q2v16, q3v16, q2v1, t1, t1v6, t2, t3},
output: []network.AddrDelay{
{Addr: q1v16, Delay: 0},
{Addr: q1v1, Delay: PublicQUICDelay},
{Addr: q2v16, Delay: 2 * PublicQUICDelay},
{Addr: q3v16, Delay: 2 * PublicQUICDelay},
{Addr: q2v1, Delay: 2 * PublicQUICDelay},
{Addr: t1, Delay: 3 * PublicQUICDelay},
{Addr: t2, Delay: 3 * PublicQUICDelay},
{Addr: t1v6, Delay: 3 * PublicQUICDelay},
{Addr: t1, Delay: 4 * PublicQUICDelay},
{Addr: t2, Delay: 5 * PublicQUICDelay},
{Addr: t3, Delay: 5 * PublicQUICDelay},
},
},
{
name: "quic-ip4-with-tcp",
addrs: []ma.Multiaddr{q1v1, t1, t2, t1v6},
addrs: []ma.Multiaddr{q1v1, t2, t1v6, t1},
output: []network.AddrDelay{
{Addr: q1v1, Delay: 0},
{Addr: t1v6, Delay: PublicQUICDelay},
{Addr: t1, Delay: 2 * PublicQUICDelay},
{Addr: t2, Delay: 3 * PublicQUICDelay},
},
},
{
name: "quic-ip4-with-tcp-ipv4",
addrs: []ma.Multiaddr{q1v1, t2, t3, t1},
output: []network.AddrDelay{
{Addr: q1v1, Delay: 0},
{Addr: t1, Delay: PublicTCPDelay},
{Addr: t2, Delay: PublicTCPDelay},
{Addr: t2, Delay: 2 * PublicQUICDelay},
{Addr: t3, Delay: 2 * PublicTCPDelay},
},
},
{
name: "quic-ip4-with-two-tcp",
addrs: []ma.Multiaddr{q1v1, t1v6, t2},
output: []network.AddrDelay{
{Addr: q1v1, Delay: 0},
{Addr: t1v6, Delay: PublicTCPDelay},
{Addr: t2, Delay: 2 * PublicTCPDelay},
},
},
{
name: "tcp-ip4-ip6",
addrs: []ma.Multiaddr{t1, t2, t1v6},
addrs: []ma.Multiaddr{t1, t2, t1v6, t3},
output: []network.AddrDelay{
{Addr: t1v6, Delay: 0},
{Addr: t1, Delay: 0},
{Addr: t2, Delay: 0},
{Addr: t1, Delay: PublicTCPDelay},
{Addr: t2, Delay: 2 * PublicTCPDelay},
{Addr: t3, Delay: 2 * PublicTCPDelay},
},
},
{
name: "empty",
addrs: []ma.Multiaddr{},
output: []network.AddrDelay{},
},
}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading