A Python port of the Invisible Internet Project (I2P)
1"""Tunnel building types — build records, reply records, tunnel pools."""
2
3import random
4import struct
5import time
6
7from i2p_data.tunnel import TunnelId
8
9
10class BuildRecord:
11 """Tunnel build request record (cleartext portion).
12
13 Fixed size: 222 bytes (4+32+4+32+32+32+32+16+1+...).
14 """
15
16 SIZE = 222
17
18 def __init__(self, receive_tunnel_id: int, our_ident: bytes,
19 next_tunnel_id: int, next_ident: bytes,
20 layer_key: bytes, iv_key: bytes,
21 reply_key: bytes, reply_iv: bytes,
22 is_gateway: bool, is_endpoint: bool):
23 self.receive_tunnel_id = receive_tunnel_id
24 self.our_ident = our_ident
25 self.next_tunnel_id = next_tunnel_id
26 self.next_ident = next_ident
27 self.layer_key = layer_key
28 self.iv_key = iv_key
29 self.reply_key = reply_key
30 self.reply_iv = reply_iv
31 self.is_gateway = is_gateway
32 self.is_endpoint = is_endpoint
33
34 def to_bytes(self) -> bytes:
35 flags = 0
36 if self.is_gateway:
37 flags |= 0x01
38 if self.is_endpoint:
39 flags |= 0x02
40 return (struct.pack("!I", self.receive_tunnel_id) +
41 self.our_ident +
42 struct.pack("!I", self.next_tunnel_id) +
43 self.next_ident +
44 self.layer_key + self.iv_key +
45 self.reply_key + self.reply_iv +
46 struct.pack("!B", flags) +
47 b"\x00" * (self.SIZE - 4 - 32 - 4 - 32 - 32 - 32 - 32 - 16 - 1))
48
49 @classmethod
50 def from_bytes(cls, data: bytes) -> "BuildRecord":
51 off = 0
52 recv_tid = struct.unpack("!I", data[off:off + 4])[0]; off += 4
53 our_ident = data[off:off + 32]; off += 32
54 next_tid = struct.unpack("!I", data[off:off + 4])[0]; off += 4
55 next_ident = data[off:off + 32]; off += 32
56 layer_key = data[off:off + 32]; off += 32
57 iv_key = data[off:off + 32]; off += 32
58 reply_key = data[off:off + 32]; off += 32
59 reply_iv = data[off:off + 16]; off += 16
60 flags = data[off]
61 return cls(recv_tid, our_ident, next_tid, next_ident,
62 layer_key, iv_key, reply_key, reply_iv,
63 is_gateway=bool(flags & 0x01),
64 is_endpoint=bool(flags & 0x02))
65
66
67class BuildReplyRecord:
68 """Tunnel build reply record: status(1) + reply_data(495) = 496 bytes."""
69
70 SIZE = 496
71
72 def __init__(self, status: int, reply_data: bytes):
73 self.status = status
74 self.reply_data = reply_data
75
76 def is_accepted(self) -> bool:
77 return self.status == 0
78
79 def to_bytes(self) -> bytes:
80 return struct.pack("!B", self.status) + self.reply_data
81
82 @classmethod
83 def from_bytes(cls, data: bytes) -> "BuildReplyRecord":
84 return cls(data[0], data[1:cls.SIZE])
85
86
87class TunnelEntry:
88 """An entry in a tunnel pool."""
89
90 def __init__(self, tunnel_id: TunnelId, gateway: bytes,
91 length: int, creation_time: int, expiration: int):
92 self.tunnel_id = tunnel_id
93 self.gateway = gateway
94 self.length = length
95 self.creation_time = creation_time
96 self.expiration = expiration
97
98 def is_expired(self, now_ms: int | None = None) -> bool:
99 if now_ms is None:
100 now_ms = int(time.time() * 1000)
101 return now_ms >= self.expiration
102
103
104class TunnelPool:
105 """Pool of tunnels (inbound or outbound)."""
106
107 def __init__(self, name: str):
108 self.name = name
109 self._tunnels: list[TunnelEntry] = []
110
111 def tunnel_count(self) -> int:
112 return len(self._tunnels)
113
114 def add(self, entry: TunnelEntry):
115 self._tunnels.append(entry)
116
117 def get(self, tunnel_id: TunnelId) -> TunnelEntry | None:
118 for t in self._tunnels:
119 if t.tunnel_id == tunnel_id:
120 return t
121 return None
122
123 def remove_expired(self, now_ms: int | None = None):
124 self._tunnels = [t for t in self._tunnels if not t.is_expired(now_ms)]
125
126 def select_random(self) -> TunnelEntry | None:
127 if not self._tunnels:
128 return None
129 return random.choice(self._tunnels)