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.
This commit is contained in:
Johan T. Halseth 2020-10-13 11:24:40 +02:00
parent 4ea494e8c5
commit 3c81a5dd73
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
5 changed files with 317 additions and 194 deletions

16
lnd.go
View File

@ -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)

36
log.go
View File

@ -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
}
}

View File

@ -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

259
rpcperms/interceptor.go Normal file
View File

@ -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)
}
}

View File

@ -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{