mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
0da35edc48
This commit adds three optional command line flags to the encryptdebugpackage: --peers, --onchain and --channels. Each of them adds the output of extra commands to the encrypted debug package.
451 lines
12 KiB
Go
451 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightningnetwork/lnd"
|
|
"github.com/lightningnetwork/lnd/lnencrypt"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/urfave/cli"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
var getDebugInfoCommand = cli.Command{
|
|
Name: "getdebuginfo",
|
|
Category: "Debug",
|
|
Usage: "Returns debug information related to the active daemon.",
|
|
Action: actionDecorator(getDebugInfo),
|
|
}
|
|
|
|
func getDebugInfo(ctx *cli.Context) error {
|
|
ctxc := getContext()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.GetDebugInfoRequest{}
|
|
resp, err := client.GetDebugInfo(ctxc, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
type DebugPackage struct {
|
|
EphemeralPubKey string `json:"ephemeral_public_key"`
|
|
EncryptedPayload string `json:"encrypted_payload"`
|
|
}
|
|
|
|
var encryptDebugPackageCommand = cli.Command{
|
|
Name: "encryptdebugpackage",
|
|
Category: "Debug",
|
|
Usage: "Collects a package of debug information and encrypts it.",
|
|
Description: `
|
|
When requesting support with lnd, it's often required to submit a lot of
|
|
debug information to the developer in order to track down a problem.
|
|
This command will collect all the relevant information and encrypt it
|
|
using the provided public key. The resulting file can then be sent to
|
|
the developer for further analysis.
|
|
Because the file is encrypted, it is safe to send it over insecure
|
|
channels or upload it to a GitHub issue.
|
|
|
|
The file by default contains the output of the following commands:
|
|
- lncli getinfo
|
|
- lncli getdebuginfo
|
|
- lncli getnetworkinfo
|
|
|
|
By specifying the following flags, additional information can be added
|
|
to the file (usually this will be requested by the developer depending
|
|
on the issue at hand):
|
|
--peers:
|
|
- lncli listpeers
|
|
--onchain:
|
|
- lncli listunspent
|
|
- lncli listchaintxns
|
|
--channels:
|
|
- lncli listchannels
|
|
- lncli pendingchannels
|
|
- lncli closedchannels
|
|
|
|
Use 'lncli encryptdebugpackage 0xxxxxx... > package.txt' to write the
|
|
encrypted package to a file called package.txt.
|
|
`,
|
|
ArgsUsage: "pubkey [--output_file F]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "pubkey",
|
|
Usage: "the public key to encrypt the information " +
|
|
"for (hex-encoded, e.g. 02aabb..), this " +
|
|
"should be provided to you by the issue " +
|
|
"tracker or developer you're requesting " +
|
|
"support from",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output_file",
|
|
Usage: "(optional) the file to write the encrypted " +
|
|
"package to; if not specified, the debug " +
|
|
"package is printed to stdout",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "peers",
|
|
Usage: "include information about connected peers " +
|
|
"(lncli listpeers)",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "onchain",
|
|
Usage: "include information about on-chain " +
|
|
"transactions (lncli listunspent, " +
|
|
"lncli listchaintxns)",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "channels",
|
|
Usage: "include information about channels " +
|
|
"(lncli listchannels, lncli pendingchannels, " +
|
|
"lncli closedchannels)",
|
|
},
|
|
},
|
|
Action: actionDecorator(encryptDebugPackage),
|
|
}
|
|
|
|
func encryptDebugPackage(ctx *cli.Context) error {
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
return cli.ShowCommandHelp(ctx, "encryptdebugpackage")
|
|
}
|
|
|
|
var (
|
|
args = ctx.Args()
|
|
pubKeyBytes []byte
|
|
err error
|
|
)
|
|
switch {
|
|
case ctx.IsSet("pubkey"):
|
|
pubKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
|
case args.Present():
|
|
pubKeyBytes, err = hex.DecodeString(args.First())
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode pubkey argument: %w", err)
|
|
}
|
|
|
|
pubKey, err := btcec.ParsePubKey(pubKeyBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse pubkey: %w", err)
|
|
}
|
|
|
|
// Collect the information we want to send from the daemon.
|
|
payload, err := collectDebugPackageInfo(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to collect debug package "+
|
|
"information: %w", err)
|
|
}
|
|
|
|
// We've collected the information we want to send, but before
|
|
// encrypting it, we want to compress it as much as possible to reduce
|
|
// the size of the final payload.
|
|
var (
|
|
compressBuf bytes.Buffer
|
|
options = brotli.WriterOptions{
|
|
Quality: brotli.BestCompression,
|
|
}
|
|
writer = brotli.NewWriterOptions(&compressBuf, options)
|
|
)
|
|
_, err = writer.Write(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to compress payload: %w", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return fmt.Errorf("unable to compress payload: %w", err)
|
|
}
|
|
|
|
// Now we have the full payload that we want to encrypt, so we'll create
|
|
// an ephemeral keypair to encrypt the payload with.
|
|
localKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to generate local key: %w", err)
|
|
}
|
|
|
|
enc, err := lnencrypt.ECDHEncrypter(localKey, pubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create encrypter: %w", err)
|
|
}
|
|
|
|
var cipherBuf bytes.Buffer
|
|
err = enc.EncryptPayloadToWriter(compressBuf.Bytes(), &cipherBuf)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encrypt payload: %w", err)
|
|
}
|
|
|
|
response := DebugPackage{
|
|
EphemeralPubKey: hex.EncodeToString(
|
|
localKey.PubKey().SerializeCompressed(),
|
|
),
|
|
EncryptedPayload: hex.EncodeToString(
|
|
cipherBuf.Bytes(),
|
|
),
|
|
}
|
|
|
|
// If the user specified an output file, we'll write the encrypted
|
|
// payload to that file.
|
|
if ctx.IsSet("output_file") {
|
|
fileName := lnd.CleanAndExpandPath(ctx.String("output_file"))
|
|
jsonBytes, err := json.Marshal(response)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to encode JSON: %w", err)
|
|
}
|
|
|
|
return os.WriteFile(fileName, jsonBytes, 0644)
|
|
}
|
|
|
|
// Finally, we'll print out the final payload as a JSON if no output
|
|
// file was specified.
|
|
printJSON(response)
|
|
|
|
return nil
|
|
}
|
|
|
|
// collectDebugPackageInfo collects the information we want to send to the
|
|
// developer(s) from the daemon.
|
|
func collectDebugPackageInfo(ctx *cli.Context) ([]byte, error) {
|
|
ctxc := getContext()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
info, err := client.GetInfo(ctxc, &lnrpc.GetInfoRequest{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting info: %w", err)
|
|
}
|
|
|
|
debugInfo, err := client.GetDebugInfo(
|
|
ctxc, &lnrpc.GetDebugInfoRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting debug info: %w", err)
|
|
}
|
|
|
|
networkInfo, err := client.GetNetworkInfo(
|
|
ctxc, &lnrpc.NetworkInfoRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting network info: %w", err)
|
|
}
|
|
|
|
var payloadBuf bytes.Buffer
|
|
addToBuf := func(msgs ...proto.Message) error {
|
|
for _, msg := range msgs {
|
|
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(
|
|
msg,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error encoding response: %w",
|
|
err)
|
|
}
|
|
|
|
payloadBuf.Write(jsonBytes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := addToBuf(info); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addToBuf(debugInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addToBuf(info, debugInfo, networkInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add optional information to the payload.
|
|
if ctx.Bool("peers") {
|
|
peers, err := client.ListPeers(ctxc, &lnrpc.ListPeersRequest{
|
|
LatestError: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting peers: %w", err)
|
|
}
|
|
if err := addToBuf(peers); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if ctx.Bool("onchain") {
|
|
unspent, err := client.ListUnspent(
|
|
ctxc, &lnrpc.ListUnspentRequest{
|
|
MaxConfs: math.MaxInt32,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting unspent: %w", err)
|
|
}
|
|
chainTxns, err := client.GetTransactions(
|
|
ctxc, &lnrpc.GetTransactionsRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting chain txns: %w",
|
|
err)
|
|
}
|
|
if err := addToBuf(unspent, chainTxns); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if ctx.Bool("channels") {
|
|
channels, err := client.ListChannels(
|
|
ctxc, &lnrpc.ListChannelsRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting channels: %w",
|
|
err)
|
|
}
|
|
pendingChannels, err := client.PendingChannels(
|
|
ctxc, &lnrpc.PendingChannelsRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting pending "+
|
|
"channels: %w", err)
|
|
}
|
|
closedChannels, err := client.ClosedChannels(
|
|
ctxc, &lnrpc.ClosedChannelsRequest{},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting closed "+
|
|
"channels: %w", err)
|
|
}
|
|
if err := addToBuf(
|
|
channels, pendingChannels, closedChannels,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return payloadBuf.Bytes(), nil
|
|
}
|
|
|
|
var decryptDebugPackageCommand = cli.Command{
|
|
Name: "decryptdebugpackage",
|
|
Category: "Debug",
|
|
Usage: "Decrypts a package of debug information.",
|
|
Description: `
|
|
Decrypt a debug package that was created with the encryptdebugpackage
|
|
command. Decryption requires the private key that corresponds to the
|
|
public key the package was encrypted to.
|
|
The command expects the encrypted package JSON to be provided on stdin.
|
|
If decryption is successful, the information will be printed to stdout.
|
|
|
|
Use 'lncli decryptdebugpackage 0xxxxxx... < package.txt > decrypted.txt'
|
|
to read the encrypted package from a file called package.txt and to
|
|
write the decrypted content to a file called decrypted.txt.
|
|
`,
|
|
ArgsUsage: "privkey [--input_file F]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "privkey",
|
|
Usage: "the hex encoded private key to decrypt the " +
|
|
"debug package",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input_file",
|
|
Usage: "(optional) the file to read the encrypted " +
|
|
"package from; if not specified, the debug " +
|
|
"package is read from stdin",
|
|
},
|
|
},
|
|
Action: actionDecorator(decryptDebugPackage),
|
|
}
|
|
|
|
func decryptDebugPackage(ctx *cli.Context) error {
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
return cli.ShowCommandHelp(ctx, "decryptdebugpackage")
|
|
}
|
|
|
|
var (
|
|
args = ctx.Args()
|
|
privKeyBytes []byte
|
|
err error
|
|
)
|
|
switch {
|
|
case ctx.IsSet("pubkey"):
|
|
privKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
|
case args.Present():
|
|
privKeyBytes, err = hex.DecodeString(args.First())
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode privkey argument: %w", err)
|
|
}
|
|
|
|
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
|
|
|
// Read the file from stdin and decode the JSON into a DebugPackage.
|
|
var pkg DebugPackage
|
|
if ctx.IsSet("input_file") {
|
|
fileName := lnd.CleanAndExpandPath(ctx.String("input_file"))
|
|
jsonBytes, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read file '%s': %w",
|
|
fileName, err)
|
|
}
|
|
|
|
err = json.Unmarshal(jsonBytes, &pkg)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode JSON: %w", err)
|
|
}
|
|
} else {
|
|
err = json.NewDecoder(os.Stdin).Decode(&pkg)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode JSON: %w", err)
|
|
}
|
|
}
|
|
|
|
// Decode the ephemeral public key and encrypted payload.
|
|
ephemeralPubKeyBytes, err := hex.DecodeString(pkg.EphemeralPubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode ephemeral public key: %w",
|
|
err)
|
|
}
|
|
encryptedPayloadBytes, err := hex.DecodeString(pkg.EncryptedPayload)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode encrypted payload: %w", err)
|
|
}
|
|
|
|
// Parse the ephemeral public key and create an encrypter.
|
|
ephemeralPubKey, err := btcec.ParsePubKey(ephemeralPubKeyBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse ephemeral public key: %w",
|
|
err)
|
|
}
|
|
enc, err := lnencrypt.ECDHEncrypter(privKey, ephemeralPubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create encrypter: %w", err)
|
|
}
|
|
|
|
// Decrypt the payload.
|
|
decryptedPayload, err := enc.DecryptPayloadFromReader(
|
|
bytes.NewReader(encryptedPayloadBytes),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decrypt payload: %w", err)
|
|
}
|
|
|
|
// Decompress the payload.
|
|
reader := brotli.NewReader(bytes.NewBuffer(decryptedPayload))
|
|
decompressedPayload, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decompress payload: %w", err)
|
|
}
|
|
|
|
fmt.Println(string(decompressedPayload))
|
|
|
|
return nil
|
|
}
|