├── minode ├── __init__.py ├── i2p │ ├── __init__.py │ ├── util.py │ ├── dialer.py │ ├── listener.py │ └── controller.py ├── core_nodes.csv ├── tls │ ├── cert.pem │ └── key.pem ├── listener.py ├── shared.py ├── pow.py ├── advertiser.py ├── i2p_core_nodes.csv ├── structure.py ├── manager.py ├── message.py ├── main.py └── connection.py ├── start.sh ├── update.sh ├── i2p_bridge.sh ├── LICENSE ├── .gitignore └── README.md /minode/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minode/i2p/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 minode/main.py "$@" 3 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | git pull 4 | -------------------------------------------------------------------------------- /i2p_bridge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 minode/main.py --i2p --no-ip --host 127.0.0.1 "$@" -------------------------------------------------------------------------------- /minode/core_nodes.csv: -------------------------------------------------------------------------------- 1 | 89.36.218.202,8444 2 | 5.45.99.75,8444 3 | 158.222.217.190,8444 4 | 109.147.204.113,1195 5 | 92.78.49.39,8444 6 | 75.167.159.54,8444 7 | 85.180.139.241,8444 8 | 95.165.168.168,8444 9 | 78.55.217.23,8844 10 | 158.222.211.81,8080 11 | 95.247.239.194,8484 12 | 178.62.12.187,8448 13 | 24.188.198.204,8111 14 | 178.11.46.221,8444 15 | 45.76.138.146,8444 16 | 85.114.135.102,8444 17 | 194.44.30.145,8446 18 | 85.25.152.9,8444 19 | -------------------------------------------------------------------------------- /minode/i2p/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import hashlib 4 | 5 | 6 | def receive_line(s): 7 | data = b'' 8 | while b'\n' not in data: 9 | d = s.recv(4096) 10 | if not d: 11 | raise ConnectionResetError 12 | data += d 13 | data = data.splitlines() 14 | return data[0] 15 | 16 | 17 | def pub_from_priv(priv): 18 | priv = base64.b64decode(priv, altchars=b'-~') 19 | # 256 for public key + 128 for signing key + 3 for certificate header + value of bytes priv[385:387] 20 | pub = priv[:387 + int.from_bytes(priv[385:387], byteorder='big')] 21 | pub = base64.b64encode(pub, altchars=b'-~') 22 | return pub 23 | 24 | 25 | def b32_from_pub(pub): 26 | return base64.b32encode(hashlib.sha256(base64.b64decode(pub, b'-~')).digest()).replace(b"=", b"").lower() + b'.b32.i2p' 27 | -------------------------------------------------------------------------------- /minode/tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICWDCCAcGgAwIBAgIJAJs5yni/cDh5MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTEzMDk1NTU3WhcNMTUxMTE0MDk1NTU3WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 7 | gQCg8XkFpIAYsTSBealTubvu4dzpMnnAOwANG5K9TJeclG9O65cmKWpH8k3hNDif 8 | xagIAI8UanBsQo6SQrK1Iby2kz6DCKmySO1OwoNOOF0Ok31N+5aWsQvYF1wLbk2m 9 | Ti/CSLWBgL25ywCCiP3Mgr+krapT4TrfvF4gCchUdcxMQQIDAQABo1AwTjAdBgNV 10 | HQ4EFgQUWuFUJQC6zu6OTDgHZzhfZxsgJOMwHwYDVR0jBBgwFoAUWuFUJQC6zu6O 11 | TDgHZzhfZxsgJOMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAT1I/x 12 | GbsYAE4pM4sVQrcuz7jLwr3k5Zve0z4WKR41W17Nc44G3DyLbkTWYESLfAYsivkx 13 | tRRtYTtJm1qmTPtedXQJK+wJGNHCWRfwSB2CYwmO7+C2rYYzkFndN68kB6RJmyOr 14 | eCX+9vkbQqgh7KfiNquJxCfMSDfhA2RszU43jg== 15 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /minode/tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKDxeQWkgBixNIF5 3 | qVO5u+7h3OkyecA7AA0bkr1Ml5yUb07rlyYpakfyTeE0OJ/FqAgAjxRqcGxCjpJC 4 | srUhvLaTPoMIqbJI7U7Cg044XQ6TfU37lpaxC9gXXAtuTaZOL8JItYGAvbnLAIKI 5 | /cyCv6StqlPhOt+8XiAJyFR1zExBAgMBAAECgYEAmd2hpQpayMCJgQsOHhRgnoXi 6 | jDOMgIInj2CADmguPi0OqTXEoGBR0ozNdfNV+zGdbmESaSNFbcrHwP7xGQgzABlv 7 | 5ANLgBYrHnW/oFCCuw4Lj/CAAHRA4its+2wzf13BYoVitDiYBt3JMRqwLV03aHyy 8 | Oqhvt2nVicz85+HERj0CQQDMJAPUIyOQLO+BPC5MsuxyQFJgie0aB5swumxanOv4 9 | J8GIvulNEJMG/dq+h/x4paV2LGDlUAOsBUmjXfTPMQAHAkEAydQtYorqYqhFZWWD 10 | 3lUMAoa8pGb6BfNXUqxdH0H8fk6B7OxYPpvwm7ce1lD1Oje3/+rMnn8i6A1p9HUy 11 | l9wvdwJAdhxIUs7Z3qsBD8bgCuRixV/NyalDk5HfCnxyAKNWK8fkw9ehaEM0rhDm 12 | JOLNAojkiND4ZvS6iyasCmdsIwx4tQJAAV+eR3NmkPFQN5ZvRU4S3NmJ4xyISw4S 13 | 5A8kOxg53aovHCunlhV9l7GxVggLAzBp4iX46oM2+5lLxUwe4gWvlQJBAK0IR8bB 14 | 85bKZ+M/O8rbs9kQHjx6GCbbDxH+qbIKkNcvLUvMgwwIFKiwqX+Tedtu2xET0mQM 15 | 9tEE5eMBOJ8GrxQ= 16 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Krzysztof Oziomek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /minode/listener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import socket 4 | import threading 5 | 6 | from connection import Connection 7 | import shared 8 | 9 | 10 | class Listener(threading.Thread): 11 | def __init__(self, host, port, family=socket.AF_INET): 12 | super().__init__(name='Listener') 13 | self.host = host 14 | self.port = port 15 | self.family = family 16 | self.s = socket.socket(self.family, socket.SOCK_STREAM) 17 | self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 18 | self.s.bind((self.host, self.port)) 19 | self.s.listen(1) 20 | self.s.settimeout(1) 21 | 22 | def run(self): 23 | while True: 24 | if shared.shutting_down: 25 | logging.debug('Shutting down Listener') 26 | break 27 | try: 28 | conn, addr = self.s.accept() 29 | logging.info('Incoming connection from: {}:{}'.format(addr[0], addr[1])) 30 | with shared.connections_lock: 31 | if len(shared.connections) > shared.connection_limit: 32 | conn.close() 33 | else: 34 | c = Connection(addr[0], addr[1], conn, 'ip', True) 35 | c.start() 36 | shared.connections.add(c) 37 | except socket.timeout: 38 | pass 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | minode_data/ 92 | /.idea/ 93 | -------------------------------------------------------------------------------- /minode/shared.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import queue 5 | import threading 6 | 7 | listening_port = 8444 8 | listening_host = '' 9 | send_outgoing_connections = True 10 | listen_for_connections = True 11 | data_directory = 'minode_data/' 12 | source_directory = os.path.dirname(os.path.realpath(__file__)) 13 | trusted_peer = None 14 | ip_enabled = True 15 | 16 | log_level = logging.INFO 17 | 18 | magic_bytes = b'\xe9\xbe\xb4\xd9' 19 | protocol_version = 3 20 | services = 3 # NODE_NETWORK, NODE_SSL 21 | stream = 1 22 | nonce = os.urandom(8) 23 | user_agent = b'/MiNode:0.3.0/' 24 | timeout = 600 25 | header_length = 24 26 | i2p_dest_obj_type = 0x493250 27 | i2p_dest_obj_version = 1 28 | 29 | i2p_enabled = False 30 | i2p_transient = False 31 | i2p_sam_host = '127.0.0.1' 32 | i2p_sam_port = 7656 33 | i2p_tunnel_length = 2 34 | i2p_session_nick = b'' 35 | i2p_dest_pub = b'' 36 | 37 | nonce_trials_per_byte = 1000 38 | payload_length_extra_bytes = 1000 39 | 40 | shutting_down = False 41 | 42 | vector_advertise_queue = queue.Queue() 43 | address_advertise_queue = queue.Queue() 44 | 45 | connections = set() 46 | connections_lock = threading.Lock() 47 | 48 | i2p_dialers = set() 49 | 50 | hosts = set() 51 | 52 | core_nodes = set() 53 | 54 | node_pool = set() 55 | unchecked_node_pool = set() 56 | 57 | i2p_core_nodes = set() 58 | i2p_node_pool = set() 59 | i2p_unchecked_node_pool = set() 60 | 61 | outgoing_connections = 8 62 | connection_limit = 250 63 | 64 | objects = {} 65 | objects_lock = threading.Lock() 66 | -------------------------------------------------------------------------------- /minode/pow.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import logging 4 | import multiprocessing 5 | import shared 6 | import struct 7 | import threading 8 | import time 9 | 10 | import structure 11 | 12 | 13 | def _pow_worker(target, initial_hash, q): 14 | nonce = 0 15 | logging.debug("target: {}, initial_hash: {}".format(target, base64.b16encode(initial_hash).decode())) 16 | trial_value = target + 1 17 | 18 | while trial_value > target: 19 | nonce += 1 20 | trial_value = struct.unpack('>Q', hashlib.sha512(hashlib.sha512(struct.pack('>Q', nonce) + initial_hash).digest()).digest()[:8])[0] 21 | 22 | q.put(struct.pack('>Q', nonce)) 23 | 24 | 25 | def _worker(obj): 26 | q = multiprocessing.Queue() 27 | p = multiprocessing.Process(target=_pow_worker, args=(obj.pow_target(), obj.pow_initial_hash(), q)) 28 | 29 | logging.debug("Starting POW process") 30 | t = time.time() 31 | p.start() 32 | nonce = q.get() 33 | p.join() 34 | 35 | logging.debug("Finished doing POW, nonce: {}, time: {}s".format(nonce, time.time() - t)) 36 | obj = structure.Object(nonce, obj.expires_time, obj.object_type, obj.version, obj.stream_number, obj.object_payload) 37 | logging.debug("Object vector is {}".format(base64.b16encode(obj.vector).decode())) 38 | 39 | with shared.objects_lock: 40 | shared.objects[obj.vector] = obj 41 | shared.vector_advertise_queue.put(obj.vector) 42 | 43 | 44 | def do_pow_and_publish(obj): 45 | t = threading.Thread(target=_worker, args=(obj, )) 46 | t.start() 47 | -------------------------------------------------------------------------------- /minode/advertiser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import time 4 | 5 | import message 6 | import shared 7 | 8 | 9 | class Advertiser(threading.Thread): 10 | def __init__(self): 11 | super().__init__(name='Advertiser') 12 | 13 | def run(self): 14 | while True: 15 | time.sleep(0.4) 16 | if shared.shutting_down: 17 | logging.debug('Shutting down Advertiser') 18 | break 19 | self._advertise_vectors() 20 | self._advertise_addresses() 21 | 22 | @staticmethod 23 | def _advertise_vectors(): 24 | vectors_to_advertise = set() 25 | while not shared.vector_advertise_queue.empty(): 26 | vectors_to_advertise.add(shared.vector_advertise_queue.get()) 27 | if len(vectors_to_advertise) > 0: 28 | for c in shared.connections.copy(): 29 | if c.status == 'fully_established': 30 | c.send_queue.put(message.Inv(vectors_to_advertise)) 31 | 32 | @staticmethod 33 | def _advertise_addresses(): 34 | addresses_to_advertise = set() 35 | while not shared.address_advertise_queue.empty(): 36 | addr = shared.address_advertise_queue.get() 37 | if addr.port == 'i2p': 38 | # We should not try to construct Addr messages with I2P destinations (yet) 39 | continue 40 | addresses_to_advertise.add(addr) 41 | if len(addresses_to_advertise) > 0: 42 | for c in shared.connections.copy(): 43 | if c.status == 'fully_established': 44 | c.send_queue.put(message.Addr(addresses_to_advertise)) 45 | -------------------------------------------------------------------------------- /minode/i2p/dialer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import socket 4 | import threading 5 | 6 | import shared 7 | from connection import Connection 8 | from i2p.util import receive_line 9 | 10 | 11 | class I2PDialer(threading.Thread): 12 | def __init__(self, destination, nick, sam_host='127.0.0.1', sam_port=7656): 13 | self.sam_host = sam_host 14 | self.sam_port = sam_port 15 | 16 | self.nick = nick 17 | self.destination = destination 18 | 19 | super().__init__(name='I2P Dial to {}'.format(self.destination)) 20 | 21 | self.s = socket.create_connection((self.sam_host, self.sam_port)) 22 | 23 | self.version_reply = [] 24 | self.success = True 25 | 26 | def run(self): 27 | logging.debug('Connecting to {}'.format(self.destination)) 28 | self._connect() 29 | if not shared.shutting_down and self.success: 30 | c = Connection(self.destination, 'i2p', self.s, 'i2p', False, self.destination) 31 | c.start() 32 | shared.connections.add(c) 33 | 34 | def _receive_line(self): 35 | line = receive_line(self.s) 36 | # logging.debug('I2PDialer <- ' + str(line)) 37 | return line 38 | 39 | def _send(self, command): 40 | # logging.debug('I2PDialer -> ' + str(command)) 41 | self.s.sendall(command) 42 | 43 | def _connect(self): 44 | self._send(b'HELLO VERSION MIN=3.0 MAX=3.3\n') 45 | self.version_reply = self._receive_line().split() 46 | if b'RESULT=OK' not in self.version_reply: 47 | logging.warning('Error while connecting to {}'.format(self.destination)) 48 | self.success = False 49 | 50 | self._send(b'STREAM CONNECT ID=' + self.nick + b' DESTINATION=' + self.destination + b'\n') 51 | reply = self._receive_line().split(b' ') 52 | if b'RESULT=OK' not in reply: 53 | logging.warning('Error while connecting to {}'.format(self.destination)) 54 | self.success = False 55 | -------------------------------------------------------------------------------- /minode/i2p/listener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import socket 4 | import threading 5 | 6 | from connection import Connection 7 | from i2p.util import receive_line 8 | import shared 9 | 10 | 11 | class I2PListener(threading.Thread): 12 | def __init__(self, nick, host='127.0.0.1', port=7656): 13 | super().__init__(name='I2P Listener') 14 | 15 | self.host = host 16 | self.port = port 17 | self.nick = nick 18 | 19 | self.s = None 20 | 21 | self.version_reply = [] 22 | 23 | self.new_socket() 24 | 25 | def _receive_line(self): 26 | line = receive_line(self.s) 27 | # logging.debug('I2PListener <- ' + str(line)) 28 | return line 29 | 30 | def _send(self, command): 31 | # logging.debug('I2PListener -> ' + str(command)) 32 | self.s.sendall(command) 33 | 34 | def new_socket(self): 35 | self.s = socket.create_connection((self.host, self.port)) 36 | self._send(b'HELLO VERSION MIN=3.0 MAX=3.3\n') 37 | self.version_reply = self._receive_line().split() 38 | assert b'RESULT=OK' in self.version_reply 39 | 40 | self._send(b'STREAM ACCEPT ID=' + self.nick + b'\n') 41 | reply = self._receive_line().split(b' ') 42 | assert b'RESULT=OK' in reply 43 | 44 | self.s.settimeout(1) 45 | 46 | def run(self): 47 | while not shared.shutting_down: 48 | try: 49 | destination = self._receive_line().split()[0] 50 | logging.info('Incoming I2P connection from: {}'.format(destination.decode())) 51 | 52 | hosts = set() 53 | for c in shared.connections.copy(): 54 | hosts.add(c.host) 55 | for d in shared.i2p_dialers.copy(): 56 | hosts.add(d.destination) 57 | if destination in hosts: 58 | logging.debug('Rejecting duplicate I2P connection.') 59 | self.s.close() 60 | else: 61 | c = Connection(destination, 'i2p', self.s, 'i2p', True, destination) 62 | c.start() 63 | shared.connections.add(c) 64 | self.new_socket() 65 | except socket.timeout: 66 | pass 67 | logging.debug('Shutting down I2P Listener') 68 | -------------------------------------------------------------------------------- /minode/i2p_core_nodes.csv: -------------------------------------------------------------------------------- 1 | IPHBFm1bfQ9HrUkq07aomTAGn~W1wChE53xprAqIftsF18cuoUCJbMYhdJl~pljhvAXHKDSePdsSWecg8yP3st0Ib0h429XaOdrxpoFJ6MI1ofkg-KFtnZ6sX~Yp5GD-z-Nqdu6H0YBlf~y18ToOT6vTUvyE5Jsb105LmRMUAP0pDon4-da9r2wD~rxGOuvkrT83CftfxAIIT1z3M6ouAFI3UBq-guEyiZszM-01yQ-IgVBXsvnou8DXrlysYeeaimL6LoLhJgTnXIDfHCfUsHbgYK0JvRdimu-eMs~BRTT7-o4N5RJjVDfsS4CUHa6JwuWYg3JNSfaJoGFlM2xeGjNSJUs5e7PkcXeqCTKZQERbdIJcFz~rGcTfvc-OfXjMf6VfU2XORKcYiA21zkHMOkQvmE1dATP8VpQTKcYYZrQrRAc5Wxn7ayf9Gdwtq0EZXeydZv36RVJ03E4CZUGQMxXOFGUXwLFXQ9QCbsbXSoukd3rAGoPgE~GboO1YJh3hAAAA 2 | lSf~Ut81pZVxDwteIXobR7G7qpnZz2eirvyKJgDFMYuOvXZVNA8bgA5qNhXR8lmOlCBCzKsfm-KIo0QndfRZhMYsFXHTxWBiF9SvPPF8c220l-c0s7cCFQwTdJ5UwZciOexsvNxBLv~1GN2DdMEgEJeUVmLvIaynzQuNgWMvr9AVo6rox2x88FZWT8kdZ2Nt0fiBm-UEd5TpZcK4q4U80t1giuqVeJPqWZjhBV-ctVLeGQC5fp~v3Ev2UeCH~43Z5olrFcCWsVL8vxc4wzMWrVZbi95f1gLW1kQ~sgV8fV~G4JanYLV5yRePC19i5VTkvtcq4HN2cEq~BKryP3AxR7m4Msqwe4gTeNEJE9D6jCmaGjZywujCTFUKo3eTbwCr7sMqZcZGzBPIBcK5syzJL05BMX16pP0ziXJmP-KZT1iEjdY3DIGHJ~mfa2lPNxuwceERnHv3BDyNvo0S0AbEQuJdqmdwFWWHL6mfo9uNYaShMYQmy38O3t6nZP9YOO~YBQAEAAcAAA== 3 | 71w249Tlf0-ImX3S5zWA3RC8zfZY7dBY8C-3Bll4yHqJT7YZwmR0u2dxe9Xi7gFN8Q0mhUBtjuNJPwL8XjiwbOPeTGytc5xazmlxI~HcZoY32YBSiushex0sr0VnJOEaa~UduhNRY7X12atmhoSOXWKJ-R9YebWQy9eDvLZinX~ytFCttl9xpxyo5WvqrC6~KTMHjA2xHALhSp5FhLf9fRWCCPl7GRY-7kHuWpa7WVs~P2pe~Uux0RTbJzLH8gwmtp-jTgtisef7mr7l9e~MQ3k3dAO0i~ifXhoBSWrSRUgprwjpmlfm3O85tBSLMsgSJK-TFxtlo-iBCMLaJSZamurOx52uX-QO7WlWaX~S93ELXMd1B0wS~PKtomyRt7i67IFDfPde0FvF~YBxEKIgy3N8Sw4S4W3AqZBIaFMM8-q5x9R08t6jOA5WbdUq7IVjSimecM~AKVNyYCShoxn3pe10bj4Xmc~sU7sDYhTI7bmR~WYYaY1NVMlBrm-Fao4MBQAEAAcAAA== 4 | BAPHJe7Gxr4bwrmJrPFKUSvxuFXqGxbno1NunoKccVklLqWxcfjrpBjVFp8OSKooc-89t-N5syP61YWw2DcL~gm0Z7NOtw9hFaPIa2ooS3an8FhnpX7~2etA-oxG1y-3pKMAMu--b443EHV3u1qaBR33azE5-GQn1qvKjoLGWi-nOZk2ogeF0o2pn6VVG-sXrlQByAF7TsolqrDOPI3P3RaRg09UK06y8CASHIr4yKsmrajO1~hnjpfrqwK1N0Y5DSCs4RXAlM5RPxf4mw116-hCGIJN30xnW2BSOaHHuA5U7Oaj-wbTdXICFpYi0M5sigogMeE1wUNB-vNcpc-hcl44BgBf-JOnWOsFxHTy2TGxkR31ockdoKjeEPOm34wVV2wNxCSGHzO25yW9PaoNdjKeplbQP~PrrRrlrTDXWEL7ZxqF6QOlCSO1UFV8HaPtqTMC13JMpX4PeAkeChtTgGQ1Pw2cQWFDBUwi2Z3x8na5MK8QNdIO2p4VYmJmZ8wgBQAEAAcAAA== 5 | kSDy6eL1pVybvIz6yYzlwz7nqBSgjPQV-YiRHygWFl8r1s60p1vSSfurkPqn8JSaopV~zgQpt5CMK8XxOv9L4NP0cfTAfvY3wCKXb~BbuBGXAXcdh-oSAQ65nXP3rpJ7g-TcdUbqYOhJVeKskHRu6Uv~ZTQyEM23rlI638bWNImy5f9bGw9ff-Fb5xj5IYjNNTWYvXmnB2GvP4TZZMRubGBauByWDDfPVg~0et7UOd7RwcnxTfpygKX41EZOJc05G6A4uBgMJjWQK6RjRa30YJ9M4nwR2xUYLb9y6IAaOZEc0khKjYbUp8KxcaG7spqnMogJR~xgWhRn~lV7b9PVsDL-0vDRuIunG5IGLU~pfviUCiv9H4mNiYft2GVvoJhRQLxWSlibJqmHzrXIE1741qX0NZkX9O9zI2gYG1Yw~t4xqdlVJYtBGMrBsRye0gBbImHhEcKo396yrz3~aZdqXiPNgisamx9tj485RgyO-JCp7NJ6WQ4cZmg6DIlNv-JZAAAA 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiNode 2 | Python 3 implementation of the Bitmessage protocol. Designed only to route objects inside the network. 3 | 4 | ## Requirements 5 | - python3 (or pypy3) 6 | - openssl 7 | 8 | ## Running 9 | ``` 10 | git clone https://github.com/TheKysek/MiNode.git 11 | ``` 12 | ``` 13 | cd MiNode 14 | chmod +x start.sh 15 | ./start.sh 16 | ``` 17 | 18 | It is worth noting that the `start.sh` file MiNode no longer tries to do a `git pull` in order to update to the latest version. 19 | Is is now done by the `update.sh` file. 20 | 21 | ## Command line 22 | ``` 23 | usage: main.py [-h] [-p PORT] [--host HOST] [--debug] [--data-dir DATA_DIR] 24 | [--no-incoming] [--no-outgoing] [--no-ip] 25 | [--trusted-peer TRUSTED_PEER] 26 | [--connection-limit CONNECTION_LIMIT] [--i2p] 27 | [--i2p-tunnel-length I2P_TUNNEL_LENGTH] 28 | [--i2p-sam-host I2P_SAM_HOST] [--i2p-sam-port I2P_SAM_PORT] 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -p PORT, --port PORT Port to listen on 33 | --host HOST Listening host 34 | --debug Enable debug logging 35 | --data-dir DATA_DIR Path to data directory 36 | --no-incoming Do not listen for incoming connections 37 | --no-outgoing Do not send outgoing connections 38 | --no-ip Do not use IP network 39 | --trusted-peer TRUSTED_PEER 40 | Specify a trusted peer we should connect to 41 | --connection-limit CONNECTION_LIMIT 42 | Maximum number of connections 43 | --i2p Enable I2P support (uses SAMv3) 44 | --i2p-tunnel-length I2P_TUNNEL_LENGTH 45 | Length of I2P tunnels 46 | --i2p-sam-host I2P_SAM_HOST 47 | Host of I2P SAMv3 bridge 48 | --i2p-sam-port I2P_SAM_PORT 49 | Port of I2P SAMv3 bridge 50 | 51 | ``` 52 | ## I2P support 53 | MiNode has support for connections over I2P network. 54 | To use it it needs an I2P router with SAMv3 activated (both Java I2P and i2pd are supported). 55 | Keep in mind that I2P connections are slow and full synchronization may take a while. 56 | ### Examples 57 | Connect to both IP and I2P networks (SAM bridge on default host and port 127.0.0.1:7656) and set tunnel length to 3 (default is 2). 58 | ``` 59 | $ ./start.sh --i2p --i2p-tunnel-length 3 60 | ``` 61 | 62 | Connect only to I2P network and listen for IP connections only from local machine. 63 | ``` 64 | $ ./start.sh --i2p --no-ip --host 127.0.0.1 65 | ``` 66 | or 67 | ``` 68 | $ ./i2p_bridge.sh 69 | ``` 70 | If you add `trustedpeer = 127.0.0.1:8444` to `keys.dat` file in PyBitmessage it will allow you to use it anonymously over I2P with MiNode acting as a bridge. 71 | ## Contact 72 | - TheKysek: BM-2cVUMXVnQXmTJDmb7q1HUyEqkT92qjwGvJ 73 | 74 | ## Links 75 | - [Bitmessage project website](https://bitmessage.org) 76 | - [Protocol specification](https://bitmessage.org/wiki/Protocol_specification) 77 | -------------------------------------------------------------------------------- /minode/i2p/controller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import logging 4 | import os 5 | import socket 6 | import threading 7 | import time 8 | 9 | from i2p.util import receive_line, pub_from_priv 10 | import shared 11 | 12 | 13 | class I2PController(threading.Thread): 14 | def __init__(self, host='127.0.0.1', port=7656, dest_priv=b''): 15 | super().__init__(name='I2P Controller') 16 | 17 | self.host = host 18 | self.port = port 19 | self.nick = b'MiNode_' + base64.b16encode(os.urandom(4)).lower() 20 | 21 | while True: 22 | try: 23 | self.s = socket.create_connection((self.host, self.port)) 24 | break 25 | except ConnectionRefusedError: 26 | logging.error("Error while connecting to I2P SAM bridge. Retrying.") 27 | time.sleep(10) 28 | 29 | self.version_reply = [] 30 | 31 | self.init_connection() 32 | 33 | if dest_priv: 34 | self.dest_priv = dest_priv 35 | self.dest_pub = pub_from_priv(dest_priv) 36 | else: 37 | self.dest_priv = b'' 38 | self.dest_pub = b'' 39 | self.generate_destination() 40 | 41 | self.create_session() 42 | 43 | def _receive_line(self): 44 | line = receive_line(self.s) 45 | # logging.debug('I2PController <- ' + str(line)) 46 | return line 47 | 48 | def _send(self, command): 49 | # logging.debug('I2PController -> ' + str(command)) 50 | self.s.sendall(command) 51 | 52 | def init_connection(self): 53 | self._send(b'HELLO VERSION MIN=3.0 MAX=3.3\n') 54 | self.version_reply = self._receive_line().split() 55 | assert b'RESULT=OK' in self.version_reply 56 | 57 | def generate_destination(self): 58 | if b'VERSION=3.0' in self.version_reply: 59 | # We will now receive old DSA_SHA1 destination :( 60 | self._send(b'DEST GENERATE\n') 61 | else: 62 | self._send(b'DEST GENERATE SIGNATURE_TYPE=EdDSA_SHA512_Ed25519\n') 63 | 64 | reply = self._receive_line().split() 65 | for par in reply: 66 | if par.startswith(b'PUB='): 67 | self.dest_pub = par.replace(b'PUB=', b'') 68 | if par.startswith(b'PRIV='): 69 | self.dest_priv = par.replace(b'PRIV=', b'') 70 | assert self.dest_priv 71 | 72 | def create_session(self): 73 | self._send(b'SESSION CREATE STYLE=STREAM ID=' + self.nick + 74 | b' inbound.length=' + str(shared.i2p_tunnel_length).encode() + 75 | b' outbound.length=' + str(shared.i2p_tunnel_length).encode() + 76 | b' DESTINATION=' + self.dest_priv + b'\n') 77 | reply = self._receive_line().split() 78 | if b'RESULT=OK' not in reply: 79 | logging.warning(reply) 80 | logging.warning('We could not create I2P session, retrying in 5 seconds.') 81 | time.sleep(5) 82 | self.create_session() 83 | 84 | def run(self): 85 | self.s.settimeout(1) 86 | while True: 87 | if not shared.shutting_down: 88 | try: 89 | msg = self._receive_line().split(b' ') 90 | if msg[0] == b'PING': 91 | self._send(b'PONG ' + msg[1] + b'\n') 92 | except socket.timeout: 93 | pass 94 | else: 95 | logging.debug('Shutting down I2P Controller') 96 | self.s.close() 97 | break 98 | -------------------------------------------------------------------------------- /minode/structure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import hashlib 4 | import logging 5 | import struct 6 | import socket 7 | import time 8 | 9 | import shared 10 | 11 | 12 | class VarInt(object): 13 | def __init__(self, n): 14 | self.n = n 15 | 16 | def to_bytes(self): 17 | if self.n < 0xfd: 18 | return struct.pack('>B', self.n) 19 | 20 | if self.n <= 0xffff: 21 | return b'\xfd' + struct.pack('>H', self.n) 22 | 23 | if self.n <= 0xffffffff: 24 | return b'\xfe' + struct.pack('>I', self.n) 25 | 26 | return b'\xff' + struct.pack('>Q', self.n) 27 | 28 | @staticmethod 29 | def length(b): 30 | if b == 0xfd: 31 | return 3 32 | if b == 0xfe: 33 | return 5 34 | if b == 0xff: 35 | return 9 36 | return 1 37 | 38 | @classmethod 39 | def from_bytes(cls, b): 40 | if cls.length(b[0]) > 1: 41 | b = b[1:] 42 | n = int.from_bytes(b, 'big') 43 | return cls(n) 44 | 45 | 46 | class Object(object): 47 | def __init__(self, nonce, expires_time, object_type, version, stream_number, object_payload): 48 | self.nonce = nonce 49 | self.expires_time = expires_time 50 | self.object_type = object_type 51 | self.version = version 52 | self.stream_number = stream_number 53 | self.object_payload = object_payload 54 | self.vector = hashlib.sha512(hashlib.sha512(self.to_bytes()).digest()).digest()[:32] 55 | 56 | def __repr__(self): 57 | return 'object, vector: {}'.format(base64.b16encode(self.vector).decode()) 58 | 59 | @classmethod 60 | def from_message(cls, m): 61 | payload = m.payload 62 | nonce, expires_time, object_type = struct.unpack('>8sQL', payload[:20]) 63 | payload = payload[20:] 64 | version_varint_length = VarInt.length(payload[0]) 65 | version = VarInt.from_bytes(payload[:version_varint_length]).n 66 | payload = payload[version_varint_length:] 67 | stream_number_varint_length = VarInt.length(payload[0]) 68 | stream_number = VarInt.from_bytes(payload[:stream_number_varint_length]).n 69 | payload = payload[stream_number_varint_length:] 70 | return cls(nonce, expires_time, object_type, version, stream_number, payload) 71 | 72 | def to_bytes(self): 73 | payload = b'' 74 | payload += self.nonce 75 | payload += struct.pack('>QL', self.expires_time, self.object_type) 76 | payload += VarInt(self.version).to_bytes() + VarInt(self.stream_number).to_bytes() 77 | payload += self.object_payload 78 | return payload 79 | 80 | def is_expired(self): 81 | return self.expires_time + 3 * 3600 < time.time() 82 | 83 | def is_valid(self): 84 | if self.is_expired(): 85 | logging.debug('Invalid object {}, reason: expired'.format(base64.b16encode(self.vector).decode())) 86 | return False 87 | if self.expires_time > time.time() + 28 * 24 * 3600 + 3 * 3600: 88 | logging.warning('Invalid object {}, reason: end of life too far in the future'.format(base64.b16encode(self.vector).decode())) 89 | return False 90 | if len(self.object_payload) > 2**18: 91 | logging.warning('Invalid object {}, reason: payload is too long'.format(base64.b16encode(self.vector).decode())) 92 | return False 93 | if self.stream_number != 1: 94 | logging.warning('Invalid object {}, reason: not in stream 1'.format(base64.b16encode(self.vector).decode())) 95 | return False 96 | data = self.to_bytes()[8:] 97 | length = len(data) + 8 + shared.payload_length_extra_bytes 98 | dt = max(self.expires_time - time.time(), 0) 99 | h = hashlib.sha512(data).digest() 100 | pow_value = int.from_bytes(hashlib.sha512(hashlib.sha512(self.nonce + h).digest()).digest()[:8], 'big') 101 | target = self.pow_target() 102 | if target < pow_value: 103 | logging.warning('Invalid object {}, reason: insufficient pow'.format(base64.b16encode(self.vector).decode())) 104 | return False 105 | return True 106 | 107 | def pow_target(self): 108 | data = self.to_bytes()[8:] 109 | length = len(data) + 8 + shared.payload_length_extra_bytes 110 | dt = max(self.expires_time - time.time(), 0) 111 | return int(2 ** 64 / (shared.nonce_trials_per_byte * (length + (dt * length) / (2 ** 16)))) 112 | 113 | def pow_initial_hash(self): 114 | return hashlib.sha512(self.to_bytes()[8:]).digest() 115 | 116 | 117 | class NetAddrNoPrefix(object): 118 | def __init__(self, services, host, port): 119 | self.services = services 120 | self.host = host 121 | self.port = port 122 | 123 | def __repr__(self): 124 | return 'net_addr_no_prefix, services: {}, host: {}, port {}'.format(self.services, self.host, self.port) 125 | 126 | def to_bytes(self): 127 | b = b'' 128 | b += struct.pack('>Q', self.services) 129 | try: 130 | host = socket.inet_pton(socket.AF_INET, self.host) 131 | b += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + host 132 | except socket.error: 133 | b += socket.inet_pton(socket.AF_INET6, self.host) 134 | b += struct.pack('>H', int(self.port)) 135 | return b 136 | 137 | @classmethod 138 | def from_bytes(cls, b): 139 | services, host, port = struct.unpack('>Q16sH', b) 140 | if host.startswith(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF'): 141 | host = socket.inet_ntop(socket.AF_INET, host[-4:]) 142 | else: 143 | host = socket.inet_ntop(socket.AF_INET6, host) 144 | return cls(services, host, port) 145 | 146 | 147 | class NetAddr(object): 148 | def __init__(self, services, host, port, stream=shared.stream): 149 | self.stream = stream 150 | self.services = services 151 | self.host = host 152 | self.port = port 153 | 154 | def __repr__(self): 155 | return 'net_addr, stream: {}, services: {}, host: {}, port {}'\ 156 | .format(self.stream, self.services, self.host, self.port) 157 | 158 | def to_bytes(self): 159 | b = b'' 160 | b += struct.pack('>Q', int(time.time())) 161 | b += struct.pack('>I', self.stream) 162 | b += NetAddrNoPrefix(self.services, self.host, self.port).to_bytes() 163 | return b 164 | 165 | @classmethod 166 | def from_bytes(cls, b): 167 | t, stream, net_addr = struct.unpack('>QI26s', b) 168 | n = NetAddrNoPrefix.from_bytes(net_addr) 169 | return cls(n.services, n.host, n.port, stream) 170 | -------------------------------------------------------------------------------- /minode/manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import logging 4 | import pickle 5 | import queue 6 | import random 7 | import threading 8 | import time 9 | 10 | from connection import Connection 11 | from i2p.dialer import I2PDialer 12 | import pow 13 | import shared 14 | import structure 15 | 16 | 17 | class Manager(threading.Thread): 18 | def __init__(self): 19 | super().__init__(name='Manager') 20 | self.q = queue.Queue() 21 | self.last_cleaned_objects = time.time() 22 | self.last_cleaned_connections = time.time() 23 | self.last_pickled_objects = time.time() 24 | self.last_pickled_nodes = time.time() 25 | self.last_published_i2p_destination = time.time() - 50 * 60 + random.uniform(-1, 1) * 300 # Publish destination 5-15 minutes after start 26 | 27 | def run(self): 28 | while True: 29 | time.sleep(0.8) 30 | now = time.time() 31 | if shared.shutting_down: 32 | logging.debug('Shutting down Manager') 33 | break 34 | if now - self.last_cleaned_objects > 90: 35 | self.clean_objects() 36 | self.last_cleaned_objects = now 37 | if now - self.last_cleaned_connections > 2: 38 | self.manage_connections() 39 | self.last_cleaned_connections = now 40 | if now - self.last_pickled_objects > 100: 41 | self.pickle_objects() 42 | self.last_pickled_objects = now 43 | if now - self.last_pickled_nodes > 60: 44 | self.pickle_nodes() 45 | self.last_pickled_nodes = now 46 | if now - self.last_published_i2p_destination > 3600: 47 | self.publish_i2p_destination() 48 | self.last_published_i2p_destination = now 49 | 50 | @staticmethod 51 | def clean_objects(): 52 | for vector in set(shared.objects): 53 | if shared.objects[vector].is_expired(): 54 | with shared.objects_lock: 55 | del shared.objects[vector] 56 | logging.debug('Deleted expired object: {}'.format(base64.b16encode(vector).decode())) 57 | 58 | @staticmethod 59 | def manage_connections(): 60 | hosts = set() 61 | outgoing_connections = 0 62 | for c in shared.connections.copy(): 63 | if not c.is_alive() or c.status == 'disconnected': 64 | with shared.connections_lock: 65 | shared.connections.remove(c) 66 | else: 67 | hosts.add(c.host) 68 | if not c.server: 69 | outgoing_connections += 1 70 | 71 | for d in shared.i2p_dialers.copy(): 72 | hosts.add(d.destination) 73 | if not d.is_alive(): 74 | shared.i2p_dialers.remove(d) 75 | 76 | to_connect = set() 77 | if shared.trusted_peer: 78 | to_connect.add(shared.trusted_peer) 79 | 80 | if outgoing_connections < shared.outgoing_connections and shared.send_outgoing_connections and not shared.trusted_peer: 81 | 82 | if shared.ip_enabled: 83 | if len(shared.unchecked_node_pool) > 16: 84 | to_connect.update(random.sample(shared.unchecked_node_pool, 16)) 85 | else: 86 | to_connect.update(shared.unchecked_node_pool) 87 | shared.unchecked_node_pool.difference_update(to_connect) 88 | if len(shared.node_pool) > 8: 89 | to_connect.update(random.sample(shared.node_pool, 8)) 90 | else: 91 | to_connect.update(shared.node_pool) 92 | 93 | if shared.i2p_enabled: 94 | if len(shared.i2p_unchecked_node_pool) > 16: 95 | to_connect.update(random.sample(shared.i2p_unchecked_node_pool, 16)) 96 | else: 97 | to_connect.update(shared.i2p_unchecked_node_pool) 98 | shared.i2p_unchecked_node_pool.difference_update(to_connect) 99 | if len(shared.i2p_node_pool) > 8: 100 | to_connect.update(random.sample(shared.i2p_node_pool, 8)) 101 | else: 102 | to_connect.update(shared.i2p_node_pool) 103 | 104 | for addr in to_connect: 105 | if addr[0] in hosts: 106 | continue 107 | if addr[1] == 'i2p' and shared.i2p_enabled: 108 | if shared.i2p_session_nick and addr[0] != shared.i2p_dest_pub: 109 | try: 110 | d = I2PDialer(addr[0], shared.i2p_session_nick, shared.i2p_sam_host, shared.i2p_sam_port) 111 | d.start() 112 | hosts.add(d.destination) 113 | shared.i2p_dialers.add(d) 114 | except Exception as e: 115 | logging.warning('Exception while trying to establish an I2P connection') 116 | logging.warning(e) 117 | else: 118 | continue 119 | else: 120 | c = Connection(addr[0], addr[1]) 121 | c.start() 122 | hosts.add(c.host) 123 | with shared.connections_lock: 124 | shared.connections.add(c) 125 | shared.hosts = hosts 126 | 127 | @staticmethod 128 | def pickle_objects(): 129 | try: 130 | with open(shared.data_directory + 'objects.pickle', mode='bw') as file: 131 | with shared.objects_lock: 132 | pickle.dump(shared.objects, file, protocol=3) 133 | logging.debug('Saved objects') 134 | except Exception as e: 135 | logging.warning('Error while saving objects') 136 | logging.warning(e) 137 | 138 | @staticmethod 139 | def pickle_nodes(): 140 | if len(shared.node_pool) > 10000: 141 | shared.node_pool = set(random.sample(shared.node_pool, 10000)) 142 | if len(shared.unchecked_node_pool) > 1000: 143 | shared.unchecked_node_pool = set(random.sample(shared.unchecked_node_pool, 1000)) 144 | 145 | if len(shared.i2p_node_pool) > 1000: 146 | shared.i2p_node_pool = set(random.sample(shared.i2p_node_pool, 1000)) 147 | if len(shared.i2p_unchecked_node_pool) > 100: 148 | shared.i2p_unchecked_node_pool = set(random.sample(shared.i2p_unchecked_node_pool, 100)) 149 | 150 | try: 151 | with open(shared.data_directory + 'nodes.pickle', mode='bw') as file: 152 | pickle.dump(shared.node_pool, file, protocol=3) 153 | with open(shared.data_directory + 'i2p_nodes.pickle', mode='bw') as file: 154 | pickle.dump(shared.i2p_node_pool, file, protocol=3) 155 | logging.debug('Saved nodes') 156 | except Exception as e: 157 | logging.warning('Error while saving nodes') 158 | logging.warning(e) 159 | 160 | @staticmethod 161 | def publish_i2p_destination(): 162 | if shared.i2p_session_nick and not shared.i2p_transient: 163 | logging.info('Publishing our I2P destination') 164 | dest_pub_raw = base64.b64decode(shared.i2p_dest_pub, altchars=b'-~') 165 | obj = structure.Object(b'\x00' * 8, int(time.time() + 2 * 3600), shared.i2p_dest_obj_type, shared.i2p_dest_obj_version, 1, dest_pub_raw) 166 | pow.do_pow_and_publish(obj) 167 | -------------------------------------------------------------------------------- /minode/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import hashlib 4 | import struct 5 | import time 6 | 7 | import shared 8 | import structure 9 | 10 | 11 | class Header(object): 12 | def __init__(self, command, payload_length, payload_checksum): 13 | self.command = command 14 | self.payload_length = payload_length 15 | self.payload_checksum = payload_checksum 16 | 17 | def __repr__(self): 18 | return 'type: header, command: "{}", payload_length: {}, payload_checksum: {}'\ 19 | .format(self.command.decode(), self.payload_length, base64.b16encode(self.payload_checksum).decode()) 20 | 21 | def to_bytes(self): 22 | b = b'' 23 | b += shared.magic_bytes 24 | b += self.command.ljust(12, b'\x00') 25 | b += struct.pack('>L', self.payload_length) 26 | b += self.payload_checksum 27 | return b 28 | 29 | @classmethod 30 | def from_bytes(cls, b): 31 | magic_bytes, command, payload_length, payload_checksum = struct.unpack('>4s12sL4s', b) 32 | 33 | if magic_bytes != shared.magic_bytes: 34 | raise ValueError('magic_bytes do not match') 35 | 36 | command = command.rstrip(b'\x00') 37 | 38 | return cls(command, payload_length, payload_checksum) 39 | 40 | 41 | class Message(object): 42 | def __init__(self, command, payload): 43 | self.command = command 44 | self.payload = payload 45 | 46 | self.payload_length = len(payload) 47 | self.payload_checksum = hashlib.sha512(payload).digest()[:4] 48 | 49 | def __repr__(self): 50 | return '{}, payload_length: {}, payload_checksum: {}'\ 51 | .format(self.command.decode(), self.payload_length, base64.b16encode(self.payload_checksum).decode()) 52 | 53 | def to_bytes(self): 54 | b = Header(self.command, self.payload_length, self.payload_checksum).to_bytes() 55 | b += self.payload 56 | return b 57 | 58 | @classmethod 59 | def from_bytes(cls, b): 60 | h = Header.from_bytes(b[:24]) 61 | 62 | payload = b[24:] 63 | payload_length = len(payload) 64 | 65 | if payload_length != h.payload_length: 66 | raise ValueError('wrong payload length, expected {}, got {}'.format(h.payload_length, payload_length)) 67 | 68 | payload_checksum = hashlib.sha512(payload).digest()[:4] 69 | 70 | if payload_checksum != h.payload_checksum: 71 | raise ValueError('wrong payload checksum, expected {}, got {}'.format(h.payload_checksum, payload_checksum)) 72 | 73 | return cls(h.command, payload) 74 | 75 | 76 | class Version(object): 77 | def __init__(self, host, port, protocol_version=shared.protocol_version, services=shared.services, 78 | nonce=shared.nonce, user_agent=shared.user_agent): 79 | self.host = host 80 | self.port = port 81 | 82 | self.protocol_version = protocol_version 83 | self.services = services 84 | self.nonce = nonce 85 | self.user_agent = user_agent 86 | 87 | def __repr__(self): 88 | return 'version, protocol_version: {}, services: {}, host: {}, port: {}, nonce: {}, user_agent: {}'\ 89 | .format(self.protocol_version, self.services, self.host, self.port, base64.b16encode(self.nonce).decode(), self.user_agent) 90 | 91 | def to_bytes(self): 92 | payload = b'' 93 | payload += struct.pack('>I', self.protocol_version) 94 | payload += struct.pack('>Q', self.services) 95 | payload += struct.pack('>Q', int(time.time())) 96 | payload += structure.NetAddrNoPrefix(shared.services, self.host, self.port).to_bytes() 97 | payload += structure.NetAddrNoPrefix(shared.services, '127.0.0.1', 8444).to_bytes() 98 | payload += self.nonce 99 | payload += structure.VarInt(len(shared.user_agent)).to_bytes() 100 | payload += shared.user_agent 101 | payload += 2 * structure.VarInt(1).to_bytes() 102 | 103 | return Message(b'version', payload).to_bytes() 104 | 105 | @classmethod 106 | def from_bytes(cls, b): 107 | m = Message.from_bytes(b) 108 | 109 | payload = m.payload 110 | 111 | protocol_version, services, t, net_addr_remote, net_addr_local, nonce = \ 112 | struct.unpack('>IQQ26s26s8s', payload[:80]) 113 | 114 | net_addr_remote = structure.NetAddrNoPrefix.from_bytes(net_addr_remote) 115 | 116 | host = net_addr_remote.host 117 | port = net_addr_remote.port 118 | 119 | payload = payload[80:] 120 | 121 | user_agent_varint_length = structure.VarInt.length(payload[0]) 122 | user_agent_length = structure.VarInt.from_bytes(payload[:user_agent_varint_length]).n 123 | 124 | payload = payload[user_agent_varint_length:] 125 | 126 | user_agent = payload[:user_agent_length] 127 | 128 | payload = payload[user_agent_length:] 129 | 130 | if payload != b'\x01\x01': 131 | raise ValueError('message not for stream 1') 132 | 133 | return cls(host, port, protocol_version, services, nonce, user_agent) 134 | 135 | 136 | class Inv(object): 137 | def __init__(self, vectors): 138 | self.vectors = set(vectors) 139 | 140 | def __repr__(self): 141 | return 'inv, count: {}'.format(len(self.vectors)) 142 | 143 | def to_bytes(self): 144 | return Message(b'inv', structure.VarInt(len(self.vectors)).to_bytes() + b''.join(self.vectors)).to_bytes() 145 | 146 | @classmethod 147 | def from_message(cls, m): 148 | payload = m.payload 149 | 150 | vector_count_varint_length = structure.VarInt.length(payload[0]) 151 | vector_count = structure.VarInt.from_bytes(payload[:vector_count_varint_length]).n 152 | 153 | payload = payload[vector_count_varint_length:] 154 | 155 | vectors = set() 156 | 157 | while payload: 158 | vectors.add(payload[:32]) 159 | payload = payload[32:] 160 | 161 | if vector_count != len(vectors): 162 | raise ValueError('malformed Inv message, wrong vector_count') 163 | 164 | return cls(vectors) 165 | 166 | 167 | class GetData(object): 168 | def __init__(self, vectors): 169 | self.vectors = set(vectors) 170 | 171 | def __repr__(self): 172 | return 'getdata, count: {}'.format(len(self.vectors)) 173 | 174 | def to_bytes(self): 175 | return Message(b'getdata', structure.VarInt(len(self.vectors)).to_bytes() + b''.join(self.vectors)).to_bytes() 176 | 177 | @classmethod 178 | def from_message(cls, m): 179 | payload = m.payload 180 | 181 | vector_count_varint_length = structure.VarInt.length(payload[0]) 182 | vector_count = structure.VarInt.from_bytes(payload[:vector_count_varint_length]).n 183 | 184 | payload = payload[vector_count_varint_length:] 185 | 186 | vectors = set() 187 | 188 | while payload: 189 | vectors.add(payload[:32]) 190 | payload = payload[32:] 191 | 192 | if vector_count != len(vectors): 193 | raise ValueError('malformed GetData message, wrong vector_count') 194 | 195 | return cls(vectors) 196 | 197 | 198 | class Addr(object): 199 | def __init__(self, addresses): 200 | self.addresses = addresses 201 | 202 | def __repr__(self): 203 | return 'addr, count: {}'.format(len(self.addresses)) 204 | 205 | def to_bytes(self): 206 | return Message(b'addr', structure.VarInt(len(self.addresses)).to_bytes() + b''.join({addr.to_bytes() for addr in self.addresses})).to_bytes() 207 | 208 | @classmethod 209 | def from_message(cls, m): 210 | payload = m.payload 211 | 212 | addr_count_varint_length = structure.VarInt.length(payload[0]) 213 | addr_count = structure.VarInt.from_bytes(payload[:addr_count_varint_length]).n 214 | 215 | payload = payload[addr_count_varint_length:] 216 | 217 | addresses = set() 218 | 219 | while payload: 220 | addresses.add(structure.NetAddr.from_bytes(payload[:38])) 221 | payload = payload[38:] 222 | 223 | return cls(addresses) 224 | -------------------------------------------------------------------------------- /minode/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import argparse 3 | import base64 4 | import csv 5 | import logging 6 | import multiprocessing 7 | import os 8 | import pickle 9 | import signal 10 | import socket 11 | 12 | from advertiser import Advertiser 13 | from manager import Manager 14 | from listener import Listener 15 | import i2p.controller 16 | import i2p.listener 17 | import shared 18 | 19 | 20 | def handler(s, f): 21 | logging.info('Gracefully shutting down MiNode') 22 | shared.shutting_down = True 23 | 24 | 25 | def parse_arguments(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('-p', '--port', help='Port to listen on', type=int) 28 | parser.add_argument('--host', help='Listening host') 29 | parser.add_argument('--debug', help='Enable debug logging', action='store_true') 30 | parser.add_argument('--data-dir', help='Path to data directory') 31 | parser.add_argument('--no-incoming', help='Do not listen for incoming connections', action='store_true') 32 | parser.add_argument('--no-outgoing', help='Do not send outgoing connections', action='store_true') 33 | parser.add_argument('--no-ip', help='Do not use IP network', action='store_true') 34 | parser.add_argument('--trusted-peer', help='Specify a trusted peer we should connect to') 35 | parser.add_argument('--connection-limit', help='Maximum number of connections', type=int) 36 | parser.add_argument('--i2p', help='Enable I2P support (uses SAMv3)', action='store_true') 37 | parser.add_argument('--i2p-tunnel-length', help='Length of I2P tunnels', type=int) 38 | parser.add_argument('--i2p-sam-host', help='Host of I2P SAMv3 bridge') 39 | parser.add_argument('--i2p-sam-port', help='Port of I2P SAMv3 bridge', type=int) 40 | parser.add_argument('--i2p-transient', help='Generate new I2P destination on start', action='store_true') 41 | 42 | args = parser.parse_args() 43 | if args.port: 44 | shared.listening_port = args.port 45 | if args.host: 46 | shared.listening_host = args.host 47 | if args.debug: 48 | shared.log_level = logging.DEBUG 49 | if args.data_dir: 50 | dir_path = args.data_dir 51 | if not dir_path.endswith('/'): 52 | dir_path += '/' 53 | shared.data_directory = dir_path 54 | if args.no_incoming: 55 | shared.listen_for_connections = False 56 | if args.no_outgoing: 57 | shared.send_outgoing_connections = False 58 | if args.no_ip: 59 | shared.ip_enabled = False 60 | if args.trusted_peer: 61 | if len(args.trusted_peer) > 50: 62 | # I2P 63 | shared.trusted_peer = (args.trusted_peer.encode(), 'i2p') 64 | else: 65 | colon_count = args.trusted_peer.count(':') 66 | if colon_count == 0: 67 | shared.trusted_peer = (args.trusted_peer, 8444) 68 | if colon_count == 1: 69 | addr = args.trusted_peer.split(':') 70 | shared.trusted_peer = (addr[0], int(addr[1])) 71 | if colon_count >= 2: 72 | # IPv6 <3 73 | addr = args.trusted_peer.split(']:') 74 | addr[0] = addr[0][1:] 75 | shared.trusted_peer = (addr[0], int(addr[1])) 76 | if args.connection_limit: 77 | shared.connection_limit = args.connection_limit 78 | if args.i2p: 79 | shared.i2p_enabled = True 80 | if args.i2p_tunnel_length: 81 | shared.i2p_tunnel_length = args.i2p_tunnel_length 82 | if args.i2p_sam_host: 83 | shared.i2p_sam_host = args.i2p_sam_host 84 | if args.i2p_sam_port: 85 | shared.i2p_sam_port = args.i2p_sam_port 86 | if args.i2p_transient: 87 | shared.i2p_transient = True 88 | 89 | 90 | def load_data(): 91 | try: 92 | with open(shared.data_directory + 'objects.pickle', mode='br') as file: 93 | shared.objects = pickle.load(file) 94 | except Exception as e: 95 | logging.warning('Error while loading objects from disk.') 96 | logging.warning(e) 97 | 98 | try: 99 | with open(shared.data_directory + 'nodes.pickle', mode='br') as file: 100 | shared.node_pool = pickle.load(file) 101 | except Exception as e: 102 | logging.warning('Error while loading nodes from disk.') 103 | logging.warning(e) 104 | 105 | try: 106 | with open(shared.data_directory + 'i2p_nodes.pickle', mode='br') as file: 107 | shared.i2p_node_pool = pickle.load(file) 108 | except Exception as e: 109 | logging.warning('Error while loading nodes from disk.') 110 | logging.warning(e) 111 | 112 | with open(os.path.join(shared.source_directory, 'core_nodes.csv'), mode='r', newline='') as f: 113 | reader = csv.reader(f) 114 | shared.core_nodes = {tuple(row) for row in reader} 115 | shared.node_pool.update(shared.core_nodes) 116 | 117 | with open(os.path.join(shared.source_directory, 'i2p_core_nodes.csv'), mode='r', newline='') as f: 118 | reader = csv.reader(f) 119 | shared.i2p_core_nodes = {(row[0].encode(), 'i2p') for row in reader} 120 | shared.i2p_node_pool.update(shared.i2p_core_nodes) 121 | 122 | 123 | def bootstrap_from_dns(): 124 | try: 125 | for item in socket.getaddrinfo('bootstrap8080.bitmessage.org', 80): 126 | shared.unchecked_node_pool.add((item[4][0], 8080)) 127 | logging.debug('Adding ' + item[4][0] + ' to unchecked_node_pool based on DNS bootstrap method') 128 | for item in socket.getaddrinfo('bootstrap8444.bitmessage.org', 80): 129 | shared.unchecked_node_pool.add((item[4][0], 8444)) 130 | logging.debug('Adding ' + item[4][0] + ' to unchecked_node_pool based on DNS bootstrap method') 131 | except Exception as e: 132 | logging.error('Error during DNS bootstrap') 133 | logging.error(e) 134 | 135 | 136 | def start_ip_listener(): 137 | listener_ipv4 = None 138 | listener_ipv6 = None 139 | 140 | if socket.has_ipv6: 141 | try: 142 | listener_ipv6 = Listener(shared.listening_host, shared.listening_port, family=socket.AF_INET6) 143 | listener_ipv6.start() 144 | except Exception as e: 145 | logging.warning('Error while starting IPv6 listener on port {}'.format(shared.listening_port)) 146 | logging.warning(e) 147 | 148 | try: 149 | listener_ipv4 = Listener(shared.listening_host, shared.listening_port) 150 | listener_ipv4.start() 151 | except Exception as e: 152 | if listener_ipv6: 153 | logging.warning('Error while starting IPv4 listener on port {}. '.format(shared.listening_port) + 154 | 'However the IPv6 one seems to be working and will probably accept IPv4 connections.') 155 | else: 156 | logging.error('Error while starting IPv4 listener on port {}. '.format(shared.listening_port) + 157 | 'You will not receive incoming connections. Please check your port configuration') 158 | logging.error(e) 159 | 160 | 161 | def start_i2p_listener(): 162 | # Grab I2P destinations from old object file 163 | for obj in shared.objects.values(): 164 | if obj.object_type == shared.i2p_dest_obj_type: 165 | shared.i2p_unchecked_node_pool.add((base64.b64encode(obj.object_payload, altchars=b'-~'), 'i2p')) 166 | 167 | dest_priv = b'' 168 | 169 | if not shared.i2p_transient: 170 | try: 171 | with open(shared.data_directory + 'i2p_dest_priv.key', mode='br') as file: 172 | dest_priv = file.read() 173 | logging.debug('Loaded I2P destination private key.') 174 | except Exception as e: 175 | logging.warning('Error while loading I2P destination private key.') 176 | logging.warning(e) 177 | 178 | logging.info('Starting I2P Controller and creating tunnels. This may take a while.') 179 | i2p_controller = i2p.controller.I2PController(shared.i2p_sam_host, shared.i2p_sam_port, dest_priv) 180 | i2p_controller.start() 181 | 182 | shared.i2p_dest_pub = i2p_controller.dest_pub 183 | shared.i2p_session_nick = i2p_controller.nick 184 | 185 | logging.info('Local I2P destination: {}'.format(shared.i2p_dest_pub.decode())) 186 | logging.info('I2P session nick: {}'.format(shared.i2p_session_nick.decode())) 187 | 188 | logging.info('Starting I2P Listener') 189 | i2p_listener = i2p.listener.I2PListener(i2p_controller.nick) 190 | i2p_listener.start() 191 | 192 | if not shared.i2p_transient: 193 | try: 194 | with open(shared.data_directory + 'i2p_dest_priv.key', mode='bw') as file: 195 | file.write(i2p_controller.dest_priv) 196 | logging.debug('Saved I2P destination private key.') 197 | except Exception as e: 198 | logging.warning('Error while saving I2P destination private key.') 199 | logging.warning(e) 200 | 201 | try: 202 | with open(shared.data_directory + 'i2p_dest.pub', mode='bw') as file: 203 | file.write(shared.i2p_dest_pub) 204 | logging.debug('Saved I2P destination public key.') 205 | except Exception as e: 206 | logging.warning('Error while saving I2P destination public key.') 207 | logging.warning(e) 208 | 209 | 210 | def main(): 211 | signal.signal(signal.SIGINT, handler) 212 | signal.signal(signal.SIGTERM, handler) 213 | 214 | parse_arguments() 215 | 216 | logging.basicConfig(level=shared.log_level, format='[%(asctime)s] [%(levelname)s] %(message)s') 217 | logging.info('Starting MiNode') 218 | 219 | logging.info('Data directory: {}'.format(shared.data_directory)) 220 | if not os.path.exists(shared.data_directory): 221 | try: 222 | os.makedirs(shared.data_directory) 223 | except Exception as e: 224 | logging.warning('Error while creating data directory in: {}'.format(shared.data_directory)) 225 | logging.warning(e) 226 | 227 | load_data() 228 | 229 | if shared.ip_enabled and not shared.trusted_peer: 230 | bootstrap_from_dns() 231 | 232 | if shared.i2p_enabled: 233 | # We are starting it before cleaning expired objects so we can collect I2P destination objects 234 | start_i2p_listener() 235 | 236 | for vector in set(shared.objects): 237 | if not shared.objects[vector].is_valid(): 238 | if shared.objects[vector].is_expired(): 239 | logging.debug('Deleted expired object: {}'.format(base64.b16encode(vector).decode())) 240 | else: 241 | logging.warning('Deleted invalid object: {}'.format(base64.b16encode(vector).decode())) 242 | del shared.objects[vector] 243 | 244 | manager = Manager() 245 | manager.start() 246 | 247 | advertiser = Advertiser() 248 | advertiser.start() 249 | 250 | if shared.listen_for_connections: 251 | start_ip_listener() 252 | 253 | 254 | if __name__ == '__main__': 255 | multiprocessing.set_start_method('spawn') 256 | main() 257 | -------------------------------------------------------------------------------- /minode/connection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import errno 4 | import logging 5 | import random 6 | import select 7 | import socket 8 | import ssl 9 | import threading 10 | import queue 11 | import time 12 | 13 | import message 14 | import shared 15 | import structure 16 | 17 | 18 | class Connection(threading.Thread): 19 | def __init__(self, host, port, s=None, network='ip', server=False, i2p_remote_dest=b''): 20 | self.host = host 21 | self.port = port 22 | self.network = network 23 | self.i2p_remote_dest = i2p_remote_dest 24 | 25 | if self.network == 'i2p': 26 | self.host_print = self.i2p_remote_dest[:8].decode() 27 | else: 28 | self.host_print = self.host 29 | 30 | super().__init__(name='Connection to {}:{}'.format(host, port)) 31 | 32 | self.send_queue = queue.Queue() 33 | 34 | self.vectors_to_get = set() 35 | self.vectors_to_send = set() 36 | 37 | self.vectors_requested = dict() 38 | 39 | self.status = 'ready' 40 | 41 | self.tls = False 42 | 43 | self.verack_received = False 44 | self.verack_sent = False 45 | 46 | self.s = s 47 | 48 | self.remote_version = None 49 | 50 | self.server = server 51 | 52 | if bool(s): 53 | self.status = 'connected' 54 | 55 | self.buffer_receive = b'' 56 | self.buffer_send = b'' 57 | 58 | self.next_message_size = shared.header_length 59 | self.next_header = True 60 | self.on_connection_fully_established_scheduled = False 61 | 62 | self.last_message_received = time.time() 63 | self.last_message_sent = time.time() 64 | 65 | def run(self): 66 | if self.s is None: 67 | self._connect() 68 | if self.status != 'connected': 69 | return 70 | self.s.settimeout(0) 71 | if not self.server: 72 | if self.network == 'ip': 73 | self.send_queue.put(message.Version(self.host, self.port)) 74 | else: 75 | self.send_queue.put(message.Version('127.0.0.1', 7656)) 76 | while True: 77 | if self.on_connection_fully_established_scheduled and not (self.buffer_send or self.buffer_receive): 78 | self._on_connection_fully_established() 79 | data = True 80 | try: 81 | if self.status == 'fully_established': 82 | data = self.s.recv(4096) 83 | self.buffer_receive += data 84 | if data and len(self.buffer_receive) < 4000000: 85 | continue 86 | else: 87 | data = self.s.recv(self.next_message_size - len(self.buffer_receive)) 88 | self.buffer_receive += data 89 | except ssl.SSLWantReadError: 90 | if self.status == 'fully_established': 91 | self._request_objects() 92 | self._send_objects() 93 | except socket.error as e: 94 | err = e.args[0] 95 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 96 | if self.status == 'fully_established': 97 | self._request_objects() 98 | self._send_objects() 99 | else: 100 | logging.debug('Disconnecting from {}:{}. Reason: {}'.format(self.host_print, self.port, e)) 101 | data = None 102 | except ConnectionResetError: 103 | logging.debug('Disconnecting from {}:{}. Reason: ConnectionResetError'.format(self.host_print, self.port)) 104 | self.status = 'disconnecting' 105 | self._process_buffer_receive() 106 | self._process_queue() 107 | self._send_data() 108 | if time.time() - self.last_message_received > shared.timeout: 109 | logging.debug( 110 | 'Disconnecting from {}:{}. Reason: time.time() - self.last_message_received > shared.timeout'.format( 111 | self.host_print, self.port)) 112 | self.status = 'disconnecting' 113 | if time.time() - self.last_message_received > 30 and self.status != 'fully_established'and self.status != 'disconnecting': 114 | logging.debug( 115 | 'Disconnecting from {}:{}. Reason: time.time() - self.last_message_received > 30 and self.status != \'fully_established\''.format( 116 | self.host_print, self.port)) 117 | self.status = 'disconnecting' 118 | if time.time() - self.last_message_sent > 300 and self.status == 'fully_established': 119 | self.send_queue.put(message.Message(b'pong', b'')) 120 | if self.status == 'disconnecting' or shared.shutting_down: 121 | data = None 122 | if not data: 123 | self.status = 'disconnected' 124 | self.s.close() 125 | logging.info('Disconnected from {}:{}'.format(self.host_print, self.port)) 126 | break 127 | time.sleep(0.2) 128 | 129 | def _connect(self): 130 | logging.debug('Connecting to {}:{}'.format(self.host_print, self.port)) 131 | 132 | try: 133 | self.s = socket.create_connection((self.host, self.port), 10) 134 | self.status = 'connected' 135 | logging.info('Established TCP connection to {}:{}'.format(self.host_print, self.port)) 136 | except Exception as e: 137 | logging.warning('Connection to {}:{} failed. Reason: {}'.format(self.host_print, self.port, e)) 138 | self.status = 'failed' 139 | 140 | def _send_data(self): 141 | if self.buffer_send and self: 142 | try: 143 | amount = self.s.send(self.buffer_send) 144 | self.buffer_send = self.buffer_send[amount:] 145 | except (BlockingIOError, ssl.SSLWantWriteError): 146 | pass 147 | except (BrokenPipeError, ConnectionResetError, ssl.SSLError, OSError) as e: 148 | logging.debug('Disconnecting from {}:{}. Reason: {}'.format(self.host_print, self.port, e)) 149 | self.status = 'disconnecting' 150 | 151 | def _do_tls_handshake(self): 152 | logging.debug('Initializing TLS connection with {}:{}'.format(self.host_print, self.port)) 153 | 154 | context = ssl.create_default_context() 155 | context.check_hostname = False 156 | context.verify_mode = ssl.CERT_NONE 157 | 158 | if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 and not ssl.OPENSSL_VERSION.startswith("LibreSSL"): 159 | # OpenSSL>=1.1 160 | context.set_ciphers('AECDH-AES256-SHA@SECLEVEL=0') 161 | else: 162 | context.set_ciphers('AECDH-AES256-SHA') 163 | 164 | context.set_ecdh_curve("secp256k1") 165 | context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE | ssl.OP_CIPHER_SERVER_PREFERENCE 166 | 167 | self.s = context.wrap_socket(self.s, server_side=self.server, do_handshake_on_connect=False) 168 | 169 | while True: 170 | try: 171 | self.s.do_handshake() 172 | break 173 | except ssl.SSLWantReadError: 174 | select.select([self.s], [], []) 175 | except ssl.SSLWantWriteError: 176 | select.select([], [self.s], []) 177 | except Exception as e: 178 | logging.debug('Disconnecting from {}:{}. Reason: {}'.format(self.host_print, self.port, e)) 179 | self.status = 'disconnecting' 180 | break 181 | self.tls = True 182 | logging.debug('Established TLS connection with {}:{}'.format(self.host_print, self.port)) 183 | 184 | def _send_message(self, m): 185 | if type(m) == message.Message and m.command == b'object': 186 | logging.debug('{}:{} <- {}'.format(self.host_print, self.port, structure.Object.from_message(m))) 187 | else: 188 | logging.debug('{}:{} <- {}'.format(self.host_print, self.port, m)) 189 | self.buffer_send += m.to_bytes() 190 | 191 | def _on_connection_fully_established(self): 192 | logging.info('Established Bitmessage protocol connection to {}:{}'.format(self.host_print, self.port)) 193 | self.on_connection_fully_established_scheduled = False 194 | if self.remote_version.services & 2 and self.network == 'ip': # NODE_SSL 195 | self._do_tls_handshake() 196 | 197 | addr = {structure.NetAddr(c.remote_version.services, c.host, c.port) for c in shared.connections if c.network != 'i2p' and c.server is False and c.status == 'fully_established'} 198 | if len(shared.node_pool) > 10: 199 | addr.update({structure.NetAddr(1, a[0], a[1]) for a in random.sample(shared.node_pool, 10)}) 200 | if len(shared.unchecked_node_pool) > 10: 201 | addr.update({structure.NetAddr(1, a[0], a[1]) for a in random.sample(shared.unchecked_node_pool, 10)}) 202 | if len(addr) != 0: 203 | self.send_queue.put(message.Addr(addr)) 204 | 205 | with shared.objects_lock: 206 | if len(shared.objects) > 0: 207 | to_send = {vector for vector in shared.objects.keys() if shared.objects[vector].expires_time > time.time()} 208 | while len(to_send) > 0: 209 | if len(to_send) > 10000: 210 | # We limit size of inv messaged to 10000 entries because they might time out in very slow networks (I2P) 211 | pack = random.sample(to_send, 10000) 212 | self.send_queue.put(message.Inv(pack)) 213 | to_send.difference_update(pack) 214 | else: 215 | self.send_queue.put(message.Inv(to_send)) 216 | to_send.clear() 217 | self.status = 'fully_established' 218 | 219 | def _process_queue(self): 220 | while not self.send_queue.empty(): 221 | m = self.send_queue.get() 222 | if m: 223 | if m == 'fully_established': 224 | self.on_connection_fully_established_scheduled = True 225 | else: 226 | self._send_message(m) 227 | self.last_message_sent = time.time() 228 | else: 229 | self.status = 'disconnecting' 230 | break 231 | 232 | def _process_buffer_receive(self): 233 | while len(self.buffer_receive) >= self.next_message_size: 234 | if self.next_header: 235 | self.next_header = False 236 | try: 237 | h = message.Header.from_bytes(self.buffer_receive[:shared.header_length]) 238 | except ValueError as e: 239 | self.status = 'disconnecting' 240 | logging.warning('Received malformed message from {}:{}: {}'.format(self.host_print, self.port, e)) 241 | break 242 | self.next_message_size += h.payload_length 243 | else: 244 | try: 245 | m = message.Message.from_bytes(self.buffer_receive[:self.next_message_size]) 246 | except ValueError as e: 247 | self.status = 'disconnecting' 248 | logging.warning('Received malformed message from {}:{}, {}'.format(self.host_print, self.port, e)) 249 | break 250 | self.next_header = True 251 | self.buffer_receive = self.buffer_receive[self.next_message_size:] 252 | self.next_message_size = shared.header_length 253 | self.last_message_received = time.time() 254 | try: 255 | self._process_message(m) 256 | except ValueError as e: 257 | self.status = 'disconnecting' 258 | logging.warning('Received malformed message from {}:{}: {}'.format(self.host_print, self.port, e)) 259 | break 260 | 261 | def _process_message(self, m): 262 | if m.command == b'version': 263 | version = message.Version.from_bytes(m.to_bytes()) 264 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, str(version))) 265 | if version.protocol_version != shared.protocol_version or version.nonce == shared.nonce: 266 | self.status = 'disconnecting' 267 | self.send_queue.put(None) 268 | else: 269 | self.send_queue.put(message.Message(b'verack', b'')) 270 | self.verack_sent = True 271 | self.remote_version = version 272 | if not self.server: 273 | self.send_queue.put('fully_established') 274 | if self.network == 'ip': 275 | shared.address_advertise_queue.put(structure.NetAddr(version.services, self.host, self.port)) 276 | shared.node_pool.add((self.host, self.port)) 277 | elif self.network == 'i2p': 278 | shared.i2p_node_pool.add((self.host, 'i2p')) 279 | if self.network == 'ip': 280 | shared.address_advertise_queue.put(structure.NetAddr(shared.services, version.host, shared.listening_port)) 281 | if self.server: 282 | if self.network == 'ip': 283 | self.send_queue.put(message.Version(self.host, self.port)) 284 | else: 285 | self.send_queue.put(message.Version('127.0.0.1', 7656)) 286 | 287 | elif m.command == b'verack': 288 | self.verack_received = True 289 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, 'verack')) 290 | if self.server: 291 | self.send_queue.put('fully_established') 292 | 293 | elif m.command == b'inv': 294 | inv = message.Inv.from_message(m) 295 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, inv)) 296 | to_get = inv.vectors.copy() 297 | to_get.difference_update(shared.objects.keys()) 298 | self.vectors_to_get.update(to_get) 299 | # Do not send objects they already have. 300 | self.vectors_to_send.difference_update(inv.vectors) 301 | 302 | elif m.command == b'object': 303 | obj = structure.Object.from_message(m) 304 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, obj)) 305 | self.vectors_requested.pop(obj.vector, None) 306 | self.vectors_to_get.discard(obj.vector) 307 | if obj.is_valid() and obj.vector not in shared.objects: 308 | with shared.objects_lock: 309 | shared.objects[obj.vector] = obj 310 | if obj.object_type == shared.i2p_dest_obj_type and obj.version == shared.i2p_dest_obj_version: 311 | dest = base64.b64encode(obj.object_payload, altchars=b'-~') 312 | logging.debug('Received I2P destination object, adding to i2p_unchecked_node_pool') 313 | logging.debug(dest) 314 | shared.i2p_unchecked_node_pool.add((dest, 'i2p')) 315 | shared.vector_advertise_queue.put(obj.vector) 316 | 317 | elif m.command == b'getdata': 318 | getdata = message.GetData.from_message(m) 319 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, getdata)) 320 | self.vectors_to_send.update(getdata.vectors) 321 | 322 | elif m.command == b'addr': 323 | addr = message.Addr.from_message(m) 324 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, addr)) 325 | for a in addr.addresses: 326 | shared.unchecked_node_pool.add((a.host, a.port)) 327 | 328 | elif m.command == b'ping': 329 | logging.debug('{}:{} -> ping'.format(self.host_print, self.port)) 330 | self.send_queue.put(message.Message(b'pong', b'')) 331 | 332 | elif m.command == b'error': 333 | logging.error('{}:{} -> error: {}'.format(self.host_print, self.port, m.payload)) 334 | 335 | else: 336 | logging.debug('{}:{} -> {}'.format(self.host_print, self.port, m)) 337 | 338 | def _request_objects(self): 339 | if self.vectors_to_get and len(self.vectors_requested) < 100: 340 | self.vectors_to_get.difference_update(shared.objects.keys()) 341 | if self.vectors_to_get: 342 | if len(self.vectors_to_get) > 64: 343 | pack = random.sample(self.vectors_to_get, 64) 344 | self.send_queue.put(message.GetData(pack)) 345 | self.vectors_requested.update({vector: time.time() for vector in pack if vector not in self.vectors_requested}) 346 | self.vectors_to_get.difference_update(pack) 347 | else: 348 | self.send_queue.put(message.GetData(self.vectors_to_get)) 349 | self.vectors_requested.update({vector: time.time() for vector in self.vectors_to_get if vector not in self.vectors_requested}) 350 | self.vectors_to_get.clear() 351 | if self.vectors_requested: 352 | self.vectors_requested = {vector: t for vector, t in self.vectors_requested.items() if vector not in shared.objects and t > time.time() - 15 * 60} 353 | to_re_request = {vector for vector, t in self.vectors_requested.items() if t < time.time() - 10 * 60} 354 | if to_re_request: 355 | self.vectors_to_get.update(to_re_request) 356 | logging.debug('Re-requesting {} objects from {}:{}'.format(len(to_re_request), self.host_print, self.port)) 357 | 358 | def _send_objects(self): 359 | if self.vectors_to_send: 360 | if len(self.vectors_to_send) > 16: 361 | to_send = random.sample(self.vectors_to_send, 16) 362 | self.vectors_to_send.difference_update(to_send) 363 | else: 364 | to_send = self.vectors_to_send.copy() 365 | self.vectors_to_send.clear() 366 | with shared.objects_lock: 367 | for vector in to_send: 368 | obj = shared.objects.get(vector, None) 369 | if obj: 370 | self.send_queue.put(message.Message(b'object', obj.to_bytes())) 371 | --------------------------------------------------------------------------------