from __future__ import annotations import argparse import select import socket import sys import threading import time import paramiko def _bridge_channels(chan: paramiko.Channel, upstream: socket.socket) -> None: try: while True: readers, _, _ = select.select([chan, upstream], [], [], 1.0) if chan in readers: data = chan.recv(4096) if not data: break upstream.sendall(data) if upstream in readers: data = upstream.recv(4096) if not data: break chan.sendall(data) finally: try: chan.close() except Exception: pass try: upstream.close() except Exception: pass def _accept_channels(transport: paramiko.Transport, local_host: str, local_port: int) -> None: while transport.is_active(): chan = transport.accept(1000) if chan is None: continue try: upstream = socket.create_connection((local_host, local_port), timeout=10) except OSError: chan.close() continue threading.Thread(target=_bridge_channels, args=(chan, upstream), daemon=True).start() def main() -> int: parser = argparse.ArgumentParser(description="Maintain reverse SSH tunnel for Asterisk IVR FastAGI.") parser.add_argument("--ssh-host", required=True) parser.add_argument("--ssh-port", type=int, default=22) parser.add_argument("--ssh-user", required=True) parser.add_argument("--ssh-password", required=True) parser.add_argument("--remote-bind-host", default="127.0.0.1") parser.add_argument("--remote-port", type=int, default=4573) parser.add_argument("--local-host", default="127.0.0.1") parser.add_argument("--local-port", type=int, default=4573) args = parser.parse_args() while True: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( args.ssh_host, port=args.ssh_port, username=args.ssh_user, password=args.ssh_password, timeout=15, banner_timeout=15, auth_timeout=15, ) transport = client.get_transport() if transport is None: raise RuntimeError("SSH transport is unavailable") transport.request_port_forward(args.remote_bind_host, args.remote_port) print( f"Reverse tunnel active: {args.remote_bind_host}:{args.remote_port} -> " f"{args.local_host}:{args.local_port}", flush=True, ) _accept_channels(transport, args.local_host, args.local_port) except KeyboardInterrupt: return 0 except Exception as exc: print(f"Reverse tunnel error: {exc}", file=sys.stderr, flush=True) time.sleep(3) finally: try: client.close() except Exception: pass if __name__ == "__main__": raise SystemExit(main())