diff --git a/config.go b/config.go index b79f52995..f2eb16ed3 100644 --- a/config.go +++ b/config.go @@ -177,6 +177,11 @@ type Config struct { faradayRemote bool loopRemote bool poolRemote bool + + // lndAdminMacaroon is the admin macaroon that is given to us by lnd + // over an in-memory connection on startup. This is only set in + // integrated lnd mode. + lndAdminMacaroon []byte } // RemoteConfig holds the configuration parameters that are needed when running @@ -219,7 +224,7 @@ type RemoteDaemonConfig struct { // lndConnectParams returns the connection parameters to connect to the local // lnd instance. func (c *Config) lndConnectParams() (string, lndclient.Network, string, - string) { + string, []byte) { // In remote lnd mode, we just pass along what was configured in the // remote section of the lnd config. @@ -227,7 +232,8 @@ func (c *Config) lndConnectParams() (string, lndclient.Network, string, return c.Remote.Lnd.RPCServer, lndclient.Network(c.Network), lncfg.CleanAndExpandPath(c.Remote.Lnd.TLSCertPath), - lncfg.CleanAndExpandPath(c.Remote.Lnd.MacaroonPath) + lncfg.CleanAndExpandPath(c.Remote.Lnd.MacaroonPath), + nil } // When we start lnd internally, we take the listen address as @@ -248,8 +254,8 @@ func (c *Config) lndConnectParams() (string, lndclient.Network, string, ) } - return lndDialAddr, lndclient.Network(c.Network), - c.Lnd.TLSCertPath, c.Lnd.AdminMacPath + return lndDialAddr, lndclient.Network(c.Network), "", "", + c.lndAdminMacaroon } // defaultConfig returns a configuration struct with all default values set. diff --git a/rpc_proxy.go b/rpc_proxy.go index 407fcdfb1..08767aa6d 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -2,10 +2,12 @@ package terminal import ( "context" + "crypto/tls" "encoding/base64" "encoding/hex" "fmt" "io/ioutil" + "net" "net/http" "strings" "time" @@ -18,6 +20,7 @@ import ( "google.golang.org/grpc/backoff" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/test/bufconn" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon.v2" ) @@ -34,7 +37,8 @@ const ( // or REST request and delegate (and convert if necessary) it to the correct // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, - permissionMap map[string][]bakery.Op) *rpcProxy { + permissionMap map[string][]bakery.Op, + bufListener *bufconn.Listener) *rpcProxy { // The gRPC web calls are protected by HTTP basic auth which is defined // by base64(username:password). Because we only have a password, we @@ -52,6 +56,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, cfg: cfg, basicAuth: basicAuth, macValidator: validator, + bufListener: bufListener, } p.grpcServer = grpc.NewServer( // From the grpxProxy doc: This codec is *crucial* to the @@ -125,6 +130,9 @@ type rpcProxy struct { basicAuth string macValidator macaroons.MacaroonValidator + bufListener *bufconn.Listener + + superMacaroon string lndConn *grpc.ClientConn faradayConn *grpc.ClientConn @@ -140,8 +148,14 @@ func (p *rpcProxy) Start() error { var err error // Setup the connection to lnd. - host, _, tlsPath, _ := p.cfg.lndConnectParams() - p.lndConn, err = dialBackend("lnd", host, tlsPath) + host, _, tlsPath, _, _ := p.cfg.lndConnectParams() + + // We use a bufconn to connect to lnd in integrated mode. + if p.cfg.LndMode == ModeIntegrated { + p.lndConn, err = dialBufConnBackend(p.bufListener) + } else { + p.lndConn, err = dialBackend("lnd", host, tlsPath) + } if err != nil { return fmt.Errorf("could not dial lnd: %v", err) } @@ -390,10 +404,13 @@ func (p *rpcProxy) basicAuthToMacaroon(ctx context.Context, return ctx, nil } - var macPath string + var ( + macPath string + macData []byte + ) switch { case isLndURI(requestURI): - _, _, _, macPath = p.cfg.lndConnectParams() + _, _, _, macPath, macData = p.cfg.lndConnectParams() case isFaradayURI(requestURI): if p.cfg.faradayRemote { @@ -421,16 +438,64 @@ func (p *rpcProxy) basicAuthToMacaroon(ctx context.Context, requestURI) } - // Now that we know which macaroon to load, do it and attach it to the - // request context. - macBytes, err := readMacaroon(lncfg.CleanAndExpandPath(macPath)) - if err != nil { - return ctx, fmt.Errorf("error reading macaroon: %v", err) + switch { + // If we have a super macaroon, we can use that one directly since it + // will contain all permissions we need. + case len(p.superMacaroon) > 0: + md.Set(HeaderMacaroon, p.superMacaroon) + + // If we have macaroon data directly, just encode them. This could be + // for initial requests to lnd while we don't have the super macaroon + // just yet (or are actually calling to bake the super macaroon). + case len(macData) > 0: + md.Set(HeaderMacaroon, hex.EncodeToString(macData)) + + // The fall back is to read the macaroon from a path. This is in remote + // mode. + case len(macPath) > 0: + // Now that we know which macaroon to load, do it and attach it + // to the request context. + macBytes, err := readMacaroon(lncfg.CleanAndExpandPath(macPath)) + if err != nil { + return ctx, fmt.Errorf("error reading macaroon %s: %v", + lncfg.CleanAndExpandPath(macPath), err) + } + + md.Set(HeaderMacaroon, hex.EncodeToString(macBytes)) } - md.Set(HeaderMacaroon, hex.EncodeToString(macBytes)) + return metadata.NewIncomingContext(ctx, md), nil } +// dialBufConnBackend dials an in-memory connection to an RPC listener and +// ignores any TLS certificate mismatches. +func dialBufConnBackend(listener *bufconn.Listener) (*grpc.ClientConn, error) { + tlsConfig := credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, + }) + conn, err := grpc.Dial( + "", + grpc.WithContextDialer( + func(context.Context, string) (net.Conn, error) { + return listener.Dial() + }, + ), + grpc.WithTransportCredentials(tlsConfig), + + // From the grpcProxy doc: This codec is *crucial* to the + // functioning of the proxy. + grpc.WithCodec(grpcProxy.Codec()), // nolint + grpc.WithTransportCredentials(tlsConfig), + grpc.WithDefaultCallOptions(maxMsgRecvSize), + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.DefaultConfig, + MinConnectTimeout: defaultConnectTimeout, + }), + ) + + return conn, err +} + // dialBackend connects to a gRPC backend through the given address and uses the // given TLS certificate to authenticate the connection. func dialBackend(name, dialAddr, tlsCertPath string) (*grpc.ClientConn, error) { @@ -470,12 +535,12 @@ func readMacaroon(macPath string) ([]byte, error) { // Load the specified macaroon file. macBytes, err := ioutil.ReadFile(macPath) if err != nil { - return nil, fmt.Errorf("unable to read macaroon path : %v", err) + return nil, fmt.Errorf("unable to read macaroon path: %v", err) } // Make sure it actually is a macaroon by parsing it. mac := &macaroon.Macaroon{} - if err = mac.UnmarshalBinary(macBytes); err != nil { + if err := mac.UnmarshalBinary(macBytes); err != nil { return nil, fmt.Errorf("unable to decode macaroon: %v", err) } diff --git a/subserver_permissions.go b/subserver_permissions.go index f100fc9cd..f810f76fc 100644 --- a/subserver_permissions.go +++ b/subserver_permissions.go @@ -26,9 +26,9 @@ func getSubserverPermissions() map[string][]bakery.Op { return result } -// getAllPermissions returns a merged map of lnd's and all subservers' macaroon -// permissions. -func getAllPermissions() 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) @@ -42,6 +42,35 @@ func getAllPermissions() map[string][]bakery.Op { return result } +// getAllPermissions retrieves all the permissions needed to bake a super +// macaroon. +func getAllPermissions() []bakery.Op { + dedupMap := make(map[string]map[string]bool) + + for _, methodPerms := range getAllMethodPermissions() { + for _, methodPerm := range methodPerms { + if dedupMap[methodPerm.Entity] == nil { + dedupMap[methodPerm.Entity] = make( + map[string]bool, + ) + } + dedupMap[methodPerm.Entity][methodPerm.Action] = true + } + } + + result := make([]bakery.Op, 0, len(dedupMap)) + for entity, actions := range dedupMap { + for action := range actions { + result = append(result, bakery.Op{ + Entity: entity, + Action: action, + }) + } + } + + return result +} + // isLndURI returns true if the given URI belongs to an RPC of lnd. func isLndURI(uri string) bool { _, ok := lnd.MainRPCServerPermissions()[uri] diff --git a/terminal.go b/terminal.go index f9d02fdec..c6d58baa3 100644 --- a/terminal.go +++ b/terminal.go @@ -4,8 +4,12 @@ import ( "context" "crypto/tls" "embed" + "encoding/hex" "errors" "fmt" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" + "github.com/lightningnetwork/lnd/rpcperms" "io/fs" "net" "net/http" @@ -39,9 +43,11 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/signal" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/encoding/protojson" "gopkg.in/macaroon-bakery.v2/bakery" ) @@ -124,6 +130,10 @@ type LightningTerminal struct { defaultImplCfg *lnd.ImplementationCfg + // lndInterceptorChain is a reference to lnd's interceptor chain that + // guards all incoming calls. This is only set in integrated mode! + lndInterceptorChain *rpcperms.InterceptorChain + wg sync.WaitGroup lndErrChan chan error @@ -170,10 +180,13 @@ func (g *LightningTerminal) Run() error { // Create the instances of our subservers now so we can hook them up to // lnd once it's fully started. + bufRpcListener := bufconn.Listen(100) g.faradayServer = frdrpc.NewRPCServer(g.cfg.faradayRpcConfig) g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) - g.rpcProxy = newRpcProxy(g.cfg, g, getAllPermissions()) + g.rpcProxy = newRpcProxy( + g.cfg, g, getAllMethodPermissions(), bufRpcListener, + ) // Overwrite the loop and pool daemon's user agent name so it sends // "litd" instead of "loopd" and "poold" respectively. @@ -183,7 +196,10 @@ func (g *LightningTerminal) Run() error { // Call the "real" main in a nested manner so the defers will properly // be executed in the case of a graceful shutdown. readyChan := make(chan struct{}) + bufReadyChan := make(chan struct{}) unlockChan := make(chan struct{}) + macChan := make(chan []byte, 1) + if g.cfg.LndMode == ModeIntegrated { lisCfg := lnd.ListenerCfg{ RPCListeners: []*lnd.ListenerWithSignal{{ @@ -191,6 +207,10 @@ func (g *LightningTerminal) Run() error { addr: g.cfg.Lnd.RPCListeners[0], }, Ready: readyChan, + }, { + Listener: bufRpcListener, + Ready: bufReadyChan, + MacChan: macChan, }}, } @@ -199,7 +219,7 @@ func (g *LightningTerminal) Run() error { RestRegistrar: g, ExternalValidator: g, DatabaseBuilder: g.defaultImplCfg.DatabaseBuilder, - WalletConfigBuilder: g.defaultImplCfg.WalletConfigBuilder, + WalletConfigBuilder: g, ChainControlBuilder: g.defaultImplCfg.ChainControlBuilder, } @@ -223,6 +243,7 @@ func (g *LightningTerminal) Run() error { } else { close(unlockChan) close(readyChan) + close(bufReadyChan) _ = g.RegisterGrpcSubserver(g.rpcProxy.grpcServer) } @@ -291,6 +312,13 @@ func (g *LightningTerminal) Run() error { return errors.New("shutting down") } + // If we're in integrated mode, we'll need to wait for lnd to send the + // macaroon after unlock before going any further. + if g.cfg.LndMode == ModeIntegrated { + <-bufReadyChan + g.cfg.lndAdminMacaroon = <-macChan + } + err = g.startSubservers() if err != nil { log.Errorf("Could not start subservers: %v", err) @@ -323,8 +351,28 @@ func (g *LightningTerminal) Run() error { // embedded daemons as external subservers that hook into the same gRPC and REST // servers that lnd started. func (g *LightningTerminal) startSubservers() error { - var basicClient lnrpc.LightningClient - host, network, tlsPath, macPath := g.cfg.lndConnectParams() + var ( + basicClient lnrpc.LightningClient + insecure bool + clientOptions []lndclient.BasicClientOption + ) + + host, network, tlsPath, macPath, macData := g.cfg.lndConnectParams() + clientOptions = append(clientOptions, lndclient.MacaroonData( + hex.EncodeToString(macData), + )) + clientOptions = append( + clientOptions, lndclient.MacFilename(path.Base(macPath)), + ) + + // If we're in integrated mode, we can retrieve the macaroon string + // from lnd directly, rather than grabbing it from disk. + if g.cfg.LndMode == ModeIntegrated { + // Set to true in integrated mode, since we will not require tls + // when communicating with lnd via a bufconn. + insecure = true + clientOptions = append(clientOptions, lndclient.Insecure()) + } // The main RPC listener of lnd might need some time to start, it could // be that we run into a connection refused a few times. We use the @@ -338,7 +386,7 @@ func (g *LightningTerminal) startSubservers() error { var err error basicClient, err = lndclient.NewBasicClient( host, tlsPath, path.Dir(macPath), string(network), - lndclient.MacFilename(path.Base(macPath)), + clientOptions..., ) return err }, defaultStartupTimeout) @@ -375,7 +423,9 @@ func (g *LightningTerminal) startSubservers() error { LndAddress: host, Network: network, TLSPath: tlsPath, + Insecure: insecure, CustomMacaroonPath: macPath, + CustomMacaroonHex: hex.EncodeToString(macData), BlockUntilChainSynced: true, BlockUntilUnlocked: true, CallerCtx: ctxc, @@ -386,11 +436,55 @@ func (g *LightningTerminal) startSubservers() error { return err } + // 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. + if g.cfg.LndMode == ModeIntegrated { + // Create a super macaroon that can be used to control lnd, + // faraday, loop, and pool, all at the same time. + bakePerms := getAllPermissions() + req := &lnrpc.BakeMacaroonRequest{ + Permissions: make( + []*lnrpc.MacaroonPermission, len(bakePerms), + ), + AllowExternalPermissions: true, + } + for idx, perm := range bakePerms { + req.Permissions[idx] = &lnrpc.MacaroonPermission{ + Entity: perm.Entity, + Action: perm.Action, + } + } + + ctx := context.Background() + res, err := basicClient.BakeMacaroon(ctx, req) + if err != nil { + return err + } + + g.rpcProxy.superMacaroon = res.Macaroon + } + + // If we're in integrated and stateless init mode, we won't create + // macaroon files in any of the subserver daemons. + createDefaultMacaroons := true + if g.cfg.LndMode == ModeIntegrated && g.lndInterceptorChain != nil && + g.lndInterceptorChain.MacaroonService() != nil { + + // If the wallet was initialized in stateless mode, we don't + // want any macaroons lying around on the filesystem. In that + // case only the UI will be able to access any of the integrated + // daemons. In all other cases we want default macaroons so we + // can use the CLI tools to interact with loop/pool/faraday. + macService := g.lndInterceptorChain.MacaroonService() + createDefaultMacaroons = !macService.StatelessInit + } + // Both connection types are ready now, let's start our subservers if // they should be started locally as an integrated service. if !g.cfg.faradayRemote { err = g.faradayServer.StartAsSubserver( - g.lndClient.LndServices, true, + g.lndClient.LndServices, createDefaultMacaroons, ) if err != nil { return err @@ -399,7 +493,9 @@ func (g *LightningTerminal) startSubservers() error { } if !g.cfg.loopRemote { - err = g.loopServer.StartAsSubserver(g.lndClient, true) + err = g.loopServer.StartAsSubserver( + g.lndClient, createDefaultMacaroons, + ) if err != nil { return err } @@ -408,7 +504,7 @@ func (g *LightningTerminal) startSubservers() error { if !g.cfg.poolRemote { err = g.poolServer.StartAsSubserver( - basicClient, g.lndClient, true, + basicClient, g.lndClient, createDefaultMacaroons, ) if err != nil { return err @@ -494,6 +590,51 @@ func (g *LightningTerminal) RegisterRestSubserver(ctx context.Context, func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, requiredPermissions []bakery.Op, fullMethod string) error { + macHex, err := macaroons.RawMacaroonFromContext(ctx) + if err != nil { + return err + } + + // If we're in integrated mode, we're using a super macaroon internally, + // which we can just pass straight to lnd for validation. But the user + // might still be using a specific macaroon, which should be handled the + // same as before. + isSuperMacaroon := macHex == g.rpcProxy.superMacaroon + if g.cfg.LndMode == ModeIntegrated && isSuperMacaroon { + macBytes, err := hex.DecodeString(macHex) + if err != nil { + return err + } + + // If we haven't connected to lnd yet, we can't check the super + // macaroon. The user will need to wait a bit. + if g.lndClient == nil { + return fmt.Errorf("cannot validate macaroon, not yet " + + "connected to lnd, please wait") + } + + // Convert permissions to the form that lndClient will accept. + permissions := make( + []lndclient.MacaroonPermission, len(requiredPermissions), + ) + for idx, perm := range requiredPermissions { + permissions[idx] = lndclient.MacaroonPermission{ + Entity: perm.Entity, + Action: perm.Action, + } + } + + res, err := g.lndClient.Client.CheckMacaroonPermissions( + ctx, macBytes, permissions, fullMethod, + ) + if !res { + return fmt.Errorf("macaroon is not valid, returned %v", + res) + } + + return err + } + // Validate all macaroons for services that are running in the local // process. Calls that we proxy to a remote host don't need to be // checked as they'll have their own interceptor. @@ -566,6 +707,25 @@ func (g *LightningTerminal) Permissions() map[string][]bakery.Op { return getSubserverPermissions() } +// BuildWalletConfig is responsible for creating or unlocking and then +// fully initializing a wallet. +// +// NOTE: This is only implemented in order for us to intercept the setup call +// and store a reference to the interceptor chain. +// +// NOTE: This is part of the lnd.WalletConfigBuilder interface. +func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, + dbs *lnd.DatabaseInstances, interceptorChain *rpcperms.InterceptorChain, + grpcListeners []*lnd.ListenerWithSignal) (*chainreg.PartialChainControl, + *btcwallet.Config, func(), error) { + + g.lndInterceptorChain = interceptorChain + + return g.defaultImplCfg.WalletConfigBuilder.BuildWalletConfig( + ctx, dbs, interceptorChain, grpcListeners, + ) +} + // shutdown stops all subservers that were started and attached to lnd. func (g *LightningTerminal) shutdown() error { var returnErr error @@ -954,7 +1114,7 @@ func (g *LightningTerminal) showStartupInfo() error { if g.cfg.LndMode == ModeRemote { // We try to query GetInfo on the remote node to find out the // alias. But the wallet might be locked. - host, network, tlsPath, macPath := g.cfg.lndConnectParams() + host, network, tlsPath, macPath, _ := g.cfg.lndConnectParams() basicClient, err := lndclient.NewBasicClient( host, tlsPath, path.Dir(macPath), string(network), lndclient.MacFilename(path.Base(macPath)),