A Python port of the Invisible Internet Project (I2P)
1"""MessageValidator — reject expired, far-future, and duplicate I2NP messages.
2
3Ported from net.i2p.router.MessageValidator.
4
5Uses a DecayingBloomFilter to detect duplicate messages. Messages are
6identified by their (message_id XOR truncated_expiration) fingerprint.
7"""
8
9from __future__ import annotations
10
11import struct
12import time
13
14from i2p_router.bloom_filter import DecayingBloomFilter
15
16
17class MessageValidator:
18 """Validate inbound I2NP messages for expiration and duplicates."""
19
20 # Java I2P uses 65 seconds of clock fudge
21 CLOCK_FUDGE_FACTOR_MS = 65_000
22
23 def __init__(self, bloom_filter: DecayingBloomFilter | None = None) -> None:
24 if bloom_filter is None:
25 bloom_filter = DecayingBloomFilter(
26 "msg-validator", duration_seconds=600, expected_entries=50000
27 )
28 self._bloom = bloom_filter
29
30 def validate_message(
31 self, message_id: int, expiration_ms: int, now_ms: int | None = None
32 ) -> bool:
33 """Return True if the message should be accepted.
34
35 Rejects expired, far-future, and duplicate messages.
36 """
37 if now_ms is None:
38 now_ms = int(time.time() * 1000)
39
40 if self._is_expired(expiration_ms, now_ms):
41 return False
42 if self._is_too_far_future(expiration_ms, now_ms):
43 return False
44 if self._is_duplicate(message_id, expiration_ms):
45 return False
46 return True
47
48 def _is_expired(self, expiration_ms: int, now_ms: int) -> bool:
49 """True if message has expired (with 1.5x fudge)."""
50 return now_ms > expiration_ms + int(self.CLOCK_FUDGE_FACTOR_MS * 1.5)
51
52 def _is_too_far_future(self, expiration_ms: int, now_ms: int) -> bool:
53 """True if expiration is unreasonably far in the future."""
54 return expiration_ms > now_ms + self.CLOCK_FUDGE_FACTOR_MS * 4
55
56 def _is_duplicate(self, message_id: int, expiration_ms: int) -> bool:
57 """True if we've already seen this message."""
58 # Fingerprint: XOR message_id with truncated expiration
59 exp_trunc = expiration_ms & 0xFFFFFFFF
60 fingerprint = struct.pack("!I", message_id ^ exp_trunc)
61 return self._bloom.add(fingerprint)