mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-29 18:05:18 +00:00
627 lines
19 KiB
Go
627 lines
19 KiB
Go
|
package swarm
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net/netip"
|
||
|
"strconv"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/libp2p/go-libp2p/core/canonicallog"
|
||
|
"github.com/libp2p/go-libp2p/core/network"
|
||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||
|
"github.com/libp2p/go-libp2p/core/peerstore"
|
||
|
"github.com/libp2p/go-libp2p/core/transport"
|
||
|
ma "github.com/multiformats/go-multiaddr"
|
||
|
madns "github.com/multiformats/go-multiaddr-dns"
|
||
|
manet "github.com/multiformats/go-multiaddr/net"
|
||
|
)
|
||
|
|
||
|
// The maximum number of address resolution steps we'll perform for a single
|
||
|
// peer (for all addresses).
|
||
|
const maxAddressResolution = 32
|
||
|
|
||
|
// Diagram of dial sync:
|
||
|
//
|
||
|
// many callers of Dial() synched w. dials many addrs results to callers
|
||
|
// ----------------------\ dialsync use earliest /--------------
|
||
|
// -----------------------\ |----------\ /----------------
|
||
|
// ------------------------>------------<------- >---------<-----------------
|
||
|
// -----------------------| \----x \----------------
|
||
|
// ----------------------| \-----x \---------------
|
||
|
// any may fail if no addr at end
|
||
|
// retry dialAttempt x
|
||
|
|
||
|
var (
|
||
|
// ErrDialBackoff is returned by the backoff code when a given peer has
|
||
|
// been dialed too frequently
|
||
|
ErrDialBackoff = errors.New("dial backoff")
|
||
|
|
||
|
// ErrDialRefusedBlackHole is returned when we are in a black holed environment
|
||
|
ErrDialRefusedBlackHole = errors.New("dial refused because of black hole")
|
||
|
|
||
|
// ErrDialToSelf is returned if we attempt to dial our own peer
|
||
|
ErrDialToSelf = errors.New("dial to self attempted")
|
||
|
|
||
|
// ErrNoTransport is returned when we don't know a transport for the
|
||
|
// given multiaddr.
|
||
|
ErrNoTransport = errors.New("no transport for protocol")
|
||
|
|
||
|
// ErrAllDialsFailed is returned when connecting to a peer has ultimately failed
|
||
|
ErrAllDialsFailed = errors.New("all dials failed")
|
||
|
|
||
|
// ErrNoAddresses is returned when we fail to find any addresses for a
|
||
|
// peer we're trying to dial.
|
||
|
ErrNoAddresses = errors.New("no addresses")
|
||
|
|
||
|
// ErrNoGoodAddresses is returned when we find addresses for a peer but
|
||
|
// can't use any of them.
|
||
|
ErrNoGoodAddresses = errors.New("no good addresses")
|
||
|
|
||
|
// ErrGaterDisallowedConnection is returned when the gater prevents us from
|
||
|
// forming a connection with a peer.
|
||
|
ErrGaterDisallowedConnection = errors.New("gater disallows connection to peer")
|
||
|
)
|
||
|
|
||
|
// DialAttempts governs how many times a goroutine will try to dial a given peer.
|
||
|
// Note: this is down to one, as we have _too many dials_ atm. To add back in,
|
||
|
// add loop back in Dial(.)
|
||
|
const DialAttempts = 1
|
||
|
|
||
|
// ConcurrentFdDials is the number of concurrent outbound dials over transports
|
||
|
// that consume file descriptors
|
||
|
const ConcurrentFdDials = 160
|
||
|
|
||
|
// DefaultPerPeerRateLimit is the number of concurrent outbound dials to make
|
||
|
// per peer
|
||
|
var DefaultPerPeerRateLimit = 8
|
||
|
|
||
|
// DialBackoff is a type for tracking peer dial backoffs. Dialbackoff is used to
|
||
|
// avoid over-dialing the same, dead peers. Whenever we totally time out on all
|
||
|
// addresses of a peer, we add the addresses to DialBackoff. Then, whenever we
|
||
|
// attempt to dial the peer again, we check each address for backoff. If it's on
|
||
|
// backoff, we don't dial the address and exit promptly. If a dial is
|
||
|
// successful, the peer and all its addresses are removed from backoff.
|
||
|
//
|
||
|
// * It's safe to use its zero value.
|
||
|
// * It's thread-safe.
|
||
|
// * It's *not* safe to move this type after using.
|
||
|
type DialBackoff struct {
|
||
|
entries map[peer.ID]map[string]*backoffAddr
|
||
|
lock sync.RWMutex
|
||
|
}
|
||
|
|
||
|
type backoffAddr struct {
|
||
|
tries int
|
||
|
until time.Time
|
||
|
}
|
||
|
|
||
|
func (db *DialBackoff) init(ctx context.Context) {
|
||
|
if db.entries == nil {
|
||
|
db.entries = make(map[peer.ID]map[string]*backoffAddr)
|
||
|
}
|
||
|
go db.background(ctx)
|
||
|
}
|
||
|
|
||
|
func (db *DialBackoff) background(ctx context.Context) {
|
||
|
ticker := time.NewTicker(BackoffMax)
|
||
|
defer ticker.Stop()
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
return
|
||
|
case <-ticker.C:
|
||
|
db.cleanup()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Backoff returns whether the client should backoff from dialing
|
||
|
// peer p at address addr
|
||
|
func (db *DialBackoff) Backoff(p peer.ID, addr ma.Multiaddr) (backoff bool) {
|
||
|
db.lock.RLock()
|
||
|
defer db.lock.RUnlock()
|
||
|
|
||
|
ap, found := db.entries[p][string(addr.Bytes())]
|
||
|
return found && time.Now().Before(ap.until)
|
||
|
}
|
||
|
|
||
|
// BackoffBase is the base amount of time to backoff (default: 5s).
|
||
|
var BackoffBase = time.Second * 5
|
||
|
|
||
|
// BackoffCoef is the backoff coefficient (default: 1s).
|
||
|
var BackoffCoef = time.Second
|
||
|
|
||
|
// BackoffMax is the maximum backoff time (default: 5m).
|
||
|
var BackoffMax = time.Minute * 5
|
||
|
|
||
|
// AddBackoff adds peer's address to backoff.
|
||
|
//
|
||
|
// Backoff is not exponential, it's quadratic and computed according to the
|
||
|
// following formula:
|
||
|
//
|
||
|
// BackoffBase + BakoffCoef * PriorBackoffs^2
|
||
|
//
|
||
|
// Where PriorBackoffs is the number of previous backoffs.
|
||
|
func (db *DialBackoff) AddBackoff(p peer.ID, addr ma.Multiaddr) {
|
||
|
saddr := string(addr.Bytes())
|
||
|
db.lock.Lock()
|
||
|
defer db.lock.Unlock()
|
||
|
bp, ok := db.entries[p]
|
||
|
if !ok {
|
||
|
bp = make(map[string]*backoffAddr, 1)
|
||
|
db.entries[p] = bp
|
||
|
}
|
||
|
ba, ok := bp[saddr]
|
||
|
if !ok {
|
||
|
bp[saddr] = &backoffAddr{
|
||
|
tries: 1,
|
||
|
until: time.Now().Add(BackoffBase),
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
backoffTime := BackoffBase + BackoffCoef*time.Duration(ba.tries*ba.tries)
|
||
|
if backoffTime > BackoffMax {
|
||
|
backoffTime = BackoffMax
|
||
|
}
|
||
|
ba.until = time.Now().Add(backoffTime)
|
||
|
ba.tries++
|
||
|
}
|
||
|
|
||
|
// Clear removes a backoff record. Clients should call this after a
|
||
|
// successful Dial.
|
||
|
func (db *DialBackoff) Clear(p peer.ID) {
|
||
|
db.lock.Lock()
|
||
|
defer db.lock.Unlock()
|
||
|
delete(db.entries, p)
|
||
|
}
|
||
|
|
||
|
func (db *DialBackoff) cleanup() {
|
||
|
db.lock.Lock()
|
||
|
defer db.lock.Unlock()
|
||
|
now := time.Now()
|
||
|
for p, e := range db.entries {
|
||
|
good := false
|
||
|
for _, backoff := range e {
|
||
|
backoffTime := BackoffBase + BackoffCoef*time.Duration(backoff.tries*backoff.tries)
|
||
|
if backoffTime > BackoffMax {
|
||
|
backoffTime = BackoffMax
|
||
|
}
|
||
|
if now.Before(backoff.until.Add(backoffTime)) {
|
||
|
good = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if !good {
|
||
|
delete(db.entries, p)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DialPeer connects to a peer.
|
||
|
//
|
||
|
// The idea is that the client of Swarm does not need to know what network
|
||
|
// the connection will happen over. Swarm can use whichever it choses.
|
||
|
// This allows us to use various transport protocols, do NAT traversal/relay,
|
||
|
// etc. to achieve connection.
|
||
|
func (s *Swarm) DialPeer(ctx context.Context, p peer.ID) (network.Conn, error) {
|
||
|
// Avoid typed nil issues.
|
||
|
c, err := s.dialPeer(ctx, p)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// internal dial method that returns an unwrapped conn
|
||
|
//
|
||
|
// It is gated by the swarm's dial synchronization systems: dialsync and
|
||
|
// dialbackoff.
|
||
|
func (s *Swarm) dialPeer(ctx context.Context, p peer.ID) (*Conn, error) {
|
||
|
log.Debugw("dialing peer", "from", s.local, "to", p)
|
||
|
err := p.Validate()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if p == s.local {
|
||
|
return nil, ErrDialToSelf
|
||
|
}
|
||
|
|
||
|
// check if we already have an open (usable) connection first, or can't have a usable
|
||
|
// connection.
|
||
|
conn, err := s.bestAcceptableConnToPeer(ctx, p)
|
||
|
if conn != nil || err != nil {
|
||
|
return conn, err
|
||
|
}
|
||
|
|
||
|
if s.gater != nil && !s.gater.InterceptPeerDial(p) {
|
||
|
log.Debugf("gater disallowed outbound connection to peer %s", p.Pretty())
|
||
|
return nil, &DialError{Peer: p, Cause: ErrGaterDisallowedConnection}
|
||
|
}
|
||
|
|
||
|
// apply the DialPeer timeout
|
||
|
ctx, cancel := context.WithTimeout(ctx, network.GetDialPeerTimeout(ctx))
|
||
|
defer cancel()
|
||
|
|
||
|
conn, err = s.dsync.Dial(ctx, p)
|
||
|
if err == nil {
|
||
|
// Ensure we connected to the correct peer.
|
||
|
// This was most likely already checked by the security protocol, but it doesn't hurt do it again here.
|
||
|
if conn.RemotePeer() != p {
|
||
|
conn.Close()
|
||
|
log.Errorw("Handshake failed to properly authenticate peer", "authenticated", conn.RemotePeer(), "expected", p)
|
||
|
return nil, fmt.Errorf("unexpected peer")
|
||
|
}
|
||
|
return conn, nil
|
||
|
}
|
||
|
|
||
|
log.Debugf("network for %s finished dialing %s", s.local, p)
|
||
|
|
||
|
if ctx.Err() != nil {
|
||
|
// Context error trumps any dial errors as it was likely the ultimate cause.
|
||
|
return nil, ctx.Err()
|
||
|
}
|
||
|
|
||
|
if s.ctx.Err() != nil {
|
||
|
// Ok, so the swarm is shutting down.
|
||
|
return nil, ErrSwarmClosed
|
||
|
}
|
||
|
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// dialWorkerLoop synchronizes and executes concurrent dials to a single peer
|
||
|
func (s *Swarm) dialWorkerLoop(p peer.ID, reqch <-chan dialRequest) {
|
||
|
w := newDialWorker(s, p, reqch, nil)
|
||
|
w.loop()
|
||
|
}
|
||
|
|
||
|
func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, error) {
|
||
|
peerAddrs := s.peers.Addrs(p)
|
||
|
if len(peerAddrs) == 0 {
|
||
|
return nil, ErrNoAddresses
|
||
|
}
|
||
|
|
||
|
peerAddrsAfterTransportResolved := make([]ma.Multiaddr, 0, len(peerAddrs))
|
||
|
for _, a := range peerAddrs {
|
||
|
tpt := s.TransportForDialing(a)
|
||
|
resolver, ok := tpt.(transport.Resolver)
|
||
|
if ok {
|
||
|
resolvedAddrs, err := resolver.Resolve(ctx, a)
|
||
|
if err != nil {
|
||
|
log.Warnf("Failed to resolve multiaddr %s by transport %v: %v", a, tpt, err)
|
||
|
continue
|
||
|
}
|
||
|
peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, resolvedAddrs...)
|
||
|
} else {
|
||
|
peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, a)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Resolve dns or dnsaddrs
|
||
|
resolved, err := s.resolveAddrs(ctx, peer.AddrInfo{
|
||
|
ID: p,
|
||
|
Addrs: peerAddrsAfterTransportResolved,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
goodAddrs := s.filterKnownUndialables(p, resolved)
|
||
|
if forceDirect, _ := network.GetForceDirectDial(ctx); forceDirect {
|
||
|
goodAddrs = ma.FilterAddrs(goodAddrs, s.nonProxyAddr)
|
||
|
}
|
||
|
goodAddrs = ma.Unique(goodAddrs)
|
||
|
|
||
|
if len(goodAddrs) == 0 {
|
||
|
return nil, ErrNoGoodAddresses
|
||
|
}
|
||
|
|
||
|
s.peers.AddAddrs(p, goodAddrs, peerstore.TempAddrTTL)
|
||
|
|
||
|
return goodAddrs, nil
|
||
|
}
|
||
|
|
||
|
func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) {
|
||
|
proto := ma.ProtocolWithCode(ma.P_P2P).Name
|
||
|
p2paddr, err := ma.NewMultiaddr("/" + proto + "/" + pi.ID.Pretty())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
resolveSteps := 0
|
||
|
|
||
|
// Recursively resolve all addrs.
|
||
|
//
|
||
|
// While the toResolve list is non-empty:
|
||
|
// * Pop an address off.
|
||
|
// * If the address is fully resolved, add it to the resolved list.
|
||
|
// * Otherwise, resolve it and add the results to the "to resolve" list.
|
||
|
toResolve := append(([]ma.Multiaddr)(nil), pi.Addrs...)
|
||
|
resolved := make([]ma.Multiaddr, 0, len(pi.Addrs))
|
||
|
for len(toResolve) > 0 {
|
||
|
// pop the last addr off.
|
||
|
addr := toResolve[len(toResolve)-1]
|
||
|
toResolve = toResolve[:len(toResolve)-1]
|
||
|
|
||
|
// if it's resolved, add it to the resolved list.
|
||
|
if !madns.Matches(addr) {
|
||
|
resolved = append(resolved, addr)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
resolveSteps++
|
||
|
|
||
|
// We've resolved too many addresses. We can keep all the fully
|
||
|
// resolved addresses but we'll need to skip the rest.
|
||
|
if resolveSteps >= maxAddressResolution {
|
||
|
log.Warnf(
|
||
|
"peer %s asked us to resolve too many addresses: %s/%s",
|
||
|
pi.ID,
|
||
|
resolveSteps,
|
||
|
maxAddressResolution,
|
||
|
)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// otherwise, resolve it
|
||
|
reqaddr := addr.Encapsulate(p2paddr)
|
||
|
resaddrs, err := s.maResolver.Resolve(ctx, reqaddr)
|
||
|
if err != nil {
|
||
|
log.Infof("error resolving %s: %s", reqaddr, err)
|
||
|
}
|
||
|
|
||
|
// add the results to the toResolve list.
|
||
|
for _, res := range resaddrs {
|
||
|
pi, err := peer.AddrInfoFromP2pAddr(res)
|
||
|
if err != nil {
|
||
|
log.Infof("error parsing %s: %s", res, err)
|
||
|
}
|
||
|
toResolve = append(toResolve, pi.Addrs...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resolved, nil
|
||
|
}
|
||
|
|
||
|
func (s *Swarm) dialNextAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, resch chan dialResult) error {
|
||
|
// check the dial backoff
|
||
|
if forceDirect, _ := network.GetForceDirectDial(ctx); !forceDirect {
|
||
|
if s.backf.Backoff(p, addr) {
|
||
|
return ErrDialBackoff
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// start the dial
|
||
|
s.limitedDial(ctx, p, addr, resch)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Swarm) canDial(addr ma.Multiaddr) bool {
|
||
|
t := s.TransportForDialing(addr)
|
||
|
return t != nil && t.CanDial(addr)
|
||
|
}
|
||
|
|
||
|
func (s *Swarm) nonProxyAddr(addr ma.Multiaddr) bool {
|
||
|
t := s.TransportForDialing(addr)
|
||
|
return !t.Proxy()
|
||
|
}
|
||
|
|
||
|
// 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,
|
||
|
// 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
|
||
|
for _, addr := range lisAddrs {
|
||
|
// we're only sure about filtering out /ip4 and /ip6 addresses, so far
|
||
|
ma.ForEach(addr, func(c ma.Component) bool {
|
||
|
if c.Protocol().Code == ma.P_IP4 || c.Protocol().Code == ma.P_IP6 {
|
||
|
ourAddrs = append(ourAddrs, addr)
|
||
|
}
|
||
|
return false
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
|
||
|
// remove black holed addrs
|
||
|
addrs = s.bhd.FilterAddrs(addrs)
|
||
|
|
||
|
return ma.FilterAddrs(addrs,
|
||
|
func(addr ma.Multiaddr) bool { return !ma.Contains(ourAddrs, addr) },
|
||
|
// TODO: Consider allowing link-local addresses
|
||
|
func(addr ma.Multiaddr) bool { return !manet.IsIP6LinkLocal(addr) },
|
||
|
func(addr ma.Multiaddr) bool {
|
||
|
return s.gater == nil || s.gater.InterceptAddrDial(p, addr)
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// limitedDial will start a dial to the given peer when
|
||
|
// it is able, respecting the various different types of rate
|
||
|
// limiting that occur without using extra goroutines per addr
|
||
|
func (s *Swarm) limitedDial(ctx context.Context, p peer.ID, a ma.Multiaddr, resp chan dialResult) {
|
||
|
timeout := s.dialTimeout
|
||
|
if lowTimeoutFilters.AddrBlocked(a) && s.dialTimeoutLocal < s.dialTimeout {
|
||
|
timeout = s.dialTimeoutLocal
|
||
|
}
|
||
|
s.limiter.AddDialJob(&dialJob{
|
||
|
addr: a,
|
||
|
peer: p,
|
||
|
resp: resp,
|
||
|
ctx: ctx,
|
||
|
timeout: timeout,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// dialAddr is the actual dial for an addr, indirectly invoked through the limiter
|
||
|
func (s *Swarm) dialAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr) (transport.CapableConn, error) {
|
||
|
// Just to double check. Costs nothing.
|
||
|
if s.local == p {
|
||
|
return nil, ErrDialToSelf
|
||
|
}
|
||
|
// Check before we start work
|
||
|
if err := ctx.Err(); err != nil {
|
||
|
log.Debugf("%s swarm not dialing. Context cancelled: %v. %s %s", s.local, err, p, addr)
|
||
|
return nil, err
|
||
|
}
|
||
|
log.Debugf("%s swarm dialing %s %s", s.local, p, addr)
|
||
|
|
||
|
tpt := s.TransportForDialing(addr)
|
||
|
if tpt == nil {
|
||
|
return nil, ErrNoTransport
|
||
|
}
|
||
|
|
||
|
start := time.Now()
|
||
|
connC, err := tpt.Dial(ctx, addr, p)
|
||
|
|
||
|
// We're recording any error as a failure here.
|
||
|
// Notably, this also applies to cancelations (i.e. if another dial attempt was faster).
|
||
|
// This is ok since the black hole detector uses a very low threshold (5%).
|
||
|
s.bhd.RecordResult(addr, err == nil)
|
||
|
|
||
|
if err != nil {
|
||
|
if s.metricsTracer != nil {
|
||
|
s.metricsTracer.FailedDialing(addr, err)
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
canonicallog.LogPeerStatus(100, connC.RemotePeer(), connC.RemoteMultiaddr(), "connection_status", "established", "dir", "outbound")
|
||
|
if s.metricsTracer != nil {
|
||
|
connWithMetrics := wrapWithMetrics(connC, s.metricsTracer, start, network.DirOutbound)
|
||
|
connWithMetrics.completedHandshake()
|
||
|
connC = connWithMetrics
|
||
|
}
|
||
|
|
||
|
// Trust the transport? Yeah... right.
|
||
|
if connC.RemotePeer() != p {
|
||
|
connC.Close()
|
||
|
err = fmt.Errorf("BUG in transport %T: tried to dial %s, dialed %s", p, connC.RemotePeer(), tpt)
|
||
|
log.Error(err)
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// success! we got one!
|
||
|
return connC, nil
|
||
|
}
|
||
|
|
||
|
// TODO We should have a `IsFdConsuming() bool` method on the `Transport` interface in go-libp2p/core/transport.
|
||
|
// This function checks if any of the transport protocols in the address requires a file descriptor.
|
||
|
// For now:
|
||
|
// A Non-circuit address which has the TCP/UNIX protocol is deemed FD consuming.
|
||
|
// For a circuit-relay address, we look at the address of the relay server/proxy
|
||
|
// and use the same logic as above to decide.
|
||
|
func isFdConsumingAddr(addr ma.Multiaddr) bool {
|
||
|
first, _ := ma.SplitFunc(addr, func(c ma.Component) bool {
|
||
|
return c.Protocol().Code == ma.P_CIRCUIT
|
||
|
})
|
||
|
|
||
|
// for safety
|
||
|
if first == nil {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
_, err1 := first.ValueForProtocol(ma.P_TCP)
|
||
|
_, err2 := first.ValueForProtocol(ma.P_UNIX)
|
||
|
return err1 == nil || err2 == nil
|
||
|
}
|
||
|
|
||
|
func isRelayAddr(addr ma.Multiaddr) bool {
|
||
|
_, err := addr.ValueForProtocol(ma.P_CIRCUIT)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
// 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{}{}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 netip.AddrPortFrom(addr, uint16(pi)), nil
|
||
|
}
|