* Close and recreate a RemoteSigner on err * Update changelog * Address Anton's comments / suggestions: - update changelog - restart TCPVal - shut down on `ErrUnexpectedResponse` * re-init remote signer client with fresh connection if Ping fails - add/update TODOs in secret connection - rename tcp.go -> tcp_client.go, same with ipc to clarify their purpose * account for `conn returned by waitConnection can be `nil` - also add TODO about RemoteSigner conn field * Tests for retrying: IPC / TCP - shorter info log on success - set conn and use it in tests to close conn * Tests for retrying: IPC / TCP - shorter info log on success - set conn and use it in tests to close conn - add rwmutex for conn field in IPC * comments and doc.go * fix ipc tests. fixes #2677 * use constants for tests * cleanup some error statements * fixes #2784, race in tests * remove print statement * minor fixes from review * update comment on sts spec * cosmetics * p2p/conn: add failing tests * p2p/conn: make SecretConnection thread safe * changelog * IPCVal signer refactor - use a .reset() method - don't use embedded RemoteSignerClient - guard RemoteSignerClient with mutex - drop the .conn - expose Close() on RemoteSignerClient * apply IPCVal refactor to TCPVal * remove mtx from RemoteSignerClient * consolidate IPCVal and TCPVal, fixes #3104 - done in tcp_client.go - now called SocketVal - takes a listener in the constructor - make tcpListener and unixListener contain all the differences * delete ipc files * introduce unix and tcp dialer for RemoteSigner * rename files - drop tcp_ prefix - rename priv_validator.go to file.go * bring back listener options * fix node * fix priv_val_server * fix node test * minor cleanup and commentsbreaking
@ -0,0 +1,263 @@ | |||
package privval | |||
import ( | |||
"errors" | |||
"fmt" | |||
"net" | |||
"sync" | |||
"time" | |||
"github.com/tendermint/tendermint/crypto" | |||
cmn "github.com/tendermint/tendermint/libs/common" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const ( | |||
defaultConnHeartBeatSeconds = 2 | |||
defaultDialRetries = 10 | |||
) | |||
// Socket errors. | |||
var ( | |||
ErrUnexpectedResponse = errors.New("received unexpected response") | |||
) | |||
var ( | |||
connHeartbeat = time.Second * defaultConnHeartBeatSeconds | |||
) | |||
// SocketValOption sets an optional parameter on the SocketVal. | |||
type SocketValOption func(*SocketVal) | |||
// SocketValHeartbeat sets the period on which to check the liveness of the | |||
// connected Signer connections. | |||
func SocketValHeartbeat(period time.Duration) SocketValOption { | |||
return func(sc *SocketVal) { sc.connHeartbeat = period } | |||
} | |||
// SocketVal implements PrivValidator. | |||
// It listens for an external process to dial in and uses | |||
// the socket to request signatures. | |||
type SocketVal struct { | |||
cmn.BaseService | |||
listener net.Listener | |||
// ping | |||
cancelPing chan struct{} | |||
pingTicker *time.Ticker | |||
connHeartbeat time.Duration | |||
// signer is mutable since it can be | |||
// reset if the connection fails. | |||
// failures are detected by a background | |||
// ping routine. | |||
// Methods on the underlying net.Conn itself | |||
// are already gorountine safe. | |||
mtx sync.RWMutex | |||
signer *RemoteSignerClient | |||
} | |||
// Check that SocketVal implements PrivValidator. | |||
var _ types.PrivValidator = (*SocketVal)(nil) | |||
// NewSocketVal returns an instance of SocketVal. | |||
func NewSocketVal( | |||
logger log.Logger, | |||
listener net.Listener, | |||
) *SocketVal { | |||
sc := &SocketVal{ | |||
listener: listener, | |||
connHeartbeat: connHeartbeat, | |||
} | |||
sc.BaseService = *cmn.NewBaseService(logger, "SocketVal", sc) | |||
return sc | |||
} | |||
//-------------------------------------------------------- | |||
// Implement PrivValidator | |||
// GetPubKey implements PrivValidator. | |||
func (sc *SocketVal) GetPubKey() crypto.PubKey { | |||
sc.mtx.RLock() | |||
defer sc.mtx.RUnlock() | |||
return sc.signer.GetPubKey() | |||
} | |||
// SignVote implements PrivValidator. | |||
func (sc *SocketVal) SignVote(chainID string, vote *types.Vote) error { | |||
sc.mtx.RLock() | |||
defer sc.mtx.RUnlock() | |||
return sc.signer.SignVote(chainID, vote) | |||
} | |||
// SignProposal implements PrivValidator. | |||
func (sc *SocketVal) SignProposal(chainID string, proposal *types.Proposal) error { | |||
sc.mtx.RLock() | |||
defer sc.mtx.RUnlock() | |||
return sc.signer.SignProposal(chainID, proposal) | |||
} | |||
//-------------------------------------------------------- | |||
// More thread safe methods proxied to the signer | |||
// Ping is used to check connection health. | |||
func (sc *SocketVal) Ping() error { | |||
sc.mtx.RLock() | |||
defer sc.mtx.RUnlock() | |||
return sc.signer.Ping() | |||
} | |||
// Close closes the underlying net.Conn. | |||
func (sc *SocketVal) Close() { | |||
sc.mtx.RLock() | |||
defer sc.mtx.RUnlock() | |||
if sc.signer != nil { | |||
if err := sc.signer.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) | |||
} | |||
} | |||
} | |||
//-------------------------------------------------------- | |||
// Service start and stop | |||
// OnStart implements cmn.Service. | |||
func (sc *SocketVal) OnStart() error { | |||
if closed, err := sc.reset(); err != nil { | |||
sc.Logger.Error("OnStart", "err", err) | |||
return err | |||
} else if closed { | |||
return fmt.Errorf("listener is closed") | |||
} | |||
// 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) | |||
if err == ErrUnexpectedResponse { | |||
return | |||
} | |||
closed, err := sc.reset() | |||
if err != nil { | |||
sc.Logger.Error("Reconnecting to remote signer failed", "err", err) | |||
continue | |||
} | |||
if closed { | |||
sc.Logger.Info("listener is closing") | |||
return | |||
} | |||
sc.Logger.Info("Re-created connection to remote signer", "impl", sc) | |||
} | |||
case <-sc.cancelPing: | |||
sc.pingTicker.Stop() | |||
return | |||
} | |||
} | |||
}() | |||
return nil | |||
} | |||
// OnStop implements cmn.Service. | |||
func (sc *SocketVal) OnStop() { | |||
if sc.cancelPing != nil { | |||
close(sc.cancelPing) | |||
} | |||
sc.Close() | |||
} | |||
//-------------------------------------------------------- | |||
// Connection and signer management | |||
// waits to accept and sets a new connection. | |||
// connection is closed in OnStop. | |||
// returns true if the listener is closed | |||
// (ie. it returns a nil conn). | |||
func (sc *SocketVal) reset() (bool, error) { | |||
sc.mtx.Lock() | |||
defer sc.mtx.Unlock() | |||
// first check if the conn already exists and close it. | |||
if sc.signer != nil { | |||
if err := sc.signer.Close(); err != nil { | |||
sc.Logger.Error("error closing connection", "err", err) | |||
} | |||
} | |||
// wait for a new conn | |||
conn, err := sc.waitConnection() | |||
if err != nil { | |||
return false, err | |||
} | |||
// listener is closed | |||
if conn == nil { | |||
return true, nil | |||
} | |||
sc.signer, err = NewRemoteSignerClient(conn) | |||
if err != nil { | |||
// failed to fetch the pubkey. close out the connection. | |||
if err := conn.Close(); err != nil { | |||
sc.Logger.Error("error closing connection", "err", err) | |||
} | |||
return false, err | |||
} | |||
return false, nil | |||
} | |||
func (sc *SocketVal) 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 | |||
} | |||
return conn, nil | |||
} | |||
// waitConnection uses the configured wait timeout to error if no external | |||
// process connects in the time period. | |||
func (sc *SocketVal) 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 | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
/* | |||
Package privval provides different implementations of the types.PrivValidator. | |||
FilePV | |||
FilePV is the simplest implementation and developer default. It uses one file for the private key and another to store state. | |||
SocketVal | |||
SocketVal establishes a connection to an external process, like a Key Management Server (KMS), using a socket. | |||
SocketVal listens for the external KMS process to dial in. | |||
SocketVal takes a listener, which determines the type of connection | |||
(ie. encrypted over tcp, or unencrypted over unix). | |||
RemoteSigner | |||
RemoteSigner is a simple wrapper around a net.Conn. It's used by both IPCVal and TCPVal. | |||
*/ | |||
package privval |
@ -1,123 +0,0 @@ | |||
package privval | |||
import ( | |||
"net" | |||
"time" | |||
cmn "github.com/tendermint/tendermint/libs/common" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// IPCValOption sets an optional parameter on the SocketPV. | |||
type IPCValOption func(*IPCVal) | |||
// IPCValConnTimeout sets the read and write timeout for connections | |||
// from external signing processes. | |||
func IPCValConnTimeout(timeout time.Duration) IPCValOption { | |||
return func(sc *IPCVal) { sc.connTimeout = timeout } | |||
} | |||
// IPCValHeartbeat sets the period on which to check the liveness of the | |||
// connected Signer connections. | |||
func IPCValHeartbeat(period time.Duration) IPCValOption { | |||
return func(sc *IPCVal) { sc.connHeartbeat = period } | |||
} | |||
// IPCVal implements PrivValidator, it uses a unix socket to request signatures | |||
// from an external process. | |||
type IPCVal struct { | |||
cmn.BaseService | |||
*RemoteSignerClient | |||
addr string | |||
connTimeout time.Duration | |||
connHeartbeat time.Duration | |||
conn net.Conn | |||
cancelPing chan struct{} | |||
pingTicker *time.Ticker | |||
} | |||
// Check that IPCVal implements PrivValidator. | |||
var _ types.PrivValidator = (*IPCVal)(nil) | |||
// NewIPCVal returns an instance of IPCVal. | |||
func NewIPCVal( | |||
logger log.Logger, | |||
socketAddr string, | |||
) *IPCVal { | |||
sc := &IPCVal{ | |||
addr: socketAddr, | |||
connTimeout: connTimeout, | |||
connHeartbeat: connHeartbeat, | |||
} | |||
sc.BaseService = *cmn.NewBaseService(logger, "IPCVal", sc) | |||
return sc | |||
} | |||
// OnStart implements cmn.Service. | |||
func (sc *IPCVal) OnStart() error { | |||
err := sc.connect() | |||
if err != nil { | |||
sc.Logger.Error("OnStart", "err", err) | |||
return err | |||
} | |||
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 *IPCVal) 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) | |||
} | |||
} | |||
} | |||
func (sc *IPCVal) connect() error { | |||
la, err := net.ResolveUnixAddr("unix", sc.addr) | |||
if err != nil { | |||
return err | |||
} | |||
conn, err := net.DialUnix("unix", nil, la) | |||
if err != nil { | |||
return err | |||
} | |||
sc.conn = newTimeoutConn(conn, sc.connTimeout) | |||
return nil | |||
} |
@ -1,132 +0,0 @@ | |||
package privval | |||
import ( | |||
"io" | |||
"net" | |||
"time" | |||
cmn "github.com/tendermint/tendermint/libs/common" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// IPCRemoteSignerOption sets an optional parameter on the IPCRemoteSigner. | |||
type IPCRemoteSignerOption func(*IPCRemoteSigner) | |||
// IPCRemoteSignerConnDeadline sets the read and write deadline for connections | |||
// from external signing processes. | |||
func IPCRemoteSignerConnDeadline(deadline time.Duration) IPCRemoteSignerOption { | |||
return func(ss *IPCRemoteSigner) { ss.connDeadline = deadline } | |||
} | |||
// IPCRemoteSignerConnRetries sets the amount of attempted retries to connect. | |||
func IPCRemoteSignerConnRetries(retries int) IPCRemoteSignerOption { | |||
return func(ss *IPCRemoteSigner) { ss.connRetries = retries } | |||
} | |||
// IPCRemoteSigner is a RPC implementation of PrivValidator that listens on a unix socket. | |||
type IPCRemoteSigner struct { | |||
cmn.BaseService | |||
addr string | |||
chainID string | |||
connDeadline time.Duration | |||
connRetries int | |||
privVal types.PrivValidator | |||
listener *net.UnixListener | |||
} | |||
// NewIPCRemoteSigner returns an instance of IPCRemoteSigner. | |||
func NewIPCRemoteSigner( | |||
logger log.Logger, | |||
chainID, socketAddr string, | |||
privVal types.PrivValidator, | |||
) *IPCRemoteSigner { | |||
rs := &IPCRemoteSigner{ | |||
addr: socketAddr, | |||
chainID: chainID, | |||
connDeadline: time.Second * defaultConnDeadlineSeconds, | |||
connRetries: defaultDialRetries, | |||
privVal: privVal, | |||
} | |||
rs.BaseService = *cmn.NewBaseService(logger, "IPCRemoteSigner", rs) | |||
return rs | |||
} | |||
// OnStart implements cmn.Service. | |||
func (rs *IPCRemoteSigner) OnStart() error { | |||
err := rs.listen() | |||
if err != nil { | |||
err = cmn.ErrorWrap(err, "listen") | |||
rs.Logger.Error("OnStart", "err", err) | |||
return err | |||
} | |||
go func() { | |||
for { | |||
conn, err := rs.listener.AcceptUnix() | |||
if err != nil { | |||
rs.Logger.Error("AcceptUnix", "err", err) | |||
return | |||
} | |||
go rs.handleConnection(conn) | |||
} | |||
}() | |||
return nil | |||
} | |||
// OnStop implements cmn.Service. | |||
func (rs *IPCRemoteSigner) OnStop() { | |||
if rs.listener != nil { | |||
if err := rs.listener.Close(); err != nil { | |||
rs.Logger.Error("OnStop", "err", cmn.ErrorWrap(err, "closing listener failed")) | |||
} | |||
} | |||
} | |||
func (rs *IPCRemoteSigner) listen() error { | |||
la, err := net.ResolveUnixAddr("unix", rs.addr) | |||
if err != nil { | |||
return err | |||
} | |||
rs.listener, err = net.ListenUnix("unix", la) | |||
return err | |||
} | |||
func (rs *IPCRemoteSigner) handleConnection(conn net.Conn) { | |||
for { | |||
if !rs.IsRunning() { | |||
return // Ignore error from listener closing. | |||
} | |||
// Reset the connection deadline | |||
conn.SetDeadline(time.Now().Add(rs.connDeadline)) | |||
req, err := readMsg(conn) | |||
if err != nil { | |||
if err != io.EOF { | |||
rs.Logger.Error("handleConnection", "err", err) | |||
} | |||
return | |||
} | |||
res, err := handleRequest(req, rs.chainID, rs.privVal) | |||
if err != nil { | |||
// only log the error; we'll reply with an error in res | |||
rs.Logger.Error("handleConnection", "err", err) | |||
} | |||
err = writeMsg(conn, res) | |||
if err != nil { | |||
rs.Logger.Error("handleConnection", "err", err) | |||
return | |||
} | |||
} | |||
} |
@ -1,147 +0,0 @@ | |||
package privval | |||
import ( | |||
"io/ioutil" | |||
"os" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
cmn "github.com/tendermint/tendermint/libs/common" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func TestIPCPVVote(t *testing.T) { | |||
var ( | |||
chainID = cmn.RandStr(12) | |||
sc, rs = testSetupIPCSocketPair(t, chainID, types.NewMockPV()) | |||
ts = time.Now() | |||
vType = types.PrecommitType | |||
want = &types.Vote{Timestamp: ts, Type: vType} | |||
have = &types.Vote{Timestamp: ts, Type: vType} | |||
) | |||
defer sc.Stop() | |||
defer rs.Stop() | |||
require.NoError(t, rs.privVal.SignVote(chainID, want)) | |||
require.NoError(t, sc.SignVote(chainID, have)) | |||
assert.Equal(t, want.Signature, have.Signature) | |||
} | |||
func TestIPCPVVoteResetDeadline(t *testing.T) { | |||
var ( | |||
chainID = cmn.RandStr(12) | |||
sc, rs = testSetupIPCSocketPair(t, chainID, types.NewMockPV()) | |||
ts = time.Now() | |||
vType = types.PrecommitType | |||
want = &types.Vote{Timestamp: ts, Type: vType} | |||
have = &types.Vote{Timestamp: ts, Type: vType} | |||
) | |||
defer sc.Stop() | |||
defer rs.Stop() | |||
time.Sleep(3 * time.Millisecond) | |||
require.NoError(t, rs.privVal.SignVote(chainID, want)) | |||
require.NoError(t, sc.SignVote(chainID, have)) | |||
assert.Equal(t, want.Signature, have.Signature) | |||
// This would exceed the deadline if it was not extended by the previous message | |||
time.Sleep(3 * time.Millisecond) | |||
require.NoError(t, rs.privVal.SignVote(chainID, want)) | |||
require.NoError(t, sc.SignVote(chainID, have)) | |||
assert.Equal(t, want.Signature, have.Signature) | |||
} | |||
func TestIPCPVVoteKeepalive(t *testing.T) { | |||
var ( | |||
chainID = cmn.RandStr(12) | |||
sc, rs = testSetupIPCSocketPair(t, chainID, types.NewMockPV()) | |||
ts = time.Now() | |||
vType = types.PrecommitType | |||
want = &types.Vote{Timestamp: ts, Type: vType} | |||
have = &types.Vote{Timestamp: ts, Type: vType} | |||
) | |||
defer sc.Stop() | |||
defer rs.Stop() | |||
time.Sleep(10 * time.Millisecond) | |||
require.NoError(t, rs.privVal.SignVote(chainID, want)) | |||
require.NoError(t, sc.SignVote(chainID, have)) | |||
assert.Equal(t, want.Signature, have.Signature) | |||
} | |||
func testSetupIPCSocketPair( | |||
t *testing.T, | |||
chainID string, | |||
privValidator types.PrivValidator, | |||
) (*IPCVal, *IPCRemoteSigner) { | |||
addr, err := testUnixAddr() | |||
require.NoError(t, err) | |||
var ( | |||
logger = log.TestingLogger() | |||
privVal = privValidator | |||
readyc = make(chan struct{}) | |||
rs = NewIPCRemoteSigner( | |||
logger, | |||
chainID, | |||
addr, | |||
privVal, | |||
) | |||
sc = NewIPCVal( | |||
logger, | |||
addr, | |||
) | |||
) | |||
IPCValConnTimeout(5 * time.Millisecond)(sc) | |||
IPCValHeartbeat(time.Millisecond)(sc) | |||
IPCRemoteSignerConnDeadline(time.Millisecond * 5)(rs) | |||
testStartIPCRemoteSigner(t, readyc, rs) | |||
<-readyc | |||
require.NoError(t, sc.Start()) | |||
assert.True(t, sc.IsRunning()) | |||
return sc, rs | |||
} | |||
func testStartIPCRemoteSigner(t *testing.T, readyc chan struct{}, rs *IPCRemoteSigner) { | |||
go func(rs *IPCRemoteSigner) { | |||
require.NoError(t, rs.Start()) | |||
assert.True(t, rs.IsRunning()) | |||
readyc <- struct{}{} | |||
}(rs) | |||
} | |||
func testUnixAddr() (string, error) { | |||
f, err := ioutil.TempFile("/tmp", "nettest") | |||
if err != nil { | |||
return "", err | |||
} | |||
addr := f.Name() | |||
err = f.Close() | |||
if err != nil { | |||
return "", err | |||
} | |||
err = os.Remove(addr) | |||
if err != nil { | |||
return "", err | |||
} | |||
return addr, nil | |||
} |
@ -0,0 +1,184 @@ | |||
package privval | |||
import ( | |||
"net" | |||
"time" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
p2pconn "github.com/tendermint/tendermint/p2p/conn" | |||
) | |||
const ( | |||
defaultAcceptDeadlineSeconds = 3 | |||
defaultConnDeadlineSeconds = 3 | |||
) | |||
// timeoutError can be used to check if an error returned from the netp package | |||
// was due to a timeout. | |||
type timeoutError interface { | |||
Timeout() bool | |||
} | |||
//------------------------------------------------------------------ | |||
// TCP Listener | |||
// TCPListenerOption sets an optional parameter on the tcpListener. | |||
type TCPListenerOption func(*tcpListener) | |||
// TCPListenerAcceptDeadline sets the deadline for the listener. | |||
// A zero time value disables the deadline. | |||
func TCPListenerAcceptDeadline(deadline time.Duration) TCPListenerOption { | |||
return func(tl *tcpListener) { tl.acceptDeadline = deadline } | |||
} | |||
// TCPListenerConnDeadline sets the read and write deadline for connections | |||
// from external signing processes. | |||
func TCPListenerConnDeadline(deadline time.Duration) TCPListenerOption { | |||
return func(tl *tcpListener) { tl.connDeadline = deadline } | |||
} | |||
// tcpListener implements net.Listener. | |||
var _ net.Listener = (*tcpListener)(nil) | |||
// tcpListener wraps a *net.TCPListener to standardise protocol timeouts | |||
// and potentially other tuning parameters. It also returns encrypted connections. | |||
type tcpListener struct { | |||
*net.TCPListener | |||
secretConnKey ed25519.PrivKeyEd25519 | |||
acceptDeadline time.Duration | |||
connDeadline time.Duration | |||
} | |||
// NewTCPListener returns a listener that accepts authenticated encrypted connections | |||
// using the given secretConnKey and the default timeout values. | |||
func NewTCPListener(ln net.Listener, secretConnKey ed25519.PrivKeyEd25519) *tcpListener { | |||
return &tcpListener{ | |||
TCPListener: ln.(*net.TCPListener), | |||
secretConnKey: secretConnKey, | |||
acceptDeadline: time.Second * defaultAcceptDeadlineSeconds, | |||
connDeadline: time.Second * defaultConnDeadlineSeconds, | |||
} | |||
} | |||
// Accept implements net.Listener. | |||
func (ln *tcpListener) Accept() (net.Conn, error) { | |||
err := ln.SetDeadline(time.Now().Add(ln.acceptDeadline)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
tc, err := ln.AcceptTCP() | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Wrap the conn in our timeout and encryption wrappers | |||
timeoutConn := newTimeoutConn(tc, ln.connDeadline) | |||
secretConn, err := p2pconn.MakeSecretConnection(timeoutConn, ln.secretConnKey) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return secretConn, nil | |||
} | |||
//------------------------------------------------------------------ | |||
// Unix Listener | |||
// unixListener implements net.Listener. | |||
var _ net.Listener = (*unixListener)(nil) | |||
type UnixListenerOption func(*unixListener) | |||
// UnixListenerAcceptDeadline sets the deadline for the listener. | |||
// A zero time value disables the deadline. | |||
func UnixListenerAcceptDeadline(deadline time.Duration) UnixListenerOption { | |||
return func(ul *unixListener) { ul.acceptDeadline = deadline } | |||
} | |||
// UnixListenerConnDeadline sets the read and write deadline for connections | |||
// from external signing processes. | |||
func UnixListenerConnDeadline(deadline time.Duration) UnixListenerOption { | |||
return func(ul *unixListener) { ul.connDeadline = deadline } | |||
} | |||
// unixListener wraps a *net.UnixListener to standardise protocol timeouts | |||
// and potentially other tuning parameters. It returns unencrypted connections. | |||
type unixListener struct { | |||
*net.UnixListener | |||
acceptDeadline time.Duration | |||
connDeadline time.Duration | |||
} | |||
// NewUnixListener returns a listener that accepts unencrypted connections | |||
// using the default timeout values. | |||
func NewUnixListener(ln net.Listener) *unixListener { | |||
return &unixListener{ | |||
UnixListener: ln.(*net.UnixListener), | |||
acceptDeadline: time.Second * defaultAcceptDeadlineSeconds, | |||
connDeadline: time.Second * defaultConnDeadlineSeconds, | |||
} | |||
} | |||
// Accept implements net.Listener. | |||
func (ln *unixListener) Accept() (net.Conn, error) { | |||
err := ln.SetDeadline(time.Now().Add(ln.acceptDeadline)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
tc, err := ln.AcceptUnix() | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Wrap the conn in our timeout wrapper | |||
conn := newTimeoutConn(tc, ln.connDeadline) | |||
// TODO: wrap in something that authenticates | |||
// with a MAC - https://github.com/tendermint/tendermint/issues/3099 | |||
return conn, nil | |||
} | |||
//------------------------------------------------------------------ | |||
// Connection | |||
// timeoutConn implements net.Conn. | |||
var _ net.Conn = (*timeoutConn)(nil) | |||
// timeoutConn wraps a net.Conn to standardise protocol timeouts / deadline resets. | |||
type timeoutConn struct { | |||
net.Conn | |||
connDeadline time.Duration | |||
} | |||
// newTimeoutConn returns an instance of newTCPTimeoutConn. | |||
func newTimeoutConn( | |||
conn net.Conn, | |||
connDeadline time.Duration) *timeoutConn { | |||
return &timeoutConn{ | |||
conn, | |||
connDeadline, | |||
} | |||
} | |||
// Read implements net.Conn. | |||
func (c timeoutConn) Read(b []byte) (n int, err error) { | |||
// Reset deadline | |||
c.Conn.SetReadDeadline(time.Now().Add(c.connDeadline)) | |||
return c.Conn.Read(b) | |||
} | |||
// Write implements net.Conn. | |||
func (c timeoutConn) Write(b []byte) (n int, err error) { | |||
// Reset deadline | |||
c.Conn.SetWriteDeadline(time.Now().Add(c.connDeadline)) | |||
return c.Conn.Write(b) | |||
} |
@ -1,216 +0,0 @@ | |||
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 | |||
} | |||
} |
@ -1,90 +0,0 @@ | |||
package privval | |||
import ( | |||
"net" | |||
"time" | |||
) | |||
// timeoutError can be used to check if an error returned from the netp package | |||
// was due to a timeout. | |||
type timeoutError interface { | |||
Timeout() bool | |||
} | |||
// tcpTimeoutListener implements net.Listener. | |||
var _ net.Listener = (*tcpTimeoutListener)(nil) | |||
// tcpTimeoutListener wraps a *net.TCPListener to standardise protocol timeouts | |||
// and potentially other tuning parameters. | |||
type tcpTimeoutListener struct { | |||
*net.TCPListener | |||
acceptDeadline time.Duration | |||
connDeadline time.Duration | |||
period time.Duration | |||
} | |||
// timeoutConn wraps a net.Conn to standardise protocol timeouts / deadline resets. | |||
type timeoutConn struct { | |||
net.Conn | |||
connDeadline time.Duration | |||
} | |||
// newTCPTimeoutListener returns an instance of tcpTimeoutListener. | |||
func newTCPTimeoutListener( | |||
ln net.Listener, | |||
acceptDeadline, connDeadline time.Duration, | |||
period time.Duration, | |||
) tcpTimeoutListener { | |||
return tcpTimeoutListener{ | |||
TCPListener: ln.(*net.TCPListener), | |||
acceptDeadline: acceptDeadline, | |||
connDeadline: connDeadline, | |||
period: period, | |||
} | |||
} | |||
// newTimeoutConn returns an instance of newTCPTimeoutConn. | |||
func newTimeoutConn( | |||
conn net.Conn, | |||
connDeadline time.Duration) *timeoutConn { | |||
return &timeoutConn{ | |||
conn, | |||
connDeadline, | |||
} | |||
} | |||
// Accept implements net.Listener. | |||
func (ln tcpTimeoutListener) Accept() (net.Conn, error) { | |||
err := ln.SetDeadline(time.Now().Add(ln.acceptDeadline)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
tc, err := ln.AcceptTCP() | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Wrap the conn in our timeout wrapper | |||
conn := newTimeoutConn(tc, ln.connDeadline) | |||
return conn, nil | |||
} | |||
// Read implements net.Listener. | |||
func (c timeoutConn) Read(b []byte) (n int, err error) { | |||
// Reset deadline | |||
c.Conn.SetReadDeadline(time.Now().Add(c.connDeadline)) | |||
return c.Conn.Read(b) | |||
} | |||
// Write implements net.Listener. | |||
func (c timeoutConn) Write(b []byte) (n int, err error) { | |||
// Reset deadline | |||
c.Conn.SetWriteDeadline(time.Now().Add(c.connDeadline)) | |||
return c.Conn.Write(b) | |||
} |