lnd/cmd/lncli/main.go

420 lines
12 KiB
Go
Raw Normal View History

// 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"
2020-09-04 16:06:12 +02:00
"syscall"
2018-07-31 09:17:17 +02:00
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/build"
2018-05-23 15:41:36 +02:00
"github.com/lightningnetwork/lnd/lncfg"
2016-01-16 19:45:54 +01:00
"github.com/lightningnetwork/lnd/lnrpc"
2017-08-18 03:51:33 +02:00
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defaultDataDir = "data"
defaultChainSubDir = "chain"
2017-08-18 03:51:33 +02:00
defaultTLSCertFilename = "tls.cert"
defaultMacaroonFilename = "admin.macaroon"
2018-07-31 10:29:12 +02:00
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()) {
2018-02-01 01:04:56 +01:00
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
}
2021-02-09 15:20:18 +01:00
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewStateClient(conn), cleanUp
}
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
2018-02-01 01:04:56 +01:00
conn := getClientConn(ctx, false)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewLightningClient(conn), cleanUp
}
2018-02-01 01:04:56 +01:00
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
2020-09-04 16:06:12 +02:00
// 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 {
2020-09-04 16:06:12 +02:00
fatal(fmt.Errorf("could not load global options: %v", err))
}
// Load the specified TLS certificate.
2020-09-04 16:06:12 +02:00
certPool, err := profile.cert()
2017-08-18 03:51:33 +02:00
if err != nil {
2020-09-04 16:06:12 +02:00
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{})
}
2017-08-18 03:51:33 +02:00
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
2017-08-18 03:51:33 +02:00
2018-02-01 01:04:56 +01:00
// Only process macaroon credentials if --no-macaroons isn't set and
// if we're not skipping macaroon processing.
2020-09-04 16:06:12 +02:00
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))
2017-08-18 03:51:33 +02:00
}
2020-09-04 16:06:12 +02:00
// 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))
2017-08-18 03:51:33 +02:00
}
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.
2020-09-04 16:06:12 +02:00
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
// Lock macaroon down to a specific IP address.
2020-09-04 16:06:12 +02:00
macaroons.IPLockConstraint(profile.Macaroons.IP),
// ... Add more constraints if needed.
}
// Apply constraints to the macaroon.
2020-09-04 16:06:12 +02:00
constrainedMac, err := macaroons.AddConstraints(
mac, macConstraints...,
)
if err != nil {
fatal(err)
}
2017-08-18 03:51:33 +02:00
// Now we append the macaroon credentials to the dial options.
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
fatal(fmt.Errorf("error cloning mac: %v", err))
}
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
2018-05-23 15:41:36 +02:00
// 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))
2020-09-04 16:06:12 +02:00
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", "signet":
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
}
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
// or flag b can be set, but not both. It returns the name of the flag or an
// error.
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
if ctx.IsSet(a) && ctx.IsSet(b) {
return "", fmt.Errorf(
"either %s or %s should be set, but not both", a, b,
)
}
if ctx.IsSet(a) {
return a, nil
}
return b, 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",
2018-07-31 10:29:12 +02:00
Value: defaultRPCHostPort,
2020-09-04 16:06:12 +02:00
Usage: "The host:port of LN daemon.",
},
cli.StringFlag{
Name: "lnddir",
Value: defaultLndDir,
Usage: "The path to lnd's base directory.",
TakesFile: true,
},
cli.StringFlag{
Name: "tlscertpath",
Value: defaultTLSCertPath,
Usage: "The path to lnd's TLS certificate.",
TakesFile: true,
},
cli.StringFlag{
Name: "chain, c",
2020-09-04 16:06:12 +02:00
Usage: "The chain lnd is running on, e.g. bitcoin.",
Value: "bitcoin",
},
cli.StringFlag{
Name: "network, n",
2020-09-04 16:06:12 +02:00
Usage: "The network lnd is running on, e.g. mainnet, " +
"testnet, etc.",
Value: "mainnet",
},
2017-08-18 03:51:33 +02:00
cli.BoolFlag{
Name: "no-macaroons",
2020-09-04 16:06:12 +02:00
Usage: "Disable macaroon authentication.",
2017-08-18 03:51:33 +02:00
},
cli.StringFlag{
Name: "macaroonpath",
Usage: "The path to macaroon file.",
TakesFile: true,
2017-08-18 03:51:33 +02:00
},
cli.Int64Flag{
Name: "macaroontimeout",
Value: 60,
2020-09-04 16:06:12 +02:00
Usage: "Anti-replay macaroon validity time in seconds.",
2017-08-18 03:51:33 +02:00
},
cli.StringFlag{
Name: "macaroonip",
2020-09-04 16:06:12 +02:00
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,
createWatchOnlyCommand,
unlockCommand,
2018-04-20 09:14:51 +02:00
changePasswordCommand,
newAddressCommand,
2019-03-05 14:22:30 +01:00
estimateFeeCommand,
sendManyCommand,
sendCoinsCommand,
listUnspentCommand,
connectCommand,
disconnectCommand,
openChannelCommand,
2021-08-24 11:21:57 +02:00
batchOpenChannelCommand,
closeChannelCommand,
closeAllChannelsCommand,
abandonChannelCommand,
listPeersCommand,
walletBalanceCommand,
channelBalanceCommand,
getInfoCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,
payInvoiceCommand,
sendToRouteCommand,
addInvoiceCommand,
lookupInvoiceCommand,
listInvoicesCommand,
listChannelsCommand,
2018-05-24 11:35:53 +02:00
closedChannelsCommand,
listPaymentsCommand,
describeGraphCommand,
getNodeMetricsCommand,
getChanInfoCommand,
getNodeInfoCommand,
queryRoutesCommand,
getNetworkInfoCommand,
debugLevelCommand,
decodePayReqCommand,
listChainTxnsCommand,
stopCommand,
signMessageCommand,
verifyMessageCommand,
feeReportCommand,
updateChannelPolicyCommand,
forwardingHistoryCommand,
exportChanBackupCommand,
verifyChanBackupCommand,
restoreChanBackupCommand,
bakeMacaroonCommand,
listMacaroonIDsCommand,
deleteMacaroonIDCommand,
2020-09-04 09:22:49 +02:00
listPermissionsCommand,
printMacaroonCommand,
2020-03-30 12:55:10 +02:00
trackPaymentCommand,
versionCommand,
profileSubCommand,
2021-02-09 15:20:18 +01:00
getStateCommand,
deletePaymentsCommand,
2021-05-31 10:03:47 +02:00
sendCustomCommand,
2021-05-31 12:06:48 +02:00
subscribeCustomCommand,
}
// Add any extra commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...)
2018-11-26 14:19:43 +01:00
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)
}
}
2020-09-04 16:06:12 +02:00
// 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 := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
2020-09-04 16:06:12 +02:00
fmt.Println()
return pw, err
}