A rough implementation of onion service based on pybitmessage plugin;

publishing interval for onionpeer object is a half of its TTL,
as for I2P destination.
This commit is contained in:
Lee Miller 2023-09-05 04:55:58 +03:00
parent 59fcd9eb2b
commit d6de7c8d1e
Signed by: lee.miller
GPG Key ID: 4F97A5EA88F4AB63
4 changed files with 163 additions and 0 deletions

View File

@ -276,6 +276,15 @@ def main():
logging.error('Unsupported proxy schema!')
return
if shared.tor:
try:
from . import tor # pylint: disable=import-outside-toplevel
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()

View File

@ -28,6 +28,10 @@ class Manager(threading.Thread):
# Publish destination 5-15 minutes after start
self.last_published_i2p_destination = \
time.time() - 50 * 60 + random.uniform(-1, 1) * 300 # nosec B311
# Publish onion 4-8 min later
self.last_published_onion_peer = \
self.last_published_i2p_destination - 3 * 3600 + \
random.uniform(1, 2) * 240 # nosec B311
def fill_bootstrap_pool(self):
"""Populate the bootstrap pool by core nodes and checked ones"""
@ -59,6 +63,9 @@ class Manager(threading.Thread):
if now - self.last_published_i2p_destination > 3600:
self.publish_i2p_destination()
self.last_published_i2p_destination = now
if now - self.last_published_onion_peer > 3600 * 4:
self.publish_onion_peer()
self.last_published_onion_peer = now
@staticmethod
def clean_objects():
@ -320,3 +327,12 @@ 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():
"""Make and publish a `structure.OnionPeer`"""
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)

View File

@ -31,6 +31,7 @@ onion_obj_version = 3
socks_proxy = None
tor = False
onion_hostname = ''
i2p_enabled = False
i2p_transient = False

137
minode/tor.py Normal file
View File

@ -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