A Python port of the Invisible Internet Project (I2P)
at main 164 lines 5.9 kB view raw
1"""First-run configuration wizard — interactive and non-interactive modes. 2 3Runs three-layer diagnostics and produces a router config: 4 Layer 1: Local-only probes (CPU, RAM, disk) 5 Layer 2: Passive network probes (STUN, DNS, RTT, bandwidth) 6 Layer 3: Post-bootstrap (deferred to router runtime) 7""" 8 9from __future__ import annotations 10 11import json 12import logging 13import os 14from pathlib import Path 15 16from i2p_apps.setup.config_advisor import advise 17from i2p_apps.setup.local_probe import run_local_probes 18from i2p_apps.setup.network_probe import run_network_probes 19from i2p_apps.setup.stun_probe import detect_nat_type 20 21log = logging.getLogger(__name__) 22 23CONFIG_FILENAME = "router.config.json" 24 25 26class SetupWizard: 27 """First-run configuration wizard.""" 28 29 def __init__(self, data_dir: str = "~/.i2p-python"): 30 self.data_dir = os.path.expanduser(data_dir) 31 os.makedirs(self.data_dir, exist_ok=True) 32 33 @property 34 def config_path(self) -> str: 35 return os.path.join(self.data_dir, CONFIG_FILENAME) 36 37 def is_first_run(self) -> bool: 38 """Check if router.config.json exists.""" 39 return not os.path.exists(self.config_path) 40 41 def save_config(self, config: dict) -> None: 42 """Save config to data_dir/router.config.json.""" 43 with open(self.config_path, "w") as f: 44 json.dump(config, f, indent=2) 45 log.info("Config saved to %s", self.config_path) 46 47 def load_config(self) -> dict | None: 48 """Load saved config, or None if first run.""" 49 if not os.path.exists(self.config_path): 50 return None 51 with open(self.config_path) as f: 52 return json.load(f) 53 54 def run_auto(self, mode: str = "normal") -> dict: 55 """Run non-interactive wizard. 56 57 Modes: 58 - normal: all probes including network 59 - paranoid: local probes only, no network traffic 60 - performance: all probes, maximize resource allocation 61 """ 62 log.info("Running setup wizard in %s mode", mode) 63 64 # Layer 1: Local probes (always run) 65 local = run_local_probes(data_dir=self.data_dir) 66 log.info( 67 "Local probes: %d crypto ops/s, %d MB RAM, %.0f MB/s disk", 68 local.crypto_ops_per_sec, 69 local.available_ram_bytes // (1024 * 1024), 70 local.disk_write_mbps, 71 ) 72 73 nat = None 74 network = None 75 76 if mode != "paranoid": 77 # Layer 2a: STUN NAT detection 78 nat = detect_nat_type() 79 log.info("NAT probe: type=%s, external=%s:%s", 80 nat.nat_type, nat.external_ip, nat.external_port) 81 82 # Layer 2b+2c: Network probes 83 network = run_network_probes( 84 include_bandwidth=(mode != "paranoid"), 85 ) 86 log.info("Network probe: DNS=%s, RTT=%.0fms", 87 network.dns.can_resolve_neutral, network.latency.rtt_ms) 88 89 # Generate recommendation 90 recommendation = advise(local, nat=nat, network=network) 91 92 config = recommendation.to_router_config_overrides() 93 config["mode"] = mode 94 config["version"] = 1 95 96 if recommendation.warnings: 97 for w in recommendation.warnings: 98 log.warning("Setup: %s", w) 99 100 self.save_config(config) 101 return config 102 103 def run_interactive(self) -> dict: 104 """Run interactive CLI wizard. Returns config dict. 105 106 For non-TTY environments, falls back to run_auto("normal"). 107 """ 108 if not os.isatty(0): 109 return self.run_auto("normal") 110 111 print("\nWelcome to I2P Python Router\n") 112 print("Checking your system...\n") 113 114 # Layer 1 115 local = run_local_probes(data_dir=self.data_dir) 116 print(f" CPU: {local.crypto_ops_per_sec:,} crypto ops/sec") 117 print(f" RAM: {local.available_ram_bytes // (1024**3)} GB available") 118 print(f" Disk: {local.disk_write_mbps:.0f} MB/s, " 119 f"{local.disk_free_bytes // (1024**3)} GB free\n") 120 121 # Ask about network tests 122 print("Would you like to test your network connection?") 123 print("This uses standard protocols (STUN, HTTPS) that look like") 124 print("normal video call and web browsing traffic.\n") 125 print("[Y]es [S]kip [P]aranoid (no network tests at all)") 126 127 choice = input("> ").strip().lower() 128 129 nat = None 130 network = None 131 132 if choice in ("y", "yes", ""): 133 nat = detect_nat_type() 134 network = run_network_probes() 135 print(f"\n NAT: {nat.nat_type}") 136 if nat.external_ip: 137 print(f" External IP: {nat.external_ip}") 138 print(f" DNS: {'OK' if network.dns.can_resolve_neutral else 'FAILED'}") 139 print(f" Latency: {network.latency.rtt_ms:.0f}ms median") 140 if network.bandwidth: 141 print(f" Bandwidth: ~{network.bandwidth.download_mbps:.0f} Mbps") 142 143 recommendation = advise(local, nat=nat, network=network) 144 config = recommendation.to_router_config_overrides() 145 146 print(f"\nRecommended configuration:") 147 print(f" Bandwidth share: {config['share_percentage']}%") 148 print(f" Transit tunnels: {config['inbound_tunnel_count']}") 149 print(f" Floodfill: {'yes' if config['floodfill'] else 'no'}") 150 print(f" Transport port: {config['listen_port']}") 151 print(f" NAT traversal: {'UPnP enabled' if config['upnp_enabled'] else 'disabled'}") 152 153 for w in recommendation.warnings: 154 print(f" WARNING: {w}") 155 156 print("\n[A]ccept [Q]uit") 157 accept = input("> ").strip().lower() 158 159 if accept in ("a", "accept", ""): 160 config["version"] = 1 161 self.save_config(config) 162 return config 163 else: 164 raise SystemExit("Setup cancelled.")