From 3c81a5dd739ed70bdfae4b3c913ea5e8ff66996e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 13 Oct 2020 11:24:40 +0200 Subject: [PATCH] rpcperms: add RPC interceptor chain This adds a new package rpcperms which houses the InterceptorChain struct. This is a central place where we'll craft interceptors to use for the GRPC server, which includes macaroon enforcement. This let us add the interceptor chain to the GRPC server before the macaroon service is ready, allowing us to avoid tearing down the GRPC server after the wallet has been unlocked. --- lnd.go | 16 +++ log.go | 36 ------ macaroons/service.go | 78 +----------- rpcperms/interceptor.go | 259 ++++++++++++++++++++++++++++++++++++++++ rpcserver.go | 122 ++++++------------- 5 files changed, 317 insertions(+), 194 deletions(-) create mode 100644 rpcperms/interceptor.go diff --git a/lnd.go b/lnd.go index 60e3d962..9cf61e5c 100644 --- a/lnd.go +++ b/lnd.go @@ -45,6 +45,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/tor" "github.com/lightningnetwork/lnd/walletunlocker" @@ -375,6 +376,15 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { return getListeners() } + // 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. + interceptorChain := rpcperms.NewInterceptorChain( + rpcsLog, cfg.NoMacaroons, + ) + rpcServerOpts := interceptorChain.CreateServerOpts() + serverOpts = append(serverOpts, rpcServerOpts...) + // We wait until the user provides a password over RPC. In case lnd is // started with the --noseedbackup flag, we use the default password // for wallet encryption. @@ -492,6 +502,11 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { ltndLog.Warnf(msg, "invoice", cfg.InvoiceMacPath) } } + + // We add the macaroon service to our RPC interceptor. This + // will start checking macaroons against permissions on every + // RPC invocation. + interceptorChain.AddMacaroonService(macaroonService) } // Now we're definitely done with the unlocker, shut it down so we can @@ -737,6 +752,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { cfg, server, macaroonService, cfg.SubRPCServers, serverOpts, restDialOpts, restProxyDest, atplManager, server.invoices, tower, restListen, rpcListeners, chainedAcceptor, + interceptorChain, ) if err != nil { err := fmt.Errorf("unable to create RPC server: %v", err) diff --git a/log.go b/log.go index 284b6ffc..c06d7418 100644 --- a/log.go +++ b/log.go @@ -1,8 +1,6 @@ package lnd import ( - "context" - "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btclog" "github.com/lightninglabs/neutrino" @@ -42,7 +40,6 @@ import ( "github.com/lightningnetwork/lnd/sweep" "github.com/lightningnetwork/lnd/watchtower" "github.com/lightningnetwork/lnd/watchtower/wtclient" - "google.golang.org/grpc" ) // replaceableLogger is a thin wrapper around a logger that is used so the @@ -175,36 +172,3 @@ func (c logClosure) String() string { func newLogClosure(c func() string) logClosure { return logClosure(c) } - -// 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 - } -} diff --git a/macaroons/service.go b/macaroons/service.go index e8d9c164..2bb2bc83 100644 --- a/macaroons/service.go +++ b/macaroons/service.go @@ -9,7 +9,6 @@ import ( "time" "github.com/lightningnetwork/lnd/channeldb/kvdb" - "google.golang.org/grpc" "google.golang.org/grpc/metadata" "gopkg.in/macaroon-bakery.v2/bakery" @@ -58,11 +57,11 @@ type Service struct { rks *RootKeyStorage - // externalValidators is a map between an absolute gRPC URIs and the + // ExternalValidators is a map between an absolute gRPC URIs and the // corresponding external macaroon validator to be used for that URI. // If no external validator for an URI is specified, the service will // use the internal validator. - externalValidators map[string]MacaroonValidator + ExternalValidators map[string]MacaroonValidator // StatelessInit denotes if the service was initialized in the stateless // mode where no macaroon files should be created on disk. @@ -125,7 +124,7 @@ func NewService(dir, location string, statelessInit bool, return &Service{ Bakery: *svc, rks: rootKeyStore, - externalValidators: make(map[string]MacaroonValidator), + ExternalValidators: make(map[string]MacaroonValidator), StatelessInit: statelessInit, }, nil } @@ -159,83 +158,16 @@ func (svc *Service) RegisterExternalValidator(fullMethod string, return fmt.Errorf("validator cannot be nil") } - _, ok := svc.externalValidators[fullMethod] + _, ok := svc.ExternalValidators[fullMethod] if ok { return fmt.Errorf("external validator for method %s already "+ "registered", fullMethod) } - svc.externalValidators[fullMethod] = validator + svc.ExternalValidators[fullMethod] = validator return nil } -// UnaryServerInterceptor is a GRPC interceptor that checks whether the -// request is authorized by the included macaroons. -func (svc *Service) UnaryServerInterceptor( - permissionMap map[string][]bakery.Op) grpc.UnaryServerInterceptor { - - return func(ctx context.Context, req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler) (interface{}, error) { - - uriPermissions, ok := permissionMap[info.FullMethod] - if !ok { - return nil, fmt.Errorf("%s: unknown permissions "+ - "required for method", info.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[info.FullMethod] - if !ok { - validator = svc - } - - // Now that we know what validator to use, let it do its work. - err := validator.ValidateMacaroon( - ctx, uriPermissions, info.FullMethod, - ) - if err != nil { - return nil, err - } - - return handler(ctx, req) - } -} - -// StreamServerInterceptor is a GRPC interceptor that checks whether the -// request is authorized by the included macaroons. -func (svc *Service) StreamServerInterceptor( - permissionMap map[string][]bakery.Op) grpc.StreamServerInterceptor { - - return func(srv interface{}, ss grpc.ServerStream, - info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - - uriPermissions, ok := permissionMap[info.FullMethod] - if !ok { - return fmt.Errorf("%s: unknown permissions required "+ - "for method", info.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[info.FullMethod] - if !ok { - validator = svc - } - - // Now that we know what validator to use, let it do its work. - err := validator.ValidateMacaroon( - ss.Context(), uriPermissions, info.FullMethod, - ) - if err != nil { - return err - } - - return handler(srv, ss) - } -} - // ValidateMacaroon validates the capabilities of a given request given a // bakery service, context, and uri. Within the passed context.Context, we // expect a macaroon to be encoded as request metadata using the key diff --git a/rpcperms/interceptor.go b/rpcperms/interceptor.go new file mode 100644 index 00000000..ea918662 --- /dev/null +++ b/rpcperms/interceptor.go @@ -0,0 +1,259 @@ +package rpcperms + +import ( + "context" + "fmt" + "sync" + + "github.com/btcsuite/btclog" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/monitoring" + "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// 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 { + // 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 + + sync.RWMutex +} + +// NewInterceptorChain creates a new InterceptorChain. +func NewInterceptorChain(log btclog.Logger, noMacaroons bool) *InterceptorChain { + return &InterceptorChain{ + noMacaroons: noMacaroons, + permissionMap: make(map[string][]bakery.Op), + rpcsLog: log, + } +} + +// 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 { + macUnaryInterceptors := []grpc.UnaryServerInterceptor{} + macStrmInterceptors := []grpc.StreamServerInterceptor{} + + // 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) + + // 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(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), + ) + + // 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 + } +} + +// 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) { + + // If noMacaroons are set, we'll always allow the call. + if r.noMacaroons { + return handler(ctx, req) + } + + r.RLock() + svc := r.svc + r.RUnlock() + + // If the macaroon service is not yet active, allow the call. + // THis means that the wallet has not yet been unlocked, and we + // allow calls to the WalletUnlockerService. + if svc == nil { + return handler(ctx, req) + } + + r.RLock() + uriPermissions, ok := r.permissionMap[info.FullMethod] + r.RUnlock() + if !ok { + return nil, fmt.Errorf("%s: unknown permissions "+ + "required for method", info.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[info.FullMethod] + if !ok { + validator = svc + } + + // Now that we know what validator to use, let it do its work. + err := validator.ValidateMacaroon( + ctx, uriPermissions, info.FullMethod, + ) + if 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 { + + // If noMacaroons are set, we'll always allow the call. + if r.noMacaroons { + return handler(srv, ss) + } + + r.RLock() + svc := r.svc + r.RUnlock() + + // If the macaroon service is not yet active, allow the call. + // THis means that the wallet has not yet been unlocked, and we + // allow calls to the WalletUnlockerService. + if svc == nil { + return handler(srv, ss) + } + + r.RLock() + uriPermissions, ok := r.permissionMap[info.FullMethod] + r.RUnlock() + if !ok { + return fmt.Errorf("%s: unknown permissions required "+ + "for method", info.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[info.FullMethod] + if !ok { + validator = svc + } + + // Now that we know what validator to use, let it do its work. + err := validator.ValidateMacaroon( + ss.Context(), uriPermissions, info.FullMethod, + ) + if err != nil { + return err + } + + return handler(srv, ss) + } +} diff --git a/rpcserver.go b/rpcserver.go index a7b200bd..954a7221 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -28,7 +28,6 @@ import ( "github.com/btcsuite/btcutil/psbt" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/davecgh/go-spew/spew" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" @@ -67,6 +66,7 @@ import ( "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sweep" "github.com/lightningnetwork/lnd/watchtower" @@ -539,9 +539,8 @@ type rpcServer struct { // selfNode is our own pubkey. selfNode route.Vertex - // allPermissions is a map of all registered gRPC URIs (including - // internal and external subservers) to the permissions they require. - allPermissions map[string][]bakery.Op + // interceptorChain is the the interceptor added to our gRPC server. + interceptorChain *rpcperms.InterceptorChain } // A compile time check to ensure that rpcServer fully implements the @@ -559,8 +558,8 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, atpl *autopilot.Manager, invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone, restListen func(net.Addr) (net.Listener, error), - getListeners rpcListeners, - chanPredicate *chanacceptor.ChainedAcceptor) (*rpcServer, error) { + getListeners rpcListeners, chanPredicate *chanacceptor.ChainedAcceptor, + interceptorChain *rpcperms.InterceptorChain) (*rpcServer, error) { // Set up router rpc backend. channelGraph := s.localChanDB.ChannelGraph() @@ -659,18 +658,19 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, // Next, we need to merge the set of sub server macaroon permissions // with the main RPC server permissions so we can unite them under a // single set of interceptors. - permissions := MainRPCServerPermissions() + for m, ops := range MainRPCServerPermissions() { + err := interceptorChain.AddPermission(m, ops) + if err != nil { + return nil, err + } + } + for _, subServerPerm := range subServerPerms { for method, ops := range subServerPerm { - // For each new method:ops combo, we also ensure that - // non of the sub-servers try to override each other. - if _, ok := permissions[method]; ok { - return nil, fmt.Errorf("detected duplicate "+ - "macaroon constraints for path: %v", - method) + err := interceptorChain.AddPermission(method, ops) + if err != nil { + return nil, err } - - permissions[method] = ops } } @@ -687,18 +687,11 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, if extSubserver != nil { macValidator := extSubserver.MacaroonValidator for method, ops := range extSubserver.Permissions { - // For each new method:ops combo, we also ensure - // that non of the sub-servers try to override - // each other. - if _, ok := permissions[method]; ok { - return nil, fmt.Errorf("detected "+ - "duplicate macaroon "+ - "constraints for path: %v", - method) + err := interceptorChain.AddPermission(method, ops) + if err != nil { + return nil, err } - permissions[method] = ops - // Give the external subservers the possibility // to also use their own validator to check any // macaroons attached to calls to this method. @@ -719,68 +712,26 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, } } - // If macaroons aren't disabled (a non-nil service), then we'll set up - // our set of interceptors which will allow us to handle the macaroon - // authentication in a single location. - macUnaryInterceptors := []grpc.UnaryServerInterceptor{} - macStrmInterceptors := []grpc.StreamServerInterceptor{} - if macService != nil { - unaryInterceptor := macService.UnaryServerInterceptor(permissions) - macUnaryInterceptors = append(macUnaryInterceptors, unaryInterceptor) - - strmInterceptor := macService.StreamServerInterceptor(permissions) - macStrmInterceptors = append(macStrmInterceptors, strmInterceptor) - } - - // 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(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(rpcsLog), - ) - strmInterceptors = append( - strmInterceptors, errorLogStreamServerInterceptor(rpcsLog), - ) - - // If any interceptors have been set up, add them to the server options. - if len(unaryInterceptors) != 0 && len(strmInterceptors) != 0 { - chainedUnary := grpc_middleware.WithUnaryServerChain( - unaryInterceptors..., - ) - chainedStream := grpc_middleware.WithStreamServerChain( - strmInterceptors..., - ) - serverOpts = append(serverOpts, chainedUnary, chainedStream) - } - // Finally, with all the pre-set up complete, we can create the main // gRPC server, and register the main lnrpc server along side. grpcServer := grpc.NewServer(serverOpts...) rootRPCServer := &rpcServer{ - cfg: cfg, - restDialOpts: restDialOpts, - listeners: listeners, - listenerCleanUp: []func(){cleanup}, - restProxyDest: restProxyDest, - subServers: subServers, - subGrpcHandlers: subGrpcHandlers, - restListen: restListen, - grpcServer: grpcServer, - server: s, - routerBackend: routerBackend, - chanPredicate: chanPredicate, - quit: make(chan struct{}, 1), - macService: macService, - selfNode: selfNode.PubKeyBytes, - allPermissions: permissions, + cfg: cfg, + restDialOpts: restDialOpts, + listeners: listeners, + listenerCleanUp: []func(){cleanup}, + restProxyDest: restProxyDest, + subServers: subServers, + subGrpcHandlers: subGrpcHandlers, + restListen: restListen, + grpcServer: grpcServer, + server: s, + routerBackend: routerBackend, + chanPredicate: chanPredicate, + quit: make(chan struct{}, 1), + macService: macService, + selfNode: selfNode.PubKeyBytes, + interceptorChain: interceptorChain, } lnrpc.RegisterLightningServer(grpcServer, rootRPCServer) @@ -6546,7 +6497,8 @@ func (r *rpcServer) BakeMacaroon(ctx context.Context, // Either we have the special entity "uri" which specifies a // full gRPC URI or we have one of the pre-defined actions. if op.Entity == macaroons.PermissionEntityCustomURI { - _, ok := r.allPermissions[op.Action] + allPermissions := r.interceptorChain.Permissions() + _, ok := allPermissions[op.Action] if !ok { return nil, fmt.Errorf("invalid permission " + "action, must be an existing URI in " + @@ -6661,7 +6613,7 @@ func (r *rpcServer) ListPermissions(_ context.Context, rpcsLog.Debugf("[listpermissions]") permissionMap := make(map[string]*lnrpc.MacaroonPermissionList) - for uri, perms := range r.allPermissions { + for uri, perms := range r.interceptorChain.Permissions() { rpcPerms := make([]*lnrpc.MacaroonPermission, len(perms)) for idx, perm := range perms { rpcPerms[idx] = &lnrpc.MacaroonPermission{