Skip to content

Drop tor v2 onion production, keep wire codec faithful#10813

Open
erickcestari wants to merge 6 commits into
lightningnetwork:masterfrom
erickcestari:tor-v2-cleanup
Open

Drop tor v2 onion production, keep wire codec faithful#10813
erickcestari wants to merge 6 commits into
lightningnetwork:masterfrom
erickcestari:tor-v2-cleanup

Conversation

@erickcestari
Copy link
Copy Markdown
Collaborator

Context

This PR is split out from #10795 ("multi: remove deprecated RPCs and
config flags scheduled for 0.21"), which removes everything that was
announced for removal in 0.21 via the 0.20 release notes. That parent
PR was carved into smaller, independently-reviewable PRs by topic;
this is the tor v2 piece.

Tor v2 onion services have been obsolete since October 2021, when
the Tor network dropped support for them. The --tor.v2 flag was deprecated
in 0.20 with a scheduled removal in 0.21, and that's what this PR
ships.

lnd should not produce v2 addresses on any code path anymore, but it
must still verify signatures on and re-broadcast peer
NodeAnnouncement messages that carry v2 entries alongside still-
valid addresses, so we cannot strip v2 from the wire codec.

This PR draws that line: stop producing v2 everywhere, keep the codec
byte-faithful.

What changes

Stop producing v2

  • lncfg.ParseAddressString rejects v2 .onion strings at the
    operator-input boundary, so --externalip, --listen,
    lncli connect <pubkey>@<v2>.onion, and
    lncli wtclient towers add <pubkey>@<v2>.onion fail fast with a
    clear error.
  • The --tor.v2 flag and its sample-config entry are gone (the
    deprecation announced in 0.20).
  • The self-announcement builder filters any v2 entry inherited from a
    previously stored self-node, so upgraded lnd instances never re-emit
    their own v2 onions.
  • tor.OnionHostToFakeIP (the OnionCat v2 → fake-IPv6 helper) and the
    matching decoder cluster (FakeIPToOnionHost, IsOnionFakeIP,
    onionPrefixBytes) are removed. The scheme is v2-only by
    construction — v3 decodes to 35 bytes and doesn't fit in an IPv6
    address.
  • On-disk legacy v2 keys (RSA1024:…) are rejected with the new
    ErrNonV3OnionKey rather than silently regenerating a fresh v3
    service, which would change the advertised onion identity.
  • tor.OnionType enum, AddOnionConfig.Type, and
    watchtower.Config.Type are removed — only V3 was left, the field
    was a no-op pass-through.

Keep the wire codec faithful

lnwire.WriteOnionAddr, graph/db.encodeOnionAddr, and the matching
decoders round-trip v2 bytes so:

  • DataToSign reproduces the exact bytes the remote peer signed,
  • signature validation succeeds,
  • the announcement is persisted to the graph DB and re-broadcast
    across restarts byte-for-byte.

RPC surfaces (GetNodeInfo, DescribeGraph) continue to expose the
full address set so external tools can independently reproduce and
verify the signed bytes.

Breaking changes / operator-facing notes

This is a breaking change for any node still configured with v2. The
--tor.v2 flag has been deprecated since 0.20 and v3 has been the
only working option network-side since October 2021, so there should
be no production node still relying on it, but the migration steps
are:

  • A v2 .onion entry in lnd.conf (under externalip /
    tor.privatekeypath / wtclient.tower etc.) will now cause lnd to
    refuse to start. Remove the entry before upgrading.
  • A pre-existing v2 private key file on disk (v2_onion_private_key,
    or a renamed key whose contents begin with RSA1024:) will cause
    the hidden-service setup to error out at startup with
    ErrNonV3OnionKey. Delete the file to let lnd generate a fresh v3
    service.
  • Peer/tower addresses already persisted from a previous version
    still decode correctly through the wire-faithful codec, only new
    operator input is refused.
  • GetNodeInfo / DescribeGraph continue to expose v2 entries when a
    remote peer's announcement contains them, so downstream tools that
    independently verify announcement signatures keep working.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request removes support for Tor v2 onion services, which have been deprecated since lnd 0.20 and are no longer supported by the Tor network. While the node will no longer produce or accept new v2 addresses, it maintains backward compatibility in the wire codec to correctly handle and propagate existing peer-signed announcements that may contain v2 entries. This ensures that network graph integrity and signature validation remain intact for legacy data.

Highlights

  • Removal of Tor v2 support: The --tor.v2 configuration flag and associated logic have been removed, as Tor v2 onion services have been obsolete since October 2021.
  • Input validation: New operator input (e.g., --externalip, lncli connect) containing v2 .onion addresses is now rejected at the boundary with a clear error message.
  • Wire codec fidelity: The wire codec and database persistence layers continue to support v2 addresses to ensure peer-signed announcements remain valid and byte-for-byte compatible.
  • Legacy key handling: On-disk legacy v2 private keys are now explicitly rejected with ErrNonV3OnionKey to prevent silent migration issues and ensure operators transition to v3.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@erickcestari erickcestari self-assigned this May 18, 2026
@erickcestari erickcestari added the backport-v0.21.x-branch This label triggers a backport to branch `v0.21.x-branch ` label May 18, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request removes support for legacy Tor v2 onion services while maintaining wire-level compatibility for existing network announcements. Key changes include the removal of the --tor.v2 flag, the deletion of v2-specific 'fake IP' encoding, and the addition of validation to reject v2 addresses at the input boundary. Feedback was provided to update test keys in tor/cmd_onion_test.go to use the colon separator required by the new production validation logic.

Comment thread tor/cmd_onion_test.go Outdated
@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label May 18, 2026
@github-actions
Copy link
Copy Markdown

PR Severity: CRITICAL. Files: lnwire/writer.go (CRITICAL - lnwire wire protocol), server.go (CRITICAL - core server). Also touches graph/db/* and watchtower/* (HIGH), lncfg/, tor/, config.go, lnd.go (MEDIUM), docs and config files (LOW). 17 non-test files, ~365 non-test lines changed - no severity bump triggered. Label: severity-critical. <!-- pr-severity-bot -->

Copy link
Copy Markdown
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Close - guess the deprecation of the addr part has its own complexities.

Comment thread docs/release-notes/release-notes-0.21.0.md
Comment thread graph/db/sql_store.go
Comment thread graph/db/sql_store.go
Comment thread docs/release-notes/release-notes-0.21.0.md
@erickcestari erickcestari requested a review from yyforyongyu May 19, 2026 13:16
@saubyk saubyk added this to the v0.21.0 milestone May 19, 2026
@saubyk saubyk added this to v0.21 May 19, 2026
@saubyk saubyk moved this to In review in v0.21 May 19, 2026
@erickcestari erickcestari force-pushed the tor-v2-cleanup branch 2 times, most recently from 58876f7 to a09efe2 Compare May 19, 2026 17:00
Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 left a comment

Choose a reason for hiding this comment

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

LGTM

The v2 removal boundary looks right, but the remaining v2 codec support should be documented more explicitly. Even though lnd no longer creates/dials v2, lnwire/graph decoding must
keep it for historical peer-signed node announcements and persisted graph data. Node signatures commit to the serialized address list, so dropping v2 there would change DataToSign()
and break validation after reconstruction/restart. Could we add a short comment near the remaining v2OnionAddr decode/preserve paths so this does not look like dead code later?

Comment thread graph/db/models/node.go Outdated

// NodeFromWireAnnouncement creates a Node instance from an
// lnwire.NodeAnnouncement1 message.
// lnwire.NodeAnnouncement1 message. All wire addresses are preserved so
Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 May 19, 2026

Choose a reason for hiding this comment

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

Nit: Maybe mention the exact use case, I honestly got a bit confused when reading the second part of the comment, it seems that wire addresses are somehow treated in a special way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've updated it. Do you think is more clear now?

Comment thread tor/cmd_onion.go Outdated
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. The self-announcement builder now
filters any v2 entry inherited from a previously stored self-node so
upgraded lnd instances never re-emit their own v2 onions.

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.
Describe the new wire-faithful storage behavior introduced by the
previous commit so operators know what to expect from peer
announcements that still carry v2 entries.
Copy link
Copy Markdown
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

One last thing!

Comment thread server.go
Comment thread server_test.go
Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 left a comment

Choose a reason for hiding this comment

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

Took another closer look from another angle on the problem, found still some issues to clean up:

Another persisted-address path worth checking is the watchtower client DB.

New tower input is already covered because AddTower parses the address through lncfg.ParseAddressString, so a fresh v2 tower address is rejected. The remaining case is upgrade
state: a tower added before this PR could already be stored in the wtclient DB with a v2 onion address.

That stored address does not go through config/input parsing again. It is decoded as part of wtdb.Tower.Addresses, converted into a wtclient.Tower via NewTowerFromDBTower, then
later handed to the session negotiator/session queue dial paths. So an upgraded node can still attempt to dial an old persisted v2 tower address.

I think the best boundary is NewTowerFromDBTower: filter v2 onion addresses before constructing the address iterator. If filtering leaves no usable addresses, the tower should not
become an active dial candidate; we should leave the DB data intact and require the operator to add a fresh v3 address for that tower pubkey.

Continuing the v2 audit, the remaining risky paths are mostly dial boundaries rather than wire/storage paths.

Several subsystems can pass persisted addresses into outbound connection attempts:

  • autopilot gets graph node addresses directly from autopilot.ChannelGraphFromDatabase, then pilot.go attempts to connect to *tor.OnionAddr without distinguishing v2/v3.
  • SCB restore uses addresses from the static channel backup / restored link node and calls ConnectToPeer for each address.
  • graph bootstrap uses graph-sourced node addresses and calls connectToPeer directly.
  • persistent reconnection mostly already filters v2 through withoutV2Onion, but it is still worth keeping in mind as another persisted-address consumer.

This is different from remote node-announcement storage: graph/lnwire should still preserve v2 so old signed announcements round-trip. But when the address leaves storage and becomes a
dial target, v2 should be skipped/rejected.

Potential areas to check:

  1. Autopilot

autopilot/graph.go exposes graph addresses directly:

addrs: n.Addresses

pilot.go then accepts any *tor.OnionAddr and tries to connect. If a remote graph node only has an old v2 onion, autopilot may still select and attempt to dial it. The graph data should
remain unchanged, but the autopilot connection path should skip v2 onion addresses.

2. SCB restore

chanrestore.go loops over backup/restored addresses and calls ConnectToPeer. If an old static channel backup contains a v2 onion, restore can still try it. Since v2 is no longer
routable on the Tor network, this should probably be skipped so restore proceeds to any remaining usable addresses. If all addresses are v2, restore cannot auto-connect and the operator
needs a fresh reachable address.

3. Graph bootstrap

discovery/bootstrapper.go samples graph node addresses and currently accepts any *tor.OnionAddr. This means graph-sourced v2 onion addresses can still be returned as bootstrap
candidates. The bootstrapper should skip v2 onion addresses while leaving graph storage unchanged.

4. Persistent peer reconnect

This path appears mostly handled already:

- startup link-node addresses use withoutV2Onion
- graph node addresses are skipped with isV2OnionAddr
- live topology updates skip v2
- fetchNodeAdvertisedAddrs filters v2

So I dont see an obvious missing filter here, but it is still part of the same category: persisted addresses should be filtered before they become dial candidates.

Together with the wtclient-specific fix, this keeps the policy consistent:

- preserve v2 in wire/graph/db for historical signature fidelity;
- do not advertise v2 in our own node announcements;
- do not use v2 as a new dial target.

@erickcestari
Copy link
Copy Markdown
Collaborator Author

erickcestari commented May 20, 2026

Thanks for the review @yyforyongyu and @ziggie1984! I've pushed 3 new commits with the fixes. I'll squash them when the review is finished.

69946e2 - setSelfNode (server.go) strips v2 from srcNode.Addresses before signing, so the self-announcement never re-broadcasts a persisted v2 entry.

69946e2 - wtclient NewTowerFromDBTower filters v2 from persisted tower addresses; returns ErrTowerOnlyV2Onion if none remain so startup skips the tower (DB row untouched, operator must add a v3).

fdbe3bd - Filters v2 at the three remaining dial boundaries ziggie called out: autopilot (pilot.go), SCB restore (chanrestore.go), and graph bootstrap (discovery/bootstrapper.go). Adds TestGraphBootstrapperSkipsV2Onion. Persistent reconnect already filtered, no change needed.

Copy link
Copy Markdown
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Linter failed

Comment thread discovery/bootstrapper.go
Comment thread tor/cmd_onion.go
Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 left a comment

Choose a reason for hiding this comment

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

LGTM

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

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

Comment thread pilot.go
// Strip persisted Tor v2 .onion entries: Tor stopped
// serving them in 2021 and the dial would never
// succeed. Covered by TestWithoutV2Onion.
addrs = withoutV2Onion(addrs)
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 we should to do this before the len() = 0 check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-v0.21.x-branch This label triggers a backport to branch `v0.21.x-branch ` severity-critical Requires expert review - security/consensus critical

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

4 participants