From 90d1d587e8cfbb3512bf7adce0b55c421c679e28 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Nov 2023 12:12:45 +0100 Subject: [PATCH 1/2] network: async tor probe --- electrum/gui/qml/qenetwork.py | 4 ++++ electrum/gui/qt/main_window.py | 16 ++++++++++++++++ electrum/network.py | 32 +++++++++++++++++++++++--------- electrum/util.py | 4 +--- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 93d9db7af..cfe0df943 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -86,6 +86,10 @@ class QENetwork(QObject, QtEventListener): self.proxySet.emit() self.proxyTorChanged.emit() + @event_listener + def on_event_tor_probed(self, *args): + self.proxyTorChanged.emit() + def _update_status(self): server = str(self.network.get_parameters().server) if self._server != server: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 524401b98..76b56e0c7 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -496,6 +496,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def on_event_cert_mismatch(self, *args): self.show_cert_mismatch_error() + @qt_event_listener + def on_event_tor_probed(self, is_tor): + self.tor_button.setVisible(is_tor) + + @qt_event_listener + def on_event_proxy_set(self, *args): + self.tor_button.setVisible(False) + def close_wallet(self): if self.wallet: self.logger.info(f'close_wallet {self.wallet.storage.path}') @@ -956,6 +964,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): network_text = "" balance_text = "" + if self.tor_button: + self.tor_button.setVisible(self.network and self.network.tor_proxy) + if self.network is None: network_text = _("Offline") icon = read_QIcon("status_disconnected.png") @@ -1634,9 +1645,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): sb.addPermanentWidget(self.lightning_button) self.update_lightning_icon() self.status_button = None + self.tor_button = None if self.network: self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog, sb_height) sb.addPermanentWidget(self.status_button) + self.tor_button = StatusBarButton(read_QIcon("tor_logo.png"), _("TOR"), + self.gui_object.show_network_dialog, sb_height) + sb.addPermanentWidget(self.tor_button) + self.tor_button.setVisible(False) run_hook('create_status_bar', sb) self.setStatusBar(sb) diff --git a/electrum/network.py b/electrum/network.py index e8b952a91..a76ed826e 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -323,6 +323,8 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL} + self.proxy = None + self.tor_proxy = False self._init_parameters_from_config() self.taskgroup = None @@ -510,6 +512,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): oneserver=self.oneserver) def _init_parameters_from_config(self) -> None: + dns_hacks.configure_dns_resolver() self.auto_connect = self.config.NETWORK_AUTO_CONNECT self._set_default_server() self._set_proxy(deserialize_proxy(self.config.NETWORK_PROXY, self.config.NETWORK_PROXY_USER, @@ -625,18 +628,29 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}" def _set_proxy(self, proxy: Optional[dict]): - self.proxy = proxy - dns_hacks.configure_dns_resolver() - self.logger.info(f'setting proxy {proxy}') + if self.proxy == proxy: + return + self.logger.info(f'setting proxy {proxy}') + self.proxy = proxy self.tor_proxy = False - if bool(proxy) and proxy['mode'] == 'socks5': - # test for Tor - self.tor_proxy = util.is_tor_socks_port(proxy['host'], int(proxy['port'])) - if self.tor_proxy: - self.logger.info(f'Proxy is TOR') - util.trigger_callback('proxy_set', self.proxy, self.tor_proxy) + def tor_probe_task(p): + assert p is not None + tor_proxy = util.is_tor_socks_port(p['host'], int(p['port'])) + if self.proxy == p: # is this the proxy we probed? + self.logger.info(f'Proxy is {"" if tor_proxy else "not "}TOR') + self._tor_probe_done(tor_proxy) + + if proxy['mode'] == 'socks5': + t = threading.Thread(target=tor_probe_task, args=(proxy,), daemon=True) + t.start() + + util.trigger_callback('proxy_set', self.proxy) + + def _tor_probe_done(self, is_tor: bool): + self.tor_proxy = is_tor + util.trigger_callback('tor_probed', is_tor) @log_exceptions async def set_parameters(self, net_params: NetworkParameters): diff --git a/electrum/util.py b/electrum/util.py index ab0626646..d27687457 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1498,9 +1498,7 @@ def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]: def is_tor_socks_port(host: str, port: int) -> bool: try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(1.0) - s.connect((host, port)) + with socket.create_connection((host, port), timeout=10) as s: # mimic "tor-resolve 0.0.0.0". # see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075 # > this is a socks5 handshake, followed by a socks RESOLVE request as defined in From 0d476f73dfafe0e41e7470d4fd52412ee245a70c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 4 Dec 2023 10:42:32 +0100 Subject: [PATCH 2/2] network: rename network.tor_proxy to network.is_proxy_tor and keep it at None until TOR probe is done. --- electrum/gui/qml/qenetwork.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/gui/qt/network_dialog.py | 2 +- electrum/network.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index cfe0df943..295bbe694 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -267,7 +267,7 @@ class QENetwork(QObject, QtEventListener): proxyTorChanged = pyqtSignal() @pyqtProperty(bool, notify=proxyTorChanged) def isProxyTor(self): - return self.network.tor_proxy + return bool(self.network.is_proxy_tor) @pyqtProperty('QVariant', notify=feeHistogramUpdated) def feeHistogram(self): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 76b56e0c7..e169e1b66 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -965,7 +965,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): balance_text = "" if self.tor_button: - self.tor_button.setVisible(self.network and self.network.tor_proxy) + self.tor_button.setVisible(self.network and bool(self.network.is_proxy_tor)) if self.network is None: network_text = _("Offline") diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index dfb6265e3..994024669 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -146,7 +146,7 @@ class NodesListWidget(QTreeWidget): def update(self, *, network: Network, servers: dict): self.clear() - use_tor = network.tor_proxy + use_tor = bool(network.is_proxy_tor) # connected servers connected_servers_item = QTreeWidgetItem([_("Connected nodes"), '']) diff --git a/electrum/network.py b/electrum/network.py index a76ed826e..46a271695 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -324,7 +324,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL} self.proxy = None - self.tor_proxy = False + self.is_proxy_tor = None self._init_parameters_from_config() self.taskgroup = None @@ -633,7 +633,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self.logger.info(f'setting proxy {proxy}') self.proxy = proxy - self.tor_proxy = False + self.is_proxy_tor = False def tor_probe_task(p): assert p is not None @@ -649,7 +649,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): util.trigger_callback('proxy_set', self.proxy) def _tor_probe_done(self, is_tor: bool): - self.tor_proxy = is_tor + self.is_proxy_tor = is_tor util.trigger_callback('tor_probed', is_tor) @log_exceptions