diff --git a/lnd.go b/lnd.go index 8aa31739..f1aedc5b 100644 --- a/lnd.go +++ b/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 - // 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 diff --git a/lntest/harness.go b/lntest/harness.go index bf8dd450..b54234da 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -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 } diff --git a/rpcperms/interceptor.go b/rpcperms/interceptor.go index 5637f647..eb3370b1 100644 --- a/rpcperms/interceptor.go +++ b/rpcperms/interceptor.go @@ -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) + } +}