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: cleanup address filtering logic #2333

Merged
merged 1 commit into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 4 additions & 61 deletions p2p/net/swarm/dial_ranker.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package swarm

import (
"net/netip"
"sort"
"strconv"
"time"
Expand Down Expand Up @@ -53,16 +52,8 @@ func noDelayRanker(addrs []ma.Multiaddr) []network.AddrDelay {
// The correct solution is to detect this situation, and not attempt to dial IPv6 addresses at all.
// IPv6 blackhole detection is tracked in https://github.com/libp2p/go-libp2p/issues/1605.
//
// Within each group (private, public IPv4, public IPv6, relay addresses) we apply the following logic:
//
// 1. Filter out addresses we don't want to dial:
// 1. If a /quic-v1 address is present, filter out /quic and /webtransport address on the same 2-tuple:
// QUIC v1 is preferred over the deprecated QUIC draft-29, and given the choice, we prefer using
// raw QUIC over using WebTransport.
// 2. If a /tcp address is present, filter out /ws or /wss addresses on the same 2-tuple:
// We prefer using raw TCP over using WebSocket.
//
// 2. Rank addresses:
// Within each group (private, public IPv4, public IPv6, relay addresses) we apply the following
// ranking logic:
//
// 1. If two QUIC addresses are present, dial the QUIC address with the lowest port first:
// This is more likely to be the listen port. After this we dial the rest of the QUIC addresses delayed by
Expand Down Expand Up @@ -99,49 +90,11 @@ 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 {

// First make a map of QUICV1 and TCP AddrPorts.
quicV1Addr := make(map[netip.AddrPort]struct{})
tcpAddr := make(map[netip.AddrPort]struct{})
for _, a := range addrs {
switch {
case isProtocolAddr(a, ma.P_WEBTRANSPORT):
case isProtocolAddr(a, ma.P_QUIC_V1):
quicV1Addr[addrPort(a, ma.P_UDP)] = struct{}{}
case isProtocolAddr(a, ma.P_WS) || isProtocolAddr(a, ma.P_WSS):
case isProtocolAddr(a, ma.P_TCP):
tcpAddr[addrPort(a, ma.P_TCP)] = struct{}{}
}
}

// Filter addresses we are sure we don't want to dial
selectedAddrs := addrs
i := 0
for _, a := range addrs {
switch {
// If a QUICDraft29 or webtransport address is reachable, QUIC-v1 will also be reachable. So we
// drop the QUICDraft29 or webtransport address
// We prefer QUIC-v1 over the older QUIC-draft29 address.
// We prefer QUIC-v1 over webtransport as it is more performant.
case isProtocolAddr(a, ma.P_WEBTRANSPORT) || isProtocolAddr(a, ma.P_QUIC):
if _, ok := quicV1Addr[addrPort(a, ma.P_UDP)]; ok {
continue
}
// If a ws address is reachable, TCP will also be reachable and it'll be more performant
case isProtocolAddr(a, ma.P_WS) || isProtocolAddr(a, ma.P_WSS):
if _, ok := tcpAddr[addrPort(a, ma.P_TCP)]; ok {
continue
}
}
selectedAddrs[i] = a
i++
}
selectedAddrs = selectedAddrs[:i]
sort.Slice(selectedAddrs, func(i, j int) bool { return score(selectedAddrs[i]) < score(selectedAddrs[j]) })
sort.Slice(addrs, func(i, j int) bool { return score(addrs[i]) < score(addrs[j]) })

res := make([]network.AddrDelay, 0, len(addrs))
quicCount := 0
for _, a := range selectedAddrs {
for _, a := range addrs {
delay := offset
switch {
case isProtocolAddr(a, ma.P_QUIC) || isProtocolAddr(a, ma.P_QUIC_V1):
Expand Down Expand Up @@ -192,16 +145,6 @@ func score(a ma.Multiaddr) int {
return (1 << 30)
}

// addrPort returns the ip and port for a. p should be either ma.P_TCP or ma.P_UDP.
// a must be an (ip, TCP) or (ip, udp) address.
func addrPort(a ma.Multiaddr, p int) netip.AddrPort {
ip, _ := manet.ToIP(a)
port, _ := a.ValueForProtocol(p)
pi, _ := strconv.Atoi(port)
addr, _ := netip.AddrFromSlice(ip)
return netip.AddrPortFrom(addr, uint16(pi))
}

func isProtocolAddr(a ma.Multiaddr, p int) bool {
found := false
ma.ForEach(a, func(c ma.Component) bool {
Expand Down
20 changes: 15 additions & 5 deletions p2p/net/swarm/dial_ranker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,26 @@ func TestNoDelayRanker(t *testing.T) {
q3 := ma.StringCast("/ip4/1.2.3.4/udp/3/quic")
q3v1 := ma.StringCast("/ip4/1.2.3.4/udp/3/quic-v1")
q4 := ma.StringCast("/ip4/1.2.3.4/udp/4/quic")
t1 := ma.StringCast("/ip4/1.2.3.5/tcp/1/")

testCase := []struct {
name string
addrs []ma.Multiaddr
output []network.AddrDelay
}{
{
name: "quic+webtransport filtered when quicv1",
addrs: []ma.Multiaddr{q1, q2, q3, q4, q1v1, q2v1, q3v1, wt1},
name: "quic-ranking",
addrs: []ma.Multiaddr{q1, q2, q3, q4, q1v1, q2v1, q3v1, wt1, t1},
output: []network.AddrDelay{
{Addr: q1, Delay: 0},
{Addr: q2, Delay: 0},
{Addr: q3, Delay: 0},
{Addr: q4, Delay: 0},
{Addr: q1v1, Delay: 0},
{Addr: q2v1, Delay: 0},
{Addr: q3v1, Delay: 0},
{Addr: q4, Delay: 0},
{Addr: wt1, Delay: 0},
{Addr: t1, Delay: 0},
},
},
}
Expand Down Expand Up @@ -103,13 +109,17 @@ func TestDelayRankerQUICDelay(t *testing.T) {
},
},
{
name: "quic+webtransport filtered when quicv1",
name: "quic-quic-v1-webtransport",
addrs: []ma.Multiaddr{q1, q2, q3, q4, q1v1, q2v1, q3v1, wt1},
output: []network.AddrDelay{
{Addr: q1v1, Delay: 0},
{Addr: q1, Delay: PublicQUICDelay},
{Addr: q2, Delay: PublicQUICDelay},
{Addr: q3, Delay: PublicQUICDelay},
{Addr: q4, Delay: PublicQUICDelay},
{Addr: q2v1, Delay: PublicQUICDelay},
{Addr: q3v1, Delay: PublicQUICDelay},
{Addr: q4, Delay: PublicQUICDelay},
{Addr: wt1, Delay: PublicQUICDelay},
},
},
{
Expand Down
109 changes: 86 additions & 23 deletions p2p/net/swarm/swarm_dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"strconv"
"sync"
"time"

Expand Down Expand Up @@ -433,8 +435,9 @@ func (s *Swarm) nonProxyAddr(addr ma.Multiaddr) bool {
// filterKnownUndialables takes a list of multiaddrs, and removes those
// that we definitely don't want to dial: addresses configured to be blocked,
// IPv6 link-local addresses, addresses without a dial-capable transport,
// and addresses that we know to be our own.
// This is an optimization to avoid wasting time on dials that we know are going to fail.
// addresses that we know to be our own, and addresses with a better tranport
// available. This is an optimization to avoid wasting time on dials that we
// know are going to fail or for which we have a better alternative.
func (s *Swarm) filterKnownUndialables(p peer.ID, addrs []ma.Multiaddr) []ma.Multiaddr {
lisAddrs, _ := s.InterfaceListenAddresses()
var ourAddrs []ma.Multiaddr
Expand All @@ -448,21 +451,17 @@ func (s *Swarm) filterKnownUndialables(p peer.ID, addrs []ma.Multiaddr) []ma.Mul
})
}

// Make a map of udp ports we are listening on to filter peers web transport addresses
ourLocalHostUDPPorts := make(map[string]bool, 2)
for _, a := range ourAddrs {
if !manet.IsIPLoopback(a) {
continue
}
if p, err := a.ValueForProtocol(ma.P_UDP); err == nil {
ourLocalHostUDPPorts[p] = true
}
}
// The order of these two filters is important. If we can only dial /webtransport,
// we don't want to filter /webtransport addresses out because the peer had a /quic-v1
// address

// filter addresses we cannot dial
addrs = ma.FilterAddrs(addrs, s.canDial)
// filter low priority addresses among the addresses we can dial
addrs = filterLowPriorityAddresses(addrs)

return ma.FilterAddrs(addrs,
func(addr ma.Multiaddr) bool { return !ma.Contains(ourAddrs, addr) },
func(addr ma.Multiaddr) bool { return checkLocalHostUDPAddrs(addr, ourLocalHostUDPPorts) },
s.canDial,
// TODO: Consider allowing link-local addresses
func(addr ma.Multiaddr) bool { return !manet.IsIP6LinkLocal(addr) },
func(addr ma.Multiaddr) bool {
Expand Down Expand Up @@ -559,15 +558,79 @@ func isRelayAddr(addr ma.Multiaddr) bool {
return err == nil
}

// checkLocalHostUDPAddrs returns false for addresses that have the same localhost port
// as the one we are listening on
// This is useful for filtering out peer's localhost webtransport addresses.
func checkLocalHostUDPAddrs(addr ma.Multiaddr, ourUDPPorts map[string]bool) bool {
if !manet.IsIPLoopback(addr) {
return true
// filterLowPriorityAddresses removes addresses inplace for which we have a better alternative
// 1. If a /quic-v1 address is present, filter out /quic and /webtransport address on the same 2-tuple:
// QUIC v1 is preferred over the deprecated QUIC draft-29, and given the choice, we prefer using
// raw QUIC over using WebTransport.
// 2. If a /tcp address is present, filter out /ws or /wss addresses on the same 2-tuple:
// We prefer using raw TCP over using WebSocket.
func filterLowPriorityAddresses(addrs []ma.Multiaddr) []ma.Multiaddr {
// make a map of QUIC v1 and TCP AddrPorts.
quicV1Addr := make(map[netip.AddrPort]struct{})
tcpAddr := make(map[netip.AddrPort]struct{})
for _, a := range addrs {
switch {
case isProtocolAddr(a, ma.P_WEBTRANSPORT):
case isProtocolAddr(a, ma.P_QUIC_V1):
ap, err := addrPort(a, ma.P_UDP)
if err != nil {
continue
}
quicV1Addr[ap] = struct{}{}
case isProtocolAddr(a, ma.P_WS) || isProtocolAddr(a, ma.P_WSS):
case isProtocolAddr(a, ma.P_TCP):
ap, err := addrPort(a, ma.P_TCP)
if err != nil {
continue
}
tcpAddr[ap] = struct{}{}
}
}
if p, err := addr.ValueForProtocol(ma.P_UDP); err == nil {
return !ourUDPPorts[p]

i := 0
for _, a := range addrs {
switch {
case isProtocolAddr(a, ma.P_WEBTRANSPORT) || isProtocolAddr(a, ma.P_QUIC):
ap, err := addrPort(a, ma.P_UDP)
if err != nil {
break
}
if _, ok := quicV1Addr[ap]; ok {
continue
}
case isProtocolAddr(a, ma.P_WS) || isProtocolAddr(a, ma.P_WSS):
ap, err := addrPort(a, ma.P_TCP)
if err != nil {
break
}
if _, ok := tcpAddr[ap]; ok {
continue
}
}
addrs[i] = a
i++
}
return addrs[:i]
}

// addrPort returns the ip and port for a. p should be either ma.P_TCP or ma.P_UDP.
// a must be an (ip, TCP) or (ip, udp) address.
func addrPort(a ma.Multiaddr, p int) (netip.AddrPort, error) {
ip, err := manet.ToIP(a)
if err != nil {
return netip.AddrPort{}, err
}
port, err := a.ValueForProtocol(p)
if err != nil {
return netip.AddrPort{}, err
}
pi, err := strconv.Atoi(port)
if err != nil {
return netip.AddrPort{}, err
}
addr, ok := netip.AddrFromSlice(ip)
if !ok {
return netip.AddrPort{}, fmt.Errorf("failed to parse IP %s", ip)
}
return true
return netip.AddrPortFrom(addr, uint16(pi)), nil
}
Loading