// Copyright (c) 2013-2017 The btcsuite developers // Copyright (c) 2015-2016 The Decred developers // Copyright (C) 2015-2022 The Lightning Network Developers package main import ( "context" "crypto/tls" "fmt" "net" "os" "path/filepath" "strings" "syscall" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/tor" "github.com/urfave/cli" "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" ) const ( defaultDataDir = "data" defaultChainSubDir = "chain" defaultTLSCertFilename = "tls.cert" defaultMacaroonFilename = "admin.macaroon" defaultRPCPort = "10009" defaultRPCHostPort = "localhost:" + defaultRPCPort envVarRPCServer = "LNCLI_RPCSERVER" envVarLNDDir = "LNCLI_LNDDIR" envVarSOCKSProxy = "LNCLI_SOCKSPROXY" envVarTLSCertPath = "LNCLI_TLSCERTPATH" envVarChain = "LNCLI_CHAIN" envVarNetwork = "LNCLI_NETWORK" envVarMacaroonPath = "LNCLI_MACAROONPATH" envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT" envVarMacaroonIP = "LNCLI_MACAROONIP" envVarProfile = "LNCLI_PROFILE" envVarMacFromJar = "LNCLI_MACFROMJAR" ) 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(lnrpc.MaxGrpcMsgSize) ) 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 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()) { 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: %w", err)) } // Create a dial options array. opts := []grpc.DialOption{ grpc.WithUnaryInterceptor( addMetadataUnaryInterceptor(profile.Metadata), ), grpc.WithStreamInterceptor( addMetaDataStreamInterceptor(profile.Metadata), ), } if profile.Insecure { opts = append(opts, grpc.WithInsecure()) } else { // Load the specified TLS certificate. certPool, err := profile.cert() if err != nil { fatal(fmt.Errorf("could not create cert pool: %w", 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{}) } opts = append(opts, 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: %w", 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, err := macaroons.NewMacaroonCredential(constrainedMac) if err != nil { fatal(fmt.Errorf("error cloning mac: %w", err)) } opts = append(opts, grpc.WithPerRPCCredentials(cred)) } // If a socksproxy server is specified we use a tor dialer // to connect to the grpc server. if ctx.GlobalIsSet("socksproxy") { socksProxy := ctx.GlobalString("socksproxy") torDialer := func(_ context.Context, addr string) (net.Conn, error) { return tor.Dial( addr, socksProxy, false, false, tor.DefaultConnTimeout, ) } opts = append(opts, grpc.WithContextDialer(torDialer)) } else { // 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: %w", err)) } return conn } // addMetadataUnaryInterceptor returns a grpc client side interceptor that // appends any key-value metadata strings to the outgoing context of a grpc // unary call. func addMetadataUnaryInterceptor( md map[string]string) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { outCtx := contextWithMetadata(ctx, md) return invoker(outCtx, method, req, reply, cc, opts...) } } // addMetaDataStreamInterceptor returns a grpc client side interceptor that // appends any key-value metadata strings to the outgoing context of a grpc // stream call. func addMetaDataStreamInterceptor( md map[string]string) grpc.StreamClientInterceptor { return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { outCtx := contextWithMetadata(ctx, md) return streamer(outCtx, desc, cc, method, opts...) } } // contextWithMetaData appends the given metadata key-value pairs to the given // context. func contextWithMetadata(ctx context.Context, md map[string]string) context.Context { kvPairs := make([]string, 0, 2*len(md)) for k, v := range md { kvPairs = append(kvPairs, k, v) } return metadata.AppendToOutgoingContext(ctx, kvPairs...) } // extractPathArgs parses the TLS certificate and macaroon paths from the // command. func extractPathArgs(ctx *cli.Context) (string, string, error) { 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// in order to fetch the // macaroon that we need. macPath = filepath.Join( lndDir, defaultDataDir, defaultChainSubDir, lnd.BitcoinChainName, 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", Value: defaultRPCHostPort, Usage: "The host:port of LN daemon.", EnvVar: envVarRPCServer, }, cli.StringFlag{ Name: "lnddir", Value: defaultLndDir, Usage: "The path to lnd's base directory.", TakesFile: true, EnvVar: envVarLNDDir, }, cli.StringFlag{ Name: "socksproxy", Usage: "The host:port of a SOCKS proxy through " + "which all connections to the LN " + "daemon will be established over.", EnvVar: envVarSOCKSProxy, }, cli.StringFlag{ Name: "tlscertpath", Value: defaultTLSCertPath, Usage: "The path to lnd's TLS certificate.", TakesFile: true, EnvVar: envVarTLSCertPath, }, cli.StringFlag{ Name: "chain, c", Usage: "The chain lnd is running on, e.g. bitcoin.", Value: "bitcoin", EnvVar: envVarChain, }, cli.StringFlag{ Name: "network, n", Usage: "The network lnd is running on, e.g. mainnet, " + "testnet, etc.", Value: "mainnet", EnvVar: envVarNetwork, }, cli.BoolFlag{ Name: "no-macaroons", Usage: "Disable macaroon authentication.", }, cli.StringFlag{ Name: "macaroonpath", Usage: "The path to macaroon file.", TakesFile: true, EnvVar: envVarMacaroonPath, }, cli.Int64Flag{ Name: "macaroontimeout", Value: 60, Usage: "Anti-replay macaroon validity time in " + "seconds.", EnvVar: envVarMacaroonTimeout, }, cli.StringFlag{ Name: "macaroonip", Usage: "If set, lock macaroon to specific IP address.", EnvVar: envVarMacaroonIP, }, 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.", EnvVar: envVarProfile, }, 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.", EnvVar: envVarMacFromJar, }, cli.StringSliceFlag{ Name: "metadata", Usage: "This flag can be used to specify a key-value " + "pair that should be appended to the " + "outgoing context before the request is sent " + "to lnd. This flag may be specified multiple " + "times. The format is: \"key:value\".", }, cli.BoolFlag{ Name: "insecure", Usage: "Connect to the rpc server without TLS " + "authentication", Hidden: true, }, } app.Commands = []cli.Command{ createCommand, createWatchOnlyCommand, unlockCommand, changePasswordCommand, newAddressCommand, estimateFeeCommand, sendManyCommand, sendCoinsCommand, listUnspentCommand, connectCommand, disconnectCommand, openChannelCommand, batchOpenChannelCommand, closeChannelCommand, closeAllChannelsCommand, abandonChannelCommand, listPeersCommand, walletBalanceCommand, channelBalanceCommand, getInfoCommand, getDebugInfoCommand, encryptDebugPackageCommand, decryptDebugPackageCommand, 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, constrainMacaroonCommand, trackPaymentCommand, versionCommand, profileSubCommand, getStateCommand, deletePaymentsCommand, sendCustomCommand, subscribeCustomCommand, fishCompletionCommand, listAliasesCommand, estimateRouteFeeCommand, generateManPageCommand, } // 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, neutrinoCommands()...) app.Commands = append(app.Commands, routerCommands()...) app.Commands = append(app.Commands, walletCommands()...) app.Commands = append(app.Commands, watchtowerCommands()...) app.Commands = append(app.Commands, wtclientCommands()...) app.Commands = append(app.Commands, devCommands()...) app.Commands = append(app.Commands, peersCommands()...) app.Commands = append(app.Commands, chainCommands()...) 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 := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert fmt.Println() return pw, err } // networkParams parses the global network flag into a chaincfg.Params. func networkParams(ctx *cli.Context) (*chaincfg.Params, error) { network := strings.ToLower(ctx.GlobalString("network")) switch network { case "mainnet": return &chaincfg.MainNetParams, nil case "testnet": return &chaincfg.TestNet3Params, nil case "regtest": return &chaincfg.RegressionNetParams, nil case "simnet": return &chaincfg.SimNetParams, nil case "signet": return &chaincfg.SigNetParams, nil default: return nil, fmt.Errorf("unknown network: %v", network) } } // parseCoinSelectionStrategy parses a coin selection strategy string // from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type. func parseCoinSelectionStrategy(ctx *cli.Context) ( lnrpc.CoinSelectionStrategy, error) { strategy := ctx.String(coinSelectionStrategyFlag.Name) if !ctx.IsSet(coinSelectionStrategyFlag.Name) { return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, nil } switch strategy { case "global-config": return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, nil case "largest": return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil case "random": return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil default: return 0, fmt.Errorf("unknown coin selection strategy "+ "%v", strategy) } }