rpcperms+lnd: gate RPC calls on RPC state

This commit makes us gate the calls to the RPC servers according to the
current RPC state. This ensures we won't try to call the RPC server
before it has been fully initialized, and that we won't call the
walletUnlocker after the wallet already has been unlocked.
This commit is contained in:
Johan T. Halseth 2021-02-05 13:33:01 +01:00
parent 0d7763fb96
commit 2877511fce
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
3 changed files with 204 additions and 28 deletions

26
lnd.go
View File

@ -334,11 +334,18 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
}
}
// Create a new RPC interceptor chain that we'll add to the GRPC
// server. This will be used to log the API calls invoked on the GRPC
// server.
// We'll create the WalletUnlockerService and check whether the wallet
// already exists.
pwService := createWalletUnlockerService(cfg)
walletExists, err := pwService.WalletExists()
if err != nil {
return err
}
// Create a new RPC interceptor that we'll add to the GRPC server. This
// will be used to log the API calls invoked on the GRPC server.
interceptorChain := rpcperms.NewInterceptorChain(
rpcsLog, cfg.NoMacaroons,
rpcsLog, cfg.NoMacaroons, walletExists,
)
rpcServerOpts := interceptorChain.CreateServerOpts()
serverOpts = append(serverOpts, rpcServerOpts...)
@ -346,9 +353,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
grpcServer := grpc.NewServer(serverOpts...)
defer grpcServer.Stop()
// We'll create the WalletUnlockerService and register this with the
// GRPC server.
pwService := createWalletUnlockerService(cfg)
// Register the WalletUnlockerService with the GRPC server.
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
// Initialize, and register our implementation of the gRPC interface
@ -410,6 +415,10 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
}
}
// Now that the wallet password has been provided, transition the RPC
// state into Unlocked.
interceptorChain.SetWalletUnlocked()
var macaroonService *macaroons.Service
if !cfg.NoMacaroons {
// Create the macaroon authentication/authorization service.
@ -739,6 +748,9 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
}
defer rpcServer.Stop()
// We transition the RPC state to Active, as the RPC server is up.
interceptorChain.SetRPCActive()
// If we're not in regtest or simnet mode, We'll wait until we're fully
// synced to continue the start up of the remainder of the daemon. This
// ensures that we don't accept any possibly invalid state transitions, or

View File

@ -300,8 +300,12 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string,
ctxt, cancel := context.WithTimeout(ctxb, DefaultTimeout)
defer cancel()
genSeedResp, err := node.GenSeed(ctxt, genSeedReq)
if err != nil {
var genSeedResp *lnrpc.GenSeedResponse
if err := wait.NoError(func() error {
genSeedResp, err = node.GenSeed(ctxt, genSeedReq)
return err
}, DefaultTimeout); err != nil {
return nil, nil, nil, err
}

View File

@ -7,13 +7,57 @@ import (
"github.com/btcsuite/btclog"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/monitoring"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
// rpcState is an enum that we use to keep track of the current RPC service
// state. This will transition as we go from startup to unlocking the wallet,
// and finally fully active.
type rpcState uint8
const (
// walletNotCreated is the starting state if the RPC server is active,
// but the wallet is not yet created. In this state we'll only allow
// calls to the WalletUnlockerService.
walletNotCreated rpcState = iota
// walletLocked indicates the RPC server is active, but the wallet is
// locked. In this state we'll only allow calls to the
// WalletUnlockerService.
walletLocked
// walletUnlocked means that the wallet has been unlocked, but the full
// RPC server is not yeat ready.
walletUnlocked
// rpcActive means that the RPC server is ready to accept calls.
rpcActive
)
var (
// ErrNoWallet is returned if the wallet does not exist.
ErrNoWallet = fmt.Errorf("wallet not created, create one to enable " +
"full RPC access")
// ErrWalletLocked is returned if the wallet is locked and any service
// other than the WalletUnlocker is called.
ErrWalletLocked = fmt.Errorf("wallet locked, unlock it to enable " +
"full RPC access")
// ErrWalletUnlocked is returned if the WalletUnlocker service is
// called when the wallet already has been unlocked.
ErrWalletUnlocked = fmt.Errorf("wallet already unlocked, " +
"WalletUnlocker service is no longer available")
// ErrRPCStarting is returned if the wallet has been unlocked but the
// RPC server is not yet ready to accept calls.
ErrRPCStarting = fmt.Errorf("the RPC server is in the process of " +
"starting up, but not yet ready to accept calls")
// macaroonWhitelist defines methods that we don't require macaroons to
// access.
macaroonWhitelist = map[string]struct{}{
@ -29,6 +73,9 @@ var (
// intercepting API calls. This is useful for logging, enforcing permissions
// etc.
type InterceptorChain struct {
// state is the current RPC state of our RPC server.
state rpcState
// noMacaroons should be set true if we don't want to check macaroons.
noMacaroons bool
@ -46,14 +93,39 @@ type InterceptorChain struct {
}
// NewInterceptorChain creates a new InterceptorChain.
func NewInterceptorChain(log btclog.Logger, noMacaroons bool) *InterceptorChain {
func NewInterceptorChain(log btclog.Logger, noMacaroons,
walletExists bool) *InterceptorChain {
startState := walletNotCreated
if walletExists {
startState = walletLocked
}
return &InterceptorChain{
state: startState,
noMacaroons: noMacaroons,
permissionMap: make(map[string][]bakery.Op),
rpcsLog: log,
}
}
// SetWalletUnlocked moves the RPC state from either walletNotCreated or
// walletLocked to walletUnlocked.
func (r *InterceptorChain) SetWalletUnlocked() {
r.Lock()
defer r.Unlock()
r.state = walletUnlocked
}
// SetRPCActive moves the RPC state from walletUnlocked to rpcActive.
func (r *InterceptorChain) SetRPCActive() {
r.Lock()
defer r.Unlock()
r.state = rpcActive
}
// AddMacaroonService adds a macaroon service to the interceptor. After this is
// done every RPC call made will have to pass a valid macaroon to be accepted.
func (r *InterceptorChain) AddMacaroonService(svc *macaroons.Service) {
@ -96,16 +168,36 @@ func (r *InterceptorChain) Permissions() map[string][]bakery.Op {
// CreateServerOpts creates the GRPC server options that can be added to a GRPC
// server in order to add this InterceptorChain.
func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
macUnaryInterceptors := []grpc.UnaryServerInterceptor{}
macStrmInterceptors := []grpc.StreamServerInterceptor{}
var unaryInterceptors []grpc.UnaryServerInterceptor
var strmInterceptors []grpc.StreamServerInterceptor
// The first interceptors we'll add to the chain is our logging
// interceptors, so we can automatically log all errors that happen
// during RPC calls.
unaryInterceptors = append(
unaryInterceptors, errorLogUnaryServerInterceptor(r.rpcsLog),
)
strmInterceptors = append(
strmInterceptors, errorLogStreamServerInterceptor(r.rpcsLog),
)
// Next we'll add our RPC state check interceptors, that will check
// whether the attempted call is allowed in the current state.
unaryInterceptors = append(
unaryInterceptors, r.rpcStateUnaryServerInterceptor(),
)
strmInterceptors = append(
strmInterceptors, r.rpcStateStreamServerInterceptor(),
)
// We'll add the macaroon interceptors. If macaroons aren't disabled,
// then these interceptors will enforce macaroon authentication.
unaryInterceptor := r.macaroonUnaryServerInterceptor()
macUnaryInterceptors = append(macUnaryInterceptors, unaryInterceptor)
strmInterceptor := r.macaroonStreamServerInterceptor()
macStrmInterceptors = append(macStrmInterceptors, strmInterceptor)
unaryInterceptors = append(
unaryInterceptors, r.macaroonUnaryServerInterceptor(),
)
strmInterceptors = append(
strmInterceptors, r.macaroonStreamServerInterceptor(),
)
// Get interceptors for Prometheus to gather gRPC performance metrics.
// If monitoring is disabled, GetPromInterceptors() will return empty
@ -114,17 +206,8 @@ func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
monitoring.GetPromInterceptors()
// Concatenate the slices of unary and stream interceptors respectively.
unaryInterceptors := append(macUnaryInterceptors, promUnaryInterceptors...)
strmInterceptors := append(macStrmInterceptors, promStrmInterceptors...)
// We'll also add our logging interceptors as well, so we can
// automatically log all errors that happen during RPC calls.
unaryInterceptors = append(
unaryInterceptors, errorLogUnaryServerInterceptor(r.rpcsLog),
)
strmInterceptors = append(
strmInterceptors, errorLogStreamServerInterceptor(r.rpcsLog),
)
unaryInterceptors = append(unaryInterceptors, promUnaryInterceptors...)
strmInterceptors = append(strmInterceptors, promStrmInterceptors...)
// Create server options from the interceptors we just set up.
chainedUnary := grpc_middleware.WithUnaryServerChain(
@ -248,3 +331,80 @@ func (r *InterceptorChain) macaroonStreamServerInterceptor() grpc.StreamServerIn
return handler(srv, ss)
}
}
// checkRPCState checks whether a call to the given server is allowed in the
// current RPC state.
func (r *InterceptorChain) checkRPCState(srv interface{}) error {
r.RLock()
state := r.state
r.RUnlock()
switch state {
// If the wallet does not exists, only calls to the WalletUnlocker are
// accepted.
case walletNotCreated:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if !ok {
return ErrNoWallet
}
// If the wallet is locked, only calls to the WalletUnlocker are
// accepted.
case walletLocked:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if !ok {
return ErrWalletLocked
}
// If the wallet is unlocked, but the RPC not yet active, we reject.
case walletUnlocked:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if ok {
return ErrWalletUnlocked
}
return ErrRPCStarting
// If the RPC is active, we allow calls to any service except the
// WalletUnlocker.
case rpcActive:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if ok {
return ErrWalletUnlocked
}
default:
return fmt.Errorf("unknown RPC state: %v", state)
}
return nil
}
// rpcStateUnaryServerInterceptor is a GRPC interceptor that checks whether
// calls to the given gGRPC server is allowed in the current rpc state.
func (r *InterceptorChain) rpcStateUnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
if err := r.checkRPCState(info.Server); err != nil {
return nil, err
}
return handler(ctx, req)
}
}
// rpcStateStreamServerInterceptor is a GRPC interceptor that checks whether
// calls to the given gGRPC server is allowed in the current rpc state.
func (r *InterceptorChain) rpcStateStreamServerInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := r.checkRPCState(srv); err != nil {
return err
}
return handler(srv, ss)
}
}