Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions chanrestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,11 @@ func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error {
"with chan restore", nodePub.SerializeCompressed())
}

// Strip persisted Tor v2 .onion entries that may have been carried
// over in an old static channel backup: Tor stopped serving v2 in 2021
// and the dial would never succeed. Covered by TestWithoutV2Onion.
addrs = withoutV2Onion(addrs)

// For each of the known addresses, we'll attempt to launch a
// persistent connection to the (pub, addr) pair. In the event that any
// of them connect, all the other stale requests will be canceled.
Expand Down
40 changes: 10 additions & 30 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ const (
defaultTorDNSHost = "soa.nodes.lightning.directory"
defaultTorDNSPort = 53
defaultTorControlPort = 9051
defaultTorV2PrivateKeyFilename = "v2_onion_private_key"
defaultTorV3PrivateKeyFilename = "v3_onion_private_key"

// defaultZMQReadDeadline is the default read deadline to be used for
Expand Down Expand Up @@ -1239,41 +1238,22 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
return nil, mkErr(str)
}

switch {
case cfg.Tor.V2 && cfg.Tor.V3:
return nil, mkErr("either tor.v2 or tor.v3 can be set, " +
"but not both")
case cfg.DisableListen && (cfg.Tor.V2 || cfg.Tor.V3):
if cfg.DisableListen && cfg.Tor.V3 {
return nil, mkErr("listening must be enabled when enabling " +
"inbound connections over Tor")
}

if cfg.Tor.PrivateKeyPath == "" {
switch {
case cfg.Tor.V2:
cfg.Tor.PrivateKeyPath = filepath.Join(
lndDir, defaultTorV2PrivateKeyFilename,
)
case cfg.Tor.V3:
cfg.Tor.PrivateKeyPath = filepath.Join(
lndDir, defaultTorV3PrivateKeyFilename,
)
}
if cfg.Tor.PrivateKeyPath == "" && cfg.Tor.V3 {
cfg.Tor.PrivateKeyPath = filepath.Join(
lndDir, defaultTorV3PrivateKeyFilename,
)
}

if cfg.Tor.WatchtowerKeyPath == "" {
switch {
case cfg.Tor.V2:
cfg.Tor.WatchtowerKeyPath = filepath.Join(
cfg.Watchtower.TowerDir,
defaultTorV2PrivateKeyFilename,
)
case cfg.Tor.V3:
cfg.Tor.WatchtowerKeyPath = filepath.Join(
cfg.Watchtower.TowerDir,
defaultTorV3PrivateKeyFilename,
)
}
if cfg.Tor.WatchtowerKeyPath == "" && cfg.Tor.V3 {
cfg.Tor.WatchtowerKeyPath = filepath.Join(
cfg.Watchtower.TowerDir,
defaultTorV3PrivateKeyFilename,
)
}

// Set up the network-related functions that will be used throughout
Expand Down
2 changes: 0 additions & 2 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,12 @@ func TestConfigToFlatMap(t *testing.T) {

// Set deprecated fields.
cfg.Bitcoin.Active = true
cfg.Tor.V2 = true

result, deprecated, err := configToFlatMap(cfg)
require.NoError(t, err)

// Check that the deprecated option has been parsed out.
require.Contains(t, deprecated, "bitcoin.active")
require.Contains(t, deprecated, "tor.v2")

// Pick a couple of random values to check.
require.Equal(t, DefaultLndDir, result["lnddir"])
Expand Down
23 changes: 20 additions & 3 deletions discovery/bootstrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,24 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
return nil
}

foundAddr := false
for _, nodeAddr := range node.Addrs() {
// If we haven't yet reached our limit, then
// we'll copy over the details of this node
// into the set of addresses to be returned.
switch nodeAddr.(type) {
case *net.TCPAddr, *tor.OnionAddr:
switch onion := nodeAddr.(type) {
case *net.TCPAddr:
case *tor.OnionAddr:
// Skip persisted Tor v2 .onion
// entries: Tor stopped serving them
// in 2021 and the dial would never
// succeed. Other addresses of the
// same node may still be usable.
if len(onion.OnionService) ==
tor.V2Len {

continue
Comment thread
erickcestari marked this conversation as resolved.
}
default:
// If this isn't a valid address
// supported by the protocol, then we'll
Expand All @@ -245,9 +257,14 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
IdentityKey: nodePub,
Address: nodeAddr,
})
foundAddr = true
}

return errFound
if foundAddr {
return errFound
}

return nil
}, func() {
clear(a)
})
Expand Down
143 changes: 143 additions & 0 deletions discovery/bootstrapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package discovery

import (
"context"
"net"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/tor"
"github.com/stretchr/testify/require"
)

// stubNode is a minimal autopilot.Node implementation used to drive
// ChannelGraphBootstrapper from a unit test.
type stubNode struct {
pub *btcec.PublicKey
addrs []net.Addr
}

func (n *stubNode) PubKey() [33]byte {
var out [33]byte
copy(out[:], n.pub.SerializeCompressed())
return out
}

func (n *stubNode) Addrs() []net.Addr { return n.addrs }

// stubChannelGraph yields a fixed list of nodes from ForEachNode and is
// otherwise unused by SampleNodeAddrs.
type stubChannelGraph struct {
nodes []autopilot.Node
}

func (s *stubChannelGraph) ForEachNode(ctx context.Context,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing godoc

cb func(context.Context, autopilot.Node) error, _ func()) error {

for _, n := range s.nodes {
if err := cb(ctx, n); err != nil {
return err
}
}

return nil
}

func (s *stubChannelGraph) ForEachNodesChannels(_ context.Context,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing godoc

_ func(context.Context, autopilot.NodeID,
[]*autopilot.ChannelEdge) error, _ func()) error {

return nil
}

// TestGraphBootstrapperSkipsV2Onion ensures SampleNodeAddrs strips Tor v2
// .onion entries from the returned candidate set so the connection manager
// never attempts to dial an obsolete v2 hidden service surfaced through the
// channel graph. A node whose remaining addresses include a v3 .onion or
// plain TCP entry is still returned for those addresses, and a node whose
// only address is v2 contributes nothing.
func TestGraphBootstrapperSkipsV2Onion(t *testing.T) {
t.Parallel()

v2 := &tor.OnionAddr{
OnionService: "3g2upl4pq6kufc4m.onion",
Port: 9735,
}
v3 := &tor.OnionAddr{
OnionService: "4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnz" +
"jpbi7utijcltosqemad.onion",
Port: 9735,
}
tcp := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9735}

mkPub := func(t *testing.T) *btcec.PublicKey {
t.Helper()
priv, err := btcec.NewPrivateKey()
require.NoError(t, err)

return priv.PubKey()
}

mixedPub := mkPub(t)
v2OnlyPub := mkPub(t)

graph := &stubChannelGraph{
nodes: []autopilot.Node{
&stubNode{
pub: mixedPub,
addrs: []net.Addr{v2, v3, tcp},
},
&stubNode{
pub: v2OnlyPub,
addrs: []net.Addr{v2},
},
},
}

// Use deterministic sampling so the hash accumulator never skips a
// node — every eligible address must surface on each call.
bs, err := NewGraphBootstrapper(graph, true)
require.NoError(t, err)

// Ask for more addresses than the graph holds so the bootstrapper
// drains it in a single pass.
got, err := bs.SampleNodeAddrs(
t.Context(), 10, map[autopilot.NodeID]struct{}{},
)
require.NoError(t, err)

// Collect (pubkey, address) pairs we expect — the v2-only node must
// not contribute any NetAddress, and the mixed node contributes only
// v3 and tcp.
type seenAddr struct {
pub [33]byte
addr string
}
seen := make(map[seenAddr]struct{}, len(got))
for _, na := range got {
var p [33]byte
copy(p[:], na.IdentityKey.SerializeCompressed())
seen[seenAddr{pub: p, addr: na.Address.String()}] = struct{}{}
}

var mixedKey, v2OnlyKey [33]byte
copy(mixedKey[:], mixedPub.SerializeCompressed())
copy(v2OnlyKey[:], v2OnlyPub.SerializeCompressed())

// Mixed node yields v3 and tcp entries.
require.Contains(t, seen, seenAddr{
pub: mixedKey, addr: v3.String(),
})
require.Contains(t, seen, seenAddr{
pub: mixedKey, addr: tcp.String(),
})

// No v2 entry surfaces, for either node.
for s := range seen {
require.NotEqual(t, v2.String(), s.addr,
"v2 onion %v leaked into candidate set", s.addr)
require.NotEqual(t, v2OnlyKey, s.pub,
"v2-only node %x should contribute nothing", s.pub)
}
}
39 changes: 16 additions & 23 deletions docs/configuring_tor.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ by using Tor for anonymous networking to establish connections.

With widespread usage of Onion Services within the network, concerns about the
difficulty of proper NAT traversal are alleviated, as usage of onion services
allows nodes to accept inbound connections even if they're behind a NAT. At the
time of writing this documentation, `lnd` supports both types of onion services:
v2 and v3.
allows nodes to accept inbound connections even if they're behind a NAT. `lnd`
supports v3 onion services only; legacy v2 onion service support has been
removed.

Before following the remainder of this documentation, you should ensure that you
already have Tor installed locally. **If you want to run v3 Onion Services, make
sure that you run at least version 0.3.3.6.**
Before following the remainder of this documentation, you should ensure that
you already have Tor installed locally. **Make sure that you run at least
version 0.3.3.6 of Tor in order to use v3 Onion Services.**
Official instructions to install the latest release of Tor can be found
[here](https://www.torproject.org/docs/tor-doc-unix.html.en).

Expand Down Expand Up @@ -81,7 +81,6 @@ Tor:
--tor.control= The host:port that Tor is listening on for Tor control connections (default: localhost:9051)
--tor.targetipaddress= IP address that Tor should use as the target of the hidden service
--tor.password= The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one.
--tor.v2 Automatically set up a v2 onion service to listen for inbound connections
--tor.v3 Automatically set up a v3 onion service to listen for inbound connections
--tor.privatekeypath= The path to the private key of the onion service being created
```
Expand All @@ -103,11 +102,8 @@ service. A path to save the onion service's private key can be specified with
the `--tor.privatekeypath` flag.

Most of these arguments have defaults, so as long as they apply to you, routing
all outbound and inbound connections through Tor can simply be done with either
v2 or v3 onion services:
```shell
$ ./lnd --tor.active --tor.v2
```
all outbound and inbound connections through Tor can simply be done with v3
onion services:
```shell
$ ./lnd --tor.active --tor.v3
```
Expand Down Expand Up @@ -159,15 +155,13 @@ authentication methods (arguably, from most to least secure):
## Listening for Inbound Connections

In order to listen for inbound connections through Tor, an onion service must be
created. There are two types of onion services: v2 and v3. v3 onion services
are the latest generation of onion services, and they provide a number of
advantages over the legacy v2 onion services. To learn more about these
benefits, see [Intro to Next Gen Onion Services](https://trac.torproject.org/projects/tor/wiki/doc/NextGenOnions).
created. `lnd` supports v3 onion services, the latest generation of onion
services. To learn more about these, see
[Intro to Next Gen Onion Services](https://trac.torproject.org/projects/tor/wiki/doc/NextGenOnions).

Both types can be created and used automatically by `lnd`. Specifying which type
should be used can easily be done by either using the `tor.v2` or `tor.v3` flag.
To prevent unintentional leaking of identifying information, it is also necessary
to add the flag `listen=localhost`.
v3 onion services are created and used automatically by `lnd` via the `tor.v3`
flag. To prevent unintentional leaking of identifying information, it is also
necessary to add the flag `listen=localhost`.

For example, v3 onion services can be used with the following flags:
```shell
Expand All @@ -176,9 +170,8 @@ $ ./lnd --tor.active --tor.v3 --listen=localhost

This will automatically create a hidden service for your node to use to listen
for inbound connections and advertise itself to the network. The onion service's
private key is saved to a file named `v2_onion_private_key` or
`v3_onion_private_key` depending on the type of onion service used in `lnd`'s
base directory. This will allow `lnd` to recreate the same hidden service upon
private key is saved to a file named `v3_onion_private_key` in `lnd`'s base
directory. This will allow `lnd` to recreate the same hidden service upon
restart. If you wish to generate a new onion service, you can simply delete this
file. The path to this private key file can also be modified with the
`--tor.privatekeypath` argument.
Expand Down
24 changes: 24 additions & 0 deletions docs/release-notes/release-notes-0.21.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,30 @@
14 and 8 respectively, now reserved). Callers must use the multi-channel
`outgoing_chan_ids` field introduced in 0.20.

* [Removed the deprecated `--tor.v2` configuration
flag](https://github.com/lightningnetwork/lnd/pull/10813). Tor stopped
serving v2 onion services in October 2021, and lnd no longer produces
v2 on any code path; `tor.OnionHostToFakeIP` is also gone. Operator
input is rejected at the boundary: `--externalip`, `--listen`,
Comment thread
erickcestari marked this conversation as resolved.
`lncli connect`, and `lncli wtclient towers add` fail fast on a v2
`.onion` string, so operators upgrading with a v2 entry in
`lnd.conf` must remove it before lnd will start. Persisted state
carried over from a previous version is also filtered before use:
the self-node announcement strips any v2 entry from the source-node
record before signing; the watchtower client drops v2 entries from
each persisted tower's address list (skipping the tower entirely if
no non-v2 address remains so the operator can attach a fresh v3
address); the autopilot, graph bootstrapper, and static-channel
backup restore paths skip v2 entries before attempting outbound
dials. The on-disk records are left intact.

Peer-signed announcements that still carry v2 are handled
byte-for-byte: the `lnwire` and `graph/db` codecs round-trip v2 so
`DataToSign` reproduces the signed bytes, signatures validate, and
the announcement is persisted and re-broadcast unchanged. RPCs like
`GetNodeInfo` and `DescribeGraph` still expose the full address
set.

## Performance Improvements

* Let the [channel graph cache be populated
Expand Down
3 changes: 2 additions & 1 deletion graph/db/addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ func encodeTCPAddr(w io.Writer, addr *net.TCPAddr) error {
}

// encodeOnionAddr serializes an onion address into its compact raw bytes
// representation.
// representation. v2 round-trips for wire fidelity even though lnd no longer
// produces it.
func encodeOnionAddr(w io.Writer, addr *tor.OnionAddr) error {
var suffixIndex int
hostLen := len(addr.OnionService)
Expand Down
Loading
Loading