ccssh 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ccssh/forward.py ADDED
@@ -0,0 +1,872 @@
1
+ """
2
+ Local (-L) and remote (-R) TCP forwarding over SSH channels.
3
+
4
+ The forwarders share the connection through the channel multiplexer
5
+ (:class:`ccssh.mux.ChannelMux`), ssh-like: exec/sftp/shell may run on
6
+ the same connection while forwarding is active. Each forwarder runs one
7
+ pump thread that multiplexes local sockets and its mux queues; the mux
8
+ owns the only transport reader, so concurrent channels never interleave
9
+ the framing (the pre-mux single-reader deadlock is gone by construction:
10
+ no lock is ever held across a blocking receive).
11
+
12
+ Orderly shutdown is a half-close: local EOF sends ``CHANNEL_EOF`` and
13
+ keeps relaying server->local until the server closes (no data loss).
14
+ """
15
+ from __future__ import annotations
16
+ import queue
17
+ import select
18
+ import socket
19
+ import threading
20
+ import time
21
+
22
+ from ccssh import channel as CH
23
+ from ccssh import wire as W
24
+
25
+
26
+ class ForwardError(Exception):
27
+ pass
28
+
29
+
30
+ def _listen(bind: str, lport: int) -> socket.socket:
31
+ """Listener socket for v4/v6 hostnames and literal addresses."""
32
+ last: OSError | None = None
33
+ try:
34
+ infos = socket.getaddrinfo(bind or "127.0.0.1", lport, type=socket.SOCK_STREAM)
35
+ except OSError:
36
+ infos = []
37
+ for af, st, proto, _canon, sa in infos or [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (bind, lport))]:
38
+ s: socket.socket | None = None
39
+ try:
40
+ s = socket.socket(af, st, proto)
41
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42
+ try:
43
+ s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
44
+ except Exception:
45
+ pass
46
+ s.bind(sa)
47
+ s.listen(16)
48
+ return s
49
+ except OSError as e:
50
+ last = e
51
+ if s is not None:
52
+ try:
53
+ s.close()
54
+ except Exception:
55
+ pass
56
+ continue
57
+ raise ForwardError(f"listen {bind}:{lport} failed: {last}")
58
+
59
+
60
+ class _PendingOpen:
61
+ __slots__ = ("event", "srv", "window", "maxpacket", "failure")
62
+
63
+ def __init__(self) -> None:
64
+ self.event = threading.Event()
65
+ self.srv: int | None = None
66
+ self.window = 0
67
+ self.maxpacket = 0
68
+ self.failure = False
69
+
70
+
71
+ class BaseForwarder:
72
+ """Shared pump/pending-open machinery for -L and -R (mux-fed)."""
73
+
74
+ def __init__(self, mux) -> None:
75
+ self._mux = mux
76
+ # Lock order is ALWAYS map_lock -> send_lock, never the reverse.
77
+ # send_lock is held only for a single non-blocking mux.send call.
78
+ self._send_lock = threading.Lock()
79
+ self._map_lock = threading.Lock()
80
+ self._stop = threading.Event()
81
+ self.closed = False
82
+ self._pending: dict[int, _PendingOpen] = {}
83
+ self._chans: dict[int, socket.socket] = {} # our id -> local socket
84
+ self._srv_to_my: dict[int, int] = {} # server id -> our id
85
+ self._my_to_srv: dict[int, int] = {} # our id -> server id
86
+ self._consumed: dict[int, int] = {}
87
+ self._pending_in: dict[int, bytes] = {} # local->server backlog
88
+ self._pending_out: dict[int, bytes] = {} # server->local backlog
89
+ self._eof_local: set[int] = set() # local EOF seen, backlog pending
90
+ self._send_window: dict[int, int] = {} # our id -> remaining window
91
+ self._send_max: dict[int, int] = {}
92
+ self._eof_sent: set[int] = set()
93
+ self._th = threading.Thread(target=self._pump, daemon=True)
94
+
95
+ # transport IO
96
+ def _send(self, payload: bytes) -> None:
97
+ with self._send_lock:
98
+ self._mux.send(payload)
99
+
100
+ def _timeout(self) -> float:
101
+ try:
102
+ return float(self._mux.timeout)
103
+ except Exception:
104
+ return 10.0
105
+
106
+ def _wait_pending(self, my_id: int, pend: _PendingOpen, timeout: float) -> None:
107
+ """
108
+ Wait for a channel-open confirm while draining mux queues.
109
+
110
+ Every installed channel (not just ours) is drained through
111
+ _handle while we wait, so a slow/dead peer on one accept cannot
112
+ head-of-line-block the rest; no lock is held while waiting.
113
+ """
114
+ deadline = time.monotonic() + timeout
115
+ # The confirm for THIS channel must have a queue to land in. Note
116
+ # it is NOT installed yet, so _pump_drain_mux (installed channels
117
+ # + catch-all) will not see it: drain it explicitly below.
118
+ my_q = self._mux.register(my_id)
119
+ while not pend.event.is_set():
120
+ if self._mux.dead:
121
+ raise ForwardError("transport closed during channel open")
122
+ if time.monotonic() > deadline:
123
+ break
124
+ if not self._pump_drain_mux():
125
+ raise ForwardError("forwarder draining failed during channel open")
126
+ try:
127
+ p = my_q.get_nowait()
128
+ except queue.Empty:
129
+ pass
130
+ else:
131
+ if isinstance(p, (bytes, bytearray)):
132
+ self._handle(p)
133
+ if pend.event.is_set():
134
+ break
135
+ if self._stop.is_set():
136
+ break
137
+ time.sleep(0.01)
138
+ if not pend.event.is_set():
139
+ raise ForwardError("channel open confirm timeout")
140
+ if pend.failure:
141
+ raise ForwardError("server refused channel open")
142
+
143
+ # to be specialised
144
+ def _handle(self, p: bytes) -> None:
145
+ raise NotImplementedError
146
+
147
+ def _pump(self) -> None:
148
+ raise NotImplementedError
149
+
150
+ def _pump_drain_mux(self) -> bool:
151
+ """Drain our channel queues + catch-all once. False to shut down."""
152
+ with self._map_lock:
153
+ chan_ids = list(self._chans)
154
+ catch = self._mux.catch_all()
155
+ queues = []
156
+ for k in chan_ids:
157
+ q = self._mux._queue_for(k)
158
+ if q is not None:
159
+ queues.append(q)
160
+ for q in queues + [catch]:
161
+ while True:
162
+ try:
163
+ p = q.get_nowait()
164
+ except queue.Empty:
165
+ break
166
+ if not isinstance(p, (bytes, bytearray)):
167
+ continue # mux sentinel: death is reported via mux.dead
168
+ try:
169
+ self._handle(p)
170
+ except ForwardError:
171
+ return False
172
+ except Exception:
173
+ pass
174
+ if self._mux.dead:
175
+ return False
176
+ return True
177
+
178
+ # shared channel helpers (brief map_lock)
179
+ def _install_channel(self, my_id: int, sock: socket.socket, srv: int, window: int, maxpacket: int) -> None:
180
+ # The mux queue must exist before any DATA can arrive (server-
181
+ # initiated opens never went through _wait_pending registration).
182
+ try:
183
+ self._mux.register(my_id)
184
+ except Exception:
185
+ pass
186
+ with self._map_lock:
187
+ if my_id in self._chans:
188
+ raise ForwardError(f"channel id collision: {my_id} already open")
189
+ self._pending.pop(my_id, None)
190
+ self._chans[my_id] = sock
191
+ self._srv_to_my[srv] = my_id
192
+ self._my_to_srv[my_id] = srv
193
+ self._consumed[my_id] = 0
194
+ # Send window is keyed by OUR id: WINDOW_ADJUST from the peer
195
+ # carries our channel number as recipient (RFC 4254 section 5.2).
196
+ self._send_window[my_id] = window
197
+ self._send_max[my_id] = max(1024, maxpacket)
198
+
199
+ def _resolve_my_locked(self, recipient: int) -> int | None:
200
+ """
201
+ Map an inbound recipient field to our channel id.
202
+
203
+ Peers address our channel number; accept the peer's own number too
204
+ (liberal in what you accept - some stacks echo it).
205
+ """
206
+ if recipient in self._chans:
207
+ return recipient
208
+ return self._srv_to_my.get(recipient)
209
+
210
+ def _on_data(self, recipient: int, data: bytes) -> None:
211
+ """Server->local direction with partial-write-safe backlog."""
212
+ with self._map_lock:
213
+ my_id = self._resolve_my_locked(recipient)
214
+ if my_id is None:
215
+ return
216
+ s = self._chans.get(my_id)
217
+ if s is None:
218
+ return
219
+ data = self._pending_out.pop(my_id, b"") + data
220
+ if len(data) > CH.MAX_BUFFER:
221
+ # Slow local reader vs fast server: fail the channel closed
222
+ # instead of letting one connection OOM the process.
223
+ over = True
224
+ else:
225
+ over = False
226
+ if over:
227
+ self._drop(my_id)
228
+ return
229
+ try:
230
+ n = s.send(data)
231
+ except BlockingIOError:
232
+ with self._map_lock:
233
+ if my_id in self._chans:
234
+ self._pending_out[my_id] = data
235
+ return
236
+ except Exception:
237
+ self._drop(my_id)
238
+ return
239
+ if n < len(data):
240
+ with self._map_lock:
241
+ if my_id in self._chans:
242
+ self._pending_out[my_id] = data[n:]
243
+ data = data[:n]
244
+ self._credit(my_id, len(data))
245
+
246
+ def _credit(self, my_id: int, n: int) -> None:
247
+ with self._map_lock:
248
+ self._consumed[my_id] = self._consumed.get(my_id, 0) + n
249
+ if self._consumed.get(my_id, 0) < CH.WINDOW_THRESHOLD:
250
+ return
251
+ srv = self._my_to_srv.get(my_id)
252
+ delta = self._consumed.get(my_id, 0)
253
+ self._consumed[my_id] = 0
254
+ if srv is not None:
255
+ try:
256
+ self._send(CH.enc_window_adjust(srv, delta))
257
+ except Exception:
258
+ pass
259
+
260
+ def _on_window(self, recipient: int, delta: int) -> None:
261
+ with self._map_lock:
262
+ my_id = self._resolve_my_locked(recipient)
263
+ if my_id is None:
264
+ return
265
+ try:
266
+ self._send_window[my_id] = CH.check_window(
267
+ self._send_window.get(my_id, 0), delta)
268
+ except ValueError:
269
+ pass # bogus increment: ignore, fail closed elsewhere
270
+
271
+ def _flush_out(self, s: socket.socket) -> None:
272
+ with self._map_lock:
273
+ my_id = next((k for k, v in self._chans.items() if v is s), None)
274
+ if my_id is None:
275
+ return
276
+ data = self._pending_out.pop(my_id, b"")
277
+ if not data:
278
+ return
279
+ try:
280
+ n = s.send(data)
281
+ except BlockingIOError:
282
+ with self._map_lock:
283
+ if my_id in self._chans:
284
+ self._pending_out[my_id] = data
285
+ return
286
+ except Exception:
287
+ self._drop(my_id)
288
+ return
289
+ if n < len(data):
290
+ with self._map_lock:
291
+ if my_id in self._chans:
292
+ self._pending_out[my_id] = data[n:]
293
+ # Credit the server window for what actually left, like _on_data.
294
+ self._credit(my_id, n)
295
+
296
+ def _on_local(self, s: socket.socket) -> None:
297
+ with self._map_lock:
298
+ my_id = next((k for k, v in self._chans.items() if v is s), None)
299
+ if my_id is None:
300
+ return
301
+ data = self._pending_in.pop(my_id, b"")
302
+ try:
303
+ got = s.recv(32768)
304
+ except BlockingIOError:
305
+ got = None # spurious readiness: keep channel
306
+ except Exception:
307
+ self._drop(my_id)
308
+ return
309
+ if got is None:
310
+ if data:
311
+ self._forward_local(my_id, data)
312
+ return
313
+ if got == b"":
314
+ # Local EOF is terminal for this direction: arm it, flush what
315
+ # remains, and stop selecting the socket (it would spin: EOF
316
+ # sockets are always "readable").
317
+ with self._map_lock:
318
+ if my_id in self._chans:
319
+ self._eof_local.add(my_id)
320
+ if data:
321
+ self._forward_local(my_id, data)
322
+ self._maybe_half_close(my_id)
323
+ return
324
+ self._forward_local(my_id, data + got)
325
+
326
+ def _flush_in(self, my_id: int) -> None:
327
+ """Retry a backlogged local->server write (window opened up)."""
328
+ with self._map_lock:
329
+ if my_id not in self._chans:
330
+ self._eof_local.discard(my_id)
331
+ return
332
+ data = self._pending_in.pop(my_id, b"")
333
+ if not data:
334
+ return
335
+ self._forward_local(my_id, data)
336
+ self._maybe_half_close(my_id)
337
+
338
+ def _maybe_half_close(self, my_id: int) -> None:
339
+ """Deliver an armed local EOF once its backlog fully drained."""
340
+ with self._map_lock:
341
+ armed = my_id in self._eof_local
342
+ drained = not self._pending_in.get(my_id)
343
+ live = my_id in self._chans
344
+ if armed and drained and live:
345
+ self._eof_local.discard(my_id)
346
+ do_half = True
347
+ else:
348
+ do_half = False
349
+ if do_half:
350
+ self._half_close(my_id)
351
+
352
+ def _forward_local(self, my_id: int, data: bytes) -> None:
353
+ with self._map_lock:
354
+ srv = self._my_to_srv.get(my_id)
355
+ if srv is None:
356
+ return
357
+ # Honor the server's maxpacket but never exceed ours: a rogue
358
+ # server announcing megabytes would make enc_data raise.
359
+ maxp = min(self._send_max.get(my_id, 32768), CH.MAXPKT)
360
+ if len(self._pending_in.get(my_id, b"")) + len(data) > CH.MAX_BUFFER:
361
+ over = True
362
+ else:
363
+ over = False
364
+ if over:
365
+ self._drop(my_id)
366
+ return
367
+ off = 0
368
+ try:
369
+ while off < len(data):
370
+ chunk = data[off:off + maxp]
371
+ with self._map_lock:
372
+ window = self._send_window.get(my_id, 0)
373
+ if window < len(chunk):
374
+ self._pending_in[my_id] = data[off:]
375
+ return
376
+ self._send_window[my_id] = window - len(chunk)
377
+ self._send(CH.enc_data(srv, chunk))
378
+ off += len(chunk)
379
+ except Exception:
380
+ self._drop(my_id)
381
+ return
382
+ if off < len(data):
383
+ with self._map_lock:
384
+ if my_id in self._chans:
385
+ self._pending_in[my_id] = data[off:]
386
+
387
+ def _half_close(self, my_id: int) -> None:
388
+ """Local EOF: CHANNEL_EOF to the server, keep the reverse path open."""
389
+ with self._map_lock:
390
+ if my_id not in self._chans or my_id in self._eof_sent:
391
+ return
392
+ srv = self._my_to_srv.get(my_id)
393
+ self._eof_sent.add(my_id)
394
+ try:
395
+ s = self._chans.get(my_id)
396
+ if s is not None:
397
+ try:
398
+ s.shutdown(socket.SHUT_RD)
399
+ except Exception:
400
+ pass
401
+ except Exception:
402
+ pass
403
+ if srv is not None:
404
+ try:
405
+ self._send(CH.enc_eof(srv))
406
+ except Exception:
407
+ pass
408
+
409
+ def _on_remote_eof(self, recipient: int) -> None:
410
+ with self._map_lock:
411
+ my_id = self._resolve_my_locked(recipient)
412
+ if my_id is None:
413
+ return
414
+ s = self._chans.get(my_id)
415
+ if s is not None:
416
+ try:
417
+ s.shutdown(socket.SHUT_WR)
418
+ except Exception:
419
+ pass
420
+
421
+ def _on_remote_close(self, recipient: int) -> None:
422
+ with self._map_lock:
423
+ my_id = self._resolve_my_locked(recipient)
424
+ if my_id is not None:
425
+ self._drop(my_id)
426
+
427
+ def _route_confirm(self, my_id: int, srv: int, window: int, maxpacket: int) -> bool:
428
+ """Complete a pending open. True if it was ours."""
429
+ with self._map_lock:
430
+ pend = self._pending.get(my_id)
431
+ if pend is None:
432
+ return False
433
+ pend.srv = srv
434
+ pend.window = window
435
+ pend.maxpacket = max(1024, maxpacket)
436
+ pend.event.set()
437
+ return True
438
+
439
+ def _route_failure(self, my_id: int) -> bool:
440
+ with self._map_lock:
441
+ pend = self._pending.get(my_id)
442
+ if pend is None:
443
+ return False
444
+ pend.failure = True
445
+ pend.event.set()
446
+ return True
447
+
448
+ def _drop(self, my_id: int) -> None:
449
+ with self._map_lock:
450
+ s = self._chans.pop(my_id, None)
451
+ self._consumed.pop(my_id, None)
452
+ self._pending_in.pop(my_id, None)
453
+ self._pending_out.pop(my_id, None)
454
+ self._eof_sent.discard(my_id)
455
+ self._eof_local.discard(my_id)
456
+ self._pending.pop(my_id, None)
457
+ srv = self._my_to_srv.pop(my_id, None)
458
+ if srv is not None:
459
+ self._srv_to_my.pop(srv, None)
460
+ self._send_window.pop(my_id, None)
461
+ self._send_max.pop(my_id, None)
462
+ try:
463
+ self._mux.unregister(my_id)
464
+ except Exception:
465
+ pass
466
+ if s is not None:
467
+ try:
468
+ s.close()
469
+ except Exception:
470
+ pass
471
+ if srv is not None:
472
+ try:
473
+ self._send(CH.enc_close(srv))
474
+ except Exception:
475
+ pass
476
+
477
+ def _close_channels(self) -> None:
478
+ for my_id in list(self._chans):
479
+ self._drop(my_id) # closes the local socket, sends CHANNEL_CLOSE
480
+
481
+
482
+ class LocalForwarder(BaseForwarder):
483
+ def __init__(self, mux, lport: int, rhost: str, rport: int,
484
+ bind: str = "127.0.0.1"):
485
+ super().__init__(mux)
486
+ self._target = (rhost, rport)
487
+ self._bind = bind
488
+ self._ls = _listen(bind, lport)
489
+ self.port = self._ls.getsockname()[1]
490
+
491
+ def start(self) -> "LocalForwarder":
492
+ self._th.start()
493
+ return self
494
+
495
+ def close(self) -> None:
496
+ self.closed = True
497
+ self._stop.set()
498
+ try:
499
+ self._ls.close()
500
+ except Exception:
501
+ pass
502
+ self._close_channels()
503
+ # NOTE: the client-owned mux keeps running (exec may continue).
504
+ try:
505
+ if self._th.is_alive():
506
+ self._th.join(timeout=2.0)
507
+ except Exception:
508
+ pass
509
+
510
+ def _pump(self) -> None:
511
+ try:
512
+ while not self._stop.is_set():
513
+ if not self._pump_drain_mux():
514
+ return
515
+ with self._map_lock:
516
+ chans = [(k, v) for k, v in self._chans.items() if k not in self._eof_local]
517
+ wlist = [self._chans[k] for k in self._pending_out if k in self._chans]
518
+ eof_backlog = [k for k in self._eof_local if k in self._chans and self._pending_in.get(k)]
519
+ for my_id in eof_backlog:
520
+ self._flush_in(my_id)
521
+ try:
522
+ rd, wr, _ = select.select([self._ls] + [s for _, s in chans], wlist, [], 0.2)
523
+ except Exception:
524
+ if self.closed:
525
+ return
526
+ continue
527
+ for s in wr:
528
+ self._flush_out(s)
529
+ for s in rd:
530
+ if s is self._ls:
531
+ try:
532
+ conn, _ = self._ls.accept()
533
+ except Exception:
534
+ continue
535
+ try:
536
+ self._open_channel(conn)
537
+ except Exception:
538
+ try:
539
+ conn.close()
540
+ except Exception:
541
+ pass
542
+ else:
543
+ self._on_local(s)
544
+ finally:
545
+ # A dying pump must not strand open sockets/queues behind.
546
+ self._stop.set()
547
+ self._close_channels()
548
+
549
+ def _open_channel(self, conn: socket.socket) -> None:
550
+ # Channel ids come from the single mux-shared namespace: disjoint
551
+ # local counters would eventually collide and misroute packets.
552
+ my_id = self._mux.alloc_chan()
553
+ with self._map_lock:
554
+ pend = _PendingOpen()
555
+ self._pending[my_id] = pend
556
+ try:
557
+ peer = conn.getpeername()
558
+ except Exception:
559
+ peer = ("127.0.0.1", 0)
560
+ self._send(CH.enc_open_direct_tcpip(my_id, self._target[0], self._target[1], orig_host=peer[0], orig_port=peer[1]))
561
+ # Wait for OUR confirm while other channels keep flowing.
562
+ try:
563
+ self._wait_pending(my_id, pend, self._timeout())
564
+ except Exception:
565
+ with self._map_lock:
566
+ self._pending.pop(my_id, None)
567
+ try:
568
+ self._mux.unregister(my_id)
569
+ except Exception:
570
+ pass
571
+ raise
572
+ assert pend.srv is not None
573
+ self._install_channel(my_id, conn, pend.srv, pend.window, pend.maxpacket)
574
+ try:
575
+ conn.setblocking(False)
576
+ except Exception:
577
+ pass
578
+
579
+ def _handle(self, p: bytes) -> None:
580
+ r = W.Reader(p)
581
+ try:
582
+ m = r.byte()
583
+ except Exception:
584
+ return
585
+ if m in (2, 4, 20, 80, 81, 82):
586
+ # Service traffic never reaches here (mux handles it); a KEX
587
+ # broadcast means rekey with open channels: fail loudly.
588
+ if m == 20:
589
+ raise ForwardError("peer started rekey during forwarding: close it and call rekey()")
590
+ return
591
+ if m == CH.MSG_CHANNEL_OPEN_CONFIRM:
592
+ try:
593
+ rcpt, srv, w, mx = r.u32(), r.u32(), r.u32(), r.u32()
594
+ except Exception:
595
+ return
596
+ self._route_confirm(rcpt, srv, w, mx)
597
+ return
598
+ if m == CH.MSG_CHANNEL_OPEN:
599
+ # A local (-L) forwarder never expects server-initiated opens:
600
+ # refuse so the peer doesn't hang waiting for an answer.
601
+ rr = W.Reader(p)
602
+ try:
603
+ rr.byte()
604
+ rr.string()
605
+ srv = rr.u32()
606
+ except Exception:
607
+ return
608
+ try:
609
+ self._send(bytes([CH.MSG_CHANNEL_OPEN_FAILURE])
610
+ + W.enc_u32(srv)
611
+ + W.enc_u32(3)
612
+ + W.enc_string(b"forwarding not supported here")
613
+ + W.enc_string(b""))
614
+ except Exception:
615
+ pass
616
+ return
617
+ if m == CH.MSG_CHANNEL_OPEN_FAILURE:
618
+ try:
619
+ rcpt = r.u32()
620
+ except Exception:
621
+ return
622
+ if not self._route_failure(rcpt):
623
+ return
624
+ return
625
+ if m == CH.MSG_CHANNEL_DATA:
626
+ try:
627
+ rcpt, data = r.u32(), r.string()
628
+ except Exception:
629
+ return
630
+ self._on_data(rcpt, data)
631
+ return
632
+ if m == CH.MSG_WINDOW_ADJUST:
633
+ try:
634
+ rcpt, delta = r.u32(), r.u32()
635
+ except Exception:
636
+ return
637
+ self._on_window(rcpt, delta)
638
+ return
639
+ if m == CH.MSG_CHANNEL_EOF:
640
+ try:
641
+ rcpt = r.u32()
642
+ except Exception:
643
+ return
644
+ self._on_remote_eof(rcpt)
645
+ return
646
+ if m == CH.MSG_CHANNEL_CLOSE:
647
+ try:
648
+ rcpt = r.u32()
649
+ except Exception:
650
+ return
651
+ self._on_remote_close(rcpt)
652
+ return
653
+
654
+
655
+ def enc_global_tcpip_forward(bind: str, port: int) -> bytes:
656
+ """
657
+ SSH_MSG_GLOBAL_REQUEST for tcpip-forward (RFC 4254 section 7.1).
658
+ """
659
+ return (bytes([80]) + W.enc_string(b"tcpip-forward") + W.enc_bool(True) + W.enc_string(bind.encode()) + W.enc_u32(port))
660
+
661
+
662
+ def enc_global_cancel_forward(bind: str, port: int) -> bytes:
663
+ """
664
+ cancel-tcpip-forward. want_reply=False: fire-and-forget on close.
665
+ """
666
+ return (bytes([80]) + W.enc_string(b"cancel-tcpip-forward") + W.enc_bool(False) + W.enc_string(bind.encode()) + W.enc_u32(port))
667
+
668
+
669
+ class RemoteForwarder(BaseForwarder):
670
+ """
671
+ Server-side listener relayed to a local target (`ssh -R`).
672
+
673
+ The server binds (bind, sport) - sport 0 means auto-assign, the real
674
+ port lands in .port (OpenSSH returns it in REQUEST_SUCCESS). Inbound
675
+ connections arrive as forwarded-tcpip channel opens; each is dialed
676
+ to (rhost, rport) locally and relayed in the background loop.
677
+ Same single-forwarder-per-connection rule as -L (see client).
678
+ """
679
+
680
+ def __init__(self, mux, sport: int, rhost: str, rport: int, bind: str = "localhost"):
681
+ super().__init__(mux)
682
+ self._bind = bind
683
+ self._req_port = sport
684
+ self.port = sport
685
+ self._target = (rhost, rport)
686
+
687
+ def start(self) -> "RemoteForwarder":
688
+ """
689
+ Register the remote listener. Other channel traffic arriving
690
+ during registration is handled, never dropped.
691
+ """
692
+ self._send(enc_global_tcpip_forward(self._bind, self._req_port))
693
+ deadline = time.monotonic() + self._timeout()
694
+ catch = self._mux.catch_all()
695
+ while True:
696
+ if self._mux.dead:
697
+ raise ForwardError("transport closed during tcpip-forward")
698
+ remaining = deadline - time.monotonic()
699
+ if remaining <= 0:
700
+ raise ForwardError(f"tcpip-forward {self._bind}:{self._req_port} timed out")
701
+ try:
702
+ p = catch.get(timeout=min(0.5, remaining))
703
+ except queue.Empty:
704
+ if self._stop.is_set():
705
+ raise ForwardError("forwarder closing during tcpip-forward")
706
+ continue
707
+ if not isinstance(p, (bytes, bytearray)):
708
+ continue
709
+ r = W.Reader(p)
710
+ try:
711
+ m = r.byte()
712
+ except Exception:
713
+ continue
714
+ if m == 81: # REQUEST_SUCCESS (+optional bound port, OpenSSH sends it)
715
+ try:
716
+ rr = W.Reader(p)
717
+ rr.byte()
718
+ self.port = rr.u32()
719
+ except Exception:
720
+ pass
721
+ break
722
+ if m == 82:
723
+ raise ForwardError(f"server refused -R {self._bind}:{self._req_port}")
724
+ self._handle(p)
725
+ self._th.start()
726
+ return self
727
+
728
+ def close(self) -> None:
729
+ self.closed = True
730
+ self._stop.set()
731
+ try:
732
+ self._send(enc_global_cancel_forward(self._bind, self.port))
733
+ except Exception:
734
+ pass
735
+ self._close_channels()
736
+ # NOTE: the client-owned mux keeps running (exec may continue).
737
+ try:
738
+ if self._th.is_alive():
739
+ self._th.join(timeout=2.0)
740
+ except Exception:
741
+ pass
742
+
743
+ def _pump(self) -> None:
744
+ try:
745
+ while not self._stop.is_set():
746
+ if not self._pump_drain_mux():
747
+ return
748
+ with self._map_lock:
749
+ chans = [(k, v) for k, v in self._chans.items() if k not in self._eof_local]
750
+ wlist = [self._chans[k] for k in self._pending_out if k in self._chans]
751
+ eof_backlog = [k for k in self._eof_local if k in self._chans and self._pending_in.get(k)]
752
+ for my_id in eof_backlog:
753
+ self._flush_in(my_id)
754
+ if not chans and not wlist:
755
+ # No channels yet: idle-wait so a dead transport is noticed
756
+ # via the queue drain above instead of blocking in select.
757
+ time.sleep(0.05)
758
+ continue
759
+ try:
760
+ rd, wr, _ = select.select([s for _, s in chans], wlist, [], 0.2)
761
+ except Exception:
762
+ if self.closed:
763
+ return
764
+ continue
765
+ for s in wr:
766
+ self._flush_out(s)
767
+ for s in rd:
768
+ self._on_local(s)
769
+ finally:
770
+ # A dying pump must not strand open sockets/queues behind.
771
+ self._stop.set()
772
+ self._close_channels()
773
+
774
+ def _handle(self, p: bytes) -> None:
775
+ r = W.Reader(p)
776
+ try:
777
+ m = r.byte()
778
+ except Exception:
779
+ return
780
+ if m in (2, 4, 81, 82):
781
+ return
782
+ if m == 20:
783
+ raise ForwardError("peer started rekey during forwarding: close it and call rekey()")
784
+ if m == CH.MSG_CHANNEL_OPEN:
785
+ rr = W.Reader(p)
786
+ try:
787
+ rr.byte()
788
+ if rr.string() != b"forwarded-tcpip":
789
+ self._refuse_open(rr)
790
+ return
791
+ srv = rr.u32()
792
+ srv_window = rr.u32()
793
+ srv_maxpacket = rr.u32()
794
+ rr.string()
795
+ rr.u32() # connected host/port (our listener side; informational)
796
+ rr.string()
797
+ rr.u32() # originator host/port (informational)
798
+ except Exception:
799
+ return
800
+ my_id = self._mux.alloc_chan()
801
+ try:
802
+ tcp = socket.create_connection(self._target, timeout=self._timeout())
803
+ except Exception:
804
+ try:
805
+ self._send(bytes([CH.MSG_CHANNEL_OPEN_FAILURE])
806
+ + W.enc_u32(srv)
807
+ + W.enc_u32(2)
808
+ + W.enc_string(b"dial failed")
809
+ + W.enc_string(b""))
810
+ except Exception:
811
+ pass
812
+ return
813
+ try:
814
+ self._send(bytes([CH.MSG_CHANNEL_OPEN_CONFIRM])
815
+ + W.enc_u32(srv)
816
+ + W.enc_u32(my_id)
817
+ + W.enc_u32(CH.WINDOW)
818
+ + W.enc_u32(CH.MAXPKT))
819
+ except Exception:
820
+ try:
821
+ tcp.close()
822
+ except Exception:
823
+ pass
824
+ return
825
+ self._install_channel(my_id, tcp, srv, srv_window, srv_maxpacket)
826
+ try:
827
+ tcp.setblocking(False)
828
+ except Exception:
829
+ pass
830
+ return
831
+ if m == CH.MSG_CHANNEL_DATA:
832
+ try:
833
+ rcpt, data = r.u32(), r.string()
834
+ except Exception:
835
+ return
836
+ self._on_data(rcpt, data)
837
+ return
838
+ if m == CH.MSG_WINDOW_ADJUST:
839
+ try:
840
+ rcpt, delta = r.u32(), r.u32()
841
+ except Exception:
842
+ return
843
+ self._on_window(rcpt, delta)
844
+ return
845
+ if m == CH.MSG_CHANNEL_EOF:
846
+ try:
847
+ rcpt = r.u32()
848
+ except Exception:
849
+ return
850
+ self._on_remote_eof(rcpt)
851
+ return
852
+ if m == CH.MSG_CHANNEL_CLOSE:
853
+ try:
854
+ rcpt = r.u32()
855
+ except Exception:
856
+ return
857
+ self._on_remote_close(rcpt)
858
+ return
859
+
860
+ def _refuse_open(self, rr: W.Reader) -> None:
861
+ try:
862
+ srv = rr.u32()
863
+ except Exception:
864
+ return
865
+ try:
866
+ self._send(bytes([CH.MSG_CHANNEL_OPEN_FAILURE])
867
+ + W.enc_u32(srv)
868
+ + W.enc_u32(3)
869
+ + W.enc_string(b"unsupported channel type")
870
+ + W.enc_string(b""))
871
+ except Exception:
872
+ pass