From 75aaa5dc374c7402e5610ac00e7c5c3a6db86c4b Mon Sep 17 00:00:00 2001 From: Steven Van Ingelgem Date: Thu, 23 Mar 2023 18:29:14 +0100 Subject: [PATCH 1/2] Implement exit_country behaviour. --- README.md | 5 ++++- setup.py | 5 ++++- torpy/circuit.py | 2 +- torpy/client.py | 8 ++++---- torpy/consesus.py | 36 +++++++++++++++++++++++++++++++----- torpy/guard.py | 11 ++++++++++- 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a96cdf8..ff9a442 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ from torpy import TorClient hostname = 'ifconfig.me' # It's possible use onion hostname here as well with TorClient() as tor: # Choose random guard node and create 3-hops circuit - with tor.create_circuit(3) as circuit: + with tor.create_circuit(3, exit_country='FR') as circuit: # Create tor stream to host with circuit.create_stream((hostname, 80)) as stream: # Now we can communicate with host @@ -125,6 +125,9 @@ with TorClient() as tor: recv = stream.recv(1024) ``` +The `exit_country` behaviour isn't available out-of-the-box, but can be enabled via `pip install torpy[exit_country]`. + + TorHttpAdapter is a convenient Tor adapter for the [requests library](https://2.python-requests.org/en/master/user/advanced/#transport-adapters). The following example shows the usage of TorHttpAdapter for multi-threaded HTTP requests: ```python diff --git a/setup.py b/setup.py index dc318e5..68e1576 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,10 @@ packages=find_packages(exclude=['tests']), python_requires='>=3.6', install_requires=['cryptography>=3.2'], - extras_require={'requests': 'requests>2.9,!=2.17.0,!=2.18.0'}, + extras_require={ + 'requests': 'requests>2.9,!=2.17.0,!=2.18.0', + 'exit_country': 'location_ipfire_db_reader', + }, entry_points={'console_scripts': ['torpy_cli=torpy.cli.console:main', 'torpy_socks=torpy.cli.socks:main'] }, diff --git a/torpy/circuit.py b/torpy/circuit.py index 8121391..2a132d4 100644 --- a/torpy/circuit.py +++ b/torpy/circuit.py @@ -585,7 +585,7 @@ def build_hops(self, hops_count): logger.info('Building %i hops circuit...', hops_count) while self.nodes_count < hops_count: if self.nodes_count == hops_count - 1: - router = self._guard.consensus.get_random_exit_node() + router = self._guard.consensus.get_random_exit_node(in_country=self._guard.exit_country) else: router = self._guard.consensus.get_random_middle_node() diff --git a/torpy/client.py b/torpy/client.py index 1a41de5..bd36169 100644 --- a/torpy/client.py +++ b/torpy/client.py @@ -48,14 +48,14 @@ def create(cls, authorities=None, cache_class=None, cache_kwargs=None, auth_data msg='Retry with another guard...', no_traceback=(socket.timeout, TorSocketConnectError,)) ) - def get_guard(self, by_flags=None): + def get_guard(self, by_flags=None, exit_country=None): # TODO: add another stuff to filter guards guard_router = self._consensus.get_random_guard_node(by_flags) - return TorGuard(guard_router, purpose='TorClient', consensus=self._consensus, auth_data=self._auth_data) + return TorGuard(guard_router, purpose='TorClient', consensus=self._consensus, auth_data=self._auth_data, exit_country=exit_country) @contextmanager - def create_circuit(self, hops_count=3, guard_by_flags=None) -> 'ContextManager[TorCircuit]': - with self.get_guard(guard_by_flags) as guard: + def create_circuit(self, hops_count=3, exit_country=None, guard_by_flags=None) -> 'ContextManager[TorCircuit]': + with self.get_guard(guard_by_flags, exit_country=exit_country) as guard: yield guard.create_circuit(hops_count) def __enter__(self): diff --git a/torpy/consesus.py b/torpy/consesus.py index aa3ddeb..d791b8c 100644 --- a/torpy/consesus.py +++ b/torpy/consesus.py @@ -34,6 +34,14 @@ from torpy.documents.dir_key_certificate import DirKeyCertificateList from torpy.documents.network_status_diff import NetworkStatusDiffDocument from torpy.dirs import AUTHORITY_DIRS, FALLBACK_DIRS +try: + from location_ipfire_db_reader import LocationDatabase + HAVE_IPFIRE = True + locdb = None +except ImportError as ex: + HAVE_IPFIRE = False +class NoRouterFound(Exception): + ... logger = logging.getLogger(__name__) @@ -305,7 +313,7 @@ def get_router(self, fingerprint) -> Router: fingerprint_b = b32decode(fingerprint.upper()) return next(onion_router for onion_router in self.document.routers if onion_router.fingerprint == fingerprint_b) - def get_routers(self, flags=None, has_dir_port=True, with_renew=True): + def get_routers(self, flags=None, has_dir_port=True, with_renew=True, in_country=None): """ Select consensus routers that satisfy certain parameters. @@ -321,11 +329,13 @@ def get_routers(self, flags=None, has_dir_port=True, with_renew=True): continue if has_dir_port and not onion_router.dir_port: continue + if not self._is_router_in_country(onion_router, in_country): + continue results.append(onion_router) return results - def get_random_router(self, flags=None, has_dir_port=None, with_renew=True): + def get_random_router(self, flags=None, has_dir_port=None, with_renew=True, in_country=None): """ Select a random consensus router that satisfy certain parameters. @@ -334,16 +344,18 @@ def get_random_router(self, flags=None, has_dir_port=None, with_renew=True): :param with_renew: Do renew consensus if old :return: router """ - routers = self.get_routers(flags, has_dir_port, with_renew) + routers = self.get_routers(flags, has_dir_port, with_renew, in_country=in_country) + if not routers: + raise NoRouterFound((flags, in_country)) return random.choice(routers) def get_random_guard_node(self, different_flags=None): flags = different_flags or [RouterFlags.Guard] return self.get_random_router(flags) - def get_random_exit_node(self): + def get_random_exit_node(self, in_country=None): flags = [RouterFlags.Fast, RouterFlags.Running, RouterFlags.Valid, RouterFlags.Exit] - return self.get_random_router(flags) + return self.get_random_router(flags, in_country=in_country) def get_random_middle_node(self): flags = [RouterFlags.Fast, RouterFlags.Running, RouterFlags.Valid] @@ -453,3 +465,17 @@ def get_responsibles(self, hidden_service): idx = (i + 1 + j) % len(hsdir_router_list) yield hsdir_router_list[idx] break + def _is_router_in_country(self, onion_router, in_country): + if in_country is None or not bool(in_country): + return True + + if not HAVE_IPFIRE: + return True + + LocationDatabase.download('location.db') + + global locdb + if locdb is None: + locdb = LocationDatabase('location.db') + + return locdb.find_country(onion_router.ip).upper() in in_country diff --git a/torpy/guard.py b/torpy/guard.py index db61f5e..9a27f99 100644 --- a/torpy/guard.py +++ b/torpy/guard.py @@ -54,11 +54,17 @@ def send(self, cell): class TorGuard: - def __init__(self, router, purpose=None, consensus=None, auth_data=None): + def __init__(self, router, purpose=None, consensus=None, auth_data=None, exit_country=None): self._router = router self._purpose = purpose self._consensus = consensus self._auth_data = auth_data + if isinstance(exit_country, str) and exit_country.strip(): + self._exit_country = (exit_country.upper().strip(),) + elif isinstance(exit_country, (list, tuple)): + self._exit_country = tuple(ec.upper().strip() for ec in exit_country) + else: + self._exit_country = None self._state = GuardState.Connecting logger.info('Connecting to guard node %s... (%s)', self._router, self._purpose) @@ -89,6 +95,9 @@ def router(self): @property def auth_data(self): return self._auth_data + @property + def exit_country(self): + return self._exit_country def __enter__(self): """Return Guard object.""" From b446f21d91b85b9302be1e526ad706b5382a6eb1 Mon Sep 17 00:00:00 2001 From: Steven Van Ingelgem Date: Thu, 5 Sep 2024 20:05:47 +0200 Subject: [PATCH 2/2] No need to download the database explicitly anymore. --- torpy/consesus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/torpy/consesus.py b/torpy/consesus.py index d791b8c..368e768 100644 --- a/torpy/consesus.py +++ b/torpy/consesus.py @@ -472,8 +472,6 @@ def _is_router_in_country(self, onion_router, in_country): if not HAVE_IPFIRE: return True - LocationDatabase.download('location.db') - global locdb if locdb is None: locdb = LocationDatabase('location.db')