A Python port of the Invisible Internet Project (I2P)
1"""Base transport interface.
2
3All transports (NTCP2, SSU2) implement this interface so TransportManager
4can coordinate between them.
5
6Ported from net.i2p.router.transport.Transport.
7"""
8
9import enum
10from abc import ABC, abstractmethod
11
12
13class TransportStyle(enum.Enum):
14 NTCP2 = "NTCP2"
15 SSU2 = "SSU2"
16
17
18class ReachabilityStatus(enum.Enum):
19 OK = "ok" # Directly reachable
20 FIREWALLED = "firewalled" # Behind NAT, needs relay
21 UNKNOWN = "unknown" # Not yet determined
22 TESTING = "testing" # Currently running peer test
23 SYMMETRIC_NAT = "symmetric_nat" # Symmetric NAT (hardest case)
24
25
26# Reachability ordering — lower index is better
27_REACHABILITY_ORDER = [
28 ReachabilityStatus.OK,
29 ReachabilityStatus.TESTING,
30 ReachabilityStatus.FIREWALLED,
31 ReachabilityStatus.SYMMETRIC_NAT,
32 ReachabilityStatus.UNKNOWN,
33]
34
35
36def _reachability_rank(status: ReachabilityStatus) -> int:
37 """Return numeric rank for a reachability status (lower is better)."""
38 try:
39 return _REACHABILITY_ORDER.index(status)
40 except ValueError:
41 return len(_REACHABILITY_ORDER)
42
43
44class TransportBid:
45 """A bid from a transport to carry a message to a peer.
46
47 The TransportManager collects bids from all registered transports
48 and selects the lowest (best) bid.
49 """
50
51 # Sentinel values
52 TRANSIENT_FAIL = 999
53 WILL_NOT_SEND = -1
54
55 def __init__(self, latency_ms: int, transport: "Transport",
56 preference: int = 0):
57 self.latency_ms = latency_ms
58 self.transport = transport
59 self.preference = preference # Tiebreaker (lower = preferred)
60
61 def __lt__(self, other: "TransportBid") -> bool:
62 if self.latency_ms != other.latency_ms:
63 return self.latency_ms < other.latency_ms
64 return self.preference < other.preference
65
66 def __repr__(self) -> str:
67 return (f"TransportBid(latency_ms={self.latency_ms}, "
68 f"transport={self.transport.style.value}, "
69 f"preference={self.preference})")
70
71
72class Transport(ABC):
73 """Abstract base for I2P transports."""
74
75 @property
76 @abstractmethod
77 def style(self) -> TransportStyle:
78 """The transport style (NTCP2 or SSU2)."""
79
80 @abstractmethod
81 async def start(self) -> None:
82 """Start the transport."""
83
84 @abstractmethod
85 async def stop(self) -> None:
86 """Stop the transport."""
87
88 @property
89 @abstractmethod
90 def is_running(self) -> bool:
91 """Whether the transport is currently running."""
92
93 @abstractmethod
94 async def bid(self, peer_hash: bytes) -> TransportBid:
95 """Bid to send a message to the given peer.
96
97 Returns a TransportBid with latency_ms=WILL_NOT_SEND if this
98 transport cannot reach the peer.
99 """
100
101 @abstractmethod
102 async def send(self, peer_hash: bytes, data: bytes) -> bool:
103 """Send data to peer. Returns True on success."""
104
105 @property
106 @abstractmethod
107 def reachability(self) -> ReachabilityStatus:
108 """Current reachability status of this transport."""
109
110 @property
111 @abstractmethod
112 def current_address(self) -> dict | None:
113 """Published address for this transport (IP, port, options).
114
115 Returns None if no address is available (e.g. transport not started).
116 """