From 4e0ffc1382d237a674214bfc16e8ee49bc0e2558 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:10:50 +0200 Subject: [PATCH 1/8] mod+faraday: update to lnd 0.11.1 We update to the newest verion of lnd so we can use the updated macaroon service. NOTE: This is a compile time dependency update only, no RPC level update is required. --- faraday.go | 4 +++- go.mod | 5 ++++- go.sum | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/faraday.go b/faraday.go index 4ef8510..48aafc6 100644 --- a/faraday.go +++ b/faraday.go @@ -62,7 +62,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/go.mod b/go.mod index 714fcd3..bdcfb43 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 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= From 02de78a1a963ef2c709ac8fb50ea7a6cc7c9f244 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 11:07:15 +0200 Subject: [PATCH 2/8] config+faraday: extract ValidateConfig To make it easier to use faraday as an external subserver, it is necessary to extract the config validation into its own function that is separate from loading the config. --- config.go | 42 +++++++++++------------------------------- faraday.go | 27 +++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/config.go b/config.go index 1783be4..6c47315 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" @@ -133,31 +131,13 @@ func DefaultConfig() Config { } } -// 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. @@ -171,7 +151,7 @@ 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 @@ -183,12 +163,12 @@ func LoadConfig() (*Config, error) { tlsKeyPathSet := config.TLSKeyPath != DefaultTLSKeyPath 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") } } @@ -212,21 +192,21 @@ func LoadConfig() (*Config, error) { // 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 48aafc6..d4e572d 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) + } + + if err := ValidateConfig(&config); err != nil { + return fmt.Errorf("error validating config: %v", err) } - serverTLSCfg, restClientCreds, err := getTLSConfig(config) + serverTLSCfg, restClientCreds, err := getTLSConfig(&config) if err != nil { return fmt.Errorf("error loading TLS config: %v", err) } From ddcdda5f4392c276e9c8cc613a1a097c75321767 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:34:25 +0200 Subject: [PATCH 3/8] config: add macaroon path to config --- config.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 6c47315..2e736c5 100644 --- a/config.go +++ b/config.go @@ -58,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 { @@ -76,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."` @@ -100,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."` @@ -125,6 +137,7 @@ func DefaultConfig() Config { DebugLevel: defaultDebugLevel, TLSCertPath: DefaultTLSCertPath, TLSKeyPath: DefaultTLSKeyPath, + MacaroonPath: DefaultMacaroonPath, RPCListen: defaultRPCListen, ChainConn: defaultChainConn, Bitcoin: chain.DefaultConfig, @@ -144,6 +157,7 @@ func ValidateConfig(config *Config) error { 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. @@ -154,13 +168,15 @@ func ValidateConfig(config *Config) error { 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 fmt.Errorf("faradaydir overwrites " + @@ -171,6 +187,11 @@ func ValidateConfig(config *Config) error { 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. @@ -186,6 +207,11 @@ func ValidateConfig(config *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 From 9f62b53790f34f07e4d3cc886e7985f4eced9982 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:35:13 +0200 Subject: [PATCH 4/8] faraday+frdrpc: add macaroon auth to faraday's server conn To secure access to faraday's RPC server, we add a macaroon authentication service and its gRPC interceptors to the daemon's server connection. --- faraday.go | 2 + frdrpc/macaroons.go | 159 ++++++++++++++++++++++++++++++++++++++++++++ frdrpc/rpcserver.go | 59 ++++++++++++++-- go.mod | 1 + 4 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 frdrpc/macaroons.go diff --git a/faraday.go b/faraday.go index d4e572d..3a37eb9 100644 --- a/faraday.go +++ b/faraday.go @@ -68,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. 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..700e15f 100644 --- a/frdrpc/rpcserver.go +++ b/frdrpc/rpcserver.go @@ -22,6 +22,7 @@ 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" @@ -95,6 +96,8 @@ type RPCServer struct { // restServer is the REST proxy server. restServer *http.Server + macaroonService *macaroons.Service + restCancel func() wg sync.WaitGroup } @@ -127,18 +130,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 +156,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 +190,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 +208,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 +270,10 @@ func (s *RPCServer) Start() error { } }() + // If we got here successfully, there's no need to shutdown anything + // anymore. + shutdownFuncs = nil + return nil } @@ -247,6 +286,12 @@ 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 } @@ -265,6 +310,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 bdcfb43..a131d72 100644 --- a/go.mod +++ b/go.mod @@ -21,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 ) From 7c2253fc2bf3147b0e80b757da609ff67692d4fe Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:41:41 +0200 Subject: [PATCH 5/8] cmd/frcli: add macaroon params to CLI --- cmd/frcli/main.go | 6 +++--- cmd/frcli/utils.go | 39 ++++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 18 deletions(-) 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 From c985437e589b0fb5b3ace48a3f13f4946e6b1b81 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:42:58 +0200 Subject: [PATCH 6/8] frdrpc: allow faraday to be used as external subserver When faraday runs in the same process as lnd (in LiT), it hooks itself into lnd's RPC server as an external subserver. But because the user should still be able to use the default faraday macaroon, the faraday daemon must be able to validate its own macaroons as lnd's macaroon service doesn't know the root key for it. --- frdrpc/rpcserver.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frdrpc/rpcserver.go b/frdrpc/rpcserver.go index 700e15f..55c60ca 100644 --- a/frdrpc/rpcserver.go +++ b/frdrpc/rpcserver.go @@ -25,6 +25,7 @@ import ( "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" @@ -296,6 +297,21 @@ func (s *RPCServer) StartAsSubserver(lndClient lndclient.LndServices) error { 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 { From 0ff7cd08a104c0acf24f882f41315c923f12bfb7 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Sep 2020 10:47:43 +0200 Subject: [PATCH 7/8] README: mention macaroons --- README.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) 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. From 699d359c5cf06e9c9dc170740449f1d712527bc0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Sep 2020 12:43:42 +0200 Subject: [PATCH 8/8] itest: add macaroon auth to client conn --- itest/rpc.go | 33 +++++++++++++++++++++++++++++++-- itest/test_context.go | 5 +++-- 2 files changed, 34 insertions(+), 4 deletions(-) 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)