A Python port of the Invisible Internet Project (I2P)
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)