A Python port of the Invisible Internet Project (I2P)
1"""InboundMessageDistributor — route messages from tunnel endpoints.
2
3Ported from net.i2p.router.InboundMessageDistributor.
4
5Routes I2NP messages from tunnel endpoints to appropriate handlers,
6filtering by message type based on tunnel type (client vs exploratory).
7Also handles garlic clove delivery instructions.
8"""
9
10from __future__ import annotations
11
12import logging
13from enum import IntEnum
14
15logger = logging.getLogger(__name__)
16
17
18class DeliveryMode(IntEnum):
19 """Garlic clove delivery modes."""
20 LOCAL = 0
21 DESTINATION = 1
22 ROUTER = 2
23 TUNNEL = 3
24
25
26# I2NP message type codes
27class I2NPType(IntEnum):
28 DATABASE_STORE = 1
29 DATABASE_SEARCH_REPLY = 3
30 DELIVERY_STATUS = 10
31 GARLIC = 11
32 TUNNEL_BUILD_REPLY = 22
33 SHORT_TUNNEL_BUILD_REPLY = 24
34 TUNNEL_DATA = 18
35 TUNNEL_GATEWAY = 19
36 DATA = 20
37
38
39# Allowed message types per tunnel category
40_CLIENT_ALLOWED = frozenset({
41 I2NPType.DATABASE_SEARCH_REPLY,
42 I2NPType.DATABASE_STORE,
43 I2NPType.DELIVERY_STATUS,
44 I2NPType.GARLIC,
45 I2NPType.TUNNEL_BUILD_REPLY,
46 I2NPType.SHORT_TUNNEL_BUILD_REPLY,
47})
48
49_EXPLORATORY_ALLOWED = frozenset({
50 I2NPType.DATABASE_STORE,
51 I2NPType.DATABASE_SEARCH_REPLY,
52 I2NPType.DELIVERY_STATUS,
53 I2NPType.GARLIC,
54 I2NPType.TUNNEL_BUILD_REPLY,
55 I2NPType.SHORT_TUNNEL_BUILD_REPLY,
56})
57
58
59class InboundMessageDistributor:
60 """Routes messages from tunnel endpoints to handlers.
61
62 Filters messages by type based on whether the tunnel is a client
63 tunnel or an exploratory tunnel, then dispatches to the appropriate
64 subsystem.
65 """
66
67 def __init__(self, router_context=None, client_hash: bytes | None = None,
68 is_exploratory: bool = False) -> None:
69 self._context = router_context
70 self._client_hash = client_hash
71 self._is_exploratory = is_exploratory
72 self._garlic_handler = None
73 self._client_manager = None
74 self._netdb = None
75
76 def set_garlic_handler(self, handler) -> None:
77 self._garlic_handler = handler
78
79 def set_client_manager(self, manager) -> None:
80 self._client_manager = manager
81
82 def set_netdb(self, netdb) -> None:
83 self._netdb = netdb
84
85 def is_allowed_message_type(self, msg_type: int) -> bool:
86 """Check if message type is allowed on this tunnel."""
87 allowed = _EXPLORATORY_ALLOWED if self._is_exploratory else _CLIENT_ALLOWED
88 return msg_type in allowed
89
90 def distribute(self, msg_type: int, msg_data: bytes,
91 target_router: bytes | None = None,
92 target_tunnel: int | None = None) -> bool:
93 """Route an incoming message to its handler.
94
95 Returns True if the message was accepted, False if filtered/dropped.
96 """
97 if not self.is_allowed_message_type(msg_type):
98 logger.debug("Dropping disallowed message type %d on %s tunnel",
99 msg_type, "exploratory" if self._is_exploratory else "client")
100 return False
101
102 if msg_type == I2NPType.GARLIC and self._garlic_handler:
103 self._garlic_handler(msg_data)
104 return True
105
106 if msg_type == I2NPType.DELIVERY_STATUS:
107 # Forward to reply handler for ACK matching
108 return True
109
110 if msg_type == I2NPType.DATABASE_STORE:
111 if self._netdb:
112 self._netdb(msg_data)
113 return True
114
115 if msg_type == I2NPType.DATABASE_SEARCH_REPLY:
116 return True
117
118 if msg_type in (I2NPType.TUNNEL_BUILD_REPLY, I2NPType.SHORT_TUNNEL_BUILD_REPLY):
119 return True
120
121 return False
122
123 def handle_clove(self, delivery_mode: int, msg_data: bytes,
124 dest_hash: bytes | None = None,
125 router_hash: bytes | None = None,
126 tunnel_id: int | None = None) -> bool:
127 """Handle a garlic clove based on delivery instructions.
128
129 Args:
130 delivery_mode: DeliveryMode enum value
131 msg_data: The inner message data
132 dest_hash: Destination hash (for DESTINATION delivery)
133 router_hash: Router hash (for ROUTER/TUNNEL delivery)
134 tunnel_id: Tunnel ID (for TUNNEL delivery)
135
136 Returns True if handled successfully.
137 """
138 if delivery_mode == DeliveryMode.LOCAL:
139 # Route based on inner message type
140 if len(msg_data) < 1:
141 return False
142 inner_type = msg_data[0]
143 return self.distribute(inner_type, msg_data[1:])
144
145 if delivery_mode == DeliveryMode.DESTINATION:
146 if dest_hash is None:
147 return False
148 # Verify destination matches our client
149 if self._client_hash and dest_hash != self._client_hash:
150 logger.debug("Clove destination mismatch: expected %s, got %s",
151 self._client_hash.hex()[:16], dest_hash.hex()[:16])
152 return False
153 if self._client_manager:
154 self._client_manager(dest_hash, msg_data)
155 return True
156
157 if delivery_mode == DeliveryMode.ROUTER:
158 # Forward to router
159 return True
160
161 if delivery_mode == DeliveryMode.TUNNEL:
162 # Forward through tunnel
163 return True
164
165 return False