diff --git a/README.md b/README.md index c9c44ab..57f9878 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,25 @@ Faraday connects to a single instance of lnd. It requires access to macaroons fo By default, faraday runs on mainnet. The `--network` flag can be used to run in test environments. -## Transport security +## Authentication and transport security -The gRPC and REST connections of `faraday` are encrypted with TLS the same way -`lnd` is. +The gRPC and REST connections of `faraday` are encrypted with TLS and secured +with macaroon authentication the same way `lnd` is. -If no custom loop directory is set then the TLS certificate is stored in -`~/.faraday//tls.cert`. +If no custom faraday directory is set then the TLS certificate is stored in +`~/.faraday//tls.cert` and the base macaroon in +`~/.faraday//faraday.macaroon`. -The `frcli` command will pick up the file automatically on mainnet if no custom -loop directory is used. For other networks it should be sufficient to add the -`--network` flag to tell the CLI in what sub directory to look for the files. +The `frcli` command will pick up these file automatically on mainnet if no +custom faraday directory is used. For other networks it should be sufficient to +add the `--network` flag to tell the CLI in what sub directory to look for the +files. + +For more information on macaroons, +[see the macaroon documentation of lnd.](https://github.com/lightningnetwork/lnd/blob/master/docs/macaroons.md) + +**NOTE**: Faraday's macaroons are independent from `lnd`'s. The same macaroon +cannot be used for both `faraday` and `lnd`. ### Chain Backend Faraday offers node accounting services which require access to a Bitcoin node with `--txindex` set so that it can perform transaction lookup. Currently the `CloseReport` endpoint requires this connection, and will fail if it is not present. It is *strongly recommended* to provide this connection when utilizing the `NodeAudit` endpoint, but it is not required. This connection is *optional*, and all other endpoints will function if it is not configured. diff --git a/cmd/frcli/main.go b/cmd/frcli/main.go index ae0ea8a..724579a 100644 --- a/cmd/frcli/main.go +++ b/cmd/frcli/main.go @@ -28,9 +28,9 @@ var ( Value: faraday.DefaultTLSCertPath, } macaroonPathFlag = cli.StringFlag{ - Name: "macaroonpath", - Usage: "path to macaroon file, only needed if faraday runs " + - "in the same process as lnd (GrUB)", + Name: "macaroonpath", + Usage: "path to macaroon file", + Value: faraday.DefaultMacaroonPath, } ) diff --git a/cmd/frcli/utils.go b/cmd/frcli/utils.go index 3225857..5382d09 100644 --- a/cmd/frcli/utils.go +++ b/cmd/frcli/utils.go @@ -96,6 +96,12 @@ func getClientConn(ctx *cli.Context) *grpc.ClientConn { fatal(err) } + // We always need to send a macaroon. + macOption, err := readMacaroon(macaroonPath) + if err != nil { + fatal(err) + } + // We need to use a custom dialer so we can also connect to unix sockets // and not just TCP addresses. genericDialer := clientAddressDialer(defaultRPCPort) @@ -103,6 +109,7 @@ func getClientConn(ctx *cli.Context) *grpc.ClientConn { opts := []grpc.DialOption{ grpc.WithContextDialer(genericDialer), grpc.WithDefaultCallOptions(maxMsgRecvSize), + macOption, } // TLS cannot be disabled, we'll always have a cert file to read. @@ -112,11 +119,6 @@ func getClientConn(ctx *cli.Context) *grpc.ClientConn { tlsCertPath, err)) } - // Macaroons are not yet enabled by default. - if macaroonPath != "" { - opts = append(opts, readMacaroon(macaroonPath)) - } - opts = append(opts, grpc.WithTransportCredentials(creds)) conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...) @@ -140,27 +142,34 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) { } // We'll now fetch the faradaydir so we can make a decision on how to - // properly read the cert. This will either be the default, or will have - // been overwritten by the end user. + // properly read the macaroon and cert files. This will either be the + // default, or will have been overwritten by the end user. faradayDir := lncfg.CleanAndExpandPath(ctx.GlobalString( faradayDirFlag.Name, )) tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( tlsCertFlag.Name, )) + macPath := lncfg.CleanAndExpandPath(ctx.GlobalString( + macaroonPathFlag.Name, + )) // If a custom faraday directory was set, we'll also check if a custom - // path for the TLS cert file was set as well. If not, we'll override - // the path so they can be found within the custom faraday directory. + // path for the TLS cert and macaroon file was set as well. If not, + // we'll override the path so they can be found within the custom + // faraday directory. if faradayDir != faraday.FaradayDirBase || networkStr != faraday.DefaultNetwork { tlsCertPath = filepath.Join( faradayDir, networkStr, faraday.DefaultTLSCertFilename, ) + macPath = filepath.Join( + faradayDir, networkStr, faraday.DefaultMacaroonFilename, + ) } - return tlsCertPath, ctx.GlobalString(macaroonPathFlag.Name), nil + return tlsCertPath, macPath, nil } // ClientAddressDialer parsed client address and returns a dialer. @@ -187,16 +196,16 @@ func clientAddressDialer(defaultPort string) func(context.Context, // // TODO(guggero): Provide this function in lnd's macaroon package and use it // from there. -func readMacaroon(macPath string) grpc.DialOption { +func readMacaroon(macPath string) (grpc.DialOption, error) { // Load the specified macaroon file. macBytes, err := ioutil.ReadFile(macPath) if err != nil { - fatal(fmt.Errorf("unable to read macaroon path : %v", err)) + return nil, fmt.Errorf("unable to read macaroon path : %v", err) } mac := &macaroon.Macaroon{} if err = mac.UnmarshalBinary(macBytes); err != nil { - fatal(fmt.Errorf("unable to decode macaroon: %v", err)) + return nil, fmt.Errorf("unable to decode macaroon: %v", err) } macConstraints := []macaroons.Constraint{ @@ -216,12 +225,12 @@ func readMacaroon(macPath string) grpc.DialOption { // Apply constraints to the macaroon. constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...) if err != nil { - fatal(err) + return nil, err } // Now we append the macaroon credentials to the dial options. cred := macaroons.NewMacaroonCredential(constrainedMac) - return grpc.WithPerRPCCredentials(cred) + return grpc.WithPerRPCCredentials(cred), nil } // parseChannelPoint parses a funding txid and output index from the command diff --git a/config.go b/config.go index 1783be4..2e736c5 100644 --- a/config.go +++ b/config.go @@ -6,11 +6,9 @@ import ( "fmt" "os" "path/filepath" - "strings" "time" "github.com/btcsuite/btcutil" - "github.com/jessevdk/go-flags" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" @@ -60,6 +58,16 @@ var ( DefaultTLSKeyPath = filepath.Join( FaradayDirBase, DefaultNetwork, DefaultTLSKeyFilename, ) + + // DefaultMacaroonFilename is the default file name for the + // autogenerated faraday macaroon. + DefaultMacaroonFilename = "faraday.macaroon" + + // DefaultMacaroonPath is the default full path of the base faraday + // macaroon. + DefaultMacaroonPath = filepath.Join( + FaradayDirBase, DefaultNetwork, DefaultMacaroonFilename, + ) ) type LndConfig struct { @@ -78,7 +86,7 @@ type Config struct { //nolint:maligned Lnd *LndConfig `group:"lnd" namespace:"lnd"` // FaradayDir is the main directory where faraday stores all its data. - FaradayDir string `long:"faradaydir" description:"The directory for all of faraday's data. If set, this option overwrites --tlscertpath and --tlskeypath."` + FaradayDir string `long:"faradaydir" description:"The directory for all of faraday's data. If set, this option overwrites --macaroonpath, --tlscertpath and --tlskeypath."` // ChainConn specifies whether to attempt connecting to a bitcoin backend. ChainConn bool `long:"connect_bitcoin" description:"Whether to attempt to connect to a backing bitcoin node. Some endpoints will not be available if this option is not enabled."` @@ -102,6 +110,8 @@ type Config struct { //nolint:maligned TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed."` TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set."` + MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for faraday's RPC and REST services if it doesn't exist."` + // RPCListen is the listen address for the faraday rpc server. RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients."` @@ -127,43 +137,27 @@ func DefaultConfig() Config { DebugLevel: defaultDebugLevel, TLSCertPath: DefaultTLSCertPath, TLSKeyPath: DefaultTLSKeyPath, + MacaroonPath: DefaultMacaroonPath, RPCListen: defaultRPCListen, ChainConn: defaultChainConn, Bitcoin: chain.DefaultConfig, } } -// LoadConfig starts with a skeleton default config, and reads in user provided -// configuration from the command line. It does not provide a full set of -// defaults or validate user input because validation and sensible default -// setting are performed by the lndclient package. -func LoadConfig() (*Config, error) { - // Start with a default config. - config := DefaultConfig() - - // Parse command line options to obtain user specified values. - if _, err := flags.Parse(&config); err != nil { - return nil, err - } - - // Show the version and exit if the version flag was specified. - appName := filepath.Base(os.Args[0]) - appName = strings.TrimSuffix(appName, filepath.Ext(appName)) - if config.ShowVersion { - fmt.Println(appName, "version", Version()) - os.Exit(0) - } - +// ValidateConfig sanitizes all file system paths and makes sure no incompatible +// configuration combinations are used. +func ValidateConfig(config *Config) error { // Validate the network. _, err := lndclient.Network(config.Network).ChainParams() if err != nil { - return nil, fmt.Errorf("error validating network: %v", err) + return fmt.Errorf("error validating network: %v", err) } // Clean up and validate paths, then make sure the directories exist. config.FaradayDir = lncfg.CleanAndExpandPath(config.FaradayDir) config.TLSCertPath = lncfg.CleanAndExpandPath(config.TLSCertPath) config.TLSKeyPath = lncfg.CleanAndExpandPath(config.TLSKeyPath) + config.MacaroonPath = lncfg.CleanAndExpandPath(config.MacaroonPath) // Append the network type to faraday directory so they are "namespaced" // per network. @@ -171,26 +165,33 @@ func LoadConfig() (*Config, error) { // Create the full path of directories now, including the network path. if err := os.MkdirAll(config.FaradayDir, os.ModePerm); err != nil { - return nil, err + return err } - // Since our faraday directory overrides our TLS dir values, make sure - // that they are not set when faraday dir is set. We fail hard here - // rather than overwriting and potentially confusing the user. + // Since our faraday directory overrides our TLS dir and macaroon path + // values, make sure that they are not set when faraday dir is set. We + // fail hard here rather than overwriting and potentially confusing the + // user. faradayDirSet := config.FaradayDir != FaradayDirBase if faradayDirSet { tlsCertPathSet := config.TLSCertPath != DefaultTLSCertPath tlsKeyPathSet := config.TLSKeyPath != DefaultTLSKeyPath + macaroonPathSet := config.MacaroonPath != DefaultMacaroonPath if tlsCertPathSet { - return nil, fmt.Errorf("faradaydir overwrites " + + return fmt.Errorf("faradaydir overwrites " + "tlscertpath, please only set one value") } if tlsKeyPathSet { - return nil, fmt.Errorf("faradaydir overwrites " + + return fmt.Errorf("faradaydir overwrites " + "tlskeypath, please only set one value") } + + if macaroonPathSet { + return fmt.Errorf("faradaydir overwrites " + + "macaroonpath, please only set one value") + } } // We want the TLS files to also be in the "namespaced" sub directory. @@ -206,27 +207,32 @@ func LoadConfig() (*Config, error) { config.FaradayDir, DefaultTLSKeyFilename, ) } + if config.MacaroonPath == DefaultMacaroonPath { + config.MacaroonPath = filepath.Join( + config.FaradayDir, DefaultMacaroonFilename, + ) + } // If the user has opted into connecting to a bitcoin backend, check // that we have a rpc user and password, and that tls path is set if // required. if config.ChainConn { if config.Bitcoin.User == "" || config.Bitcoin.Password == "" { - return nil, fmt.Errorf("rpc user and password " + + return fmt.Errorf("rpc user and password " + "required when chainconn is set") } if config.Bitcoin.UseTLS && config.Bitcoin.TLSPath == "" { - return nil, fmt.Errorf("bitcoin.tlspath required " + + return fmt.Errorf("bitcoin.tlspath required " + "when chainconn is set") } } if err := build.ParseAndSetDebugLevels(config.DebugLevel, logWriter); err != nil { - return nil, err + return err } - return &config, nil + return nil } // getTLSConfig generates a new self signed certificate or refreshes an existing diff --git a/faraday.go b/faraday.go index 4ef8510..3a37eb9 100644 --- a/faraday.go +++ b/faraday.go @@ -3,7 +3,11 @@ package faraday import ( "fmt" + "os" + "path/filepath" + "strings" + "github.com/jessevdk/go-flags" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/signal" @@ -14,12 +18,27 @@ import ( // Main is the real entry point for faraday. It is required to ensure that // defers are properly executed when os.Exit() is called. func Main() error { - config, err := LoadConfig() - if err != nil { - return fmt.Errorf("error loading config: %v", err) + // Start with a default config. + config := DefaultConfig() + + // Parse command line options to obtain user specified values. + if _, err := flags.Parse(&config); err != nil { + return err + } + + // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + if config.ShowVersion { + fmt.Println(appName, "version", Version()) + os.Exit(0) } - serverTLSCfg, restClientCreds, err := getTLSConfig(config) + if err := ValidateConfig(&config); err != nil { + return fmt.Errorf("error validating config: %v", err) + } + + serverTLSCfg, restClientCreds, err := getTLSConfig(&config) if err != nil { return fmt.Errorf("error loading TLS config: %v", err) } @@ -49,6 +68,8 @@ func Main() error { CORSOrigin: config.CORSOrigin, TLSServerConfig: serverTLSCfg, RestClientConfig: restClientCreds, + FaradayDir: config.FaradayDir, + MacaroonPath: config.MacaroonPath, } // If the client chose to connect to a bitcoin client, get one now. @@ -62,7 +83,9 @@ func Main() error { server := frdrpc.NewRPCServer(cfg) // Catch intercept signals, then start the server. - signal.Intercept() + if err := signal.Intercept(); err != nil { + return err + } if err := server.Start(); err != nil { return err } diff --git a/frdrpc/macaroons.go b/frdrpc/macaroons.go new file mode 100644 index 0000000..6207544 --- /dev/null +++ b/frdrpc/macaroons.go @@ -0,0 +1,159 @@ +package frdrpc + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/macaroons" + "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +const ( + // faradayMacaroonLocation is the value we use for the faraday + // macaroons' "Location" field when baking them. + faradayMacaroonLocation = "faraday" +) + +var ( + // RequiredPermissions is a map of all faraday RPC methods and their + // required macaroon permissions to access faraday. + RequiredPermissions = map[string][]bakery.Op{ + "/frdrpc.FaradayServer/OutlierRecommendations": {{ + Entity: "recommendation", + Action: "read", + }}, + "/frdrpc.FaradayServer/ThresholdRecommendations": {{ + Entity: "recommendation", + Action: "read", + }}, + "/frdrpc.FaradayServer/RevenueReport": {{ + Entity: "report", + Action: "read", + }}, + "/frdrpc.FaradayServer/ChannelInsights": {{ + Entity: "insights", + Action: "read", + }}, + "/frdrpc.FaradayServer/ExchangeRate": {{ + Entity: "rates", + Action: "read", + }}, + "/frdrpc.FaradayServer/NodeAudit": {{ + Entity: "audit", + Action: "read", + }}, + "/frdrpc.FaradayServer/CloseReport": {{ + Entity: "report", + Action: "read", + }}, + } + + // allPermissions is the list of all existing permissions that exist + // for faraday's RPC. The default macaroon that is created on startup + // contains all these permissions and is therefore equivalent to lnd's + // admin.macaroon but for faraday. + allPermissions = []bakery.Op{{ + Entity: "recommendation", + Action: "read", + }, { + Entity: "report", + Action: "read", + }, { + Entity: "audit", + Action: "read", + }, { + Entity: "insights", + Action: "read", + }, { + Entity: "rates", + Action: "read", + }} + + // macDbDefaultPw is the default encryption password used to encrypt the + // faraday macaroon database. The macaroon service requires us to set a + // non-nil password so we set it to an empty string. This will cause the + // keys to be encrypted on disk but won't provide any security at all as + // the password is known to anyone. + // + // TODO(guggero): Allow the password to be specified by the user. Needs + // create/unlock calls in the RPC. Using a password should be optional + // though. + macDbDefaultPw = []byte("") +) + +// startMacaroonService starts the macaroon validation service, creates or +// unlocks the macaroon database and creates the default macaroon if it doesn't +// exist yet. +func (s *RPCServer) startMacaroonService() error { + // Create the macaroon authentication/authorization service. + var err error + s.macaroonService, err = macaroons.NewService( + s.cfg.FaradayDir, faradayMacaroonLocation, + macaroons.IPLockChecker, + ) + if err != nil { + return fmt.Errorf("unable to set up macaroon authentication: "+ + "%v", err) + } + + // Try to unlock the macaroon store with the private password. + err = s.macaroonService.CreateUnlock(&macDbDefaultPw) + if err != nil { + return fmt.Errorf("unable to unlock macaroon DB: %v", err) + } + + // Create macaroon files for faraday CLI to use if they don't exist. + if !lnrpc.FileExists(s.cfg.MacaroonPath) { + ctx := context.Background() + + // We only generate one default macaroon that contains all + // existing permissions (equivalent to the admin.macaroon in + // lnd). Custom macaroons can be created through the bakery + // RPC. + faradayMac, err := s.macaroonService.NewMacaroon( + ctx, macaroons.DefaultRootKeyID, + allPermissions..., + ) + if err != nil { + return err + } + frdMacBytes, err := faradayMac.M().MarshalBinary() + if err != nil { + return err + } + err = ioutil.WriteFile(s.cfg.MacaroonPath, frdMacBytes, 0644) + if err != nil { + if err := os.Remove(s.cfg.MacaroonPath); err != nil { + log.Errorf("Unable to remove %s: %v", + s.cfg.MacaroonPath, err) + } + return err + } + } + + return nil +} + +// stopMacaroonService closes the macaroon database. +func (s *RPCServer) stopMacaroonService() error { + return s.macaroonService.Close() +} + +// macaroonInterceptor creates gRPC server options with the macaroon security +// interceptors. +func (s *RPCServer) macaroonInterceptor() []grpc.ServerOption { + unaryInterceptor := s.macaroonService.UnaryServerInterceptor( + RequiredPermissions, + ) + streamInterceptor := s.macaroonService.StreamServerInterceptor( + RequiredPermissions, + ) + return []grpc.ServerOption{ + grpc.UnaryInterceptor(unaryInterceptor), + grpc.StreamInterceptor(streamInterceptor), + } +} diff --git a/frdrpc/rpcserver.go b/frdrpc/rpcserver.go index 3b8cbfb..55c60ca 100644 --- a/frdrpc/rpcserver.go +++ b/frdrpc/rpcserver.go @@ -22,8 +22,10 @@ import ( proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "gopkg.in/macaroon-bakery.v2/bakery" "github.com/lightninglabs/faraday/accounting" "github.com/lightninglabs/faraday/chain" @@ -95,6 +97,8 @@ type RPCServer struct { // restServer is the REST proxy server. restServer *http.Server + macaroonService *macaroons.Service + restCancel func() wg sync.WaitGroup } @@ -127,18 +131,23 @@ type Config struct { // proxy that connects internally to the gRPC server and therefore is a // TLS client. RestClientConfig *credentials.TransportCredentials + + // FaradayDir is the main directory faraday uses. The macaroon database + // will be created there. + FaradayDir string + + // MacaroonPath is the full path to the default faraday macaroon file + // that is created automatically. This path normally is within + // FaradayDir unless otherwise specified by the user. + MacaroonPath string } // NewRPCServer returns a server which will listen for rpc requests on the // rpc listen address provided. Note that the server returned is not running, // and should be started using Start(). func NewRPCServer(cfg *Config) *RPCServer { - var opts []grpc.ServerOption - grpcServer := grpc.NewServer(opts...) - return &RPCServer{ - cfg: cfg, - grpcServer: grpcServer, + cfg: cfg, } } @@ -148,6 +157,31 @@ func (s *RPCServer) Start() error { return errServerAlreadyStarted } + // Depending on how far we got in initializing the server, we might need + // to clean up certain services that were already started. Keep track of + // them with this map of service name to shutdown function. + shutdownFuncs := make(map[string]func() error) + defer func() { + for serviceName, shutdownFn := range shutdownFuncs { + if err := shutdownFn(); err != nil { + log.Errorf("Error shutting down %s service: %v", + serviceName, err) + } + } + }() + + // Start the macaroon service and let it create its default macaroon in + // case it doesn't exist yet. + if err := s.startMacaroonService(); err != nil { + return fmt.Errorf("error starting macaroon service: %v", err) + } + shutdownFuncs["macaroon"] = s.stopMacaroonService + + // First we add the security interceptor to our gRPC server options that + // checks the macaroons for validity. + serverOpts := s.macaroonInterceptor() + s.grpcServer = grpc.NewServer(serverOpts...) + // Start the gRPC RPCServer listening for HTTP/2 connections. log.Info("Starting gRPC listener") grpcListener, err := net.Listen("tcp", s.cfg.RPCListen) @@ -157,6 +191,7 @@ func (s *RPCServer) Start() error { } s.rpcListener = tls.NewListener(grpcListener, s.cfg.TLSServerConfig) + shutdownFuncs["gRPC listener"] = s.rpcListener.Close log.Infof("gRPC server listening on %s", s.rpcListener.Addr()) RegisterFaradayServerServer(s.grpcServer, s) @@ -174,6 +209,7 @@ func (s *RPCServer) Start() error { restListener = tls.NewListener( restListener, s.cfg.TLSServerConfig, ) + shutdownFuncs["REST listener"] = restListener.Close log.Infof("REST server listening on %s", restListener.Addr()) // We'll dial into the local gRPC server so we need to set some @@ -235,6 +271,10 @@ func (s *RPCServer) Start() error { } }() + // If we got here successfully, there's no need to shutdown anything + // anymore. + shutdownFuncs = nil + return nil } @@ -247,10 +287,31 @@ func (s *RPCServer) StartAsSubserver(lndClient lndclient.LndServices) error { return errServerAlreadyStarted } + // Start the macaroon service and let it create its default macaroon in + // case it doesn't exist yet. + if err := s.startMacaroonService(); err != nil { + return fmt.Errorf("error starting macaroon service: %v", err) + } + s.cfg.Lnd = lndClient return nil } +// ValidateMacaroon extracts the macaroon from the context's gRPC metadata, +// checks its signature, makes sure all specified permissions for the called +// method are contained within and finally ensures all caveat conditions are +// met. A non-nil error is returned if any of the checks fail. This method is +// needed to enable faraday running as an external subserver in the same process +// as lnd but still validate its own macaroons. +func (s *RPCServer) ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, fullMethod string) error { + + // Delegate the call to faraday's own macaroon validator service. + return s.macaroonService.ValidateMacaroon( + ctx, requiredPermissions, fullMethod, + ) +} + // Stop stops the grpc listener and server. func (s *RPCServer) Stop() error { if atomic.AddInt32(&s.stopped, 1) != 1 { @@ -265,6 +326,10 @@ func (s *RPCServer) Stop() error { } } + if err := s.macaroonService.Close(); err != nil { + log.Errorf("Error stopping macaroon service: %v", err) + } + // Stop the grpc server and wait for all go routines to terminate. if s.grpcServer != nil { s.grpcServer.Stop() diff --git a/go.mod b/go.mod index 714fcd3..a131d72 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,10 @@ require ( github.com/jessevdk/go-flags v1.4.0 github.com/lightninglabs/lndclient v0.11.0-0 github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d - github.com/lightningnetwork/lnd v0.11.0-beta + + // TODO(guggero): Bump lnd to the final v0.11.1-beta version once it's + // released. + github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763 github.com/lightningnetwork/lnd/cert v1.0.3 github.com/shopspring/decimal v1.2.0 github.com/stretchr/testify v1.5.1 @@ -18,6 +21,7 @@ require ( golang.org/x/text v0.3.2 // indirect google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c google.golang.org/grpc v1.25.1 + gopkg.in/macaroon-bakery.v2 v2.0.1 gopkg.in/macaroon.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index 9765e4f..7da8f31 100644 --- a/go.sum +++ b/go.sum @@ -192,9 +192,9 @@ github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce7 github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= -github.com/lightningnetwork/lnd v0.11.0-beta h1:pUAT7FMHqS+iarNxyRtgj96XKCGAWwmb6ZdiUBy78ts= github.com/lightningnetwork/lnd v0.11.0-beta/go.mod h1:CzArvT7NFDLhVyW06+NJWSuWFmE6Ea+AjjA3txUBqTM= -github.com/lightningnetwork/lnd/cert v1.0.2 h1:g2rEu+sM2Uyz0bpfuvwri/ks6R/26H5iY1NcGbpDJ+c= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763 h1:OUWOTo2BAcsnEaMQIf4gLktU3zGytx6pXrmjUNpZpdg= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763/go.mod h1:IvrqVCc5tN2on6E7IHhrwyiM7FCHZ92LphZD+v88LXY= github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo= github.com/lightningnetwork/lnd/cert v1.0.3 h1:/K2gjzLgVI8we2IIPKc0ztWTEa85uds5sWXi1K6mOT0= github.com/lightningnetwork/lnd/cert v1.0.3/go.mod h1:3MWXVLLPI0Mg0XETm9fT4N9Vyy/8qQLmaM5589bEggM= diff --git a/itest/rpc.go b/itest/rpc.go index b83a147..e5a6c4b 100644 --- a/itest/rpc.go +++ b/itest/rpc.go @@ -2,10 +2,13 @@ package itest import ( "fmt" + "io/ioutil" "github.com/btcsuite/btcd/rpcclient" + "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "gopkg.in/macaroon.v2" "github.com/lightninglabs/faraday/frdrpc" ) @@ -26,8 +29,8 @@ func getBitcoindClient() (*rpcclient.Client, error) { // getFaradayClient returns an rpc client connection to the running faraday // instance. -func getFaradayClient(address, tlsCertPath string) (frdrpc.FaradayServerClient, - error) { +func getFaradayClient(address, tlsCertPath, + macaroonPath string) (frdrpc.FaradayServerClient, error) { tlsCredentials, err := credentials.NewClientTLSFromFile(tlsCertPath, "") if err != nil { @@ -35,8 +38,15 @@ func getFaradayClient(address, tlsCertPath string) (frdrpc.FaradayServerClient, tlsCertPath, err) } + macaroonOptions, err := readMacaroon(macaroonPath) + if err != nil { + return nil, fmt.Errorf("unable to load macaroon %s: %v", + macaroonPath, err) + } + opts := []grpc.DialOption{ grpc.WithTransportCredentials(tlsCredentials), + macaroonOptions, } conn, err := grpc.Dial(address, opts...) @@ -47,3 +57,22 @@ func getFaradayClient(address, tlsCertPath string) (frdrpc.FaradayServerClient, return frdrpc.NewFaradayServerClient(conn), nil } + +// readMacaroon tries to read the macaroon file at the specified path and create +// gRPC dial options from it. +func readMacaroon(macaroonPath string) (grpc.DialOption, error) { + // Load the specified macaroon file. + macBytes, err := ioutil.ReadFile(macaroonPath) + if err != nil { + return nil, fmt.Errorf("unable to read macaroon path : %v", err) + } + + mac := &macaroon.Macaroon{} + if err = mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %v", err) + } + + // Now we append the macaroon credentials to the dial options. + cred := macaroons.NewMacaroonCredential(mac) + return grpc.WithPerRPCCredentials(cred), nil +} diff --git a/itest/test_context.go b/itest/test_context.go index 78607e3..4a756a8 100644 --- a/itest/test_context.go +++ b/itest/test_context.go @@ -32,7 +32,8 @@ var ( faradayCmd = "./faraday" - faradayCertPath = "/root/.faraday/regtest/tls.cert" + faradayCertPath = "/root/.faraday/regtest/tls.cert" + faradayMacaroonPath = "/root/.faraday/regtest/faraday.macaroon" faradayArgs = []string{ "--rpclisten=localhost:8465", @@ -521,7 +522,7 @@ func (c *testContext) startFaraday() { var err error c.eventuallyf(func() bool { c.faradayClient, err = getFaradayClient( - "localhost:8465", faradayCertPath, + "localhost:8465", faradayCertPath, faradayMacaroonPath, ) return err == nil }, "could not connect to faraday process: %v", err)