A Python port of the Invisible Internet Project (I2P)
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)