From 03a37d32fcea81344ff3f95e088db303cf3d3c91 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Tue, 10 Jan 2023 17:22:46 +1300 Subject: [PATCH] tests: add integration tests for gating of outgoing connections --- core/connmgr/gater.go | 1 - p2p/test/connectiongating/gating_test.go | 286 ++++++++++++++++++ .../mock_connection_gater_test.go | 109 +++++++ 3 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 p2p/test/connectiongating/gating_test.go create mode 100644 p2p/test/connectiongating/mock_connection_gater_test.go diff --git a/core/connmgr/gater.go b/core/connmgr/gater.go index 672aef9528..82fa56a876 100644 --- a/core/connmgr/gater.go +++ b/core/connmgr/gater.go @@ -52,7 +52,6 @@ import ( // DisconnectReasons is that we require stream multiplexing capability to open a // control protocol stream to transmit the message. type ConnectionGater interface { - // InterceptPeerDial tests whether we're permitted to Dial the specified peer. // // This is called by the network.Network implementation when dialling a peer. diff --git a/p2p/test/connectiongating/gating_test.go b/p2p/test/connectiongating/gating_test.go new file mode 100644 index 0000000000..3df743c8ef --- /dev/null +++ b/p2p/test/connectiongating/gating_test.go @@ -0,0 +1,286 @@ +package connectiongating + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/libp2p/go-libp2p/p2p/net/swarm" + + "github.com/golang/mock/gomock" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" +) + +//go:generate go run github.com/golang/mock/mockgen -package connectiongating -destination mock_connection_gater_test.go github.com/libp2p/go-libp2p/core/connmgr ConnectionGater + +// This list should contain (at least) one address for every transport we have. +var addrs = []ma.Multiaddr{ + ma.StringCast("/ip4/127.0.0.1/tcp/0"), + ma.StringCast("/ip4/127.0.0.1/tcp/0/ws"), + ma.StringCast("/ip4/127.0.0.1/udp/0/quic"), + ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1"), + ma.StringCast("/ip4/127.0.0.1/udp/0/quic-v1/webtransport"), +} + +func transportName(a ma.Multiaddr) string { + _, tr := ma.SplitLast(a) + return tr.Protocol().Name +} + +func stripCertHash(addr ma.Multiaddr) ma.Multiaddr { + for { + if _, err := addr.ValueForProtocol(ma.P_CERTHASH); err != nil { + break + } + addr, _ = ma.SplitLast(addr) + } + return addr +} + +func TestInterceptPeerDial(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("dialing %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New(libp2p.ConnectionGater(connGater)) + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New(libp2p.ListenAddrs(a)) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + connGater.EXPECT().InterceptPeerDial(h2.ID()) + require.ErrorIs(t, h1.Connect(ctx, peer.AddrInfo{ID: h2.ID(), Addrs: h2.Addrs()}), swarm.ErrGaterDisallowedConnection) + }) + } +} + +func TestInterceptAddrDial(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("dialing %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New(libp2p.ConnectionGater(connGater)) + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New(libp2p.ListenAddrs(a)) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + gomock.InOrder( + connGater.EXPECT().InterceptPeerDial(h2.ID()).Return(true), + connGater.EXPECT().InterceptAddrDial(h2.ID(), h2.Addrs()[0]), + ) + require.ErrorIs(t, h1.Connect(ctx, peer.AddrInfo{ID: h2.ID(), Addrs: h2.Addrs()}), swarm.ErrNoGoodAddresses) + }) + } +} + +func TestInterceptSecuredOutgoing(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("dialing %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New(libp2p.ConnectionGater(connGater)) + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New(libp2p.ListenAddrs(a)) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + gomock.InOrder( + connGater.EXPECT().InterceptPeerDial(h2.ID()).Return(true), + connGater.EXPECT().InterceptAddrDial(h2.ID(), gomock.Any()).Return(true), + connGater.EXPECT().InterceptSecured(network.DirOutbound, h2.ID(), gomock.Any()).Do(func(_ network.Direction, _ peer.ID, addrs network.ConnMultiaddrs) { + // remove the certhash component from WebTransport addresses + require.Equal(t, stripCertHash(h2.Addrs()[0]).String(), addrs.RemoteMultiaddr().String()) + }), + ) + err = h1.Connect(ctx, peer.AddrInfo{ID: h2.ID(), Addrs: h2.Addrs()}) + require.Error(t, err) + // There's a bug in the WebSocket library, making Close block for up to 5s. + // See https://github.com/nhooyr/websocket/issues/355 for details. + if _, err := a.ValueForProtocol(ma.P_WS); err == nil { + require.NotErrorIs(t, err, context.DeadlineExceeded) + } + }) + } +} + +func TestInterceptUpgradedOutgoing(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("dialing %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New(libp2p.ConnectionGater(connGater)) + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New(libp2p.ListenAddrs(a)) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + gomock.InOrder( + connGater.EXPECT().InterceptPeerDial(h2.ID()).Return(true), + connGater.EXPECT().InterceptAddrDial(h2.ID(), gomock.Any()).Return(true), + connGater.EXPECT().InterceptSecured(network.DirOutbound, h2.ID(), gomock.Any()).Return(true), + connGater.EXPECT().InterceptUpgraded(gomock.Any()).Do(func(c network.Conn) { + // remove the certhash component from WebTransport addresses + require.Equal(t, stripCertHash(h2.Addrs()[0]), c.RemoteMultiaddr()) + require.Equal(t, h1.ID(), c.LocalPeer()) + require.Equal(t, h2.ID(), c.RemotePeer()) + })) + err = h1.Connect(ctx, peer.AddrInfo{ID: h2.ID(), Addrs: h2.Addrs()}) + require.Error(t, err) + // There's a bug in the WebSocket library, making Close block for up to 5s. + // See https://github.com/nhooyr/websocket/issues/355 for details. + if _, err := a.ValueForProtocol(ma.P_WS); err == nil { + require.NotErrorIs(t, err, context.DeadlineExceeded) + } + }) + } +} + +func TestInterceptAccept(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("accepting %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New() + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New( + libp2p.ListenAddrs(a), + libp2p.ConnectionGater(connGater), + ) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + // The basic host dials the first connection. + connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) { + // remove the certhash component from WebTransport addresses + require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr()) + }) + h1.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), time.Hour) + _, err = h1.NewStream(ctx, h2.ID(), protocol.TestingID) + require.Error(t, err) + // There's a bug in the WebSocket library, making Close block for up to 5s. + // See https://github.com/nhooyr/websocket/issues/355 for details. + if _, err := a.ValueForProtocol(ma.P_WS); err == nil { + require.NotErrorIs(t, err, context.DeadlineExceeded) + } + }) + } +} + +func TestInterceptSecuredIncoming(t *testing.T) { + for _, a := range addrs { + t.Run(fmt.Sprintf("accepting %s", transportName(a)), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New() + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New( + libp2p.ListenAddrs(a), + libp2p.ConnectionGater(connGater), + ) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + gomock.InOrder( + connGater.EXPECT().InterceptAccept(gomock.Any()).Return(true), + connGater.EXPECT().InterceptSecured(network.DirInbound, h1.ID(), gomock.Any()).Do(func(_ network.Direction, _ peer.ID, addrs network.ConnMultiaddrs) { + // remove the certhash component from WebTransport addresses + require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr()) + }), + ) + h1.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), time.Hour) + _, err = h1.NewStream(ctx, h2.ID(), protocol.TestingID) + require.Error(t, err) + // There's a bug in the WebSocket library, making Close block for up to 5s. + // See https://github.com/nhooyr/websocket/issues/355 for details. + if _, err := a.ValueForProtocol(ma.P_WS); err == nil { + require.NotErrorIs(t, err, context.DeadlineExceeded) + } + }) + } +} + +func TestInterceptUpgradedIncoming(t *testing.T) { + for _, a := range addrs { + _, tr := ma.SplitLast(a) + t.Run(fmt.Sprintf("accepting %s", tr.Protocol().Name), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + connGater := NewMockConnectionGater(ctrl) + + h1, err := libp2p.New() + require.NoError(t, err) + defer h1.Close() + h2, err := libp2p.New( + libp2p.ListenAddrs(a), + libp2p.ConnectionGater(connGater), + ) + require.NoError(t, err) + defer h2.Close() + require.Len(t, h2.Addrs(), 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + gomock.InOrder( + connGater.EXPECT().InterceptAccept(gomock.Any()).Return(true), + connGater.EXPECT().InterceptSecured(network.DirInbound, h1.ID(), gomock.Any()).Return(true), + connGater.EXPECT().InterceptUpgraded(gomock.Any()).Do(func(c network.Conn) { + // remove the certhash component from WebTransport addresses + require.Equal(t, stripCertHash(h2.Addrs()[0]), c.LocalMultiaddr()) + require.Equal(t, h1.ID(), c.RemotePeer()) + require.Equal(t, h2.ID(), c.LocalPeer()) + }), + ) + h1.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), time.Hour) + _, err = h1.NewStream(ctx, h2.ID(), protocol.TestingID) + require.Error(t, err) + // There's a bug in the WebSocket library, making Close block for up to 5s. + // See https://github.com/nhooyr/websocket/issues/355 for details. + if _, err := a.ValueForProtocol(ma.P_WS); err == nil { + require.NotErrorIs(t, err, context.DeadlineExceeded) + } + }) + } +} diff --git a/p2p/test/connectiongating/mock_connection_gater_test.go b/p2p/test/connectiongating/mock_connection_gater_test.go new file mode 100644 index 0000000000..54be42e563 --- /dev/null +++ b/p2p/test/connectiongating/mock_connection_gater_test.go @@ -0,0 +1,109 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p/core/connmgr (interfaces: ConnectionGater) + +// Package connectiongating is a generated GoMock package. +package connectiongating + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + control "github.com/libp2p/go-libp2p/core/control" + network "github.com/libp2p/go-libp2p/core/network" + peer "github.com/libp2p/go-libp2p/core/peer" + multiaddr "github.com/multiformats/go-multiaddr" +) + +// MockConnectionGater is a mock of ConnectionGater interface. +type MockConnectionGater struct { + ctrl *gomock.Controller + recorder *MockConnectionGaterMockRecorder +} + +// MockConnectionGaterMockRecorder is the mock recorder for MockConnectionGater. +type MockConnectionGaterMockRecorder struct { + mock *MockConnectionGater +} + +// NewMockConnectionGater creates a new mock instance. +func NewMockConnectionGater(ctrl *gomock.Controller) *MockConnectionGater { + mock := &MockConnectionGater{ctrl: ctrl} + mock.recorder = &MockConnectionGaterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConnectionGater) EXPECT() *MockConnectionGaterMockRecorder { + return m.recorder +} + +// InterceptAccept mocks base method. +func (m *MockConnectionGater) InterceptAccept(arg0 network.ConnMultiaddrs) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterceptAccept", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// InterceptAccept indicates an expected call of InterceptAccept. +func (mr *MockConnectionGaterMockRecorder) InterceptAccept(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptAccept", reflect.TypeOf((*MockConnectionGater)(nil).InterceptAccept), arg0) +} + +// InterceptAddrDial mocks base method. +func (m *MockConnectionGater) InterceptAddrDial(arg0 peer.ID, arg1 multiaddr.Multiaddr) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterceptAddrDial", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// InterceptAddrDial indicates an expected call of InterceptAddrDial. +func (mr *MockConnectionGaterMockRecorder) InterceptAddrDial(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptAddrDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptAddrDial), arg0, arg1) +} + +// InterceptPeerDial mocks base method. +func (m *MockConnectionGater) InterceptPeerDial(arg0 peer.ID) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterceptPeerDial", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// InterceptPeerDial indicates an expected call of InterceptPeerDial. +func (mr *MockConnectionGaterMockRecorder) InterceptPeerDial(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptPeerDial", reflect.TypeOf((*MockConnectionGater)(nil).InterceptPeerDial), arg0) +} + +// InterceptSecured mocks base method. +func (m *MockConnectionGater) InterceptSecured(arg0 network.Direction, arg1 peer.ID, arg2 network.ConnMultiaddrs) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterceptSecured", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + return ret0 +} + +// InterceptSecured indicates an expected call of InterceptSecured. +func (mr *MockConnectionGaterMockRecorder) InterceptSecured(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptSecured", reflect.TypeOf((*MockConnectionGater)(nil).InterceptSecured), arg0, arg1, arg2) +} + +// InterceptUpgraded mocks base method. +func (m *MockConnectionGater) InterceptUpgraded(arg0 network.Conn) (bool, control.DisconnectReason) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterceptUpgraded", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(control.DisconnectReason) + return ret0, ret1 +} + +// InterceptUpgraded indicates an expected call of InterceptUpgraded. +func (mr *MockConnectionGaterMockRecorder) InterceptUpgraded(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterceptUpgraded", reflect.TypeOf((*MockConnectionGater)(nil).InterceptUpgraded), arg0) +}