ceremonyclient/go-libp2p/p2p/transport/quic/transport.go
Cassandra Heart 6a7cbab864
perf grinding at the extremes
- removed defer/recover/panic where warranted
- found and eliminated race conditions when extremely high connection pressure was hit
- resolved race condition in blossomsub around mesh maintenance
- incorporated resource manager scaling to work off low/hi watermark values
2024-08-21 14:45:31 -05:00

412 lines
11 KiB
Go

package libp2pquic
import (
"context"
"crypto/tls"
"errors"
"fmt"
"math/rand"
"net"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/connmgr"
ic "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/pnet"
tpt "github.com/libp2p/go-libp2p/core/transport"
p2ptls "github.com/libp2p/go-libp2p/p2p/security/tls"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
logging "github.com/ipfs/go-log/v2"
ma "github.com/multiformats/go-multiaddr"
mafmt "github.com/multiformats/go-multiaddr-fmt"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/quic-go/quic-go"
)
var log = logging.Logger("quic-transport")
var ErrHolePunching = errors.New("hole punching attempted; no active dial")
var HolePunchTimeout = 5 * time.Second
const errorCodeConnectionGating = 0x47415445 // GATE in ASCII
// The Transport implements the tpt.Transport interface for QUIC connections.
type transport struct {
privKey ic.PrivKey
localPeer peer.ID
identity *p2ptls.Identity
connManager *quicreuse.ConnManager
gater connmgr.ConnectionGater
rcmgr network.ResourceManager
holePunchingMx sync.Mutex
holePunching map[holePunchKey]*activeHolePunch
rndMx sync.Mutex
rnd rand.Rand
connMx sync.Mutex
conns map[quic.Connection]*conn
listenersMu sync.Mutex
// map of UDPAddr as string to a virtualListeners
listeners map[string][]*virtualListener
}
var _ tpt.Transport = &transport{}
type holePunchKey struct {
addr string
peer peer.ID
}
type activeHolePunch struct {
connCh chan tpt.CapableConn
fulfilled bool
}
// NewTransport creates a new QUIC transport
func NewTransport(key ic.PrivKey, connManager *quicreuse.ConnManager, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr network.ResourceManager) (tpt.Transport, error) {
if len(psk) > 0 {
log.Error("QUIC doesn't support private networks yet.")
return nil, errors.New("QUIC doesn't support private networks yet")
}
localPeer, err := peer.IDFromPrivateKey(key)
if err != nil {
return nil, err
}
identity, err := p2ptls.NewIdentity(key)
if err != nil {
return nil, err
}
if rcmgr == nil {
rcmgr = &network.NullResourceManager{}
}
return &transport{
privKey: key,
localPeer: localPeer,
identity: identity,
connManager: connManager,
gater: gater,
rcmgr: rcmgr,
conns: make(map[quic.Connection]*conn),
holePunching: make(map[holePunchKey]*activeHolePunch),
rnd: *rand.New(rand.NewSource(time.Now().UnixNano())),
listeners: make(map[string][]*virtualListener),
}, nil
}
// Dial dials a new QUIC connection
func (t *transport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (_c tpt.CapableConn, _err error) {
if ok, isClient, _ := network.GetSimultaneousConnect(ctx); ok && !isClient {
return t.holePunch(ctx, raddr, p)
}
scope, err := t.rcmgr.OpenConnection(network.DirOutbound, false, raddr)
if err != nil {
log.Debugw("resource manager blocked outgoing connection", "peer", p, "addr", raddr, "error", err)
return nil, err
}
c, err := t.dialWithScope(ctx, raddr, p, scope)
if err != nil {
scope.Done()
return nil, err
}
return c, nil
}
func (t *transport) dialWithScope(ctx context.Context, raddr ma.Multiaddr, p peer.ID, scope network.ConnManagementScope) (tpt.CapableConn, error) {
if err := scope.SetPeer(p); err != nil {
log.Debugw("resource manager blocked outgoing connection for peer", "peer", p, "addr", raddr, "error", err)
return nil, err
}
tlsConf, keyCh := t.identity.ConfigForPeer(p)
pconn, err := t.connManager.DialQUIC(ctx, raddr, tlsConf, t.allowWindowIncrease)
if err != nil {
return nil, err
}
// Should be ready by this point, don't block.
var remotePubKey ic.PubKey
select {
case remotePubKey = <-keyCh:
default:
}
if remotePubKey == nil {
pconn.CloseWithError(1, "")
return nil, errors.New("p2p/transport/quic BUG: expected remote pub key to be set")
}
localMultiaddr, err := quicreuse.ToQuicMultiaddr(pconn.LocalAddr(), pconn.ConnectionState().Version)
if err != nil {
pconn.CloseWithError(1, "")
return nil, err
}
c := &conn{
quicConn: pconn,
transport: t,
scope: scope,
localPeer: t.localPeer,
localMultiaddr: localMultiaddr,
remotePubKey: remotePubKey,
remotePeerID: p,
remoteMultiaddr: raddr,
}
if t.gater != nil && !t.gater.InterceptSecured(network.DirOutbound, p, c) {
pconn.CloseWithError(errorCodeConnectionGating, "connection gated")
return nil, fmt.Errorf("secured connection gated")
}
t.addConn(pconn, c)
return c, nil
}
func (t *transport) addConn(conn quic.Connection, c *conn) {
t.connMx.Lock()
t.conns[conn] = c
t.connMx.Unlock()
}
func (t *transport) removeConn(conn quic.Connection) {
t.connMx.Lock()
delete(t.conns, conn)
t.connMx.Unlock()
}
func (t *transport) holePunch(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (tpt.CapableConn, error) {
network, saddr, err := manet.DialArgs(raddr)
if err != nil {
return nil, err
}
addr, err := net.ResolveUDPAddr(network, saddr)
if err != nil {
return nil, err
}
tr, err := t.connManager.TransportForDial(network, addr)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, HolePunchTimeout)
key := holePunchKey{addr: addr.String(), peer: p}
t.holePunchingMx.Lock()
if _, ok := t.holePunching[key]; ok {
t.holePunchingMx.Unlock()
tr.DecreaseCount()
cancel()
return nil, fmt.Errorf("already punching hole for %s", addr)
}
connCh := make(chan tpt.CapableConn, 1)
t.holePunching[key] = &activeHolePunch{connCh: connCh}
t.holePunchingMx.Unlock()
var timer *time.Timer
payload := make([]byte, 64)
var punchErr error
loop:
for i := 0; ; i++ {
t.rndMx.Lock()
_, err := t.rnd.Read(payload)
t.rndMx.Unlock()
if err != nil {
punchErr = err
break
}
if _, err := tr.WriteTo(payload, addr); err != nil {
punchErr = err
break
}
maxSleep := 10 * (i + 1) * (i + 1) // in ms
if maxSleep > 200 {
maxSleep = 200
}
d := 10*time.Millisecond + time.Duration(rand.Intn(maxSleep))*time.Millisecond
if timer == nil {
timer = time.NewTimer(d)
} else {
timer.Reset(d)
}
select {
case c := <-connCh:
t.holePunchingMx.Lock()
delete(t.holePunching, key)
t.holePunchingMx.Unlock()
tr.DecreaseCount()
cancel()
if timer != nil {
timer.Stop()
}
return c, nil
case <-timer.C:
case <-ctx.Done():
punchErr = ErrHolePunching
break loop
}
}
// we only arrive here if punchErr != nil
t.holePunchingMx.Lock()
select {
case c := <-t.holePunching[key].connCh:
tr.DecreaseCount()
cancel()
if timer != nil {
timer.Stop()
}
delete(t.holePunching, key)
t.holePunchingMx.Unlock()
return c, nil
default:
tr.DecreaseCount()
cancel()
if timer != nil {
timer.Stop()
}
delete(t.holePunching, key)
t.holePunchingMx.Unlock()
return nil, punchErr
}
}
// Don't use mafmt.QUIC as we don't want to dial DNS addresses. Just /ip{4,6}/udp/quic-v1
var dialMatcher = mafmt.And(mafmt.IP, mafmt.Base(ma.P_UDP), mafmt.Base(ma.P_QUIC_V1))
// CanDial determines if we can dial to an address
func (t *transport) CanDial(addr ma.Multiaddr) bool {
return dialMatcher.Matches(addr)
}
// Listen listens for new QUIC connections on the passed multiaddr.
func (t *transport) Listen(addr ma.Multiaddr) (tpt.Listener, error) {
var tlsConf tls.Config
tlsConf.GetConfigForClient = func(_ *tls.ClientHelloInfo) (*tls.Config, error) {
// return a tls.Config that verifies the peer's certificate chain.
// Note that since we have no way of associating an incoming QUIC connection with
// the peer ID calculated here, we don't actually receive the peer's public key
// from the key chan.
conf, _ := t.identity.ConfigForPeer("")
return conf, nil
}
tlsConf.NextProtos = []string{"libp2p"}
udpAddr, version, err := quicreuse.FromQuicMultiaddr(addr)
if err != nil {
return nil, err
}
t.listenersMu.Lock()
listeners := t.listeners[udpAddr.String()]
var underlyingListener *listener
var acceptRunner *acceptLoopRunner
if len(listeners) != 0 {
// We already have an underlying listener, let's use it
underlyingListener = listeners[0].listener
acceptRunner = listeners[0].acceptRunnner
// Make sure our underlying listener is listening on the specified QUIC version
if _, ok := underlyingListener.localMultiaddrs[version]; !ok {
t.listenersMu.Unlock()
return nil, fmt.Errorf("can't listen on quic version %v, underlying listener doesn't support it", version)
}
} else {
ln, err := t.connManager.ListenQUIC(addr, &tlsConf, t.allowWindowIncrease)
if err != nil {
t.listenersMu.Unlock()
return nil, err
}
l, err := newListener(ln, t, t.localPeer, t.privKey, t.rcmgr)
if err != nil {
_ = ln.Close()
t.listenersMu.Unlock()
return nil, err
}
underlyingListener = &l
acceptRunner = &acceptLoopRunner{
acceptSem: make(chan struct{}, 1),
muxer: make(map[quic.VersionNumber]chan acceptVal),
}
}
l := &virtualListener{
listener: underlyingListener,
version: version,
udpAddr: udpAddr.String(),
t: t,
acceptRunnner: acceptRunner,
acceptChan: acceptRunner.AcceptForVersion(version),
}
listeners = append(listeners, l)
t.listeners[udpAddr.String()] = listeners
t.listenersMu.Unlock()
return l, nil
}
func (t *transport) allowWindowIncrease(conn quic.Connection, size uint64) bool {
// If the QUIC connection tries to increase the window before we've inserted it
// into our connections map (which we do right after dialing / accepting it),
// we have no way to account for that memory. This should be very rare.
// Block this attempt. The connection can request more memory later.
t.connMx.Lock()
c, ok := t.conns[conn]
t.connMx.Unlock()
if !ok {
return false
}
return c.allowWindowIncrease(size)
}
// Proxy returns true if this transport proxies.
func (t *transport) Proxy() bool {
return false
}
// Protocols returns the set of protocols handled by this transport.
func (t *transport) Protocols() []int {
return t.connManager.Protocols()
}
func (t *transport) String() string {
return "QUIC"
}
func (t *transport) Close() error {
return nil
}
func (t *transport) CloseVirtualListener(l *virtualListener) error {
t.listenersMu.Lock()
var err error
listeners := t.listeners[l.udpAddr]
if len(listeners) == 1 {
// This is the last virtual listener here, so we can close the underlying listener
err = l.listener.Close()
delete(t.listeners, l.udpAddr)
t.listenersMu.Unlock()
return err
}
for i := 0; i < len(listeners); i++ {
// Swap remove
if l == listeners[i] {
listeners[i] = listeners[len(listeners)-1]
listeners = listeners[:len(listeners)-1]
t.listeners[l.udpAddr] = listeners
break
}
}
t.listenersMu.Unlock()
return nil
}