diff --git a/README.md b/README.md index afac59f4d..5baf695a7 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ To use LiT with a remote `lnd` instance please [follow these instructions](./doc Note that LiT requires `lnd` to be built with **all of its subservers** and requires running at least v0.11.0. Download the latest [official release binary](https://github.com/lightningnetwork/lnd/releases/latest) or build `lnd` from source by following the [installation instructions](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md). If you choose to build `lnd` from source, use the following command to enable all the relevant subservers: ```shell -⛰ make install tags="signrpc walletrpc chainrpc invoicesrpc" +⛰ make install tags="signrpc walletrpc chainrpc invoicesrpc verrpc" ``` ## Interaction diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index 0bb0594e7..fdd0c0bd4 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -834,7 +834,7 @@ func bakeSuperMacaroon(cfg *LitNodeConfig, readOnly bool) (string, error) { lndAdminCtx := macaroonContext(ctxt, lndAdminMacBytes) lndConn := lnrpc.NewLightningClient(rawConn) - superMacPermissions := terminal.GetAllPermissions(readOnly) + superMacPermissions := terminal.GetAllPermissions(readOnly, nil) nullID := [4]byte{} superMacHex, err := terminal.BakeSuperMacaroon( lndAdminCtx, lndConn, session.NewSuperMacaroonRootKeyID(nullID), diff --git a/rpc_proxy.go b/rpc_proxy.go index 1662da772..00a07559a 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "strings" + "sync" "time" "github.com/improbable-eng/grpc-web/go/grpcweb" @@ -59,7 +60,7 @@ func (e *proxyErr) Unwrap() error { // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, superMacValidator session.SuperMacaroonValidator, - permissionMap map[string][]bakery.Op, + getPermissionMap func() map[string][]bakery.Op, bufListener *bufconn.Listener) *rpcProxy { // The gRPC web calls are protected by HTTP basic auth which is defined @@ -77,7 +78,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, p := &rpcProxy{ cfg: cfg, basicAuth: basicAuth, - permissionMap: permissionMap, + getPermsMap: getPermissionMap, macValidator: validator, superMacValidator: superMacValidator, bufListener: bufListener, @@ -146,9 +147,16 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, // +---------------------+ // type rpcProxy struct { - cfg *Config - basicAuth string + cfg *Config + basicAuth string + + // permissionMap holds the required permissions for each supported URI. + // This map most _not_ be accessed directly. Instead, the + // rpcProxy.getPerms method should always be used. permissionMap map[string][]bakery.Op + getPermsMap func() map[string][]bakery.Op + getPermsOnce sync.Once + permsMu sync.Mutex macValidator macaroons.MacaroonValidator superMacValidator session.SuperMacaroonValidator @@ -373,7 +381,7 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - uriPermissions, ok := p.permissionMap[info.FullMethod] + uriPermissions, ok := p.getPerms()[info.FullMethod] if !ok { return nil, fmt.Errorf("%s: unknown permissions "+ "required for method", info.FullMethod) @@ -414,7 +422,7 @@ func (p *rpcProxy) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - uriPermissions, ok := p.permissionMap[info.FullMethod] + uriPermissions, ok := p.getPerms()[info.FullMethod] if !ok { return fmt.Errorf("%s: unknown permissions required "+ "for method", info.FullMethod) @@ -580,7 +588,7 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, fullMethod string) ([]byte, error) { - requiredPermissions, ok := p.permissionMap[fullMethod] + requiredPermissions, ok := p.getPerms()[fullMethod] if !ok { return nil, fmt.Errorf("%s: unknown permissions required for "+ "method", fullMethod) @@ -624,6 +632,20 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, return nil, nil } +// getPerms returns a map from URI to permissions required for the URI. The +// first time it is called, the getPermsMap is used to set the contents of the +// map. After that, the same map is returned. +func (p *rpcProxy) getPerms() map[string][]bakery.Op { + p.permsMu.Lock() + defer p.permsMu.Unlock() + + p.getPermsOnce.Do(func() { + p.permissionMap = p.getPermsMap() + }) + + return p.permissionMap +} + // dialBufConnBackend dials an in-memory connection to an RPC listener and // ignores any TLS certificate mismatches. func dialBufConnBackend(listener *bufconn.Listener) (*grpc.ClientConn, error) { diff --git a/session_rpcserver.go b/session_rpcserver.go index ed4032157..3554e5ad7 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -12,6 +12,7 @@ import ( "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/session" "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" "gopkg.in/macaroon.v2" ) @@ -39,6 +40,7 @@ type sessionRpcServerConfig struct { superMacBaker func(ctx context.Context, rootKeyID uint64, recipe *session.MacaroonRecipe) (string, error) firstConnectionDeadline time.Duration + getAllPermissions func(readOnly bool) []bakery.Op } // newSessionRPCServer creates a new sessionRpcServer using the passed config. @@ -205,7 +207,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { mac, err := s.cfg.superMacBaker( context.Background(), sess.MacaroonRootKey, &session.MacaroonRecipe{ - Permissions: GetAllPermissions(readOnly), + Permissions: s.cfg.getAllPermissions(readOnly), Caveats: caveats, }, ) diff --git a/subserver_permissions.go b/subserver_permissions.go index f350f5e66..42b95c33a 100644 --- a/subserver_permissions.go +++ b/subserver_permissions.go @@ -1,10 +1,30 @@ package terminal import ( + "net" + "strings" + faraday "github.com/lightninglabs/faraday/frdrpcserver/perms" loop "github.com/lightninglabs/loop/loopd/perms" pool "github.com/lightninglabs/pool/perms" "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/autopilot" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" + "github.com/lightningnetwork/lnd/lnrpc/chainrpc" + "github.com/lightningnetwork/lnd/lnrpc/devrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/neutrinorpc" + "github.com/lightningnetwork/lnd/lnrpc/peersrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" + "github.com/lightningnetwork/lnd/lntest/mock" + "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/sweep" "gopkg.in/macaroon-bakery.v2/bakery" ) @@ -39,13 +59,30 @@ var ( "/lnrpc.State/SubscribeState": {}, "/lnrpc.State/GetState": {}, } + + // lndSubServerNameToTag is a map from the name of an LND subserver to + // the name of the LND tag that corresponds to the subserver. This map + // only contains the subserver-to-tag pairs for the pairs where the + // names differ. + lndSubServerNameToTag = map[string]string{ + "WalletKitRPC": "walletrpc", + "DevRPC": "dev", + "NeutrinoKitRPC": "neutrinorpc", + "VersionRPC": "verrpc", + "WatchtowerClientRPC": "wtclientrpc", + } + + // lndSubServerPerms is a map from LND subserver name to permissions + // map. + lndSubServerPerms = make(map[string]map[string][]bakery.Op) ) // getSubserverPermissions returns a merged map of all subserver macaroon // permissions. func getSubserverPermissions() map[string][]bakery.Op { - mapSize := len(faraday.RequiredPermissions) + + mapSize := len(litPermissions) + len(faraday.RequiredPermissions) + len(loop.RequiredPermissions) + len(pool.RequiredPermissions) + result := make(map[string][]bakery.Op, mapSize) for key, value := range faraday.RequiredPermissions { result[key] = value @@ -64,30 +101,54 @@ func getSubserverPermissions() map[string][]bakery.Op { // getAllMethodPermissions returns a merged map of lnd's and all subservers' // method macaroon permissions. -func getAllMethodPermissions() map[string][]bakery.Op { - subserverPermissions := getSubserverPermissions() - lndPermissions := lnd.MainRPCServerPermissions() - mapSize := len(subserverPermissions) + len(lndPermissions) + - len(whiteListedMethods) +func getAllMethodPermissions( + lndBuildTags map[string]bool) map[string][]bakery.Op { + + litsubServerPerms := getSubserverPermissions() + lndPerms := lnd.MainRPCServerPermissions() + + mapSize := len(litsubServerPerms) + len(lndSubServerPerms) + + len(lndPerms) + len(whiteListedMethods) + result := make(map[string][]bakery.Op, mapSize) - for key, value := range lndPermissions { + + for key, value := range lndPerms { result[key] = value } - for key, value := range subserverPermissions { + + for key, value := range whiteListedMethods { result[key] = value } - for key, value := range whiteListedMethods { + + for key, value := range litsubServerPerms { result[key] = value } + + for subServerName, perms := range lndSubServerPerms { + name := subServerName + if tagName, ok := lndSubServerNameToTag[name]; ok { + name = tagName + } + + if !lndBuildTags[strings.ToLower(name)] { + continue + } + + for key, value := range perms { + result[key] = value + } + } + return result } // GetAllPermissions retrieves all the permissions needed to bake a super // macaroon. -func GetAllPermissions(readOnly bool) []bakery.Op { - dedupMap := make(map[string]map[string]bool) +func GetAllPermissions(readOnly bool, + lndBuildTags map[string]bool) []bakery.Op { - for _, methodPerms := range getAllMethodPermissions() { + dedupMap := make(map[string]map[string]bool) + for _, methodPerms := range getAllMethodPermissions(lndBuildTags) { for _, methodPerm := range methodPerms { if methodPerm.Action == "" || methodPerm.Entity == "" { continue @@ -121,8 +182,9 @@ func GetAllPermissions(readOnly bool) []bakery.Op { // isLndURI returns true if the given URI belongs to an RPC of lnd. func isLndURI(uri string) bool { - _, ok := lnd.MainRPCServerPermissions()[uri] - return ok + _, lndSubServerCall := lndSubServerPerms[uri] + _, lndCall := lnd.MainRPCServerPermissions()[uri] + return lndCall || lndSubServerCall } // isLoopURI returns true if the given URI belongs to an RPC of loopd. @@ -148,3 +210,83 @@ func isLitURI(uri string) bool { _, ok := litPermissions[uri] return ok } + +func init() { + ss := lnrpc.RegisteredSubServers() + for _, subServer := range ss { + _, perms, err := subServer.NewGrpcHandler().CreateSubServer( + &mockConfig{}, + ) + if err != nil { + panic(err) + } + + name := subServer.SubServerName + lndSubServerPerms[name] = make(map[string][]bakery.Op) + for key, value := range perms { + lndSubServerPerms[name][key] = value + } + } +} + +// mockConfig implements lnrpc.SubServerConfigDispatcher. It provides th +// functionality required so that the lnrpc.GrpcHandler.CreateSubServer +// function can be called without panicking. +type mockConfig struct{} + +var _ lnrpc.SubServerConfigDispatcher = (*mockConfig)(nil) + +// FetchConfig is a mock implementation of lnrpc.SubServerConfigDispatcher. It +// is used as a parameter to lnrpc.GrpcHandler.CreateSubServer and allows the +// function to be called without panicking. This is useful because +// CreateSubServer can be used to extract the permissions required by each +// registered subserver. +// +// TODO(elle): remove this once the sub-server permission lists in LND have been +// exported +func (t *mockConfig) FetchConfig(subServerName string) (interface{}, bool) { + switch subServerName { + case "InvoicesRPC": + return &invoicesrpc.Config{}, true + case "WatchtowerClientRPC": + return &wtclientrpc.Config{ + Resolver: func(_, _ string) (*net.TCPAddr, error) { + return nil, nil + }, + }, true + case "AutopilotRPC": + return &autopilotrpc.Config{ + Manager: &autopilot.Manager{}, + }, true + case "ChainRPC": + return &chainrpc.Config{ + ChainNotifier: &chainreg.NoChainBackend{}, + }, true + case "DevRPC": + return &devrpc.Config{}, true + case "NeutrinoKitRPC": + return &neutrinorpc.Config{}, true + case "PeersRPC": + return &peersrpc.Config{}, true + case "RouterRPC": + return &routerrpc.Config{ + Router: &routing.ChannelRouter{}, + }, true + case "SignRPC": + return &signrpc.Config{ + Signer: &mock.DummySigner{}, + }, true + case "WalletKitRPC": + return &walletrpc.Config{ + FeeEstimator: &chainreg.NoChainBackend{}, + Wallet: &mock.WalletController{}, + KeyRing: &mock.SecretKeyRing{}, + Sweeper: &sweep.UtxoSweeper{}, + Chain: &mock.ChainIO{}, + }, true + case "WatchtowerRPC": + return &watchtowerrpc.Config{}, true + default: + return nil, false + } +} diff --git a/terminal.go b/terminal.go index 710266db5..8d93ceb16 100644 --- a/terminal.go +++ b/terminal.go @@ -145,6 +145,8 @@ type LightningTerminal struct { lndClient *lndclient.GrpcLndServices basicClient lnrpc.LightningClient + lndBuildTags map[string]bool + faradayServer *frdrpcserver.RPCServer faradayStarted bool @@ -170,7 +172,8 @@ type LightningTerminal struct { // New creates a new instance of the lightning-terminal daemon. func New() *LightningTerminal { return &LightningTerminal{ - lndErrChan: make(chan error, 1), + lndErrChan: make(chan error, 1), + lndBuildTags: make(map[string]bool), } } @@ -200,8 +203,10 @@ func (g *LightningTerminal) Run() error { g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) g.rpcProxy = newRpcProxy( - g.cfg, g, g.validateSuperMacaroon, getAllMethodPermissions(), - bufRpcListener, + g.cfg, g, g.validateSuperMacaroon, + func() map[string][]bakery.Op { + return getAllMethodPermissions(g.lndBuildTags) + }, bufRpcListener, ) g.sessionRpcServer, err = newSessionRPCServer(&sessionRpcServerConfig{ basicAuth: g.rpcProxy.basicAuth, @@ -233,6 +238,9 @@ func (g *LightningTerminal) Run() error { ) }, firstConnectionDeadline: g.cfg.FirstLNCConnDeadline, + getAllPermissions: func(readOnly bool) []bakery.Op { + return GetAllPermissions(readOnly, g.lndBuildTags) + }, }) if err != nil { return fmt.Errorf("could not create new session rpc "+ @@ -486,6 +494,16 @@ func (g *LightningTerminal) startSubservers() error { return err } + // Collect the tags that LND was built with. + version, err := g.lndClient.Versioner.GetVersion(ctxc) + if err != nil { + return err + } + + for _, tag := range version.BuildTags { + g.lndBuildTags[strings.ToLower(tag)] = true + } + // In the integrated mode, we received an admin macaroon once lnd was // ready. We can now bake a "super macaroon" that contains all // permissions of all daemons that we can use for any internal calls. @@ -497,7 +515,7 @@ func (g *LightningTerminal) startSubservers() error { ctx, g.basicClient, session.NewSuperMacaroonRootKeyID( [4]byte{}, ), - GetAllPermissions(false), nil, + GetAllPermissions(false, g.lndBuildTags), nil, ) if err != nil { return err