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) }