ceremonyclient/go-libp2p/p2p/protocol/holepunch/holepunch_test.go
2024-06-07 06:25:43 +00:00

512 lines
16 KiB
Go

package holepunch_test
import (
"context"
"net"
"sync"
"testing"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-testing/race"
"github.com/libp2p/go-libp2p/core/host"
"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/p2p/protocol/circuitv2/proto"
relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
holepunch_pb "github.com/libp2p/go-libp2p/p2p/protocol/holepunch/pb"
"github.com/libp2p/go-libp2p/p2p/protocol/identify"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
"github.com/libp2p/go-msgio/pbio"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/stretchr/testify/require"
)
type mockEventTracer struct {
mutex sync.Mutex
events []*holepunch.Event
}
func (m *mockEventTracer) Trace(evt *holepunch.Event) {
m.mutex.Lock()
m.events = append(m.events, evt)
m.mutex.Unlock()
}
func (m *mockEventTracer) getEvents() []*holepunch.Event {
m.mutex.Lock()
defer m.mutex.Unlock()
// copy the slice
return append([]*holepunch.Event{}, m.events...)
}
var _ holepunch.EventTracer = &mockEventTracer{}
type mockMaddrFilter struct {
filterLocal func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
filterRemote func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
}
func (m mockMaddrFilter) FilterLocal(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
return m.filterLocal(remoteID, maddrs)
}
func (m mockMaddrFilter) FilterRemote(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
return m.filterRemote(remoteID, maddrs)
}
var _ holepunch.AddrFilter = &mockMaddrFilter{}
type mockIDService struct {
identify.IDService
}
var _ identify.IDService = &mockIDService{}
func newMockIDService(t *testing.T, h host.Host) identify.IDService {
ids, err := identify.NewIDService(h)
require.NoError(t, err)
ids.Start()
t.Cleanup(func() { ids.Close() })
return &mockIDService{IDService: ids}
}
func (s *mockIDService) OwnObservedAddrs() []ma.Multiaddr {
return append(s.IDService.OwnObservedAddrs(), ma.StringCast("/ip4/1.1.1.1/tcp/1234"))
}
func TestNoHolePunchIfDirectConnExists(t *testing.T) {
tr := &mockEventTracer{}
h1, hps := mkHostWithHolePunchSvc(t, holepunch.WithTracer(tr))
defer h1.Close()
h2, _ := mkHostWithHolePunchSvc(t)
defer h2.Close()
require.NoError(t, h1.Connect(context.Background(), peer.AddrInfo{
ID: h2.ID(),
Addrs: h2.Addrs(),
}))
time.Sleep(50 * time.Millisecond)
nc1 := len(h1.Network().ConnsToPeer(h2.ID()))
require.GreaterOrEqual(t, nc1, 1)
nc2 := len(h2.Network().ConnsToPeer(h1.ID()))
require.GreaterOrEqual(t, nc2, 1)
require.NoError(t, hps.DirectConnect(h2.ID()))
require.Len(t, h1.Network().ConnsToPeer(h2.ID()), nc1)
require.Len(t, h2.Network().ConnsToPeer(h1.ID()), nc2)
require.Empty(t, tr.getEvents())
}
func TestDirectDialWorks(t *testing.T) {
if race.WithRace() {
t.Skip("modifying manet.Private4 is racy")
}
// mark all addresses as public
cpy := manet.Private4
manet.Private4 = []*net.IPNet{}
defer func() { manet.Private4 = cpy }()
tr := &mockEventTracer{}
h1, h1ps := mkHostWithHolePunchSvc(t, holepunch.WithTracer(tr))
defer h1.Close()
h2, _ := mkHostWithHolePunchSvc(t)
defer h2.Close()
h2.RemoveStreamHandler(holepunch.Protocol)
h1.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), peerstore.ConnectedAddrTTL)
// try to hole punch without any connection and streams, if it works -> it's a direct connection
require.Empty(t, h1.Network().ConnsToPeer(h2.ID()))
require.NoError(t, h1ps.DirectConnect(h2.ID()))
require.GreaterOrEqual(t, len(h1.Network().ConnsToPeer(h2.ID())), 1)
require.GreaterOrEqual(t, len(h2.Network().ConnsToPeer(h1.ID())), 1)
events := tr.getEvents()
require.Len(t, events, 1)
require.Equal(t, holepunch.DirectDialEvtT, events[0].Type)
}
func TestEndToEndSimConnect(t *testing.T) {
h1tr := &mockEventTracer{}
h2tr := &mockEventTracer{}
h1, h2, relay, _ := makeRelayedHosts(t, []holepunch.Option{holepunch.WithTracer(h1tr)}, []holepunch.Option{holepunch.WithTracer(h2tr)}, true)
defer h1.Close()
defer h2.Close()
defer relay.Close()
// wait till a direct connection is complete
ensureDirectConn(t, h1, h2)
// ensure no hole-punching streams are open on either side
ensureNoHolePunchingStream(t, h1, h2)
var h2Events []*holepunch.Event
require.Eventually(t,
func() bool {
h2Events = h2tr.getEvents()
return len(h2Events) == 3
},
time.Second,
10*time.Millisecond,
)
require.Equal(t, holepunch.StartHolePunchEvtT, h2Events[0].Type)
require.Equal(t, holepunch.HolePunchAttemptEvtT, h2Events[1].Type)
require.Equal(t, holepunch.EndHolePunchEvtT, h2Events[2].Type)
h1Events := h1tr.getEvents()
// We don't really expect a hole-punched connection to be established in this test,
// as we probably don't get the timing right for the TCP simultaneous open.
// From time to time, it still happens occasionally, and then we get a EndHolePunchEvtT here.
if len(h1Events) != 2 && len(h1Events) != 3 {
t.Fatal("expected either 2 or 3 events")
}
require.Equal(t, holepunch.StartHolePunchEvtT, h1Events[0].Type)
require.Equal(t, holepunch.HolePunchAttemptEvtT, h1Events[1].Type)
if len(h1Events) == 3 {
require.Equal(t, holepunch.EndHolePunchEvtT, h1Events[2].Type)
}
}
func TestFailuresOnInitiator(t *testing.T) {
tcs := map[string]struct {
rhandler func(s network.Stream)
errMsg string
holePunchTimeout time.Duration
filter func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
}{
"responder does NOT send a CONNECT message": {
rhandler: func(s network.Stream) {
wr := pbio.NewDelimitedWriter(s)
wr.WriteMsg(&holepunch_pb.HolePunch{Type: holepunch_pb.HolePunch_SYNC.Enum()})
},
errMsg: "expect CONNECT message, got SYNC",
},
"responder does NOT support protocol": {
rhandler: nil,
},
"unable to READ CONNECT message from responder": {
rhandler: func(s network.Stream) {
s.Reset()
},
errMsg: "failed to read CONNECT message",
},
"responder does NOT reply within hole punch deadline": {
holePunchTimeout: 10 * time.Millisecond,
rhandler: func(s network.Stream) { time.Sleep(5 * time.Second) },
errMsg: "i/o deadline reached",
},
"no addrs after filtering": {
errMsg: "aborting hole punch initiation as we have no public address",
rhandler: func(s network.Stream) { time.Sleep(5 * time.Second) },
filter: func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
return []ma.Multiaddr{}
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.holePunchTimeout != 0 {
cpy := holepunch.StreamTimeout
holepunch.StreamTimeout = tc.holePunchTimeout
defer func() { holepunch.StreamTimeout = cpy }()
}
tr := &mockEventTracer{}
h1, h2, relay, _ := makeRelayedHosts(t, nil, nil, false)
defer h1.Close()
defer h2.Close()
defer relay.Close()
opts := []holepunch.Option{holepunch.WithTracer(tr)}
if tc.filter != nil {
f := mockMaddrFilter{
filterLocal: tc.filter,
filterRemote: tc.filter,
}
opts = append(opts, holepunch.WithAddrFilter(f))
}
hps := addHolePunchService(t, h2, opts...)
// wait until the hole punching protocol has actually started
require.Eventually(t, func() bool {
protos, _ := h2.Peerstore().SupportsProtocols(h1.ID(), holepunch.Protocol)
return len(protos) > 0
}, 200*time.Millisecond, 10*time.Millisecond)
if tc.rhandler != nil {
h1.SetStreamHandler(holepunch.Protocol, tc.rhandler)
} else {
h1.RemoveStreamHandler(holepunch.Protocol)
}
err := hps.DirectConnect(h1.ID())
require.Error(t, err)
if tc.errMsg != "" {
require.Contains(t, err.Error(), tc.errMsg)
}
})
}
}
func addrsToBytes(as []ma.Multiaddr) [][]byte {
bzs := make([][]byte, 0, len(as))
for _, a := range as {
bzs = append(bzs, a.Bytes())
}
return bzs
}
func TestFailuresOnResponder(t *testing.T) {
tcs := map[string]struct {
initiator func(s network.Stream)
errMsg string
holePunchTimeout time.Duration
filter func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr
}{
"initiator does NOT send a CONNECT message": {
initiator: func(s network.Stream) {
pbio.NewDelimitedWriter(s).WriteMsg(&holepunch_pb.HolePunch{Type: holepunch_pb.HolePunch_SYNC.Enum()})
},
errMsg: "expected CONNECT message",
},
"initiator does NOT send a SYNC message after a CONNECT message": {
initiator: func(s network.Stream) {
w := pbio.NewDelimitedWriter(s)
w.WriteMsg(&holepunch_pb.HolePunch{
Type: holepunch_pb.HolePunch_CONNECT.Enum(),
ObsAddrs: addrsToBytes([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/1234")}),
})
w.WriteMsg(&holepunch_pb.HolePunch{Type: holepunch_pb.HolePunch_CONNECT.Enum()})
},
errMsg: "expected SYNC message",
},
"initiator does NOT reply within hole punch deadline": {
holePunchTimeout: 10 * time.Millisecond,
initiator: func(s network.Stream) {
pbio.NewDelimitedWriter(s).WriteMsg(&holepunch_pb.HolePunch{
Type: holepunch_pb.HolePunch_CONNECT.Enum(),
ObsAddrs: addrsToBytes([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/1234")}),
})
time.Sleep(10 * time.Second)
},
errMsg: "i/o deadline reached",
},
"initiator does NOT send any addresses in CONNECT": {
holePunchTimeout: 10 * time.Millisecond,
initiator: func(s network.Stream) {
pbio.NewDelimitedWriter(s).WriteMsg(&holepunch_pb.HolePunch{Type: holepunch_pb.HolePunch_CONNECT.Enum()})
time.Sleep(10 * time.Second)
},
errMsg: "expected CONNECT message to contain at least one address",
},
"no addrs after filtering": {
errMsg: "rejecting hole punch request, as we don't have any public addresses",
initiator: func(s network.Stream) {
pbio.NewDelimitedWriter(s).WriteMsg(&holepunch_pb.HolePunch{
Type: holepunch_pb.HolePunch_CONNECT.Enum(),
ObsAddrs: addrsToBytes([]ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/1234")}),
})
time.Sleep(10 * time.Second)
},
filter: func(remoteID peer.ID, maddrs []ma.Multiaddr) []ma.Multiaddr {
return []ma.Multiaddr{}
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.holePunchTimeout != 0 {
cpy := holepunch.StreamTimeout
holepunch.StreamTimeout = tc.holePunchTimeout
defer func() { holepunch.StreamTimeout = cpy }()
}
tr := &mockEventTracer{}
opts := []holepunch.Option{holepunch.WithTracer(tr)}
if tc.filter != nil {
f := mockMaddrFilter{
filterLocal: tc.filter,
filterRemote: tc.filter,
}
opts = append(opts, holepunch.WithAddrFilter(f))
}
h1, h2, relay, _ := makeRelayedHosts(t, opts, nil, false)
defer h1.Close()
defer h2.Close()
defer relay.Close()
s, err := h2.NewStream(network.WithAllowLimitedConn(context.Background(), "holepunch"), h1.ID(), holepunch.Protocol)
require.NoError(t, err)
go tc.initiator(s)
getTracerError := func(tr *mockEventTracer) []string {
var errs []string
events := tr.getEvents()
for _, ev := range events {
if errEv, ok := ev.Evt.(*holepunch.ProtocolErrorEvt); ok {
errs = append(errs, errEv.Error)
}
}
return errs
}
require.Eventually(t, func() bool { return len(getTracerError(tr)) > 0 }, 5*time.Second, 100*time.Millisecond)
errs := getTracerError(tr)
require.Len(t, errs, 1)
require.Contains(t, errs[0], tc.errMsg)
})
}
}
func ensureNoHolePunchingStream(t *testing.T, h1, h2 host.Host) {
require.Eventually(t, func() bool {
for _, c := range h1.Network().ConnsToPeer(h2.ID()) {
for _, s := range c.GetStreams() {
if s.ID() == string(holepunch.Protocol) {
return false
}
}
}
return true
}, 5*time.Second, 50*time.Millisecond)
require.Eventually(t, func() bool {
for _, c := range h2.Network().ConnsToPeer(h1.ID()) {
for _, s := range c.GetStreams() {
if s.ID() == string(holepunch.Protocol) {
return false
}
}
}
return true
}, 5*time.Second, 50*time.Millisecond)
}
func ensureDirectConn(t *testing.T, h1, h2 host.Host) {
require.Eventually(t, func() bool {
for _, c := range h1.Network().ConnsToPeer(h2.ID()) {
if _, err := c.RemoteMultiaddr().ValueForProtocol(ma.P_CIRCUIT); err != nil {
return true
}
}
return false
}, 5*time.Second, 50*time.Millisecond)
require.Eventually(t, func() bool {
for _, c := range h2.Network().ConnsToPeer(h1.ID()) {
if _, err := c.RemoteMultiaddr().ValueForProtocol(ma.P_CIRCUIT); err != nil {
return true
}
}
return false
}, 5*time.Second, 50*time.Millisecond)
}
func mkHostWithStaticAutoRelay(t *testing.T, relay host.Host) host.Host {
if race.WithRace() {
t.Skip("modifying manet.Private4 is racy")
}
pi := peer.AddrInfo{
ID: relay.ID(),
Addrs: relay.Addrs(),
}
cpy := manet.Private4
manet.Private4 = []*net.IPNet{}
defer func() { manet.Private4 = cpy }()
h, err := libp2p.New(
libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0")),
libp2p.EnableRelay(),
libp2p.EnableAutoRelayWithStaticRelays([]peer.AddrInfo{pi}),
libp2p.ForceReachabilityPrivate(),
libp2p.ResourceManager(&network.NullResourceManager{}),
)
require.NoError(t, err)
// wait till we have a relay addr
require.Eventually(t, func() bool {
for _, a := range h.Addrs() {
if _, err := a.ValueForProtocol(ma.P_CIRCUIT); err == nil {
return true
}
}
return false
}, 5*time.Second, 50*time.Millisecond)
return h
}
func makeRelayedHosts(t *testing.T, h1opt, h2opt []holepunch.Option, addHolePuncher bool) (h1, h2, relay host.Host, hps *holepunch.Service) {
t.Helper()
h1, _ = mkHostWithHolePunchSvc(t, h1opt...)
var err error
relay, err = libp2p.New(
libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0")),
libp2p.DisableRelay(),
libp2p.ResourceManager(&network.NullResourceManager{}),
)
require.NoError(t, err)
_, err = relayv2.New(relay)
require.NoError(t, err)
// make sure the relay service is started and advertised by Identify
h, err := libp2p.New(
libp2p.NoListenAddrs,
libp2p.Transport(tcp.NewTCPTransport),
libp2p.DisableRelay(),
)
require.NoError(t, err)
defer h.Close()
require.NoError(t, h.Connect(context.Background(), peer.AddrInfo{ID: relay.ID(), Addrs: relay.Addrs()}))
require.Eventually(t, func() bool {
supported, err := h.Peerstore().SupportsProtocols(relay.ID(), proto.ProtoIDv2Hop)
return err == nil && len(supported) > 0
}, 3*time.Second, 100*time.Millisecond)
h2 = mkHostWithStaticAutoRelay(t, relay)
if addHolePuncher {
hps = addHolePunchService(t, h2, h2opt...)
}
// h1 has a relay addr
// h2 should connect to the relay addr
var raddr ma.Multiaddr
for _, a := range h2.Addrs() {
if _, err := a.ValueForProtocol(ma.P_CIRCUIT); err == nil {
raddr = a
break
}
}
require.NotEmpty(t, raddr)
require.NoError(t, h1.Connect(context.Background(), peer.AddrInfo{
ID: h2.ID(),
Addrs: []ma.Multiaddr{raddr},
}))
return
}
func addHolePunchService(t *testing.T, h host.Host, opts ...holepunch.Option) *holepunch.Service {
t.Helper()
hps, err := holepunch.NewService(h, newMockIDService(t, h), opts...)
require.NoError(t, err)
return hps
}
func mkHostWithHolePunchSvc(t *testing.T, opts ...holepunch.Option) (host.Host, *holepunch.Service) {
t.Helper()
h, err := libp2p.New(
libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0"), ma.StringCast("/ip6/::1/tcp/0")),
libp2p.ForceReachabilityPrivate(),
libp2p.ResourceManager(&network.NullResourceManager{}),
)
require.NoError(t, err)
hps, err := holepunch.NewService(h, newMockIDService(t, h), opts...)
require.NoError(t, err)
return h, hps
}