ceremonyclient/go-libp2p/p2p/transport/quicreuse/connmgr_test.go
2023-08-20 23:07:43 -05:00

274 lines
8.5 KiB
Go

package quicreuse
import (
"context"
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"runtime"
"testing"
"time"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/quic-go/quic-go"
"github.com/stretchr/testify/require"
)
func checkClosed(t *testing.T, cm *ConnManager) {
for _, r := range []*reuse{cm.reuseUDP4, cm.reuseUDP6} {
if r == nil {
continue
}
r.mutex.Lock()
for _, tr := range r.globalListeners {
require.Zero(t, tr.GetCount())
}
for _, trs := range r.unicast {
for _, tr := range trs {
require.Zero(t, tr.GetCount())
}
}
r.mutex.Unlock()
}
require.Eventually(t, func() bool { return !isGarbageCollectorRunning() }, 200*time.Millisecond, 10*time.Millisecond)
}
func TestListenQUICDraft29Disabled(t *testing.T) {
cm, err := NewConnManager([32]byte{}, DisableDraft29(), DisableReuseport())
require.NoError(t, err)
defer cm.Close()
_, err = cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic"), &tls.Config{}, nil)
require.EqualError(t, err, "can't listen on `/quic` multiaddr (QUIC draft 29 version) when draft 29 support is disabled")
ln, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{"proto"}}, nil)
require.NoError(t, err)
require.NoError(t, ln.Close())
require.False(t, isGarbageCollectorRunning())
}
func TestListenOnSameProto(t *testing.T) {
t.Run("with reuseport", func(t *testing.T) {
testListenOnSameProto(t, true)
})
t.Run("without reuseport", func(t *testing.T) {
testListenOnSameProto(t, false)
})
}
func testListenOnSameProto(t *testing.T, enableReuseport bool) {
var opts []Option
if !enableReuseport {
opts = append(opts, DisableReuseport())
}
cm, err := NewConnManager([32]byte{}, opts...)
require.NoError(t, err)
defer checkClosed(t, cm)
defer cm.Close()
const alpn = "proto"
var tlsConf tls.Config
tlsConf.NextProtos = []string{alpn}
ln1, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil)
require.NoError(t, err)
defer ln1.Close()
addr := ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%d/quic-v1", ln1.Addr().(*net.UDPAddr).Port))
_, err = cm.ListenQUIC(addr, &tls.Config{NextProtos: []string{alpn}}, nil)
require.EqualError(t, err, "already listening for protocol "+alpn)
// listening on a different address works
ln2, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), &tls.Config{NextProtos: []string{alpn}}, nil)
require.NoError(t, err)
defer ln2.Close()
}
// The conn passed to quic-go should be a conn that quic-go can be
// type-asserted to a UDPConn. That way, it can use all kinds of optimizations.
func TestConnectionPassedToQUICForListening(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping on windows. Windows doesn't support these optimizations")
}
cm, err := NewConnManager([32]byte{}, DisableReuseport())
require.NoError(t, err)
defer cm.Close()
raddr := ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1")
naddr, _, err := FromQuicMultiaddr(raddr)
require.NoError(t, err)
netw, _, err := manet.DialArgs(raddr)
require.NoError(t, err)
_, err = cm.ListenQUIC(raddr, &tls.Config{NextProtos: []string{"proto"}}, nil)
require.NoError(t, err)
quicTr, err := cm.transportForListen(netw, naddr)
require.NoError(t, err)
defer quicTr.Close()
if _, ok := quicTr.(*singleOwnerTransport).Transport.Conn.(quic.OOBCapablePacketConn); !ok {
t.Fatal("connection passed to quic-go cannot be type asserted to a *net.UDPConn")
}
}
func TestAcceptErrorGetCleanedUp(t *testing.T) {
raddr := ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1")
cm, err := NewConnManager([32]byte{}, DisableReuseport())
require.NoError(t, err)
defer cm.Close()
originalNumberOfGoroutines := runtime.NumGoroutine()
t.Log("num goroutines:", originalNumberOfGoroutines)
// This spawns a background goroutine for the listener
l, err := cm.ListenQUIC(raddr, &tls.Config{NextProtos: []string{"proto"}}, nil)
require.NoError(t, err)
// We spawned a goroutine for the listener
require.Greater(t, runtime.NumGoroutine(), originalNumberOfGoroutines)
l.Close()
// Now make sure we have less goroutines than before
// Manually doing the same as require.Eventually, except avoiding adding a goroutine
goRoutinesCleanedUp := false
for i := 0; i < 50; i++ {
t.Log("num goroutines:", runtime.NumGoroutine())
if runtime.NumGoroutine() <= originalNumberOfGoroutines {
goRoutinesCleanedUp = true
break
}
time.Sleep(100 * time.Millisecond)
}
require.True(t, goRoutinesCleanedUp, "goroutines were not cleaned up")
}
// The connection passed to quic-go needs to be type-assertable to a net.UDPConn,
// in order to enable features like batch processing and ECN.
func TestConnectionPassedToQUICForDialing(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping on windows. Windows doesn't support these optimizations")
}
cm, err := NewConnManager([32]byte{}, DisableReuseport())
require.NoError(t, err)
defer cm.Close()
raddr := ma.StringCast("/ip4/127.0.0.1/udp/1234/quic-v1")
naddr, _, err := FromQuicMultiaddr(raddr)
require.NoError(t, err)
netw, _, err := manet.DialArgs(raddr)
require.NoError(t, err)
quicTr, err := cm.TransportForDial(netw, naddr)
require.NoError(t, err, "dial error")
defer quicTr.Close()
if _, ok := quicTr.(*singleOwnerTransport).Transport.Conn.(quic.OOBCapablePacketConn); !ok {
t.Fatal("connection passed to quic-go cannot be type asserted to a *net.UDPConn")
}
}
func getTLSConfForProto(t *testing.T, alpn string) (peer.ID, *tls.Config) {
t.Helper()
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
id, err := peer.IDFromPrivateKey(priv)
require.NoError(t, err)
// We use the libp2p TLS certificate here, just because it's convenient.
identity, err := libp2ptls.NewIdentity(priv)
require.NoError(t, err)
var tlsConf tls.Config
tlsConf.NextProtos = []string{alpn}
tlsConf.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
c, _ := identity.ConfigForPeer("")
c.NextProtos = tlsConf.NextProtos
return c, nil
}
return id, &tlsConf
}
func connectWithProtocol(t *testing.T, addr net.Addr, alpn string) (peer.ID, error) {
t.Helper()
clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
clientIdentity, err := libp2ptls.NewIdentity(clientKey)
require.NoError(t, err)
tlsConf, peerChan := clientIdentity.ConfigForPeer("")
cconn, err := net.ListenUDP("udp4", nil)
tlsConf.NextProtos = []string{alpn}
require.NoError(t, err)
c, err := quic.Dial(context.Background(), cconn, addr, tlsConf, nil)
if err != nil {
return "", err
}
defer c.CloseWithError(0, "")
require.Equal(t, alpn, c.ConnectionState().TLS.NegotiatedProtocol)
serverID, err := peer.IDFromPublicKey(<-peerChan)
require.NoError(t, err)
return serverID, nil
}
func TestListener(t *testing.T) {
t.Run("with reuseport", func(t *testing.T) {
testListener(t, true)
})
t.Run("without reuseport", func(t *testing.T) {
testListener(t, false)
})
}
func testListener(t *testing.T, enableReuseport bool) {
var opts []Option
if !enableReuseport {
opts = append(opts, DisableReuseport())
}
cm, err := NewConnManager([32]byte{}, opts...)
require.NoError(t, err)
id1, tlsConf1 := getTLSConfForProto(t, "proto1")
ln1, err := cm.ListenQUIC(ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), tlsConf1, nil)
require.NoError(t, err)
id2, tlsConf2 := getTLSConfForProto(t, "proto2")
ln2, err := cm.ListenQUIC(
ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/udp/%d/quic-v1", ln1.Addr().(*net.UDPAddr).Port)),
tlsConf2,
nil,
)
require.NoError(t, err)
require.Equal(t, ln1.Addr(), ln2.Addr())
// Test that the right certificate is served.
id, err := connectWithProtocol(t, ln1.Addr(), "proto1")
require.NoError(t, err)
require.Equal(t, id1, id)
id, err = connectWithProtocol(t, ln1.Addr(), "proto2")
require.NoError(t, err)
require.Equal(t, id2, id)
// No such protocol registered.
_, err = connectWithProtocol(t, ln1.Addr(), "proto3")
require.Error(t, err)
// Now close the first listener to test that it's properly deregistered.
require.NoError(t, ln1.Close())
_, err = connectWithProtocol(t, ln1.Addr(), "proto1")
require.Error(t, err)
// connecting to the other listener should still be possible
id, err = connectWithProtocol(t, ln1.Addr(), "proto2")
require.NoError(t, err)
require.Equal(t, id2, id)
ln2.Close()
cm.Close()
checkClosed(t, cm)
}