From 17e0165d4c2e97e7dacdc9b4eea12ea8983ef44e Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 13:25:59 +0200 Subject: [PATCH 1/5] loopd: add TLS config to listenerCfg struct As a preparation to be able to listen on a TLS enabled listener, we add a TLS config parameter to the listener setup functions. --- loopd/daemon.go | 15 +++++++++------ loopd/run.go | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/loopd/daemon.go b/loopd/daemon.go index f171d7f..31151fb 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -2,6 +2,7 @@ package loopd import ( "context" + "crypto/tls" "errors" "fmt" "net" @@ -29,11 +30,13 @@ var ( // listenerCfg holds closures used to retrieve listeners for the gRPC services. type listenerCfg struct { - // grpcListener returns a listener to use for the gRPC server. - grpcListener func() (net.Listener, error) + // grpcListener returns a TLS listener to use for the gRPC server, based + // on the passed TLS configuration. + grpcListener func(*tls.Config) (net.Listener, error) - // restListener returns a listener to use for the REST proxy. - restListener func() (net.Listener, error) + // restListener returns a TLS listener to use for the REST proxy, based + // on the passed TLS configuration. + restListener func(*tls.Config) (net.Listener, error) // getLnd returns a grpc connection to an lnd instance. getLnd func(lndclient.Network, *lndConfig) (*lndclient.GrpcLndServices, @@ -175,7 +178,7 @@ func (d *Daemon) startWebServers() error { // Next, start the gRPC server listening for HTTP/2 connections. log.Infof("Starting gRPC listener") - d.grpcListener, err = d.listenerCfg.grpcListener() + d.grpcListener, err = d.listenerCfg.grpcListener(nil) if err != nil { return fmt.Errorf("RPC server unable to listen on %s: %v", d.cfg.RPCListen, err) @@ -213,7 +216,7 @@ func (d *Daemon) startWebServers() error { return err } - d.restListener, err = d.listenerCfg.restListener() + d.restListener, err = d.listenerCfg.restListener(nil) if err != nil { return fmt.Errorf("REST proxy unable to listen on %s: %v", d.cfg.RESTListen, err) diff --git a/loopd/run.go b/loopd/run.go index 6317a11..dd6a393 100644 --- a/loopd/run.go +++ b/loopd/run.go @@ -2,6 +2,7 @@ package loopd import ( "context" + "crypto/tls" "fmt" "net" "os" @@ -51,22 +52,32 @@ type RPCConfig struct { // and RPCConfig. func newListenerCfg(config *Config, rpcCfg RPCConfig) *listenerCfg { return &listenerCfg{ - grpcListener: func() (net.Listener, error) { + grpcListener: func(tlsCfg *tls.Config) (net.Listener, error) { // If a custom RPC listener is set, we will listen on // it instead of the regular tcp socket. if rpcCfg.RPCListener != nil { return rpcCfg.RPCListener, nil } - return net.Listen("tcp", config.RPCListen) + listener, err := net.Listen("tcp", config.RPCListen) + if err != nil { + return nil, err + } + + return tls.NewListener(listener, tlsCfg), nil }, - restListener: func() (net.Listener, error) { + restListener: func(tlsCfg *tls.Config) (net.Listener, error) { // If a custom RPC listener is set, we disable REST. if rpcCfg.RPCListener != nil { return nil, nil } - return net.Listen("tcp", config.RESTListen) + listener, err := net.Listen("tcp", config.RESTListen) + if err != nil { + return nil, err + } + + return tls.NewListener(listener, tlsCfg), nil }, getLnd: func(network lndclient.Network, cfg *lndConfig) ( *lndclient.GrpcLndServices, error) { From a8d93bec6a76b76849f759abbba46f90e841e25b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 13:26:00 +0200 Subject: [PATCH 2/5] loopd: add TLS to the daemon's server connection --- go.mod | 1 + go.sum | 2 + loopd/config.go | 177 +++++++++++++++++++++++++++++++++++++++++++----- loopd/daemon.go | 31 +++++++-- loopd/run.go | 2 +- 5 files changed, 190 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index cf4b839..7102d55 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( 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 + github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/queue v1.0.4 github.com/stretchr/testify v1.5.1 github.com/urfave/cli v1.20.0 diff --git a/go.sum b/go.sum index cd663f1..5381885 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,8 @@ github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea 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/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= github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= diff --git a/loopd/config.go b/loopd/config.go index feb40dd..ae062be 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -1,30 +1,62 @@ package loopd import ( + "crypto/tls" + "crypto/x509" "fmt" "os" "path/filepath" + "time" "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/lsat" + "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc/credentials" ) var ( - loopDirBase = btcutil.AppDataDir("loop", false) + // LoopDirBase is the default main directory where loop stores its data. + LoopDirBase = btcutil.AppDataDir("loop", false) + + // DefaultNetwork is the default bitcoin network loop runs on. + DefaultNetwork = "mainnet" - defaultNetwork = "mainnet" defaultLogLevel = "info" defaultLogDirname = "logs" defaultLogFilename = "loopd.log" - defaultLogDir = filepath.Join(loopDirBase, defaultLogDirname) - defaultConfigFile = filepath.Join( - loopDirBase, defaultNetwork, defaultConfigFilename, + + defaultLogDir = filepath.Join(LoopDirBase, defaultLogDirname) + defaultConfigFile = filepath.Join( + LoopDirBase, DefaultNetwork, defaultConfigFilename, ) defaultMaxLogFiles = 3 defaultMaxLogFileSize = 10 defaultLoopOutMaxParts = uint32(5) + + // DefaultTLSCertFilename is the default file name for the autogenerated + // TLS certificate. + DefaultTLSCertFilename = "tls.cert" + + // DefaultTLSKeyFilename is the default file name for the autogenerated + // TLS key. + DefaultTLSKeyFilename = "tls.key" + + defaultSelfSignedOrganization = "loop autogenerated cert" + + // DefaultTLSCertPath is the default full path of the autogenerated TLS + // certificate. + DefaultTLSCertPath = filepath.Join( + LoopDirBase, DefaultNetwork, DefaultTLSCertFilename, + ) + + // DefaultTLSKeyPath is the default full path of the autogenerated TLS + // key. + DefaultTLSKeyPath = filepath.Join( + LoopDirBase, DefaultNetwork, DefaultTLSKeyFilename, + ) ) type lndConfig struct { @@ -50,12 +82,20 @@ type Config struct { RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."` - LoopDir string `long:"loopdir" description:"The directory for all of loop's data."` - ConfigFile string `long:"configfile" description:"Path to configuration file."` - DataDir string `long:"datadir" description:"Directory for loopdb."` + LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath and --tlskeypath."` + ConfigFile string `long:"configfile" description:"Path to configuration file."` + DataDir string `long:"datadir" description:"Directory for loopdb."` + + TLSCertPath string `long:"tlscertpath" description:"Path to write the TLS certificate for loop's RPC and REST services."` + TLSKeyPath string `long:"tlskeypath" description:"Path to write the TLS private key for loop's RPC and REST services."` + TLSExtraIPs []string `long:"tlsextraip" description:"Adds an extra IP to the generated certificate."` + TLSExtraDomains []string `long:"tlsextradomain" description:"Adds an extra domain to the generated certificate."` + 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."` + LogDir string `long:"logdir" description:"Directory to log output."` - MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` - MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` + MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)."` + MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."` DebugLevel string `long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` MaxLSATCost uint32 `long:"maxlsatcost" description:"Maximum cost in satoshis that loopd is going to pay for an LSAT token automatically. Does not include routing fees."` @@ -78,19 +118,21 @@ const ( // DefaultConfig returns all default values for the Config struct. func DefaultConfig() Config { return Config{ - Network: defaultNetwork, + Network: DefaultNetwork, RPCListen: "localhost:11010", RESTListen: "localhost:8081", Server: &loopServerConfig{ NoTLS: false, }, - LoopDir: loopDirBase, + LoopDir: LoopDirBase, ConfigFile: defaultConfigFile, - DataDir: loopDirBase, + DataDir: LoopDirBase, LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, DebugLevel: defaultLogLevel, + TLSCertPath: DefaultTLSCertPath, + TLSKeyPath: DefaultTLSKeyPath, MaxLSATCost: lsat.DefaultMaxCostSats, MaxLSATFee: lsat.DefaultMaxRoutingFeeSats, LoopOutMaxParts: defaultLoopOutMaxParts, @@ -106,15 +148,20 @@ func Validate(cfg *Config) error { cfg.LoopDir = lncfg.CleanAndExpandPath(cfg.LoopDir) cfg.DataDir = lncfg.CleanAndExpandPath(cfg.DataDir) cfg.LogDir = lncfg.CleanAndExpandPath(cfg.LogDir) + cfg.TLSCertPath = lncfg.CleanAndExpandPath(cfg.TLSCertPath) + cfg.TLSKeyPath = lncfg.CleanAndExpandPath(cfg.TLSKeyPath) // Since our loop directory overrides our log/data dir values, make sure // that they are not set when loop dir is set. We hard here rather than // overwriting and potentially confusing the user. - logDirSet := cfg.LogDir != defaultLogDir - dataDirSet := cfg.DataDir != loopDirBase - loopDirSet := cfg.LoopDir != loopDirBase + loopDirSet := cfg.LoopDir != LoopDirBase if loopDirSet { + logDirSet := cfg.LogDir != defaultLogDir + dataDirSet := cfg.DataDir != LoopDirBase + tlsCertPathSet := cfg.TLSCertPath != DefaultTLSCertPath + tlsKeyPathSet := cfg.TLSKeyPath != DefaultTLSKeyPath + if logDirSet { return fmt.Errorf("loopdir overwrites logdir, please " + "only set one value") @@ -125,7 +172,17 @@ func Validate(cfg *Config) error { "only set one value") } - // Once we are satisfied that neither config value was set, we + if tlsCertPathSet { + return fmt.Errorf("loopdir overwrites tlscertpath, " + + "please only set one value") + } + + if tlsKeyPathSet { + return fmt.Errorf("loopdir overwrites tlskeypath, " + + "please only set one value") + } + + // Once we are satisfied that no other config value was set, we // replace them with our loop dir. cfg.DataDir = cfg.LoopDir cfg.LogDir = filepath.Join(cfg.LoopDir, defaultLogDirname) @@ -136,6 +193,20 @@ func Validate(cfg *Config) error { cfg.DataDir = filepath.Join(cfg.DataDir, cfg.Network) cfg.LogDir = filepath.Join(cfg.LogDir, cfg.Network) + // We want the TLS files to also be in the "namespaced" sub directory. + // Replace the default values with actual values in case the user + // specified either loopdir or datadir. + if cfg.TLSCertPath == DefaultTLSCertPath { + cfg.TLSCertPath = filepath.Join( + cfg.DataDir, DefaultTLSCertFilename, + ) + } + if cfg.TLSKeyPath == DefaultTLSKeyPath { + cfg.TLSKeyPath = filepath.Join( + cfg.DataDir, DefaultTLSKeyFilename, + ) + } + // If either of these directories do not exist, create them. if err := os.MkdirAll(cfg.DataDir, os.ModePerm); err != nil { return err @@ -147,3 +218,75 @@ func Validate(cfg *Config) error { return nil } + +// getTLSConfig generates a new self signed certificate or refreshes an existing +// one if necessary, then returns the full TLS configuration for initializing +// a secure server interface. +func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials, + error) { + + // Let's load our certificate first or create then load if it doesn't + // yet exist. + certData, parsedCert, err := loadCertWithCreate(cfg) + if err != nil { + return nil, nil, err + } + + // If the certificate expired or it was outdated, delete it and the TLS + // key and generate a new pair. + if time.Now().After(parsedCert.NotAfter) { + log.Info("TLS certificate is expired or outdated, " + + "removing old file then generating a new one") + + err := os.Remove(cfg.TLSCertPath) + if err != nil { + return nil, nil, err + } + + err = os.Remove(cfg.TLSKeyPath) + if err != nil { + return nil, nil, err + } + + certData, _, err = loadCertWithCreate(cfg) + if err != nil { + return nil, nil, err + } + } + + tlsCfg := cert.TLSConfFromCert(certData) + restCreds, err := credentials.NewClientTLSFromFile( + cfg.TLSCertPath, "", + ) + if err != nil { + return nil, nil, err + } + + return tlsCfg, &restCreds, nil +} + +// loadCertWithCreate tries to load the TLS certificate from disk. If the +// specified cert and key files don't exist, the certificate/key pair is created +// first. +func loadCertWithCreate(cfg *Config) (tls.Certificate, *x509.Certificate, + error) { + + // Ensure we create TLS key and certificate if they don't exist. + if !lnrpc.FileExists(cfg.TLSCertPath) && + !lnrpc.FileExists(cfg.TLSKeyPath) { + + log.Infof("Generating TLS certificates...") + err := cert.GenCertPair( + defaultSelfSignedOrganization, cfg.TLSCertPath, + cfg.TLSKeyPath, cfg.TLSExtraIPs, + cfg.TLSExtraDomains, cfg.TLSDisableAutofill, + cert.DefaultAutogenValidity, + ) + if err != nil { + return tls.Certificate{}, nil, err + } + log.Infof("Done generating TLS certificates") + } + + return cert.LoadCert(cfg.TLSCertPath, cfg.TLSKeyPath) +} diff --git a/loopd/daemon.go b/loopd/daemon.go index 31151fb..63794e7 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "strings" "sync" "sync/atomic" @@ -178,11 +179,15 @@ func (d *Daemon) startWebServers() error { // Next, start the gRPC server listening for HTTP/2 connections. log.Infof("Starting gRPC listener") - d.grpcListener, err = d.listenerCfg.grpcListener(nil) + serverTLSCfg, restClientCreds, err := getTLSConfig(d.cfg) + if err != nil { + return fmt.Errorf("could not create gRPC server options: %v", + err) + } + d.grpcListener, err = d.listenerCfg.grpcListener(serverTLSCfg) if err != nil { return fmt.Errorf("RPC server unable to listen on %s: %v", d.cfg.RPCListen, err) - } // The default JSON marshaler of the REST proxy only sets OrigName to @@ -206,17 +211,33 @@ func (d *Daemon) startWebServers() error { restHandler = allowCORS(restHandler, d.cfg.CORSOrigin) } proxyOpts := []grpc.DialOption{ - grpc.WithInsecure(), + grpc.WithTransportCredentials(*restClientCreds), grpc.WithDefaultCallOptions(maxMsgRecvSize), } + + // With TLS enabled by default, we cannot call 0.0.0.0 internally from + // the REST proxy as that IP address isn't in the cert. We need to + // rewrite it to the loopback address. + restProxyDest := d.cfg.RPCListen + switch { + case strings.Contains(restProxyDest, "0.0.0.0"): + restProxyDest = strings.Replace( + restProxyDest, "0.0.0.0", "127.0.0.1", 1, + ) + + case strings.Contains(restProxyDest, "[::]"): + restProxyDest = strings.Replace( + restProxyDest, "[::]", "[::1]", 1, + ) + } err = looprpc.RegisterSwapClientHandlerFromEndpoint( - ctx, mux, d.cfg.RPCListen, proxyOpts, + ctx, mux, restProxyDest, proxyOpts, ) if err != nil { return err } - d.restListener, err = d.listenerCfg.restListener(nil) + d.restListener, err = d.listenerCfg.restListener(serverTLSCfg) if err != nil { return fmt.Errorf("REST proxy unable to listen on %s: %v", d.cfg.RESTListen, err) diff --git a/loopd/run.go b/loopd/run.go index dd6a393..199a5ca 100644 --- a/loopd/run.go +++ b/loopd/run.go @@ -251,7 +251,7 @@ func getConfigPath(cfg Config, loopDir string) string { // If the user has set a loop directory that is different to the default // we will use this loop directory as the location of our config file. // We do not namespace by network, because this is a custom loop dir. - if loopDir != loopDirBase { + if loopDir != LoopDirBase { return filepath.Join(loopDir, defaultConfigFilename) } From 39d1121c4b72002cd7a5ef86d5fafe63059c29b7 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 13:26:01 +0200 Subject: [PATCH 3/5] cmd/loop: add TLS params to CLI --- cmd/loop/main.go | 98 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 1e7e7ff..512524d 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -6,15 +6,20 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "strconv" + "strings" "time" + "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/loopd" "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/protobuf-hex-display/json" "github.com/lightninglabs/protobuf-hex-display/jsonpb" "github.com/lightninglabs/protobuf-hex-display/proto" + "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/macaroons" "github.com/btcsuite/btcutil" @@ -43,10 +48,22 @@ var ( // that we set when sending it over the line. defaultMacaroonTimeout int64 = 60 + loopDirFlag = cli.StringFlag{ + Name: "loopdir", + Value: loopd.LoopDirBase, + Usage: "path to loop's base directory", + } + networkFlag = cli.StringFlag{ + Name: "network, n", + Usage: "the network loop is running on e.g. mainnet, " + + "testnet, etc.", + Value: loopd.DefaultNetwork, + } + tlsCertFlag = cli.StringFlag{ - Name: "tlscertpath", - Usage: "path to loop's TLS certificate, only needed if loop " + - "runs in the same process as lnd", + Name: "tlscertpath", + Usage: "path to loop's TLS certificate", + Value: loopd.DefaultTLSCertPath, } macaroonPathFlag = cli.StringFlag{ Name: "macaroonpath", @@ -103,6 +120,8 @@ func main() { Value: "localhost:11010", Usage: "loopd daemon address host:port", }, + networkFlag, + loopDirFlag, tlsCertFlag, macaroonPathFlag, } @@ -121,8 +140,10 @@ func main() { func getClient(ctx *cli.Context) (looprpc.SwapClientClient, func(), error) { rpcServer := ctx.GlobalString("rpcserver") - tlsCertPath := ctx.GlobalString(tlsCertFlag.Name) - macaroonPath := ctx.GlobalString(macaroonPathFlag.Name) + tlsCertPath, macaroonPath, err := extractPathArgs(ctx) + if err != nil { + return nil, nil, err + } conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath) if err != nil { return nil, nil, err @@ -137,6 +158,40 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate) } +// extractPathArgs parses the TLS certificate and macaroon paths from the +// command. +func extractPathArgs(ctx *cli.Context) (string, string, error) { + // We'll start off by parsing the network. This is needed to determine + // the correct path to the TLS certificate and macaroon when not + // specified. + networkStr := strings.ToLower(ctx.GlobalString("network")) + _, err := lndclient.Network(networkStr).ChainParams() + if err != nil { + return "", "", err + } + + // We'll now fetch the loopdir 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. + loopDir := lncfg.CleanAndExpandPath(ctx.GlobalString(loopDirFlag.Name)) + tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( + tlsCertFlag.Name, + )) + + // If a custom lnd directory was set, we'll also check if custom paths + // for the TLS cert file were set as well. If not, we'll override their + // paths so they can be found within the custom loop directory set. This + // allows us to set a custom lnd directory, along with custom paths to + // the TLS cert file. + if loopDir != loopd.LoopDirBase || networkStr != loopd.DefaultNetwork { + tlsCertPath = filepath.Join( + loopDir, networkStr, loopd.DefaultTLSCertFilename, + ) + } + + return tlsCertPath, ctx.GlobalString(macaroonPathFlag.Name), nil +} + type inLimits struct { maxMinerFee btcutil.Amount maxSwapFee btcutil.Amount @@ -322,32 +377,23 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, grpc.WithDefaultCallOptions(maxMsgRecvSize), } - switch { - // If a TLS certificate file is specified, we need to load it and build - // transport credentials with it. - case tlsCertPath != "": - creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") - if err != nil { - fatal(err) - } - - // Macaroons are only allowed to be transmitted over a TLS - // enabled connection. - if macaroonPath != "" { - opts = append(opts, readMacaroon(macaroonPath)) - } - - opts = append(opts, grpc.WithTransportCredentials(creds)) + // TLS cannot be disabled, we'll always have a cert file to read. + creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") + if err != nil { + fatal(err) + } - // By default, if no certificate is supplied, we assume the RPC server - // runs without TLS. - default: - opts = append(opts, grpc.WithInsecure()) + // Macaroons are not yet enabled by default. + if macaroonPath != "" { + opts = append(opts, readMacaroon(macaroonPath)) } + opts = append(opts, grpc.WithTransportCredentials(creds)) + conn, err := grpc.Dial(address, opts...) if err != nil { - return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + return nil, fmt.Errorf("unable to connect to RPC server: %v", + err) } return conn, nil From dc99df0bfbac5be4d7094b820eca7a575eb57d1a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 13:26:06 +0200 Subject: [PATCH 4/5] cmd/loop: return error in readMacaroon As we only use the readMacaroon function inside getClientConn where we have an error return value anyway, we might as well pass the error along correctly instead of failing hard directly. --- cmd/loop/main.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 512524d..6847975 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -380,12 +380,16 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, // TLS cannot be disabled, we'll always have a cert file to read. creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") if err != nil { - fatal(err) + return nil, err } // Macaroons are not yet enabled by default. if macaroonPath != "" { - opts = append(opts, readMacaroon(macaroonPath)) + macOption, err := readMacaroon(macaroonPath) + if err != nil { + return nil, err + } + opts = append(opts, macOption) } opts = append(opts, grpc.WithTransportCredentials(creds)) @@ -401,16 +405,16 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, // readMacaroon tries to read the macaroon file at the specified path and create // gRPC dial options from it. -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{ @@ -430,10 +434,10 @@ 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 } From 458ba7dc9e8de13dc8f7411ff584f6f8b3da1d2d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:33:40 +0200 Subject: [PATCH 5/5] README: mention TLS --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 05853ec..5a1e0ea 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,18 @@ pending swaps after a restart. Information about pending swaps is stored persistently in the swap database. Its location is `~/.loopd//loop.db`. +## Transport security + +The gRPC and REST connections of `loopd` are encrypted with TLS the same way +`lnd` is. + +If no custom loop directory is set then the TLS certificate is stored in +`~/.loopd//tls.cert`. + +The `loop` 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. + ## Multiple Simultaneous Swaps It is possible to execute multiple swaps simultaneously. Just keep loopd