From a6e0e3c56d9a01d8c437d7d8770bb2b95f2940e3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 31 Oct 2022 12:15:51 +0200 Subject: [PATCH 01/17] multi: always use Lit's TLS cert In this commit, we remove Lit's dependency on LND's tls cert in integrated mode. With this commit, even if Lit is started in integrated mode, it will create its own tls cert to use for serving the webserver. --- cmd/litcli/main.go | 25 ---------- config.go | 77 ++++++++++++++++-------------- doc/config-lnd-integrated.md | 11 +++++ itest/litd_mode_integrated_test.go | 21 ++++---- 4 files changed, 60 insertions(+), 74 deletions(-) diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index 4ccf03ed4..453bc325a 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...) @@ -175,18 +162,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/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. From d044b9f2c0a192c6bcb532c21e43200eab686df5 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 Feb 2023 15:27:34 +0200 Subject: [PATCH 02/17] multi: add StopDaemon method to lit In this commit, a new LitService proto is added and the StopDaemon method is added to the service. This is added due to the fact that in an upcoming commit, we want Litd to continue running even if LND cannot be connected to and so this means that Lit will no longer shutdown if LNDs StopDaemon method is called. --- cmd/litcli/lit.go | 37 +++++++ cmd/litcli/main.go | 1 + litrpc/lit.pb.go | 193 +++++++++++++++++++++++++++++++++++ litrpc/lit.proto | 15 +++ litrpc/lit_grpc.pb.go | 101 ++++++++++++++++++ litrpc/litservice.pb.json.go | 48 +++++++++ perms/permissions.go | 4 + rpc_proxy.go | 80 +++++++++------ terminal.go | 76 +++++++------- 9 files changed, 484 insertions(+), 71 deletions(-) create mode 100644 cmd/litcli/lit.go create mode 100644 litrpc/lit.pb.go create mode 100644 litrpc/lit.proto create mode 100644 litrpc/lit_grpc.pb.go create mode 100644 litrpc/litservice.pb.json.go 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 453bc325a..1bb599123 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -75,6 +75,7 @@ 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...) err := app.Run(os.Args) if err != nil { 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/perms/permissions.go b/perms/permissions.go index 9fb8727f4..09344f95d 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -86,6 +86,10 @@ var ( Entity: "privacymap", Action: "read", }}, + "/litrpc.LitService/StopDaemon": {{ + Entity: "litd", + Action: "write", + }}, } // whiteListedLNDMethods is a map of all lnd RPC methods that don't diff --git a/rpc_proxy.go b/rpc_proxy.go index d4bd45788..4ed0b22af 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -13,6 +13,7 @@ import ( "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" @@ -111,40 +112,41 @@ 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 { + litrpc.UnimplementedLitServiceServer + cfg *Config basicAuth string permsMgr *perms.Manager @@ -254,6 +256,18 @@ func (p *rpcProxy) Stop() error { return nil } +// 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) { + + interceptor.RequestShutdown() + + return &litrpc.StopDaemonResponse{}, nil +} + // isHandling checks if the specified request is something to be handled by lnd // or any of the attached sub daemons. If true is returned, the call was handled // by the RPC proxy and the caller MUST NOT handle it again. If false is diff --git a/terminal.go b/terminal.go index 550f1612e..57e668ec7 100644 --- a/terminal.go +++ b/terminal.go @@ -829,6 +829,7 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, if withLitRPC { litrpc.RegisterSessionsServer(server, g.sessionRpcServer) litrpc.RegisterAccountsServer(server, g.accountRpcServer) + litrpc.RegisterLitServiceServer(server, g.rpcProxy) } litrpc.RegisterFirewallServer(server, g.sessionRpcServer) @@ -1159,44 +1160,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, From db2d5221d2a7b4eb2edfa4593f9fc0534099fe1a Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 Feb 2023 16:05:30 +0200 Subject: [PATCH 03/17] rpc_proxy: add hasStarted method to rpcProxy Add a `started` variable to the rpcProxy that is used to indicate if the proxy is ready to handle requests. This is because currently the webserver is dependent on the rpcProxy to start and we want to be able to start the webserver without being dependent on the rpcProxy so that it can be used to handle status requests in a future commit. So with this commit, we can now saftely start the webserver earlier on and then if requests come through for the rpcProxy, an error will be displayed to the user. --- rpc_proxy.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/rpc_proxy.go b/rpc_proxy.go index 4ed0b22af..be2dcf95d 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "strings" + "sync/atomic" "time" "github.com/improbable-eng/grpc-web/go/grpcweb" @@ -37,6 +38,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 { @@ -145,6 +150,10 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, // | - 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 @@ -218,9 +227,17 @@ func (p *rpcProxy) Start() error { } } + atomic.CompareAndSwapInt32(&p.started, 0, 1) + return nil } +// 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 +} + // Stop shuts down the lnd connection. func (p *rpcProxy) Stop() error { p.grpcServer.Stop() @@ -386,6 +403,10 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if !p.hasStarted() { + return nil, ErrWaitingToStart + } + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return nil, fmt.Errorf("%s: unknown permissions "+ @@ -427,6 +448,10 @@ func (p *rpcProxy) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if !p.hasStarted() { + return ErrWaitingToStart + } + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return fmt.Errorf("%s: unknown permissions required "+ From f06d735036af56139131d18d60d2b13b147b10e6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 31 Oct 2022 13:26:36 +0200 Subject: [PATCH 04/17] multi: split out LND connection from rpcProxy Remove the responsibility of creating an LND connection from the rpcProxy and instead let the main LightningTerminal struct handle it. All the lnd-connection specific functions are also moved into their own file. --- lnd_connection.go | 85 ++++++++++++++++++++++++++++++++++++++++++ rpc_proxy.go | 94 ++--------------------------------------------- terminal.go | 18 ++++++++- 3 files changed, 104 insertions(+), 93 deletions(-) create mode 100644 lnd_connection.go 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/rpc_proxy.go b/rpc_proxy.go index be2dcf95d..77ded1902 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -2,12 +2,10 @@ package terminal import ( "context" - "crypto/tls" "encoding/base64" "encoding/hex" "fmt" "io/ioutil" - "net" "net/http" "strings" "sync/atomic" @@ -21,12 +19,9 @@ import ( "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" ) @@ -65,7 +60,7 @@ 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) *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 @@ -85,7 +80,6 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, permsMgr: permsMgr, macValidator: validator, superMacValidator: superMacValidator, - bufListener: bufListener, } p.grpcServer = grpc.NewServer( // From the grpxProxy doc: This codec is *crucial* to the @@ -162,7 +156,6 @@ type rpcProxy struct { macValidator macaroons.MacaroonValidator superMacValidator session.SuperMacaroonValidator - bufListener *bufconn.Listener superMacaroon string @@ -176,21 +169,9 @@ type rpcProxy struct { } // Start creates initial connection to lnd. -func (p *rpcProxy) Start() error { +func (p *rpcProxy) Start(lndConn *grpc.ClientConn) error { var err error - - // Setup the connection to lnd. - host, _, tlsPath, _, _ := p.cfg.lndConnectParams() - - // We use a bufconn to connect to lnd in integrated mode. - if p.cfg.LndMode == ModeIntegrated { - p.lndConn, err = dialBufConnBackend(p.bufListener) - } else { - p.lndConn, err = dialBackend("lnd", host, tlsPath) - } - if err != nil { - return fmt.Errorf("could not dial lnd: %v", err) - } + p.lndConn = lndConn // Make sure we can connect to all the daemons that are configured to be // running in remote mode. @@ -242,13 +223,6 @@ func (p *rpcProxy) hasStarted() bool { 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) @@ -662,68 +636,6 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, 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/terminal.go b/terminal.go index 57e668ec7..8c2918ae0 100644 --- a/terminal.go +++ b/terminal.go @@ -154,6 +154,7 @@ type LightningTerminal struct { wg sync.WaitGroup errQueue *queue.ConcurrentQueue[error] + lndConn *grpc.ClientConn lndClient *lndclient.GrpcLndServices basicClient lnrpc.LightningClient @@ -237,7 +238,7 @@ func (g *LightningTerminal) Run() error { g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) g.rpcProxy = newRpcProxy( - g.cfg, g, g.validateSuperMacaroon, g.permsMgr, bufRpcListener, + g.cfg, g, g.validateSuperMacaroon, g.permsMgr, ) g.accountService, err = accounts.NewService( filepath.Dir(g.cfg.MacaroonPath), g.errQueue.ChanIn(), @@ -429,11 +430,17 @@ func (g *LightningTerminal) Run() error { } }() + // Connect to LND. + g.lndConn, err = connectLND(g.cfg, bufRpcListener) + if err != nil { + return fmt.Errorf("could not connect to LND: %v", err) + } + // 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 { + if err := g.rpcProxy.Start(g.lndConn); err != nil { return fmt.Errorf("error starting lnd gRPC proxy server: %v", err) } @@ -1121,6 +1128,13 @@ func (g *LightningTerminal) shutdown() error { } } + if g.lndConn != nil { + if err := g.lndConn.Close(); err != nil { + log.Errorf("Error closing lnd connection: %v", err) + returnErr = err + } + } + if g.httpServer != nil { if err := g.httpServer.Close(); err != nil { log.Errorf("Error stopping UI server: %v", err) From 5b1e5fd5ee54fbed3fd45386c34f94df51204f34 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 31 Oct 2022 15:36:16 +0200 Subject: [PATCH 05/17] terminal: modularise the Run function A pure refactor commit. It just splits various parts of the main Run function out into helper functions in order to more clearly show the steps taken to set up LiT. --- terminal.go | 85 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/terminal.go b/terminal.go index 8c2918ae0..8cc365f28 100644 --- a/terminal.go +++ b/terminal.go @@ -475,12 +475,39 @@ 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) + log.Errorf("Could not set up LND clients: %w", err) return err } + // 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 + } + + err = g.startIntegratedDaemons(createDefaultMacaroons) + if err != nil { + log.Errorf("Could not start integrated daemons: %v", err) + return err + } + + err = g.startInternalSubServers(createDefaultMacaroons) + if err != nil { + return fmt.Errorf("could not start litd sub-servers: %v", err) + } + // Now block until we receive an error or the main shutdown signal. select { case err := <-g.loopServer.ErrChan: @@ -506,10 +533,8 @@ func (g *LightningTerminal) Run() error { 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 @@ -550,7 +575,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 @@ -593,7 +618,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 @@ -621,26 +647,19 @@ 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 { + return 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 - } +// startIntegratedDaemons starts all embedded daemons as external sub-servers +// that hook into the same gRPC and REST servers that lnd started. +func (g *LightningTerminal) startIntegratedDaemons( + createDefaultMacaroons bool) error { - // Both connection types are ready now, let's start our subservers if + // Both connection types are ready now, let's start our sub-servers if // they should be started locally as an integrated service. if !g.cfg.faradayRemote { log.Infof("Starting integrated faraday daemon") - err = g.faradayServer.StartAsSubserver( + err := g.faradayServer.StartAsSubserver( g.lndClient.LndServices, createDefaultMacaroons, ) if err != nil { @@ -651,7 +670,7 @@ func (g *LightningTerminal) startSubservers() error { if !g.cfg.loopRemote { log.Infof("Starting integrated loop daemon") - err = g.loopServer.StartAsSubserver( + err := g.loopServer.StartAsSubserver( g.lndClient, createDefaultMacaroons, ) if err != nil { @@ -662,7 +681,7 @@ func (g *LightningTerminal) startSubservers() error { if !g.cfg.poolRemote { log.Infof("Starting integrated pool daemon") - err = g.poolServer.StartAsSubserver( + err := g.poolServer.StartAsSubserver( g.basicClient, g.lndClient, createDefaultMacaroons, ) if err != nil { @@ -671,10 +690,21 @@ func (g *LightningTerminal) startSubservers() error { g.poolStarted = true } + return nil +} + +// 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, @@ -752,7 +782,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) } From 5bae925dcb90b15519a350e92a556c4a755a4564 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 Feb 2023 16:38:44 +0200 Subject: [PATCH 06/17] terminal: start the webserver as soon as possible Since all the relevant dependancies have been accouted for, we know move the starting of the main webserver to earlier on in the Run function. This is in preparation for later when the webserver will be "always-on" and will be used to server status information about the rest of the LiT subservers. --- terminal.go | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/terminal.go b/terminal.go index 8cc365f28..ceaf8af92 100644 --- a/terminal.go +++ b/terminal.go @@ -231,15 +231,30 @@ func (g *LightningTerminal) Run() error { return fmt.Errorf("could not create permissions manager") } + // Construct the rpcProxy. It must be initialised before the main web + // server is started. + g.rpcProxy = newRpcProxy(g.cfg, g, g.validateSuperMacaroon, g.permsMgr) + + // 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) + } + // 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.rpcProxy = newRpcProxy( - g.cfg, g, g.validateSuperMacaroon, g.permsMgr, - ) g.accountService, err = accounts.NewService( filepath.Dir(g.cfg.MacaroonPath), g.errQueue.ChanIn(), ) @@ -338,12 +353,14 @@ func (g *LightningTerminal) Run() error { // Call the "real" main in a nested manner so the defers will properly // be executed in the case of a graceful shutdown. - readyChan := make(chan struct{}) - bufReadyChan := make(chan struct{}) - unlockChan := make(chan struct{}) - 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{{ @@ -392,14 +409,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 @@ -437,16 +446,11 @@ func (g *LightningTerminal) Run() error { } // 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. + // 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. From 82230fdb834d4a9d873c936fb0f9a02b9c1e17cb Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 28 Oct 2022 14:50:41 +0200 Subject: [PATCH 07/17] litrpc: add Status server protos Add the protos for a new Status server that can be used to query the status of a number of LiT's subservers. --- litrpc/lit-status.pb.go | 293 +++++++++++++++++++++++++++++++++++ litrpc/lit-status.proto | 27 ++++ litrpc/lit-status_grpc.pb.go | 101 ++++++++++++ litrpc/status.pb.json.go | 48 ++++++ 4 files changed, 469 insertions(+) create mode 100644 litrpc/lit-status.pb.go create mode 100644 litrpc/lit-status.proto create mode 100644 litrpc/lit-status_grpc.pb.go create mode 100644 litrpc/status.pb.json.go 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/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) + } +} From 00fda361df3a7236acd4860088463b6f47ec8d1f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 Feb 2023 17:00:09 +0200 Subject: [PATCH 08/17] multi: implement the status server --- perms/permissions.go | 11 ++++ rpc_proxy.go | 12 ++++- status_server.go | 120 +++++++++++++++++++++++++++++++++++++++++++ terminal.go | 11 +++- 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 status_server.go diff --git a/perms/permissions.go b/perms/permissions.go index 09344f95d..af1f5795d 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -92,6 +92,14 @@ var ( }}, } + // 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 // require any macaroon authentication. whiteListedLNDMethods = map[string][]bakery.Op{ @@ -174,6 +182,9 @@ func NewManager(withAllSubServers bool) (*Manager, error) { permissions[loopPerms] = loop.RequiredPermissions permissions[poolPerms] = pool.RequiredPermissions permissions[litPerms] = LitPermissions + for k, v := range MacaroonWhitelist { + permissions[litPerms][k] = v + } permissions[lndPerms] = lnd.MainRPCServerPermissions() for k, v := range whiteListedLNDMethods { permissions[lndPerms][k] = v diff --git a/rpc_proxy.go b/rpc_proxy.go index 77ded1902..f4bd72e46 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -219,6 +219,14 @@ func (p *rpcProxy) hasStarted() bool { return atomic.LoadInt32(&p.started) == 1 } +// 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), + ) +} + // Stop shuts down the lnd connection. func (p *rpcProxy) Stop() error { p.grpcServer.Stop() @@ -377,7 +385,7 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - if !p.hasStarted() { + if !p.hasStarted() && !isStatusReq(info.FullMethod) { return nil, ErrWaitingToStart } @@ -422,7 +430,7 @@ func (p *rpcProxy) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if !p.hasStarted() { + if !p.hasStarted() && !isStatusReq(info.FullMethod) { return ErrWaitingToStart } diff --git a/status_server.go b/status_server.go new file mode 100644 index 000000000..de979cd50 --- /dev/null +++ b/status_server.go @@ -0,0 +1,120 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + + "github.com/lightninglabs/lightning-terminal/litrpc" +) + +// 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/terminal.go b/terminal.go index ceaf8af92..3725090fa 100644 --- a/terminal.go +++ b/terminal.go @@ -158,6 +158,8 @@ type LightningTerminal struct { lndClient *lndclient.GrpcLndServices basicClient lnrpc.LightningClient + statusServer *statusServer + faradayServer *frdrpcserver.RPCServer faradayStarted bool @@ -196,7 +198,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 @@ -872,6 +876,7 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, 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) @@ -926,6 +931,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 From e998ad439fab4a95768a40b2406a4d82de034a69 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 15 Feb 2023 17:11:12 +0200 Subject: [PATCH 09/17] cmd/litcli: commands for status server --- cmd/litcli/main.go | 1 + cmd/litcli/status.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 cmd/litcli/status.go diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index 1bb599123..893be7fef 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -76,6 +76,7 @@ func main() { 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 { 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 +} From 79347305c60293ed7133dda9e1e07c42d7a215c2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 15:12:01 +0200 Subject: [PATCH 10/17] multi: refactor perms IsURI methods In this commit, the various IsURI methods of the permissions manager are removed and replaced with a single IsSubServerURI method that gets passed both the URI and the subserver that should be checked. This will greatly simplifiy implementing a subserver manager in an upcoming commit. --- perms/permissions.go | 89 +++++++++++++++++++------------------------- rpc_proxy.go | 41 ++++++++++++++------ terminal.go | 8 ++-- 3 files changed, 71 insertions(+), 67 deletions(-) diff --git a/perms/permissions.go b/perms/permissions.go index af1f5795d..b072e684a 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -135,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. @@ -158,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 @@ -177,17 +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 := 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[litPerms][k] = v + permissions[SubServerLit][k] = v } - permissions[lndPerms] = lnd.MainRPCServerPermissions() + 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 @@ -214,7 +214,7 @@ func NewManager(withAllSubServers bool) (*Manager, error) { if withAllSubServers || lndAutoCompiledSubServers[name] { - permissions[lndPerms][key] = value + permissions[SubServerLnd][key] = value } } } @@ -352,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] @@ -382,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 f4bd72e46..f8b44e220 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -349,6 +349,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 @@ -357,17 +359,25 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, // handled by the integrated daemons that are hooking into lnd's // gRPC server. switch { - case p.permsMgr.IsFaradayURI(requestURI) && p.cfg.faradayRemote: + case isSubServerURI(perms.SubServerFaraday, requestURI) && + p.cfg.faradayRemote: + return outCtx, p.faradayConn, nil - case p.permsMgr.IsLoopURI(requestURI) && p.cfg.loopRemote: + case isSubServerURI(perms.SubServerLoop, requestURI) && + p.cfg.loopRemote: + return outCtx, p.loopConn, nil - case p.permsMgr.IsPoolURI(requestURI) && p.cfg.poolRemote: + case isSubServerURI(perms.SubServerPool, requestURI) && + p.cfg.poolRemote: + return outCtx, p.poolConn, nil // Calls to LiT session RPC aren't allowed in some cases. - case p.permsMgr.IsLitURI(requestURI) && !allowLitRPC: + case isSubServerURI(perms.SubServerLit, requestURI) && + !allowLitRPC: + return outCtx, nil, status.Errorf( codes.Unimplemented, "unknown service %s", requestURI, @@ -523,31 +533,31 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, macData []byte ) switch { - case p.permsMgr.IsLndURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() - case p.permsMgr.IsFaradayURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerFaraday, requestURI): if p.cfg.faradayRemote { macPath = p.cfg.Remote.Faraday.MacaroonPath } else { macPath = p.cfg.Faraday.MacaroonPath } - case p.permsMgr.IsLoopURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerLoop, requestURI): if p.cfg.loopRemote { macPath = p.cfg.Remote.Loop.MacaroonPath } else { macPath = p.cfg.Loop.MacaroonPath } - case p.permsMgr.IsPoolURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerPool, requestURI): if p.cfg.poolRemote { macPath = p.cfg.Remote.Pool.MacaroonPath } else { macPath = p.cfg.Pool.MacaroonPath } - case p.permsMgr.IsLitURI(requestURI): + case p.permsMgr.IsSubServerURI(perms.SubServerLit, requestURI): macPath = p.cfg.MacaroonPath default: @@ -624,18 +634,25 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, // Is this actually a request that goes to a daemon that is running // remotely? + isSubServerURI := p.permsMgr.IsSubServerURI switch { - case p.permsMgr.IsFaradayURI(fullMethod) && p.cfg.faradayRemote: + case isSubServerURI(perms.SubServerFaraday, fullMethod) && + p.cfg.faradayRemote: + return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Faraday.MacaroonPath, )) - case p.permsMgr.IsLoopURI(fullMethod) && p.cfg.loopRemote: + case isSubServerURI(perms.SubServerLoop, fullMethod) && + p.cfg.loopRemote: + return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Loop.MacaroonPath, )) - case p.permsMgr.IsPoolURI(fullMethod) && p.cfg.poolRemote: + case isSubServerURI(perms.SubServerPool, fullMethod) && + p.cfg.poolRemote: + return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Pool.MacaroonPath, )) diff --git a/terminal.go b/terminal.go index 3725090fa..b25521b4c 100644 --- a/terminal.go +++ b/terminal.go @@ -964,7 +964,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, // 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): + case g.permsMgr.IsSubServerURI(perms.SubServerFaraday, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.faradayRemote { @@ -988,7 +988,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case g.permsMgr.IsLoopURI(fullMethod): + case g.permsMgr.IsSubServerURI(perms.SubServerLoop, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.loopRemote { @@ -1012,7 +1012,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case g.permsMgr.IsPoolURI(fullMethod): + case g.permsMgr.IsSubServerURI(perms.SubServerPool, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.poolRemote { @@ -1036,7 +1036,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case g.permsMgr.IsLitURI(fullMethod): + case g.permsMgr.IsSubServerURI(perms.SubServerLit, fullMethod): if !g.macaroonServiceStarted { return fmt.Errorf("the macaroon service has not " + "started yet") From 77a9db737bbdf555164fe527527226e71184dd67 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 15:24:54 +0200 Subject: [PATCH 11/17] multi: define a SubServer and add a subServer manager Define a SubServer interface and a subServer manager --- subserver_mgr.go | 440 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 subserver_mgr.go 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 +} From 26415d47a88c197b3cb0ac101da6891ee46ce2f3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 15:42:12 +0200 Subject: [PATCH 12/17] multi: plug in subserverMgr into LightningTerminal --- rpc_proxy.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++++--- terminal.go | 32 +++++++++++++++++++++- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/rpc_proxy.go b/rpc_proxy.go index f8b44e220..871efb816 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -60,7 +60,8 @@ func (e *proxyErr) Unwrap() error { // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, superMacValidator session.SuperMacaroonValidator, - permsMgr *perms.Manager) *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 @@ -80,6 +81,8 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, permsMgr: permsMgr, macValidator: validator, superMacValidator: superMacValidator, + statusServer: statusServer, + subServerMgr: subServerMgr, } p.grpcServer = grpc.NewServer( // From the grpxProxy doc: This codec is *crucial* to the @@ -164,6 +167,9 @@ type rpcProxy struct { loopConn *grpc.ClientConn poolConn *grpc.ClientConn + statusServer *statusServer + subServerMgr *subServerMgr + grpcServer *grpc.Server grpcWebProxy *grpcweb.WrappedGrpcServer } @@ -315,6 +321,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) @@ -358,6 +370,11 @@ 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. + handled, conn := p.subServerMgr.GetRemoteConn(requestURI) + if handled { + return outCtx, conn, nil + } + switch { case isSubServerURI(perms.SubServerFaraday, requestURI) && p.cfg.faradayRemote: @@ -405,6 +422,12 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, "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 @@ -434,6 +457,35 @@ 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 + + isSubServerURI := p.permsMgr.IsSubServerURI + + if isSubServerURI(perms.SubServerLit, requestURI) || + isSubServerURI(perms.SubServerLnd, requestURI) || + isSubServerURI(perms.SubServerPool, requestURI) || + isSubServerURI(perms.SubServerLoop, requestURI) || + isSubServerURI(perms.SubServerFaraday, requestURI) { + + return nil + } + + handled, system := p.subServerMgr.HandledBy(requestURI) + if !handled { + 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{}, @@ -450,6 +502,12 @@ func (p *rpcProxy) StreamServerInterceptor(srv interface{}, "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 @@ -529,10 +587,14 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, } var ( - macPath string - macData []byte + handled, path = p.subServerMgr.MacaroonPath(requestURI) + macPath string + macData []byte ) switch { + case handled: + macPath = path + case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() @@ -632,10 +694,17 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, return nil, err } + isSubServerURI := p.permsMgr.IsSubServerURI + // Is this actually a request that goes to a daemon that is running // remotely? - isSubServerURI := p.permsMgr.IsSubServerURI + handled, macBytes, err := p.subServerMgr.ReadRemoteMacaroon(fullMethod) + if handled { + return macBytes, err + } + switch { + case isSubServerURI(perms.SubServerFaraday, fullMethod) && p.cfg.faradayRemote: diff --git a/terminal.go b/terminal.go index b25521b4c..07a99d80a 100644 --- a/terminal.go +++ b/terminal.go @@ -159,6 +159,7 @@ type LightningTerminal struct { basicClient lnrpc.LightningClient statusServer *statusServer + subServerMgr *subServerMgr faradayServer *frdrpcserver.RPCServer faradayStarted bool @@ -235,9 +236,14 @@ func (g *LightningTerminal) Run() error { return fmt.Errorf("could not create permissions manager") } + 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) + g.rpcProxy = newRpcProxy( + 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. @@ -449,6 +455,10 @@ func (g *LightningTerminal) Run() error { return fmt.Errorf("could not connect to LND: %v", err) } + // 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. if err := g.rpcProxy.Start(g.lndConn); err != nil { @@ -663,6 +673,10 @@ func (g *LightningTerminal) setUpLNDClients() error { func (g *LightningTerminal) startIntegratedDaemons( createDefaultMacaroons bool) error { + g.subServerMgr.StartIntegratedServers( + g.basicClient, g.lndClient, createDefaultMacaroons, + ) + // Both connection types are ready now, let's start our sub-servers if // they should be started locally as an integrated service. if !g.cfg.faradayRemote { @@ -856,6 +870,8 @@ func (g *LightningTerminal) RegisterGrpcSubserver(server *grpc.Server) error { func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, withLitRPC bool) { + g.subServerMgr.RegisterRPCServices(server) + // 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 @@ -960,10 +976,19 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, ) } + handled, err := g.subServerMgr.ValidateMacaroon( + ctx, requiredPermissions, fullMethod, + ) + // Validate all macaroons for services that are running in the local // process. Calls that we proxy to a remote host don't need to be // checked as they'll have their own interceptor. switch { + case handled: + if err != nil { + return err + } + case g.permsMgr.IsSubServerURI(perms.SubServerFaraday, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. @@ -1114,6 +1139,11 @@ func (g *LightningTerminal) shutdown() error { } } + err := g.subServerMgr.Stop() + if err != nil { + returnErr = err + } + if g.autopilotClient != nil { g.autopilotClient.Stop() } From fbf009a67cdd2539f4a6998f60edef32871691da Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 15:59:19 +0200 Subject: [PATCH 13/17] lit: add a faraday SubServer --- rpc_proxy.go | 49 ++---------------------- subservers.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.go | 61 +++++------------------------- 3 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 subservers.go diff --git a/rpc_proxy.go b/rpc_proxy.go index 871efb816..752cc05a0 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -162,10 +162,9 @@ type rpcProxy struct { superMacaroon string - lndConn *grpc.ClientConn - faradayConn *grpc.ClientConn - loopConn *grpc.ClientConn - poolConn *grpc.ClientConn + lndConn *grpc.ClientConn + loopConn *grpc.ClientConn + poolConn *grpc.ClientConn statusServer *statusServer subServerMgr *subServerMgr @@ -181,19 +180,6 @@ func (p *rpcProxy) Start(lndConn *grpc.ClientConn) error { // 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) - } - } - if p.cfg.loopRemote { p.loopConn, err = dialBackend( "loop", p.cfg.Remote.Loop.RPCServer, @@ -237,13 +223,6 @@ func isStatusReq(uri string) bool { func (p *rpcProxy) Stop() error { p.grpcServer.Stop() - if p.faradayConn != nil { - if err := p.faradayConn.Close(); err != nil { - log.Errorf("Error closing faraday connection: %v", err) - return err - } - } - if p.loopConn != nil { if err := p.loopConn.Close(); err != nil { log.Errorf("Error closing loop connection: %v", err) @@ -376,11 +355,6 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, } switch { - case isSubServerURI(perms.SubServerFaraday, requestURI) && - p.cfg.faradayRemote: - - return outCtx, p.faradayConn, nil - case isSubServerURI(perms.SubServerLoop, requestURI) && p.cfg.loopRemote: @@ -467,8 +441,7 @@ func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { if isSubServerURI(perms.SubServerLit, requestURI) || isSubServerURI(perms.SubServerLnd, requestURI) || isSubServerURI(perms.SubServerPool, requestURI) || - isSubServerURI(perms.SubServerLoop, requestURI) || - isSubServerURI(perms.SubServerFaraday, requestURI) { + isSubServerURI(perms.SubServerLoop, requestURI) { return nil } @@ -598,13 +571,6 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() - case p.permsMgr.IsSubServerURI(perms.SubServerFaraday, requestURI): - if p.cfg.faradayRemote { - macPath = p.cfg.Remote.Faraday.MacaroonPath - } else { - macPath = p.cfg.Faraday.MacaroonPath - } - case p.permsMgr.IsSubServerURI(perms.SubServerLoop, requestURI): if p.cfg.loopRemote { macPath = p.cfg.Remote.Loop.MacaroonPath @@ -705,13 +671,6 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, switch { - case isSubServerURI(perms.SubServerFaraday, fullMethod) && - p.cfg.faradayRemote: - - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Faraday.MacaroonPath, - )) - case isSubServerURI(perms.SubServerLoop, fullMethod) && p.cfg.loopRemote: diff --git a/subservers.go b/subservers.go new file mode 100644 index 000000000..3b4433a75 --- /dev/null +++ b/subservers.go @@ -0,0 +1,103 @@ +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/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 +} diff --git a/terminal.go b/terminal.go index 07a99d80a..5c72c2849 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" @@ -161,9 +160,6 @@ type LightningTerminal struct { statusServer *statusServer subServerMgr *subServerMgr - faradayServer *frdrpcserver.RPCServer - faradayStarted bool - autopilotClient autopilotserver.Autopilot ruleMgrs rules.ManagerSet @@ -262,7 +258,7 @@ func (g *LightningTerminal) Run() error { // Create the instances of our subservers now so we can hook them up to // lnd once it's fully started. - g.faradayServer = frdrpcserver.NewRPCServer(g.cfg.faradayRpcConfig) + g.initSubServers() g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) g.accountService, err = accounts.NewService( @@ -679,17 +675,6 @@ func (g *LightningTerminal) startIntegratedDaemons( // Both connection types are ready now, let's start our sub-servers 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( @@ -876,10 +861,6 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, // 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) } @@ -989,30 +970,6 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, return err } - case g.permsMgr.IsSubServerURI(perms.SubServerFaraday, 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.IsSubServerURI(perms.SubServerLoop, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. @@ -1117,13 +1074,6 @@ func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, func (g *LightningTerminal) shutdown() 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 { @@ -1527,6 +1477,15 @@ 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, + )) +} + // BakeSuperMacaroon uses the lnd client to bake a macaroon that can include // permissions for multiple daemons. func BakeSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient, From b9863f1f612215feb721efacb8391786cb05c50d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 16:08:12 +0200 Subject: [PATCH 14/17] lit: add a loop SubServer --- rpc_proxy.go | 45 ++--------------------- subservers.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.go | 70 +++++------------------------------ 3 files changed, 112 insertions(+), 103 deletions(-) diff --git a/rpc_proxy.go b/rpc_proxy.go index 752cc05a0..c6b8b07fc 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -163,7 +163,6 @@ type rpcProxy struct { superMacaroon string lndConn *grpc.ClientConn - loopConn *grpc.ClientConn poolConn *grpc.ClientConn statusServer *statusServer @@ -180,16 +179,6 @@ func (p *rpcProxy) Start(lndConn *grpc.ClientConn) error { // Make sure we can connect to all the daemons that are configured to be // running in remote mode. - 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) - } - } - if p.cfg.poolRemote { p.poolConn, err = dialBackend( "pool", p.cfg.Remote.Pool.RPCServer, @@ -223,13 +212,6 @@ func isStatusReq(uri string) bool { func (p *rpcProxy) Stop() error { p.grpcServer.Stop() - if p.loopConn != nil { - if err := p.loopConn.Close(); err != nil { - log.Errorf("Error closing loop connection: %v", err) - return err - } - } - if p.poolConn != nil { if err := p.poolConn.Close(); err != nil { log.Errorf("Error closing pool connection: %v", err) @@ -355,10 +337,6 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, } switch { - case isSubServerURI(perms.SubServerLoop, requestURI) && - p.cfg.loopRemote: - - return outCtx, p.loopConn, nil case isSubServerURI(perms.SubServerPool, requestURI) && p.cfg.poolRemote: @@ -440,8 +418,7 @@ func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { if isSubServerURI(perms.SubServerLit, requestURI) || isSubServerURI(perms.SubServerLnd, requestURI) || - isSubServerURI(perms.SubServerPool, requestURI) || - isSubServerURI(perms.SubServerLoop, requestURI) { + isSubServerURI(perms.SubServerPool, requestURI) { return nil } @@ -571,13 +548,6 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() - case p.permsMgr.IsSubServerURI(perms.SubServerLoop, requestURI): - if p.cfg.loopRemote { - macPath = p.cfg.Remote.Loop.MacaroonPath - } else { - macPath = p.cfg.Loop.MacaroonPath - } - case p.permsMgr.IsSubServerURI(perms.SubServerPool, requestURI): if p.cfg.poolRemote { macPath = p.cfg.Remote.Pool.MacaroonPath @@ -669,17 +639,8 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, return macBytes, err } - switch { - - case isSubServerURI(perms.SubServerLoop, fullMethod) && - p.cfg.loopRemote: - - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Loop.MacaroonPath, - )) - - case isSubServerURI(perms.SubServerPool, fullMethod) && - p.cfg.poolRemote: + if isSubServerURI(perms.SubServerPool, fullMethod) && + p.cfg.poolRemote { return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Pool.MacaroonPath, diff --git a/subservers.go b/subservers.go index 3b4433a75..08b6fc7e1 100644 --- a/subservers.go +++ b/subservers.go @@ -6,6 +6,8 @@ import ( "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/lightningnetwork/lnd/lnrpc" "google.golang.org/grpc" ) @@ -101,3 +103,101 @@ func (f *faradaySubServer) ServerErrChan() chan error { 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 +} diff --git a/terminal.go b/terminal.go index 5c72c2849..5df989bda 100644 --- a/terminal.go +++ b/terminal.go @@ -32,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" @@ -164,9 +163,6 @@ type LightningTerminal struct { ruleMgrs rules.ManagerSet - loopServer *loopd.Daemon - loopStarted bool - poolServer *pool.Server poolStarted bool @@ -259,7 +255,6 @@ func (g *LightningTerminal) Run() error { // Create the instances of our subservers now so we can hook them up to // lnd once it's fully started. g.initSubServers() - g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) g.accountService, err = accounts.NewService( filepath.Dir(g.cfg.MacaroonPath), g.errQueue.ChanIn(), @@ -352,9 +347,8 @@ 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" + // Overwrite the pool daemon's user agent name so it sends "litd" + // instead of and "poold". pool.SetAgentName("litd") // Call the "real" main in a nested manner so the defers will properly @@ -524,12 +518,6 @@ func (g *LightningTerminal) Run() error { // 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 { @@ -675,17 +663,6 @@ func (g *LightningTerminal) startIntegratedDaemons( // Both connection types are ready now, let's start our sub-servers if // they should be started locally as an integrated service. - 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 - } - if !g.cfg.poolRemote { log.Infof("Starting integrated pool daemon") err := g.poolServer.StartAsSubserver( @@ -861,10 +838,6 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, // 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.loopRemote { - looprpc.RegisterSwapClientServer(server, g.loopServer) - } - if !g.cfg.poolRemote { poolrpc.RegisterTraderServer(server, g.poolServer) } @@ -970,30 +943,6 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, return err } - case g.permsMgr.IsSubServerURI(perms.SubServerLoop, 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.IsSubServerURI(perms.SubServerPool, fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. @@ -1074,14 +1023,6 @@ func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, func (g *LightningTerminal) shutdown() error { var returnErr error - 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) @@ -1484,6 +1425,13 @@ func (g *LightningTerminal) initSubServers() { 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, + )) } // BakeSuperMacaroon uses the lnd client to bake a macaroon that can include From ae563c6a2ea0a264ffd2531578b3c51a47cce862 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 16:20:11 +0200 Subject: [PATCH 15/17] lit: add a pool SubServer --- rpc_proxy.go | 59 +++------------------------- subservers.go | 91 +++++++++++++++++++++++++++++++++++++++++++ terminal.go | 106 +++++++++----------------------------------------- 3 files changed, 116 insertions(+), 140 deletions(-) diff --git a/rpc_proxy.go b/rpc_proxy.go index c6b8b07fc..21c5f49f5 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -162,8 +162,7 @@ type rpcProxy struct { superMacaroon string - lndConn *grpc.ClientConn - poolConn *grpc.ClientConn + lndConn *grpc.ClientConn statusServer *statusServer subServerMgr *subServerMgr @@ -174,21 +173,8 @@ type rpcProxy struct { // Start creates initial connection to lnd. func (p *rpcProxy) Start(lndConn *grpc.ClientConn) error { - var err error p.lndConn = lndConn - // Make sure we can connect to all the daemons that are configured to be - // running in remote mode. - 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) - } - } - atomic.CompareAndSwapInt32(&p.started, 0, 1) return nil @@ -212,13 +198,6 @@ func isStatusReq(uri string) bool { func (p *rpcProxy) Stop() error { p.grpcServer.Stop() - if p.poolConn != nil { - if err := p.poolConn.Close(); err != nil { - log.Errorf("Error closing pool connection: %v", err) - return err - } - } - return nil } @@ -336,25 +315,17 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, return outCtx, conn, nil } - switch { - - case isSubServerURI(perms.SubServerPool, requestURI) && - p.cfg.poolRemote: - - return outCtx, p.poolConn, nil - // Calls to LiT session RPC aren't allowed in some cases. - case isSubServerURI(perms.SubServerLit, 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 } } @@ -417,8 +388,7 @@ func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { isSubServerURI := p.permsMgr.IsSubServerURI if isSubServerURI(perms.SubServerLit, requestURI) || - isSubServerURI(perms.SubServerLnd, requestURI) || - isSubServerURI(perms.SubServerPool, requestURI) { + isSubServerURI(perms.SubServerLnd, requestURI) { return nil } @@ -548,13 +518,6 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() - case p.permsMgr.IsSubServerURI(perms.SubServerPool, requestURI): - if p.cfg.poolRemote { - macPath = p.cfg.Remote.Pool.MacaroonPath - } else { - macPath = p.cfg.Pool.MacaroonPath - } - case p.permsMgr.IsSubServerURI(perms.SubServerLit, requestURI): macPath = p.cfg.MacaroonPath @@ -630,8 +593,6 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, return nil, err } - isSubServerURI := p.permsMgr.IsSubServerURI - // Is this actually a request that goes to a daemon that is running // remotely? handled, macBytes, err := p.subServerMgr.ReadRemoteMacaroon(fullMethod) @@ -639,14 +600,6 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, return macBytes, err } - if isSubServerURI(perms.SubServerPool, fullMethod) && - p.cfg.poolRemote { - - return readMacaroon(lncfg.CleanAndExpandPath( - p.cfg.Remote.Pool.MacaroonPath, - )) - } - return nil, nil } diff --git a/subservers.go b/subservers.go index 08b6fc7e1..cb7457dda 100644 --- a/subservers.go +++ b/subservers.go @@ -8,6 +8,8 @@ import ( "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" ) @@ -201,3 +203,92 @@ func (l *loopSubServer) ServerErrChan() chan error { 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 5df989bda..50ca523dc 100644 --- a/terminal.go +++ b/terminal.go @@ -163,9 +163,6 @@ type LightningTerminal struct { ruleMgrs rules.ManagerSet - poolServer *pool.Server - poolStarted bool - rpcProxy *rpcProxy httpServer *http.Server @@ -255,7 +252,6 @@ func (g *LightningTerminal) Run() error { // Create the instances of our subservers now so we can hook them up to // lnd once it's fully started. g.initSubServers() - g.poolServer = pool.NewServer(g.cfg.Pool) g.accountService, err = accounts.NewService( filepath.Dir(g.cfg.MacaroonPath), g.errQueue.ChanIn(), ) @@ -347,10 +343,6 @@ func (g *LightningTerminal) Run() error { "server: %v", err) } - // Overwrite the pool daemon's user agent name so it sends "litd" - // instead of and "poold". - 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. var ( @@ -505,11 +497,11 @@ func (g *LightningTerminal) Run() error { createDefaultMacaroons = !macService.StatelessInit } - err = g.startIntegratedDaemons(createDefaultMacaroons) - if err != nil { - log.Errorf("Could not start integrated daemons: %v", err) - return err - } + // 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 { @@ -652,31 +644,6 @@ func (g *LightningTerminal) setUpLNDClients() error { return nil } -// startIntegratedDaemons starts all embedded daemons as external sub-servers -// that hook into the same gRPC and REST servers that lnd started. -func (g *LightningTerminal) startIntegratedDaemons( - createDefaultMacaroons bool) error { - - g.subServerMgr.StartIntegratedServers( - g.basicClient, g.lndClient, createDefaultMacaroons, - ) - - // Both connection types are ready now, let's start our sub-servers if - // they should be started locally as an integrated service. - 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 - } - - return nil -} - // startInternalSubServers starts all Litd specific sub-servers. func (g *LightningTerminal) startInternalSubServers( createDefaultMacaroons bool) error { @@ -834,14 +801,6 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, g.subServerMgr.RegisterRPCServices(server) - // 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.poolRemote { - poolrpc.RegisterTraderServer(server, g.poolServer) - } - if withLitRPC { litrpc.RegisterSessionsServer(server, g.sessionRpcServer) litrpc.RegisterAccountsServer(server, g.accountRpcServer) @@ -930,44 +889,17 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, ) } - handled, err := g.subServerMgr.ValidateMacaroon( - ctx, requiredPermissions, fullMethod, - ) - // Validate all macaroons for services that are running in the local // process. Calls that we proxy to a remote host don't need to be // checked as they'll have their own interceptor. - switch { - case handled: - if err != nil { - return err - } - - case g.permsMgr.IsSubServerURI(perms.SubServerPool, 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.IsSubServerURI(perms.SubServerLit, fullMethod): + if g.permsMgr.IsSubServerURI(perms.SubServerLit, fullMethod) { if !g.macaroonServiceStarted { return fmt.Errorf("the macaroon service has not " + "started yet") @@ -1023,13 +955,6 @@ func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, func (g *LightningTerminal) shutdown() error { var returnErr error - 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 @@ -1432,6 +1357,13 @@ func (g *LightningTerminal) initSubServers() { 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 From 76e11a966afa0f166be02a62271fc3967476c64f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 16:37:01 +0200 Subject: [PATCH 16/17] multi: track LiT and LND status With this commit, we now also track the start-up state of LiT and LiT's connection to LND. These differ from the other sub-servers (loop, pool & faraday) because a failure to start LiT or LND is fatal and so should stop the rest of the start-process, however, we still want the webserver to continue serving the new Status server so that the UI can query the start-up status of LND and LiT. So: if any errors occur while starting/connecting to LND or any other errors occur while starting any of LiTs other processes, then we throw an error but we dont kill the main LiT process. The main process is only killed upon receiving a shutdown signal. --- itest/litd_node.go | 125 ++++++++++++++++++++++++++++++++++++++++----- rpc_proxy.go | 22 +++++--- status_server.go | 5 ++ terminal.go | 123 ++++++++++++++++++++++++++++++-------------- 4 files changed, 218 insertions(+), 57 deletions(-) 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/rpc_proxy.go b/rpc_proxy.go index 21c5f49f5..a798814cd 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -385,16 +385,24 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { var system string - isSubServerURI := p.permsMgr.IsSubServerURI + handled, subServerName := p.subServerMgr.HandledBy(requestURI) + switch { + case handled: + system = subServerName - if isSubServerURI(perms.SubServerLit, requestURI) || - isSubServerURI(perms.SubServerLnd, requestURI) { + case p.permsMgr.IsSubServerURI(perms.SubServerLnd, requestURI): + system = LNDSubServer - return nil - } + 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. + if isStatusReq(requestURI) { + return nil + } - handled, system := p.subServerMgr.HandledBy(requestURI) - if !handled { + default: return fmt.Errorf("unknown gRPC web request: %v", requestURI) } diff --git a/status_server.go b/status_server.go index de979cd50..1f9e2d826 100644 --- a/status_server.go +++ b/status_server.go @@ -8,6 +8,11 @@ import ( "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 { diff --git a/terminal.go b/terminal.go index 50ca523dc..5152d6345 100644 --- a/terminal.go +++ b/terminal.go @@ -225,6 +225,9 @@ func (g *LightningTerminal) Run() error { return fmt.Errorf("could not create permissions manager") } + 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 @@ -249,9 +252,49 @@ func (g *LightningTerminal) Run() error { 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(), ) @@ -380,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) { @@ -413,19 +454,25 @@ 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) } @@ -434,7 +481,11 @@ func (g *LightningTerminal) Run() error { // Connect to LND. g.lndConn, err = connectLND(g.cfg, bufRpcListener) if err != nil { - return fmt.Errorf("could not connect to LND: %v", err) + 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 @@ -462,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 { @@ -478,8 +535,11 @@ func (g *LightningTerminal) Run() error { // Set up all the LND clients required by LiT. err = g.setUpLNDClients() if err != nil { - log.Errorf("Could not set up LND clients: %w", 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 @@ -508,6 +568,9 @@ func (g *LightningTerminal) Run() error { 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 { @@ -518,10 +581,14 @@ func (g *LightningTerminal) Run() error { } 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 @@ -951,8 +1018,9 @@ 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 err := g.subServerMgr.Stop() @@ -1011,13 +1079,6 @@ 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.lndConn != nil { if err := g.lndConn.Close(); err != nil { log.Errorf("Error closing lnd connection: %v", err) @@ -1025,18 +1086,6 @@ func (g *LightningTerminal) shutdown() error { } } - if g.httpServer != nil { - if err := g.httpServer.Close(); err != nil { - log.Errorf("Error stopping UI server: %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. From 486fae3a1e844668c12ca936dff9f103863da8bb Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 16 Feb 2023 16:40:35 +0200 Subject: [PATCH 17/17] rpc_proxy: allow Litd's StopDaemon call at any time --- rpc_proxy.go | 15 +++++++++++++-- terminal.go | 5 ++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/rpc_proxy.go b/rpc_proxy.go index a798814cd..2dca09169 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -194,6 +194,16 @@ func isStatusReq(uri string) bool { ) } +// 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() @@ -397,8 +407,9 @@ func (p *rpcProxy) checkSubSystemStarted(requestURI string) error { system = LitdSubServer // If the request is for the status server, then we allow the - // request even if Lit has not properly started. - if isStatusReq(requestURI) { + // 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 } diff --git a/terminal.go b/terminal.go index 5152d6345..958bbeea0 100644 --- a/terminal.go +++ b/terminal.go @@ -573,11 +573,10 @@ func (g *LightningTerminal) start() error { // Now block until we receive an error or the main shutdown signal. select { - 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: