"""Generic TCP client tunnel — forwards local connections to I2P destinations. Ported from net.i2p.i2ptunnel.I2PTunnelClient. """ from __future__ import annotations import logging import random from i2p_apps.i2ptunnel.config import TunnelDefinition from i2p_apps.i2ptunnel.forwarder import bridge from i2p_apps.i2ptunnel.tasks import ClientTunnelTask logger = logging.getLogger(__name__) class GenericClientTask(ClientTunnelTask): """Generic TCP client tunnel. Accepts local TCP connections, resolves the configured I2P destination(s), and bridges the connection through SAM. """ def __init__(self, config: TunnelDefinition, session) -> None: super().__init__(config, session) self._destinations = self._parse_destinations(config.target_destination) @staticmethod def _parse_destinations(dest_str: str) -> list[str]: """Parse comma/space-separated destination list.""" result = [] for part in dest_str.replace(",", " ").split(): part = part.strip() if part: result.append(part) return result def _pick_destination(self) -> str: """Pick a destination (random if multiple).""" if len(self._destinations) == 1: return self._destinations[0] return random.choice(self._destinations) async def _resolve_destination(self, dest: str) -> str | None: """Resolve a destination, using naming lookup for .i2p hostnames. b32 addresses are returned as-is (the router resolves them). """ if dest.endswith(".b32.i2p"): return dest if dest.endswith(".i2p"): resolved = await self._session.lookup(dest) if resolved is None: logger.warning("Cannot resolve %s — not in address book", dest) return resolved # Already a base64 destination return dest async def handle_client(self, reader, writer) -> None: dest = self._pick_destination() resolved = await self._resolve_destination(dest) if resolved is None: try: writer.close() await writer.wait_closed() except Exception: pass return try: remote_reader, remote_writer = await self._session.connect(resolved) except Exception: logger.debug("Connection to %s failed", dest) try: writer.close() await writer.wait_closed() except Exception: pass return await bridge(reader, writer, remote_reader, remote_writer)