diff --git a/lnd.go b/lnd.go index 11997d14..0643ba6f 100644 --- a/lnd.go +++ b/lnd.go @@ -1508,12 +1508,43 @@ func initNeutrinoBackend(cfg *Config, chainDir string) (*neutrino.ChainService, AddPeers: cfg.NeutrinoMode.AddPeers, ConnectPeers: cfg.NeutrinoMode.ConnectPeers, Dialer: func(addr net.Addr) (net.Conn, error) { + dialAddr := addr + if tor.IsOnionFakeIP(addr) { + // Because the Neutrino address manager only + // knows IP addresses, we need to turn any fake + // tcp6 address that actually encodes an Onion + // v2 address back into the hostname + // representation before we can pass it to the + // dialer. + var err error + dialAddr, err = tor.FakeIPToOnionHost(addr) + if err != nil { + return nil, err + } + } + return cfg.net.Dial( - addr.Network(), addr.String(), + dialAddr.Network(), dialAddr.String(), cfg.ConnectionTimeout, ) }, NameResolver: func(host string) ([]net.IP, error) { + if tor.IsOnionHost(host) { + // Neutrino internally uses btcd's address + // manager which only operates on an IP level + // and does not understand onion hosts. We need + // to turn an onion host into a fake + // representation of an IP address to make it + // possible to connect to a block filter backend + // that serves on an Onion v2 hidden service. + fakeIP, err := tor.OnionHostToFakeIP(host) + if err != nil { + return nil, err + } + + return []net.IP{fakeIP}, nil + } + addrs, err := cfg.net.LookupHost(host) if err != nil { return nil, err diff --git a/tor/tor.go b/tor/tor.go index e5430f57..26e36a7e 100644 --- a/tor/tor.go +++ b/tor/tor.go @@ -1,6 +1,7 @@ package tor import ( + "bytes" "crypto/rand" "encoding/hex" "fmt" @@ -39,6 +40,15 @@ var ( 22: "bad truncation", 23: "bad/missing server cookie", } + + // onionPrefixBytes is a special purpose IPv6 prefix to encode Onion v2 + // addresses with. Because Neutrino uses the address manager of btcd + // which only understands net.IP addresses instead of net.Addr, we need + // to convert any .onion addresses into fake IPv6 addresses if we want + // to use a Tor hidden service as a Neutrino backend. This is the same + // range used by OnionCat, which is part part of the RFC4193 unique + // local IPv6 unicast address range. + onionPrefixBytes = []byte{0xfd, 0x87, 0xd8, 0x7e, 0xeb, 0x43} ) // proxyConn is a wrapper around net.Conn that allows us to expose the actual @@ -248,3 +258,60 @@ func IsOnionHost(host string) bool { return true } + +// IsOnionFakeIP checks whether a given net.Addr is a fake IPv6 address that +// encodes an Onion v2 address. +func IsOnionFakeIP(addr net.Addr) bool { + _, err := FakeIPToOnionHost(addr) + return err == nil +} + +// OnionHostToFakeIP encodes an Onion v2 address into a fake IPv6 address that +// encodes the same information but can be used for libraries that operate on an +// IP address base only, like btcd's address manager. For example, this will +// turn the onion host ld47qlr6h2b7hrrf.onion into the ip6 address +// fd87:d87e:eb43:58f9:f82e:3e3e:83f3:c625. +func OnionHostToFakeIP(host string) (net.IP, error) { + if len(host) != V2Len { + return nil, fmt.Errorf("invalid onion v2 host: %v", host) + } + + data, err := Base32Encoding.DecodeString(host[:V2Len-OnionSuffixLen]) + if err != nil { + return nil, err + } + + ip := make([]byte, len(onionPrefixBytes)+len(data)) + copy(ip, onionPrefixBytes) + copy(ip[len(onionPrefixBytes):], data) + return ip, nil +} + +// FakeIPToOnionHost turns a fake IPv6 address that encodes an Onion v2 address +// back into its onion host address representation. For example, this will turn +// the fake tcp6 address [fd87:d87e:eb43:58f9:f82e:3e3e:83f3:c625]:8333 back +// into ld47qlr6h2b7hrrf.onion:8333. +func FakeIPToOnionHost(fakeIP net.Addr) (net.Addr, error) { + tcpAddr, ok := fakeIP.(*net.TCPAddr) + if !ok { + return nil, fmt.Errorf("invalid fake onion IP address: %v", + fakeIP) + } + + ip := tcpAddr.IP + if len(ip) != len(onionPrefixBytes)+V2DecodedLen { + return nil, fmt.Errorf("invalid fake onion IP address length: "+ + "%v", fakeIP) + } + + if !bytes.Equal(ip[:len(onionPrefixBytes)], onionPrefixBytes) { + return nil, fmt.Errorf("invalid fake onion IP address prefix: "+ + "%v", fakeIP) + } + + host := Base32Encoding.EncodeToString(ip[len(onionPrefixBytes):]) + return &OnionAddr{ + OnionService: host + ".onion", + Port: tcpAddr.Port, + }, nil +} diff --git a/tor/tor_test.go b/tor/tor_test.go new file mode 100644 index 00000000..594b77df --- /dev/null +++ b/tor/tor_test.go @@ -0,0 +1,36 @@ +package tor + +import ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + testOnion = "ld47qlr6h2b7hrrf.onion" + testFakeIP = "fd87:d87e:eb43:58f9:f82e:3e3e:83f3:c625" +) + +// TestOnionHostToFakeIP tests that an onion host address can be converted into +// a fake tcp6 address successfully. +func TestOnionHostToFakeIP(t *testing.T) { + ip, err := OnionHostToFakeIP(testOnion) + require.NoError(t, err) + require.Equal(t, testFakeIP, ip.String()) +} + +// TestFakeIPToOnionHost tests that a fake tcp6 address can be converted back +// into its original .onion host address successfully. +func TestFakeIPToOnionHost(t *testing.T) { + tcpAddr, err := net.ResolveTCPAddr( + "tcp6", fmt.Sprintf("[%s]:8333", testFakeIP), + ) + require.NoError(t, err) + require.True(t, IsOnionFakeIP(tcpAddr)) + + onionHost, err := FakeIPToOnionHost(tcpAddr) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%s:8333", testOnion), onionHost.String()) +}