A Python port of the Invisible Internet Project (I2P)
at main 82 lines 2.7 kB view raw
1"""Generic TCP client tunnel — forwards local connections to I2P destinations. 2 3Ported from net.i2p.i2ptunnel.I2PTunnelClient. 4""" 5 6from __future__ import annotations 7 8import logging 9import random 10 11from i2p_apps.i2ptunnel.config import TunnelDefinition 12from i2p_apps.i2ptunnel.forwarder import bridge 13from i2p_apps.i2ptunnel.tasks import ClientTunnelTask 14 15logger = logging.getLogger(__name__) 16 17 18class GenericClientTask(ClientTunnelTask): 19 """Generic TCP client tunnel. 20 21 Accepts local TCP connections, resolves the configured I2P destination(s), 22 and bridges the connection through SAM. 23 """ 24 25 def __init__(self, config: TunnelDefinition, session) -> None: 26 super().__init__(config, session) 27 self._destinations = self._parse_destinations(config.target_destination) 28 29 @staticmethod 30 def _parse_destinations(dest_str: str) -> list[str]: 31 """Parse comma/space-separated destination list.""" 32 result = [] 33 for part in dest_str.replace(",", " ").split(): 34 part = part.strip() 35 if part: 36 result.append(part) 37 return result 38 39 def _pick_destination(self) -> str: 40 """Pick a destination (random if multiple).""" 41 if len(self._destinations) == 1: 42 return self._destinations[0] 43 return random.choice(self._destinations) 44 45 async def _resolve_destination(self, dest: str) -> str | None: 46 """Resolve a destination, using naming lookup for .i2p hostnames. 47 48 b32 addresses are returned as-is (the router resolves them). 49 """ 50 if dest.endswith(".b32.i2p"): 51 return dest 52 if dest.endswith(".i2p"): 53 resolved = await self._session.lookup(dest) 54 if resolved is None: 55 logger.warning("Cannot resolve %s — not in address book", dest) 56 return resolved 57 # Already a base64 destination 58 return dest 59 60 async def handle_client(self, reader, writer) -> None: 61 dest = self._pick_destination() 62 resolved = await self._resolve_destination(dest) 63 if resolved is None: 64 try: 65 writer.close() 66 await writer.wait_closed() 67 except Exception: 68 pass 69 return 70 71 try: 72 remote_reader, remote_writer = await self._session.connect(resolved) 73 except Exception: 74 logger.debug("Connection to %s failed", dest) 75 try: 76 writer.close() 77 await writer.wait_closed() 78 except Exception: 79 pass 80 return 81 82 await bridge(reader, writer, remote_reader, remote_writer)