You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
592 lines
16 KiB
592 lines
16 KiB
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) |
|
} |
|
}
|
|
|