A Python port of the Invisible Internet Project (I2P)
1"""I2PTunnel controller — manages lifecycle of a single tunnel.
2
3Ported from net.i2p.i2ptunnel.TunnelController.
4"""
5
6from __future__ import annotations
7
8import enum
9import logging
10
11from i2p_apps.i2ptunnel.config import TunnelDefinition
12
13logger = logging.getLogger(__name__)
14
15
16class TunnelState(enum.Enum):
17 STOPPED = "stopped"
18 STARTING = "starting"
19 RUNNING = "running"
20 STOPPING = "stopping"
21 DESTROYED = "destroyed"
22
23
24class TunnelController:
25 """Manages the lifecycle of a single I2P tunnel.
26
27 State machine: STOPPED -> STARTING -> RUNNING -> STOPPING -> STOPPED
28 DESTROYED is terminal.
29 """
30
31 def __init__(self, config: TunnelDefinition, session_factory) -> None:
32 self._config = config
33 self._session_factory = session_factory
34 self._state = TunnelState.STOPPED
35 self._session = None
36 self._tunnel_task = None
37
38 @property
39 def state(self) -> TunnelState:
40 return self._state
41
42 @property
43 def config(self) -> TunnelDefinition:
44 return self._config
45
46 @property
47 def session(self):
48 return self._session
49
50 async def start(self) -> None:
51 """Start the tunnel: create session and tunnel task."""
52 if self._state == TunnelState.DESTROYED:
53 raise RuntimeError("Cannot start a destroyed tunnel")
54 if self._state == TunnelState.RUNNING:
55 return # already running
56
57 self._state = TunnelState.STARTING
58 logger.info("Starting tunnel %r", self._config.name)
59
60 try:
61 self._session = await self._session_factory.create(
62 self._config,
63 priv_key=self._config.priv_key_file or None,
64 )
65 self._state = TunnelState.RUNNING
66 logger.info("Tunnel %r is RUNNING", self._config.name)
67 except Exception:
68 self._state = TunnelState.STOPPED
69 logger.exception("Failed to start tunnel %r", self._config.name)
70 raise
71
72 async def stop(self) -> None:
73 """Stop the tunnel: release session and clean up."""
74 if self._state in (TunnelState.STOPPED, TunnelState.DESTROYED):
75 return # nothing to do
76
77 self._state = TunnelState.STOPPING
78 logger.info("Stopping tunnel %r", self._config.name)
79
80 if self._session is not None:
81 try:
82 await self._session_factory.release(self._session)
83 except Exception:
84 logger.exception("Error releasing session for tunnel %r",
85 self._config.name)
86 self._session = None
87
88 self._tunnel_task = None
89 self._state = TunnelState.STOPPED
90 logger.info("Tunnel %r is STOPPED", self._config.name)
91
92 async def restart(self) -> None:
93 """Stop then start the tunnel."""
94 await self.stop()
95 await self.start()
96
97 def destroy(self) -> None:
98 """Mark tunnel as destroyed — cannot be restarted."""
99 self._state = TunnelState.DESTROYED
100 logger.info("Tunnel %r DESTROYED", self._config.name)