A Python port of the Invisible Internet Project (I2P)
1"""Peer selector for tunnel building — selects peers by tier and generates hop configs.
2
3Ported from net.i2p.router.tunnel.pool.TunnelPeerSelector.
4"""
5
6from __future__ import annotations
7
8import os
9import struct
10from typing import TYPE_CHECKING
11
12from i2p_peer.hop_config import TunnelHopConfig
13from i2p_peer.organizer import PeerTier, _TIER_ORDER
14
15if TYPE_CHECKING:
16 from i2p_peer.organizer import ProfileOrganizer
17
18
19class PeerSelector:
20 """Selects peers for tunnel building based on profile scores."""
21
22 def __init__(self, organizer: ProfileOrganizer):
23 self._organizer = organizer
24
25 def select_peers(self, count: int, exclude: set[bytes] | None = None) -> list[bytes]:
26 """Select count peers, preferring higher-tier peers.
27
28 Selection strategy:
29 1. Walk tiers from FAST down to STANDARD, collecting eligible peers.
30 2. Exclude any in the exclude set.
31 3. Return top ``count`` peers (tier order preserved).
32 """
33 exclude = exclude or set()
34 selected: list[bytes] = []
35
36 # Walk tiers best-to-worst, skip FAILING and BANNED
37 eligible_tiers = [PeerTier.FAST, PeerTier.HIGH_CAPACITY, PeerTier.STANDARD]
38 for tier in eligible_tiers:
39 tier_hashes = self._organizer._tiers.get(tier, set())
40 for h in tier_hashes:
41 if h not in exclude:
42 selected.append(h)
43 if len(selected) >= count:
44 return selected[:count]
45
46 return selected[:count]
47
48 def select_hops(self, length: int, exclude: set[bytes] | None = None) -> list[TunnelHopConfig]:
49 """Select peers and generate full hop configs for tunnel building.
50
51 - First hop is gateway (is_gateway=True)
52 - Last hop is endpoint (is_endpoint=True)
53 - A single hop is both gateway and endpoint
54 - Each hop gets random keys generated with os.urandom()
55 - receive_tunnel_id is a random positive uint32
56 """
57 peer_hashes = self.select_peers(length, exclude=exclude)
58 if not peer_hashes:
59 return []
60
61 hops: list[TunnelHopConfig] = []
62 for i, peer_hash in enumerate(peer_hashes):
63 # Random positive tunnel ID (1 to 2^32-1)
64 tid = struct.unpack("!I", os.urandom(4))[0]
65 if tid == 0:
66 tid = 1 # ensure positive
67
68 hop = TunnelHopConfig(
69 peer_hash=peer_hash,
70 receive_tunnel_id=tid,
71 layer_key=os.urandom(32),
72 iv_key=os.urandom(32),
73 reply_key=os.urandom(32),
74 reply_iv=os.urandom(16),
75 is_gateway=(i == 0),
76 is_endpoint=(i == len(peer_hashes) - 1),
77 )
78 hops.append(hop)
79
80 return hops