"""CLI entry point for I2PTunnel: i2p-tunnel command. Usage: i2p-tunnel start [--config PATH] [--sam-host HOST] [--sam-port PORT] i2p-tunnel list [--config PATH] i2p-tunnel add --name NAME --type TYPE --port PORT ... """ from __future__ import annotations import argparse import asyncio import logging import os import signal import sys from pathlib import Path from i2p_apps.i2ptunnel.group import TunnelControllerGroup logger = logging.getLogger(__name__) DEFAULT_CONFIG_DIR = Path(os.environ.get("I2P_DATA_DIR", Path.home() / ".i2p-python")) DEFAULT_CONFIG = DEFAULT_CONFIG_DIR / "i2ptunnel.config" def _get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="i2p-tunnel", description="I2P tunnel manager — HTTP proxy, SOCKS proxy, server tunnels", ) sub = parser.add_subparsers(dest="command") # start start_p = sub.add_parser("start", help="Start tunnels from config") start_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, help="Config file path") start_p.add_argument("--sam-host", default="127.0.0.1", help="SAM bridge host") start_p.add_argument("--sam-port", type=int, default=7656, help="SAM bridge port") # list list_p = sub.add_parser("list", help="List configured tunnels") list_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, help="Config file path") # add add_p = sub.add_parser("add", help="Add a tunnel to config") add_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, help="Config file path") add_p.add_argument("--name", required=True, help="Tunnel name") add_p.add_argument("--type", required=True, dest="tunnel_type", help="Tunnel type (httpclient, server, etc.)") add_p.add_argument("--port", type=int, required=True, help="Listen port (clients) or target port (servers)") add_p.add_argument("--interface", default="127.0.0.1", help="Listen interface") add_p.add_argument("--target", default="", help="Target destination (clients) or host (servers)") add_p.add_argument("--start-on-load", action="store_true", help="Start automatically") return parser async def _run_start(args: argparse.Namespace) -> None: """Start all configured tunnels and run until interrupted.""" if not args.config.exists(): print(f"Config file not found: {args.config}", file=sys.stderr) sys.exit(1) group = TunnelControllerGroup(args.config, args.sam_host, args.sam_port) await group.load_and_start() started = sum(1 for c in group.controllers if c.state.value == "running") total = len(group.controllers) print(f"Started {started}/{total} tunnels") # Wait for interrupt stop_event = asyncio.Event() loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, stop_event.set) await stop_event.wait() print("\nShutting down tunnels...") await group.stop_all() print("All tunnels stopped.") def _run_list(args: argparse.Namespace) -> None: """List configured tunnels.""" from i2p_apps.i2ptunnel.config import TunnelConfigParser if not args.config.exists(): print(f"Config file not found: {args.config}", file=sys.stderr) sys.exit(1) tunnels = TunnelConfigParser.load(args.config) if not tunnels: print("No tunnels configured.") return for i, t in enumerate(tunnels): auto = " [auto]" if t.start_on_load else "" dest = t.target_destination or f"{t.target_host}:{t.target_port}" or "" print(f" {i}: {t.name} ({t.type.value}) " f"-> {dest} on {t.interface}:{t.listen_port}{auto}") def _run_add(args: argparse.Namespace) -> None: """Add a tunnel to config file.""" from i2p_apps.i2ptunnel.config import TunnelConfigParser, TunnelDefinition, TunnelType try: tunnel_type = TunnelType(args.tunnel_type) except ValueError: print(f"Unknown tunnel type: {args.tunnel_type}", file=sys.stderr) sys.exit(1) # Load existing tunnels = TunnelConfigParser.load(args.config) if args.config.exists() else [] td = TunnelDefinition( name=args.name, type=tunnel_type, listen_port=args.port if tunnel_type.is_client else 0, target_port=args.port if tunnel_type.is_server else 0, target_destination=args.target if tunnel_type.is_client else "", target_host=args.target if tunnel_type.is_server else "", interface=args.interface, start_on_load=args.start_on_load, ) tunnels.append(td) args.config.parent.mkdir(parents=True, exist_ok=True) TunnelConfigParser.save(args.config, tunnels) print(f"Added tunnel {args.name!r} ({tunnel_type.value})") def main() -> None: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") parser = _get_parser() args = parser.parse_args() if args.command == "start": asyncio.run(_run_start(args)) elif args.command == "list": _run_list(args) elif args.command == "add": _run_add(args) else: parser.print_help() sys.exit(1)