From db6b6a16f8a380de135480c8d2c10183de6bf846 Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 27 Jan 2026 12:49:19 +0100 Subject: [PATCH 1/2] lnpeermgr: add_peer: fix check if proxy enabled LNPeerManager.add_peer would only check if self.network.proxy is set, which it is always as Network is initialized with self.proxy = ProxySettings(). Instead it should check if proxy is set and enabled. --- electrum/lnworker.py | 2 +- tests/test_lnpeer.py | 3 ++- tests/test_lnpeermgr.py | 60 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 tests/test_lnpeermgr.py diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 4270dad72..f6b503538 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -588,7 +588,7 @@ class LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): host, port, timestamp = self.choose_preferred_address(list(addrs)) port = int(port) - if not self.network.proxy: + if not self.network.proxy or not self.network.proxy.enabled: # Try DNS-resolving the host (if needed). This is simply so that # the caller gets a nice exception if it cannot be resolved. # (we don't do the DNS lookup if a proxy is set, to avoid a DNS-leak) diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index 6107a8b49..be4ca3d75 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -26,7 +26,7 @@ from electrum import bitcoin from electrum import util from electrum import constants from electrum import bip32 -from electrum.network import Network +from electrum.network import Network, ProxySettings from electrum import simple_config, lnutil from electrum.lnaddr import lnencode, LnAddr, lndecode from electrum.bitcoin import COIN, sha256 @@ -71,6 +71,7 @@ class MockNetwork: self.path_finder = LNPathFinder(self.channel_db) self.lngossip = MockLNGossip() self.tx_queue = asyncio.Queue() + self.proxy = ProxySettings() self._blockchain = MockBlockchain() def get_local_height(self): diff --git a/tests/test_lnpeermgr.py b/tests/test_lnpeermgr.py new file mode 100644 index 000000000..169f7ddc1 --- /dev/null +++ b/tests/test_lnpeermgr.py @@ -0,0 +1,60 @@ +import logging +import os +import socket +import asyncio +from unittest import mock + +from . import ElectrumTestCase + +from electrum.lntransport import ConnStringFormatError +from electrum.logging import console_stderr_handler + + +class TestLNPeerManager(ElectrumTestCase): + TESTNET = True + + @classmethod + def setUpClass(cls): + super().setUpClass() + console_stderr_handler.setLevel(logging.DEBUG) + + async def asyncSetUp(self): + lnwallet = self.create_mock_lnwallet(name='mock_lnwallet_anchors', has_anchors=True) + self.lnpeermgr = lnwallet.lnpeermgr + await super().asyncSetUp() + + async def test_add_peer_conn_string_errors(self): + unknown_node_id = os.urandom(33) + peermgr = self.lnpeermgr + peermgr._add_peer = mock.Mock(side_effect=NotImplementedError) + + # Trampoline enabled, unknown node (no address in trampolines) + channel_db = peermgr.network.channel_db + peermgr.network.channel_db = None + try: + with self.assertRaises(ConnStringFormatError) as cm: + await peermgr.add_peer(unknown_node_id.hex()) + self.assertIn("Address unknown for node", str(cm.exception)) + finally: + peermgr.network.channel_db = channel_db # re-set channel db + + # Trampoline disabled, unknown node (no address in channel_db) + with mock.patch.object(peermgr.network.channel_db, 'get_node_addresses', return_value=[]): + with self.assertRaises(ConnStringFormatError) as cm: + await peermgr.add_peer(unknown_node_id.hex()) + self.assertIn("Don't know any addresses for node", str(cm.exception)) + + # .onion address, but no proxy configured + onion_conn_str = unknown_node_id.hex() + "@somewhere.onion:9735" + self.assertFalse(peermgr.network.proxy.enabled) + with self.assertRaises(ConnStringFormatError) as cm: + await peermgr.add_peer(onion_conn_str) + self.assertIn(".onion address, but no proxy configured", str(cm.exception)) + + # Hostname does not resolve (getaddrinfo failed) + bad_host_conn_str = unknown_node_id.hex() + "@badhost:9735" + loop = asyncio.get_running_loop() + with mock.patch.object(loop, 'getaddrinfo', side_effect=socket.gaierror): + with self.assertRaises(ConnStringFormatError) as cm: + await peermgr.add_peer(bad_host_conn_str) + self.assertIn("Hostname does not resolve", str(cm.exception)) From 79ef429b3ca6c4b17bb9f0a2c208f550f8b2fefb Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 27 Jan 2026 11:44:49 +0100 Subject: [PATCH 2/2] lnworker: don't connect to onion peers if no proxy When gossip is enabled we waste a lot of time trying to connect to onion peers if we don't have a proxy enabled. We should just skip them and try to connect to clearnet peers instead. --- electrum/lntransport.py | 3 +++ electrum/lnworker.py | 19 +++++++++++++------ tests/test_lnpeermgr.py | 30 ++++++++++++++++++++++++++++++ tests/test_lntransport.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/electrum/lntransport.py b/electrum/lntransport.py index 716f35e65..8f242eaa0 100644 --- a/electrum/lntransport.py +++ b/electrum/lntransport.py @@ -186,6 +186,9 @@ class LNPeerAddr: def net_addr_str(self) -> str: return str(self._net_addr) + def is_onion(self) -> bool: + return self.host.endswith('.onion') + def __eq__(self, other): if not isinstance(other, LNPeerAddr): return False diff --git a/electrum/lnworker.py b/electrum/lnworker.py index f6b503538..c8846595d 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -483,6 +483,8 @@ class LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): continue if not self.is_good_peer(peer): continue + if peer.is_onion() and not self.network.proxy or not self.network.proxy.enabled: + continue return [peer] # try random peer from graph unconnected_nodes = self.channel_db.get_200_randomly_sorted_nodes_not_in(self.peers.keys()) @@ -491,7 +493,10 @@ class LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): addrs = self.channel_db.get_node_addresses(node_id) if not addrs: continue - host, port, timestamp = self.choose_preferred_address(list(addrs)) + address = self.choose_preferred_address(list(addrs)) + if not address: + continue + host, port, timestamp = address try: peer = LNPeerAddr(host, port, node_id) except ValueError: @@ -550,15 +555,17 @@ class LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): self.logger.info(f'got {len(peers)} ln peers from dns seed') return peers - @staticmethod - def choose_preferred_address(addr_list: Sequence[Tuple[str, int, int]]) -> Tuple[str, int, int]: + def choose_preferred_address(self, addr_list: Sequence[Tuple[str, int, int]]) -> Optional[Tuple[str, int, int]]: assert len(addr_list) >= 1 # choose the most recent one that is an IP for host, port, timestamp in sorted(addr_list, key=lambda a: -a[2]): if is_ip_address(host): return host, port, timestamp + if not self.network.proxy or not self.network.proxy.enabled: + addr_list = [(h, p, ts) for h, p, ts in addr_list if not h.endswith('.onion')] + if not addr_list: + return None # otherwise choose one at random - # TODO maybe filter out onion if not on tor? choice = random.choice(addr_list) return choice @@ -583,9 +590,9 @@ class LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): host, port = addr.host, addr.port else: addrs = self.channel_db.get_node_addresses(node_id) - if not addrs: + if not addrs or not (address := self.choose_preferred_address(list(addrs))): raise ConnStringFormatError(_('Don\'t know any addresses for node:') + ' ' + node_id.hex()) - host, port, timestamp = self.choose_preferred_address(list(addrs)) + host, port, timestamp = address port = int(port) if not self.network.proxy or not self.network.proxy.enabled: diff --git a/tests/test_lnpeermgr.py b/tests/test_lnpeermgr.py index 169f7ddc1..01f9444f0 100644 --- a/tests/test_lnpeermgr.py +++ b/tests/test_lnpeermgr.py @@ -58,3 +58,33 @@ class TestLNPeerManager(ElectrumTestCase): with self.assertRaises(ConnStringFormatError) as cm: await peermgr.add_peer(bad_host_conn_str) self.assertIn("Hostname does not resolve", str(cm.exception)) + + def test_choose_preferred_address(self): + peermgr = self.lnpeermgr + + # prefer most recent IP address + addr_list = [ + ("192.168.1.1", 9735, 100), + ("host.onion", 9735, 200), + ("10.0.0.1", 9735, 150), + ("host.com", 9735, 250) + ] + result = peermgr.choose_preferred_address(addr_list) + self.assertEqual(result, ("10.0.0.1", 9735, 150)) # Most recent IP + + # no IP, proxy disabled, filter .onion and choose random + self.assertFalse(peermgr.network.proxy.enabled) + addr_list = [("host.com", 9735, 100), ("host.onion", 9735, 200)] + result = peermgr.choose_preferred_address(addr_list) + self.assertEqual(result, ("host.com", 9735, 100)) + + # empty list after filtering + addr_list = [("host.onion", 9735, 100)] + result = peermgr.choose_preferred_address(addr_list) + self.assertIsNone(result) + + # return onion if proxy enabled + peermgr.network.proxy.enabled = True + addr_list = [("host.onion", 9735, 100)] + result = peermgr.choose_preferred_address(addr_list) + self.assertEqual(result, ("host.onion", 9735, 100)) diff --git a/tests/test_lntransport.py b/tests/test_lntransport.py index bcad665db..360b35b81 100644 --- a/tests/test_lntransport.py +++ b/tests/test_lntransport.py @@ -141,3 +141,35 @@ class TestLNTransport(ElectrumTestCase): self.assertEqual(extract_nodeid(f"{pubkey1.hex()}@[2001:41d0:e:734::1]:8888"), (pubkey1, "[2001:41d0:e:734::1]:8888")) # just pubkey self.assertEqual(extract_nodeid(f"{pubkey1.hex()}"), (pubkey1, None)) + + +class TestLNPeerAddr(ElectrumTestCase): + + def test_validate_net_address(self): + # Test invalid host + with self.assertRaises(ValueError): + LNPeerAddr("", 9735, b'\x00'*33) + with self.assertRaises(ValueError): + LNPeerAddr("999.999.999.999", 9735, b'\x00'*33) + # Test invalid port + with self.assertRaises(ValueError): + LNPeerAddr("127.0.0.1", -1, b'\x00'*33) + with self.assertRaises(ValueError): + LNPeerAddr("127.0.0.1", 70000, b'\x00'*33) + + def test_is_onion(self): + # Test onion addresses + addr1 = LNPeerAddr("example.onion", 9735, b'\x00'*33) + self.assertTrue(addr1.is_onion()) + addr2 = LNPeerAddr("subdomain.example.onion", 9735, b'\x00'*33) + self.assertTrue(addr2.is_onion()) + + # Test non-onion + addr3 = LNPeerAddr("example.com", 9735, b'\x00'*33) + self.assertFalse(addr3.is_onion()) + addr4 = LNPeerAddr("127.0.0.1", 9735, b'\x00'*33) + self.assertFalse(addr4.is_onion()) + addr5 = LNPeerAddr("::1", 9735, b'\x00'*33) + self.assertFalse(addr5.is_onion()) + addr6 = LNPeerAddr("onion", 9735, b'\x00'*33) + self.assertFalse(addr6.is_onion())