lnd/cmd/lncli/main.go
Oliver Gugger b28bf29c5f
lncli: don't read macaroons when not using them
When we either don't use macaroons (because the global --no-macaroons
flag is set) or don't need them (for the wallet unlocker commands), we
don't try to read the file at all to avoid running into an error if the
file doesn't exist (which it doesn't in those two cases).
2020-09-25 15:54:10 +02:00

381 lines
11 KiB
Go

// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// Copyright (C) 2015-2017 The Lightning Network Developers
package main
import (
"crypto/tls"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"golang.org/x/crypto/ssh/terminal"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defaultDataDir = "data"
defaultChainSubDir = "chain"
defaultTLSCertFilename = "tls.cert"
defaultMacaroonFilename = "admin.macaroon"
defaultRPCPort = "10009"
defaultRPCHostPort = "localhost:" + defaultRPCPort
)
var (
defaultLndDir = btcutil.AppDataDir("lnd", false)
defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
)
func fatal(err error) {
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
os.Exit(1)
}
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
}
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
conn := getClientConn(ctx, false)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewLightningClient(conn), cleanUp
}
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
// First, we'll get the selected stored profile or an ephemeral one
// created from the global options in the CLI context.
profile, err := getGlobalOptions(ctx, skipMacaroons)
if err != nil {
fatal(fmt.Errorf("could not load global options: %v", err))
}
// Load the specified TLS certificate.
certPool, err := profile.cert()
if err != nil {
fatal(fmt.Errorf("could not create cert pool: %v", err))
}
// Build transport credentials from the certificate pool. If there is no
// certificate pool, we expect the server to use a non-self-signed
// certificate such as a certificate obtained from Let's Encrypt.
var creds credentials.TransportCredentials
if certPool != nil {
creds = credentials.NewClientTLSFromCert(certPool, "")
} else {
// Fallback to the system pool. Using an empty tls config is an
// alternative to x509.SystemCertPool(). That call is not
// supported on Windows.
creds = credentials.NewTLS(&tls.Config{})
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
// Only process macaroon credentials if --no-macaroons isn't set and
// if we're not skipping macaroon processing.
if !profile.NoMacaroons && !skipMacaroons {
// Find out which macaroon to load.
macName := profile.Macaroons.Default
if ctx.GlobalIsSet("macfromjar") {
macName = ctx.GlobalString("macfromjar")
}
var macEntry *macaroonEntry
for _, entry := range profile.Macaroons.Jar {
if entry.Name == macName {
macEntry = entry
break
}
}
if macEntry == nil {
fatal(fmt.Errorf("macaroon with name '%s' not found "+
"in profile", macName))
}
// Get and possibly decrypt the specified macaroon.
//
// TODO(guggero): Make it possible to cache the password so we
// don't need to ask for it every time.
mac, err := macEntry.loadMacaroon(readPassword)
if err != nil {
fatal(fmt.Errorf("could not load macaroon: %v", err))
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of the
// macaroon. It's good for 60 seconds by default to make up for
// any discrepancy between client and server clocks, but leaking
// the macaroon before it becomes invalid makes it possible for
// an attacker to reuse the macaroon. In addition, the validity
// time of the macaroon is extended by the time the server clock
// is behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more than 60
// seconds).
// TODO(aakselrod): add better anti-replay protection.
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
// Lock macaroon down to a specific IP address.
macaroons.IPLockConstraint(profile.Macaroons.IP),
// ... Add more constraints if needed.
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(
mac, macConstraints...,
)
if err != nil {
fatal(err)
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(constrainedMac)
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
opts = append(opts, grpc.WithContextDialer(genericDialer))
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
conn, err := grpc.Dial(profile.RPCServer, opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
}
return conn
}
// 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 active chain and network. These are
// needed to determine the correct path to the macaroon when not
// specified.
chain := strings.ToLower(ctx.GlobalString("chain"))
switch chain {
case "bitcoin", "litecoin":
default:
return "", "", fmt.Errorf("unknown chain: %v", chain)
}
network := strings.ToLower(ctx.GlobalString("network"))
switch network {
case "mainnet", "testnet", "regtest", "simnet":
default:
return "", "", fmt.Errorf("unknown network: %v", network)
}
// We'll now fetch the lnddir so we can make a decision on how to
// properly read the macaroons (if needed) and also the cert. This will
// either be the default, or will have been overwritten by the end
// user.
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
// If the macaroon path as been manually provided, then we'll only
// target the specified file.
var macPath string
if ctx.GlobalString("macaroonpath") != "" {
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
} else {
// Otherwise, we'll go into the path:
// lnddir/data/chain/<chain>/<network> in order to fetch the
// macaroon that we need.
macPath = filepath.Join(
lndDir, defaultDataDir, defaultChainSubDir, chain,
network, defaultMacaroonFilename,
)
}
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
// If a custom lnd directory was set, we'll also check if custom paths
// for the TLS cert and macaroon file were set as well. If not, we'll
// override their paths so they can be found within the custom lnd
// directory set. This allows us to set a custom lnd directory, along
// with custom paths to the TLS cert and macaroon file.
if lndDir != defaultLndDir {
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
}
return tlsCertPath, macPath, nil
}
func main() {
app := cli.NewApp()
app.Name = "lncli"
app.Version = build.Version() + " commit=" + build.Commit
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "rpcserver",
Value: defaultRPCHostPort,
Usage: "The host:port of LN daemon.",
},
cli.StringFlag{
Name: "lnddir",
Value: defaultLndDir,
Usage: "The path to lnd's base directory.",
},
cli.StringFlag{
Name: "tlscertpath",
Value: defaultTLSCertPath,
Usage: "The path to lnd's TLS certificate.",
},
cli.StringFlag{
Name: "chain, c",
Usage: "The chain lnd is running on, e.g. bitcoin.",
Value: "bitcoin",
},
cli.StringFlag{
Name: "network, n",
Usage: "The network lnd is running on, e.g. mainnet, " +
"testnet, etc.",
Value: "mainnet",
},
cli.BoolFlag{
Name: "no-macaroons",
Usage: "Disable macaroon authentication.",
},
cli.StringFlag{
Name: "macaroonpath",
Usage: "The path to macaroon file.",
},
cli.Int64Flag{
Name: "macaroontimeout",
Value: 60,
Usage: "Anti-replay macaroon validity time in seconds.",
},
cli.StringFlag{
Name: "macaroonip",
Usage: "If set, lock macaroon to specific IP address.",
},
cli.StringFlag{
Name: "profile, p",
Usage: "Instead of reading settings from command " +
"line parameters or using the default " +
"profile, use a specific profile. If " +
"a default profile is set, this flag can be " +
"set to an empty string to disable reading " +
"values from the profiles file.",
},
cli.StringFlag{
Name: "macfromjar",
Usage: "Use this macaroon from the profile's " +
"macaroon jar instead of the default one. " +
"Can only be used if profiles are defined.",
},
}
app.Commands = []cli.Command{
createCommand,
unlockCommand,
changePasswordCommand,
newAddressCommand,
estimateFeeCommand,
sendManyCommand,
sendCoinsCommand,
listUnspentCommand,
connectCommand,
disconnectCommand,
openChannelCommand,
closeChannelCommand,
closeAllChannelsCommand,
abandonChannelCommand,
listPeersCommand,
walletBalanceCommand,
channelBalanceCommand,
getInfoCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,
payInvoiceCommand,
sendToRouteCommand,
addInvoiceCommand,
lookupInvoiceCommand,
listInvoicesCommand,
listChannelsCommand,
closedChannelsCommand,
listPaymentsCommand,
describeGraphCommand,
getNodeMetricsCommand,
getChanInfoCommand,
getNodeInfoCommand,
queryRoutesCommand,
getNetworkInfoCommand,
debugLevelCommand,
decodePayReqCommand,
listChainTxnsCommand,
stopCommand,
signMessageCommand,
verifyMessageCommand,
feeReportCommand,
updateChannelPolicyCommand,
forwardingHistoryCommand,
exportChanBackupCommand,
verifyChanBackupCommand,
restoreChanBackupCommand,
bakeMacaroonCommand,
listMacaroonIDsCommand,
deleteMacaroonIDCommand,
listPermissionsCommand,
printMacaroonCommand,
trackPaymentCommand,
versionCommand,
profileSubCommand,
}
// Add any extra commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...)
app.Commands = append(app.Commands, invoicesCommands()...)
app.Commands = append(app.Commands, routerCommands()...)
app.Commands = append(app.Commands, walletCommands()...)
app.Commands = append(app.Commands, watchtowerCommands()...)
app.Commands = append(app.Commands, wtclientCommands()...)
if err := app.Run(os.Args); err != nil {
fatal(err)
}
}
// readPassword reads a password from the terminal. This requires there to be an
// actual TTY so passing in a password from stdin won't work.
func readPassword(text string) ([]byte, error) {
fmt.Print(text)
// The variable syscall.Stdin is of a different type in the Windows API
// that's why we need the explicit cast. And of course the linter
// doesn't like it either.
pw, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
fmt.Println()
return pw, err
}