A Python port of the Invisible Internet Project (I2P)
at main 296 lines 10 kB view raw
1"""Peer profiles, selection, and banning. 2 3Ported from net.i2p.router.peermanager.PeerProfile. 4""" 5 6import time 7 8 9class PeerProfile: 10 """Full peer profile with reputation metrics. 11 12 Ported from net.i2p.router.peermanager.PeerProfile. 13 """ 14 15 def __init__(self, router_hash: bytes): 16 self.router_hash = router_hash 17 18 # Tunnel building 19 self.tunnel_builds_succeeded = 0 20 self.tunnel_builds_failed = 0 21 self.tunnel_builds_rejected = 0 22 23 # Communication 24 self.send_success_count = 0 25 self.send_failure_count = 0 26 self.db_store_success_count = 0 27 self.db_store_failure_count = 0 28 self.db_lookup_success_count = 0 29 self.db_lookup_failure_count = 0 30 31 # Timing 32 self.last_heard_from: float = 0.0 33 self.first_heard_about: float = 0.0 34 self.last_send_success: float = 0.0 35 self.last_send_failure: float = 0.0 36 37 # Computed scores (updated by calculators) 38 self.capacity: float = 0.0 39 self.speed: float = 0.0 40 self.integration: float = 0.0 41 42 # Classification 43 self.is_expanding: bool = False 44 self.is_active: bool = True 45 self.is_banned: bool = False 46 self.ban_expiry: float = 0.0 47 48 # Latency tracking 49 self._latency_samples: list[float] = [] 50 self._max_latency_samples = 50 51 52 # Legacy compat 53 self._successes = 0 54 self._failures = 0 55 56 # --- Tunnel build recording --- 57 58 def record_tunnel_build_success(self) -> None: 59 self.tunnel_builds_succeeded += 1 60 61 def record_tunnel_build_failure(self) -> None: 62 self.tunnel_builds_failed += 1 63 64 def record_tunnel_build_rejection(self) -> None: 65 self.tunnel_builds_rejected += 1 66 67 # --- Communication recording --- 68 69 def record_send_success(self) -> None: 70 self.send_success_count += 1 71 self.last_send_success = time.time() 72 73 def record_send_failure(self) -> None: 74 self.send_failure_count += 1 75 self.last_send_failure = time.time() 76 77 def record_db_store_success(self) -> None: 78 self.db_store_success_count += 1 79 80 def record_db_store_failure(self) -> None: 81 self.db_store_failure_count += 1 82 83 def record_db_lookup_success(self) -> None: 84 self.db_lookup_success_count += 1 85 86 def record_db_lookup_failure(self) -> None: 87 self.db_lookup_failure_count += 1 88 89 # --- Latency --- 90 91 def record_latency(self, latency_ms: float) -> None: 92 self._latency_samples.append(latency_ms) 93 if len(self._latency_samples) > self._max_latency_samples: 94 self._latency_samples = self._latency_samples[-self._max_latency_samples:] 95 96 # --- Activity --- 97 98 def heard_from(self) -> None: 99 now = time.time() 100 self.last_heard_from = now 101 if self.first_heard_about == 0.0: 102 self.first_heard_about = now 103 104 # --- Computed properties --- 105 106 @property 107 def tunnel_success_rate(self) -> float: 108 total = self.tunnel_builds_succeeded + self.tunnel_builds_failed 109 if total == 0: 110 return 0.0 111 return self.tunnel_builds_succeeded / total 112 113 @property 114 def send_success_rate(self) -> float: 115 total = self.send_success_count + self.send_failure_count 116 if total == 0: 117 return 0.0 118 return self.send_success_count / total 119 120 @property 121 def db_success_rate(self) -> float: 122 total = (self.db_store_success_count + self.db_store_failure_count + 123 self.db_lookup_success_count + self.db_lookup_failure_count) 124 if total == 0: 125 return 0.0 126 successes = self.db_store_success_count + self.db_lookup_success_count 127 return successes / total 128 129 @property 130 def average_latency(self) -> float: 131 if not self._latency_samples: 132 return 0.0 133 return sum(self._latency_samples) / len(self._latency_samples) 134 135 @property 136 def is_established(self) -> bool: 137 """Has enough history to be classified.""" 138 total_tunnel = self.tunnel_builds_succeeded + self.tunnel_builds_failed 139 total_send = self.send_success_count + self.send_failure_count 140 return total_tunnel >= 5 and total_send >= 5 141 142 # --- Ban management --- 143 144 def ban(self, duration_seconds: float) -> None: 145 self.is_banned = True 146 self.ban_expiry = time.time() + duration_seconds 147 148 def unban(self) -> None: 149 self.is_banned = False 150 self.ban_expiry = 0.0 151 152 @property 153 def is_currently_banned(self) -> bool: 154 if not self.is_banned: 155 return False 156 if time.time() >= self.ban_expiry: 157 self.is_banned = False 158 self.ban_expiry = 0.0 159 return False 160 return True 161 162 # --- Serialization --- 163 164 def to_dict(self) -> dict: 165 return { 166 "router_hash": self.router_hash.hex(), 167 "tunnel_builds_succeeded": self.tunnel_builds_succeeded, 168 "tunnel_builds_failed": self.tunnel_builds_failed, 169 "tunnel_builds_rejected": self.tunnel_builds_rejected, 170 "send_success_count": self.send_success_count, 171 "send_failure_count": self.send_failure_count, 172 "db_store_success_count": self.db_store_success_count, 173 "db_store_failure_count": self.db_store_failure_count, 174 "db_lookup_success_count": self.db_lookup_success_count, 175 "db_lookup_failure_count": self.db_lookup_failure_count, 176 "last_heard_from": self.last_heard_from, 177 "first_heard_about": self.first_heard_about, 178 "last_send_success": self.last_send_success, 179 "last_send_failure": self.last_send_failure, 180 "capacity": self.capacity, 181 "speed": self.speed, 182 "integration": self.integration, 183 "is_expanding": self.is_expanding, 184 "is_active": self.is_active, 185 "is_banned": self.is_banned, 186 "ban_expiry": self.ban_expiry, 187 "latency_samples": list(self._latency_samples), 188 } 189 190 @classmethod 191 def from_dict(cls, data: dict) -> "PeerProfile": 192 p = cls(bytes.fromhex(data["router_hash"])) 193 p.tunnel_builds_succeeded = data["tunnel_builds_succeeded"] 194 p.tunnel_builds_failed = data["tunnel_builds_failed"] 195 p.tunnel_builds_rejected = data["tunnel_builds_rejected"] 196 p.send_success_count = data["send_success_count"] 197 p.send_failure_count = data["send_failure_count"] 198 p.db_store_success_count = data["db_store_success_count"] 199 p.db_store_failure_count = data["db_store_failure_count"] 200 p.db_lookup_success_count = data["db_lookup_success_count"] 201 p.db_lookup_failure_count = data["db_lookup_failure_count"] 202 p.last_heard_from = data["last_heard_from"] 203 p.first_heard_about = data["first_heard_about"] 204 p.last_send_success = data["last_send_success"] 205 p.last_send_failure = data["last_send_failure"] 206 p.capacity = data["capacity"] 207 p.speed = data["speed"] 208 p.integration = data["integration"] 209 p.is_expanding = data["is_expanding"] 210 p.is_active = data["is_active"] 211 p.is_banned = data["is_banned"] 212 p.ban_expiry = data["ban_expiry"] 213 p._latency_samples = list(data.get("latency_samples", [])) 214 return p 215 216 # --- Legacy compatibility --- 217 218 def record_success(self): 219 self._successes += 1 220 self.capacity = self._successes / (self._successes + self._failures + 1) 221 222 def record_failure(self): 223 self._failures += 1 224 self.capacity = self._successes / (self._successes + self._failures + 1) 225 226 def tunnel_build_success(self): 227 self.tunnel_builds_succeeded += 1 228 229 def tunnel_build_failure(self): 230 self.tunnel_builds_failed += 1 231 232 def update_last_heard(self, timestamp_ms: int): 233 self.last_heard_from = timestamp_ms 234 235 def decay(self, factor: float = 0.95): 236 self.capacity *= factor 237 self._successes = int(self._successes * factor) 238 self._failures = int(self._failures * factor) 239 240 def score(self) -> float: 241 total = self.tunnel_builds_succeeded + self.tunnel_builds_failed 242 tsr = self.tunnel_builds_succeeded / total if total > 0 else 0.0 243 return self.capacity + tsr * 0.5 244 245 246class PeerSelector: 247 """Select peers by profile score.""" 248 249 def __init__(self, profiles: list[PeerProfile]): 250 self._profiles = profiles 251 252 def select_best(self, n: int, exclude: set[bytes] | None = None) -> list[PeerProfile]: 253 exclude = exclude or set() 254 candidates = [p for p in self._profiles if p.router_hash not in exclude] 255 candidates.sort(key=lambda p: p.score(), reverse=True) 256 return candidates[:n] 257 258 259class BanManager: 260 """Temporary ban management for misbehaving peers.""" 261 262 def __init__(self): 263 self._bans: dict[bytes, dict] = {} 264 265 def ban(self, router_hash: bytes, reason: str, duration_ms: int, 266 now_ms: int | None = None): 267 if now_ms is None: 268 now_ms = int(time.time() * 1000) 269 self._bans[router_hash] = { 270 "reason": reason, 271 "expires": now_ms + duration_ms, 272 } 273 274 def unban(self, router_hash: bytes): 275 self._bans.pop(router_hash, None) 276 277 def is_banned(self, router_hash: bytes, now_ms: int | None = None) -> bool: 278 info = self._bans.get(router_hash) 279 if info is None: 280 return False 281 if now_ms is None: 282 now_ms = int(time.time() * 1000) 283 if now_ms >= info["expires"]: 284 del self._bans[router_hash] 285 return False 286 return True 287 288 def ban_count(self) -> int: 289 return len(self._bans) 290 291 def cleanup(self, now_ms: int | None = None): 292 if now_ms is None: 293 now_ms = int(time.time() * 1000) 294 expired = [h for h, info in self._bans.items() if now_ms >= info["expires"]] 295 for h in expired: 296 del self._bans[h]