package privval import ( "errors" "net" "time" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" p2pconn "github.com/tendermint/tendermint/p2p/conn" "github.com/tendermint/tendermint/types" ) const ( defaultAcceptDeadlineSeconds = 3 defaultConnDeadlineSeconds = 3 defaultConnHeartBeatSeconds = 2 defaultDialRetries = 10 ) // Socket errors. var ( ErrDialRetryMax = errors.New("dialed maximum retries") ErrConnTimeout = errors.New("remote signer timed out") ErrUnexpectedResponse = errors.New("received unexpected response") ) var ( acceptDeadline = time.Second * defaultAcceptDeadlineSeconds connTimeout = time.Second * defaultConnDeadlineSeconds connHeartbeat = time.Second * defaultConnHeartBeatSeconds ) // TCPValOption sets an optional parameter on the SocketPV. type TCPValOption func(*TCPVal) // TCPValAcceptDeadline sets the deadline for the TCPVal listener. // A zero time value disables the deadline. func TCPValAcceptDeadline(deadline time.Duration) TCPValOption { return func(sc *TCPVal) { sc.acceptDeadline = deadline } } // TCPValConnTimeout sets the read and write timeout for connections // from external signing processes. func TCPValConnTimeout(timeout time.Duration) TCPValOption { return func(sc *TCPVal) { sc.connTimeout = timeout } } // TCPValHeartbeat sets the period on which to check the liveness of the // connected Signer connections. func TCPValHeartbeat(period time.Duration) TCPValOption { return func(sc *TCPVal) { sc.connHeartbeat = period } } // TCPVal implements PrivValidator, it uses a socket to request signatures // from an external process. type TCPVal struct { cmn.BaseService *RemoteSignerClient addr string acceptDeadline time.Duration connTimeout time.Duration connHeartbeat time.Duration privKey ed25519.PrivKeyEd25519 conn net.Conn listener net.Listener cancelPing chan struct{} pingTicker *time.Ticker } // Check that TCPVal implements PrivValidator. var _ types.PrivValidator = (*TCPVal)(nil) // NewTCPVal returns an instance of TCPVal. func NewTCPVal( logger log.Logger, socketAddr string, privKey ed25519.PrivKeyEd25519, ) *TCPVal { sc := &TCPVal{ addr: socketAddr, acceptDeadline: acceptDeadline, connTimeout: connTimeout, connHeartbeat: connHeartbeat, privKey: privKey, } sc.BaseService = *cmn.NewBaseService(logger, "TCPVal", sc) return sc } // OnStart implements cmn.Service. func (sc *TCPVal) OnStart() error { if err := sc.listen(); err != nil { sc.Logger.Error("OnStart", "err", err) return err } conn, err := sc.waitConnection() if err != nil { sc.Logger.Error("OnStart", "err", err) return err } sc.conn = conn sc.RemoteSignerClient, err = NewRemoteSignerClient(sc.conn) if err != nil { return err } // Start a routine to keep the connection alive sc.cancelPing = make(chan struct{}, 1) sc.pingTicker = time.NewTicker(sc.connHeartbeat) go func() { for { select { case <-sc.pingTicker.C: err := sc.Ping() if err != nil { sc.Logger.Error( "Ping", "err", err, ) } case <-sc.cancelPing: sc.pingTicker.Stop() return } } }() return nil } // OnStop implements cmn.Service. func (sc *TCPVal) OnStop() { if sc.cancelPing != nil { close(sc.cancelPing) } if sc.conn != nil { if err := sc.conn.Close(); err != nil { sc.Logger.Error("OnStop", "err", err) } } if sc.listener != nil { if err := sc.listener.Close(); err != nil { sc.Logger.Error("OnStop", "err", err) } } } func (sc *TCPVal) acceptConnection() (net.Conn, error) { conn, err := sc.listener.Accept() if err != nil { if !sc.IsRunning() { return nil, nil // Ignore error from listener closing. } return nil, err } conn, err = p2pconn.MakeSecretConnection(conn, sc.privKey) if err != nil { return nil, err } return conn, nil } func (sc *TCPVal) listen() error { ln, err := net.Listen(cmn.ProtocolAndAddress(sc.addr)) if err != nil { return err } sc.listener = newTCPTimeoutListener( ln, sc.acceptDeadline, sc.connTimeout, sc.connHeartbeat, ) return nil } // waitConnection uses the configured wait timeout to error if no external // process connects in the time period. func (sc *TCPVal) waitConnection() (net.Conn, error) { var ( connc = make(chan net.Conn, 1) errc = make(chan error, 1) ) go func(connc chan<- net.Conn, errc chan<- error) { conn, err := sc.acceptConnection() if err != nil { errc <- err return } connc <- conn }(connc, errc) select { case conn := <-connc: return conn, nil case err := <-errc: return nil, err } }