WIP: Add support for tor using PySocks and optionally stem #2
|
@ -9,7 +9,8 @@ RUN apt-add-repository ppa:purplei2p/i2pd && apt-get update -qq
|
|||
RUN apt-get install -yq --no-install-suggests --no-install-recommends \
|
||||
python3-dev python3-pip python-is-python3 python3.11-dev python3.11-venv
|
||||
|
||||
RUN apt-get install -yq --no-install-suggests --no-install-recommends sudo i2pd
|
||||
RUN apt-get install -yq --no-install-suggests --no-install-recommends \
|
||||
sudo i2pd tor
|
||||
|
||||
RUN echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
sudo service i2pd start
|
||||
sudo service tor start
|
||||
|
|
|
@ -30,7 +30,7 @@ usage: main.py [-h] [-p PORT] [--host HOST] [--debug] [--data-dir DATA_DIR]
|
|||
[--connection-limit CONNECTION_LIMIT] [--i2p]
|
||||
[--i2p-tunnel-length I2P_TUNNEL_LENGTH]
|
||||
[--i2p-sam-host I2P_SAM_HOST] [--i2p-sam-port I2P_SAM_PORT]
|
||||
[--i2p-transient]
|
||||
[--i2p-transient] [--socks-proxy SOCKS_PROXY] [--tor]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
@ -53,6 +53,10 @@ optional arguments:
|
|||
--i2p-sam-port I2P_SAM_PORT
|
||||
Port of I2P SAMv3 bridge
|
||||
--i2p-transient Generate new I2P destination on start
|
||||
--socks-proxy SOCKS_PROXY
|
||||
SOCKS proxy address in the form <HOST>:<PORT>
|
||||
--tor The SOCKS proxy is tor, use 127.0.0.1:9050 if not
|
||||
specified, start tor and setup a hidden service
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import errno
|
|||
import logging
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
|
@ -78,7 +79,11 @@ class ConnectionBase(threading.Thread):
|
|||
self.s.settimeout(0)
|
||||
if not self.server:
|
||||
if self.network == 'ip':
|
||||
self.send_queue.put(message.Version(self.host, self.port))
|
||||
version_kwargs = (
|
||||
{'services': 1} if self.host.endswith('.onion') else {})
|
||||
self.send_queue.put(message.Version(
|
||||
('127.0.0.1' if shared.socks_proxy else self.host),
|
||||
self.port, **version_kwargs))
|
||||
else:
|
||||
self.send_queue.put(message.Version('127.0.0.1', 7656))
|
||||
while True:
|
||||
|
@ -257,12 +262,17 @@ class ConnectionBase(threading.Thread):
|
|||
'Established Bitmessage protocol connection to %s:%s',
|
||||
self.host_print, self.port)
|
||||
self.on_connection_fully_established_scheduled = False
|
||||
if self.remote_version.services & 2 and self.network == 'ip':
|
||||
self._do_tls_handshake() # NODE_SSL
|
||||
if ( # NODE_SSL
|
||||
self.remote_version.services & 2 and self.network == 'ip'
|
||||
and not self.host.endswith('.onion')
|
||||
and not (self.server and shared.tor)
|
||||
):
|
||||
self._do_tls_handshake()
|
||||
|
||||
addr = {
|
||||
structure.NetAddr(c.remote_version.services, c.host, c.port)
|
||||
for c in shared.connections if c.network != 'i2p'
|
||||
and not c.host.endswith('.onion')
|
||||
and c.server is False and c.status == 'fully_established'}
|
||||
# pylint: disable=unsubscriptable-object
|
||||
# https://github.com/pylint-dev/pylint/issues/3637
|
||||
|
@ -393,17 +403,25 @@ class ConnectionBase(threading.Thread):
|
|||
if not self.server:
|
||||
self.send_queue.put('fully_established')
|
||||
if self.network == 'ip':
|
||||
if self.host.endswith('.onion'):
|
||||
shared.onion_pool.add((self.host, self.port))
|
||||
else:
|
||||
shared.address_advertise_queue.put(structure.NetAddr(
|
||||
version.services, self.host, self.port))
|
||||
shared.node_pool.add((self.host, self.port))
|
||||
elif self.network == 'i2p':
|
||||
shared.i2p_node_pool.add((self.host, 'i2p'))
|
||||
if self.network == 'ip':
|
||||
if (
|
||||
self.network == 'ip' and shared.listen_for_connections
|
||||
and version.host != '127.0.0.1'
|
||||
):
|
||||
shared.address_advertise_queue.put(structure.NetAddr(
|
||||
shared.services, version.host, shared.listening_port))
|
||||
if self.server:
|
||||
if self.network == 'ip':
|
||||
self.send_queue.put(message.Version(self.host, self.port))
|
||||
version_kwargs = {'services': 1} if shared.tor else {}
|
||||
self.send_queue.put(message.Version(
|
||||
self.host, self.port, **version_kwargs))
|
||||
else:
|
||||
self.send_queue.put(message.Version('127.0.0.1', 7656))
|
||||
|
||||
|
@ -500,6 +518,13 @@ class Connection(ConnectionBase):
|
|||
' adding to i2p_unchecked_node_pool')
|
||||
logging.debug(dest)
|
||||
shared.i2p_unchecked_node_pool.add((dest, 'i2p'))
|
||||
elif (
|
||||
obj.object_type == shared.onion_obj_type
|
||||
and obj.version == shared.onion_obj_version
|
||||
):
|
||||
peer = structure.OnionPeer.from_object(obj)
|
||||
logging.debug('Received onion peer object: %s', peer)
|
||||
shared.onion_unchecked_pool.add((peer.host, peer.port))
|
||||
shared.vector_advertise_queue.put(obj.vector)
|
||||
|
||||
def _process_msg_getdata(self, m):
|
||||
|
@ -517,4 +542,46 @@ class Bootstrapper(ConnectionBase):
|
|||
self.send_queue.put(None)
|
||||
|
||||
|
||||
class SocksConnection(Connection):
|
||||
"""The socks proxied connection"""
|
||||
def _connect(self):
|
||||
peer_str = '{0.host_print}:{0.port}'.format(self)
|
||||
logging.debug('Connecting to %s', peer_str)
|
||||
|
||||
import socks
|
||||
|
||||
proxy_type = socks.PROXY_TYPES[shared.socks_proxy.scheme.upper()]
|
||||
|
||||
try:
|
||||
self.s = socks.create_connection(
|
||||
(self.host, self.port), 30, None, proxy_type,
|
||||
shared.socks_proxy.hostname, shared.socks_proxy.port, True,
|
||||
shared.socks_proxy.username, shared.socks_proxy.password, None)
|
||||
self.status = 'connected'
|
||||
logging.debug('Established SOCKS connection to %s', peer_str)
|
||||
except socket.timeout:
|
||||
pass
|
||||
except socks.GeneralProxyError as e:
|
||||
e = e.socket_err
|
||||
if isinstance(e, socket.timeout) or (
|
||||
# general failure, unreachable, refused
|
||||
not e.errno and re.match(r'^0x0[1,4,5].*', e.msg)
|
||||
):
|
||||
logcall = logging.debug
|
||||
else:
|
||||
logcall = logging.info
|
||||
logcall('Connection to %s failed. Reason: %s', peer_str, e)
|
||||
except OSError as e:
|
||||
# unreachable, refused, no route
|
||||
(logging.info if e.errno not in (0, 101, 111, 113)
|
||||
else logging.debug)(
|
||||
'Connection to %s failed. Reason: %s', peer_str, e)
|
||||
except Exception:
|
||||
logging.info(
|
||||
'Connection to %s failed.', peer_str, exc_info=True)
|
||||
|
||||
if self.status != 'connected':
|
||||
self.status = 'failed'
|
||||
|
||||
|
||||
shared.connection = Connection
|
||||
|
|
|
@ -5,8 +5,15 @@ import base64
|
|||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
from urllib import parse
|
||||
|
||||
try:
|
||||
import socks
|
||||
except ImportError:
|
||||
socks = None
|
||||
|
||||
from . import i2p, shared
|
||||
from .advertiser import Advertiser
|
||||
|
@ -52,6 +59,16 @@ def parse_arguments(): # pylint: disable=too-many-branches,too-many-statements
|
|||
'--i2p-transient', action='store_true',
|
||||
help='Generate new I2P destination on start')
|
||||
|
||||
if socks is not None:
|
||||
parser.add_argument(
|
||||
'--socks-proxy',
|
||||
help='SOCKS proxy address in the form <HOST>:<PORT>')
|
||||
parser.add_argument(
|
||||
'--tor', action='store_true',
|
||||
help='The SOCKS proxy is tor, use 127.0.0.1:9050 if not specified,'
|
||||
' start tor and setup a hidden service'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.port:
|
||||
shared.listening_port = args.port
|
||||
|
@ -71,7 +88,8 @@ def parse_arguments(): # pylint: disable=too-many-branches,too-many-statements
|
|||
if args.no_ip:
|
||||
shared.ip_enabled = False
|
||||
if args.trusted_peer:
|
||||
if len(args.trusted_peer) > 50:
|
||||
if len(args.trusted_peer
|
||||
) > 50 and not args.trusted_peer.endswith('onion'):
|
||||
# I2P
|
||||
shared.trusted_peer = (args.trusted_peer.encode(), 'i2p')
|
||||
else:
|
||||
|
@ -99,6 +117,17 @@ def parse_arguments(): # pylint: disable=too-many-branches,too-many-statements
|
|||
if args.i2p_transient:
|
||||
shared.i2p_transient = True
|
||||
|
||||
if socks is None:
|
||||
return
|
||||
if args.tor:
|
||||
shared.tor = True
|
||||
if not args.socks_proxy:
|
||||
args.socks_proxy = '127.0.0.1:9050'
|
||||
if args.socks_proxy:
|
||||
if not re.match(r'^.*://', args.socks_proxy):
|
||||
args.socks_proxy = '//' + args.socks_proxy
|
||||
shared.socks_proxy = parse.urlparse(args.socks_proxy, scheme='socks5')
|
||||
|
||||
|
||||
def bootstrap_from_dns():
|
||||
"""Addes addresses of bootstrap servers to core nodes"""
|
||||
|
@ -242,7 +271,31 @@ def main():
|
|||
'Error while creating data directory in: %s',
|
||||
shared.data_directory, exc_info=True)
|
||||
|
||||
if shared.ip_enabled and not shared.trusted_peer:
|
||||
if shared.socks_proxy and shared.send_outgoing_connections:
|
||||
try:
|
||||
socks.PROXY_TYPES[shared.socks_proxy.scheme.upper()]
|
||||
except KeyError:
|
||||
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)
|
||||
tor = None
|
||||
else:
|
||||
if not tor.start_tor_service():
|
||||
logging.warning('The tor service has hot started.')
|
||||
tor = None
|
||||
if not tor:
|
||||
try:
|
||||
socket.socket().bind(('127.0.0.1', 9050))
|
||||
return
|
||||
except (OSError, socket.error):
|
||||
pass
|
||||
|
||||
if shared.ip_enabled and not shared.trusted_peer and not shared.tor:
|
||||
bootstrap_from_dns()
|
||||
|
||||
if shared.i2p_enabled:
|
||||
|
|
|
@ -11,7 +11,7 @@ import threading
|
|||
import time
|
||||
|
||||
from . import proofofwork, shared, structure
|
||||
from .connection import Bootstrapper, Connection
|
||||
from .connection import Bootstrapper, Connection, SocksConnection
|
||||
from .i2p import I2PDialer
|
||||
|
||||
|
||||
|
@ -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():
|
||||
|
@ -126,17 +133,31 @@ class Manager(threading.Thread):
|
|||
):
|
||||
|
||||
if shared.ip_enabled:
|
||||
if len(shared.unchecked_node_pool) > 16:
|
||||
sample_length = 16
|
||||
if shared.tor:
|
||||
if len(shared.onion_unchecked_pool) > 4:
|
||||
to_connect.update(random.sample(
|
||||
tuple(shared.unchecked_node_pool), 16))
|
||||
tuple(shared.onion_unchecked_pool), 4))
|
||||
else:
|
||||
to_connect.update(shared.onion_unchecked_pool)
|
||||
shared.onion_unchecked_pool.difference_update(to_connect)
|
||||
if len(shared.onion_pool) > 2:
|
||||
to_connect.update(random.sample(
|
||||
tuple(shared.onion_pool), 2))
|
||||
else:
|
||||
to_connect.update(shared.onion_pool)
|
||||
sample_length = 8
|
||||
if len(shared.unchecked_node_pool) > sample_length:
|
||||
to_connect.update(random.sample(
|
||||
tuple(shared.unchecked_node_pool), sample_length))
|
||||
else:
|
||||
to_connect.update(shared.unchecked_node_pool)
|
||||
if outgoing_connections < shared.outgoing_connections / 2:
|
||||
bootstrap()
|
||||
shared.unchecked_node_pool.difference_update(to_connect)
|
||||
if len(shared.node_pool) > 8:
|
||||
if len(shared.node_pool) > sample_length / 2:
|
||||
to_connect.update(random.sample(
|
||||
tuple(shared.node_pool), 8))
|
||||
tuple(shared.node_pool), int(sample_length / 2)))
|
||||
else:
|
||||
to_connect.update(shared.node_pool)
|
||||
|
||||
|
@ -174,7 +195,9 @@ class Manager(threading.Thread):
|
|||
else:
|
||||
continue
|
||||
else:
|
||||
connect((host, port))
|
||||
connect(
|
||||
(host, port),
|
||||
Connection if not shared.socks_proxy else SocksConnection)
|
||||
hosts.add(group)
|
||||
shared.hosts = hosts
|
||||
|
||||
|
@ -214,6 +237,17 @@ class Manager(threading.Thread):
|
|||
logging.warning(
|
||||
'Error while loading nodes from disk.', exc_info=True)
|
||||
|
||||
try:
|
||||
with open(
|
||||
os.path.join(shared.data_directory, 'onion_nodes.pickle'), 'br'
|
||||
) as src:
|
||||
shared.onion_pool = pickle.load(src)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
logging.warning(
|
||||
'Error while loading nodes from disk.', exc_info=True)
|
||||
|
||||
with open(
|
||||
os.path.join(shared.source_directory, 'core_nodes.csv'),
|
||||
'r', newline='', encoding='ascii'
|
||||
|
@ -259,6 +293,13 @@ class Manager(threading.Thread):
|
|||
shared.i2p_unchecked_node_pool = set(random.sample(
|
||||
tuple(shared.i2p_unchecked_node_pool), 100))
|
||||
|
||||
if len(shared.onion_pool) > 1000:
|
||||
shared.onion_pool = set(
|
||||
random.sample(shared.onion_pool, 1000))
|
||||
if len(shared.onion_unchecked_pool) > 100:
|
||||
shared.onion_unchecked_pool = set(
|
||||
random.sample(shared.onion_unchecked_pool, 100))
|
||||
|
||||
try:
|
||||
with open(
|
||||
os.path.join(shared.data_directory, 'nodes.pickle'), 'bw'
|
||||
|
@ -268,6 +309,10 @@ class Manager(threading.Thread):
|
|||
os.path.join(shared.data_directory, 'i2p_nodes.pickle'), 'bw'
|
||||
) as dst:
|
||||
pickle.dump(shared.i2p_node_pool, dst, protocol=3)
|
||||
with open(
|
||||
os.path.join(shared.data_directory, 'onion_nodes.pickle'), 'bw'
|
||||
) as dst:
|
||||
pickle.dump(shared.onion_pool, dst, protocol=3)
|
||||
logging.debug('Saved nodes')
|
||||
except Exception:
|
||||
logging.warning('Error while saving nodes', exc_info=True)
|
||||
|
@ -283,3 +328,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)
|
||||
|
|
|
@ -21,11 +21,17 @@ protocol_version = 3
|
|||
services = 3 # NODE_NETWORK, NODE_SSL
|
||||
stream = 1
|
||||
nonce = os.urandom(8)
|
||||
user_agent = b'/MiNode:0.3.3/'
|
||||
user_agent = b'/MiNode:0.3.4/'
|
||||
timeout = 600
|
||||
header_length = 24
|
||||
i2p_dest_obj_type = 0x493250
|
||||
i2p_dest_obj_version = 1
|
||||
onion_obj_type = 0x746f72
|
||||
onion_obj_version = 3
|
||||
|
||||
socks_proxy = None
|
||||
tor = False
|
||||
onion_hostname = ''
|
||||
|
||||
i2p_enabled = False
|
||||
i2p_transient = False
|
||||
|
@ -59,6 +65,9 @@ i2p_core_nodes = set()
|
|||
i2p_node_pool = set()
|
||||
i2p_unchecked_node_pool = set()
|
||||
|
||||
onion_pool = set()
|
||||
onion_unchecked_pool = set()
|
||||
|
||||
outgoing_connections = 8
|
||||
connection_limit = 250
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Protocol structures"""
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
@ -223,3 +225,39 @@ class NetAddr():
|
|||
stream, net_addr = struct.unpack('>QI26s', b)[1:]
|
||||
n = NetAddrNoPrefix.from_bytes(net_addr)
|
||||
return cls(n.services, n.host, n.port, stream)
|
||||
|
||||
|
||||
class OnionPeer():
|
||||
def __init__(self, host, port=8444, stream=None, dest_pub=None):
|
||||
self.stream = stream or shared.stream
|
||||
self.host = host
|
||||
self.port = port
|
||||
try:
|
||||
self.dest_pub = dest_pub or base64.b32decode(
|
||||
re.search(r'(.*)\.onion', host).groups()[0], True)
|
||||
except (AttributeError, binascii.Error) as e:
|
||||
raise ValueError('Malformed hostname') from e
|
||||
|
||||
def __repr__(self):
|
||||
return 'onion_peer, stream: {}, host: {}, port {}'.format(
|
||||
self.stream, self.host, self.port)
|
||||
|
||||
def to_object(self):
|
||||
payload = b''
|
||||
payload += VarInt(self.port).to_bytes()
|
||||
payload += b'\xfd\x87\xd8\x7e\xeb\x43'
|
||||
payload += self.dest_pub
|
||||
return Object(
|
||||
b'\x00' * 8, int(time.time() + 8 * 3600), shared.onion_obj_type,
|
||||
shared.onion_obj_version, self.stream, payload)
|
||||
|
||||
@classmethod
|
||||
def from_object(cls, obj):
|
||||
payload = obj.object_payload
|
||||
port_length = VarInt.length(payload[0])
|
||||
port = VarInt.from_bytes(payload[:port_length]).n
|
||||
if payload[port_length:port_length + 6] != b'\xfd\x87\xd8\x7e\xeb\x43':
|
||||
raise ValueError('Malformed onion peer object')
|
||||
dest_pub = payload[port_length + 6:]
|
||||
host = base64.b32encode(dest_pub).lower().decode() + '.onion'
|
||||
return cls(host, port, obj.stream_number, dest_pub)
|
||||
|
|
7
minode/tests/common.py
Normal file
7
minode/tests/common.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import socket
|
||||
|
||||
try:
|
||||
socket.socket().bind(('127.0.0.1', 9050))
|
||||
tor_port_free = True
|
||||
except (OSError, socket.error):
|
||||
tor_port_free = False
|
|
@ -13,6 +13,8 @@ import psutil
|
|||
from minode.i2p import util
|
||||
from minode.structure import NetAddrNoPrefix
|
||||
|
||||
from .common import tor_port_free
|
||||
|
||||
try:
|
||||
socket.socket().bind(('127.0.0.1', 7656))
|
||||
i2p_port_free = True
|
||||
|
@ -185,3 +187,21 @@ class TestProcessI2P(TestProcess):
|
|||
class TestProcessNoI2P(TestProcessShutdown):
|
||||
"""Test minode process shutdown with --i2p and no IP"""
|
||||
_process_cmd = ['minode', '--i2p', '--no-ip']
|
||||
|
||||
|
||||
@unittest.skipIf(tor_port_free, 'No running tor detected')
|
||||
class TestProcessTor(TestProcessProto):
|
||||
"""A test case for minode process running with tor enabled"""
|
||||
_process_cmd = ['minode', '--tor']
|
||||
_wait_time = 60
|
||||
|
||||
def test_connections(self):
|
||||
"""Check minode process connections"""
|
||||
for _ in range(self._wait_time):
|
||||
time.sleep(0.5)
|
||||
connections = self.connections()
|
||||
for c in connections:
|
||||
self.assertEqual(c.raddr[0], '127.0.0.1')
|
||||
self.assertEqual(c.raddr[1], 9050)
|
||||
if len(connections) > self._connection_limit / 2:
|
||||
break
|
||||
|
|
|
@ -23,6 +23,9 @@ sample_addr_data = unhexlify(
|
|||
sample_object_data = unhexlify(
|
||||
'000000000000000000000000652724030000002a010248454c4c4f')
|
||||
|
||||
sample_onion_host = \
|
||||
'bmtestlmgmvpbsg7kzmrxu47chs3cdou2tj4t5iloocgujzsf3e7rbqd.onion'
|
||||
|
||||
logging.basicConfig(
|
||||
level=shared.log_level,
|
||||
format='[%(asctime)s] [%(levelname)s] %(message)s')
|
||||
|
@ -192,3 +195,21 @@ class TestStructure(unittest.TestCase):
|
|||
nonce, obj.expires_time, obj.object_type, obj.version,
|
||||
obj.stream_number, obj.object_payload)
|
||||
self.assertTrue(obj.is_valid())
|
||||
|
||||
def test_onion_peer(self):
|
||||
"""Make an onion peer object and decode it back"""
|
||||
with self.assertRaises(ValueError):
|
||||
onion_peer = structure.OnionPeer('testing2')
|
||||
with self.assertRaises(ValueError):
|
||||
onion_peer = structure.OnionPeer('testing.onion')
|
||||
onion_peer = structure.OnionPeer(sample_onion_host)
|
||||
self.assertEqual(onion_peer.stream, shared.stream)
|
||||
obj = onion_peer.to_object()
|
||||
self.assertEqual(obj.object_type, shared.onion_obj_type)
|
||||
self.assertEqual(obj.version, shared.onion_obj_version)
|
||||
decoded = structure.OnionPeer.from_object(obj)
|
||||
self.assertEqual(decoded.dest_pub, onion_peer.dest_pub)
|
||||
self.assertEqual(decoded.port, onion_peer.port)
|
||||
obj.object_payload = obj.object_payload[0:1] + obj.object_payload[2:]
|
||||
with self.assertRaises(ValueError):
|
||||
structure.OnionPeer.from_object(obj)
|
||||
|
|
64
minode/tests/test_tor.py
Normal file
64
minode/tests/test_tor.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""Tests for tor module"""
|
||||
import collections
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from minode import shared
|
||||
|
||||
from .common import tor_port_free
|
||||
|
||||
try:
|
||||
from minode import tor
|
||||
except ImportError:
|
||||
tor = None
|
||||
|
||||
|
||||
Proxy = collections.namedtuple('Proxy', ['hostname', 'port'])
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
tor_port_free or tor is None, 'Inapropriate environment for tor service')
|
||||
class TestTor(unittest.TestCase):
|
||||
"""A test case running the tor service"""
|
||||
tor = None
|
||||
_files = ['onion_dest_priv.key', 'onion_dest.pub']
|
||||
|
||||
@classmethod
|
||||
def cleanup(cls):
|
||||
"""Remove used files"""
|
||||
for f in cls._files:
|
||||
try:
|
||||
os.remove(os.path.join(shared.data_directory, f))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
shared.data_directory = tempfile.gettempdir()
|
||||
shared.socks_proxy = Proxy('127.0.0.1', 9050)
|
||||
cls.cleanup()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.tor:
|
||||
cls.tor.close()
|
||||
cls.cleanup()
|
||||
|
||||
def test_tor(self):
|
||||
"""Start the tor service as in main and check the environment"""
|
||||
self.tor = tor.start_tor_service()
|
||||
if not self.tor:
|
||||
self.fail('The tor service has hot started.')
|
||||
|
||||
with open(
|
||||
os.path.join(shared.data_directory, 'onion_dest.pub'),
|
||||
'r', encoding='ascii'
|
||||
) as key_file:
|
||||
onion_key = key_file.read()
|
||||
self.assertEqual(onion_key + '.onion', shared.onion_hostname)
|
||||
|
||||
# with open(
|
||||
# os.path.join(shared.data_directory, 'onion_dest_priv.key'), 'rb'
|
||||
# ) as key_file:
|
||||
# private_key = key_file.read()
|
147
minode/tor.py
Normal file
147
minode/tor.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
"""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
|
||||
|
||||
if os.getuid() == 0:
|
||||
logging.info('Tor is not going to start as root')
|
||||
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'))
|
||||
port = str(shared.socks_proxy.port)
|
||||
tor_config = {
|
||||
'SocksPort': port,
|
||||
'ControlSocket': control_socket}
|
||||
|
||||
for attempt in range(20):
|
||||
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=90,
|
||||
init_msg_handler=logwrite)
|
||||
except OSError:
|
||||
if not attempt:
|
||||
if not shared.listen_for_connections:
|
||||
return
|
||||
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
|
||||
|
||||
if not shared.listen_for_connections:
|
||||
return True
|
||||
|
||||
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 controller
|
||||
|
||||
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 controller
|
|
@ -1,2 +1,4 @@
|
|||
coverage
|
||||
psutil
|
||||
PySocks
|
||||
stem
|
||||
|
|
1
setup.py
1
setup.py
|
@ -24,6 +24,7 @@ setup(
|
|||
packages=find_packages(exclude=('*tests',)),
|
||||
package_data={'': ['*.csv', 'tls/*.pem']},
|
||||
entry_points={'console_scripts': ['minode = minode.main:main']},
|
||||
extras_require={'proxy': ['PySocks'], 'tor': ['PySocks', 'stem>1.8.0']},
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License"
|
||||
"Operating System :: OS Independent",
|
||||
|
|
Loading…
Reference in New Issue
Block a user