A Python port of the Invisible Internet Project (I2P)
1"""Config advisor — maps probe results to recommended RouterConfig values.
2
3Takes LocalProbeResult, NatProbeResult, and NetworkProbeResult and produces
4a ConfigRecommendation with explanations and warnings.
5"""
6
7from __future__ import annotations
8
9import socket
10from dataclasses import dataclass, field
11
12from i2p_apps.setup.local_probe import LocalProbeResult
13from i2p_apps.setup.stun_probe import NatProbeResult
14from i2p_apps.setup.network_probe import NetworkProbeResult
15
16GB = 1_073_741_824 # 1 GiB in bytes
17
18
19@dataclass
20class ConfigRecommendation:
21 """Recommended config values with explanations."""
22
23 bandwidth_limit_kbps: int = 0
24 share_percentage: int = 50
25 inbound_tunnel_count: int = 5
26 outbound_tunnel_count: int = 5
27 floodfill: bool = False
28 nat_type: str = "unknown"
29 external_ip: str | None = None
30 upnp_enabled: bool = True
31 listen_port: int = 9700
32 warnings: list[str] = field(default_factory=list)
33 notes: list[str] = field(default_factory=list)
34
35 def to_router_config_overrides(self) -> dict:
36 """Return dict of RouterConfig field overrides."""
37 return {
38 "bandwidth_limit_kbps": self.bandwidth_limit_kbps,
39 "share_percentage": self.share_percentage,
40 "inbound_tunnel_count": self.inbound_tunnel_count,
41 "outbound_tunnel_count": self.outbound_tunnel_count,
42 "floodfill": self.floodfill,
43 "nat_type": self.nat_type,
44 "external_ip": self.external_ip,
45 "upnp_enabled": self.upnp_enabled,
46 "listen_port": self.listen_port,
47 }
48
49
50def _find_available_port(preferred: int) -> int:
51 """Check if preferred port is available, return it or next available."""
52 try:
53 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
54 s.bind(("0.0.0.0", preferred))
55 return preferred
56 except OSError:
57 # Port in use, try next
58 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
59 s.bind(("0.0.0.0", 0))
60 return s.getsockname()[1]
61
62
63def advise(
64 local: LocalProbeResult,
65 nat: NatProbeResult | None = None,
66 network: NetworkProbeResult | None = None,
67) -> ConfigRecommendation:
68 """Map probe results to recommended config values."""
69 warnings: list[str] = []
70 notes: list[str] = []
71
72 # CPU → tunnel count + floodfill eligibility
73 crypto = local.crypto_ops_per_sec
74 if crypto > 50000:
75 tunnels = 20
76 floodfill_eligible = True
77 elif crypto > 10000:
78 tunnels = 10
79 floodfill_eligible = True
80 elif crypto > 2000:
81 tunnels = 5
82 floodfill_eligible = False
83 else:
84 tunnels = 2
85 floodfill_eligible = False
86
87 # Bandwidth → share percentage + limit
88 share = 50
89 limit = 0
90 if network and network.bandwidth:
91 bw = network.bandwidth.download_mbps
92 if bw >= 100:
93 share = 80
94 limit = 80_000
95 elif bw >= 10:
96 share = 60
97 limit = int(bw * 600)
98 elif bw >= 1:
99 share = 40
100 limit = int(bw * 400)
101 else:
102 share = 30
103 limit = int(bw * 300)
104
105 # NAT → UPnP + warnings
106 upnp = True
107 nat_type = "unknown"
108 external_ip = None
109
110 if nat:
111 nat_type = nat.nat_type
112 external_ip = nat.external_ip
113
114 if nat.nat_type == "symmetric":
115 warnings.append(
116 "Symmetric NAT detected — inbound connections will not work. "
117 "SSU2 relay mode recommended."
118 )
119 upnp = False
120 elif nat.nat_type == "cone":
121 upnp = True
122 elif not nat.nat_present:
123 upnp = False
124 notes.append("Open internet detected — no NAT traversal needed.")
125
126 # Floodfill: requires good CPU + good bandwidth + non-symmetric NAT
127 floodfill = (
128 floodfill_eligible
129 and share >= 60
130 and (nat is None or nat.nat_type != "symmetric")
131 )
132
133 # Port selection
134 listen_port = _find_available_port(9700)
135
136 return ConfigRecommendation(
137 bandwidth_limit_kbps=limit,
138 share_percentage=share,
139 inbound_tunnel_count=tunnels,
140 outbound_tunnel_count=tunnels,
141 floodfill=floodfill,
142 nat_type=nat_type,
143 external_ip=external_ip,
144 upnp_enabled=upnp,
145 listen_port=listen_port,
146 warnings=warnings,
147 notes=notes,
148 )