lnworker: don't connect to onion peers if no proxy
What changed, and why it matters
This commit changes Electrum's Lightning networking code so that, when no Tor/proxy is enabled, it simply skips over '.onion' peer addresses instead of trying to connect to them. The goal is to avoid wasting connection attempts on addresses that cannot be reached without a proxy. It is a performance and reliability improvement, not a fix for an exploitable security vulnerability.
No urgent security action required. Treat as a normal reliability/performance improvement. Reviewers may want to confirm that the new 'is_onion()' check correctly handles only v3 onion suffixes and does not accidentally filter other address types.
Security signals we found
Behavioral hardening: avoids futile outbound connection attempts to unreachable onion services when no proxy is configured
No memory-safety, cryptographic, authentication, or authorization changes
No input parsing of untrusted data beyond existing LNPeerAddr validation
No CVE, advisory, or vendor security disclosure referenced in commit
Evidence from the diff
The patch adds an LNPeerAddr.is_onion() helper and updates LNPeerManager to filter onion addresses out of candidate peer lists when self.network.proxy is absent or disabled. choose_preferred_address() now returns None if all addresses are onion and no proxy is available, and callers skip those peers. Tests are added for the new helper and the address-selection logic.
Changed components
electrum/lnworker.pyelectrum/lntransport.pytests/test_lnpeermgr.pytests/test_lntransport.pyInspect captured patch +78 / −6
diff --git a/electrum/lntransport.py b/electrum/lntransport.py
index 716f35e..8f242ea 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 f6b5035..c884659 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 169f7dd..01f9444 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 bcad665..360b35b 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())
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.