ProximileAdmin commited on
Commit
ff2fb98
·
verified ·
1 Parent(s): 386c753

Create ssh_tunneler.py

Browse files
Files changed (1) hide show
  1. ssh_tunneler.py +355 -0
ssh_tunneler.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HTTP Tunnel over SSH using Paramiko with improved monitoring and reliability
4
+
5
+ This script establishes an HTTP tunnel over SSH using the Paramiko library
6
+ with additional monitoring capabilities and automatic reconnection.
7
+ """
8
+
9
+ import socket
10
+ import select
11
+ import threading
12
+ import logging
13
+ import paramiko
14
+ import time
15
+
16
+
17
+ class SSHTunnel:
18
+ """
19
+ A class that establishes an HTTP tunnel over SSH using Paramiko with improved monitoring.
20
+
21
+ This creates a secure tunnel that forwards traffic from a local port
22
+ to a remote HTTP port through an SSH connection and automatically reconnects if needed.
23
+ """
24
+
25
+ def __init__(self, ssh_host, remote_port=80, local_port=80, ssh_port=22,
26
+ username=None, password=None, key_filename=None,
27
+ reconnect_interval=30, keep_alive_interval=15):
28
+ """
29
+ Initialize the SSH tunnel with the given parameters.
30
+
31
+ Args:
32
+ ssh_host (str): The SSH server hostname or IP address
33
+ remote_port (int): The remote HTTP port to forward to (default: 80)
34
+ local_port (int): The local port to listen on (default: 80)
35
+ ssh_port (int): The SSH server port (default: 22)
36
+ username (str): The SSH username
37
+ password (str): The SSH password (optional if using key_filename)
38
+ key_filename (str): Path to private key file (optional if using password)
39
+ reconnect_interval (int): Seconds to wait before reconnecting (default: 30)
40
+ keep_alive_interval (int): Seconds between keep-alive packets (default: 15)
41
+ """
42
+ self.ssh_host = ssh_host
43
+ self.remote_port = remote_port
44
+ self.local_port = local_port
45
+ self.ssh_port = ssh_port
46
+ self.username = username
47
+ self.password = password
48
+ self.key_filename = key_filename
49
+ self.reconnect_interval = reconnect_interval
50
+ self.keep_alive_interval = keep_alive_interval
51
+
52
+ self.server_socket = None
53
+ self.transport = None
54
+ self.client = None
55
+ self.is_running = False
56
+ self.should_reconnect = True
57
+ self.last_activity_time = time.time()
58
+ self.connection_status = "disconnected"
59
+ self.connection_error = None
60
+ self.threads = []
61
+
62
+ # Set up logging
63
+ self.logger = logging.getLogger('ssh_tunnel')
64
+ handler = logging.StreamHandler()
65
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
66
+ handler.setFormatter(formatter)
67
+ self.logger.addHandler(handler)
68
+ self.logger.setLevel(logging.INFO)
69
+
70
+ def start(self):
71
+ """
72
+ Start the SSH tunnel with monitoring threads.
73
+
74
+ This method establishes the SSH connection and starts listening for
75
+ incoming connections on the local port, with additional threads for monitoring.
76
+
77
+ Returns:
78
+ bool: True if the tunnel was started successfully, False otherwise.
79
+ """
80
+ if self.is_running:
81
+ self.logger.warning("Tunnel is already running")
82
+ return True
83
+
84
+ self.should_reconnect = True
85
+ self.connect()
86
+
87
+ # Start the monitoring thread
88
+ monitor_thread = threading.Thread(target=self._monitor_connection)
89
+ monitor_thread.daemon = True
90
+ monitor_thread.start()
91
+ self.threads.append(monitor_thread)
92
+
93
+ # Start the keep-alive thread
94
+ keepalive_thread = threading.Thread(target=self._send_keepalive)
95
+ keepalive_thread.daemon = True
96
+ keepalive_thread.start()
97
+ self.threads.append(keepalive_thread)
98
+
99
+ return self.is_running
100
+
101
+ def connect(self):
102
+ """
103
+ Establish the SSH connection and start the server socket.
104
+
105
+ Returns:
106
+ bool: True if connection was successful, False otherwise.
107
+ """
108
+ try:
109
+ # Create an SSH client
110
+ self.client = paramiko.SSHClient()
111
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
112
+
113
+ # Connect to the SSH server
114
+ self.logger.info(f"Connecting to SSH server {self.ssh_host}:{self.ssh_port}")
115
+ connect_kwargs = {
116
+ 'hostname': self.ssh_host,
117
+ 'port': self.ssh_port,
118
+ 'username': self.username,
119
+ }
120
+
121
+ if self.password:
122
+ connect_kwargs['password'] = self.password
123
+
124
+ if self.key_filename:
125
+ connect_kwargs['key_filename'] = self.key_filename
126
+
127
+ self.client.connect(**connect_kwargs)
128
+
129
+ # Get the transport layer
130
+ self.transport = self.client.get_transport()
131
+ self.transport.set_keepalive(self.keep_alive_interval)
132
+
133
+ # Start a server socket to listen for incoming connections
134
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
135
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
136
+ try:
137
+ self.server_socket.bind(('', self.local_port))
138
+ self.server_socket.listen(5)
139
+ except OSError as e:
140
+ self.logger.error(f"Could not bind to port {self.local_port}: {str(e)}")
141
+ self.client.close()
142
+ return False
143
+
144
+ self.is_running = True
145
+ self.connection_status = "connected"
146
+ self.connection_error = None
147
+ self.last_activity_time = time.time()
148
+ self.logger.info(f"SSH tunnel established. Forwarding local port {self.local_port} "
149
+ f"to remote port {self.remote_port} via {self.ssh_host}")
150
+
151
+ # Start the main thread to handle incoming connections
152
+ connection_thread = threading.Thread(target=self._handle_connections)
153
+ connection_thread.daemon = True
154
+ connection_thread.start()
155
+ self.threads.append(connection_thread)
156
+
157
+ return True
158
+
159
+ except Exception as e:
160
+ self.logger.error(f"Failed to connect to SSH server: {str(e)}")
161
+ self.connection_status = "error"
162
+ self.connection_error = str(e)
163
+ self.is_running = False
164
+ return False
165
+
166
+ def _monitor_connection(self):
167
+ """
168
+ Monitor the connection and reconnect if necessary.
169
+ """
170
+ while self.should_reconnect:
171
+ if not self.is_running:
172
+ self.logger.info("Connection is down, attempting to reconnect...")
173
+ successful = self.connect()
174
+ if not successful:
175
+ self.logger.warning(f"Reconnection failed, waiting {self.reconnect_interval} seconds...")
176
+ time.sleep(self.reconnect_interval)
177
+
178
+ # Check if transport is still active
179
+ elif self.transport and not self.transport.is_active():
180
+ self.logger.warning("Transport is no longer active")
181
+ self.stop(reconnect=True)
182
+
183
+ # Check for activity timeout (in case keepalive fails)
184
+ elif time.time() - self.last_activity_time > self.reconnect_interval * 2:
185
+ self.logger.warning("No activity detected, reconnecting...")
186
+ self.stop(reconnect=True)
187
+
188
+ time.sleep(5) # Check connection status every 5 seconds
189
+
190
+ def _send_keepalive(self):
191
+ """
192
+ Send keepalive packets to maintain the SSH connection.
193
+ """
194
+ while self.should_reconnect:
195
+ if self.is_running and self.transport and self.transport.is_active():
196
+ try:
197
+ self.transport.send_ignore()
198
+ self.last_activity_time = time.time()
199
+ self.logger.debug("Sent keepalive packet")
200
+ except Exception as e:
201
+ self.logger.warning(f"Failed to send keepalive: {str(e)}")
202
+
203
+ time.sleep(self.keep_alive_interval)
204
+
205
+ def check_status(self):
206
+ """
207
+ Check the current status of the tunnel.
208
+
209
+ Returns:
210
+ dict: Status information including whether the tunnel is running,
211
+ connection status, and any error messages.
212
+ """
213
+ return {
214
+ "is_running": self.is_running,
215
+ "status": self.connection_status,
216
+ "error": self.connection_error,
217
+ "last_activity": time.time() - self.last_activity_time
218
+ }
219
+
220
+ def _handle_connections(self):
221
+ """
222
+ Handle incoming connections on the local port.
223
+
224
+ This method accepts incoming connections and creates a new thread
225
+ to handle each connection.
226
+ """
227
+ try:
228
+ while self.is_running:
229
+ try:
230
+ client_socket, client_addr = self.server_socket.accept()
231
+ self.logger.info(f"New connection from {client_addr[0]}:{client_addr[1]}")
232
+ self.last_activity_time = time.time()
233
+
234
+ thread = threading.Thread(
235
+ target=self._forward_traffic,
236
+ args=(client_socket, client_addr)
237
+ )
238
+ thread.daemon = True
239
+ thread.start()
240
+ self.threads.append(thread)
241
+
242
+ except (socket.timeout, socket.error) as e:
243
+ if self.is_running:
244
+ self.logger.error(f"Socket error: {str(e)}")
245
+ except Exception as e:
246
+ if self.is_running:
247
+ self.logger.error(f"Error accepting connection: {str(e)}")
248
+
249
+ except Exception as e:
250
+ self.logger.error(f"Error in connection handler: {str(e)}")
251
+ if self.is_running:
252
+ self.stop(reconnect=True)
253
+
254
+ def _forward_traffic(self, client_socket, client_addr):
255
+ """
256
+ Forward traffic between the local client and the remote server.
257
+
258
+ Args:
259
+ client_socket (socket.socket): The socket for the local client connection
260
+ client_addr (tuple): The address of the local client
261
+ """
262
+ channel = None
263
+ try:
264
+ # Create a channel to the remote host through the SSH transport
265
+ remote_addr = ('127.0.0.1', self.remote_port)
266
+ channel = self.transport.open_channel(
267
+ "direct-tcpip", remote_addr, client_addr
268
+ )
269
+
270
+ if channel is None:
271
+ self.logger.error(f"Failed to open channel to {remote_addr[0]}:{remote_addr[1]}")
272
+ client_socket.close()
273
+ return
274
+
275
+ self.logger.info(f"Channel opened to {remote_addr[0]}:{remote_addr[1]}")
276
+ self.last_activity_time = time.time()
277
+
278
+ # Forward data in both directions
279
+ while True:
280
+ r, w, e = select.select([client_socket, channel], [], [], 1)
281
+
282
+ if client_socket in r:
283
+ data = client_socket.recv(4096)
284
+ if len(data) == 0:
285
+ break
286
+ channel.send(data)
287
+ self.last_activity_time = time.time()
288
+
289
+ if channel in r:
290
+ data = channel.recv(4096)
291
+ if len(data) == 0:
292
+ break
293
+ client_socket.send(data)
294
+ self.last_activity_time = time.time()
295
+
296
+ except Exception as e:
297
+ self.logger.error(f"Error forwarding traffic: {str(e)}")
298
+
299
+ finally:
300
+ client_socket.close()
301
+ if channel:
302
+ channel.close()
303
+ self.logger.info(f"Connection from {client_addr[0]}:{client_addr[1]} closed")
304
+
305
+ def stop(self, reconnect=False):
306
+ """
307
+ Stop the SSH tunnel.
308
+
309
+ Args:
310
+ reconnect (bool): If True, the tunnel will attempt to reconnect after stopping.
311
+ """
312
+ self.is_running = False
313
+
314
+ if self.server_socket:
315
+ try:
316
+ self.server_socket.close()
317
+ except Exception:
318
+ pass
319
+ self.server_socket = None
320
+
321
+ if self.transport:
322
+ try:
323
+ self.transport.close()
324
+ except Exception:
325
+ pass
326
+ self.transport = None
327
+
328
+ if self.client:
329
+ try:
330
+ self.client.close()
331
+ except Exception:
332
+ pass
333
+ self.client = None
334
+
335
+ if not reconnect:
336
+ self.should_reconnect = False
337
+ self.logger.info("SSH tunnel stopped permanently")
338
+ else:
339
+ self.logger.info("SSH tunnel stopped, will attempt to reconnect")
340
+
341
+ def __enter__(self):
342
+ """
343
+ Enter the context manager.
344
+
345
+ Returns:
346
+ SSHTunnel: The SSH tunnel instance.
347
+ """
348
+ self.start()
349
+ return self
350
+
351
+ def __exit__(self, exc_type, exc_val, exc_tb):
352
+ """
353
+ Exit the context manager.
354
+ """
355
+ self.stop(reconnect=False)