package p2p_test import ( "context" "io" "net" "testing" "time" "github.com/fortytw2/leaktest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/internal/p2p" "github.com/tendermint/tendermint/libs/bytes" "github.com/tendermint/tendermint/types" ) // transportFactory is used to set up transports for tests. type transportFactory func(t *testing.T) p2p.Transport // testTransports is a registry of transport factories for withTransports(). var testTransports = map[string]transportFactory{} // withTransports is a test helper that runs a test against all transports // registered in testTransports. func withTransports(t *testing.T, tester func(*testing.T, transportFactory)) { t.Helper() for name, transportFactory := range testTransports { transportFactory := transportFactory t.Run(name, func(t *testing.T) { t.Cleanup(leaktest.Check(t)) tester(t, transportFactory) }) } } func TestTransport_AcceptClose(t *testing.T) { // Just test accept unblock on close, happy path is tested widely elsewhere. withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) // In-progress Accept should error on concurrent close. errCh := make(chan error, 1) go func() { time.Sleep(200 * time.Millisecond) errCh <- a.Close() }() _, err := a.Accept() require.Error(t, err) require.Equal(t, io.EOF, err) require.NoError(t, <-errCh) // Closed transport should return error immediately. _, err = a.Accept() require.Error(t, err) require.Equal(t, io.EOF, err) }) } func TestTransport_DialEndpoints(t *testing.T) { ipTestCases := []struct { ip net.IP ok bool }{ {net.IPv4zero, true}, {net.IPv6zero, true}, {nil, false}, {net.IPv4bcast, false}, {net.IPv4allsys, false}, {[]byte{1, 2, 3}, false}, {[]byte{1, 2, 3, 4, 5}, false}, } withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) endpoints := a.Endpoints() require.NotEmpty(t, endpoints) endpoint := endpoints[0] // Spawn a goroutine to simply accept any connections until closed. go func() { for { conn, err := a.Accept() if err != nil { return } _ = conn.Close() } }() // Dialing self should work. conn, err := a.Dial(ctx, endpoint) require.NoError(t, err) require.NoError(t, conn.Close()) // Dialing empty endpoint should error. _, err = a.Dial(ctx, p2p.Endpoint{}) require.Error(t, err) // Dialing without protocol should error. noProtocol := endpoint noProtocol.Protocol = "" _, err = a.Dial(ctx, noProtocol) require.Error(t, err) // Dialing with invalid protocol should error. fooProtocol := endpoint fooProtocol.Protocol = "foo" _, err = a.Dial(ctx, fooProtocol) require.Error(t, err) // Tests for networked endpoints (with IP). if len(endpoint.IP) > 0 && endpoint.Protocol != p2p.MemoryProtocol { for _, tc := range ipTestCases { tc := tc t.Run(tc.ip.String(), func(t *testing.T) { e := endpoint e.IP = tc.ip conn, err := a.Dial(ctx, e) if tc.ok { require.NoError(t, conn.Close()) require.NoError(t, err) } else { require.Error(t, err, "endpoint=%s", e) } }) } // Non-networked endpoints should error. noIP := endpoint noIP.IP = nil noIP.Port = 0 noIP.Path = "foo" _, err := a.Dial(ctx, noIP) require.Error(t, err) } else { // Tests for non-networked endpoints (no IP). noPath := endpoint noPath.Path = "" _, err = a.Dial(ctx, noPath) require.Error(t, err) } }) } func TestTransport_Dial(t *testing.T) { // Most just tests dial failures, happy path is tested widely elsewhere. withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) require.NotEmpty(t, a.Endpoints()) require.NotEmpty(t, b.Endpoints()) aEndpoint := a.Endpoints()[0] bEndpoint := b.Endpoints()[0] // Context cancellation should error. We can't test timeouts since we'd // need a non-responsive endpoint. cancelCtx, cancel := context.WithCancel(ctx) cancel() _, err := a.Dial(cancelCtx, bEndpoint) require.Error(t, err) require.Equal(t, err, context.Canceled) // Unavailable endpoint should error. err = b.Close() require.NoError(t, err) _, err = a.Dial(ctx, bEndpoint) require.Error(t, err) // Dialing from a closed transport should still work. errCh := make(chan error, 1) go func() { conn, err := a.Accept() if err == nil { _ = conn.Close() } errCh <- err }() conn, err := b.Dial(ctx, aEndpoint) require.NoError(t, err) require.NoError(t, conn.Close()) require.NoError(t, <-errCh) }) } func TestTransport_Endpoints(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) // Both transports return valid and different endpoints. aEndpoints := a.Endpoints() bEndpoints := b.Endpoints() require.NotEmpty(t, aEndpoints) require.NotEmpty(t, bEndpoints) require.NotEqual(t, aEndpoints, bEndpoints) for _, endpoint := range append(aEndpoints, bEndpoints...) { err := endpoint.Validate() require.NoError(t, err, "invalid endpoint %q", endpoint) } // When closed, the transport should no longer return any endpoints. err := a.Close() require.NoError(t, err) require.Empty(t, a.Endpoints()) require.NotEmpty(t, b.Endpoints()) }) } func TestTransport_Protocols(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) protocols := a.Protocols() endpoints := a.Endpoints() require.NotEmpty(t, protocols) require.NotEmpty(t, endpoints) for _, endpoint := range endpoints { require.Contains(t, protocols, endpoint.Protocol) } }) } func TestTransport_String(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) require.NotEmpty(t, a.String()) }) } func TestConnection_Handshake(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, ba := dialAccept(t, a, b) // A handshake should pass the given keys and NodeInfo. aKey := ed25519.GenPrivKey() aInfo := types.NodeInfo{ NodeID: types.NodeIDFromPubKey(aKey.PubKey()), ProtocolVersion: types.ProtocolVersion{ P2P: 1, Block: 2, App: 3, }, ListenAddr: "listenaddr", Network: "network", Version: "1.2.3", Channels: bytes.HexBytes([]byte{0xf0, 0x0f}), Moniker: "moniker", Other: types.NodeInfoOther{ TxIndex: "txindex", RPCAddress: "rpc.domain.com", }, } bKey := ed25519.GenPrivKey() bInfo := types.NodeInfo{NodeID: types.NodeIDFromPubKey(bKey.PubKey())} errCh := make(chan error, 1) go func() { // Must use assert due to goroutine. peerInfo, peerKey, err := ba.Handshake(ctx, bInfo, bKey) if err == nil { assert.Equal(t, aInfo, peerInfo) assert.Equal(t, aKey.PubKey(), peerKey) } errCh <- err }() peerInfo, peerKey, err := ab.Handshake(ctx, aInfo, aKey) require.NoError(t, err) require.Equal(t, bInfo, peerInfo) require.Equal(t, bKey.PubKey(), peerKey) require.NoError(t, <-errCh) }) } func TestConnection_HandshakeCancel(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) // Handshake should error on context cancellation. ab, ba := dialAccept(t, a, b) timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) cancel() _, _, err := ab.Handshake(timeoutCtx, types.NodeInfo{}, ed25519.GenPrivKey()) require.Error(t, err) require.Equal(t, context.Canceled, err) _ = ab.Close() _ = ba.Close() // Handshake should error on context timeout. ab, ba = dialAccept(t, a, b) timeoutCtx, cancel = context.WithTimeout(ctx, 200*time.Millisecond) defer cancel() _, _, err = ab.Handshake(timeoutCtx, types.NodeInfo{}, ed25519.GenPrivKey()) require.Error(t, err) require.Equal(t, context.DeadlineExceeded, err) _ = ab.Close() _ = ba.Close() }) } func TestConnection_FlushClose(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, _ := dialAcceptHandshake(t, a, b) // FIXME: FlushClose should be removed (and replaced by separate Flush // and Close calls if necessary). We can't reliably test it, so we just // make sure it closes both ends and that it's idempotent. err := ab.FlushClose() require.NoError(t, err) _, _, err = ab.ReceiveMessage() require.Error(t, err) require.Equal(t, io.EOF, err) _, err = ab.SendMessage(chID, []byte("closed")) require.Error(t, err) require.Equal(t, io.EOF, err) err = ab.FlushClose() require.NoError(t, err) }) } func TestConnection_LocalRemoteEndpoint(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, ba := dialAcceptHandshake(t, a, b) // Local and remote connection endpoints correspond to each other. require.NotEmpty(t, ab.LocalEndpoint()) require.NotEmpty(t, ba.LocalEndpoint()) require.Equal(t, ab.LocalEndpoint(), ba.RemoteEndpoint()) require.Equal(t, ab.RemoteEndpoint(), ba.LocalEndpoint()) }) } func TestConnection_SendReceive(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, ba := dialAcceptHandshake(t, a, b) // Can send and receive a to b. ok, err := ab.SendMessage(chID, []byte("foo")) require.NoError(t, err) require.True(t, ok) ch, msg, err := ba.ReceiveMessage() require.NoError(t, err) require.Equal(t, []byte("foo"), msg) require.Equal(t, chID, ch) // Can send and receive b to a. _, err = ba.SendMessage(chID, []byte("bar")) require.NoError(t, err) _, msg, err = ab.ReceiveMessage() require.NoError(t, err) require.Equal(t, []byte("bar"), msg) // TrySendMessage also works. ok, err = ba.TrySendMessage(chID, []byte("try")) require.NoError(t, err) require.True(t, ok) ch, msg, err = ab.ReceiveMessage() require.NoError(t, err) require.Equal(t, []byte("try"), msg) require.Equal(t, chID, ch) // Connections should still be active after closing the transports. err = a.Close() require.NoError(t, err) err = b.Close() require.NoError(t, err) _, err = ab.SendMessage(chID, []byte("still here")) require.NoError(t, err) ch, msg, err = ba.ReceiveMessage() require.NoError(t, err) require.Equal(t, chID, ch) require.Equal(t, []byte("still here"), msg) // Close one side of the connection. Both sides should then error // with io.EOF when trying to send or receive. err = ba.Close() require.NoError(t, err) _, _, err = ab.ReceiveMessage() require.Error(t, err) require.Equal(t, io.EOF, err) _, err = ab.TrySendMessage(chID, []byte("closed try")) require.Error(t, err) require.Equal(t, io.EOF, err) _, err = ab.SendMessage(chID, []byte("closed")) require.Error(t, err) require.Equal(t, io.EOF, err) _, _, err = ba.ReceiveMessage() require.Error(t, err) require.Equal(t, io.EOF, err) _, err = ba.TrySendMessage(chID, []byte("closed try")) require.Error(t, err) require.Equal(t, io.EOF, err) _, err = ba.SendMessage(chID, []byte("closed")) require.Error(t, err) require.Equal(t, io.EOF, err) }) } func TestConnection_Status(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, _ := dialAcceptHandshake(t, a, b) // FIXME: This isn't implemented in all transports, so for now we just // check that it doesn't panic, which isn't really much of a test. ab.Status() }) } func TestConnection_String(t *testing.T) { withTransports(t, func(t *testing.T, makeTransport transportFactory) { a := makeTransport(t) b := makeTransport(t) ab, _ := dialAccept(t, a, b) require.NotEmpty(t, ab.String()) }) } func TestEndpoint_NodeAddress(t *testing.T) { var ( ip4 = []byte{1, 2, 3, 4} ip4in6 = net.IPv4(1, 2, 3, 4) ip6 = []byte{0xb1, 0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01} id = types.NodeID("00112233445566778899aabbccddeeff00112233") ) testcases := []struct { endpoint p2p.Endpoint expect p2p.NodeAddress }{ // Valid endpoints. { p2p.Endpoint{Protocol: "tcp", IP: ip4, Port: 8080, Path: "path"}, p2p.NodeAddress{Protocol: "tcp", Hostname: "1.2.3.4", Port: 8080, Path: "path"}, }, { p2p.Endpoint{Protocol: "tcp", IP: ip4in6, Port: 8080, Path: "path"}, p2p.NodeAddress{Protocol: "tcp", Hostname: "1.2.3.4", Port: 8080, Path: "path"}, }, { p2p.Endpoint{Protocol: "tcp", IP: ip6, Port: 8080, Path: "path"}, p2p.NodeAddress{Protocol: "tcp", Hostname: "b10c::1", Port: 8080, Path: "path"}, }, { p2p.Endpoint{Protocol: "memory", Path: "foo"}, p2p.NodeAddress{Protocol: "memory", Path: "foo"}, }, { p2p.Endpoint{Protocol: "memory", Path: string(id)}, p2p.NodeAddress{Protocol: "memory", Path: string(id)}, }, // Partial (invalid) endpoints. {p2p.Endpoint{}, p2p.NodeAddress{}}, {p2p.Endpoint{Protocol: "tcp"}, p2p.NodeAddress{Protocol: "tcp"}}, {p2p.Endpoint{IP: net.IPv4(1, 2, 3, 4)}, p2p.NodeAddress{Hostname: "1.2.3.4"}}, {p2p.Endpoint{Port: 8080}, p2p.NodeAddress{}}, {p2p.Endpoint{Path: "path"}, p2p.NodeAddress{Path: "path"}}, } for _, tc := range testcases { tc := tc t.Run(tc.endpoint.String(), func(t *testing.T) { // Without NodeID. expect := tc.expect require.Equal(t, expect, tc.endpoint.NodeAddress("")) // With NodeID. expect.NodeID = id require.Equal(t, expect, tc.endpoint.NodeAddress(expect.NodeID)) }) } } func TestEndpoint_String(t *testing.T) { var ( ip4 = []byte{1, 2, 3, 4} ip4in6 = net.IPv4(1, 2, 3, 4) ip6 = []byte{0xb1, 0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01} nodeID = types.NodeID("00112233445566778899aabbccddeeff00112233") ) testcases := []struct { endpoint p2p.Endpoint expect string }{ // Non-networked endpoints. {p2p.Endpoint{Protocol: "memory", Path: string(nodeID)}, "memory:" + string(nodeID)}, {p2p.Endpoint{Protocol: "file", Path: "foo"}, "file:///foo"}, {p2p.Endpoint{Protocol: "file", Path: "👋"}, "file:///%F0%9F%91%8B"}, // IPv4 endpoints. {p2p.Endpoint{Protocol: "tcp", IP: ip4}, "tcp://1.2.3.4"}, {p2p.Endpoint{Protocol: "tcp", IP: ip4in6}, "tcp://1.2.3.4"}, {p2p.Endpoint{Protocol: "tcp", IP: ip4, Port: 8080}, "tcp://1.2.3.4:8080"}, {p2p.Endpoint{Protocol: "tcp", IP: ip4, Port: 8080, Path: "/path"}, "tcp://1.2.3.4:8080/path"}, {p2p.Endpoint{Protocol: "tcp", IP: ip4, Path: "path/👋"}, "tcp://1.2.3.4/path/%F0%9F%91%8B"}, // IPv6 endpoints. {p2p.Endpoint{Protocol: "tcp", IP: ip6}, "tcp://b10c::1"}, {p2p.Endpoint{Protocol: "tcp", IP: ip6, Port: 8080}, "tcp://[b10c::1]:8080"}, {p2p.Endpoint{Protocol: "tcp", IP: ip6, Port: 8080, Path: "/path"}, "tcp://[b10c::1]:8080/path"}, {p2p.Endpoint{Protocol: "tcp", IP: ip6, Path: "path/👋"}, "tcp://b10c::1/path/%F0%9F%91%8B"}, // Partial (invalid) endpoints. {p2p.Endpoint{}, ""}, {p2p.Endpoint{Protocol: "tcp"}, "tcp:"}, {p2p.Endpoint{IP: []byte{1, 2, 3, 4}}, "1.2.3.4"}, {p2p.Endpoint{IP: []byte{0xb1, 0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}}, "b10c::1"}, {p2p.Endpoint{Port: 8080}, ""}, {p2p.Endpoint{Path: "foo"}, "/foo"}, } for _, tc := range testcases { tc := tc t.Run(tc.expect, func(t *testing.T) { require.Equal(t, tc.expect, tc.endpoint.String()) }) } } func TestEndpoint_Validate(t *testing.T) { var ( ip4 = []byte{1, 2, 3, 4} ip4in6 = net.IPv4(1, 2, 3, 4) ip6 = []byte{0xb1, 0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01} ) testcases := []struct { endpoint p2p.Endpoint expectValid bool }{ // Valid endpoints. {p2p.Endpoint{Protocol: "tcp", IP: ip4}, true}, {p2p.Endpoint{Protocol: "tcp", IP: ip4in6}, true}, {p2p.Endpoint{Protocol: "tcp", IP: ip6}, true}, {p2p.Endpoint{Protocol: "tcp", IP: ip4, Port: 8008}, true}, {p2p.Endpoint{Protocol: "tcp", IP: ip4, Port: 8080, Path: "path"}, true}, {p2p.Endpoint{Protocol: "memory", Path: "path"}, true}, // Invalid endpoints. {p2p.Endpoint{}, false}, {p2p.Endpoint{IP: ip4}, false}, {p2p.Endpoint{Protocol: "tcp"}, false}, {p2p.Endpoint{Protocol: "tcp", IP: []byte{1, 2, 3}}, false}, {p2p.Endpoint{Protocol: "tcp", Port: 8080, Path: "path"}, false}, } for _, tc := range testcases { tc := tc t.Run(tc.endpoint.String(), func(t *testing.T) { err := tc.endpoint.Validate() if tc.expectValid { require.NoError(t, err) } else { require.Error(t, err) } }) } } // dialAccept is a helper that dials b from a and returns both sides of the // connection. func dialAccept(t *testing.T, a, b p2p.Transport) (p2p.Connection, p2p.Connection) { t.Helper() endpoints := b.Endpoints() require.NotEmpty(t, endpoints, "peer not listening on any endpoints") ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() acceptCh := make(chan p2p.Connection, 1) errCh := make(chan error, 1) go func() { conn, err := b.Accept() errCh <- err acceptCh <- conn }() dialConn, err := a.Dial(ctx, endpoints[0]) require.NoError(t, err) acceptConn := <-acceptCh require.NoError(t, <-errCh) t.Cleanup(func() { _ = dialConn.Close() _ = acceptConn.Close() }) return dialConn, acceptConn } // dialAcceptHandshake is a helper that dials and handshakes b from a and // returns both sides of the connection. func dialAcceptHandshake(t *testing.T, a, b p2p.Transport) (p2p.Connection, p2p.Connection) { t.Helper() ab, ba := dialAccept(t, a, b) ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() errCh := make(chan error, 1) go func() { privKey := ed25519.GenPrivKey() nodeInfo := types.NodeInfo{NodeID: types.NodeIDFromPubKey(privKey.PubKey())} _, _, err := ba.Handshake(ctx, nodeInfo, privKey) errCh <- err }() privKey := ed25519.GenPrivKey() nodeInfo := types.NodeInfo{NodeID: types.NodeIDFromPubKey(privKey.PubKey())} _, _, err := ab.Handshake(ctx, nodeInfo, privKey) require.NoError(t, err) timer := time.NewTimer(2 * time.Second) defer timer.Stop() select { case err := <-errCh: require.NoError(t, err) case <-timer.C: require.Fail(t, "handshake timed out") } return ab, ba }