diff --git a/minode/main.py b/minode/main.py index bacecba..4b02b4c 100644 --- a/minode/main.py +++ b/minode/main.py @@ -272,6 +272,15 @@ def main(): logging.error('Unsupported proxy schema!') return + if shared.tor: + try: + from . import tor + except ImportError: + logging.info('Failed to import tor module.', exc_info=True) + else: + if not tor.start_tor_service(): + logging.warning('Failed to start tor service.') + if shared.ip_enabled and not shared.trusted_peer: bootstrap_from_dns() diff --git a/minode/manager.py b/minode/manager.py index 6591c40..2ec1dab 100644 --- a/minode/manager.py +++ b/minode/manager.py @@ -25,7 +25,7 @@ class Manager(threading.Thread): self.last_pickled_objects = time.time() self.last_pickled_nodes = time.time() # Publish destination 5-15 minutes after start - self.last_published_i2p_destination = \ + self.last_published_destination = \ time.time() - 50 * 60 + random.uniform(-1, 1) * 300 # nosec B311 def run(self): @@ -49,9 +49,10 @@ class Manager(threading.Thread): if now - self.last_pickled_nodes > 60: self.pickle_nodes() self.last_pickled_nodes = now - if now - self.last_published_i2p_destination > 3600: + if now - self.last_published_destination > 3600: self.publish_i2p_destination() - self.last_published_i2p_destination = now + self.publish_onion_peer() + self.last_published_destination = now @staticmethod def clean_objects(): @@ -290,3 +291,11 @@ class Manager(threading.Thread): shared.i2p_dest_obj_type, shared.i2p_dest_obj_version, shared.stream, dest_pub_raw) proofofwork.do_pow_and_publish(obj) + + @staticmethod + def publish_onion_peer(): + if shared.onion_hostname: + logging.info('Publishing our onion peer') + obj = structure.OnionPeer( + shared.onion_hostname, shared.listening_port).to_object() + proofofwork.do_pow_and_publish(obj) diff --git a/minode/shared.py b/minode/shared.py index 120d581..6304f39 100644 --- a/minode/shared.py +++ b/minode/shared.py @@ -31,6 +31,7 @@ onion_obj_version = 3 socks_proxy = None tor = False +onion_hostname = '' i2p_enabled = False i2p_transient = False diff --git a/minode/tor.py b/minode/tor.py new file mode 100644 index 0000000..3e5ec1c --- /dev/null +++ b/minode/tor.py @@ -0,0 +1,137 @@ +"""Tor specific procedures""" +import logging +import os +import stat +import random +import tempfile + +import stem +import stem.control +import stem.process +import stem.util +import stem.version + +from . import shared + + +def logwrite(line): + """A simple log writing handler for tor messages""" + try: + level, line = line.split('[', 1)[1].split(']', 1) + except (IndexError, ValueError): + logging.warning(line) + else: + if level in ('err', 'warn'): + logging.info('(tor)%s', line) + + +def start_tor_service(): + """Start own tor instance and configure a hidden service""" + try: + socket_dir = os.path.join(shared.data_directory, 'tor') + os.makedirs(socket_dir, exist_ok=True) + except OSError: + try: + socket_dir = tempfile.mkdtemp() + except OSError: + logging.info('Failed to create a temp dir.') + return + + try: + present_permissions = os.stat(socket_dir)[0] + disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO + allowed_permissions = ((1 << 32) - 1) ^ disallowed_permissions + os.chmod(socket_dir, allowed_permissions & present_permissions) + except OSError: + logging.debug('Failed to set dir permissions.') + return + + stem.util.log.get_logger().setLevel(logging.WARNING) + + control_socket = os.path.abspath(os.path.join(socket_dir, 'tor_control')) + tor_config = { + 'SocksPort': str(shared.socks_proxy.port), + 'ControlSocket': control_socket} + + for attempt in range(50): + if attempt > 0: + port = random.randint(32767, 65535) # nosec B311 + tor_config['SocksPort'] = str(port) + try: + stem.process.launch_tor_with_config( + tor_config, take_ownership=True, timeout=20, + init_msg_handler=logwrite) + except OSError: + if not attempt: + try: + stem.version.get_system_tor_version() + except IOError: + return + continue + else: + logging.info('Started tor on port %s', port) + break + else: + logging.debug('Failed to start tor.') + return + + try: + controller = stem.control.Controller.from_socket_file(control_socket) + controller.authenticate() + except stem.SocketError: + logging.debug('Failed to instantiate or authenticate on controller.') + return + + onionkey = onionkeytype = None + try: + with open( + os.path.join(shared.data_directory, 'onion_dest_priv.key'), + 'r', encoding='ascii' + ) as src: + onionkey = src.read() + logging.debug('Loaded onion service private key.') + onionkeytype = 'ED25519-V3' + except FileNotFoundError: + pass + except Exception: + logging.info( + 'Error while loading onion service private key.', exc_info=True) + + response = controller.create_ephemeral_hidden_service( + shared.listening_port, key_type=onionkeytype or 'NEW', + key_content=onionkey or 'BEST' + ) + + if not response.is_ok(): + logging.info('Bad response from controller ):') + return + + shared.onion_hostname = '{}.onion'.format(response.service_id) + logging.info('Started hidden service %s', shared.onion_hostname) + + if onionkey: + return True + + try: + with open( + os.path.join(shared.data_directory, 'onion_dest_priv.key'), + 'w', encoding='ascii' + ) as src: + src.write(response.private_key) + logging.debug('Saved onion service private key.') + except Exception: + logging.warning( + 'Error while saving onion service private key.', exc_info=True) + + try: + with open( + os.path.join(shared.data_directory, 'onion_dest.pub'), + 'w', encoding='ascii' + ) as src: + src.write(response.service_id) + logging.debug('Saved onion service public key.') + except Exception: + logging.warning( + 'Error while saving onion service public key.', exc_info=True) + + return True