9aacc35989
This commit adds a new GetState call to the state service in order to make leader election integrate simply with k8s using a readinessProbe.
593 lines
16 KiB
Go
593 lines
16 KiB
Go
package rpcperms
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"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"
|
|
"github.com/lightningnetwork/lnd/subscribe"
|
|
"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 (
|
|
// waitingToStart indicates that we're at the beginning of the startup
|
|
// process. In a cluster evironment this may mean that we're waiting to
|
|
// become the leader in which case RPC calls will be disabled until
|
|
// this instance has been elected as leader.
|
|
waitingToStart rpcState = iota
|
|
|
|
// 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
|
|
|
|
// 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 (
|
|
// ErrWaitingToStart is returned if LND is still wating to start,
|
|
// possibly blocked until elected as the leader.
|
|
ErrWaitingToStart = fmt.Errorf("waiting to start, RPC services not " +
|
|
"available")
|
|
|
|
// 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{}{
|
|
// We allow all calls to the WalletUnlocker without macaroons.
|
|
"/lnrpc.WalletUnlocker/GenSeed": {},
|
|
"/lnrpc.WalletUnlocker/InitWallet": {},
|
|
"/lnrpc.WalletUnlocker/UnlockWallet": {},
|
|
"/lnrpc.WalletUnlocker/ChangePassword": {},
|
|
|
|
// The State service must be available at all times, even
|
|
// before we can check macaroons, so we whitelist it.
|
|
"/lnrpc.State/SubscribeState": {},
|
|
"/lnrpc.State/GetState": {},
|
|
}
|
|
)
|
|
|
|
// InterceptorChain is a struct that can be added to the running GRPC server,
|
|
// intercepting API calls. This is useful for logging, enforcing permissions
|
|
// etc.
|
|
type InterceptorChain struct {
|
|
started sync.Once
|
|
stopped sync.Once
|
|
|
|
// state is the current RPC state of our RPC server.
|
|
state rpcState
|
|
|
|
// ntfnServer is a subscription server we use to notify clients of the
|
|
// State service when the state changes.
|
|
ntfnServer *subscribe.Server
|
|
|
|
// noMacaroons should be set true if we don't want to check macaroons.
|
|
noMacaroons bool
|
|
|
|
// svc is the macaroon service used to enforce permissions in case
|
|
// macaroons are used.
|
|
svc *macaroons.Service
|
|
|
|
// permissionMap is the permissions to enforce if macaroons are used.
|
|
permissionMap map[string][]bakery.Op
|
|
|
|
// rpcsLog is the logger used to log calles to the RPCs intercepted.
|
|
rpcsLog btclog.Logger
|
|
|
|
quit chan struct{}
|
|
sync.RWMutex
|
|
}
|
|
|
|
// A compile time check to ensure that InterceptorChain fully implements the
|
|
// StateServer gRPC service.
|
|
var _ lnrpc.StateServer = (*InterceptorChain)(nil)
|
|
|
|
// NewInterceptorChain creates a new InterceptorChain.
|
|
func NewInterceptorChain(log btclog.Logger, noMacaroons bool) *InterceptorChain {
|
|
return &InterceptorChain{
|
|
state: waitingToStart,
|
|
ntfnServer: subscribe.NewServer(),
|
|
noMacaroons: noMacaroons,
|
|
permissionMap: make(map[string][]bakery.Op),
|
|
rpcsLog: log,
|
|
quit: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start starts the InterceptorChain, which is needed to start the state
|
|
// subscription server it powers.
|
|
func (r *InterceptorChain) Start() error {
|
|
var err error
|
|
r.started.Do(func() {
|
|
err = r.ntfnServer.Start()
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// Stop stops the InterceptorChain and its internal state subscription server.
|
|
func (r *InterceptorChain) Stop() error {
|
|
var err error
|
|
r.stopped.Do(func() {
|
|
close(r.quit)
|
|
err = r.ntfnServer.Stop()
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// SetWalletNotCreated moves the RPC state from either waitingToStart to
|
|
// walletNotCreated.
|
|
func (r *InterceptorChain) SetWalletNotCreated() {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
r.state = walletNotCreated
|
|
_ = r.ntfnServer.SendUpdate(r.state)
|
|
}
|
|
|
|
// SetWalletLocked moves the RPC state from either walletNotCreated to
|
|
// walletLocked.
|
|
func (r *InterceptorChain) SetWalletLocked() {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
r.state = walletLocked
|
|
_ = r.ntfnServer.SendUpdate(r.state)
|
|
}
|
|
|
|
// SetWalletUnlocked moves the RPC state from either walletNotCreated or
|
|
// walletLocked to walletUnlocked.
|
|
func (r *InterceptorChain) SetWalletUnlocked() {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
r.state = walletUnlocked
|
|
_ = r.ntfnServer.SendUpdate(r.state)
|
|
}
|
|
|
|
// SetRPCActive moves the RPC state from walletUnlocked to rpcActive.
|
|
func (r *InterceptorChain) SetRPCActive() {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
r.state = rpcActive
|
|
_ = r.ntfnServer.SendUpdate(r.state)
|
|
}
|
|
|
|
// rpcStateToWalletState converts rpcState to lnrpc.WalletState. Returns
|
|
// WAITING_TO_START and an error on conversion error.
|
|
func rpcStateToWalletState(state rpcState) (lnrpc.WalletState, error) {
|
|
const defaultState = lnrpc.WalletState_WAITING_TO_START
|
|
var walletState lnrpc.WalletState
|
|
|
|
switch state {
|
|
case waitingToStart:
|
|
walletState = lnrpc.WalletState_WAITING_TO_START
|
|
case walletNotCreated:
|
|
walletState = lnrpc.WalletState_NON_EXISTING
|
|
case walletLocked:
|
|
walletState = lnrpc.WalletState_LOCKED
|
|
case walletUnlocked:
|
|
walletState = lnrpc.WalletState_UNLOCKED
|
|
case rpcActive:
|
|
walletState = lnrpc.WalletState_RPC_ACTIVE
|
|
|
|
default:
|
|
return defaultState, fmt.Errorf("unknown wallet state %v", state)
|
|
}
|
|
|
|
return walletState, nil
|
|
}
|
|
|
|
// SubscribeState subscribes to the state of the wallet. The current wallet
|
|
// state will always be delivered immediately.
|
|
//
|
|
// NOTE: Part of the StateService interface.
|
|
func (r *InterceptorChain) SubscribeState(req *lnrpc.SubscribeStateRequest,
|
|
stream lnrpc.State_SubscribeStateServer) error {
|
|
|
|
sendStateUpdate := func(state rpcState) error {
|
|
walletState, err := rpcStateToWalletState(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return stream.Send(&lnrpc.SubscribeStateResponse{
|
|
State: walletState,
|
|
})
|
|
}
|
|
|
|
// Subscribe to state updates.
|
|
client, err := r.ntfnServer.Subscribe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Cancel()
|
|
|
|
// Always start by sending the current state.
|
|
r.RLock()
|
|
state := r.state
|
|
r.RUnlock()
|
|
|
|
if err := sendStateUpdate(state); err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case e := <-client.Updates():
|
|
newState := e.(rpcState)
|
|
|
|
// Ignore already sent state.
|
|
if newState == state {
|
|
continue
|
|
}
|
|
|
|
state = newState
|
|
err := sendStateUpdate(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case <-stream.Context().Done():
|
|
return stream.Context().Err()
|
|
|
|
case <-r.quit:
|
|
return fmt.Errorf("server exiting")
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetState returns he current wallet state.
|
|
func (r *InterceptorChain) GetState(_ context.Context,
|
|
req *lnrpc.GetStateRequest) (*lnrpc.GetStateResponse, error) {
|
|
|
|
r.RLock()
|
|
state := r.state
|
|
r.RUnlock()
|
|
|
|
walletState, err := rpcStateToWalletState(state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lnrpc.GetStateResponse{
|
|
State: walletState,
|
|
}, nil
|
|
}
|
|
|
|
// 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) {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
r.svc = svc
|
|
}
|
|
|
|
// AddPermission adds a new macaroon rule for the given method.
|
|
func (r *InterceptorChain) AddPermission(method string, ops []bakery.Op) error {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
|
|
if _, ok := r.permissionMap[method]; ok {
|
|
return fmt.Errorf("detected duplicate macaroon constraints "+
|
|
"for path: %v", method)
|
|
}
|
|
|
|
r.permissionMap[method] = ops
|
|
return nil
|
|
}
|
|
|
|
// Permissions returns the current set of macaroon permissions.
|
|
func (r *InterceptorChain) Permissions() map[string][]bakery.Op {
|
|
r.RLock()
|
|
defer r.RUnlock()
|
|
|
|
// Make a copy under the read lock to avoid races.
|
|
c := make(map[string][]bakery.Op)
|
|
for k, v := range r.permissionMap {
|
|
s := make([]bakery.Op, len(v))
|
|
copy(s, v)
|
|
c[k] = s
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// 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 {
|
|
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.
|
|
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
|
|
// slices.
|
|
promUnaryInterceptors, promStrmInterceptors :=
|
|
monitoring.GetPromInterceptors()
|
|
|
|
// Concatenate the slices of unary and stream interceptors respectively.
|
|
unaryInterceptors = append(unaryInterceptors, promUnaryInterceptors...)
|
|
strmInterceptors = append(strmInterceptors, promStrmInterceptors...)
|
|
|
|
// Create server options from the interceptors we just set up.
|
|
chainedUnary := grpc_middleware.WithUnaryServerChain(
|
|
unaryInterceptors...,
|
|
)
|
|
chainedStream := grpc_middleware.WithStreamServerChain(
|
|
strmInterceptors...,
|
|
)
|
|
serverOpts := []grpc.ServerOption{chainedUnary, chainedStream}
|
|
|
|
return serverOpts
|
|
}
|
|
|
|
// errorLogUnaryServerInterceptor is a simple UnaryServerInterceptor that will
|
|
// automatically log any errors that occur when serving a client's unary
|
|
// request.
|
|
func errorLogUnaryServerInterceptor(logger btclog.Logger) grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler) (interface{}, error) {
|
|
|
|
resp, err := handler(ctx, req)
|
|
if err != nil {
|
|
// TODO(roasbeef): also log request details?
|
|
logger.Errorf("[%v]: %v", info.FullMethod, err)
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
}
|
|
|
|
// errorLogStreamServerInterceptor is a simple StreamServerInterceptor that
|
|
// will log any errors that occur while processing a client or server streaming
|
|
// RPC.
|
|
func errorLogStreamServerInterceptor(logger btclog.Logger) grpc.StreamServerInterceptor {
|
|
return func(srv interface{}, ss grpc.ServerStream,
|
|
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
|
|
err := handler(srv, ss)
|
|
if err != nil {
|
|
logger.Errorf("[%v]: %v", info.FullMethod, err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
// checkMacaroon validates that the context contains the macaroon needed to
|
|
// invoke the given RPC method.
|
|
func (r *InterceptorChain) checkMacaroon(ctx context.Context,
|
|
fullMethod string) error {
|
|
|
|
// If noMacaroons is set, we'll always allow the call.
|
|
if r.noMacaroons {
|
|
return nil
|
|
}
|
|
|
|
// Check whether the method is whitelisted, if so we'll allow it
|
|
// regardless of macaroons.
|
|
_, ok := macaroonWhitelist[fullMethod]
|
|
if ok {
|
|
return nil
|
|
}
|
|
|
|
r.RLock()
|
|
svc := r.svc
|
|
r.RUnlock()
|
|
|
|
// If the macaroon service is not yet active, we cannot allow
|
|
// the call.
|
|
if svc == nil {
|
|
return fmt.Errorf("unable to determine macaroon permissions")
|
|
}
|
|
|
|
r.RLock()
|
|
uriPermissions, ok := r.permissionMap[fullMethod]
|
|
r.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("%s: unknown permissions required for method",
|
|
fullMethod)
|
|
}
|
|
|
|
// Find out if there is an external validator registered for
|
|
// this method. Fall back to the internal one if there isn't.
|
|
validator, ok := svc.ExternalValidators[fullMethod]
|
|
if !ok {
|
|
validator = svc
|
|
}
|
|
|
|
// Now that we know what validator to use, let it do its work.
|
|
return validator.ValidateMacaroon(ctx, uriPermissions, fullMethod)
|
|
}
|
|
|
|
// MacaroonUnaryServerInterceptor is a GRPC interceptor that checks whether the
|
|
// request is authorized by the included macaroons.
|
|
func (r *InterceptorChain) MacaroonUnaryServerInterceptor() grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, req interface{},
|
|
info *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler) (interface{}, error) {
|
|
|
|
// Check macaroons.
|
|
if err := r.checkMacaroon(ctx, info.FullMethod); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return handler(ctx, req)
|
|
}
|
|
}
|
|
|
|
// MacaroonStreamServerInterceptor is a GRPC interceptor that checks whether
|
|
// the request is authorized by the included macaroons.
|
|
func (r *InterceptorChain) MacaroonStreamServerInterceptor() grpc.StreamServerInterceptor {
|
|
return func(srv interface{}, ss grpc.ServerStream,
|
|
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
|
|
// Check macaroons.
|
|
err := r.checkMacaroon(ss.Context(), info.FullMethod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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 {
|
|
// The StateService is being accessed, we allow the call regardless of
|
|
// the current state.
|
|
_, ok := srv.(lnrpc.StateServer)
|
|
if ok {
|
|
return nil
|
|
}
|
|
|
|
r.RLock()
|
|
state := r.state
|
|
r.RUnlock()
|
|
|
|
switch state {
|
|
|
|
// Do not accept any RPC calls (unless to the state service) until LND
|
|
// has not started.
|
|
case waitingToStart:
|
|
return ErrWaitingToStart
|
|
|
|
// 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)
|
|
}
|
|
}
|