A Python port of the Invisible Internet Project (I2P)
at main 155 lines 5.4 kB view raw
1"""CLI entry point for I2PTunnel: i2p-tunnel command. 2 3Usage: 4 i2p-tunnel start [--config PATH] [--sam-host HOST] [--sam-port PORT] 5 i2p-tunnel list [--config PATH] 6 i2p-tunnel add --name NAME --type TYPE --port PORT ... 7""" 8 9from __future__ import annotations 10 11import argparse 12import asyncio 13import logging 14import os 15import signal 16import sys 17from pathlib import Path 18 19from i2p_apps.i2ptunnel.group import TunnelControllerGroup 20 21logger = logging.getLogger(__name__) 22 23DEFAULT_CONFIG_DIR = Path(os.environ.get("I2P_DATA_DIR", Path.home() / ".i2p-python")) 24DEFAULT_CONFIG = DEFAULT_CONFIG_DIR / "i2ptunnel.config" 25 26 27def _get_parser() -> argparse.ArgumentParser: 28 parser = argparse.ArgumentParser( 29 prog="i2p-tunnel", 30 description="I2P tunnel manager — HTTP proxy, SOCKS proxy, server tunnels", 31 ) 32 sub = parser.add_subparsers(dest="command") 33 34 # start 35 start_p = sub.add_parser("start", help="Start tunnels from config") 36 start_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, 37 help="Config file path") 38 start_p.add_argument("--sam-host", default="127.0.0.1", 39 help="SAM bridge host") 40 start_p.add_argument("--sam-port", type=int, default=7656, 41 help="SAM bridge port") 42 43 # list 44 list_p = sub.add_parser("list", help="List configured tunnels") 45 list_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, 46 help="Config file path") 47 48 # add 49 add_p = sub.add_parser("add", help="Add a tunnel to config") 50 add_p.add_argument("--config", type=Path, default=DEFAULT_CONFIG, 51 help="Config file path") 52 add_p.add_argument("--name", required=True, help="Tunnel name") 53 add_p.add_argument("--type", required=True, dest="tunnel_type", 54 help="Tunnel type (httpclient, server, etc.)") 55 add_p.add_argument("--port", type=int, required=True, 56 help="Listen port (clients) or target port (servers)") 57 add_p.add_argument("--interface", default="127.0.0.1", 58 help="Listen interface") 59 add_p.add_argument("--target", default="", 60 help="Target destination (clients) or host (servers)") 61 add_p.add_argument("--start-on-load", action="store_true", 62 help="Start automatically") 63 64 return parser 65 66 67async def _run_start(args: argparse.Namespace) -> None: 68 """Start all configured tunnels and run until interrupted.""" 69 if not args.config.exists(): 70 print(f"Config file not found: {args.config}", file=sys.stderr) 71 sys.exit(1) 72 73 group = TunnelControllerGroup(args.config, args.sam_host, args.sam_port) 74 await group.load_and_start() 75 76 started = sum(1 for c in group.controllers if c.state.value == "running") 77 total = len(group.controllers) 78 print(f"Started {started}/{total} tunnels") 79 80 # Wait for interrupt 81 stop_event = asyncio.Event() 82 loop = asyncio.get_running_loop() 83 for sig in (signal.SIGINT, signal.SIGTERM): 84 loop.add_signal_handler(sig, stop_event.set) 85 86 await stop_event.wait() 87 print("\nShutting down tunnels...") 88 await group.stop_all() 89 print("All tunnels stopped.") 90 91 92def _run_list(args: argparse.Namespace) -> None: 93 """List configured tunnels.""" 94 from i2p_apps.i2ptunnel.config import TunnelConfigParser 95 96 if not args.config.exists(): 97 print(f"Config file not found: {args.config}", file=sys.stderr) 98 sys.exit(1) 99 100 tunnels = TunnelConfigParser.load(args.config) 101 if not tunnels: 102 print("No tunnels configured.") 103 return 104 105 for i, t in enumerate(tunnels): 106 auto = " [auto]" if t.start_on_load else "" 107 dest = t.target_destination or f"{t.target_host}:{t.target_port}" or "" 108 print(f" {i}: {t.name} ({t.type.value}) " 109 f"-> {dest} on {t.interface}:{t.listen_port}{auto}") 110 111 112def _run_add(args: argparse.Namespace) -> None: 113 """Add a tunnel to config file.""" 114 from i2p_apps.i2ptunnel.config import TunnelConfigParser, TunnelDefinition, TunnelType 115 116 try: 117 tunnel_type = TunnelType(args.tunnel_type) 118 except ValueError: 119 print(f"Unknown tunnel type: {args.tunnel_type}", file=sys.stderr) 120 sys.exit(1) 121 122 # Load existing 123 tunnels = TunnelConfigParser.load(args.config) if args.config.exists() else [] 124 125 td = TunnelDefinition( 126 name=args.name, 127 type=tunnel_type, 128 listen_port=args.port if tunnel_type.is_client else 0, 129 target_port=args.port if tunnel_type.is_server else 0, 130 target_destination=args.target if tunnel_type.is_client else "", 131 target_host=args.target if tunnel_type.is_server else "", 132 interface=args.interface, 133 start_on_load=args.start_on_load, 134 ) 135 tunnels.append(td) 136 137 args.config.parent.mkdir(parents=True, exist_ok=True) 138 TunnelConfigParser.save(args.config, tunnels) 139 print(f"Added tunnel {args.name!r} ({tunnel_type.value})") 140 141 142def main() -> None: 143 logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") 144 parser = _get_parser() 145 args = parser.parse_args() 146 147 if args.command == "start": 148 asyncio.run(_run_start(args)) 149 elif args.command == "list": 150 _run_list(args) 151 elif args.command == "add": 152 _run_add(args) 153 else: 154 parser.print_help() 155 sys.exit(1)