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:
parent
0d7763fb96
commit
2877511fce
26
lnd.go
26
lnd.go
@ -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
|
// We'll create the WalletUnlockerService and check whether the wallet
|
||||||
// server. This will be used to log the API calls invoked on the GRPC
|
// already exists.
|
||||||
// server.
|
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(
|
interceptorChain := rpcperms.NewInterceptorChain(
|
||||||
rpcsLog, cfg.NoMacaroons,
|
rpcsLog, cfg.NoMacaroons, walletExists,
|
||||||
)
|
)
|
||||||
rpcServerOpts := interceptorChain.CreateServerOpts()
|
rpcServerOpts := interceptorChain.CreateServerOpts()
|
||||||
serverOpts = append(serverOpts, rpcServerOpts...)
|
serverOpts = append(serverOpts, rpcServerOpts...)
|
||||||
@ -346,9 +353,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
|
|||||||
grpcServer := grpc.NewServer(serverOpts...)
|
grpcServer := grpc.NewServer(serverOpts...)
|
||||||
defer grpcServer.Stop()
|
defer grpcServer.Stop()
|
||||||
|
|
||||||
// We'll create the WalletUnlockerService and register this with the
|
// Register the WalletUnlockerService with the GRPC server.
|
||||||
// GRPC server.
|
|
||||||
pwService := createWalletUnlockerService(cfg)
|
|
||||||
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
lnrpc.RegisterWalletUnlockerServer(grpcServer, pwService)
|
||||||
|
|
||||||
// Initialize, and register our implementation of the gRPC interface
|
// 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
|
var macaroonService *macaroons.Service
|
||||||
if !cfg.NoMacaroons {
|
if !cfg.NoMacaroons {
|
||||||
// Create the macaroon authentication/authorization service.
|
// Create the macaroon authentication/authorization service.
|
||||||
@ -739,6 +748,9 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
|
|||||||
}
|
}
|
||||||
defer rpcServer.Stop()
|
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
|
// 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
|
// 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
|
// ensures that we don't accept any possibly invalid state transitions, or
|
||||||
|
@ -300,8 +300,12 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string,
|
|||||||
|
|
||||||
ctxt, cancel := context.WithTimeout(ctxb, DefaultTimeout)
|
ctxt, cancel := context.WithTimeout(ctxb, DefaultTimeout)
|
||||||
defer cancel()
|
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
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,13 +7,57 @@ import (
|
|||||||
|
|
||||||
"github.com/btcsuite/btclog"
|
"github.com/btcsuite/btclog"
|
||||||
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/lightningnetwork/lnd/monitoring"
|
"github.com/lightningnetwork/lnd/monitoring"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
"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 (
|
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
|
// macaroonWhitelist defines methods that we don't require macaroons to
|
||||||
// access.
|
// access.
|
||||||
macaroonWhitelist = map[string]struct{}{
|
macaroonWhitelist = map[string]struct{}{
|
||||||
@ -29,6 +73,9 @@ var (
|
|||||||
// intercepting API calls. This is useful for logging, enforcing permissions
|
// intercepting API calls. This is useful for logging, enforcing permissions
|
||||||
// etc.
|
// etc.
|
||||||
type InterceptorChain struct {
|
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 should be set true if we don't want to check macaroons.
|
||||||
noMacaroons bool
|
noMacaroons bool
|
||||||
|
|
||||||
@ -46,14 +93,39 @@ type InterceptorChain struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewInterceptorChain creates a new InterceptorChain.
|
// 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{
|
return &InterceptorChain{
|
||||||
|
state: startState,
|
||||||
noMacaroons: noMacaroons,
|
noMacaroons: noMacaroons,
|
||||||
permissionMap: make(map[string][]bakery.Op),
|
permissionMap: make(map[string][]bakery.Op),
|
||||||
rpcsLog: log,
|
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
|
// 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.
|
// done every RPC call made will have to pass a valid macaroon to be accepted.
|
||||||
func (r *InterceptorChain) AddMacaroonService(svc *macaroons.Service) {
|
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
|
// CreateServerOpts creates the GRPC server options that can be added to a GRPC
|
||||||
// server in order to add this InterceptorChain.
|
// server in order to add this InterceptorChain.
|
||||||
func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
|
func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
|
||||||
macUnaryInterceptors := []grpc.UnaryServerInterceptor{}
|
var unaryInterceptors []grpc.UnaryServerInterceptor
|
||||||
macStrmInterceptors := []grpc.StreamServerInterceptor{}
|
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,
|
// We'll add the macaroon interceptors. If macaroons aren't disabled,
|
||||||
// then these interceptors will enforce macaroon authentication.
|
// then these interceptors will enforce macaroon authentication.
|
||||||
unaryInterceptor := r.macaroonUnaryServerInterceptor()
|
unaryInterceptors = append(
|
||||||
macUnaryInterceptors = append(macUnaryInterceptors, unaryInterceptor)
|
unaryInterceptors, r.macaroonUnaryServerInterceptor(),
|
||||||
|
)
|
||||||
strmInterceptor := r.macaroonStreamServerInterceptor()
|
strmInterceptors = append(
|
||||||
macStrmInterceptors = append(macStrmInterceptors, strmInterceptor)
|
strmInterceptors, r.macaroonStreamServerInterceptor(),
|
||||||
|
)
|
||||||
|
|
||||||
// Get interceptors for Prometheus to gather gRPC performance metrics.
|
// Get interceptors for Prometheus to gather gRPC performance metrics.
|
||||||
// If monitoring is disabled, GetPromInterceptors() will return empty
|
// If monitoring is disabled, GetPromInterceptors() will return empty
|
||||||
@ -114,17 +206,8 @@ func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
|
|||||||
monitoring.GetPromInterceptors()
|
monitoring.GetPromInterceptors()
|
||||||
|
|
||||||
// Concatenate the slices of unary and stream interceptors respectively.
|
// Concatenate the slices of unary and stream interceptors respectively.
|
||||||
unaryInterceptors := append(macUnaryInterceptors, promUnaryInterceptors...)
|
unaryInterceptors = append(unaryInterceptors, promUnaryInterceptors...)
|
||||||
strmInterceptors := append(macStrmInterceptors, promStrmInterceptors...)
|
strmInterceptors = append(strmInterceptors, 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),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create server options from the interceptors we just set up.
|
// Create server options from the interceptors we just set up.
|
||||||
chainedUnary := grpc_middleware.WithUnaryServerChain(
|
chainedUnary := grpc_middleware.WithUnaryServerChain(
|
||||||
@ -248,3 +331,80 @@ func (r *InterceptorChain) macaroonStreamServerInterceptor() grpc.StreamServerIn
|
|||||||
return handler(srv, ss)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user