loopd: add TLS to the daemon's server connection

pull/286/head
Oliver Gugger 4 years ago
parent 17e0165d4c
commit a8d93bec6a
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -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

@ -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=

@ -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 <subsystem>=<level>,<subsystem2>=<level>,... 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)
}

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

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

Loading…
Cancel
Save