- package p2p
-
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "math"
- "net"
- "strconv"
- "sync"
-
- "golang.org/x/net/netutil"
-
- "github.com/tendermint/tendermint/crypto"
- "github.com/tendermint/tendermint/internal/libs/protoio"
- "github.com/tendermint/tendermint/internal/p2p/conn"
- "github.com/tendermint/tendermint/libs/log"
- p2pproto "github.com/tendermint/tendermint/proto/tendermint/p2p"
- "github.com/tendermint/tendermint/types"
- )
-
- const (
- MConnProtocol Protocol = "mconn"
- TCPProtocol Protocol = "tcp"
- )
-
- // MConnTransportOptions sets options for MConnTransport.
- type MConnTransportOptions struct {
- // MaxAcceptedConnections is the maximum number of simultaneous accepted
- // (incoming) connections. Beyond this, new connections will block until
- // a slot is free. 0 means unlimited.
- //
- // FIXME: We may want to replace this with connection accounting in the
- // Router, since it will need to do e.g. rate limiting and such as well.
- // But it might also make sense to have per-transport limits.
- MaxAcceptedConnections uint32
- }
-
- // MConnTransport is a Transport implementation using the current multiplexed
- // Tendermint protocol ("MConn").
- type MConnTransport struct {
- logger log.Logger
- options MConnTransportOptions
- mConnConfig conn.MConnConfig
- channelDescs []*ChannelDescriptor
-
- closeOnce sync.Once
- doneCh chan struct{}
- listener net.Listener
- }
-
- // NewMConnTransport sets up a new MConnection transport. This uses the
- // proprietary Tendermint MConnection protocol, which is implemented as
- // conn.MConnection.
- func NewMConnTransport(
- logger log.Logger,
- mConnConfig conn.MConnConfig,
- channelDescs []*ChannelDescriptor,
- options MConnTransportOptions,
- ) *MConnTransport {
- return &MConnTransport{
- logger: logger,
- options: options,
- mConnConfig: mConnConfig,
- doneCh: make(chan struct{}),
- channelDescs: channelDescs,
- }
- }
-
- // String implements Transport.
- func (m *MConnTransport) String() string {
- return string(MConnProtocol)
- }
-
- // Protocols implements Transport. We support tcp for backwards-compatibility.
- func (m *MConnTransport) Protocols() []Protocol {
- return []Protocol{MConnProtocol, TCPProtocol}
- }
-
- // Endpoints implements Transport.
- func (m *MConnTransport) Endpoints() []Endpoint {
- if m.listener == nil {
- return []Endpoint{}
- }
- select {
- case <-m.doneCh:
- return []Endpoint{}
- default:
- }
-
- endpoint := Endpoint{
- Protocol: MConnProtocol,
- }
- if addr, ok := m.listener.Addr().(*net.TCPAddr); ok {
- endpoint.IP = addr.IP
- endpoint.Port = uint16(addr.Port)
- }
- return []Endpoint{endpoint}
- }
-
- // Listen asynchronously listens for inbound connections on the given endpoint.
- // It must be called exactly once before calling Accept(), and the caller must
- // call Close() to shut down the listener.
- //
- // FIXME: Listen currently only supports listening on a single endpoint, it
- // might be useful to support listening on multiple addresses (e.g. IPv4 and
- // IPv6, or a private and public address) via multiple Listen() calls.
- func (m *MConnTransport) Listen(endpoint Endpoint) error {
- if m.listener != nil {
- return errors.New("transport is already listening")
- }
- if err := m.validateEndpoint(endpoint); err != nil {
- return err
- }
-
- listener, err := net.Listen("tcp", net.JoinHostPort(
- endpoint.IP.String(), strconv.Itoa(int(endpoint.Port))))
- if err != nil {
- return err
- }
- if m.options.MaxAcceptedConnections > 0 {
- // FIXME: This will establish the inbound connection but simply hang it
- // until another connection is released. It would probably be better to
- // return an error to the remote peer or close the connection. This is
- // also a DoS vector since the connection will take up kernel resources.
- // This was just carried over from the legacy P2P stack.
- listener = netutil.LimitListener(listener, int(m.options.MaxAcceptedConnections))
- }
- m.listener = listener
-
- return nil
- }
-
- // Accept implements Transport.
- func (m *MConnTransport) Accept(ctx context.Context) (Connection, error) {
- if m.listener == nil {
- return nil, errors.New("transport is not listening")
- }
-
- conCh := make(chan net.Conn)
- errCh := make(chan error)
- go func() {
- tcpConn, err := m.listener.Accept()
- if err != nil {
- select {
- case errCh <- err:
- case <-ctx.Done():
- }
- }
- select {
- case conCh <- tcpConn:
- case <-ctx.Done():
- }
- }()
-
- select {
- case <-ctx.Done():
- m.listener.Close()
- return nil, io.EOF
- case <-m.doneCh:
- m.listener.Close()
- return nil, io.EOF
- case err := <-errCh:
- return nil, err
- case tcpConn := <-conCh:
- return newMConnConnection(m.logger, tcpConn, m.mConnConfig, m.channelDescs), nil
- }
-
- }
-
- // Dial implements Transport.
- func (m *MConnTransport) Dial(ctx context.Context, endpoint Endpoint) (Connection, error) {
- if err := m.validateEndpoint(endpoint); err != nil {
- return nil, err
- }
- if endpoint.Port == 0 {
- endpoint.Port = 26657
- }
-
- dialer := net.Dialer{}
- tcpConn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(
- endpoint.IP.String(), strconv.Itoa(int(endpoint.Port))))
- if err != nil {
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- default:
- return nil, err
- }
- }
-
- return newMConnConnection(m.logger, tcpConn, m.mConnConfig, m.channelDescs), nil
- }
-
- // Close implements Transport.
- func (m *MConnTransport) Close() error {
- var err error
- m.closeOnce.Do(func() {
- close(m.doneCh)
- if m.listener != nil {
- err = m.listener.Close()
- }
- })
- return err
- }
-
- // SetChannels sets the channel descriptors to be used when
- // establishing a connection.
- //
- // FIXME: To be removed when the legacy p2p stack is removed. Channel
- // descriptors should be managed by the router. The underlying transport and
- // connections should be agnostic to everything but the channel ID's which are
- // initialized in the handshake.
- func (m *MConnTransport) AddChannelDescriptors(channelDesc []*ChannelDescriptor) {
- m.channelDescs = append(m.channelDescs, channelDesc...)
- }
-
- // validateEndpoint validates an endpoint.
- func (m *MConnTransport) validateEndpoint(endpoint Endpoint) error {
- if err := endpoint.Validate(); err != nil {
- return err
- }
- if endpoint.Protocol != MConnProtocol && endpoint.Protocol != TCPProtocol {
- return fmt.Errorf("unsupported protocol %q", endpoint.Protocol)
- }
- if len(endpoint.IP) == 0 {
- return errors.New("endpoint has no IP address")
- }
- if endpoint.Path != "" {
- return fmt.Errorf("endpoints with path not supported (got %q)", endpoint.Path)
- }
- return nil
- }
-
- // mConnConnection implements Connection for MConnTransport.
- type mConnConnection struct {
- logger log.Logger
- conn net.Conn
- mConnConfig conn.MConnConfig
- channelDescs []*ChannelDescriptor
- receiveCh chan mConnMessage
- errorCh chan error
- doneCh chan struct{}
- closeOnce sync.Once
-
- mconn *conn.MConnection // set during Handshake()
- }
-
- // mConnMessage passes MConnection messages through internal channels.
- type mConnMessage struct {
- channelID ChannelID
- payload []byte
- }
-
- // newMConnConnection creates a new mConnConnection.
- func newMConnConnection(
- logger log.Logger,
- conn net.Conn,
- mConnConfig conn.MConnConfig,
- channelDescs []*ChannelDescriptor,
- ) *mConnConnection {
- return &mConnConnection{
- logger: logger,
- conn: conn,
- mConnConfig: mConnConfig,
- channelDescs: channelDescs,
- receiveCh: make(chan mConnMessage),
- errorCh: make(chan error, 1), // buffered to avoid onError leak
- doneCh: make(chan struct{}),
- }
- }
-
- // Handshake implements Connection.
- func (c *mConnConnection) Handshake(
- ctx context.Context,
- nodeInfo types.NodeInfo,
- privKey crypto.PrivKey,
- ) (types.NodeInfo, crypto.PubKey, error) {
- var (
- mconn *conn.MConnection
- peerInfo types.NodeInfo
- peerKey crypto.PubKey
- errCh = make(chan error, 1)
- )
- // To handle context cancellation, we need to do the handshake in a
- // goroutine and abort the blocking network calls by closing the connection
- // when the context is canceled.
- go func() {
- // FIXME: Since the MConnection code panics, we need to recover it and turn it
- // into an error. We should remove panics instead.
- defer func() {
- if r := recover(); r != nil {
- errCh <- fmt.Errorf("recovered from panic: %v", r)
- }
- }()
- var err error
- mconn, peerInfo, peerKey, err = c.handshake(ctx, nodeInfo, privKey)
-
- select {
- case errCh <- err:
- case <-ctx.Done():
- }
-
- }()
-
- select {
- case <-ctx.Done():
- _ = c.Close()
- return types.NodeInfo{}, nil, ctx.Err()
-
- case err := <-errCh:
- if err != nil {
- return types.NodeInfo{}, nil, err
- }
- c.mconn = mconn
- if err = c.mconn.Start(ctx); err != nil {
- return types.NodeInfo{}, nil, err
- }
- return peerInfo, peerKey, nil
- }
- }
-
- // handshake is a helper for Handshake, simplifying error handling so we can
- // keep context handling and panic recovery in Handshake. It returns an
- // unstarted but handshaked MConnection, to avoid concurrent field writes.
- func (c *mConnConnection) handshake(
- ctx context.Context,
- nodeInfo types.NodeInfo,
- privKey crypto.PrivKey,
- ) (*conn.MConnection, types.NodeInfo, crypto.PubKey, error) {
- if c.mconn != nil {
- return nil, types.NodeInfo{}, nil, errors.New("connection is already handshaked")
- }
-
- secretConn, err := conn.MakeSecretConnection(c.conn, privKey)
- if err != nil {
- return nil, types.NodeInfo{}, nil, err
- }
-
- wg := &sync.WaitGroup{}
- var pbPeerInfo p2pproto.NodeInfo
- errCh := make(chan error, 2)
- wg.Add(1)
- go func() {
- defer wg.Done()
- _, err := protoio.NewDelimitedWriter(secretConn).WriteMsg(nodeInfo.ToProto())
- select {
- case errCh <- err:
- case <-ctx.Done():
- }
-
- }()
- wg.Add(1)
- go func() {
- defer wg.Done()
- _, err := protoio.NewDelimitedReader(secretConn, types.MaxNodeInfoSize()).ReadMsg(&pbPeerInfo)
- select {
- case errCh <- err:
- case <-ctx.Done():
- }
- }()
-
- wg.Wait()
-
- if err, ok := <-errCh; ok && err != nil {
- return nil, types.NodeInfo{}, nil, err
- }
-
- if err := ctx.Err(); err != nil {
- return nil, types.NodeInfo{}, nil, err
- }
-
- peerInfo, err := types.NodeInfoFromProto(&pbPeerInfo)
- if err != nil {
- return nil, types.NodeInfo{}, nil, err
- }
-
- mconn := conn.NewMConnectionWithConfig(
- c.logger.With("peer", c.RemoteEndpoint().NodeAddress(peerInfo.NodeID)),
- secretConn,
- c.channelDescs,
- c.onReceive,
- c.onError,
- c.mConnConfig,
- )
-
- return mconn, peerInfo, secretConn.RemotePubKey(), nil
- }
-
- // onReceive is a callback for MConnection received messages.
- func (c *mConnConnection) onReceive(ctx context.Context, chID ChannelID, payload []byte) {
- select {
- case c.receiveCh <- mConnMessage{channelID: chID, payload: payload}:
- case <-ctx.Done():
- }
- }
-
- // onError is a callback for MConnection errors. The error is passed via errorCh
- // to ReceiveMessage (but not SendMessage, for legacy P2P stack behavior).
- func (c *mConnConnection) onError(ctx context.Context, e interface{}) {
- err, ok := e.(error)
- if !ok {
- err = fmt.Errorf("%v", err)
- }
- // We have to close the connection here, since MConnection will have stopped
- // the service on any errors.
- _ = c.Close()
- select {
- case c.errorCh <- err:
- case <-ctx.Done():
- }
- }
-
- // String displays connection information.
- func (c *mConnConnection) String() string {
- return c.RemoteEndpoint().String()
- }
-
- // SendMessage implements Connection.
- func (c *mConnConnection) SendMessage(ctx context.Context, chID ChannelID, msg []byte) error {
- if chID > math.MaxUint8 {
- return fmt.Errorf("MConnection only supports 1-byte channel IDs (got %v)", chID)
- }
- select {
- case err := <-c.errorCh:
- return err
- case <-ctx.Done():
- return io.EOF
- default:
- if ok := c.mconn.Send(chID, msg); !ok {
- return errors.New("sending message timed out")
- }
-
- return nil
- }
- }
-
- // ReceiveMessage implements Connection.
- func (c *mConnConnection) ReceiveMessage(ctx context.Context) (ChannelID, []byte, error) {
- select {
- case err := <-c.errorCh:
- return 0, nil, err
- case <-c.doneCh:
- return 0, nil, io.EOF
- case <-ctx.Done():
- return 0, nil, io.EOF
- case msg := <-c.receiveCh:
- return msg.channelID, msg.payload, nil
- }
- }
-
- // LocalEndpoint implements Connection.
- func (c *mConnConnection) LocalEndpoint() Endpoint {
- endpoint := Endpoint{
- Protocol: MConnProtocol,
- }
- if addr, ok := c.conn.LocalAddr().(*net.TCPAddr); ok {
- endpoint.IP = addr.IP
- endpoint.Port = uint16(addr.Port)
- }
- return endpoint
- }
-
- // RemoteEndpoint implements Connection.
- func (c *mConnConnection) RemoteEndpoint() Endpoint {
- endpoint := Endpoint{
- Protocol: MConnProtocol,
- }
- if addr, ok := c.conn.RemoteAddr().(*net.TCPAddr); ok {
- endpoint.IP = addr.IP
- endpoint.Port = uint16(addr.Port)
- }
- return endpoint
- }
-
- // Close implements Connection.
- func (c *mConnConnection) Close() error {
- var err error
- c.closeOnce.Do(func() {
- if c.mconn != nil && c.mconn.IsRunning() {
- err = c.mconn.Stop()
- } else {
- err = c.conn.Close()
- }
- close(c.doneCh)
- })
- return err
- }
|