-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathssh_forwarding.py
More file actions
132 lines (117 loc) · 4.82 KB
/
Copy pathssh_forwarding.py
File metadata and controls
132 lines (117 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""Shared SSH forwarding primitives for user tunnels and the Agent preset."""
import ipaddress
import secrets
import socket
import threading
FORWARD_BIND_HOST = '127.0.0.1'
FORWARD_CONNECT_TIMEOUT = 10
FORWARD_POLL_SECONDS = 1
FORWARD_BUFFER_BYTES = 65536
FORWARD_MAX_CONNECTIONS = 32
_contexts_lock = threading.Lock()
def is_loopback_address(address):
try:
value = ipaddress.ip_address(address)
return value.is_loopback or bool(getattr(value, 'ipv4_mapped', None) and value.ipv4_mapped.is_loopback)
except ValueError:
return False
class SSHForwarding:
def __init__(self, transport):
self.transport = transport
self.id = secrets.token_urlsafe(12)
self.connections = threading.BoundedSemaphore(FORWARD_MAX_CONNECTIONS)
self.lock = threading.Lock()
self.request_lock = threading.Lock()
self.routes = {}
def request_remote(self, port, owner, accept, cancelled):
# Paramiko has one global request response slot and one TCP handler.
# A timed-out caller must not release this lock for its still-running I/O.
if not self.request_lock.acquire(blocking=False):
raise RuntimeError('Another remote tunnel request is still pending. Retry after it finishes.')
try:
if cancelled():
raise RuntimeError('Tunnel setup was cancelled.')
allocated = self.transport.request_port_forward(FORWARD_BIND_HOST, port, handler=self._dispatch)
if not 1 <= allocated <= 65535:
raise RuntimeError('The SSH server returned an invalid listening port.')
with self.lock:
if allocated in self.routes:
raise RuntimeError('The SSH server reused an active tunnel port.')
self.routes[allocated] = (owner, accept)
if cancelled():
self.cancel_remote(allocated, owner)
return allocated
finally:
self.request_lock.release()
def cancel_remote(self, port, owner):
with self.lock:
route = self.routes.get(port)
if not route or route[0] is not owner:
return
del self.routes[port]
if self.transport.is_active():
# cancel_port_forward clears Paramiko's handler for every listener.
self.transport.global_request('cancel-tcpip-forward', (FORWARD_BIND_HOST, port), wait=False)
def _dispatch(self, channel, origin, destination):
with self.lock:
route = self.routes.get(destination[1])
if not route:
channel.close()
return
# Callbacks only admit and start a worker; never connect or relay here.
try:
route[1](channel, origin, destination)
except Exception:
channel.close()
def forwarding_for(transport):
with _contexts_lock:
context = getattr(transport, '_standterm_forwarding', None)
if context is None:
context = SSHForwarding(transport)
transport._standterm_forwarding = context
return context
def relay_tcp(left, right, stopped, progress=None):
"""Drain both TCP directions, including responses after a write half-close."""
failed = threading.Event()
left.settimeout(FORWARD_POLL_SECONDS)
right.settimeout(FORWARD_POLL_SECONDS)
def pump(source, destination, direction):
try:
while not stopped() and not failed.is_set():
try:
data = source.recv(FORWARD_BUFFER_BYTES)
except socket.timeout:
continue
if not data:
if hasattr(destination, 'shutdown_write'):
destination.shutdown_write()
else:
destination.shutdown(socket.SHUT_WR)
return
pending = memoryview(data)
while pending and not stopped() and not failed.is_set():
try:
sent = destination.send(pending)
except socket.timeout:
continue
if not sent:
raise EOFError('The forwarding destination closed.')
pending = pending[sent:]
if progress:
progress(direction, sent)
except (OSError, EOFError):
failed.set()
source.close()
destination.close()
reverse = threading.Thread(target=pump, args=(right, left, 'received'), daemon=True)
try:
reverse.start()
pump(left, right, 'sent')
while reverse.is_alive():
if stopped() or failed.is_set():
left.close()
right.close()
reverse.join(FORWARD_POLL_SECONDS)
finally:
left.close()
right.close()