diff --git a/cmd/litcli/lit.go b/cmd/litcli/lit.go new file mode 100644 index 000000000..376f95c80 --- /dev/null +++ b/cmd/litcli/lit.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "fmt" + + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/urfave/cli" +) + +var litCommands = []cli.Command{ + { + Name: "stop", + Usage: "shutdown the LiT daemon", + Category: "LiT", + Action: shutdownLit, + }, +} + +func shutdownLit(ctx *cli.Context) error { + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewLitServiceClient(clientConn) + + ctxb := context.Background() + _, err = client.StopDaemon(ctxb, &litrpc.StopDaemonRequest{}) + if err != nil { + return err + } + + fmt.Println("Successfully shutdown LiTd") + + return nil +} diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index 4ccf03ed4..893be7fef 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -11,7 +11,6 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/protobuf-hex-display/jsonpb" "github.com/lightninglabs/protobuf-hex-display/proto" - "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/macaroons" "github.com/urfave/cli" @@ -47,16 +46,6 @@ var ( Usage: "path to lit's TLS certificate", Value: terminal.DefaultTLSCertPath, } - lndMode = cli.StringFlag{ - Name: "lndmode", - Usage: "the mode that lnd is running in: remote or integrated", - Value: terminal.ModeIntegrated, - } - lndTlsCertFlag = cli.StringFlag{ - Name: "lndtlscertpath", - Usage: "path to lnd's TLS certificate", - Value: lnd.DefaultConfig().TLSCertPath, - } macaroonPathFlag = cli.StringFlag{ Name: "macaroonpath", Usage: "path to lit's macaroon file", @@ -78,9 +67,7 @@ func main() { }, networkFlag, baseDirFlag, - lndMode, tlsCertFlag, - lndTlsCertFlag, macaroonPathFlag, } app.Commands = append(app.Commands, sessionCommands...) @@ -88,6 +75,8 @@ func main() { app.Commands = append(app.Commands, listActionsCommand) app.Commands = append(app.Commands, privacyMapCommands) app.Commands = append(app.Commands, autopilotCommands) + app.Commands = append(app.Commands, litCommands...) + app.Commands = append(app.Commands, statusCommands...) err := app.Run(os.Args) if err != nil { @@ -175,18 +164,6 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) { ) } - // Get the LND mode. If Lit is in integrated LND mode, then LND's tls - // cert is used directly. Otherwise, Lit's own tls cert is used. - lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name)) - if lndmode == terminal.ModeIntegrated { - tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( - lndTlsCertFlag.Name, - )) - - return tlsCertPath, macaroonPath, nil - } - - // Lit is in remote LND mode. So we need Lit's tls cert. tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( tlsCertFlag.Name, )) diff --git a/cmd/litcli/status.go b/cmd/litcli/status.go new file mode 100644 index 000000000..88fae265d --- /dev/null +++ b/cmd/litcli/status.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/urfave/cli" +) + +var statusCommands = []cli.Command{ + { + Name: "status", + Usage: "info about litd status", + Category: "Status", + Action: getStatus, + }, +} + +func getStatus(ctx *cli.Context) error { + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewStatusClient(clientConn) + + ctxb := context.Background() + resp, err := client.SubServerState( + ctxb, &litrpc.SubServerStatusReq{}, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} diff --git a/config.go b/config.go index cb9d741a7..a036b4770 100644 --- a/config.go +++ b/config.go @@ -152,6 +152,9 @@ type Config struct { LetsEncryptDir string `long:"letsencryptdir" description:"The directory where the Let's Encrypt library will store its key and certificate."` LetsEncryptListen string `long:"letsencryptlisten" description:"The IP:port on which LiT will listen for Let's Encrypt challenges. Let's Encrypt will always try to contact on port 80. Often non-root processes are not allowed to bind to ports lower than 1024. This configuration option allows a different port to be used, but must be used in combination with port forwarding from port 80. This configuration can also be used to specify another IP address to listen on, for example an IPv6 address."` + TLSCertPath string `long:"tlscertpath" description:"Path to write the self signed TLS certificate for LiT's RPC and REST proxy service (if Let's Encrypt is not used). This only applies to the HTTP(S)Listen port."` + TLSKeyPath string `long:"tlskeypath" description:"Path to write the self signed TLS private key for LiT's RPC and REST proxy service (if Let's Encrypt is not used). This only applies to the HTTP(S)Listen port."` + LitDir string `long:"lit-dir" description:"The main directory where LiT looks for its configuration file. If LiT is running in 'remote' lnd mode, this is also the directory where the TLS certificates and log files are stored by default."` ConfigFile string `long:"configfile" description:"Path to LiT's configuration file."` @@ -211,9 +214,6 @@ type Config struct { // RemoteConfig holds the configuration parameters that are needed when running // LiT in the "remote" lnd mode. type RemoteConfig struct { - LitTLSCertPath string `long:"lit-tlscertpath" description:"For lnd remote mode only: Path to write the self signed TLS certificate for LiT's RPC and REST proxy service (if Let's Encrypt is not used)."` - LitTLSKeyPath string `long:"lit-tlskeypath" description:"For lnd remote mode only: Path to write the self signed TLS private key for LiT's RPC and REST proxy service (if Let's Encrypt is not used)."` - LitLogDir string `long:"lit-logdir" description:"For lnd remote mode only: Directory to log output."` LitMaxLogFiles int `long:"lit-maxlogfiles" description:"For lnd remote mode only: Maximum logfiles to keep (0 for no rotation)"` LitMaxLogFileSize int `long:"lit-maxlogfilesize" description:"For lnd remote mode only: Maximum logfile size in MB"` @@ -286,9 +286,9 @@ func (c *Config) lndConnectParams() (string, lndclient.Network, string, func defaultConfig() *Config { return &Config{ HTTPSListen: defaultHTTPSListen, + TLSCertPath: DefaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, Remote: &RemoteConfig{ - LitTLSCertPath: DefaultTLSCertPath, - LitTLSKeyPath: defaultTLSKeyPath, LitDebugLevel: defaultLogLevel, LitLogDir: defaultLogDir, LitMaxLogFiles: defaultMaxLogFiles, @@ -598,6 +598,33 @@ func loadConfigFile(preCfg *Config, interceptor signal.Interceptor) (*Config, return nil, fmt.Errorf("invalid lnd mode %v", cfg.LndMode) } + // If the provided lit directory is not the default, we'll modify the + // path to all of the files and directories that will live within it. + if litDir != DefaultLitDir { + if cfg.TLSKeyPath == defaultTLSKeyPath { + cfg.TLSKeyPath = filepath.Join( + litDir, DefaultTLSKeyFilename, + ) + } + + if cfg.TLSCertPath == DefaultTLSCertPath { + cfg.TLSCertPath = filepath.Join( + litDir, DefaultTLSCertFilename, + ) + } + } + + cfg.TLSCertPath = lncfg.CleanAndExpandPath(cfg.TLSCertPath) + cfg.TLSKeyPath = lncfg.CleanAndExpandPath(cfg.TLSKeyPath) + + // Make sure the parent directories of our certificate files exist. + if err := makeDirectories(filepath.Dir(cfg.TLSCertPath)); err != nil { + return nil, err + } + if err := makeDirectories(filepath.Dir(cfg.TLSKeyPath)); err != nil { + return nil, err + } + // Warn about missing config file only after all other configuration is // done. This prevents the warning on help messages and invalid options. // Note this should go directly before the return. @@ -637,25 +664,15 @@ func validateRemoteModeConfig(cfg *Config) error { // path to all of the files and directories that will live within it. litDir := lnd.CleanAndExpandPath(cfg.LitDir) if litDir != DefaultLitDir { - r.LitTLSCertPath = filepath.Join(litDir, DefaultTLSCertFilename) - r.LitTLSKeyPath = filepath.Join(litDir, DefaultTLSKeyFilename) - r.LitLogDir = filepath.Join(litDir, defaultLogDirname) + if r.LitLogDir == defaultLogDir { + r.LitLogDir = filepath.Join( + litDir, defaultLogDirname, + ) + } } - r.LitTLSCertPath = lncfg.CleanAndExpandPath(r.LitTLSCertPath) - r.LitTLSKeyPath = lncfg.CleanAndExpandPath(r.LitTLSKeyPath) r.LitLogDir = lncfg.CleanAndExpandPath(r.LitLogDir) - // Make sure the parent directories of our certificate files exist. We - // don't need to do the same for the log dir as the log rotator will do - // just that. - if err := makeDirectories(filepath.Dir(r.LitTLSCertPath)); err != nil { - return err - } - if err := makeDirectories(filepath.Dir(r.LitTLSKeyPath)); err != nil { - return err - } - // In remote mode, we don't call lnd's ValidateConfig that sets up a // logging backend for us. We need to manually create and start one. The // root logger should've already been created as part of the default @@ -745,8 +762,7 @@ func readUIPassword(config *Config) error { func buildTLSConfigForHttp2(config *Config) (*tls.Config, error) { var tlsConfig *tls.Config - switch { - case config.LetsEncrypt: + if config.LetsEncrypt { serverName := config.LetsEncryptHost if serverName == "" { return nil, errors.New("let's encrypt host name " + @@ -782,10 +798,9 @@ func buildTLSConfigForHttp2(config *Config) (*tls.Config, error) { tlsConfig = &tls.Config{ GetCertificate: manager.GetCertificate, } - - case config.LndMode == ModeRemote: - tlsCertPath := config.Remote.LitTLSCertPath - tlsKeyPath := config.Remote.LitTLSKeyPath + } else { + tlsCertPath := config.TLSCertPath + tlsKeyPath := config.TLSKeyPath if !lnrpc.FileExists(tlsCertPath) && !lnrpc.FileExists(tlsKeyPath) { @@ -807,16 +822,6 @@ func buildTLSConfigForHttp2(config *Config) (*tls.Config, error) { "keys: %v", err) } tlsConfig = cert.TLSConfFromCert(tlsCert) - - default: - tlsCert, _, err := cert.LoadCert( - config.Lnd.TLSCertPath, config.Lnd.TLSKeyPath, - ) - if err != nil { - return nil, fmt.Errorf("failed reading TLS server "+ - "keys: %v", err) - } - tlsConfig = cert.TLSConfFromCert(tlsCert) } // lnd's cipher suites are too restrictive for HTTP/2, we need to add diff --git a/doc/config-lnd-integrated.md b/doc/config-lnd-integrated.md index 076daa629..c80fdc26f 100644 --- a/doc/config-lnd-integrated.md +++ b/doc/config-lnd-integrated.md @@ -71,6 +71,8 @@ Example `~/.lit/lit.conf`: ```text # Application Options httpslisten=0.0.0.0:8443 +tlscertpath=~/.lit/tls.cert +tlskeypath=~/.lit/tls.key letsencrypt=true letsencrypthost=loop.merchant.com lnd-mode=integrated @@ -117,6 +119,15 @@ system: - **On Linux**: `~/.lit/lit.conf` - **On Windows**: `~/AppData/Roaming/Lit/lit.conf` +## LiT and LND interfaces + +Port 10009 is the port that LND uses to expose its gRPC interface. LND's tls +cert and macaroons will be required when making requests to this interface. + +Port 8443 is a port that LiT uses to expose a variety of interfaces: gRPC, +REST, grpc-web. When making requests using this interface, LiT's tls cert and +macaroons should be used. + ## Upgrade Existing Nodes If you already have existing `lnd`, `loop`, or `faraday` nodes, you can easily diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index cc7ac75cf..b30d093f5 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -291,7 +291,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runGRPCAuthTest( - ttt, cfg.LitAddr(), cfg.TLSCertPath, + ttt, cfg.LitAddr(), cfg.LitTLSCertPath, endpoint.macaroonFn(cfg), endpoint.requestFn, endpoint.successPattern, @@ -315,7 +315,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runUIPasswordCheck( - ttt, cfg.LitAddr(), cfg.TLSCertPath, + ttt, cfg.LitAddr(), cfg.LitTLSCertPath, cfg.UIPassword, endpoint.requestFn, false, endpoint.successPattern, ) @@ -364,7 +364,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runGRPCAuthTest( - ttt, cfg.LitAddr(), cfg.TLSCertPath, + ttt, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile, endpoint.requestFn, endpoint.successPattern, @@ -403,7 +403,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { defer cancel() rawLNCConn := setUpLNCConn( - ctxt, t.t, cfg.LitAddr(), cfg.TLSCertPath, + ctxt, t.t, cfg.LitAddr(), cfg.LitTLSCertPath, cfg.LitMacPath, litrpc.SessionType_TYPE_MACAROON_READONLY, nil, ) @@ -434,11 +434,11 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { ht := newHarnessTest(tt, net) runAccountSystemTest( - ht, net.Alice, cfg.RPCAddr(), cfg.TLSCertPath, + ht, net.Alice, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile, 1, ) runAccountSystemTest( - ht, net.Alice, cfg.LitAddr(), cfg.TLSCertPath, + ht, net.Alice, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile, 2, ) }) @@ -465,7 +465,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { } rawLNCConn := setUpLNCConn( - ctxt, t.t, cfg.LitAddr(), cfg.TLSCertPath, + ctxt, t.t, cfg.LitAddr(), cfg.LitTLSCertPath, cfg.LitMacPath, litrpc.SessionType_TYPE_MACAROON_CUSTOM, customPerms, ) @@ -525,14 +525,11 @@ func setUpLNCConn(ctx context.Context, t *testing.T, hostPort, tlsCertPath, // runCertificateCheck checks that the TLS certificates presented to clients are // what we expect them to be. func runCertificateCheck(t *testing.T, node *HarnessNode) { - // In integrated mode we expect the LiT HTTPS port (8443 by default) and - // lnd's RPC port to present the same certificate, namely lnd's TLS - // cert. litCerts, err := getServerCertificates(node.Cfg.LitAddr()) require.NoError(t, err) require.Len(t, litCerts, 1) require.Equal( - t, "lnd autogenerated cert", litCerts[0].Issuer.Organization[0], + t, "litd autogenerated cert", litCerts[0].Issuer.Organization[0], ) lndCerts, err := getServerCertificates(node.Cfg.RPCAddr()) @@ -541,8 +538,6 @@ func runCertificateCheck(t *testing.T, node *HarnessNode) { require.Equal( t, "lnd autogenerated cert", lndCerts[0].Issuer.Organization[0], ) - - require.Equal(t, litCerts[0].Raw, lndCerts[0].Raw) } // runGRPCAuthTest tests authentication of the given gRPC interface. diff --git a/itest/litd_node.go b/itest/litd_node.go index 8cb5b2cce..a0e555df1 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -24,6 +24,8 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/faraday/frdrpc" + terminal "github.com/lightninglabs/lightning-terminal" + "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/pool/poolrpc" "github.com/lightningnetwork/lnd/lnrpc" @@ -160,17 +162,18 @@ func (cfg *LitNodeConfig) GenArgs() []string { // policyUpdateMap defines a type to store channel policy updates. It has the // format, -// { -// "chanPoint1": { -// "advertisingNode1": [ -// policy1, policy2, ... -// ], -// "advertisingNode2": [ -// policy1, policy2, ... -// ] -// }, -// "chanPoint2": ... -// } +// +// { +// "chanPoint1": { +// "advertisingNode1": [ +// policy1, policy2, ... +// ], +// "advertisingNode2": [ +// policy1, policy2, ... +// ] +// }, +// "chanPoint2": ... +// } type policyUpdateMap map[string]map[string][]*lnrpc.RoutingPolicy // HarnessNode represents an instance of lnd running within our test network @@ -228,9 +231,13 @@ type HarnessNode struct { // methods SignMessage and VerifyMessage. SignerClient signrpc.SignerClient - // conn is the underlying connection to the grpc endpoint of the node. + // conn is the underlying connection to the lnd grpc endpoint of the + // node. conn *grpc.ClientConn + // litConn is the underlying connection to Lit's grpc endpoint. + litConn *grpc.ClientConn + // RouterClient, WalletKitClient, WatchtowerClient cannot be embedded, // because a name collision would occur with LightningClient. RouterClient routerrpc.RouterClient @@ -596,6 +603,16 @@ func (hn *HarnessNode) start(litdBinary string, litdError chan<- error, return nil } + // Also connect to Lit's RPC port for any Litd specific calls. + litConn, err := connectLitRPC( + context.Background(), hn.Cfg.LitAddr(), hn.Cfg.LitTLSCertPath, + hn.Cfg.LitMacPath, + ) + if err != nil { + return err + } + hn.litConn = litConn + return hn.initLightningClient(conn) } @@ -603,7 +620,37 @@ func (hn *HarnessNode) start(litdBinary string, litdError chan<- error, func (hn *HarnessNode) WaitUntilStarted(conn grpc.ClientConnInterface, timeout time.Duration) error { - err := hn.waitForState(conn, timeout, func(s lnrpc.WalletState) bool { + // First wait for Litd state server to show that LND has started. + ctx := context.Background() + rawConn, err := connectLitRPC( + ctx, hn.Cfg.LitAddr(), hn.Cfg.LitTLSCertPath, "", + ) + if err != nil { + return err + } + + litConn := litrpc.NewStatusClient(rawConn) + + err = wait.NoError(func() error { + states, err := litConn.SubServerState( + ctx, &litrpc.SubServerStatusReq{}, + ) + if err != nil { + return err + } + + lndStatus, ok := states.SubServers[terminal.LNDSubServer] + if !ok || !lndStatus.Running { + return fmt.Errorf("LND has not yet started") + } + + return nil + }, lntest.DefaultTimeout) + if err != nil { + return err + } + + err = hn.waitForState(conn, timeout, func(s lnrpc.WalletState) bool { return s >= lnrpc.WalletState_SERVER_ACTIVE }) if err != nil { @@ -1106,6 +1153,21 @@ func (hn *HarnessNode) stop() error { } } + // If lit is running in remote mode, then calling LNDs StopDaemon + // method will not shut down Lit, and so we need to explicitly request + // lit to shut down. + if hn.Cfg.RemoteMode { + ctx, cancel := context.WithTimeout( + context.Background(), lntest.DefaultTimeout, + ) + litConn := litrpc.NewLitServiceClient(hn.litConn) + _, err := litConn.StopDaemon(ctx, &litrpc.StopDaemonRequest{}) + cancel() + if err != nil { + return err + } + } + // Wait for lnd process and other goroutines to exit. select { case <-hn.processExit: @@ -1800,3 +1862,40 @@ func (hn *HarnessNode) getChannelPolicies(include bool) policyUpdateMap { return policyUpdates } + +// connectLigRPC can be used to connect to the lit rpc server. +func connectLitRPC(ctx context.Context, hostPort, tlsCertPath, + macPath string) (*grpc.ClientConn, error) { + + tlsCreds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") + if err != nil { + return nil, err + } + + opts := []grpc.DialOption{ + grpc.WithBlock(), + grpc.WithTransportCredentials(tlsCreds), + } + + if macPath != "" { + macBytes, err := ioutil.ReadFile(macPath) + if err != nil { + return nil, err + } + + mac := &macaroon.Macaroon{} + if err = mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("error unmarshalling macaroon "+ + "file: %v", err) + } + + macCred, err := macaroons.NewMacaroonCredential(mac) + if err != nil { + return nil, fmt.Errorf("error cloning mac: %v", err) + } + + opts = append(opts, grpc.WithPerRPCCredentials(macCred)) + } + + return grpc.DialContext(ctx, hostPort, opts...) +} diff --git a/litrpc/lit-status.pb.go b/litrpc/lit-status.pb.go new file mode 100644 index 000000000..b50a607d4 --- /dev/null +++ b/litrpc/lit-status.pb.go @@ -0,0 +1,293 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: lit-status.proto + +package litrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SubServerStatusReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SubServerStatusReq) Reset() { + *x = SubServerStatusReq{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_status_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubServerStatusReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubServerStatusReq) ProtoMessage() {} + +func (x *SubServerStatusReq) ProtoReflect() protoreflect.Message { + mi := &file_lit_status_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubServerStatusReq.ProtoReflect.Descriptor instead. +func (*SubServerStatusReq) Descriptor() ([]byte, []int) { + return file_lit_status_proto_rawDescGZIP(), []int{0} +} + +type SubServerStatusResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // A map of sub-server names to their status. + SubServers map[string]*SubServerStatus `protobuf:"bytes,1,rep,name=sub_servers,json=subServers,proto3" json:"sub_servers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *SubServerStatusResp) Reset() { + *x = SubServerStatusResp{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_status_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubServerStatusResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubServerStatusResp) ProtoMessage() {} + +func (x *SubServerStatusResp) ProtoReflect() protoreflect.Message { + mi := &file_lit_status_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubServerStatusResp.ProtoReflect.Descriptor instead. +func (*SubServerStatusResp) Descriptor() ([]byte, []int) { + return file_lit_status_proto_rawDescGZIP(), []int{1} +} + +func (x *SubServerStatusResp) GetSubServers() map[string]*SubServerStatus { + if x != nil { + return x.SubServers + } + return nil +} + +type SubServerStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // running is true if the sub-server is currently running. + Running bool `protobuf:"varint,1,opt,name=running,proto3" json:"running,omitempty"` + // error describes an error that might have resulted in the sub-server not + // starting up properly. + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *SubServerStatus) Reset() { + *x = SubServerStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_status_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubServerStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubServerStatus) ProtoMessage() {} + +func (x *SubServerStatus) ProtoReflect() protoreflect.Message { + mi := &file_lit_status_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubServerStatus.ProtoReflect.Descriptor instead. +func (*SubServerStatus) Descriptor() ([]byte, []int) { + return file_lit_status_proto_rawDescGZIP(), []int{2} +} + +func (x *SubServerStatus) GetRunning() bool { + if x != nil { + return x.Running + } + return false +} + +func (x *SubServerStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_lit_status_proto protoreflect.FileDescriptor + +var file_lit_status_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x6c, 0x69, 0x74, 0x2d, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x75, + 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, + 0x22, 0xbb, 0x01, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x4c, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x2e, 0x53, 0x75, 0x62, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x73, 0x75, 0x62, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x1a, 0x56, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x41, + 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x32, 0x53, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x49, 0x0a, 0x0e, 0x53, + 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, + 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_lit_status_proto_rawDescOnce sync.Once + file_lit_status_proto_rawDescData = file_lit_status_proto_rawDesc +) + +func file_lit_status_proto_rawDescGZIP() []byte { + file_lit_status_proto_rawDescOnce.Do(func() { + file_lit_status_proto_rawDescData = protoimpl.X.CompressGZIP(file_lit_status_proto_rawDescData) + }) + return file_lit_status_proto_rawDescData +} + +var file_lit_status_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_lit_status_proto_goTypes = []interface{}{ + (*SubServerStatusReq)(nil), // 0: litrpc.SubServerStatusReq + (*SubServerStatusResp)(nil), // 1: litrpc.SubServerStatusResp + (*SubServerStatus)(nil), // 2: litrpc.SubServerStatus + nil, // 3: litrpc.SubServerStatusResp.SubServersEntry +} +var file_lit_status_proto_depIdxs = []int32{ + 3, // 0: litrpc.SubServerStatusResp.sub_servers:type_name -> litrpc.SubServerStatusResp.SubServersEntry + 2, // 1: litrpc.SubServerStatusResp.SubServersEntry.value:type_name -> litrpc.SubServerStatus + 0, // 2: litrpc.Status.SubServerState:input_type -> litrpc.SubServerStatusReq + 1, // 3: litrpc.Status.SubServerState:output_type -> litrpc.SubServerStatusResp + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_lit_status_proto_init() } +func file_lit_status_proto_init() { + if File_lit_status_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_lit_status_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubServerStatusReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_status_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubServerStatusResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_status_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubServerStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_lit_status_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lit_status_proto_goTypes, + DependencyIndexes: file_lit_status_proto_depIdxs, + MessageInfos: file_lit_status_proto_msgTypes, + }.Build() + File_lit_status_proto = out.File + file_lit_status_proto_rawDesc = nil + file_lit_status_proto_goTypes = nil + file_lit_status_proto_depIdxs = nil +} diff --git a/litrpc/lit-status.proto b/litrpc/lit-status.proto new file mode 100644 index 000000000..eb39f6221 --- /dev/null +++ b/litrpc/lit-status.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package litrpc; + +option go_package = "github.com/lightninglabs/lightning-terminal/litrpc"; + +// The Status server can be used to query the state of various LiT sub-servers. +service Status { + rpc SubServerState (SubServerStatusReq) returns (SubServerStatusResp); +} + +message SubServerStatusReq { +} + +message SubServerStatusResp { + // A map of sub-server names to their status. + map sub_servers = 1; +} + +message SubServerStatus { + // running is true if the sub-server is currently running. + bool running = 1; + + // error describes an error that might have resulted in the sub-server not + // starting up properly. + string error = 2; +} \ No newline at end of file diff --git a/litrpc/lit-status_grpc.pb.go b/litrpc/lit-status_grpc.pb.go new file mode 100644 index 000000000..388c2b5c4 --- /dev/null +++ b/litrpc/lit-status_grpc.pb.go @@ -0,0 +1,101 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package litrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// StatusClient is the client API for Status service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StatusClient interface { + SubServerState(ctx context.Context, in *SubServerStatusReq, opts ...grpc.CallOption) (*SubServerStatusResp, error) +} + +type statusClient struct { + cc grpc.ClientConnInterface +} + +func NewStatusClient(cc grpc.ClientConnInterface) StatusClient { + return &statusClient{cc} +} + +func (c *statusClient) SubServerState(ctx context.Context, in *SubServerStatusReq, opts ...grpc.CallOption) (*SubServerStatusResp, error) { + out := new(SubServerStatusResp) + err := c.cc.Invoke(ctx, "/litrpc.Status/SubServerState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StatusServer is the server API for Status service. +// All implementations must embed UnimplementedStatusServer +// for forward compatibility +type StatusServer interface { + SubServerState(context.Context, *SubServerStatusReq) (*SubServerStatusResp, error) + mustEmbedUnimplementedStatusServer() +} + +// UnimplementedStatusServer must be embedded to have forward compatible implementations. +type UnimplementedStatusServer struct { +} + +func (UnimplementedStatusServer) SubServerState(context.Context, *SubServerStatusReq) (*SubServerStatusResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method SubServerState not implemented") +} +func (UnimplementedStatusServer) mustEmbedUnimplementedStatusServer() {} + +// UnsafeStatusServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StatusServer will +// result in compilation errors. +type UnsafeStatusServer interface { + mustEmbedUnimplementedStatusServer() +} + +func RegisterStatusServer(s grpc.ServiceRegistrar, srv StatusServer) { + s.RegisterService(&Status_ServiceDesc, srv) +} + +func _Status_SubServerState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubServerStatusReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatusServer).SubServerState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Status/SubServerState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatusServer).SubServerState(ctx, req.(*SubServerStatusReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Status_ServiceDesc is the grpc.ServiceDesc for Status service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Status_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "litrpc.Status", + HandlerType: (*StatusServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SubServerState", + Handler: _Status_SubServerState_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lit-status.proto", +} diff --git a/litrpc/lit.pb.go b/litrpc/lit.pb.go new file mode 100644 index 000000000..f3d86c606 --- /dev/null +++ b/litrpc/lit.pb.go @@ -0,0 +1,193 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: lit.proto + +package litrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StopDaemonRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StopDaemonRequest) Reset() { + *x = StopDaemonRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopDaemonRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopDaemonRequest) ProtoMessage() {} + +func (x *StopDaemonRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopDaemonRequest.ProtoReflect.Descriptor instead. +func (*StopDaemonRequest) Descriptor() ([]byte, []int) { + return file_lit_proto_rawDescGZIP(), []int{0} +} + +type StopDaemonResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StopDaemonResponse) Reset() { + *x = StopDaemonResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopDaemonResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopDaemonResponse) ProtoMessage() {} + +func (x *StopDaemonResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopDaemonResponse.ProtoReflect.Descriptor instead. +func (*StopDaemonResponse) Descriptor() ([]byte, []int) { + return file_lit_proto_rawDescGZIP(), []int{1} +} + +var File_lit_proto protoreflect.FileDescriptor + +var file_lit_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x6c, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x22, 0x13, 0x0a, 0x11, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, 0x70, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x51, + 0x0a, 0x0a, 0x4c, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0a, + 0x53, 0x74, 0x6f, 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x19, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x74, 0x6f, 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, + 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, + 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_lit_proto_rawDescOnce sync.Once + file_lit_proto_rawDescData = file_lit_proto_rawDesc +) + +func file_lit_proto_rawDescGZIP() []byte { + file_lit_proto_rawDescOnce.Do(func() { + file_lit_proto_rawDescData = protoimpl.X.CompressGZIP(file_lit_proto_rawDescData) + }) + return file_lit_proto_rawDescData +} + +var file_lit_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_lit_proto_goTypes = []interface{}{ + (*StopDaemonRequest)(nil), // 0: litrpc.StopDaemonRequest + (*StopDaemonResponse)(nil), // 1: litrpc.StopDaemonResponse +} +var file_lit_proto_depIdxs = []int32{ + 0, // 0: litrpc.LitService.StopDaemon:input_type -> litrpc.StopDaemonRequest + 1, // 1: litrpc.LitService.StopDaemon:output_type -> litrpc.StopDaemonResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_lit_proto_init() } +func file_lit_proto_init() { + if File_lit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_lit_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopDaemonRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopDaemonResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_lit_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lit_proto_goTypes, + DependencyIndexes: file_lit_proto_depIdxs, + MessageInfos: file_lit_proto_msgTypes, + }.Build() + File_lit_proto = out.File + file_lit_proto_rawDesc = nil + file_lit_proto_goTypes = nil + file_lit_proto_depIdxs = nil +} diff --git a/litrpc/lit.proto b/litrpc/lit.proto new file mode 100644 index 000000000..73a55d5cc --- /dev/null +++ b/litrpc/lit.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package litrpc; + +option go_package = "github.com/lightninglabs/lightning-terminal/litrpc"; + +service LitService { + rpc StopDaemon (StopDaemonRequest) returns (StopDaemonResponse); +} + +message StopDaemonRequest { +} + +message StopDaemonResponse { +} \ No newline at end of file diff --git a/litrpc/lit_grpc.pb.go b/litrpc/lit_grpc.pb.go new file mode 100644 index 000000000..48fdbe609 --- /dev/null +++ b/litrpc/lit_grpc.pb.go @@ -0,0 +1,101 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package litrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LitServiceClient is the client API for LitService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LitServiceClient interface { + StopDaemon(ctx context.Context, in *StopDaemonRequest, opts ...grpc.CallOption) (*StopDaemonResponse, error) +} + +type litServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLitServiceClient(cc grpc.ClientConnInterface) LitServiceClient { + return &litServiceClient{cc} +} + +func (c *litServiceClient) StopDaemon(ctx context.Context, in *StopDaemonRequest, opts ...grpc.CallOption) (*StopDaemonResponse, error) { + out := new(StopDaemonResponse) + err := c.cc.Invoke(ctx, "/litrpc.LitService/StopDaemon", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LitServiceServer is the server API for LitService service. +// All implementations must embed UnimplementedLitServiceServer +// for forward compatibility +type LitServiceServer interface { + StopDaemon(context.Context, *StopDaemonRequest) (*StopDaemonResponse, error) + mustEmbedUnimplementedLitServiceServer() +} + +// UnimplementedLitServiceServer must be embedded to have forward compatible implementations. +type UnimplementedLitServiceServer struct { +} + +func (UnimplementedLitServiceServer) StopDaemon(context.Context, *StopDaemonRequest) (*StopDaemonResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method StopDaemon not implemented") +} +func (UnimplementedLitServiceServer) mustEmbedUnimplementedLitServiceServer() {} + +// UnsafeLitServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LitServiceServer will +// result in compilation errors. +type UnsafeLitServiceServer interface { + mustEmbedUnimplementedLitServiceServer() +} + +func RegisterLitServiceServer(s grpc.ServiceRegistrar, srv LitServiceServer) { + s.RegisterService(&LitService_ServiceDesc, srv) +} + +func _LitService_StopDaemon_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StopDaemonRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LitServiceServer).StopDaemon(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.LitService/StopDaemon", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LitServiceServer).StopDaemon(ctx, req.(*StopDaemonRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// LitService_ServiceDesc is the grpc.ServiceDesc for LitService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LitService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "litrpc.LitService", + HandlerType: (*LitServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "StopDaemon", + Handler: _LitService_StopDaemon_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lit.proto", +} diff --git a/litrpc/litservice.pb.json.go b/litrpc/litservice.pb.json.go new file mode 100644 index 000000000..3eee216fa --- /dev/null +++ b/litrpc/litservice.pb.json.go @@ -0,0 +1,48 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: lit.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterLitServiceJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.LitService.StopDaemon"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &StopDaemonRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewLitServiceClient(conn) + resp, err := client.StopDaemon(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/litrpc/status.pb.json.go b/litrpc/status.pb.json.go new file mode 100644 index 000000000..5fc62744b --- /dev/null +++ b/litrpc/status.pb.json.go @@ -0,0 +1,48 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: lit-status.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterStatusJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.Status.SubServerState"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &SubServerStatusReq{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewStatusClient(conn) + resp, err := client.SubServerState(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/lnd_connection.go b/lnd_connection.go new file mode 100644 index 000000000..0ad9ca6cd --- /dev/null +++ b/lnd_connection.go @@ -0,0 +1,85 @@ +package terminal + +import ( + "context" + "crypto/tls" + "fmt" + "net" + + grpcProxy "github.com/mwitkow/grpc-proxy/proxy" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/test/bufconn" +) + +// connectLND sets up LiT's LND connection. +func connectLND(cfg *Config, bufListener *bufconn.Listener) (*grpc.ClientConn, + error) { + + if cfg.lndRemote { + host, _, tlsPath, _, _ := cfg.lndConnectParams() + return dialBackend("lnd", host, tlsPath) + } + + // If LND is running in integrated mode, then we use a bufconn to + // connect to lnd in integrated mode. + return dialBufConnBackend(bufListener) +} + +// 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) { + tlsConfig, err := credentials.NewClientTLSFromFile(tlsCertPath, "") + if err != nil { + return nil, fmt.Errorf("could not read %s TLS cert %s: %v", + name, tlsCertPath, err) + } + + opts := []grpc.DialOption{ + // 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, + }), + } + + log.Infof("Dialing %s gRPC server at %s", name, dialAddr) + cc, err := grpc.Dial(dialAddr, opts...) + if err != nil { + return nil, fmt.Errorf("failed dialing %s backend: %v", name, + err) + } + return cc, 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, + }) + + opts := []grpc.DialOption{ + grpc.WithContextDialer( + func(context.Context, string) (net.Conn, error) { + return listener.Dial() + }, + ), + // 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 grpc.Dial("", opts...) +} diff --git a/perms/permissions.go b/perms/permissions.go index 9fb8727f4..b072e684a 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -86,6 +86,18 @@ var ( Entity: "privacymap", Action: "read", }}, + "/litrpc.LitService/StopDaemon": {{ + Entity: "litd", + Action: "write", + }}, + } + + // MacaroonWhitelist defines methods that we don't require macaroons to + // access. + MacaroonWhitelist = map[string][]bakery.Op{ + // The Status service must be available at all times, even + // before we can check macaroons, so we whitelist it. + "/litrpc.Status/SubServerState": {}, } // whiteListedLNDMethods is a map of all lnd RPC methods that don't @@ -123,15 +135,15 @@ var ( } ) -// subServerName is a name used to identify a particular Lit sub-server. -type subServerName string +// SubServerName is a name used to identify a particular Lit sub-server. +type SubServerName string const ( - poolPerms subServerName = "pool" - loopPerms subServerName = "loop" - faradayPerms subServerName = "faraday" - litPerms subServerName = "lit" - lndPerms subServerName = "lnd" + SubServerPool SubServerName = "pool" + SubServerLoop SubServerName = "loop" + SubServerFaraday SubServerName = "faraday" + SubServerLit SubServerName = "lit" + SubServerLnd SubServerName = "lnd" ) // Manager manages the permission lists that Lit requires. @@ -146,7 +158,7 @@ type Manager struct { // It contains all the permissions that will not change throughout the // lifetime of the manager. It maps sub-server name to uri to permission // operations. - fixedPerms map[subServerName]map[string][]bakery.Op + fixedPerms map[SubServerName]map[string][]bakery.Op // perms is a map containing all permissions that the manager knows // are available for use. This map will start out not including any of @@ -165,14 +177,17 @@ type Manager struct { // then OnLNDBuildTags can be used to specify the exact sub-servers that LND // was compiled with and then only the corresponding permissions will be added. func NewManager(withAllSubServers bool) (*Manager, error) { - permissions := make(map[subServerName]map[string][]bakery.Op) - permissions[faradayPerms] = faraday.RequiredPermissions - permissions[loopPerms] = loop.RequiredPermissions - permissions[poolPerms] = pool.RequiredPermissions - permissions[litPerms] = LitPermissions - permissions[lndPerms] = lnd.MainRPCServerPermissions() + permissions := make(map[SubServerName]map[string][]bakery.Op) + permissions[SubServerFaraday] = faraday.RequiredPermissions + permissions[SubServerLoop] = loop.RequiredPermissions + permissions[SubServerPool] = pool.RequiredPermissions + permissions[SubServerLit] = LitPermissions + for k, v := range MacaroonWhitelist { + permissions[SubServerLit][k] = v + } + permissions[SubServerLnd] = lnd.MainRPCServerPermissions() for k, v := range whiteListedLNDMethods { - permissions[lndPerms][k] = v + permissions[SubServerLnd][k] = v } // Collect all LND sub-server permissions along with the name of the @@ -199,7 +214,7 @@ func NewManager(withAllSubServers bool) (*Manager, error) { if withAllSubServers || lndAutoCompiledSubServers[name] { - permissions[lndPerms][key] = value + permissions[SubServerLnd][key] = value } } } @@ -337,28 +352,39 @@ func (pm *Manager) ActivePermissions(readOnly bool) []bakery.Op { // _except_ for any LND permissions. In other words, this returns permissions // for which the external validator of Lit is responsible. func (pm *Manager) GetLitPerms() map[string][]bakery.Op { - mapSize := len(pm.fixedPerms[litPerms]) + - len(pm.fixedPerms[faradayPerms]) + - len(pm.fixedPerms[loopPerms]) + len(pm.fixedPerms[poolPerms]) + mapSize := len(pm.fixedPerms[SubServerLit]) + + len(pm.fixedPerms[SubServerFaraday]) + + len(pm.fixedPerms[SubServerPool]) + + len(pm.fixedPerms[SubServerLoop]) result := make(map[string][]bakery.Op, mapSize) - for key, value := range pm.fixedPerms[faradayPerms] { + for key, value := range pm.fixedPerms[SubServerFaraday] { result[key] = value } - for key, value := range pm.fixedPerms[loopPerms] { + for key, value := range pm.fixedPerms[SubServerLoop] { result[key] = value } - for key, value := range pm.fixedPerms[poolPerms] { + for key, value := range pm.fixedPerms[SubServerPool] { result[key] = value } - for key, value := range pm.fixedPerms[litPerms] { + for key, value := range pm.fixedPerms[SubServerLit] { result[key] = value } return result } -// IsLndURI returns true if the given URI belongs to an RPC of lnd. -func (pm *Manager) IsLndURI(uri string) bool { +// IsSubServerURI if the given URI belongs to the RPC of the given server. +func (pm *Manager) IsSubServerURI(name SubServerName, uri string) bool { + if name == SubServerLnd { + return pm.isLndURI(uri) + } + + _, ok := pm.fixedPerms[name][uri] + return ok +} + +// isLndURI returns true if the given URI belongs to an RPC of lnd. +func (pm *Manager) isLndURI(uri string) bool { var lndSubServerCall bool for _, subserverPermissions := range pm.lndSubServerPerms { _, found := subserverPermissions[uri] @@ -367,34 +393,10 @@ func (pm *Manager) IsLndURI(uri string) bool { break } } - _, lndCall := pm.fixedPerms[lndPerms][uri] + _, lndCall := pm.fixedPerms[SubServerLnd][uri] return lndCall || lndSubServerCall } -// IsLoopURI returns true if the given URI belongs to an RPC of loopd. -func (pm *Manager) IsLoopURI(uri string) bool { - _, ok := pm.fixedPerms[loopPerms][uri] - return ok -} - -// IsFaradayURI returns true if the given URI belongs to an RPC of faraday. -func (pm *Manager) IsFaradayURI(uri string) bool { - _, ok := pm.fixedPerms[faradayPerms][uri] - return ok -} - -// IsPoolURI returns true if the given URI belongs to an RPC of poold. -func (pm *Manager) IsPoolURI(uri string) bool { - _, ok := pm.fixedPerms[poolPerms][uri] - return ok -} - -// IsLitURI returns true if the given URI belongs to an RPC of LiT. -func (pm *Manager) IsLitURI(uri string) bool { - _, ok := pm.fixedPerms[litPerms][uri] - return ok -} - // mockConfig implements lnrpc.SubServerConfigDispatcher. It provides the // functionality required so that the lnrpc.GrpcHandler.CreateSubServer // function can be called without panicking. diff --git a/rpc_proxy.go b/rpc_proxy.go index d4bd45788..2dca09169 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -2,29 +2,26 @@ package terminal import ( "context" - "crypto/tls" "encoding/base64" "encoding/hex" "fmt" "io/ioutil" - "net" "net/http" "strings" + "sync/atomic" "time" "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/macaroons" grpcProxy "github.com/mwitkow/grpc-proxy/proxy" "google.golang.org/grpc" - "google.golang.org/grpc/backoff" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "google.golang.org/grpc/test/bufconn" "gopkg.in/macaroon.v2" ) @@ -36,6 +33,10 @@ const ( HeaderMacaroon = "Macaroon" ) +// ErrWaitingToStart is returned if Lit's rpcProxy is not yet ready to handle +// calls. +var ErrWaitingToStart = fmt.Errorf("waiting for the RPC server to start") + // proxyErr is an error type that adds more context to an error occurring in the // proxy. type proxyErr struct { @@ -59,7 +60,8 @@ func (e *proxyErr) Unwrap() error { // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, superMacValidator session.SuperMacaroonValidator, - permsMgr *perms.Manager, bufListener *bufconn.Listener) *rpcProxy { + permsMgr *perms.Manager, statusServer *statusServer, + subServerMgr *subServerMgr) *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 @@ -79,7 +81,8 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, permsMgr: permsMgr, macValidator: validator, superMacValidator: superMacValidator, - bufListener: bufListener, + statusServer: statusServer, + subServerMgr: subServerMgr, } p.grpcServer = grpc.NewServer( // From the grpxProxy doc: This codec is *crucial* to the @@ -111,147 +114,113 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, // it is handled there. If not, the director will forward the call to either a // local or remote lnd instance. // -// any RPC or grpc-web call -// | -// V -// +---+----------------------+ -// | grpc-web proxy | -// +---+----------------------+ -// | -// 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 | -// +---------------------+ -// +// any RPC or grpc-web call +// | +// V +// +---+----------------------+ +// | grpc-web proxy | +// +---+----------------------+ +// | +// 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 | +// +---------------------+ type rpcProxy struct { + // started is set to 1 once the rpcProxy has successfully started. It + // must only ever be used atomically. + started int32 + + litrpc.UnimplementedLitServiceServer + cfg *Config basicAuth string permsMgr *perms.Manager macValidator macaroons.MacaroonValidator superMacValidator session.SuperMacaroonValidator - bufListener *bufconn.Listener superMacaroon string - lndConn *grpc.ClientConn - faradayConn *grpc.ClientConn - loopConn *grpc.ClientConn - poolConn *grpc.ClientConn + lndConn *grpc.ClientConn + + statusServer *statusServer + subServerMgr *subServerMgr grpcServer *grpc.Server grpcWebProxy *grpcweb.WrappedGrpcServer } // Start creates initial connection to lnd. -func (p *rpcProxy) Start() error { - var err error +func (p *rpcProxy) Start(lndConn *grpc.ClientConn) error { + p.lndConn = lndConn - // Setup the connection to lnd. - host, _, tlsPath, _, _ := p.cfg.lndConnectParams() + atomic.CompareAndSwapInt32(&p.started, 0, 1) - // 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) - } - - // Make sure we can connect to all the daemons that are configured to be - // running in remote mode. - if p.cfg.faradayRemote { - p.faradayConn, err = dialBackend( - "faraday", p.cfg.Remote.Faraday.RPCServer, - lncfg.CleanAndExpandPath( - p.cfg.Remote.Faraday.TLSCertPath, - ), - ) - if err != nil { - return fmt.Errorf("could not dial remote faraday: %v", - err) - } - } + return nil +} - if p.cfg.loopRemote { - p.loopConn, err = dialBackend( - "loop", p.cfg.Remote.Loop.RPCServer, - lncfg.CleanAndExpandPath(p.cfg.Remote.Loop.TLSCertPath), - ) - if err != nil { - return fmt.Errorf("could not dial remote loop: %v", err) - } - } +// hasStarted returns true if the rpcProxy has started and is ready to handle +// requests. +func (p *rpcProxy) hasStarted() bool { + return atomic.LoadInt32(&p.started) == 1 +} - if p.cfg.poolRemote { - p.poolConn, err = dialBackend( - "pool", p.cfg.Remote.Pool.RPCServer, - lncfg.CleanAndExpandPath(p.cfg.Remote.Pool.TLSCertPath), - ) - if err != nil { - return fmt.Errorf("could not dial remote pool: %v", err) - } - } +// isStatusReq returns true if the given request is intended for the +// litrpc.Status service. +func isStatusReq(uri string) bool { + return strings.HasPrefix( + uri, fmt.Sprintf("/%s", litrpc.Status_ServiceDesc.ServiceName), + ) +} - return nil +// isShutdownReq returns true if the given request is a Litd shutdown request. +func isShutdownReq(uri string) bool { + return strings.HasPrefix( + uri, fmt.Sprintf( + "/%s/%s", litrpc.LitService_ServiceDesc.ServiceName, + "StopDaemon", + ), + ) } // Stop shuts down the lnd connection. func (p *rpcProxy) Stop() error { p.grpcServer.Stop() - if p.lndConn != nil { - if err := p.lndConn.Close(); err != nil { - log.Errorf("Error closing lnd connection: %v", err) - return err - } - } - - if p.faradayConn != nil { - if err := p.faradayConn.Close(); err != nil { - log.Errorf("Error closing faraday connection: %v", err) - return err - } - } + return nil +} - if p.loopConn != nil { - if err := p.loopConn.Close(); err != nil { - log.Errorf("Error closing loop connection: %v", err) - return err - } - } +// StopDaemon will send a shutdown request to the interrupt handler, triggering +// a graceful shutdown of the daemon. +// +// NOTE: this is part of the litrpc.LitServiceServer interface. +func (p *rpcProxy) StopDaemon(_ context.Context, + _ *litrpc.StopDaemonRequest) (*litrpc.StopDaemonResponse, error) { - if p.poolConn != nil { - if err := p.poolConn.Close(); err != nil { - log.Errorf("Error closing pool connection: %v", err) - return err - } - } + interceptor.RequestShutdown() - return nil + return &litrpc.StopDaemonResponse{}, nil } // isHandling checks if the specified request is something to be handled by lnd @@ -302,6 +271,12 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, outCtx := metadata.NewOutgoingContext(ctx, mdCopy) + // Check that the target subsystem is running. + err := p.checkSubSystemStarted(requestURI) + if err != nil { + return nil, nil, err + } + // Is there a basic auth or super macaroon set? authHeaders := md.Get("authorization") macHeader := md.Get(HeaderMacaroon) @@ -336,6 +311,8 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, } } + isSubServerURI := p.permsMgr.IsSubServerURI + // Direct the call to the correct backend. All gRPC calls end up // here since our gRPC server instance doesn't have any handlers // registered itself. So all daemon calls that are remote are @@ -343,26 +320,22 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, // since it must either be an lnd call or something that'll be // handled by the integrated daemons that are hooking into lnd's // gRPC server. - switch { - case p.permsMgr.IsFaradayURI(requestURI) && p.cfg.faradayRemote: - return outCtx, p.faradayConn, nil - - case p.permsMgr.IsLoopURI(requestURI) && p.cfg.loopRemote: - return outCtx, p.loopConn, nil - - case p.permsMgr.IsPoolURI(requestURI) && p.cfg.poolRemote: - return outCtx, p.poolConn, nil + handled, conn := p.subServerMgr.GetRemoteConn(requestURI) + if handled { + return outCtx, conn, nil + } // Calls to LiT session RPC aren't allowed in some cases. - case p.permsMgr.IsLitURI(requestURI) && !allowLitRPC: + if isSubServerURI(perms.SubServerLit, requestURI) && + !allowLitRPC { + return outCtx, nil, status.Errorf( codes.Unimplemented, "unknown service %s", requestURI, ) - - default: - return outCtx, p.lndConn, nil } + + return outCtx, p.lndConn, nil } } @@ -372,12 +345,22 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if !p.hasStarted() && !isStatusReq(info.FullMethod) { + return nil, ErrWaitingToStart + } + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return nil, fmt.Errorf("%s: unknown permissions "+ "required for method", info.FullMethod) } + // Check that the target subsystem is running. + err := p.checkSubSystemStarted(info.FullMethod) + if err != nil { + return nil, err + } + // For now, basic authentication is just a quick fix until we // have proper macaroon support implemented in the UI. We allow // gRPC web requests to have it and "convert" the auth into a @@ -407,18 +390,63 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, return handler(ctx, req) } +// checkSubSystemStarted checks if the subsystem responsible for handling the +// given URI has started. +func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { + var system string + + handled, subServerName := p.subServerMgr.HandledBy(requestURI) + switch { + case handled: + system = subServerName + + case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): + system = LNDSubServer + + case p.permsMgr.IsSubServerURI(perms.SubServerLit, requestURI): + system = LitdSubServer + + // If the request is for the status server, then we allow the + // request even if Lit has not properly started. We also allow + // the request through if it is a Litd shutdown. + if isStatusReq(requestURI) || isShutdownReq(requestURI) { + return nil + } + + default: + return fmt.Errorf("unknown gRPC web request: %v", requestURI) + } + + started, startErr := p.statusServer.getSubServerState(system) + if !started { + return fmt.Errorf("%s is not running: %s", system, startErr) + } + + return nil +} + // StreamServerInterceptor is a GRPC interceptor that checks whether the // request is authorized by the included macaroons. func (p *rpcProxy) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if !p.hasStarted() && !isStatusReq(info.FullMethod) { + return ErrWaitingToStart + } + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return fmt.Errorf("%s: unknown permissions required "+ "for method", info.FullMethod) } + // Check that the target subsystem is running. + err := p.checkSubSystemStarted(info.FullMethod) + if err != nil { + return err + } + // For now, basic authentication is just a quick fix until we // have proper macaroon support implemented in the UI. We allow // gRPC web requests to have it and "convert" the auth into a @@ -498,35 +526,18 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, } var ( - macPath string - macData []byte + handled, path = p.subServerMgr.MacaroonPath(requestURI) + macPath string + macData []byte ) switch { - case p.permsMgr.IsLndURI(requestURI): - _, _, _, macPath, macData = p.cfg.lndConnectParams() + case handled: + macPath = path - case p.permsMgr.IsFaradayURI(requestURI): - if p.cfg.faradayRemote { - macPath = p.cfg.Remote.Faraday.MacaroonPath - } else { - macPath = p.cfg.Faraday.MacaroonPath - } - - case p.permsMgr.IsLoopURI(requestURI): - if p.cfg.loopRemote { - macPath = p.cfg.Remote.Loop.MacaroonPath - } else { - macPath = p.cfg.Loop.MacaroonPath - } - - case p.permsMgr.IsPoolURI(requestURI): - if p.cfg.poolRemote { - macPath = p.cfg.Remote.Pool.MacaroonPath - } else { - macPath = p.cfg.Pool.MacaroonPath - } + case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): + _, _, _, macPath, macData = p.cfg.lndConnectParams() - case p.permsMgr.IsLitURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerLit, requestURI): macPath = p.cfg.MacaroonPath default: @@ -603,88 +614,14 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, // Is this actually a request that goes to a daemon that is running // remotely? - switch { - case p.permsMgr.IsFaradayURI(fullMethod) && p.cfg.faradayRemote: - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Faraday.MacaroonPath, - )) - - case p.permsMgr.IsLoopURI(fullMethod) && p.cfg.loopRemote: - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Loop.MacaroonPath, - )) - - case p.permsMgr.IsPoolURI(fullMethod) && p.cfg.poolRemote: - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Pool.MacaroonPath, - )) + handled, macBytes, err := p.subServerMgr.ReadRemoteMacaroon(fullMethod) + if handled { + return macBytes, err } return nil, 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) { - var opts []grpc.DialOption - tlsConfig, err := credentials.NewClientTLSFromFile(tlsCertPath, "") - if err != nil { - return nil, fmt.Errorf("could not read %s TLS cert %s: %v", - name, tlsCertPath, err) - } - - opts = append( - opts, - - // 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, - }), - ) - - log.Infof("Dialing %s gRPC server at %s", name, dialAddr) - cc, err := grpc.Dial(dialAddr, opts...) - if err != nil { - return nil, fmt.Errorf("failed dialing %s backend: %v", name, - err) - } - return cc, nil -} - // readMacaroon tries to read the macaroon file at the specified path and create // gRPC dial options from it. func readMacaroon(macPath string) ([]byte, error) { diff --git a/status_server.go b/status_server.go new file mode 100644 index 000000000..1f9e2d826 --- /dev/null +++ b/status_server.go @@ -0,0 +1,125 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + + "github.com/lightninglabs/lightning-terminal/litrpc" +) + +const ( + LNDSubServer = "lnd" + LitdSubServer = "litd" +) + +// statusServer is an implementation of the litrpc.StatusServer which can be +// queried for the status of various LiT sub-servers. +type statusServer struct { + litrpc.UnimplementedStatusServer + + subServers map[string]*subServerStatus + mu sync.RWMutex +} + +// subServerStatus represents the status of a sub-server. +type subServerStatus struct { + running bool + err string +} + +// newSubServerStatus constructs a new subServerStatus. +func newSubServerStatus() *subServerStatus { + return &subServerStatus{} +} + +// newStatusServer constructs a new statusServer. +func newStatusServer() *statusServer { + return &statusServer{ + subServers: map[string]*subServerStatus{}, + } +} + +// RegisterSubServer will create a new sub-server entry for the statusServer to +// keep track of. +func (s *statusServer) RegisterSubServer(name string) { + s.mu.RLock() + defer s.mu.RUnlock() + + s.subServers[name] = newSubServerStatus() +} + +// SubServerState queries the current status of a given sub-server. +// +// NOTE: this is part of the litrpc.StatusServer interface. +func (s *statusServer) SubServerState(_ context.Context, + _ *litrpc.SubServerStatusReq) (*litrpc.SubServerStatusResp, + error) { + + s.mu.RLock() + defer s.mu.RUnlock() + + resp := make(map[string]*litrpc.SubServerStatus, len(s.subServers)) + for server, status := range s.subServers { + resp[server] = &litrpc.SubServerStatus{ + Running: status.running, + Error: status.err, + } + } + + return &litrpc.SubServerStatusResp{ + SubServers: resp, + }, nil +} + +// getSubServerState queries the current status of a given sub-server. +func (s *statusServer) getSubServerState(name string) (bool, string) { + s.mu.RLock() + defer s.mu.RUnlock() + + system, ok := s.subServers[name] + if !ok { + return false, "" + } + + return system.running, system.err +} + +// setServerRunning can be used to set the status of a sub-server as running +// with no errors. +func (s *statusServer) setServerRunning(name string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.subServers[name] = &subServerStatus{ + running: true, + } +} + +// setServerStopped can be used to set the status of a sub-server as not running +// and with no errors. +func (s *statusServer) setServerStopped(name string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.subServers[name] = &subServerStatus{ + running: false, + } +} + +// setServerErrored can be used to set the status of a sub-server as not running +// and also to set an error message for the sub-server. +func (s *statusServer) setServerErrored(name string, errStr string, + params ...interface{}) { + + s.mu.Lock() + defer s.mu.Unlock() + + err := fmt.Sprintf(errStr, params...) + log.Errorf("could not start the %s sub-server: %s", name, err) + + s.subServers[name] = &subServerStatus{ + running: false, + err: err, + } +} diff --git a/subserver_mgr.go b/subserver_mgr.go new file mode 100644 index 000000000..72489a743 --- /dev/null +++ b/subserver_mgr.go @@ -0,0 +1,440 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + + "github.com/lightninglabs/lightning-terminal/perms" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/macaroons" + "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// SubServer defines an interface that should be implemented by any sub-server +// that the subServer manager should manage. A sub-server can be run in either +// integrated or remote mode. A sub-server is considered non-fatal to LiT +// meaning that if a sub-server fails to start, LiT can safely continue with its +// operations and other sub-servers can too. +type SubServer interface { + // Name returns the name of the sub-server. + Name() string + + // Remote returns true if the sub-server is running remotely and so + // should be connected to instead of spinning up an integrated server. + Remote() bool + + // RemoteConfig returns the config required to connect to the sub-server + // if it is running in remote mode. + RemoteConfig() *RemoteDaemonConfig + + // Start starts the sub-server in integrated mode. + Start(lnrpc.LightningClient, *lndclient.GrpcLndServices, bool) error + + // Stop stops the sub-server in integrated mode. + Stop() error + + // RegisterGrpcService must register the sub-server's GRPC server with + // the given registrar. + RegisterGrpcService(grpc.ServiceRegistrar) + + // PermsSubServer returns the name that the permission manager stores + // the permissions for this SubServer under. + PermsSubServer() perms.SubServerName + + // ServerErrChan returns an error channel that should be listened on + // after starting the sub-server to listen for any runtime errors. It + // is optional and may be set to nil. This only applies in integrated + // mode. + ServerErrChan() chan error + + // MacPath returns the path to the sub-server's macaroon if it is not + // running in remote mode. + MacPath() string + + macaroons.MacaroonValidator +} + +// subServer is a wrapper around the SubServer interface and is used by the +// subServerMgr to manage a SubServer. +type subServer struct { + integratedStarted bool + startedMu sync.RWMutex + + stopped sync.Once + + SubServer + + remoteConn *grpc.ClientConn + + wg sync.WaitGroup + quit chan struct{} +} + +// started returns true if the subServer has been started. This only applies if +// the subServer is running in integrated mode. +func (s *subServer) started() bool { + s.startedMu.RLock() + defer s.startedMu.RUnlock() + + return s.integratedStarted +} + +// setStarted sets the subServer as started or not. This only applies if the +// subServer is running in integrated mode. +func (s *subServer) setStarted(started bool) { + s.startedMu.Lock() + defer s.startedMu.Unlock() + + s.integratedStarted = started +} + +// stop the subServer by closing the connection to it if it is remote or by +// stopping the integrated process. +func (s *subServer) stop() error { + // If the sub-server has not yet started, then we can exit early. + if !s.started() { + return nil + } + + var returnErr error + s.stopped.Do(func() { + close(s.quit) + s.wg.Wait() + + // If running in remote mode, close the connection. + if s.Remote() && s.remoteConn != nil { + err := s.remoteConn.Close() + if err != nil { + returnErr = fmt.Errorf("could not close "+ + "remote connection: %v", err) + } + return + } + + // Else, stop the integrated sub-server process. + err := s.Stop() + if err != nil { + returnErr = fmt.Errorf("could not close "+ + "integrated connection: %v", err) + return + } + + if s.ServerErrChan() == nil { + return + } + + select { + case returnErr = <-s.ServerErrChan(): + default: + } + }) + + return returnErr +} + +// startIntegrated starts the subServer in integrated mode. +func (s *subServer) startIntegrated(lndClient lnrpc.LightningClient, + lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool, + onError func(error)) error { + + err := s.Start(lndClient, lndGrpc, withMacaroonService) + if err != nil { + return err + } + s.setStarted(true) + + if s.ServerErrChan() == nil { + return nil + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + select { + case err := <-s.ServerErrChan(): + // The sub server should shut itself down if an error + // happens. We don't need to try to stop it again. + s.setStarted(false) + + onError(fmt.Errorf("received "+ + "critical error from sub-server, "+ + "shutting down: %v", err), + ) + + case <-s.quit: + } + }() + + return nil +} + +// connectRemote attempts to make a connection to the remote sub-server. +func (s *subServer) connectRemote() error { + var err error + s.remoteConn, err = dialBackend( + s.Name(), s.RemoteConfig().RPCServer, + lncfg.CleanAndExpandPath(s.RemoteConfig().TLSCertPath), + ) + if err != nil { + return fmt.Errorf("remote dial error: %v", err) + } + + return nil +} + +// subServerMgr manages a set of subServer objects. +type subServerMgr struct { + servers []*subServer + mu sync.RWMutex + + statusServer *statusServer + permsMgr *perms.Manager +} + +// newSubServerMgr constructs a new subServerMgr. +func newSubServerMgr(permsMgr *perms.Manager, + statusServer *statusServer) *subServerMgr { + + return &subServerMgr{ + servers: []*subServer{}, + statusServer: statusServer, + permsMgr: permsMgr, + } +} + +// AddServer adds a new subServer to the manager's set. +func (s *subServerMgr) AddServer(ss SubServer) { + s.mu.Lock() + defer s.mu.Unlock() + + s.servers = append(s.servers, &subServer{ + SubServer: ss, + quit: make(chan struct{}), + }) + + s.statusServer.RegisterSubServer(ss.Name()) +} + +// StartIntegratedServers starts all the manager's sub-servers that should be +// started in integrated mode. +func (s *subServerMgr) StartIntegratedServers(lndClient lnrpc.LightningClient, + lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool) { + + s.mu.Lock() + defer s.mu.Unlock() + + for _, ss := range s.servers { + if ss.Remote() { + continue + } + + err := ss.startIntegrated( + lndClient, lndGrpc, withMacaroonService, + func(err error) { + s.statusServer.setServerErrored( + ss.Name(), err.Error(), + ) + }, + ) + if err != nil { + s.statusServer.setServerErrored(ss.Name(), err.Error()) + continue + } + + s.statusServer.setServerRunning(ss.Name()) + } +} + +// ConnectRemoteSubServers creates connections to all the manager's sub-servers +// that are running remotely. +func (s *subServerMgr) ConnectRemoteSubServers() { + s.mu.Lock() + defer s.mu.Unlock() + + for _, ss := range s.servers { + if !ss.Remote() { + continue + } + + err := ss.connectRemote() + if err != nil { + s.statusServer.setServerErrored(ss.Name(), err.Error()) + continue + } + + s.statusServer.setServerRunning(ss.Name()) + } +} + +// RegisterRPCServices registers all the manager's sub-servers with the given +// grpc registrar. +func (s *subServerMgr) RegisterRPCServices(server grpc.ServiceRegistrar) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + // In remote mode the "director" of the RPC proxy will act as + // a catch-all for any gRPC request that isn't known because we + // didn't register any server for it. The director will then + // forward the request to the remote service. + if ss.Remote() { + continue + } + + ss.RegisterGrpcService(server) + } +} + +// GetRemoteConn checks if any of the manager's sub-servers owns the given uri +// and if so, the remote connection to that sub-server is returned. The bool +// return value indicates if the uri is managed by one of the sub-servers +// running in remote mode. +func (s *subServerMgr) GetRemoteConn(uri string) (bool, *grpc.ClientConn) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if !s.permsMgr.IsSubServerURI(ss.PermsSubServer(), uri) { + continue + } + + if !ss.Remote() { + return false, nil + } + + return true, ss.remoteConn + } + + return false, nil +} + +// ValidateMacaroon checks if any of the manager's sub-servers owns the given +// uri and if so, if it is running in remote mode, then true is returned since +// the macaroon will be validated by the remote subserver itself when the +// request arrives. Otherwise, the integrated sub-server's validator validates +// the macaroon. +func (s *subServerMgr) ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, uri string) (bool, error) { + + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if !s.permsMgr.IsSubServerURI(ss.PermsSubServer(), uri) { + continue + } + + if ss.Remote() { + return true, nil + } + + if !ss.started() { + return true, fmt.Errorf("%s is not yet ready for "+ + "requests, lnd possibly still starting or "+ + "syncing", ss.Name()) + } + + err := ss.ValidateMacaroon(ctx, requiredPermissions, uri) + if err != nil { + return true, &proxyErr{ + proxyContext: ss.Name(), + wrapped: fmt.Errorf("invalid macaroon: %v", + err), + } + } + } + + return false, nil +} + +// HandledBy returns true if one of its sub-servers owns the given URI. +func (s *subServerMgr) HandledBy(uri string) (bool, string) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if !s.permsMgr.IsSubServerURI(ss.PermsSubServer(), uri) { + continue + } + + return true, ss.Name() + } + + return false, "" +} + +// MacaroonPath checks if any of the manager's sub-servers owns the given uri +// and if so, the appropriate macaroon path is returned for that sub-server. +func (s *subServerMgr) MacaroonPath(uri string) (bool, string) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if !s.permsMgr.IsSubServerURI(ss.PermsSubServer(), uri) { + continue + } + + if ss.Remote() { + return true, ss.RemoteConfig().MacaroonPath + } + + return true, ss.MacPath() + } + + return false, "" +} + +// ReadRemoteMacaroon checks if any of the manager's sub-servers running in +// remote mode owns the given uri and if so, the appropriate macaroon path is +// returned for that sub-server. +func (s *subServerMgr) ReadRemoteMacaroon(uri string) (bool, []byte, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if !s.permsMgr.IsSubServerURI(ss.PermsSubServer(), uri) { + continue + } + + if !ss.Remote() { + return false, nil, nil + } + + macBytes, err := readMacaroon(lncfg.CleanAndExpandPath( + ss.RemoteConfig().MacaroonPath, + )) + + return true, macBytes, err + } + + return false, nil, nil +} + +// Stop stops all the manager's sub-servers +func (s *subServerMgr) Stop() error { + var returnErr error + + s.mu.RLock() + defer s.mu.RUnlock() + + for _, ss := range s.servers { + if ss.Remote() { + continue + } + + err := ss.stop() + if err != nil { + log.Errorf("Error stopping %s: %v", ss.Name(), err) + returnErr = err + } + + s.statusServer.setServerStopped(ss.Name()) + } + + return returnErr +} diff --git a/subservers.go b/subservers.go new file mode 100644 index 000000000..cb7457dda --- /dev/null +++ b/subservers.go @@ -0,0 +1,294 @@ +package terminal + +import ( + "github.com/lightninglabs/faraday" + "github.com/lightninglabs/faraday/frdrpc" + "github.com/lightninglabs/faraday/frdrpcserver" + "github.com/lightninglabs/lightning-terminal/perms" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/loopd" + "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/pool" + "github.com/lightninglabs/pool/poolrpc" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc" +) + +// faradaySubServer implements the SubServer interface. +type faradaySubServer struct { + *frdrpcserver.RPCServer + + remote bool + cfg *faraday.Config + remoteCfg *RemoteDaemonConfig +} + +// A compile-time check to ensure that faradaySubServer implements SubServer. +var _ SubServer = (*faradaySubServer)(nil) + +// NewFaradaySubServer returns a new faraday implementation of the SubServer +// interface. +func NewFaradaySubServer(cfg *faraday.Config, rpcCfg *frdrpcserver.Config, + remoteCfg *RemoteDaemonConfig, remote bool) SubServer { + + return &faradaySubServer{ + RPCServer: frdrpcserver.NewRPCServer(rpcCfg), + cfg: cfg, + remoteCfg: remoteCfg, + remote: remote, + } +} + +// Name returns the name of the sub-server. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) Name() string { + return "faraday" +} + +// Remote returns true if the sub-server is running remotely and so +// should be connected to instead of spinning up an integrated server. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) Remote() bool { + return f.remote +} + +// RemoteConfig returns the config required to connect to the sub-server +// if it is running in remote mode. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) RemoteConfig() *RemoteDaemonConfig { + return f.remoteCfg +} + +// Start starts the sub-server in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) Start(_ lnrpc.LightningClient, + lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool) error { + + return f.StartAsSubserver( + lndGrpc.LndServices, withMacaroonService, + ) +} + +// RegisterGrpcService must register the sub-server's GRPC server with the given +// registrar. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) RegisterGrpcService(service grpc.ServiceRegistrar) { + frdrpc.RegisterFaradayServerServer(service, f) +} + +// PermsSubServer returns the name that the permission manager stores the +// permissions for this SubServer under. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) PermsSubServer() perms.SubServerName { + return perms.SubServerFaraday +} + +// ServerErrChan returns an error channel that should be listened on after +// starting the sub-server to listen for any runtime errors. It is optional and +// may be set to nil. This only applies in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) ServerErrChan() chan error { + return nil +} + +// MacPath returns the path to the sub-server's macaroon if it is not running in +// remote mode. +// +// NOTE: this is part of the SubServer interface. +func (f *faradaySubServer) MacPath() string { + return f.cfg.MacaroonPath +} + +// faradaySubServer implements the SubServer interface. +type loopSubServer struct { + *loopd.Daemon + remote bool + cfg *loopd.Config + remoteCfg *RemoteDaemonConfig +} + +// A compile-time check to ensure that faradaySubServer implements SubServer. +var _ SubServer = (*loopSubServer)(nil) + +// NewLoopSubServer returns a new loop implementation of the SubServer +// interface. +func NewLoopSubServer(cfg *loopd.Config, remoteCfg *RemoteDaemonConfig, + remote bool) SubServer { + + return &loopSubServer{ + Daemon: loopd.New(cfg, nil), + cfg: cfg, + remoteCfg: remoteCfg, + remote: remote, + } +} + +// Name returns the name of the sub-server. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) Name() string { + return "loop" +} + +// Remote returns true if the sub-server is running remotely and so should be +// connected to instead of spinning up an integrated server. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) Remote() bool { + return l.remote +} + +// RemoteConfig returns the config required to connect to the sub-server if it +// is running in remote mode. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) RemoteConfig() *RemoteDaemonConfig { + return l.remoteCfg +} + +// Start starts the sub-server in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) Start(_ lnrpc.LightningClient, + lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool) error { + + return l.StartAsSubserver(lndGrpc, withMacaroonService) +} + +// Stop stops the sub-server in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) Stop() error { + l.Daemon.Stop() + + return nil +} + +// RegisterGrpcService must register the sub-server's GRPC server with the given +// registrar. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) RegisterGrpcService(registrar grpc.ServiceRegistrar) { + looprpc.RegisterSwapClientServer(registrar, l) +} + +// PermsSubServer returns the name that the permission manager stores the +// permissions for this SubServer under. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) PermsSubServer() perms.SubServerName { + return perms.SubServerLoop +} + +// ServerErrChan returns an error channel that should be listened on after +// starting the sub-server to listen for any runtime errors. It is optional and +// may be set to nil. This only applies in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) ServerErrChan() chan error { + return l.ErrChan +} + +// MacPath returns the path to the sub-server's macaroon if it is not running in +// remote mode. +// +// NOTE: this is part of the SubServer interface. +func (l *loopSubServer) MacPath() string { + return l.cfg.MacaroonPath +} + +// poolSubServer implements the SubServer interface. +type poolSubServer struct { + *pool.Server + remote bool + cfg *pool.Config + remoteCfg *RemoteDaemonConfig +} + +// A compile-time check to ensure that faradaySubServer implements SubServer. +var _ SubServer = (*poolSubServer)(nil) + +// NewPoolSubServer returns a new pool implementation of the SubServer +// interface. +func NewPoolSubServer(cfg *pool.Config, remoteCfg *RemoteDaemonConfig, + remote bool) SubServer { + + return &poolSubServer{ + Server: pool.NewServer(cfg), + cfg: cfg, + remoteCfg: remoteCfg, + remote: remote, + } +} + +// Name returns the name of the sub-server. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) Name() string { + return "pool" +} + +// Remote returns true if the sub-server is running remotely and so should be +// connected to instead of spinning up an integrated server. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) Remote() bool { + return p.remote +} + +// RemoteConfig returns the config required to connect to the sub-server if it +// is running in remote mode. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) RemoteConfig() *RemoteDaemonConfig { + return p.remoteCfg +} + +// Start starts the sub-server in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) Start(lnClient lnrpc.LightningClient, + lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool) error { + + return p.StartAsSubserver(lnClient, lndGrpc, withMacaroonService) +} + +// RegisterGrpcService must register the sub-server's GRPC server with the given +// registrar. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) RegisterGrpcService(registrar grpc.ServiceRegistrar) { + poolrpc.RegisterTraderServer(registrar, p) +} + +// PermsSubServer returns the name that the permission manager stores the +// permissions for this SubServer under. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) PermsSubServer() perms.SubServerName { + return perms.SubServerPool +} + +// ServerErrChan returns an error channel that should be listened on after +// starting the sub-server to listen for any runtime errors. It is optional and +// may be set to nil. This only applies in integrated mode. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) ServerErrChan() chan error { + return nil +} + +// MacPath returns the path to the sub-server's macaroon if it is not running in +// remote mode. +// +// NOTE: this is part of the SubServer interface. +func (p *poolSubServer) MacPath() string { + return p.cfg.MacaroonPath +} diff --git a/terminal.go b/terminal.go index 550f1612e..958bbeea0 100644 --- a/terminal.go +++ b/terminal.go @@ -20,7 +20,6 @@ import ( restProxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jessevdk/go-flags" "github.com/lightninglabs/faraday/frdrpc" - "github.com/lightninglabs/faraday/frdrpcserver" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/autopilotserver" "github.com/lightninglabs/lightning-terminal/firewall" @@ -33,7 +32,6 @@ import ( "github.com/lightninglabs/lightning-terminal/session" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" - "github.com/lightninglabs/loop/loopd" "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/pool" "github.com/lightninglabs/pool/poolrpc" @@ -154,22 +152,17 @@ type LightningTerminal struct { wg sync.WaitGroup errQueue *queue.ConcurrentQueue[error] + lndConn *grpc.ClientConn lndClient *lndclient.GrpcLndServices basicClient lnrpc.LightningClient - faradayServer *frdrpcserver.RPCServer - faradayStarted bool + statusServer *statusServer + subServerMgr *subServerMgr autopilotClient autopilotserver.Autopilot ruleMgrs rules.ManagerSet - loopServer *loopd.Daemon - loopStarted bool - - poolServer *pool.Server - poolStarted bool - rpcProxy *rpcProxy httpServer *http.Server @@ -195,7 +188,9 @@ type LightningTerminal struct { // New creates a new instance of the lightning-terminal daemon. func New() *LightningTerminal { - return &LightningTerminal{} + return &LightningTerminal{ + statusServer: newStatusServer(), + } } // Run starts everything and then blocks until either the application is shut @@ -230,15 +225,76 @@ func (g *LightningTerminal) Run() error { return fmt.Errorf("could not create permissions manager") } - // 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 = frdrpcserver.NewRPCServer(g.cfg.faradayRpcConfig) - g.loopServer = loopd.New(g.cfg.Loop, nil) - g.poolServer = pool.NewServer(g.cfg.Pool) + g.statusServer.RegisterSubServer(LNDSubServer) + g.statusServer.RegisterSubServer(LitdSubServer) + + g.subServerMgr = newSubServerMgr(g.permsMgr, g.statusServer) + + // Construct the rpcProxy. It must be initialised before the main web + // server is started. g.rpcProxy = newRpcProxy( - g.cfg, g, g.validateSuperMacaroon, g.permsMgr, bufRpcListener, + g.cfg, g, g.validateSuperMacaroon, g.permsMgr, g.statusServer, + g.subServerMgr, ) + + // 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(); err != nil { + return fmt.Errorf("error creating REST proxy: %v", err) + } + } + + // 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 UI HTTP server: %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() + if startErr != nil { + g.statusServer.setServerErrored( + LitdSubServer, "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") + + if g.rpcProxy != nil { + if err := g.rpcProxy.Stop(); err != nil { + log.Errorf("Error stopping rpc proxy: %v", err) + } + } + + if g.httpServer != nil { + if err := g.httpServer.Close(); err != nil { + log.Errorf("Error stopping UI server: %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() error { + // Create the instances of our subservers now so we can hook them up to + // lnd once it's fully started. + g.initSubServers() + + var err error g.accountService, err = accounts.NewService( filepath.Dir(g.cfg.MacaroonPath), g.errQueue.ChanIn(), ) @@ -330,19 +386,16 @@ func (g *LightningTerminal) Run() error { "server: %v", err) } - // Overwrite the loop and pool daemon's user agent name so it sends - // "litd" instead of "loopd" and "poold" respectively. - loop.AgentName = "litd" - pool.SetAgentName("litd") - // 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{}) - lndQuit := make(chan struct{}) - macChan := make(chan []byte, 1) - + 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{{ @@ -370,9 +423,7 @@ func (g *LightningTerminal) Run() error { go func() { defer g.wg.Done() - err := lnd.Main( - g.cfg.Lnd, lisCfg, implCfg, shutdownInterceptor, - ) + err := lnd.Main(g.cfg.Lnd, lisCfg, implCfg, interceptor) if e, ok := err.(*flags.Error); err != nil && (!ok || e.Type != flags.ErrHelp) { @@ -391,14 +442,6 @@ func (g *LightningTerminal) Run() error { _ = g.RegisterGrpcSubserver(g.rpcProxy.grpcServer) } - // 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(); err != nil { - return fmt.Errorf("error creating REST proxy: %v", err) - } - } - // 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 @@ -411,35 +454,50 @@ func (g *LightningTerminal) Run() error { case <-readyChan: case err := <-g.errQueue.ChanOut(): - return err + g.statusServer.setServerErrored( + LNDSubServer, "error from errQueue channel", + ) + return fmt.Errorf("could not start LND: %v", err) case <-lndQuit: - return nil + g.statusServer.setServerErrored( + LNDSubServer, "lndQuit channel closed", + ) + return fmt.Errorf("LND has stopped") - case <-shutdownInterceptor.ShutdownChannel(): - return errors.New("shutting down") + case <-interceptor.ShutdownChannel(): + return fmt.Errorf("received the shutdown signal") } // We now know that starting lnd was successful. If we now run into an // error, we must shut down lnd correctly. defer func() { - err := g.shutdown() + err := g.shutdownSubServers() if err != nil { log.Errorf("Error shutting down: %v", err) } }() + // Connect to LND. + g.lndConn, err = connectLND(g.cfg, bufRpcListener) + if err != nil { + g.statusServer.setServerErrored( + LNDSubServer, "could not connect to LND: %v", err, + ) + + return fmt.Errorf("could not connect to LND") + } + + // Initialise any connections to sub-servers that we are running in + // remote mode. + g.subServerMgr.ConnectRemoteSubServers() + // Now start the RPC proxy that will handle all incoming gRPC, grpc-web - // and REST requests. We also 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.rpcProxy.Start(); err != nil { + // and REST requests. + if err := g.rpcProxy.Start(g.lndConn); err != nil { return fmt.Errorf("error starting lnd gRPC proxy server: %v", err) } - if err := g.startMainWebServer(); err != nil { - return fmt.Errorf("error starting UI HTTP server: %v", err) - } // 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. @@ -455,12 +513,18 @@ func (g *LightningTerminal) Run() error { return err case <-lndQuit: - return nil + g.statusServer.setServerErrored( + LNDSubServer, "lndQuit channel closed", + ) + return fmt.Errorf("LND is not running") - case <-shutdownInterceptor.ShutdownChannel(): + case <-interceptor.ShutdownChannel(): return errors.New("shutting down") } + // We can now set the status of LND as running. + g.statusServer.setServerRunning(LNDSubServer) + // 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 { @@ -468,41 +532,69 @@ func (g *LightningTerminal) Run() error { g.cfg.lndAdminMacaroon = <-macChan } - err = g.startSubservers() + // Set up all the LND clients required by LiT. + err = g.setUpLNDClients() if err != nil { - log.Errorf("Could not start subservers: %v", err) - return err + g.statusServer.setServerErrored( + LNDSubServer, "could not set up LND clients: %v", err, + ) + + return fmt.Errorf("could not start LND") } + // 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 sub-servers if + // they should be started locally as an integrated service. + g.subServerMgr.StartIntegratedServers( + g.basicClient, g.lndClient, createDefaultMacaroons, + ) + + err = g.startInternalSubServers(createDefaultMacaroons) + 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.statusServer.setServerRunning(LitdSubServer) + // Now block until we receive an error or the main shutdown signal. select { - case err := <-g.loopServer.ErrChan: - // Loop will shut itself down if an error happens. We don't need - // to try to stop it again. - g.loopStarted = false - log.Errorf("Received critical error from loop, shutting down: "+ - "%v", err) - case err := <-g.errQueue.ChanOut(): if err != nil { - log.Errorf("Received critical error from subsystem, "+ - "shutting down: %v", err) + return fmt.Errorf("received critical error from "+ + "subsystem, shutting down: %v", err) } case <-lndQuit: - return nil + g.statusServer.setServerErrored( + LNDSubServer, "lndQuit channel closed", + ) + + return fmt.Errorf("LND is not running") - case <-shutdownInterceptor.ShutdownChannel(): - log.Infof("Shutdown signal received") + case <-interceptor.ShutdownChannel(): + log.Infof("received the shutdown signal") } return nil } -// startSubservers creates an internal connection to lnd and then starts all -// embedded daemons as external subservers that hook into the same gRPC and REST -// servers that lnd started. -func (g *LightningTerminal) startSubservers() error { +// setUpLNDClients sets up the various LND clients required by LiT. +func (g *LightningTerminal) setUpLNDClients() error { var ( insecure bool clientOptions []lndclient.BasicClientOption @@ -543,7 +635,7 @@ func (g *LightningTerminal) startSubservers() error { return err }, defaultStartupTimeout) if err != nil { - return err + return fmt.Errorf("could not create basic LND Client: %v", err) } // Now we know that the connection itself is ready. But we also need to @@ -586,7 +678,8 @@ func (g *LightningTerminal) startSubservers() error { }, ) if err != nil { - return err + return fmt.Errorf("could not create LND Services client: %v", + err) } // Pass LND's build tags to the permission manager so that it can @@ -614,60 +707,21 @@ func (g *LightningTerminal) startSubservers() error { g.rpcProxy.superMacaroon = superMacaroon } - // 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 { - log.Infof("Starting integrated faraday daemon") - err = g.faradayServer.StartAsSubserver( - g.lndClient.LndServices, createDefaultMacaroons, - ) - if err != nil { - return err - } - g.faradayStarted = true - } - - if !g.cfg.loopRemote { - log.Infof("Starting integrated loop daemon") - err = g.loopServer.StartAsSubserver( - g.lndClient, createDefaultMacaroons, - ) - if err != nil { - return err - } - g.loopStarted = true - } + return nil +} - if !g.cfg.poolRemote { - log.Infof("Starting integrated pool daemon") - err = g.poolServer.StartAsSubserver( - g.basicClient, g.lndClient, createDefaultMacaroons, - ) - if err != nil { - return err - } - g.poolStarted = true - } +// startInternalSubServers starts all Litd specific sub-servers. +func (g *LightningTerminal) startInternalSubServers( + createDefaultMacaroons bool) error { log.Infof("Starting LiT macaroon service") + + var err error g.macaroonService, err = lndclient.NewMacaroonService( &lndclient.MacaroonServiceConfig{ - DBPath: filepath.Join(g.cfg.LitDir, g.cfg.Network), + DBPath: filepath.Join( + g.cfg.LitDir, g.cfg.Network, + ), MacaroonLocation: "litd", StatelessInit: !createDefaultMacaroons, RequiredPerms: perms.LitPermissions, @@ -745,7 +799,8 @@ func (g *LightningTerminal) startSubservers() error { } if !g.cfg.Autopilot.Disable { - info, err := g.lndClient.Client.GetInfo(ctxc) + ctx := context.Background() + info, err := g.lndClient.Client.GetInfo(ctx) if err != nil { return fmt.Errorf("GetInfo call failed: %v", err) } @@ -810,25 +865,13 @@ func (g *LightningTerminal) RegisterGrpcSubserver(server *grpc.Server) error { func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, withLitRPC bool) { - // In remote mode the "director" of the RPC proxy will act as a catch- - // all for any gRPC request that isn't known because we didn't register - // any server for it. The director will then forward the request to the - // remote service. - if !g.cfg.faradayRemote { - frdrpc.RegisterFaradayServerServer(server, g.faradayServer) - } - - if !g.cfg.loopRemote { - looprpc.RegisterSwapClientServer(server, g.loopServer) - } - - if !g.cfg.poolRemote { - poolrpc.RegisterTraderServer(server, g.poolServer) - } + g.subServerMgr.RegisterRPCServices(server) if withLitRPC { litrpc.RegisterSessionsServer(server, g.sessionRpcServer) litrpc.RegisterAccountsServer(server, g.accountRpcServer) + litrpc.RegisterLitServiceServer(server, g.rpcProxy) + litrpc.RegisterStatusServer(server, g.statusServer) } litrpc.RegisterFirewallServer(server, g.sessionRpcServer) @@ -883,6 +926,10 @@ func (g *LightningTerminal) RegisterRestSubserver(ctx context.Context, func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, requiredPermissions []bakery.Op, fullMethod string) error { + if _, ok := perms.MacaroonWhitelist[fullMethod]; ok { + return nil + } + macHex, err := macaroons.RawMacaroonFromContext(ctx) if err != nil { return err @@ -911,80 +958,14 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, // 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. - switch { - case g.permsMgr.IsFaradayURI(fullMethod): - // In remote mode we just pass through the request, the remote - // daemon will check the macaroon. - if g.cfg.faradayRemote { - return nil - } - - if !g.faradayStarted { - return fmt.Errorf("faraday is not yet ready for " + - "requests, lnd possibly still starting or " + - "syncing") - } - - err = g.faradayServer.ValidateMacaroon( - ctx, requiredPermissions, fullMethod, - ) - if err != nil { - return &proxyErr{ - proxyContext: "faraday", - wrapped: fmt.Errorf("invalid macaroon: %v", - err), - } - } - - case g.permsMgr.IsLoopURI(fullMethod): - // In remote mode we just pass through the request, the remote - // daemon will check the macaroon. - if g.cfg.loopRemote { - return nil - } - - if !g.loopStarted { - return fmt.Errorf("loop is not yet ready for " + - "requests, lnd possibly still starting or " + - "syncing") - } - - err = g.loopServer.ValidateMacaroon( - ctx, requiredPermissions, fullMethod, - ) - if err != nil { - return &proxyErr{ - proxyContext: "loop", - wrapped: fmt.Errorf("invalid macaroon: %v", - err), - } - } - - case g.permsMgr.IsPoolURI(fullMethod): - // In remote mode we just pass through the request, the remote - // daemon will check the macaroon. - if g.cfg.poolRemote { - return nil - } - - if !g.poolStarted { - return fmt.Errorf("pool is not yet ready for " + - "requests, lnd possibly still starting or " + - "syncing") - } - - err = g.poolServer.ValidateMacaroon( - ctx, requiredPermissions, fullMethod, - ) - if err != nil { - return &proxyErr{ - proxyContext: "pool", - wrapped: fmt.Errorf("invalid macaroon: %v", - err), - } - } + handled, err := g.subServerMgr.ValidateMacaroon( + ctx, requiredPermissions, fullMethod, + ) + if handled { + return err + } - case g.permsMgr.IsLitURI(fullMethod): + if g.permsMgr.IsSubServerURI(perms.SubServerLit, fullMethod) { if !g.macaroonServiceStarted { return fmt.Errorf("the macaroon service has not " + "started yet") @@ -1036,30 +1017,14 @@ func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, ) } -// shutdown stops all subservers that were started and attached to lnd. -func (g *LightningTerminal) shutdown() error { +// shutdownSubServers stops all subservers that were started and attached to +// lnd. +func (g *LightningTerminal) shutdownSubServers() error { var returnErr error - if g.faradayStarted { - if err := g.faradayServer.Stop(); err != nil { - log.Errorf("Error stopping faraday: %v", err) - returnErr = err - } - } - - if g.loopStarted { - g.loopServer.Stop() - if err := <-g.loopServer.ErrChan; err != nil { - log.Errorf("Error stopping loop: %v", err) - returnErr = err - } - } - - if g.poolStarted { - if err := g.poolServer.Stop(); err != nil { - log.Errorf("Error stopping pool: %v", err) - returnErr = err - } + err := g.subServerMgr.Stop() + if err != nil { + returnErr = err } if g.autopilotClient != nil { @@ -1113,25 +1078,13 @@ func (g *LightningTerminal) shutdown() error { g.restCancel() } - if g.rpcProxy != nil { - if err := g.rpcProxy.Stop(); err != nil { - log.Errorf("Error stopping lnd proxy: %v", err) - returnErr = err - } - } - - if g.httpServer != nil { - if err := g.httpServer.Close(); err != nil { - log.Errorf("Error stopping UI server: %v", err) + if g.lndConn != nil { + if err := g.lndConn.Close(); err != nil { + log.Errorf("Error closing lnd connection: %v", err) returnErr = err } } - // In case the error wasn't thrown by lnd, make sure we stop it too. - interceptor.RequestShutdown() - - g.wg.Wait() - // 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. @@ -1159,44 +1112,43 @@ func (g *LightningTerminal) shutdown() error { // 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 | -// +---------------------+ -// +// 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, @@ -1439,6 +1391,29 @@ func (g *LightningTerminal) validateSuperMacaroon(ctx context.Context, return nil } +// initSubServers registers the faraday and loop sub-servers with the +// subServerMgr. +func (g *LightningTerminal) initSubServers() { + g.subServerMgr.AddServer(NewFaradaySubServer( + g.cfg.Faraday, g.cfg.faradayRpcConfig, g.cfg.Remote.Faraday, + g.cfg.faradayRemote, + )) + + // Overwrite the loop daemon's user agent name so it sends "litd" + // instead of "loopd". + loop.AgentName = "litd" + g.subServerMgr.AddServer(NewLoopSubServer( + g.cfg.Loop, g.cfg.Remote.Loop, g.cfg.loopRemote, + )) + + // Overwrite the pool daemon's user agent name so it sends "litd" + // instead of and "poold". + pool.SetAgentName("litd") + g.subServerMgr.AddServer(NewPoolSubServer( + g.cfg.Pool, g.cfg.Remote.Pool, g.cfg.poolRemote, + )) +} + // BakeSuperMacaroon uses the lnd client to bake a macaroon that can include // permissions for multiple daemons. func BakeSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient,