mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-17 13:06:14 +02:00
Tor stopped serving v2 onion services in October 2021; lnd should not
produce v2 addresses anymore, but it must still verify signatures on
and re-broadcast peer NodeAnnouncement messages that carry v2 entries.
Stop accepting v2 as configuration input (lncfg), strip the legacy
`--tor.v2` flag from the sample config, and remove the
`tor.OnionHostToFakeIP` helper. Operator entry points (`--externalip`,
`--listen`, `lncli connect`, `lncli wtclient towers add`) fail fast on
a v2 `.onion` string, so upgrading nodes must remove any v2 entry from
`lnd.conf` before lnd will start.
Filter persisted v2 state before use without rewriting on-disk records:
the self-announcement builder strips any v2 entry inherited from the
stored self-node; the watchtower client drops v2 entries from each
persisted tower's address list (skipping the tower entirely if no
non-v2 address remains); the autopilot connector, graph bootstrapper,
and static-channel backup restore paths skip v2 entries before
attempting outbound dials. Restrict the Tor controller's ADD_ONION
path to v3 keys, including the encrypted on-disk legacy-key fallback.
For inbound announcements, keep the wire codec wire-faithful:
`lnwire.WriteOnionAddr`, `graph/db.encodeOnionAddr`, and the matching
decoders round-trip v2 bytes so `DataToSign` reproduces the bytes the
remote peer signed, signature validation succeeds, and the announcement
is persisted to the graph DB and re-broadcast across restarts byte-for-
byte. RPC surfaces continue to expose the full address set so external
tools can independently reproduce and verify the signed bytes.
Add a netann regression test that signs a [v3, v2, ipv4] announcement,
round-trips it through Encode/Decode, verifies the signature, and
confirms the resulting models.Node preserves the v2 entry. Add a
graph bootstrapper test asserting v2 entries are skipped while v3 and
plain TCP entries on the same node still surface as bootstrap
candidates.
(cherry picked from commit 2ae1db83b3)
240 lines
6.9 KiB
Go
240 lines
6.9 KiB
Go
package lnd
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/tor"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestNodeAnnouncementTimestampComparison tests the timestamp comparison
|
|
// logic used in setSelfNode to ensure node announcements have strictly
|
|
// increasing timestamps at second precision (as required by BOLT-07 and
|
|
// enforced by the database storage).
|
|
func TestNodeAnnouncementTimestampComparison(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Use a simple base time for the tests.
|
|
baseTime := int64(1000)
|
|
|
|
tests := []struct {
|
|
name string
|
|
srcNodeLastUpdate time.Time
|
|
nodeLastUpdate time.Time
|
|
expectedResult time.Time
|
|
description string
|
|
}{
|
|
{
|
|
name: "same second different nanoseconds",
|
|
srcNodeLastUpdate: time.Unix(baseTime, 0),
|
|
nodeLastUpdate: time.Unix(baseTime, 500_000_000),
|
|
expectedResult: time.Unix(baseTime+1, 0),
|
|
description: "Edge case: timestamps in same second " +
|
|
"but different nanoseconds. Must increment " +
|
|
"to avoid persisting same second-level " +
|
|
"timestamp.",
|
|
},
|
|
{
|
|
name: "different seconds",
|
|
srcNodeLastUpdate: time.Unix(baseTime, 0),
|
|
nodeLastUpdate: time.Unix(baseTime+2, 0),
|
|
expectedResult: time.Unix(baseTime+2, 0),
|
|
description: "Normal case: current time is already " +
|
|
"in a different (later) second. No increment " +
|
|
"needed.",
|
|
},
|
|
{
|
|
name: "exactly equal",
|
|
srcNodeLastUpdate: time.Unix(baseTime, 123456789),
|
|
nodeLastUpdate: time.Unix(baseTime, 123456789),
|
|
expectedResult: time.Unix(baseTime+1, 123456789),
|
|
description: "Timestamps are identical. Must " +
|
|
"increment to ensure strictly greater " +
|
|
"timestamp.",
|
|
},
|
|
{
|
|
name: "exactly equal - zero nanoseconds",
|
|
srcNodeLastUpdate: time.Unix(baseTime, 0),
|
|
nodeLastUpdate: time.Unix(baseTime, 0),
|
|
expectedResult: time.Unix(baseTime+1, 0),
|
|
description: "Timestamps are identical at second " +
|
|
"precision (0 nanoseconds), as would be read " +
|
|
"from DB. Must increment.",
|
|
},
|
|
{
|
|
name: "clock skew - persisted is newer",
|
|
srcNodeLastUpdate: time.Unix(baseTime+5, 0),
|
|
nodeLastUpdate: time.Unix(baseTime+3, 0),
|
|
expectedResult: time.Unix(baseTime+6, 0),
|
|
description: "Clock went backwards: persisted " +
|
|
"timestamp is newer than current time. Must " +
|
|
"increment from persisted timestamp.",
|
|
},
|
|
{
|
|
name: "clock skew - same second",
|
|
srcNodeLastUpdate: time.Unix(baseTime+5, 100_000_000),
|
|
nodeLastUpdate: time.Unix(baseTime+5, 900_000_000),
|
|
expectedResult: time.Unix(baseTime+6, 100_000_000),
|
|
description: "Clock skew within same second. Must " +
|
|
"increment to ensure strictly greater " +
|
|
"second-level timestamp.",
|
|
},
|
|
{
|
|
name: "same second component different " +
|
|
"minute",
|
|
srcNodeLastUpdate: time.Unix(baseTime, 0),
|
|
nodeLastUpdate: time.Unix(baseTime+60, 0),
|
|
expectedResult: time.Unix(baseTime+60, 0),
|
|
description: "Same seconds component (:00) but " +
|
|
"different minutes. Current time is later. " +
|
|
"Verifies we use .Unix() not .Second().",
|
|
},
|
|
{
|
|
name: "lower second component but " +
|
|
"later time",
|
|
srcNodeLastUpdate: time.Unix(baseTime+58, 0),
|
|
nodeLastUpdate: time.Unix(baseTime+63, 0),
|
|
expectedResult: time.Unix(baseTime+63, 0),
|
|
description: "Persisted has second=58, current has " +
|
|
"second=3 (next minute). Current is later " +
|
|
"overall. Verifies .Unix() not .Second().",
|
|
},
|
|
{
|
|
name: "higher second component but " +
|
|
"earlier time",
|
|
srcNodeLastUpdate: time.Unix(baseTime+63, 0),
|
|
nodeLastUpdate: time.Unix(baseTime+58, 0),
|
|
expectedResult: time.Unix(baseTime+64, 0),
|
|
description: "Persisted has second=3 (next minute), " +
|
|
"current has second=58. Persisted is later " +
|
|
"overall. Verifies .Unix() not .Second().",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := calculateNodeAnnouncementTimestamp(
|
|
tc.srcNodeLastUpdate,
|
|
tc.nodeLastUpdate,
|
|
)
|
|
|
|
// Verify we got the expected result.
|
|
require.Equal(
|
|
t, tc.expectedResult, result,
|
|
"Unexpected result: %s", tc.description,
|
|
)
|
|
|
|
// Verify result is strictly greater than persisted
|
|
// timestamp. This is an additional check to ensure
|
|
// the result is strictly greater than the persisted
|
|
// timestamp.
|
|
require.Greater(
|
|
t, result.Unix(), tc.srcNodeLastUpdate.Unix(),
|
|
"Result must be strictly greater than "+
|
|
"persisted timestamp: %s",
|
|
tc.description,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseAddrRejectsTorV2 ensures that parseAddr rejects v2 .onion hosts at
|
|
// the operator-input boundary. This is the path used by lncli connect (via
|
|
// rpcserver.ConnectPeer) and the --addpeer config option, mirroring the
|
|
// equivalent gate in lncfg.ParseAddressString.
|
|
func TestParseAddrRejectsTorV2(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
v2Host = "3g2upl4pq6kufc4m.onion"
|
|
v3Host = "4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpb" +
|
|
"i7utijcltosqemad.onion"
|
|
)
|
|
|
|
netCfg := &tor.ClearNet{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
address string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "v2 without port is rejected",
|
|
address: v2Host,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "v2 with port is rejected",
|
|
address: v2Host + ":9735",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "v3 without port is accepted",
|
|
address: v3Host,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "v3 with port is accepted",
|
|
address: v3Host + ":9735",
|
|
expectErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
addr, err := parseAddr(tc.address, netCfg)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
require.Contains(
|
|
t, err.Error(), "tor v2 onion",
|
|
)
|
|
require.Nil(t, addr)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
onionAddr, ok := addr.(*tor.OnionAddr)
|
|
require.True(t, ok)
|
|
require.Equal(t, v3Host, onionAddr.OnionService)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithoutV2Onion ensures that Tor v2 onion addresses are dropped from
|
|
// reconnect/dial consumption paths (startup persistent reconnect, live
|
|
// topology updates, fetchNodeAdvertisedAddrs) while non-onion and v3 onion
|
|
// addresses pass through unchanged. Storage and gossip re-broadcast remain
|
|
// byte-faithful elsewhere.
|
|
func TestWithoutV2Onion(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,
|
|
}
|
|
|
|
require.True(t, isV2OnionAddr(v2))
|
|
require.False(t, isV2OnionAddr(v3))
|
|
require.False(t, isV2OnionAddr(tcp))
|
|
|
|
filtered := withoutV2Onion([]net.Addr{v2, v3, tcp, v2})
|
|
require.Equal(t, []net.Addr{v3, tcp}, filtered)
|
|
|
|
// An all-v2 input filters to an empty slice; callers such as
|
|
// fetchNodeAdvertisedAddrs treat this as "no advertised address".
|
|
require.Empty(t, withoutV2Onion([]net.Addr{v2, v2}))
|
|
}
|