A Python port of the Invisible Internet Project (I2P)
at main 129 lines 4.2 kB view raw
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)