2023-08-23 01:42:34 +02:00
|
|
|
"""Tests for network connections"""
|
2024-07-17 11:57:05 +02:00
|
|
|
import ipaddress
|
2023-08-23 01:42:34 +02:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import unittest
|
|
|
|
import tempfile
|
|
|
|
import time
|
2023-08-26 18:13:05 +02:00
|
|
|
from contextlib import contextmanager
|
2023-08-23 01:42:34 +02:00
|
|
|
|
|
|
|
from minode import connection, main, shared
|
2023-08-28 01:02:54 +02:00
|
|
|
from minode.listener import Listener
|
2023-08-23 01:42:34 +02:00
|
|
|
from minode.manager import Manager
|
|
|
|
|
2023-08-28 01:02:54 +02:00
|
|
|
from .test_process import TestProcessProto
|
|
|
|
|
2023-08-23 01:42:34 +02:00
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
level=logging.INFO,
|
|
|
|
format='[%(asctime)s] [%(levelname)s] %(message)s')
|
|
|
|
|
|
|
|
|
2023-08-26 18:13:05 +02:00
|
|
|
@contextmanager
|
|
|
|
def time_offset(offset):
|
|
|
|
"""
|
|
|
|
Replace time.time() by a mock returning a constant value
|
|
|
|
with given offset from current time.
|
|
|
|
"""
|
|
|
|
started = time.time()
|
|
|
|
time_call = time.time
|
|
|
|
try:
|
|
|
|
time.time = lambda: started + offset
|
|
|
|
yield time_call
|
|
|
|
finally:
|
|
|
|
time.time = time_call
|
|
|
|
|
|
|
|
|
2023-08-28 01:02:54 +02:00
|
|
|
@contextmanager
|
|
|
|
def run_listener(host='localhost', port=8444):
|
|
|
|
"""
|
|
|
|
Run the Listener with zero connection limit and
|
|
|
|
reset variables in shared after its stop.
|
|
|
|
"""
|
|
|
|
connection_limit = shared.connection_limit
|
|
|
|
shared.connection_limit = 0
|
|
|
|
try:
|
|
|
|
listener = Listener(host, port)
|
|
|
|
listener.start()
|
|
|
|
yield listener
|
|
|
|
except OSError:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
shared.connection_limit = connection_limit
|
|
|
|
shared.connections.clear()
|
|
|
|
shared.shutting_down = True
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
2023-08-23 01:42:34 +02:00
|
|
|
class TestNetwork(unittest.TestCase):
|
|
|
|
"""Test case starting connections"""
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
shared.data_directory = tempfile.gettempdir()
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
shared.core_nodes.clear()
|
|
|
|
shared.unchecked_node_pool.clear()
|
|
|
|
shared.objects = {}
|
|
|
|
try:
|
|
|
|
os.remove(os.path.join(shared.data_directory, 'objects.pickle'))
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
|
2023-08-26 18:13:05 +02:00
|
|
|
def _make_initial_nodes(self):
|
2023-08-23 01:42:34 +02:00
|
|
|
Manager.load_data()
|
2023-08-20 05:48:05 +02:00
|
|
|
core_nodes_len = len(shared.core_nodes)
|
|
|
|
self.assertGreaterEqual(core_nodes_len, 3)
|
2023-08-23 01:42:34 +02:00
|
|
|
|
|
|
|
main.bootstrap_from_dns()
|
2023-08-20 05:48:05 +02:00
|
|
|
self.assertGreaterEqual(len(shared.core_nodes), core_nodes_len)
|
2024-07-17 11:57:05 +02:00
|
|
|
for host, _ in shared.core_nodes:
|
|
|
|
try:
|
|
|
|
ipaddress.IPv4Address(host)
|
|
|
|
except ipaddress.AddressValueError:
|
|
|
|
try:
|
|
|
|
ipaddress.IPv6Address(host)
|
|
|
|
except ipaddress.AddressValueError:
|
|
|
|
self.fail('Found not an IP address in the core nodes')
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.fail('No IPv6 address found in the core nodes')
|
2023-08-20 05:48:05 +02:00
|
|
|
|
|
|
|
def test_bootstrap(self):
|
|
|
|
"""Start bootstrappers and check node pool"""
|
|
|
|
if shared.core_nodes:
|
|
|
|
shared.core_nodes = set()
|
|
|
|
if shared.unchecked_node_pool:
|
|
|
|
shared.unchecked_node_pool = set()
|
|
|
|
|
|
|
|
self._make_initial_nodes()
|
|
|
|
self.assertEqual(len(shared.unchecked_node_pool), 0)
|
|
|
|
|
|
|
|
for node in shared.core_nodes:
|
|
|
|
c = connection.Bootstrapper(*node)
|
|
|
|
c.start()
|
|
|
|
c.join()
|
|
|
|
if len(shared.unchecked_node_pool) > 2:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.fail(
|
|
|
|
'Failed to find at least 3 nodes'
|
|
|
|
' after running %s bootstrappers' % len(shared.core_nodes))
|
2023-08-23 01:42:34 +02:00
|
|
|
|
2023-08-26 18:13:05 +02:00
|
|
|
def test_connection(self):
|
|
|
|
"""Check a normal connection - should receive objects"""
|
|
|
|
self._make_initial_nodes()
|
|
|
|
|
2023-08-23 01:42:34 +02:00
|
|
|
started = time.time()
|
|
|
|
nodes = list(shared.core_nodes.union(shared.unchecked_node_pool))
|
|
|
|
random.shuffle(nodes)
|
|
|
|
|
|
|
|
for node in nodes:
|
|
|
|
# unknown = node not in shared.node_pool
|
|
|
|
# self.assertTrue(unknown)
|
|
|
|
unknown = True
|
|
|
|
shared.node_pool.discard(node)
|
|
|
|
|
|
|
|
c = connection.Connection(*node)
|
|
|
|
c.start()
|
|
|
|
connection_started = time.time()
|
2023-08-28 01:02:54 +02:00
|
|
|
while c.status not in ('disconnected', 'failed'):
|
2023-08-23 01:42:34 +02:00
|
|
|
# The addr of established connection is added to nodes pool
|
|
|
|
if unknown and c.status == 'fully_established':
|
|
|
|
unknown = False
|
|
|
|
self.assertIn(node, shared.node_pool)
|
|
|
|
if shared.objects or time.time() - connection_started > 90:
|
|
|
|
c.status = 'disconnecting'
|
|
|
|
if time.time() - started > 300:
|
|
|
|
c.status = 'disconnecting'
|
|
|
|
self.fail('Failed to receive an object in %s sec' % 300)
|
|
|
|
time.sleep(0.2)
|
|
|
|
if shared.objects: # got some objects
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.fail('Failed to establish a proper connection')
|
2023-08-26 18:13:05 +02:00
|
|
|
|
|
|
|
def test_time_offset(self):
|
|
|
|
"""Assert the network bans for large time offset"""
|
|
|
|
def try_connect(nodes, timeout, call):
|
|
|
|
started = call()
|
|
|
|
for node in nodes:
|
|
|
|
c = connection.Connection(*node)
|
|
|
|
c.start()
|
|
|
|
while call() < started + timeout:
|
|
|
|
if c.status == 'fully_established':
|
|
|
|
return 'Established a connection'
|
|
|
|
if c.status in ('disconnected', 'failed'):
|
|
|
|
break
|
|
|
|
time.sleep(0.2)
|
|
|
|
else:
|
|
|
|
return 'Spent too much time trying to connect'
|
|
|
|
|
|
|
|
def time_offset_connections(nodes, offset):
|
|
|
|
"""Spoof time.time and open connections with given time offset"""
|
|
|
|
with time_offset(offset) as time_call:
|
|
|
|
result = try_connect(nodes, 200, time_call)
|
|
|
|
if result:
|
|
|
|
self.fail(result)
|
|
|
|
|
|
|
|
self._make_initial_nodes()
|
|
|
|
nodes = random.sample(
|
|
|
|
tuple(shared.core_nodes.union(shared.unchecked_node_pool)), 5)
|
|
|
|
|
|
|
|
time_offset_connections(nodes, 4000)
|
|
|
|
time_offset_connections(nodes, -4000)
|
2023-08-28 01:02:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
class TestListener(TestProcessProto):
|
|
|
|
"""A separate test case for Listener with a process with --trusted-peer"""
|
|
|
|
_process_cmd = ['minode', '--trusted-peer', '127.0.0.1']
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
shared.shutting_down = False
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tearDownClass(cls):
|
|
|
|
super().tearDownClass()
|
|
|
|
shared.shutting_down = False
|
|
|
|
|
|
|
|
def test_listener(self):
|
|
|
|
"""Start Listener and try to connect"""
|
|
|
|
with run_listener() as listener:
|
|
|
|
if not listener:
|
|
|
|
self.fail('Failed to start listener')
|
|
|
|
|
|
|
|
c = connection.Connection('127.0.0.1', 8444)
|
|
|
|
shared.connections.add(c)
|
|
|
|
|
|
|
|
for _ in range(30):
|
|
|
|
if len(shared.connections) > 1:
|
|
|
|
self.fail('The listener ignored connection limit')
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
|
|
|
shared.connection_limit = 2
|
|
|
|
c.start()
|
|
|
|
started = time.time()
|
|
|
|
while c.status not in ('disconnected', 'failed'):
|
|
|
|
if c.status == 'fully_established':
|
|
|
|
self.fail('Connected to itself')
|
|
|
|
if time.time() - started > 90:
|
|
|
|
c.status = 'disconnecting'
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
server = None
|
|
|
|
started = time.time()
|
|
|
|
while not server:
|
|
|
|
time.sleep(0.2)
|
|
|
|
if time.time() - started > 90:
|
|
|
|
self.fail('Failed to establish the connection')
|
|
|
|
for c in shared.connections:
|
|
|
|
if c.status == 'fully_established':
|
|
|
|
server = c
|
|
|
|
self.assertTrue(server.server)
|
|
|
|
|
|
|
|
while not self.process.connections():
|
|
|
|
time.sleep(0.2)
|
|
|
|
if time.time() - started > 90:
|
|
|
|
self.fail('Failed to connect to listener')
|
|
|
|
|
|
|
|
client = self.process.connections()[0]
|
|
|
|
self.assertEqual(client.raddr[0], '127.0.0.1')
|
|
|
|
self.assertEqual(client.raddr[1], 8444)
|
|
|
|
self.assertEqual(server.host, client.laddr[0])
|
|
|
|
# self.assertEqual(server.port, client.laddr[1])
|
|
|
|
server.status = 'disconnecting'
|
|
|
|
|
|
|
|
self.assertFalse(listener.is_alive())
|
2023-09-04 03:58:02 +02:00
|
|
|
|
|
|
|
def test_listener_timeoffset(self):
|
|
|
|
"""Run listener with a large time offset - shouldn't connect"""
|
|
|
|
with time_offset(4000):
|
|
|
|
with run_listener() as listener:
|
|
|
|
if not listener:
|
|
|
|
self.fail('Failed to start listener')
|
|
|
|
shared.connection_limit = 2
|
|
|
|
for _ in range(30):
|
|
|
|
for c in shared.connections:
|
|
|
|
if c.status == 'fully_established':
|
|
|
|
self.fail('Established a connection')
|
|
|
|
time.sleep(0.5)
|
2024-07-17 12:31:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
class TestBootstrapProcess(TestProcessProto):
|
|
|
|
"""A separate test case for bootstrapping with a minode process"""
|
|
|
|
_listen = True
|
|
|
|
_connection_limit = 24
|
|
|
|
|
|
|
|
def test_bootstrap(self):
|
|
|
|
"""Start a bootstrapper for the local process and check node pool"""
|
|
|
|
if shared.unchecked_node_pool:
|
|
|
|
shared.unchecked_node_pool = set()
|
|
|
|
|
|
|
|
started = time.time()
|
|
|
|
while not self.connections():
|
|
|
|
if time.time() - started > 60:
|
|
|
|
self.fail('Failed to establish a connection')
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
for _ in range(3):
|
|
|
|
c = connection.Bootstrapper('127.0.0.1', 8444)
|
|
|
|
c.start()
|
|
|
|
c.join()
|
|
|
|
if len(shared.unchecked_node_pool) > 2:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.fail(
|
|
|
|
'Failed to find at least 3 nodes'
|
|
|
|
' after 3 tries to bootstrap with the local process')
|