lightning-terminal/terminal.go
Viktor Tigerström 7bba957c8b
multi: add dev kvdb to sql code migration
Add the necessary code to trigger the kvdb to sql code migration in dev
builds.
2026-02-18 12:07:15 +01:00

2138 lines
65 KiB
Go

package terminal
import (
"context"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io/fs"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
restProxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/jessevdk/go-flags"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/autopilotserver"
"github.com/lightninglabs/lightning-terminal/firewall"
"github.com/lightninglabs/lightning-terminal/firewalldb"
"github.com/lightninglabs/lightning-terminal/litrpc"
litmac "github.com/lightninglabs/lightning-terminal/macaroons"
"github.com/lightninglabs/lightning-terminal/perms"
"github.com/lightninglabs/lightning-terminal/queue"
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
"github.com/lightninglabs/lightning-terminal/rules"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightninglabs/lightning-terminal/status"
"github.com/lightninglabs/lightning-terminal/subservers"
"github.com/lightninglabs/lndclient"
taprootassets "github.com/lightninglabs/taproot-assets"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/msgmux"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sweep"
grpcProxy "github.com/mwitkow/grpc-proxy/proxy"
"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"
"gopkg.in/macaroon.v2"
)
const (
MainnetServer = "autopilot.lightning.finance:12010"
TestnetServer = "test.autopilot.lightning.finance:12010"
// lndWalletReadyStatus is a custom status that will be used with the
// LND subserver. If the subserver is in this state then it will allow
// certain wallet calls through while denying other calls that require
// LND to be fully started.
lndWalletReadyStatus = "Wallet Ready"
defaultServerTimeout = 10 * time.Second
defaultConnectTimeout = 15 * time.Second
defaultRPCTimeout = 3 * time.Minute
minimumRPCTimeout = 30 * time.Second
defaultStartupTimeout = 5 * time.Second
)
// restRegistration is a function type that represents a REST proxy
// registration.
type restRegistration func(context.Context, *restProxy.ServeMux, string,
[]grpc.DialOption) error
var (
// maxMsgRecvSize is the largest message our REST proxy will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
// macDatabaseOpenTimeout is how long we wait for acquiring the lock on
// the macaroon database before we give up with an error.
macDatabaseOpenTimeout = time.Second * 5
// appFilesDir is the sub directory of the above build directory which
// we pass to the HTTP server.
appFilesDir = "app/build"
// appFilesPrefix is the path prefix the static assets of the UI are
// exposed under. This variable can be overwritten during build time if
// a different deployment path should be used.
appFilesPrefix = ""
// patternRESTRequest is the regular expression that matches all REST
// URIs that are currently used by lnd, faraday, loop and pool.
patternRESTRequest = regexp.MustCompile(`^/v\d/.*`)
// lndRESTRegistrations is the list of all lnd REST handler registration
// functions we want to call when creating our REST proxy. We include
// all lnd subserver packages here, even though some might not be active
// in a remote lnd node. That will result in an "UNIMPLEMENTED" error
// instead of a 404 which should be an okay tradeoff vs. connecting
// first and querying all enabled subservers to dynamically populate
// this list.
lndRESTRegistrations = []restRegistration{
lnrpc.RegisterLightningHandlerFromEndpoint,
lnrpc.RegisterWalletUnlockerHandlerFromEndpoint,
lnrpc.RegisterStateHandlerFromEndpoint,
autopilotrpc.RegisterAutopilotHandlerFromEndpoint,
chainrpc.RegisterChainNotifierHandlerFromEndpoint,
invoicesrpc.RegisterInvoicesHandlerFromEndpoint,
routerrpc.RegisterRouterHandlerFromEndpoint,
signrpc.RegisterSignerHandlerFromEndpoint,
verrpc.RegisterVersionerHandlerFromEndpoint,
walletrpc.RegisterWalletKitHandlerFromEndpoint,
watchtowerrpc.RegisterWatchtowerHandlerFromEndpoint,
wtclientrpc.RegisterWatchtowerClientHandlerFromEndpoint,
}
// minimalCompatibleVersion is the minimal lnd version that is required
// to run LiT in remote mode.
minimalCompatibleVersion = &verrpc.Version{
AppMajor: 0,
AppMinor: 19,
AppPatch: 0,
BuildTags: []string{
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
},
}
// walletUnlockerServiceMethods defines methods of the wallet unlocker
// service that we don't require macaroons to access. We also allow
// these methods to be called even if lnd is not yet fully marked as
// started up (because it cannot start if it's still locked or no wallet
// exists).
walletUnlockerServiceMethods = map[string]struct{}{
"/lnrpc.WalletUnlocker/GenSeed": {},
"/lnrpc.WalletUnlocker/InitWallet": {},
"/lnrpc.WalletUnlocker/UnlockWallet": {},
"/lnrpc.WalletUnlocker/ChangePassword": {},
}
// stateServiceMethods defines status methods that we don't require
// macaroons to access.
stateServiceMethods = map[string]struct{}{
// The State service must be available at all times, even
// before we can check macaroons, so we whitelist it.
"/lnrpc.State/SubscribeState": {},
"/lnrpc.State/GetState": {},
}
)
// LightningTerminal is the main grand unified binary instance. Its task is to
// start an lnd node then start and register external subservers to it.
type LightningTerminal struct {
cfg *Config
defaultImplCfg *lnd.ImplementationCfg
permsMgr *perms.Manager
// 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
errQueue *queue.ConcurrentQueue[error]
lndConnID string
lndConn *grpc.ClientConn
lndClient *lndclient.GrpcLndServices
// basicClient may be accessed by other sub-systems but this access
// should be provided via the basicLNDClient method.
basicClient lnrpc.LightningClient
basicClientSet atomic.Bool
subServerMgr *subservers.Manager
statusMgr *status.Manager
autopilotClient autopilotserver.Autopilot
ruleMgrs rules.ManagerSet
rpcProxy *rpcProxy
httpServer *http.Server
sessionRpcServer *sessionRpcServer
sessionRpcServerStarted bool
macaroonService *lndclient.MacaroonService
macaroonServiceStarted bool
macaroonDB kvdb.Backend
middleware *mid.Manager
middlewareStarted bool
accountService *accounts.InterceptorService
accountServiceStarted bool
accountRpcServer *accounts.RPCServer
stores *stores
restHandler http.Handler
restCancel func()
}
// New creates a new instance of the lightning-terminal daemon.
func New() *LightningTerminal {
return &LightningTerminal{
statusMgr: status.NewStatusManager(),
}
}
// stores holds a collection of the DB stores that are used by LiT.
type stores struct {
accounts accounts.Store
sessions session.Store
firewall *firewalldb.DB
// closeFns holds various callbacks that can be used to close any open
// stores in the stores struct.
closeFns map[string]func() error
}
// close closes all open stores in the stores struct. It returns an error if
// any of the stores could not be closed.
func (s *stores) close() error {
var returnErr error
for storeName, closeFn := range s.closeFns {
err := closeFn()
if err != nil {
log.Errorf("error closing %s store: %v",
storeName, err)
returnErr = err
}
}
return returnErr
}
// Run starts everything and then blocks until either the application is shut
// down or a critical error happens.
func (g *LightningTerminal) Run(ctx context.Context) error {
// Hook interceptor for os signals.
shutdownInterceptor, err := signal.Intercept()
if err != nil {
return fmt.Errorf("could not intercept signals: %v", err)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Make sure the context is canceled if the user requests shutdown and
// that the shutdown signal is requested if the context is canceled.
go func() {
select {
// Client requests shutdown, cancel the wait.
case <-shutdownInterceptor.ShutdownChannel():
cancel()
// The check was completed and the above defer canceled the
// context. We can just exit the goroutine, nothing more to do.
case <-ctx.Done():
shutdownInterceptor.RequestShutdown()
}
}()
cfg, err := loadAndValidateConfig(shutdownInterceptor)
if err != nil {
return fmt.Errorf("could not load config: %w", err)
}
g.cfg = cfg
g.defaultImplCfg = g.cfg.Lnd.ImplementationConfig(shutdownInterceptor)
// Show version at startup.
log.Infof("LiT version: %s", RichVersion())
// This concurrent error queue can be used by every component that can
// raise runtime errors. Using a queue will prevent us from blocking on
// sending errors to it, as long as the queue is running.
g.errQueue = queue.NewConcurrentQueue[error](queue.DefaultQueueSize)
g.errQueue.Start()
defer g.errQueue.Stop()
// Construct a new Manager.
g.permsMgr, err = perms.NewManager(false)
if err != nil {
return fmt.Errorf("could not create permissions manager: %w",
err)
}
// The litcli status command will call the "/lnrpc.State/GetState" RPC.
// As the status command is available to the user before the macaroons
// have been loaded/created, and before the lnd clients have been
// set up, we need to override the isReady check for this specific
// URI as soon as LND can accept the call, i.e. when the lnd sub-server
// is in the "Wallet Ready" state. The same goes for the streaming
// variant of the status RPC and any calls to the wallet unlocker
// service.
lndOverride := func(uri, manualStatus string) (bool, bool) {
_, isWalletUnlockerService := walletUnlockerServiceMethods[uri]
_, isStatusService := stateServiceMethods[uri]
// If this is a call to the wallet unlocker or status subserver,
// we return true for ready if we've set up everything for lnd,
// and it is just waiting to be unlocked.
if isWalletUnlockerService || isStatusService {
return manualStatus == lndWalletReadyStatus, true
}
return false, false
}
// Register LND, LiT and Accounts with the status manager.
err = g.statusMgr.RegisterAndEnableSubServer(
subservers.LND, status.WithIsReadyOverride(lndOverride),
)
if err != nil {
return err
}
err = g.statusMgr.RegisterAndEnableSubServer(subservers.LIT)
if err != nil {
return err
}
err = g.statusMgr.RegisterSubServer(subservers.ACCOUNTS)
if err != nil {
return err
}
// Also enable the accounts subserver if it's not disabled.
if !g.cfg.Accounts.Disable {
g.statusMgr.SetEnabled(subservers.ACCOUNTS)
}
// Create the instances of our subservers now so we can hook them up to
// lnd once it's fully started.
g.subServerMgr = subservers.NewManager(g.permsMgr, g.statusMgr)
// Register our sub-servers. This must be done before the REST proxy is
// set up so that the correct REST handlers are registered.
err = g.initSubServers()
if err != nil {
return fmt.Errorf("could not initialise sub-servers: %w", err)
}
// Construct the rpcProxy. It must be initialised before the main web
// server is started.
g.rpcProxy = newRpcProxy(
g.cfg, g, g.validateSuperMacaroon, g.permsMgr, g.subServerMgr,
g.statusMgr, g.basicLNDClient,
)
// Register any gRPC services that should be served using LiT's
// gRPC server regardless of the LND mode being used.
litrpc.RegisterProxyServer(g.rpcProxy.grpcServer, g.rpcProxy)
litrpc.RegisterStatusServer(g.rpcProxy.grpcServer, g.statusMgr)
// Start the main web server that dispatches requests either to the
// static UI file server or the RPC proxy. This makes it possible to
// unlock lnd through the UI.
if err := g.startMainWebServer(); err != nil {
return fmt.Errorf("error starting main proxy HTTP server: %v",
err)
}
// We'll also create a REST proxy that'll convert any REST calls to gRPC
// calls and forward them to the internal listener.
if g.cfg.EnableREST {
if err := g.createRESTProxy(ctx); err != nil {
return fmt.Errorf("error creating REST proxy: %v", err)
}
}
// Attempt to start Lit and all of its sub-servers. If an error is
// returned, it means that either one of Lit's internal sub-servers
// could not start or LND could not start or be connected to.
startErr := g.start(ctx)
if startErr != nil {
g.statusMgr.SetErrored(
subservers.LIT, "could not start Lit: %v", startErr,
)
}
// Now block until we receive an error or the main shutdown
// signal.
<-shutdownInterceptor.ShutdownChannel()
log.Infof("Shutdown signal received")
err = g.shutdownSubServers()
if err != nil {
log.Errorf("Error shutting down: %v", err)
}
g.wg.Wait()
return startErr
}
// start attempts to start all the various components of Litd. Only Litd and
// LND errors are considered fatal and will result in an error being returned.
// If any of the sub-servers managed by the subServerMgr error while starting
// up, these are considered non-fatal and will not result in an error being
// returned.
func (g *LightningTerminal) start(ctx context.Context) error {
var err error
accountServiceErrCallback := func(err error) {
g.statusMgr.SetErrored(subservers.ACCOUNTS, err.Error())
log.Errorf("Error thrown in the accounts service, keeping "+
"litd running: %v", err,
)
}
networkDir := filepath.Join(g.cfg.LitDir, g.cfg.Network)
err = makeDirectories(networkDir)
if err != nil {
return fmt.Errorf("could not create network directory: %v", err)
}
// We create a reference to the `accountRpcServer` here before starting
// it and prior to setting up the LND connection. This is because when
// the LND connection is set up for an integrated LND instance, LND will
// call litd's `RegisterGrpcSubserver` function during the setup of the
// connection.
// That function calls `registerSubDaemonGrpcServers` which requires
// that the `accountRpcServer` pointer exist, to not nil pointer panic
// when requests get passed to the server.
g.accountRpcServer = accounts.NewRPCServer()
g.ruleMgrs = rules.NewRuleManagerSet()
if !g.cfg.Autopilot.Disable {
if g.cfg.Autopilot.Address == "" &&
len(g.cfg.Autopilot.DialOpts) == 0 {
switch g.cfg.Network {
case "mainnet":
g.cfg.Autopilot.Address = MainnetServer
case "testnet":
g.cfg.Autopilot.Address = TestnetServer
default:
return errors.New("no autopilot server " +
"address specified")
}
}
g.cfg.Autopilot.LitVersion = autopilotserver.Version{
Major: uint32(appMajor),
Minor: uint32(appMinor),
Patch: uint32(appPatch),
}
g.autopilotClient, err = autopilotserver.NewClient(
g.cfg.Autopilot,
)
if err != nil {
return err
}
}
// Similar to the init of the `accountRpcServer` reference above, we
// create a reference to the `sessionRpcServer` here before setting up
// the LND connection. See the comment above for the `accountRpcServer`
// to understand why this is necessary.
g.sessionRpcServer = newSessionRPCServer()
// Call the "real" main in a nested manner so the defers will properly
// be executed in the case of a graceful shutdown.
var (
bufRpcListener = bufconn.Listen(100)
readyChan = make(chan struct{})
bufReadyChan = make(chan struct{})
unlockChan = make(chan struct{})
lndQuit = make(chan struct{})
macChan = make(chan []byte, 1)
)
if g.cfg.LndMode == ModeIntegrated {
lisCfg := lnd.ListenerCfg{
RPCListeners: []*lnd.ListenerWithSignal{{
Listener: &onDemandListener{
addr: g.cfg.Lnd.RPCListeners[0],
},
Ready: readyChan,
}, {
Listener: bufRpcListener,
Ready: bufReadyChan,
MacChan: macChan,
}},
}
var auxComponents lnd.AuxComponents
switch g.cfg.TaprootAssetsMode {
case ModeRemote, ModeDisable:
log.Warnf("Taproot Assets daemon is either disabled " +
"or running in remote mode. Taproot Asset " +
"channel functionality will NOT be " +
"available. To enable, set Taproot Assets " +
"mode to 'integrated' in the config file.")
case ModeIntegrated:
components, err := g.buildAuxComponents(ctx)
if err != nil {
return fmt.Errorf("could not build aux "+
"components: %w", err)
}
auxComponents = *components
}
// nolint:ll
implCfg := &lnd.ImplementationCfg{
GrpcRegistrar: g,
RestRegistrar: g,
ExternalValidator: g,
DatabaseBuilder: g.defaultImplCfg.DatabaseBuilder,
WalletConfigBuilder: g,
ChainControlBuilder: g.defaultImplCfg.ChainControlBuilder,
AuxComponents: auxComponents,
}
g.wg.Add(1)
go func() {
defer g.wg.Done()
// Note that LND will call litd's RegisterGrpcSubserver
// function during the execution of this, as `g` is
// referenced in the passed `implCfg`
err := lnd.Main(g.cfg.Lnd, lisCfg, implCfg, interceptor)
if e, ok := err.(*flags.Error); err != nil &&
(!ok || e.Type != flags.ErrHelp) {
errStr := fmt.Sprintf("Error running main "+
"lnd: %v", err)
log.Errorf(errStr)
g.statusMgr.SetErrored(subservers.LND, errStr)
g.errQueue.ChanIn() <- err
return
}
close(lndQuit)
}()
} else {
close(unlockChan)
close(readyChan)
close(bufReadyChan)
_ = g.RegisterGrpcSubserver(g.rpcProxy.grpcServer)
}
// Wait for lnd to be started up so we know we have a TLS cert.
select {
// If lnd needs to be unlocked we get the signal that it's ready to do
// so. We then go ahead and start the UI so we can unlock it there as
// well.
case <-unlockChan:
// If lnd is running with --noseedbackup and doesn't need unlocking, we
// get the ready signal immediately.
case <-readyChan:
case err := <-g.errQueue.ChanOut():
g.statusMgr.SetErrored(
subservers.LND, "error from errQueue channel",
)
return fmt.Errorf("could not start LND: %v", err)
case <-lndQuit:
g.statusMgr.SetErrored(
subservers.LND, "lndQuit channel closed",
)
return fmt.Errorf("LND has stopped")
case <-ctx.Done():
return ctx.Err()
}
// Connect to LND.
g.lndConn, err = connectLND(g.cfg, bufRpcListener)
if err != nil {
g.statusMgr.SetErrored(
subservers.LND, "could not connect to LND: %v", err,
)
return fmt.Errorf("could not connect to LND")
}
// In order to be able to create unique middleware request identifiers,
// we set a new unique connection ID. This should be refreshed every
// time we (re)connect to LND.
// TODO: This assumes that litd needs to be restarted when the
// connection to LND is interrupted, leading to a unique connection ID.
// When automatic reconnection is implemented, we need to make sure that
// the connection ID is refreshed when the connection is re-established.
g.lndConnID = randId(rules.LndConnIdLen)
// Initialise any connections to sub-servers that we are running in
// remote mode.
g.subServerMgr.ConnectRemoteSubServers()
// bakeSuperMac is a closure that can be used to bake a new super
// macaroon that contains all active permissions.
bakeSuperMac := func(ctx context.Context, rootKeyIDSuffix uint32,
readOnly bool) (string, error) {
var suffixBytes [4]byte
binary.BigEndian.PutUint32(suffixBytes[:], rootKeyIDSuffix)
rootKeyID := litmac.NewSuperMacaroonRootKeyID(suffixBytes)
return litmac.BakeSuperMacaroon(
ctx, g.basicClient, rootKeyID,
g.permsMgr.ActivePermissions(readOnly), nil,
)
}
// Now start the RPC proxy that will handle all incoming gRPC, grpc-web
// and REST requests.
if err := g.rpcProxy.Start(g.lndConn, bakeSuperMac); err != nil {
return fmt.Errorf("error starting lnd gRPC proxy server: %v",
err)
}
// We now set a custom status for the LND sub-server to indicate that
// the wallet is ready.
// This is done _before_ we have set up the lnd clients so that the
// litcli status command won't error before the lnd sub-server has
// been marked as running.
g.statusMgr.SetCustomStatus(subservers.LND, lndWalletReadyStatus)
// Now that we have started the main UI web server, show some useful
// information to the user so they can access the web UI easily.
if err := g.showStartupInfo(ctx); err != nil {
return fmt.Errorf("error displaying startup info: %v", err)
}
// waitForSignal is a helper closure that can be used to wait on the
// given channel for a signal while also being responsive to an error
// from the error Queue, LND quiting or the interceptor receiving a
// shutdown signal.
waitForSignal := func(c chan struct{}) error {
select {
case <-c:
return nil
case err := <-g.errQueue.ChanOut():
return err
case <-lndQuit:
g.statusMgr.SetErrored(
subservers.LND, "lndQuit channel closed",
)
return fmt.Errorf("LND has stopped")
case <-ctx.Done():
return ctx.Err()
}
}
// Wait for lnd to be unlocked, then start all clients.
if err = waitForSignal(readyChan); err != nil {
return err
}
// 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 {
if err = waitForSignal(bufReadyChan); err != nil {
return err
}
// Create a new macReady channel that will serve to signal that
// the LND macaroon is ready. Spin off a goroutine that will
// close this channel when the macaroon has been received.
macReady := make(chan struct{})
go func() {
g.cfg.lndAdminMacaroon = <-macChan
close(macReady)
}()
if err = waitForSignal(macReady); err != nil {
return err
}
}
// Since we are now connected to LND, we can now set up a basic LND
// client. Note this doesn't require LND to be synced, but can still be
// used to fetch info from LND such as its macaroons. Therefore, it's ok
// set it up prior to setting up the stores and starting the other RPC
// servers, as the setup will be fast.
err = g.setupBasicLNDClient(ctx, lndQuit)
if err != nil {
g.statusMgr.SetErrored(
subservers.LND,
"could not to set up a basic LND client: %v", err,
)
return fmt.Errorf("could not start LND")
}
g.stores, err = NewStores(
ctx, g.cfg, g.basicClient, clock.NewDefaultClock(),
)
if err != nil {
return fmt.Errorf("could not create stores: %v", err)
}
if err := g.stores.firewall.Start(ctx); err != nil {
return fmt.Errorf("could not start firewall DB: %v", err)
}
g.accountService, err = accounts.NewService(
g.stores.accounts, accountServiceErrCallback,
)
if err != nil {
return fmt.Errorf("error creating account service: %v", err)
}
if err != nil {
return fmt.Errorf("could not create new session rpc "+
"server: %v", err)
}
// Set up a full LND client. With this, we now have all LND clients
// needed for LiT to be fully started.
err = g.setupFullLNDClient(ctx, lndQuit)
if err != nil {
g.statusMgr.SetErrored(
subservers.LND,
"could not to set up a full LND client: %v", err,
)
return fmt.Errorf("could not start LND")
}
// Mark that lnd is now completely running after connecting the
// lnd clients.
g.statusMgr.SetRunning(subservers.LND)
// Both connection types are ready now, let's start our sub-servers if
// they should be started locally as an integrated service.
createDefaultMacaroons := !g.cfg.statelessInitMode
g.subServerMgr.StartIntegratedServers(
g.basicClient, g.lndClient, createDefaultMacaroons,
)
err = g.startInternalSubServers(ctx, !g.cfg.statelessInitMode)
if err != nil {
return fmt.Errorf("could not start litd sub-servers: %v", err)
}
// We can now set the status of LiT as running.
g.statusMgr.SetRunning(subservers.LIT)
// Now block until we receive an error or the main shutdown signal.
select {
case err := <-g.errQueue.ChanOut():
if err != nil {
return fmt.Errorf("received critical error from "+
"subsystem, shutting down: %v", err)
}
case <-lndQuit:
g.statusMgr.SetErrored(
subservers.LND, "lndQuit channel closed",
)
return fmt.Errorf("LND is not running")
case <-ctx.Done():
log.Infof("Shutdown signal received")
}
return nil
}
// basicLNDClient provides access to LiT's basicClient if it has been set.
func (g *LightningTerminal) basicLNDClient() (lnrpc.LightningClient, error) {
if !g.basicClientSet.Load() {
return nil, fmt.Errorf("basic LND client has not yet been set")
}
return g.basicClient, nil
}
// checkRunning checks if we should continue running for the duration of the
// defaultStartupTimeout, or else returns an error indicating why a shut-down is
// needed.
func (g *LightningTerminal) checkRunning(ctx context.Context,
lndQuit chan struct{}) error {
select {
case err := <-g.errQueue.ChanOut():
return fmt.Errorf("error from subsystem: %v", err)
case <-lndQuit:
return fmt.Errorf("LND has stopped")
case <-ctx.Done():
return ctx.Err()
case <-time.After(g.cfg.LndConnectInterval):
return nil
}
}
// setupBasicLNDClient sets up a basic LND client that can be used to connect to
// LND without requiring LND to be fully synced. Since this client is only a
// basic client, not all of LNDs functionality is available through it.
func (g *LightningTerminal) setupBasicLNDClient(ctx context.Context,
lndQuit chan struct{}) error {
var (
err error
clientOptions []lndclient.BasicClientOption
)
host, network, tlsPath, macPath, macData := g.cfg.lndConnectParams()
clientOptions = append(clientOptions, lndclient.MacaroonData(
hex.EncodeToString(macData),
))
clientOptions = append(
clientOptions, lndclient.MacFilename(filepath.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 {
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
// basic client connection to find out if the RPC server is started yet
// because that doesn't do anything else than just connect.
log.Infof("Connecting basic lnd client")
for {
// Create an lnd client now that we have the full configuration.
// We'll need a basic client and a full client because not all
// subservers have the same requirements.
g.basicClient, err = lndclient.NewBasicClient(
host, tlsPath, filepath.Dir(macPath),
string(network), clientOptions...,
)
if err == nil {
log.Infof("Basic lnd client connected")
break
}
g.statusMgr.SetErrored(
subservers.LIT,
"Error when setting up basic LND Client: %v", err,
)
err = g.checkRunning(ctx, lndQuit)
if err != nil {
return err
}
log.Infof("Retrying to connect basic lnd client")
}
g.basicClientSet.Store(true)
// If we're in integrated and stateless init mode, we won't create
// macaroon files in any of the subserver daemons.
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()
g.cfg.statelessInitMode = macService.StatelessInit
}
return nil
}
// setupFullLNDClient connects a up a full LND client to LND. Note that the
// setup of this client will block until LND is fully synced and unlocked.
func (g *LightningTerminal) setupFullLNDClient(ctx context.Context,
lndQuit chan struct{}) error {
var (
err error
insecure bool
)
host, network, tlsPath, macPath, macData := g.cfg.lndConnectParams()
if g.cfg.LndMode == ModeIntegrated {
// Ssince we will not require tls when communicating with lnd
// via a bufconn in integrated mode, we set the insecure flag
// to true.
insecure = true
}
// When setting up a full LND client, we we need to wait for two things:
// The chain notifier to be ready and the lnd wallet being fully synced
// to its chain backend. The chain notifier will always be ready first
// so if we instruct the lndclient to wait for the wallet sync, we
// should be fully ready to start all our subservers. This will just
// block until lnd signals readiness.
log.Infof("Connecting full lnd client")
for {
g.lndClient, err = lndclient.NewLndServices(
&lndclient.LndServicesConfig{
LndAddress: host,
Network: network,
TLSPath: tlsPath,
Insecure: insecure,
CustomMacaroonPath: macPath,
CustomMacaroonHex: hex.EncodeToString(
macData,
),
CallerCtx: ctx,
CheckVersion: minimalCompatibleVersion,
RPCTimeout: g.cfg.LndRPCTimeout,
ChainSyncPollInterval: g.cfg.LndConnectInterval,
BlockUntilChainSynced: true,
BlockUntilUnlocked: true,
BlockUntilChainNotifier: true,
},
)
if err == nil {
log.Infof("Full lnd client connected")
break
}
g.statusMgr.SetErrored(
subservers.LIT,
"Error when creating LND Services client: %v",
err,
)
err = g.checkRunning(ctx, lndQuit)
if err != nil {
return err
}
log.Infof("Retrying to create LND Services client")
}
// Pass LND's build tags to the permission manager so that it can
// filter the available permissions accordingly.
g.permsMgr.OnLNDBuildTags(g.lndClient.Version.BuildTags)
// 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.
log.Infof("Baking internal super macaroon")
superMacaroon, err := litmac.BakeSuperMacaroon(
ctx, g.basicClient, litmac.NewSuperMacaroonRootKeyID(
[4]byte{},
),
g.permsMgr.ActivePermissions(false), nil,
)
if err != nil {
return err
}
g.rpcProxy.superMacaroon = superMacaroon
}
return nil
}
// startInternalSubServers starts all Litd specific sub-servers.
func (g *LightningTerminal) startInternalSubServers(ctx context.Context,
createDefaultMacaroons bool) error {
log.Infof("Starting LiT macaroon service")
// Set up the macaroon service.
rks, db, err := lndclient.NewBoltMacaroonStore(
filepath.Join(g.cfg.LitDir, g.cfg.Network),
lncfg.MacaroonDBName, macDatabaseOpenTimeout,
)
if err != nil {
return err
}
g.macaroonDB = db
g.macaroonService, err = lndclient.NewMacaroonService(
&lndclient.MacaroonServiceConfig{
RootKeyStore: rks,
MacaroonLocation: "litd",
StatelessInit: !createDefaultMacaroons,
RequiredPerms: perms.RequiredPermissions,
LndClient: &g.lndClient.LndServices,
EphemeralKey: lndclient.SharedKeyNUMS,
KeyLocator: lndclient.SharedKeyLocator,
MacaroonPath: g.cfg.MacaroonPath,
},
)
if err != nil {
log.Errorf("Could not create a new macaroon service: %v", err)
return err
}
if err := g.macaroonService.Start(); err != nil {
return fmt.Errorf("could not start macaroon service: %v", err)
}
g.macaroonServiceStarted = true
superMacBaker := func(ctx context.Context, rootKeyID uint64,
perms []bakery.Op, caveats []macaroon.Caveat) (string, error) {
return litmac.BakeSuperMacaroon(
ctx, g.basicClient, rootKeyID, perms, caveats,
)
}
log.Infof("Starting LiT accounts server")
g.accountRpcServer.Start(g.accountService, superMacBaker)
if !g.cfg.Autopilot.Disable {
withLndVersion := func(cfg *autopilotserver.Config) {
cfg.LndVersion = autopilotserver.Version{
Major: g.lndClient.Version.AppMajor,
Minor: g.lndClient.Version.AppMinor,
Patch: g.lndClient.Version.AppPatch,
}
}
err = g.autopilotClient.Start(ctx, withLndVersion)
if err != nil {
return fmt.Errorf("could not start the autopilot "+
"client: %v", err)
}
}
log.Infof("Starting LiT session server")
sessionCfg := &sessionRpcServerConfig{
db: g.stores.sessions,
basicAuth: g.rpcProxy.basicAuth,
grpcOptions: []grpc.ServerOption{
// nolint:staticcheck,
grpc.CustomCodec(grpcProxy.Codec()),
grpc.ChainStreamInterceptor(
g.rpcProxy.StreamServerInterceptor,
),
grpc.ChainUnaryInterceptor(
g.rpcProxy.UnaryServerInterceptor,
),
grpc.UnknownServiceHandler(
grpcProxy.TransparentHandler(
// Don't allow calls to litrpc.
g.rpcProxy.makeDirector(false),
),
),
},
registerGrpcServers: func(server *grpc.Server) {
g.registerSubDaemonGrpcServers(server, true)
},
superMacBaker: superMacBaker,
firstConnectionDeadline: g.cfg.FirstLNCConnDeadline,
permMgr: g.permsMgr,
actionsDB: g.stores.firewall,
autopilot: g.autopilotClient,
ruleMgrs: g.ruleMgrs,
privMap: g.stores.firewall,
}
if err = g.sessionRpcServer.start(ctx, sessionCfg); err != nil {
return err
}
g.sessionRpcServerStarted = true
// The rest of the function only applies if the rpc middleware
// interceptor has been enabled.
if g.cfg.RPCMiddleware.Disabled {
log.Infof("Internal sub server startup complete")
return nil
}
// Even if the accounts service fails on the Start function, or the
// accounts service is disabled, we still want to call Stop function as
// this closes the contexts and the db store which were opened with the
// accounts.NewService function call in the LightningTerminal start
// function above.
closeAccountService := func() {
if err := g.accountService.Stop(); err != nil {
// We only log the error if we fail to stop the service,
// as it's not critical that this succeeds in order to
// keep litd running
log.Errorf("Error stopping account service: %v", err)
}
}
log.Infof("Starting LiT account service")
if !g.cfg.Accounts.Disable {
err = g.accountService.Start(
ctx, g.lndClient.Client, g.lndClient.Router,
g.lndClient.ChainParams,
)
if err != nil {
log.Errorf("error starting account service: %v, "+
"disabling account service", err)
g.statusMgr.SetErrored(subservers.ACCOUNTS, err.Error())
closeAccountService()
} else {
g.statusMgr.SetRunning(subservers.ACCOUNTS)
g.accountServiceStarted = true
}
} else {
closeAccountService()
}
requestLogger, err := firewall.NewRequestLogger(
g.cfg.Firewall.RequestLogger, g.stores.firewall,
)
if err != nil {
return fmt.Errorf("error creating new request logger")
}
privacyMapper := firewall.NewPrivacyMapper(
g.stores.firewall, firewall.CryptoRandIntn,
g.stores.sessions,
)
mw := []mid.RequestInterceptor{
privacyMapper,
g.accountService,
requestLogger,
}
if !g.cfg.Autopilot.Disable {
ruleEnforcer := firewall.NewRuleEnforcer(
g.stores.firewall, g.stores.firewall,
g.stores.sessions,
g.autopilotClient.ListFeaturePerms,
g.permsMgr, g.lndClient.NodePubkey,
g.lndClient.Router,
g.lndClient.Client, g.lndConnID, g.ruleMgrs,
func(ctx context.Context, reqID uint64,
reason string) error {
return requestLogger.MarkAction(
ctx, reqID, firewalldb.ActionStateError,
reason,
)
}, g.stores.firewall,
)
mw = append(mw, ruleEnforcer)
}
// Start the middleware manager.
log.Infof("Starting LiT middleware manager")
g.middleware = mid.NewManager(
g.cfg.RPCMiddleware.InterceptTimeout,
g.lndClient.Client, g.errQueue.ChanIn(), mw...,
)
if err = g.middleware.Start(ctx); err != nil {
return err
}
g.middlewareStarted = true
log.Infof("Internal sub server startup complete")
return nil
}
// RegisterGrpcSubserver is a callback on the lnd.SubserverConfig struct that is
// called once lnd has initialized its main gRPC server instance. It gives the
// daemons (or external subservers) the possibility to register themselves to
// the same server instance.
//
// NOTE: This is part of the lnd.GrpcRegistrar interface.
func (g *LightningTerminal) RegisterGrpcSubserver(server *grpc.Server) error {
if err := g.defaultImplCfg.RegisterGrpcSubserver(server); err != nil {
return err
}
// Register all other daemon RPC servers that are running in-process.
// The LiT session server should be enabled on the main interface.
g.registerSubDaemonGrpcServers(server, false)
return nil
}
// registerSubDaemonGrpcServers registers the sub daemon (Faraday, Loop, Pool
// and LiT session) servers to a given gRPC server, given they are running in
// the local process. Some of LiT's own sub-servers should be registered with
// LNC sessions and some should not - the forLNCSession boolean can be used to
// control this.
func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server,
forLNCSession bool) {
g.subServerMgr.RegisterRPCServices(server)
if forLNCSession {
litrpc.RegisterStatusServer(server, g.statusMgr)
litrpc.RegisterProxyServer(server, g.rpcProxy)
} else {
litrpc.RegisterSessionsServer(server, g.sessionRpcServer)
if !g.cfg.Accounts.Disable {
litrpc.RegisterAccountsServer(
server, g.accountRpcServer,
)
}
}
litrpc.RegisterFirewallServer(server, g.sessionRpcServer)
if !g.cfg.Autopilot.Disable {
litrpc.RegisterAutopilotServer(server, g.sessionRpcServer)
}
}
// RegisterRestSubserver is a callback on the lnd.SubserverConfig struct that is
// called once lnd has initialized its main REST server instance. It gives the
// daemons (or external subservers) the possibility to register themselves to
// the same server instance.
//
// NOTE: This is part of the lnd.RestRegistrar interface.
func (g *LightningTerminal) RegisterRestSubserver(ctx context.Context,
mux *restProxy.ServeMux, endpoint string,
dialOpts []grpc.DialOption) error {
err := g.defaultImplCfg.RegisterRestSubserver(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
if !g.cfg.Accounts.Disable {
err = litrpc.RegisterAccountsHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
}
err = litrpc.RegisterFirewallHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
err = litrpc.RegisterAutopilotHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
err = litrpc.RegisterSessionsHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
err = litrpc.RegisterProxyHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
err = litrpc.RegisterStatusHandlerFromEndpoint(
ctx, mux, endpoint, dialOpts,
)
if err != nil {
return err
}
return g.subServerMgr.RegisterRestServices(ctx, mux, endpoint, dialOpts)
}
// ValidateMacaroon extracts the macaroon from the context's gRPC metadata,
// checks its signature, makes sure all specified permissions for the called
// method are contained within and finally ensures all caveat conditions are
// met. A non-nil error is returned if any of the checks fail.
//
// NOTE: This is part of the lnd.ExternalValidator interface.
func (g *LightningTerminal) ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, fullMethod string) error {
// If the URL being queried has been whitelisted, then no macaroon
// validation is required for the query.
if g.permsMgr.IsWhiteListedURL(fullMethod) {
return nil
}
macHex, err := macaroons.RawMacaroonFromContext(ctx)
if err != nil {
return err
}
// If we're using a super macaroon, we just make sure it is valid and
// contains all the permissions needed. If we get to this point, we're
// either in integrated lnd mode where this is the only macaroon
// validation function, and we're done after the check. Or we're in
// remote lnd mode but the request is for an in-process daemon which we
// can validate here. Any request for a remote sub-daemon goes through
// the proxy and its director and any super macaroon will be converted
// to a daemon specific macaroon before directing the call to the remote
// daemon. Those calls don't land here.
if litmac.IsSuperMacaroon(macHex) {
macBytes, err := hex.DecodeString(macHex)
if err != nil {
return err
}
return g.validateSuperMacaroon(
ctx, macBytes, requiredPermissions, fullMethod,
)
}
// 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.
handledBySubserver, err := g.subServerMgr.ValidateMacaroon(
ctx, requiredPermissions, fullMethod,
)
if handledBySubserver {
return err
}
if g.permsMgr.IsSubServerURI(subservers.LIT, fullMethod) {
if !g.macaroonServiceStarted {
return fmt.Errorf("the macaroon service has not " +
"started yet")
}
if err := g.macaroonService.ValidateMacaroon(
ctx, requiredPermissions, fullMethod,
); err != nil {
return &proxyErr{
proxyContext: "lit",
wrapped: fmt.Errorf("invalid macaroon: %w",
err),
}
}
}
// Because lnd will spin up its own gRPC server with macaroon
// interceptors if it is running in this process, it will check its
// macaroons there. If lnd is running remotely, that process will check
// the macaroons. So we don't need to worry about anything other than
// the subservers that are running in the local process.
return nil
}
// Permissions returns all permissions for which the external validator of the
// terminal is responsible.
//
// NOTE: This is part of the lnd.ExternalValidator interface.
func (g *LightningTerminal) Permissions() map[string][]bakery.Op {
return g.permsMgr.GetLitPerms()
}
// 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, auxComponents *lnd.AuxComponents,
interceptorChain *rpcperms.InterceptorChain,
grpcListeners []*lnd.ListenerWithSignal) (*chainreg.PartialChainControl,
*btcwallet.Config, func(), error) {
g.lndInterceptorChain = interceptorChain
return g.defaultImplCfg.WalletConfigBuilder.BuildWalletConfig(
ctx, dbs, auxComponents, interceptorChain, grpcListeners,
)
}
// buildAuxComponent builds the auxiliary components required by lnd when
// running in integrated mode with tapd being the service that provides the
// aux component implementations.
func (g *LightningTerminal) buildAuxComponents(
ctx context.Context) (*lnd.AuxComponents, error) {
errNotAvailable := fmt.Errorf("tapd is not available, both lnd and " +
"tapd must be started in integrated mode for Taproot " +
"Assets Channels to be available")
tapdWrapper, available := g.subServerMgr.GetServer(subservers.TAP)
if !available {
return nil, errNotAvailable
}
if tapdWrapper.Remote() {
return nil, errNotAvailable
}
tapdOpt := tapdWrapper.Impl()
tapdAny, err := tapdOpt.UnwrapOrErr(errors.New("tapd not available"))
if err != nil {
return nil, err
}
tapd, ok := tapdAny.(*taprootassets.Server)
if !ok {
return nil, fmt.Errorf("tapd is not of the expected type")
}
router := msgmux.NewMultiMsgRouter()
router.Start(ctx)
err = router.RegisterEndpoint(tapd)
if err != nil {
return nil, fmt.Errorf("error registering tapd endpoint: %w",
err)
}
return &lnd.AuxComponents{
AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](tapd),
MsgRouter: fn.Some[msgmux.Router](router),
AuxFundingController: fn.Some[funding.AuxFundingController](
tapd,
),
AuxSigner: fn.Some[lnwallet.AuxSigner](tapd),
TrafficShaper: fn.Some[htlcswitch.AuxTrafficShaper](tapd),
AuxDataParser: fn.Some[lnd.AuxDataParser](tapd),
AuxChanCloser: fn.Some[chancloser.AuxChanCloser](tapd),
AuxSweeper: fn.Some[sweep.AuxSweeper](tapd),
AuxContractResolver: fn.Some[lnwallet.AuxContractResolver](
tapd,
),
AuxChannelNegotiator: fn.Some[lnwallet.AuxChannelNegotiator](
tapd,
),
}, nil
}
// shutdownSubServers stops all subservers that were started and attached to
// lnd.
func (g *LightningTerminal) shutdownSubServers() error {
var returnErr error
err := g.subServerMgr.Stop()
if err != nil {
returnErr = err
}
if g.autopilotClient != nil {
g.autopilotClient.Stop()
}
if g.sessionRpcServerStarted {
if err := g.sessionRpcServer.stop(); err != nil {
log.Errorf("Error closing session DB: %v", err)
returnErr = err
}
}
if g.macaroonServiceStarted {
if err := g.macaroonService.Stop(); err != nil {
log.Errorf("Error stopping macaroon service: %v", err)
returnErr = err
}
}
if g.macaroonDB != nil {
g.macaroonDB.Close()
}
if g.accountServiceStarted {
if err := g.accountService.Stop(); err != nil {
log.Errorf("Error stopping account service: %v", err)
returnErr = err
}
}
if g.middlewareStarted {
g.middleware.Stop()
}
if g.ruleMgrs != nil {
if err := g.ruleMgrs.Stop(); err != nil {
log.Errorf("Error stopping rule manager set: %v", err)
returnErr = err
}
}
if g.stores != nil {
if g.stores.firewall != nil {
if err := g.stores.firewall.Stop(); err != nil {
log.Errorf("Error stoppint firewall DB: %v",
err)
returnErr = err
}
}
err = g.stores.close()
if err != nil {
log.Errorf("Error closing stores: %v", err)
returnErr = err
}
}
if g.lndClient != nil {
g.lndClient.Close()
}
if g.restCancel != nil {
g.restCancel()
}
if g.lndConn != nil {
if err := g.lndConn.Close(); err != nil {
log.Errorf("Error closing lnd connection: %v", err)
returnErr = err
}
}
if g.rpcProxy != nil {
if err := g.rpcProxy.Stop(); err != nil {
log.Errorf("Error stopping rpc proxy: %v", err)
returnErr = err
}
}
if g.httpServer != nil {
if err := g.httpServer.Close(); err != nil {
log.Errorf("Error stopping UI server: %v", err)
returnErr = err
}
}
// Do we have any last errors to display? We use an anonymous function,
// so we can use return instead of breaking to a label in the default
// case.
func() {
for {
select {
case err := <-g.errQueue.ChanOut():
if err != nil {
log.Errorf("Error while stopping "+
"litd: %v", err)
returnErr = err
}
default:
return
}
}
}()
return returnErr
}
// startMainWebServer creates the main web HTTP server that delegates requests
// between the embedded HTTP server and the RPC proxy. An incoming request will
// go through the following chain of components:
//
// Request on port 8443 <------------------------------------+
// | converted gRPC request |
// v |
// +---+----------------------+ other +----------------+ |
// | Main web HTTP server +------->+ Embedded HTTP | |
// +---+----------------------+____+ +----------------+ |
// | | |
// v any RPC or grpc-web call | any REST call |
// +---+----------------------+ |->+----------------+ |
// | grpc-web proxy | + grpc-gateway +-----------+
// +---+----------------------+ +----------------+
// |
// v native gRPC call with basic auth
// +---+----------------------+
// | interceptors |
// +---+----------------------+
// |
// v native gRPC call with macaroon
// +---+----------------------+
// | gRPC server |
// +---+----------------------+
// |
// v unknown authenticated call, gRPC server is just a wrapper
// +---+----------------------+
// | director |
// +---+----------------------+
// |
// v authenticated call
// +---+----------------------+ call to lnd or integrated daemon
// | lnd (remote or local) +---------------+
// | faraday remote | |
// | loop remote | +----------v----------+
// | pool remote | | lnd local subserver |
// +--------------------------+ | - faraday |
// | - loop |
// | - pool |
// +---------------------+
func (g *LightningTerminal) startMainWebServer() error {
// Initialize the in-memory file server from the content compiled by
// the go:embed directive. Since everything's relative to the root dir,
// we need to create an FS of the sub directory app/build.
buildDir, err := fs.Sub(appBuildFS, appFilesDir)
if err != nil {
return err
}
staticFileServer := http.FileServer(&ClientRouteWrapper{
assets: http.FS(buildDir),
})
// Both gRPC (web) and static file requests will come into through the
// main UI HTTP server. We use this simple switching handler to send the
// requests to the correct implementation.
httpHandler := func(resp http.ResponseWriter, req *http.Request) {
// If this is some kind of gRPC, gRPC Web or REST call that
// should go to lnd or one of the daemons, pass it to the proxy
// that handles all those calls.
if g.rpcProxy.isHandling(resp, req) {
return
}
// REST requests aren't that easy to identify, we have to look
// at the URL itself. If this is a REST request, we give it
// directly to our REST handler which will then forward it to
// us again but converted to a gRPC request.
if g.cfg.EnableREST && isRESTRequest(req) {
log.Infof("Handling REST request: %s", req.URL.Path)
g.restHandler.ServeHTTP(resp, req)
return
}
// If the UI is disabled, then we return a 401 here to prevent
// serving any of the static files.
if g.cfg.DisableUI {
resp.WriteHeader(http.StatusUnauthorized)
return
}
// If we got here, it's a static file the browser wants, or
// something we don't know in which case the static file server
// will answer with a 404.
log.Infof("Handling static file request: %s", req.URL.Path)
// Add 1-year cache header for static files. React uses content-
// based hashes in file names, so when any file is updated, the
// url will change causing the browser cached version to be
// invalidated.
var re = regexp.MustCompile(`^/(static|fonts|icons)/.*`)
if re.MatchString(req.URL.Path) {
resp.Header().Set("Cache-Control", "max-age=31536000")
}
// Transfer static files using gzip to save up to 70% of
// bandwidth.
gzipHandler := makeGzipHandler(staticFileServer.ServeHTTP)
gzipHandler(resp, req)
}
// Create and start our HTTPS server now that will handle both gRPC web
// and static file requests.
g.httpServer = &http.Server{
// To make sure that long-running calls and indefinitely opened
// streaming connections aren't terminated by the internal
// proxy, we need to disable all timeouts except the one for
// reading the HTTP headers. That timeout shouldn't be removed
// as we would otherwise be prone to the slowloris attack where
// an attacker takes too long to send the headers and uses up
// connections that way. Once the headers are read, we either
// know it's a static resource and can deliver that very cheaply
// or check the authentication for other calls.
WriteTimeout: 0,
IdleTimeout: 0,
ReadTimeout: 0,
ReadHeaderTimeout: defaultServerTimeout,
Handler: http.HandlerFunc(httpHandler),
}
httpListener, err := net.Listen("tcp", g.cfg.HTTPSListen)
if err != nil {
return fmt.Errorf("unable to listen on %v: %v",
g.cfg.HTTPSListen, err)
}
tlsConfig, err := buildTLSConfigForHttp2(g.cfg)
if err != nil {
return fmt.Errorf("unable to create TLS config: %v", err)
}
tlsListener := tls.NewListener(httpListener, tlsConfig)
g.wg.Add(1)
go func() {
defer g.wg.Done()
log.Infof("Listening for http_tls on: %v", tlsListener.Addr())
err := g.httpServer.Serve(tlsListener)
if err != nil && err != http.ErrServerClosed {
log.Errorf("http_tls server error: %v", err)
}
}()
// We only enable an additional HTTP only listener if the user
// explicitly sets a value.
if g.cfg.HTTPListen != "" {
insecureListener, err := net.Listen("tcp", g.cfg.HTTPListen)
if err != nil {
return fmt.Errorf("unable to listen on %v: %v",
g.cfg.HTTPListen, err)
}
g.wg.Add(1)
go func() {
defer g.wg.Done()
log.Infof("Listening for http on: %v",
insecureListener.Addr())
err := g.httpServer.Serve(insecureListener)
if err != nil && err != http.ErrServerClosed {
log.Errorf("http server error: %v", err)
}
}()
}
return nil
}
// createRESTProxy creates a grpc-gateway based REST proxy that takes any call
// identified as a REST call, converts it to a gRPC request and forwards it to
// our local main server for further triage/forwarding.
func (g *LightningTerminal) createRESTProxy(ctx context.Context) error {
// The default JSON marshaler of the REST proxy only sets OrigName to
// true, which instructs it to use the same field names as specified in
// the proto file and not switch to camel case. What we also want is
// that the marshaler prints all values, even if they are falsey.
customMarshalerOption := restProxy.WithMarshalerOption(
restProxy.MIMEWildcard, &restProxy.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
},
},
)
// For our REST dial options, we increase the max message size that
// we'll decode to allow clients to hit endpoints which return more data
// such as the DescribeGraph call. We set this to 200MiB atm. Should be
// the same value as maxMsgRecvSize in lnd/cmd/lncli/main.go.
restDialOpts := []grpc.DialOption{
// We are forwarding the requests directly to the address of our
// own local listener. To not need to mess with the TLS
// certificate (which might be tricky if we're using Let's
// Encrypt), we just skip the certificate verification.
// Injecting a malicious hostname into the listener address will
// result in an error on startup so this should be quite safe.
grpc.WithTransportCredentials(credentials.NewTLS(
&tls.Config{InsecureSkipVerify: true},
)),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200),
),
}
// We use our own RPC listener as the destination for our REST proxy.
// If the listener is set to listen on all interfaces, we replace it
// with localhost, as we cannot dial it directly.
restProxyDest := toLocalAddress(g.cfg.HTTPSListen)
// Now start the REST proxy for our gRPC server above. We'll ensure
// we direct LND to connect to its loopback address rather than a
// wildcard to prevent certificate issues when accessing the proxy
// externally.
restMux := restProxy.NewServeMux(customMarshalerOption)
ctx, cancel := context.WithCancel(ctx)
g.restCancel = cancel
// Enable WebSocket and CORS support as well. A request will pass
// through the following chain:
// req ---> CORS handler --> WS proxy ---> REST proxy --> gRPC endpoint
// where gRPC endpoint is our main HTTP(S) listener again.
restHandler := lnrpc.NewWebSocketProxy(
restMux, log, g.cfg.Lnd.WSPingInterval, g.cfg.Lnd.WSPongWait,
lnrpc.LndClientStreamingURIs,
)
g.restHandler = allowCORS(restHandler, g.cfg.RestCORS)
// First register all lnd handlers. This will make it possible to speak
// REST over the main RPC listener port in both remote and integrated
// mode. In integrated mode the user can still use the --lnd.restlisten
// to spin up an extra REST listener that also offers the same
// functionality, but is no longer required. In remote mode REST will
// only be enabled on the main HTTP(S) listener.
for _, registrationFn := range lndRESTRegistrations {
err := registrationFn(ctx, restMux, restProxyDest, restDialOpts)
if err != nil {
return fmt.Errorf("error registering REST handler: %v",
err)
}
}
// Now register all handlers for faraday, loop and pool.
err := g.RegisterRestSubserver(
ctx, restMux, restProxyDest, restDialOpts,
)
if err != nil {
return fmt.Errorf("error registering REST handler: %v", err)
}
return nil
}
// validateSuperMacaroon makes sure the given macaroon is a valid super macaroon
// that was issued by lnd and contains all the required permissions, even if
// the actual RPC method isn't a lnd request.
func (g *LightningTerminal) validateSuperMacaroon(ctx context.Context,
superMacaroon []byte, requiredPermissions []bakery.Op,
fullMethod string) error {
// 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, superMacaroon, permissions, fullMethod,
)
if err != nil {
return fmt.Errorf("lnd macaroon validation failed: %v",
err)
}
if !res {
return fmt.Errorf("macaroon is not valid")
}
return nil
}
// initSubServers registers the faraday and loop sub-servers with the
// subServerMgr.
func (g *LightningTerminal) initSubServers() error {
err := g.subServerMgr.AddServer(
subservers.NewFaradaySubServer(
g.cfg.Faraday, g.cfg.faradayRpcConfig,
g.cfg.Remote.Faraday, g.cfg.faradayRemote,
), g.cfg.FaradayMode != ModeDisable,
)
if err != nil {
return fmt.Errorf("could not register Faraday subserver: %w",
err)
}
err = g.subServerMgr.AddServer(
subservers.NewLoopSubServer(
g.cfg.Loop, g.cfg.Remote.Loop, g.cfg.loopRemote,
), g.cfg.LoopMode != ModeDisable,
)
if err != nil {
return fmt.Errorf("could not register Loop subserver: %w", err)
}
err = g.subServerMgr.AddServer(
subservers.NewPoolSubServer(
g.cfg.Pool, g.cfg.Remote.Pool, g.cfg.poolRemote,
), g.cfg.PoolMode != ModeDisable,
)
if err != nil {
return fmt.Errorf("could not register Pool subserver: %w", err)
}
err = g.subServerMgr.AddServer(
subservers.NewTaprootAssetsSubServer(
g.cfg.Network, g.cfg.TaprootAssets,
g.cfg.Remote.TaprootAssets,
g.cfg.tapRemote, g.cfg.lndRemote,
), g.cfg.TaprootAssetsMode != ModeDisable,
)
if err != nil {
return fmt.Errorf("could not register Taproot Assets "+
"subserver: %w", err)
}
return nil
}
// allowCORS wraps the given http.Handler with a function that adds the
// Access-Control-Allow-Origin header to the response.
func allowCORS(handler http.Handler, origins []string) http.Handler {
allowHeaders := "Access-Control-Allow-Headers"
allowMethods := "Access-Control-Allow-Methods"
allowOrigin := "Access-Control-Allow-Origin"
// If the user didn't supply any origins that means CORS is disabled
// and we should return the original handler.
if len(origins) == 0 {
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Skip everything if the browser doesn't send the Origin field.
if origin == "" {
handler.ServeHTTP(w, r)
return
}
// Set the static header fields first.
w.Header().Set(
allowHeaders,
"Content-Type, Accept, Grpc-Metadata-Macaroon",
)
w.Header().Set(allowMethods, "GET, POST, DELETE")
// Either we allow all origins or the incoming request matches
// a specific origin in our list of allowed origins.
for _, allowedOrigin := range origins {
if allowedOrigin == "*" || origin == allowedOrigin {
// Only set allowed origin to requested origin.
w.Header().Set(allowOrigin, origin)
break
}
}
// For a pre-flight request we only need to send the headers
// back. No need to call the rest of the chain.
if r.Method == "OPTIONS" {
return
}
// Everything's prepared now, we can pass the request along the
// chain of handlers.
handler.ServeHTTP(w, r)
})
}
// showStartupInfo shows useful information to the user to easily access the
// web UI that was just started.
func (g *LightningTerminal) showStartupInfo(ctx context.Context) error {
info := struct {
mode string
status string
alias string
version string
webURI string
}{
mode: g.cfg.LndMode,
status: "locked",
alias: g.cfg.Lnd.Alias,
version: build.Version(),
webURI: fmt.Sprintf("https://%s", strings.ReplaceAll(
strings.ReplaceAll(
g.cfg.HTTPSListen, "0.0.0.0", "localhost",
), "[::]", "localhost",
)),
}
// In remote mode we try to query the info.
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()
basicClient, err := lndclient.NewBasicClient(
host, tlsPath, filepath.Dir(macPath), string(network),
lndclient.MacFilename(filepath.Base(macPath)),
)
if err != nil {
return fmt.Errorf("error querying remote node: %v", err)
}
res, err := basicClient.GetInfo(ctx, &lnrpc.GetInfoRequest{})
if err != nil {
if !lndclient.IsUnlockError(err) {
return fmt.Errorf("error querying remote "+
"node : %v", err)
}
// Node is locked.
info.status = "locked"
info.alias = "???? (node is locked)"
info.version = "???? (node is locked)"
} else {
info.status = "online"
info.alias = res.Alias
info.version = res.Version
}
}
// In integrated mode, we can derive the state from our configuration.
if g.cfg.LndMode == ModeIntegrated {
// If the integrated node is running with no seed backup, the
// wallet cannot be locked and the node is online right away.
if g.cfg.Lnd.NoSeedBackup {
info.status = "online"
}
}
// If there's an additional HTTP listener, list it as well.
listenAddr := g.cfg.HTTPSListen
if g.cfg.HTTPListen != "" {
host := toLocalAddress(g.cfg.HTTPListen)
info.webURI = fmt.Sprintf("%s or http://%s", info.webURI, host)
listenAddr = fmt.Sprintf("%s, %s", listenAddr, g.cfg.HTTPListen)
}
webInterfaceString := fmt.Sprintf(
"%s (open %s in your browser)", listenAddr, info.webURI,
)
if g.cfg.DisableUI {
webInterfaceString = "disabled"
}
// nolint:ll
str := "" +
"----------------------------------------------------------\n" +
" Lightning Terminal (LiT) by Lightning Labs \n" +
" \n" +
" LND Operating mode %s \n" +
" LND Node status %s \n" +
" LND Alias %s \n" +
" LND Version %s \n" +
" LiT Version %s \n" +
" Web interface %s \n" +
"----------------------------------------------------------\n"
fmt.Printf(str, info.mode, info.status, info.alias, info.version,
RichVersion(), webInterfaceString)
return nil
}
// ClientRouteWrapper is a wrapper around a FileSystem which properly handles
// URL routes that are defined in the client app but unknown to the backend
// http server
type ClientRouteWrapper struct {
assets http.FileSystem
}
// Open intercepts requests to open files. If the file does not exist and there
// is no file extension, then assume this is a client side route and return the
// contents of index.html
func (i *ClientRouteWrapper) Open(name string) (http.File, error) {
localName := name
// The file prefix can be overwritten during build time.
if appFilesPrefix != "" {
localName = strings.Replace(name, appFilesPrefix, "/", 1)
}
localName = strings.ReplaceAll(localName, "//", "/")
ret, err := i.assets.Open(localName)
if !os.IsNotExist(err) || filepath.Ext(localName) != "" {
return ret, err
}
return i.assets.Open("/index.html")
}
// toLocalAddress converts an address that is meant as a wildcard listening
// address ("0.0.0.0" or "[::]") into an address that can be dialed (localhost).
func toLocalAddress(listenerAddress string) string {
addr := strings.ReplaceAll(listenerAddress, "0.0.0.0", "localhost")
return strings.ReplaceAll(addr, "[::]", "localhost")
}
// isRESTRequest determines if a request is a REST request by checking that the
// URI starts with /vX/ where X is a single digit number. This is currently true
// for all REST URIs of lnd, faraday, loop and pool as they all either start
// with /v1/ or /v2/.
func isRESTRequest(req *http.Request) bool {
return patternRESTRequest.MatchString(req.URL.Path)
}
// randId generates a random character string of length n.
func randId(n int) string {
var letters = []rune(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
)
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))] //nolint: gosec
}
return string(b)
}