mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-16 13:00:19 +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.
282 lines
7.7 KiB
Go
282 lines
7.7 KiB
Go
package tor
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/connmgr"
|
|
"github.com/miekg/dns"
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// dnsCodes maps the DNS response codes to a friendly description. This does
|
|
// not include the BADVERS code because of duplicate keys and the underlying
|
|
// DNS (miekg/dns) package not using it. For more info, see
|
|
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml.
|
|
var dnsCodes = map[int]string{
|
|
0: "no error",
|
|
1: "format error",
|
|
2: "server failure",
|
|
3: "non-existent domain",
|
|
4: "not implemented",
|
|
5: "query refused",
|
|
6: "name exists when it should not",
|
|
7: "RR set exists when it should not",
|
|
8: "RR set that should exist does not",
|
|
9: "server not authoritative for zone",
|
|
10: "name not contained in zone",
|
|
16: "TSIG signature failure",
|
|
17: "key not recognized",
|
|
18: "signature out of time window",
|
|
19: "bad TKEY mode",
|
|
20: "duplicate key name",
|
|
21: "algorithm not supported",
|
|
22: "bad truncation",
|
|
23: "bad/missing server cookie",
|
|
}
|
|
|
|
// proxyConn is a wrapper around net.Conn that allows us to expose the actual
|
|
// remote address we're dialing, rather than the proxy's address.
|
|
type proxyConn struct {
|
|
net.Conn
|
|
remoteAddr net.Addr
|
|
}
|
|
|
|
func (c *proxyConn) RemoteAddr() net.Addr {
|
|
return c.remoteAddr
|
|
}
|
|
|
|
// Dial is a wrapper over the non-exported dial function that returns a wrapper
|
|
// around net.Conn in order to expose the actual remote address we're dialing,
|
|
// rather than the proxy's address.
|
|
func Dial(address, socksAddr string, streamIsolation bool,
|
|
skipProxyForClearNetTargets bool,
|
|
timeout time.Duration) (net.Conn, error) {
|
|
|
|
conn, err := dialProxy(
|
|
address, socksAddr, streamIsolation,
|
|
skipProxyForClearNetTargets, timeout,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial proxy failed: %w", err)
|
|
}
|
|
|
|
// Now that the connection is established, we'll create our internal
|
|
// proxyConn that will serve in populating the correct remote address
|
|
// of the connection, rather than using the proxy's address.
|
|
remoteAddr, err := ParseAddr(address, socksAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &proxyConn{
|
|
Conn: conn,
|
|
remoteAddr: remoteAddr,
|
|
}, nil
|
|
}
|
|
|
|
// dialProxy establishes a connection to the address via the provided TOR SOCKS
|
|
// proxy. Only TCP traffic may be routed via Tor.
|
|
//
|
|
// streamIsolation determines if we should force stream isolation for this new
|
|
// connection. If enabled, new connections will use a fresh circuit, rather than
|
|
// possibly re-using an existing circuit.
|
|
//
|
|
// skipProxyForClearNetTargets argument allows the dialer to directly connect
|
|
// to the provided address if it does not represent an union service, skipping
|
|
// the SOCKS proxy.
|
|
func dialProxy(address, socksAddr string, streamIsolation bool,
|
|
skipProxyForClearNetTargets bool,
|
|
timeout time.Duration) (net.Conn, error) {
|
|
|
|
// If we were requested to force stream isolation for this connection,
|
|
// we'll populate the authentication credentials with random data as
|
|
// Tor will create a new circuit for each set of credentials.
|
|
var auth *proxy.Auth
|
|
if streamIsolation {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
auth = &proxy.Auth{
|
|
User: hex.EncodeToString(b[:8]),
|
|
Password: hex.EncodeToString(b[8:]),
|
|
}
|
|
}
|
|
|
|
clearDialer := &net.Dialer{Timeout: timeout}
|
|
if skipProxyForClearNetTargets {
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The SOCKS proxy is skipped if the target
|
|
// is not an union address.
|
|
if !IsOnionHost(host) {
|
|
return clearDialer.Dial("tcp", address)
|
|
}
|
|
}
|
|
|
|
// Establish the connection through Tor's SOCKS proxy.
|
|
dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, clearDialer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("establish sock proxy: %w", err)
|
|
}
|
|
|
|
return dialer.Dial("tcp", address)
|
|
}
|
|
|
|
// LookupHost performs DNS resolution on a given host via Tor's native resolver.
|
|
// Only IPv4 addresses are returned.
|
|
func LookupHost(host, socksAddr string) ([]string, error) {
|
|
ip, err := connmgr.TorLookupIP(host, socksAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only one IPv4 address is returned by the TorLookupIP function.
|
|
return []string{ip[0].String()}, nil
|
|
}
|
|
|
|
// LookupSRV uses Tor's SOCKS proxy to route DNS SRV queries. Tor does not
|
|
// natively support SRV queries so we must route all SRV queries through the
|
|
// proxy by connecting directly to a DNS server and querying it. The DNS server
|
|
// must have TCP resolution enabled for the given port.
|
|
func LookupSRV(service, proto, name, socksAddr,
|
|
dnsServer string, streamIsolation bool, skipProxyForClearNetTargets bool,
|
|
timeout time.Duration) (string, []*net.SRV, error) {
|
|
|
|
// Connect to the DNS server we'll be using to query SRV records.
|
|
conn, err := dialProxy(
|
|
dnsServer, socksAddr, streamIsolation,
|
|
skipProxyForClearNetTargets, timeout,
|
|
)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
dnsConn := &dns.Conn{Conn: conn}
|
|
defer dnsConn.Close()
|
|
|
|
// Once connected, we'll construct the SRV request for the host
|
|
// following the format _service._proto.name. as described in RFC #2782.
|
|
host := fmt.Sprintf("_%s._%s.%s.", service, proto, name)
|
|
msg := new(dns.Msg).SetQuestion(host, dns.TypeSRV)
|
|
|
|
// Send the request to the DNS server and read its response.
|
|
if err := dnsConn.WriteMsg(msg); err != nil {
|
|
return "", nil, err
|
|
}
|
|
resp, err := dnsConn.ReadMsg()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// We'll fail if we were unable to query the DNS server for our record.
|
|
if resp.Rcode != dns.RcodeSuccess {
|
|
return "", nil, fmt.Errorf("unable to query for SRV records: "+
|
|
"%s", dnsCodes[resp.Rcode])
|
|
}
|
|
|
|
// Retrieve the RR(s) of the Answer section.
|
|
var rrs []*net.SRV
|
|
for _, rr := range resp.Answer {
|
|
srv := rr.(*dns.SRV)
|
|
rrs = append(rrs, &net.SRV{
|
|
Target: srv.Target,
|
|
Port: srv.Port,
|
|
Priority: srv.Priority,
|
|
Weight: srv.Weight,
|
|
})
|
|
}
|
|
|
|
return "", rrs, nil
|
|
}
|
|
|
|
// ResolveTCPAddr uses Tor's proxy to resolve TCP addresses instead of the
|
|
// standard system resolver provided in the `net` package.
|
|
func ResolveTCPAddr(address, socksAddr string) (*net.TCPAddr, error) {
|
|
// Split host:port since the lookup function does not take a port.
|
|
host, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do we already have an IP? Then we don't need to look up anything.
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return &net.TCPAddr{
|
|
IP: ip,
|
|
Port: p,
|
|
}, nil
|
|
}
|
|
|
|
ip, err := LookupHost(host, socksAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &net.TCPAddr{
|
|
IP: net.ParseIP(ip[0]),
|
|
Port: p,
|
|
}, nil
|
|
}
|
|
|
|
// ParseAddr parses an address from its string format to a net.Addr.
|
|
func ParseAddr(address, socksAddr string) (net.Addr, error) {
|
|
host, portStr, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if IsOnionHost(host) {
|
|
return &OnionAddr{OnionService: host, Port: port}, nil
|
|
}
|
|
|
|
return ResolveTCPAddr(address, socksAddr)
|
|
}
|
|
|
|
// IsOnionHost determines whether a host is part of an onion address.
|
|
func IsOnionHost(host string) bool {
|
|
// Note the starting index of the onion suffix in the host depending
|
|
// on its length.
|
|
var suffixIndex int
|
|
switch len(host) {
|
|
case V2Len:
|
|
suffixIndex = V2Len - OnionSuffixLen
|
|
case V3Len:
|
|
suffixIndex = V3Len - OnionSuffixLen
|
|
default:
|
|
return false
|
|
}
|
|
|
|
// Make sure the host ends with the ".onion" suffix.
|
|
if host[suffixIndex:] != OnionSuffix {
|
|
return false
|
|
}
|
|
|
|
// We'll now attempt to decode the host without its suffix, as the
|
|
// suffix includes invalid characters. This will tell us if the host is
|
|
// actually valid if successful.
|
|
host = host[:suffixIndex]
|
|
if _, err := Base32Encoding.DecodeString(host); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|