diff --git a/src/bridge/config.py b/src/bridge/config.py index 1000be3..241d22e 100644 --- a/src/bridge/config.py +++ b/src/bridge/config.py @@ -87,6 +87,10 @@ def _parse_tunnel(name: str, data: dict) -> TunnelConfig: timeout_seconds=hc.get("timeout_seconds", 5), ) + direction = str(data.get("direction", "reverse")) + if direction not in ("reverse", "local"): + raise ConfigError(f"Tunnel '{name}' direction must be 'reverse' or 'local', got: {direction!r}") + return TunnelConfig( name=name, host=str(data["host"]), @@ -97,6 +101,7 @@ def _parse_tunnel(name: str, data: dict) -> TunnelConfig: actor=str(data["actor"]), reconnect=reconnect, health_check=health_check, + direction=direction, ) diff --git a/src/bridge/manager.py b/src/bridge/manager.py index 2a91e04..371ae41 100644 --- a/src/bridge/manager.py +++ b/src/bridge/manager.py @@ -18,12 +18,16 @@ log = logging.getLogger(__name__) def build_ssh_command(cfg: TunnelConfig) -> List[str]: - """Build the SSH reverse tunnel command.""" + """Build the SSH tunnel command (reverse -R or local -L).""" key = os.path.expanduser(cfg.ssh_key) + if cfg.direction == "local": + forward_flag = ["-L", f"{cfg.local_port}:127.0.0.1:{cfg.remote_port}"] + else: + forward_flag = ["-R", f"{cfg.remote_port}:127.0.0.1:{cfg.local_port}"] return [ "ssh", "-N", - "-R", f"{cfg.remote_port}:127.0.0.1:{cfg.local_port}", + *forward_flag, "-i", key, "-o", "ServerAliveInterval=10", "-o", "ServerAliveCountMax=3", diff --git a/src/bridge/models.py b/src/bridge/models.py index 8beca65..899f0d4 100644 --- a/src/bridge/models.py +++ b/src/bridge/models.py @@ -40,6 +40,7 @@ class TunnelConfig: actor: str reconnect: ReconnectPolicy = field(default_factory=ReconnectPolicy) health_check: Optional[HealthCheckConfig] = None + direction: str = "reverse" # "reverse" (-R) or "local" (-L) @dataclass