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{