A Python port of the Invisible Internet Project (I2P)
at main 144 lines 4.9 kB view raw
1"""Peer connection management — address extraction, connection pooling, and NTCP2 connector. 2 3Provides utilities for connecting to I2P peers via the real NTCP2 transport: 4- extract_ntcp2_address(): parse NTCP2 params from a RouterInfo 5- ConnectionPool: track active connections by peer hash 6- PeerConnector: initiate NTCP2 handshakes to remote peers 7""" 8 9from __future__ import annotations 10 11import asyncio 12import hashlib 13import logging 14from dataclasses import dataclass 15 16from i2p_data.data_helper import from_base64 17 18from i2p_transport.ntcp2_real_server import NTCP2RealConnector 19 20logger = logging.getLogger(__name__) 21 22 23def extract_ntcp2_address(router_info) -> tuple[str, int, bytes, bytes] | None: 24 """Extract NTCP2 connection params from a RouterInfo. 25 26 Returns (host, port, static_key_32bytes, iv_16bytes) or None. 27 Looks for address with transport="NTCP2" and options containing "s" and "i". 28 """ 29 for addr in router_info.addresses: 30 if addr.transport != "NTCP2": 31 continue 32 opts = addr.options 33 s_b64 = opts.get("s") 34 i_b64 = opts.get("i") 35 if s_b64 is None or i_b64 is None: 36 continue 37 host = opts.get("host") 38 port_str = opts.get("port") 39 if host is None or port_str is None: 40 continue 41 try: 42 static_key = from_base64(s_b64) 43 iv = from_base64(i_b64) 44 port = int(port_str) 45 except (ValueError, Exception): 46 continue 47 return (host, port, static_key, iv) 48 return None 49 50 51class ConnectionPool: 52 """Tracks active NTCP2 connections by peer hash.""" 53 54 def __init__(self, max_connections: int = 50): 55 self._connections: dict[bytes, object] = {} 56 self._max = max_connections 57 58 def add(self, peer_hash: bytes, connection) -> bool: 59 """Add connection. Returns False if at capacity.""" 60 if len(self._connections) >= self._max: 61 return False 62 self._connections[peer_hash] = connection 63 return True 64 65 def get(self, peer_hash: bytes): 66 """Get connection by peer hash, or None.""" 67 return self._connections.get(peer_hash) 68 69 def remove(self, peer_hash: bytes) -> None: 70 """Remove a connection by peer hash. No-op if not present.""" 71 self._connections.pop(peer_hash, None) 72 73 def is_connected(self, peer_hash: bytes) -> bool: 74 """Return True if peer_hash has an active connection.""" 75 return peer_hash in self._connections 76 77 def get_all_peer_hashes(self) -> list[bytes]: 78 """Return all connected peer hashes.""" 79 return list(self._connections.keys()) 80 81 @property 82 def active_count(self) -> int: 83 """Number of active connections.""" 84 return len(self._connections) 85 86 87class PeerConnector: 88 """Connects to I2P peers via NTCP2. 89 90 Extracts NTCP2 address from RouterInfo, initiates handshake, 91 and returns established connection. 92 """ 93 94 def __init__( 95 self, 96 our_static_keypair: tuple[bytes, bytes], 97 our_iv: bytes, 98 our_ri_bytes: bytes | None = None, 99 ): 100 self._our_static = our_static_keypair 101 self._our_iv = our_iv 102 self._our_ri_bytes = our_ri_bytes or b"" 103 104 def set_our_router_info(self, ri_bytes: bytes) -> None: 105 """Set our own serialized RouterInfo to send in msg3.""" 106 self._our_ri_bytes = ri_bytes 107 108 async def connect(self, router_info) -> object | None: 109 """Connect to a peer using NTCP2. 110 111 1. Extract NTCP2 address from RouterInfo 112 2. Create NTCP2RealConnector 113 3. Perform handshake (sends OUR RouterInfo in msg3) 114 4. Return connection (or None on failure) 115 """ 116 params = extract_ntcp2_address(router_info) 117 if params is None: 118 logger.debug("No NTCP2 address found in RouterInfo") 119 return None 120 121 host, port, peer_static_pub, peer_iv = params 122 123 # Compute the SHA-256 hash of the peer's RouterIdentity for handshake obfuscation. 124 # Java: _bobHash = new SessionKey(peer.calculateHash().getData()) 125 # calculateHash() = SHA-256 of the RouterIdentity bytes (NOT the full RouterInfo) 126 identity_bytes = router_info.identity.to_bytes() 127 peer_ri_hash = hashlib.sha256(identity_bytes).digest() 128 129 try: 130 connector = NTCP2RealConnector() 131 conn = await connector.connect( 132 host=host, 133 port=port, 134 our_static_key=self._our_static, 135 our_ri_bytes=self._our_ri_bytes, 136 peer_static_pub=peer_static_pub, 137 peer_ri_hash=peer_ri_hash, 138 peer_iv=peer_iv, 139 ) 140 logger.info("Connected to peer at %s:%d", host, port) 141 return conn 142 except Exception: 143 logger.exception("Failed to connect to peer at %s:%d", host, port) 144 return None