Compare commits

...

4 Commits

Author SHA1 Message Date
Oliver Gugger a6cd3ab04c
findlostchannel: add new command to ask all nodes about a channel 3 weeks ago
Oliver Gugger cc284baa67
Merge pull request #132 from lightninglabs/triggerforceclose
triggerforceclose: make compatible with all nodes, add Tor support
3 weeks ago
Oliver Gugger 6acc81815e
root: bump version to v0.13.1 3 weeks ago
Oliver Gugger e3285daf5b
triggerforceclose: support Tor connections 3 weeks ago

@ -0,0 +1,148 @@
package main
import (
"fmt"
"os"
"time"
"github.com/gogo/protobuf/jsonpb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/spf13/cobra"
)
type findLostChannelCommand struct {
ChannelGraph string
ChannelPoint string
TorProxy string
ConnectTimeout time.Duration
rootKey *rootKey
cmd *cobra.Command
}
func newFindLostChannelCommand() *cobra.Command {
cc := &findLostChannelCommand{}
cc.cmd = &cobra.Command{
Use: "findlostchannel",
Short: "Try to find out what node a channel was made with",
Long: `Connects to _all_ nodes given in the graph file and
attempts to find out if the node has knowledge of the given channel.
Can be used to try to recover funds if the peer is not known (because no
channel.backup was created).
The graph.json file can be created with 'lncli describegraph > graph.json' on
any node in the network.`,
Example: `chantools findlostchannel \
--channel_graph graph.json \
--channel_point abcdef01234...:x`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.ChannelGraph, "channel_graph", "", "the channel graph "+
"file as a JSON file, containing all nodes and their "+
"network addresses of the network",
)
cc.cmd.Flags().StringVar(
&cc.ChannelPoint, "channel_point", "", "funding transaction "+
"outpoint of the channel to attempt to find the peer "+
"for (<txid>:<txindex>)",
)
cc.cmd.Flags().StringVar(
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
"connections (to .onion addresses)",
)
cc.cmd.Flags().DurationVar(
&cc.ConnectTimeout, "connect_timeout", time.Second*30, "time "+
"to wait for a connection to be established",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
return cc.cmd
}
func (c *findLostChannelCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
identityPath := lnd.IdentityPath(chainParams)
child, pubKey, _, err := lnd.DeriveKey(
extendedKey, identityPath, chainParams,
)
if err != nil {
return fmt.Errorf("could not derive identity key: %w", err)
}
identityPriv, err := child.ECPrivKey()
if err != nil {
return fmt.Errorf("could not get identity private key: %w", err)
}
identityECDH := &keychain.PrivKeyECDH{
PrivKey: identityPriv,
}
outPoint, err := parseOutPoint(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
}
graphBytes, err := os.ReadFile(c.ChannelGraph)
if err != nil {
return fmt.Errorf("error reading graph JSON file %s: "+
"%v", c.ChannelGraph, err)
}
graph := &lnrpc.ChannelGraph{}
err = jsonpb.UnmarshalString(string(graphBytes), graph)
if err != nil {
return fmt.Errorf("error parsing graph JSON: %w", err)
}
for idx := range graph.Nodes {
node := graph.Nodes[idx]
if node.PubKey == "" {
continue
}
for _, addr := range node.Addresses {
nodeAddr := fmt.Sprintf("%s@%s", node.PubKey, addr.Addr)
p, err := connectPeer(
nodeAddr, c.TorProxy, pubKey, identityECDH,
c.ConnectTimeout,
)
if err != nil {
log.Infof("Error with node %s: %v", node.PubKey,
err)
continue
}
channelID := lnwire.NewChanIDFromOutPoint(outPoint)
// Channel ID (32 byte) + u16 for the data length (which
// will be 0).
data := make([]byte, 34)
copy(data[:32], channelID[:])
log.Infof("Sending channel re-establish to peer to "+
"trigger force close of channel %v", outPoint)
err = p.SendMessageLazy(
true, &lnwire.ChannelReestablish{
ChanID: channelID,
},
)
if err != nil {
return err
}
// Continue with next node.
break
}
}
return nil
}

@ -30,7 +30,7 @@ const (
// version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the
// download link shown in the README.
version = "0.13.0"
version = "0.13.1"
na = "n/a"
// lndVersion is the current version of lnd that we support. This is
@ -110,6 +110,7 @@ func main() {
newDocCommand(),
newFakeChanBackupCommand(),
newFilterBackupCommand(),
newFindLostChannelCommand(),
newFixOldBackupCommand(),
newForceCloseCommand(),
newGenImportScriptCommand(),

@ -2,7 +2,6 @@ package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
@ -16,12 +15,15 @@ import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra"
)
var (
dialTimeout = time.Minute
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
)
type triggerForceCloseCommand struct {
@ -30,6 +32,8 @@ type triggerForceCloseCommand struct {
APIURL string
TorProxy string
rootKey *rootKey
cmd *cobra.Command
}
@ -63,6 +67,10 @@ does not properly respond to a Data Loss Protection re-establish message).'`,
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
"connections (to .onion addresses)",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
return cc.cmd
@ -94,7 +102,9 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("error parsing channel point: %w", err)
}
err = requestForceClose(c.Peer, pubKey, outPoint, identityECDH)
err = requestForceClose(
c.Peer, c.TorProxy, pubKey, outPoint, identityECDH,
)
if err != nil {
return fmt.Errorf("error requesting force close: %w", err)
}
@ -134,34 +144,44 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}
func requestForceClose(peerHost string, peerPubKey *btcec.PublicKey,
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {
var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
dialNet = &tor.ProxyNet{
SOCKS: torProxy,
DNS: defaultTorDNSHostPort,
StreamIsolation: false,
SkipProxyForClearNetTargets: true,
}
}
log.Debugf("Attempting to resolve peer address %v", peerHost)
peerAddr, err := lncfg.ParseLNAddressString(
peerHost, "9735", net.ResolveTCPAddr,
peerHost, "9735", dialNet.ResolveTCPAddr,
)
if err != nil {
return fmt.Errorf("error parsing peer address: %w", err)
return nil, fmt.Errorf("error parsing peer address: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
conn, err := noiseDial(
identity, peerAddr, &tor.ClearNet{}, dialTimeout,
)
log.Debugf("Attempting to dial resolved peer address %v",
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return fmt.Errorf("error dialing peer: %w", err)
return nil, fmt.Errorf("error dialing peer: %w", err)
}
log.Infof("Attempting to connect to peer %x, dial timeout is %v",
peerPubKey.SerializeCompressed(), dialTimeout)
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
req := &connmgr.ConnReq{
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
return nil, fmt.Errorf("error connecting to peer: %w", err)
}
log.Infof("Connection established to peer %x",
@ -171,10 +191,25 @@ func requestForceClose(peerHost string, peerPubKey *btcec.PublicKey,
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return fmt.Errorf("peer %x disconnected",
return nil, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
}
return p, nil
}
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {
p, err := connectPeer(
peerHost, torProxy, peerPubKey, identity, dialTimeout,
)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
// Channel ID (32 byte) + u16 for the data length (which will be 0).
data := make([]byte, 34)
copy(data[:32], channelID[:])

@ -30,6 +30,7 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>])
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```

Loading…
Cancel
Save