A Python port of the Invisible Internet Project (I2P)
at main 967 lines 33 kB view raw
1"""I2CP message types — wire format and type registry.""" 2 3import struct 4from abc import ABC, abstractmethod 5 6 7class I2CPMessage(ABC): 8 """Base class for I2CP messages. 9 10 Wire format: 4-byte big-endian length + 1-byte type + payload. 11 Length includes the type byte but not itself. 12 """ 13 14 TYPE: int = -1 15 _registry: dict[int, type["I2CPMessage"]] = {} 16 17 @classmethod 18 def __init_subclass__(cls, **kwargs): 19 super().__init_subclass__(**kwargs) 20 if hasattr(cls, 'TYPE') and cls.TYPE >= 0: 21 I2CPMessage._registry[cls.TYPE] = cls 22 23 @abstractmethod 24 def payload_bytes(self) -> bytes: 25 """Serialize message payload (after type byte).""" 26 27 @classmethod 28 @abstractmethod 29 def _from_payload(cls, payload: bytes) -> "I2CPMessage": 30 """Deserialize from payload bytes.""" 31 32 def to_wire(self) -> bytes: 33 payload = self.payload_bytes() 34 length = 1 + len(payload) # type byte + payload 35 return struct.pack("!IB", length, self.TYPE) + payload 36 37 @classmethod 38 def from_wire(cls, data: bytes) -> "I2CPMessage": 39 if len(data) < 5: 40 raise ValueError(f"I2CP message needs at least 5 bytes, got {len(data)}") 41 length = struct.unpack("!I", data[:4])[0] 42 msg_type = data[4] 43 payload = data[5:4 + length] 44 msg_cls = cls._registry.get(msg_type) 45 if msg_cls is None: 46 raise ValueError(f"Unknown I2CP message type: {msg_type}") 47 return msg_cls._from_payload(payload) 48 49 50class GetDateMessage(I2CPMessage): 51 """Type 32: C→R request current date. Payload: version string + optional options.""" 52 53 TYPE = 32 54 55 def __init__(self, version: str = "0.9.62", options: dict[str, str] | None = None): 56 self.version = version 57 self.options = options or {} 58 59 def payload_bytes(self) -> bytes: 60 vb = self.version.encode("utf-8") 61 result = struct.pack("!B", len(vb)) + vb 62 if self.options: 63 result += _encode_properties(self.options) 64 return result 65 66 @classmethod 67 def _from_payload(cls, payload: bytes) -> "GetDateMessage": 68 vlen = payload[0] 69 version = payload[1:1 + vlen].decode("utf-8") 70 offset = 1 + vlen 71 options: dict[str, str] = {} 72 if offset < len(payload): 73 options, _ = _decode_properties(payload[offset:]) 74 return cls(version, options) 75 76 77class SetDateMessage(I2CPMessage): 78 """Type 33: R→C send current date. Payload: date(8) + version.""" 79 80 TYPE = 33 81 82 def __init__(self, date_ms: int, version: str = "0.9.62"): 83 self.date_ms = date_ms 84 self.version = version 85 86 def payload_bytes(self) -> bytes: 87 vb = self.version.encode("utf-8") 88 return struct.pack("!Q", self.date_ms) + struct.pack("!B", len(vb)) + vb 89 90 @classmethod 91 def _from_payload(cls, payload: bytes) -> "SetDateMessage": 92 date_ms = struct.unpack("!Q", payload[:8])[0] 93 vlen = payload[8] 94 version = payload[9:9 + vlen].decode("utf-8") 95 return cls(date_ms, version) 96 97 98class SessionStatusMessage(I2CPMessage): 99 """Type 20: R→C session creation result. session_id(2) + status(1).""" 100 101 TYPE = 20 102 103 STATUS_DESTROYED = 0 104 STATUS_CREATED = 1 105 STATUS_UPDATED = 2 106 STATUS_INVALID = 3 107 STATUS_REFUSED = 4 108 109 def __init__(self, session_id: int, status: int): 110 self.session_id = session_id 111 self.status = status 112 113 def payload_bytes(self) -> bytes: 114 return struct.pack("!HB", self.session_id, self.status) 115 116 @classmethod 117 def _from_payload(cls, payload: bytes) -> "SessionStatusMessage": 118 session_id, status = struct.unpack("!HB", payload[:3]) 119 return cls(session_id, status) 120 121 122class CreateSessionMessage(I2CPMessage): 123 """Type 1: C→R create session. destination_data + options.""" 124 125 TYPE = 1 126 127 def __init__(self, destination_data: bytes, options: dict[str, str] | None = None): 128 self.destination_data = destination_data 129 self.options = options or {} 130 131 def payload_bytes(self) -> bytes: 132 opts = _encode_properties(self.options) 133 return (struct.pack("!H", len(self.destination_data)) + 134 self.destination_data + opts) 135 136 @classmethod 137 def _from_payload(cls, payload: bytes) -> "CreateSessionMessage": 138 dest_len = struct.unpack("!H", payload[:2])[0] 139 dest_data = payload[2:2 + dest_len] 140 opts, _ = _decode_properties(payload[2 + dest_len:]) 141 return cls(dest_data, opts) 142 143 144class DestroySessionMessage(I2CPMessage): 145 """Type 3: C→R destroy session. session_id(2).""" 146 147 TYPE = 3 148 149 def __init__(self, session_id: int): 150 self.session_id = session_id 151 152 def payload_bytes(self) -> bytes: 153 return struct.pack("!H", self.session_id) 154 155 @classmethod 156 def _from_payload(cls, payload: bytes) -> "DestroySessionMessage": 157 session_id = struct.unpack("!H", payload[:2])[0] 158 return cls(session_id) 159 160 161class SendMessageMessage(I2CPMessage): 162 """Type 5: C→R send message. 163 164 Wire format: session_id(2) + Destination(self-delimiting) + 165 Payload(4-byte size + data) + nonce(4). 166 """ 167 168 TYPE = 5 169 170 def __init__(self, session_id: int, destination_data: bytes, 171 payload: bytes, nonce: int = 0): 172 self.session_id = session_id 173 self.destination_data = destination_data 174 self.payload = payload 175 self.nonce = nonce 176 177 def payload_bytes(self) -> bytes: 178 return (struct.pack("!H", self.session_id) + 179 self.destination_data + 180 struct.pack("!I", len(self.payload)) + self.payload + 181 struct.pack("!I", self.nonce)) 182 183 @classmethod 184 def _from_payload(cls, payload: bytes) -> "SendMessageMessage": 185 session_id = struct.unpack("!H", payload[:2])[0] 186 dest_data, dest_len = _parse_destination_from(payload, 2) 187 off = 2 + dest_len 188 pl_len = struct.unpack("!I", payload[off:off + 4])[0] 189 off += 4 190 pl = payload[off:off + pl_len] 191 off += pl_len 192 nonce = struct.unpack("!I", payload[off:off + 4])[0] 193 return cls(session_id, dest_data, pl, nonce) 194 195 196class MessageStatusMessage(I2CPMessage): 197 """Type 22: R→C delivery status. session_id(2) + msg_id(4) + nonce(4) + status(1) + size(4).""" 198 199 TYPE = 22 200 201 STATUS_AVAILABLE = 0 202 STATUS_SEND_ACCEPTED = 1 203 STATUS_SEND_BEST_EFFORT_SUCCESS = 2 204 STATUS_SEND_BEST_EFFORT_FAILURE = 3 205 STATUS_SEND_GUARANTEED_SUCCESS = 4 206 STATUS_SEND_GUARANTEED_FAILURE = 5 207 STATUS_SEND_SUCCESS_LOCAL = 6 208 STATUS_SEND_FAILURE_LOCAL = 7 209 STATUS_SEND_FAILURE_ROUTER = 8 210 STATUS_SEND_FAILURE_NETWORK = 9 211 STATUS_SEND_FAILURE_BAD_SESSION = 10 212 STATUS_SEND_FAILURE_BAD_MESSAGE = 11 213 STATUS_SEND_FAILURE_BAD_OPTIONS = 12 214 STATUS_SEND_FAILURE_OVERFLOW = 13 215 STATUS_SEND_FAILURE_EXPIRED = 14 216 STATUS_SEND_FAILURE_LOCAL_LEASESET = 15 217 STATUS_SEND_FAILURE_NO_TUNNELS = 16 218 STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION = 17 219 STATUS_SEND_FAILURE_DESTINATION = 18 220 STATUS_SEND_FAILURE_BAD_LEASESET = 19 221 STATUS_SEND_FAILURE_EXPIRED_LEASESET = 20 222 STATUS_SEND_FAILURE_NO_LEASESET = 21 223 224 _SUCCESS_STATUSES = frozenset({1, 2, 4, 6}) 225 _FAILURE_STATUSES = frozenset({3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}) 226 227 _STATUS_NAMES = { 228 0: "AVAILABLE", 229 1: "SEND_ACCEPTED", 230 2: "SEND_BEST_EFFORT_SUCCESS", 231 3: "SEND_BEST_EFFORT_FAILURE", 232 4: "SEND_GUARANTEED_SUCCESS", 233 5: "SEND_GUARANTEED_FAILURE", 234 6: "SEND_SUCCESS_LOCAL", 235 7: "SEND_FAILURE_LOCAL", 236 8: "SEND_FAILURE_ROUTER", 237 9: "SEND_FAILURE_NETWORK", 238 10: "SEND_FAILURE_BAD_SESSION", 239 11: "SEND_FAILURE_BAD_MESSAGE", 240 12: "SEND_FAILURE_BAD_OPTIONS", 241 13: "SEND_FAILURE_OVERFLOW", 242 14: "SEND_FAILURE_EXPIRED", 243 15: "SEND_FAILURE_LOCAL_LEASESET", 244 16: "SEND_FAILURE_NO_TUNNELS", 245 17: "SEND_FAILURE_UNSUPPORTED_ENCRYPTION", 246 18: "SEND_FAILURE_DESTINATION", 247 19: "SEND_FAILURE_BAD_LEASESET", 248 20: "SEND_FAILURE_EXPIRED_LEASESET", 249 21: "SEND_FAILURE_NO_LEASESET", 250 } 251 252 def __init__(self, session_id: int, msg_id: int, nonce: int, status: int, size: int): 253 self.session_id = session_id 254 self.msg_id = msg_id 255 self.nonce = nonce 256 self.status = status 257 self.size = size 258 259 def is_success(self) -> bool: 260 return self.status in self._SUCCESS_STATUSES 261 262 def is_failure(self) -> bool: 263 return self.status in self._FAILURE_STATUSES 264 265 def status_name(self) -> str: 266 return self._STATUS_NAMES.get(self.status, f"UNKNOWN_{self.status}") 267 268 def payload_bytes(self) -> bytes: 269 return struct.pack("!HIIBI", self.session_id, self.msg_id, 270 self.nonce, self.status, self.size) 271 272 @classmethod 273 def _from_payload(cls, payload: bytes) -> "MessageStatusMessage": 274 session_id, msg_id, nonce, status, size = struct.unpack("!HIIBI", payload[:15]) 275 return cls(session_id, msg_id, nonce, status, size) 276 277 278class MessagePayloadMessage(I2CPMessage): 279 """Type 31: R→C message payload. session_id(2) + msg_id(4) + payload_len(4) + payload.""" 280 281 TYPE = 31 282 283 def __init__(self, session_id: int, msg_id: int, payload: bytes): 284 self.session_id = session_id 285 self.msg_id = msg_id 286 self.payload = payload 287 288 def payload_bytes(self) -> bytes: 289 return (struct.pack("!HII", self.session_id, self.msg_id, len(self.payload)) + 290 self.payload) 291 292 @classmethod 293 def _from_payload(cls, payload: bytes) -> "MessagePayloadMessage": 294 session_id, msg_id, pl_len = struct.unpack("!HII", payload[:10]) 295 pl = payload[10:10 + pl_len] 296 return cls(session_id, msg_id, pl) 297 298 299class ReceiveMessageBeginMessage(I2CPMessage): 300 """Type 6: R→C notify message available. session_id(2) + msg_id(4).""" 301 302 TYPE = 6 303 304 def __init__(self, session_id: int, msg_id: int): 305 self.session_id = session_id 306 self.msg_id = msg_id 307 308 def payload_bytes(self) -> bytes: 309 return struct.pack("!HI", self.session_id, self.msg_id) 310 311 @classmethod 312 def _from_payload(cls, payload: bytes) -> "ReceiveMessageBeginMessage": 313 session_id, msg_id = struct.unpack("!HI", payload[:6]) 314 return cls(session_id, msg_id) 315 316 317class ReceiveMessageEndMessage(I2CPMessage): 318 """Type 7: C→R acknowledge message received. session_id(2) + msg_id(4).""" 319 320 TYPE = 7 321 322 def __init__(self, session_id: int, msg_id: int): 323 self.session_id = session_id 324 self.msg_id = msg_id 325 326 def payload_bytes(self) -> bytes: 327 return struct.pack("!HI", self.session_id, self.msg_id) 328 329 @classmethod 330 def _from_payload(cls, payload: bytes) -> "ReceiveMessageEndMessage": 331 session_id, msg_id = struct.unpack("!HI", payload[:6]) 332 return cls(session_id, msg_id) 333 334 335class HostLookupMessage(I2CPMessage): 336 """Type 38: C→R resolve hostname. 337 338 Wire format: session_id(2) + request_id(4) + timeout(4) + 339 lookup_type(1) + [hash(32) | hostname(1-byte len + str)]. 340 Java type codes: 0=LOOKUP_HASH, 1=LOOKUP_HOST. 341 """ 342 343 TYPE = 38 344 345 LOOKUP_HASH = 0 346 LOOKUP_HOST = 1 347 348 def __init__(self, session_id: int, request_id: int, 349 hostname: str | None = None, dest_hash: bytes | None = None, 350 timeout: int = 10000): 351 self.session_id = session_id 352 self.request_id = request_id 353 self.hostname = hostname 354 self.dest_hash = dest_hash 355 self.timeout = timeout 356 357 def payload_bytes(self) -> bytes: 358 parts = [struct.pack("!HII", self.session_id, self.request_id, self.timeout)] 359 if self.dest_hash is not None: 360 parts.append(struct.pack("!B", self.LOOKUP_HASH)) # 0 = hash 361 parts.append(self.dest_hash) 362 elif self.hostname is not None: 363 hb = self.hostname.encode("utf-8") 364 parts.append(struct.pack("!BB", self.LOOKUP_HOST, len(hb))) # 1 = host, 1-byte len 365 parts.append(hb) 366 return b"".join(parts) 367 368 @classmethod 369 def _from_payload(cls, payload: bytes) -> "HostLookupMessage": 370 session_id, request_id, timeout = struct.unpack("!HII", payload[:10]) 371 lookup_type = payload[10] 372 hostname = None 373 dest_hash = None 374 if lookup_type == cls.LOOKUP_HASH: 375 dest_hash = payload[11:11 + 32] 376 elif lookup_type == cls.LOOKUP_HOST: 377 name_len = payload[11] 378 hostname = payload[12:12 + name_len].decode("utf-8") 379 return cls(session_id, request_id, hostname=hostname, 380 dest_hash=dest_hash, timeout=timeout) 381 382 383class HostReplyMessage(I2CPMessage): 384 """Type 39: R→C hostname resolution result.""" 385 386 TYPE = 39 387 388 RESULT_SUCCESS = 0 389 RESULT_FAILURE = 1 390 RESULT_SECRET_REQUIRED = 2 391 RESULT_KEY_REQUIRED = 3 392 RESULT_SECRET_AND_KEY_REQUIRED = 4 393 RESULT_DECRYPTION_FAILURE = 5 394 395 def __init__(self, session_id: int, request_id: int, result_code: int, 396 destination_data: bytes | None = None): 397 self.session_id = session_id 398 self.request_id = request_id 399 self.result_code = result_code 400 self.destination_data = destination_data 401 402 def payload_bytes(self) -> bytes: 403 parts = [struct.pack("!HIB", self.session_id, self.request_id, self.result_code)] 404 if self.result_code == 0 and self.destination_data is not None: 405 parts.append(self.destination_data) # self-delimiting, no length prefix 406 return b"".join(parts) 407 408 @classmethod 409 def _from_payload(cls, payload: bytes) -> "HostReplyMessage": 410 session_id, request_id, result_code = struct.unpack("!HIB", payload[:7]) 411 dest_data = None 412 if result_code == 0 and len(payload) > 7: 413 dest_data = payload[7:] # remaining bytes are the destination 414 return cls(session_id, request_id, result_code, dest_data) 415 416 417class DisconnectMessage(I2CPMessage): 418 """Type 30: R→C disconnect. reason string.""" 419 420 TYPE = 30 421 422 def __init__(self, reason: str = ""): 423 self.reason = reason 424 425 def payload_bytes(self) -> bytes: 426 rb = self.reason.encode("utf-8") 427 return struct.pack("!B", len(rb)) + rb 428 429 @classmethod 430 def _from_payload(cls, payload: bytes) -> "DisconnectMessage": 431 if not payload: 432 return cls("") 433 rlen = payload[0] 434 reason = payload[1:1 + rlen].decode("utf-8") 435 return cls(reason) 436 437 438class GetBandwidthLimitsMessage(I2CPMessage): 439 """Type 8: C→R request bandwidth limits. Empty payload.""" 440 441 TYPE = 8 442 443 def payload_bytes(self) -> bytes: 444 return b"" 445 446 @classmethod 447 def _from_payload(cls, payload: bytes) -> "GetBandwidthLimitsMessage": 448 return cls() 449 450 451class BandwidthLimitsMessage(I2CPMessage): 452 """Type 23: R→C bandwidth limits. 16 x int32 = 64 bytes.""" 453 454 TYPE = 23 455 456 def __init__(self, limits: list[int]): 457 if len(limits) != 16: 458 raise ValueError(f"Expected 16 limit values, got {len(limits)}") 459 self.limits = list(limits) 460 461 def payload_bytes(self) -> bytes: 462 return struct.pack("!16i", *self.limits) 463 464 @classmethod 465 def _from_payload(cls, payload: bytes) -> "BandwidthLimitsMessage": 466 values = list(struct.unpack("!16i", payload[:64])) 467 return cls(values) 468 469 @property 470 def client_inbound(self) -> int: 471 return self.limits[0] 472 473 @property 474 def client_outbound(self) -> int: 475 return self.limits[1] 476 477 @property 478 def router_inbound(self) -> int: 479 return self.limits[2] 480 481 @property 482 def router_outbound(self) -> int: 483 return self.limits[4] 484 485 486class DestLookupMessage(I2CPMessage): 487 """Type 34: C→R legacy dest lookup. dest_hash(32).""" 488 489 TYPE = 34 490 491 def __init__(self, dest_hash: bytes): 492 if len(dest_hash) != 32: 493 raise ValueError(f"dest_hash must be 32 bytes, got {len(dest_hash)}") 494 self.dest_hash = dest_hash 495 496 def payload_bytes(self) -> bytes: 497 return self.dest_hash 498 499 @classmethod 500 def _from_payload(cls, payload: bytes) -> "DestLookupMessage": 501 return cls(payload[:32]) 502 503 504class DestReplyMessage(I2CPMessage): 505 """Type 35: R→C dest lookup result. Variable length.""" 506 507 TYPE = 35 508 509 def __init__(self, destination_data: bytes | None = None, 510 hash_data: bytes | None = None): 511 self.destination_data = destination_data 512 self.hash_data = hash_data 513 514 def payload_bytes(self) -> bytes: 515 if self.destination_data is not None: 516 return self.destination_data 517 if self.hash_data is not None: 518 return self.hash_data 519 return b"" 520 521 @classmethod 522 def _from_payload(cls, payload: bytes) -> "DestReplyMessage": 523 if len(payload) == 0: 524 return cls() # failure, no data 525 if len(payload) == 32: 526 return cls(hash_data=payload) # hash echo (failure) 527 return cls(destination_data=payload) # success 528 529 530class ReportAbuseMessage(I2CPMessage): 531 """Type 29: bidirectional abuse report. session_id(2) + severity(1) + reason + msg_id(4).""" 532 533 TYPE = 29 534 535 def __init__(self, session_id: int, severity: int, reason: str, message_id: int): 536 self.session_id = session_id 537 self.severity = severity 538 self.reason = reason 539 self.message_id = message_id 540 541 def payload_bytes(self) -> bytes: 542 rb = self.reason.encode("utf-8") 543 return (struct.pack("!HB", self.session_id, self.severity) + 544 struct.pack("!B", len(rb)) + rb + 545 struct.pack("!I", self.message_id)) 546 547 @classmethod 548 def _from_payload(cls, payload: bytes) -> "ReportAbuseMessage": 549 session_id, severity = struct.unpack("!HB", payload[:3]) 550 rlen = payload[3] 551 reason = payload[4:4 + rlen].decode("utf-8") 552 message_id = struct.unpack("!I", payload[4 + rlen:8 + rlen])[0] 553 return cls(session_id, severity, reason, message_id) 554 555 556class RequestLeaseSetMessage(I2CPMessage): 557 """Type 21: R→C request lease set. 558 559 Wire format: session_id(2) + num_tunnels(1) + 560 [router_hash(32) + tunnel_id(4)] * N + end_date(8). 561 """ 562 563 TYPE = 21 564 565 def __init__(self, session_id: int, 566 tunnels: list[tuple[bytes, int]], 567 end_date: int): 568 self.session_id = session_id 569 self.tunnels = tunnels 570 self.end_date = end_date 571 572 def payload_bytes(self) -> bytes: 573 parts = [struct.pack("!HB", self.session_id, len(self.tunnels))] 574 for router_hash, tunnel_id in self.tunnels: 575 parts.append(router_hash) 576 parts.append(struct.pack("!I", tunnel_id)) 577 parts.append(struct.pack("!Q", self.end_date)) 578 return b"".join(parts) 579 580 @classmethod 581 def _from_payload(cls, payload: bytes) -> "RequestLeaseSetMessage": 582 session_id, num_tunnels = struct.unpack("!HB", payload[:3]) 583 offset = 3 584 tunnels = [] 585 for _ in range(num_tunnels): 586 router_hash = payload[offset:offset + 32] 587 tunnel_id = struct.unpack("!I", payload[offset + 32:offset + 36])[0] 588 tunnels.append((router_hash, tunnel_id)) 589 offset += 36 590 end_date = struct.unpack("!Q", payload[offset:offset + 8])[0] 591 return cls(session_id, tunnels, end_date) 592 593 594class RequestVariableLeaseSetMessage(I2CPMessage): 595 """Type 37: R→C request variable lease set. 596 597 Wire format: session_id(2) + num_leases(1) + 598 [tunnel_gw(32) + tunnel_id(4) + end_date(8)] * N. 599 """ 600 601 TYPE = 37 602 603 def __init__(self, session_id: int, 604 leases: list[tuple[bytes, int, int]]): 605 self.session_id = session_id 606 self.leases = leases 607 608 def payload_bytes(self) -> bytes: 609 parts = [struct.pack("!HB", self.session_id, len(self.leases))] 610 for gw_hash, tunnel_id, end_date_ms in self.leases: 611 parts.append(gw_hash) 612 parts.append(struct.pack("!IQ", tunnel_id, end_date_ms)) 613 return b"".join(parts) 614 615 @classmethod 616 def _from_payload(cls, payload: bytes) -> "RequestVariableLeaseSetMessage": 617 session_id, num_leases = struct.unpack("!HB", payload[:3]) 618 offset = 3 619 leases = [] 620 for _ in range(num_leases): 621 gw_hash = payload[offset:offset + 32] 622 tunnel_id, end_date_ms = struct.unpack( 623 "!IQ", payload[offset + 32:offset + 44]) 624 leases.append((gw_hash, tunnel_id, end_date_ms)) 625 offset += 44 626 return cls(session_id, leases) 627 628 629class CreateLeaseSetMessage(I2CPMessage): 630 """Type 4: C→R create lease set. 631 632 Wire format: session_id(2) + signing_private_key_len(2) + 633 signing_private_key + private_key_len(2) + private_key + 634 lease_set_data. 635 """ 636 637 TYPE = 4 638 639 def __init__(self, session_id: int, signing_private_key: bytes, 640 private_key: bytes, lease_set_data: bytes): 641 self.session_id = session_id 642 self.signing_private_key = signing_private_key 643 self.private_key = private_key 644 self.lease_set_data = lease_set_data 645 646 def payload_bytes(self) -> bytes: 647 return (struct.pack("!HH", self.session_id, 648 len(self.signing_private_key)) + 649 self.signing_private_key + 650 struct.pack("!H", len(self.private_key)) + 651 self.private_key + 652 self.lease_set_data) 653 654 @classmethod 655 def _from_payload(cls, payload: bytes) -> "CreateLeaseSetMessage": 656 session_id, spk_len = struct.unpack("!HH", payload[:4]) 657 offset = 4 658 signing_private_key = payload[offset:offset + spk_len] 659 offset += spk_len 660 pk_len = struct.unpack("!H", payload[offset:offset + 2])[0] 661 offset += 2 662 private_key = payload[offset:offset + pk_len] 663 offset += pk_len 664 lease_set_data = payload[offset:] 665 return cls(session_id, signing_private_key, private_key, 666 lease_set_data) 667 668 669class CreateLeaseSet2Message(I2CPMessage): 670 """Type 41: C→R create lease set 2. 671 672 Wire format: session_id(2) + ls_type(1) + lease_set_data_len(4) + 673 lease_set_data + num_keys(1) + 674 [enc_type(2) + enc_len(2) + priv_key(enc_len)] * N. 675 """ 676 677 TYPE = 41 678 679 LS_TYPE_LEASESET = 1 680 LS_TYPE_LS2 = 3 681 LS_TYPE_ENCRYPTED_LS2 = 5 682 LS_TYPE_META_LS2 = 7 683 684 def __init__(self, session_id: int, ls_type: int, 685 lease_set_data: bytes, 686 private_keys: list[tuple[int, bytes]]): 687 self.session_id = session_id 688 self.ls_type = ls_type 689 self.lease_set_data = lease_set_data 690 self.private_keys = private_keys 691 692 def payload_bytes(self) -> bytes: 693 parts = [ 694 struct.pack("!HBI", self.session_id, self.ls_type, 695 len(self.lease_set_data)), 696 self.lease_set_data, 697 struct.pack("!B", len(self.private_keys)), 698 ] 699 for enc_type, priv_key in self.private_keys: 700 parts.append(struct.pack("!HH", enc_type, len(priv_key))) 701 parts.append(priv_key) 702 return b"".join(parts) 703 704 @classmethod 705 def _from_payload(cls, payload: bytes) -> "CreateLeaseSet2Message": 706 session_id, ls_type, ls_data_len = struct.unpack( 707 "!HBI", payload[:7]) 708 offset = 7 709 lease_set_data = payload[offset:offset + ls_data_len] 710 offset += ls_data_len 711 num_keys = payload[offset] 712 offset += 1 713 private_keys = [] 714 for _ in range(num_keys): 715 enc_type, enc_len = struct.unpack( 716 "!HH", payload[offset:offset + 4]) 717 offset += 4 718 priv_key = payload[offset:offset + enc_len] 719 offset += enc_len 720 private_keys.append((enc_type, priv_key)) 721 return cls(session_id, ls_type, lease_set_data, private_keys) 722 723 724def _parse_destination_from(data: bytes, offset: int) -> tuple[bytes, int]: 725 """Parse a self-delimiting Destination from data at offset. 726 727 Returns (destination_bytes, total_dest_length). 728 The destination is: 256 (pub) + 128 (sig) + cert (3 + cert_payload_len). 729 """ 730 cert_start = offset + 384 # 256 + 128 731 if len(data) < cert_start + 3: 732 raise ValueError("Data too short for destination") 733 cert_payload_len = struct.unpack("!H", data[cert_start + 1:cert_start + 3])[0] 734 dest_len = 384 + 3 + cert_payload_len 735 return data[offset:offset + dest_len], dest_len 736 737 738class DateAndFlags: 739 """Combined date + flags for SendMessageExpiresMessage. 740 741 Wire format: 8 bytes total. Upper 16 bits = flags, lower 48 bits = date_ms. 742 """ 743 744 SEND_RELIABLE = 0x0001 745 REQUEST_LEASESET = 0x0002 746 747 def __init__(self, date_ms: int = 0, flags: int = 0): 748 self.date_ms = date_ms 749 self.flags = flags 750 751 def to_bytes(self) -> bytes: 752 combined = (self.flags << 48) | (self.date_ms & 0xFFFFFFFFFFFF) 753 return struct.pack("!Q", combined) 754 755 @classmethod 756 def from_bytes(cls, data: bytes) -> "DateAndFlags": 757 combined = struct.unpack("!Q", data[:8])[0] 758 flags = (combined >> 48) & 0xFFFF 759 date_ms = combined & 0xFFFFFFFFFFFF 760 return cls(date_ms, flags) 761 762 763class ReconfigureSessionMessage(I2CPMessage): 764 """Type 2: C→R reconfigure session. session_id(2) + SessionConfig(var).""" 765 766 TYPE = 2 767 768 def __init__(self, session_id: int, session_config): 769 self.session_id = session_id 770 self.session_config = session_config 771 772 def payload_bytes(self) -> bytes: 773 return struct.pack("!H", self.session_id) + self.session_config.to_bytes() 774 775 @classmethod 776 def _from_payload(cls, payload: bytes) -> "ReconfigureSessionMessage": 777 from i2p_client.session_config import WireSessionConfig 778 session_id = struct.unpack("!H", payload[:2])[0] 779 sc = WireSessionConfig.from_bytes(payload[2:]) 780 return cls(session_id, sc) 781 782 783class SendMessageExpiresMessage(I2CPMessage): 784 """Type 36: C→R send message with expiration. 785 786 Wire format: session_id(2) + Destination(self-delimiting) + 787 Payload(4-byte size + data) + nonce(4) + expiration(8). 788 """ 789 790 TYPE = 36 791 792 def __init__(self, session_id: int, destination_data: bytes, 793 payload: bytes, nonce: int = 0, 794 expiration: DateAndFlags | None = None): 795 self.session_id = session_id 796 self.destination_data = destination_data 797 self.payload = payload 798 self.nonce = nonce 799 self.expiration = expiration or DateAndFlags() 800 801 def payload_bytes(self) -> bytes: 802 return (struct.pack("!H", self.session_id) + 803 self.destination_data + 804 struct.pack("!I", len(self.payload)) + self.payload + 805 struct.pack("!I", self.nonce) + 806 self.expiration.to_bytes()) 807 808 @classmethod 809 def _from_payload(cls, payload: bytes) -> "SendMessageExpiresMessage": 810 session_id = struct.unpack("!H", payload[:2])[0] 811 dest_data, dest_len = _parse_destination_from(payload, 2) 812 off = 2 + dest_len 813 pl_len = struct.unpack("!I", payload[off:off + 4])[0] 814 off += 4 815 pl = payload[off:off + pl_len] 816 off += pl_len 817 nonce = struct.unpack("!I", payload[off:off + 4])[0] 818 off += 4 819 expiration = DateAndFlags.from_bytes(payload[off:off + 8]) 820 return cls(session_id, dest_data, pl, nonce, expiration) 821 822 823# SigType code -> public key length (bytes) for BlindingInfoMessage endpoint type 3 824_SIGTYPE_PUBKEY_LEN = { 825 0: 128, # DSA_SHA1 826 1: 64, # ECDSA_SHA256_P256 827 2: 96, # ECDSA_SHA384_P384 828 3: 132, # ECDSA_SHA512_P521 829 7: 32, # EdDSA_SHA512_Ed25519 830 11: 32, # RedDSA_SHA512_Ed25519 831} 832 833 834class BlindingInfoMessage(I2CPMessage): 835 """I2CP BlindingInfo message (Type 42, C->R). 836 837 Tells the router about blinding configuration for encrypted LeaseSet2. 838 839 Wire format: 840 session_id(2) + endpoint_type(1) + auth_type(1) + blind_type(2) + 841 expiration(4) + [endpoint_data] + [auth_key(32)] 842 """ 843 844 TYPE = 42 845 846 # Endpoint type constants 847 ENDPOINT_HASH = 0 848 ENDPOINT_HOST = 1 849 ENDPOINT_DEST = 2 850 ENDPOINT_PUBKEY = 3 851 852 # Auth type constants 853 AUTH_NONE = 0 854 AUTH_DH = 1 855 AUTH_PSK = 2 856 857 def __init__( 858 self, 859 session_id: int, 860 endpoint_type: int, 861 auth_type: int, 862 blind_type: int, 863 expiration: int, 864 endpoint_data: bytes, 865 auth_key: bytes | None = None, 866 ) -> None: 867 self.session_id = session_id 868 self.endpoint_type = endpoint_type 869 self.auth_type = auth_type 870 self.blind_type = blind_type 871 self.expiration = expiration 872 self.endpoint_data = endpoint_data 873 self.auth_key = auth_key 874 875 def payload_bytes(self) -> bytes: 876 parts = [ 877 struct.pack("!H", self.session_id), 878 struct.pack("!B", self.endpoint_type), 879 struct.pack("!B", self.auth_type), 880 struct.pack("!H", self.blind_type), 881 struct.pack("!I", self.expiration), 882 self.endpoint_data, 883 ] 884 if self.auth_type != self.AUTH_NONE and self.auth_key is not None: 885 parts.append(self.auth_key) 886 return b"".join(parts) 887 888 @classmethod 889 def _from_payload(cls, payload: bytes) -> "BlindingInfoMessage": 890 if len(payload) < 10: 891 raise ValueError("BlindingInfoMessage payload too short") 892 893 session_id = struct.unpack("!H", payload[0:2])[0] 894 endpoint_type = payload[2] 895 auth_type = payload[3] 896 blind_type = struct.unpack("!H", payload[4:6])[0] 897 expiration = struct.unpack("!I", payload[6:10])[0] 898 899 offset = 10 900 901 # Parse endpoint data based on type 902 if endpoint_type == cls.ENDPOINT_HASH: 903 endpoint_data = payload[offset:offset + 32] 904 offset += 32 905 elif endpoint_type == cls.ENDPOINT_HOST: 906 host_len = payload[offset] 907 endpoint_data = payload[offset:offset + 1 + host_len] 908 offset += 1 + host_len 909 elif endpoint_type == cls.ENDPOINT_DEST: 910 endpoint_data, dest_len = _parse_destination_from(payload, offset) 911 offset += dest_len 912 elif endpoint_type == cls.ENDPOINT_PUBKEY: 913 sig_type_code = struct.unpack("!H", payload[offset:offset + 2])[0] 914 pubkey_len = _SIGTYPE_PUBKEY_LEN.get(sig_type_code) 915 if pubkey_len is None: 916 raise ValueError(f"Unknown SigType code {sig_type_code}") 917 ep_len = 2 + pubkey_len 918 endpoint_data = payload[offset:offset + ep_len] 919 offset += ep_len 920 else: 921 raise ValueError(f"Unknown endpoint type {endpoint_type}") 922 923 # Parse auth key if present 924 auth_key = None 925 if auth_type != cls.AUTH_NONE: 926 auth_key = payload[offset:offset + 32] 927 offset += 32 928 929 return cls( 930 session_id=session_id, 931 endpoint_type=endpoint_type, 932 auth_type=auth_type, 933 blind_type=blind_type, 934 expiration=expiration, 935 endpoint_data=endpoint_data, 936 auth_key=auth_key, 937 ) 938 939 940def _encode_properties(props: dict[str, str]) -> bytes: 941 """Encode properties as I2P wire format: count(2) + (key_len(2) + key + val_len(2) + val)*.""" 942 parts = [struct.pack("!H", len(props))] 943 for k, v in sorted(props.items()): 944 kb = k.encode("utf-8") 945 vb = v.encode("utf-8") 946 parts.append(struct.pack("!H", len(kb)) + kb + struct.pack("!H", len(vb)) + vb) 947 return b"".join(parts) 948 949 950def _decode_properties(data: bytes) -> tuple[dict[str, str], int]: 951 """Decode properties from I2P wire format. Returns (dict, bytes_consumed).""" 952 if len(data) < 2: 953 return {}, 0 954 count = struct.unpack("!H", data[:2])[0] 955 offset = 2 956 props = {} 957 for _ in range(count): 958 klen = struct.unpack("!H", data[offset:offset + 2])[0] 959 offset += 2 960 key = data[offset:offset + klen].decode("utf-8") 961 offset += klen 962 vlen = struct.unpack("!H", data[offset:offset + 2])[0] 963 offset += 2 964 val = data[offset:offset + vlen].decode("utf-8") 965 offset += vlen 966 props[key] = val 967 return props, offset