mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
[Fix] Virtual Printer proxy: transparent TCP for X1C/X1 compatibility (#757)
The closed-source bambu_networking DLL validates TLS connection parameters
and rejects connections where the certificate doesn't match the printer's
real BBL CA certificate. The TLS-terminating proxy presented Bambuddy's
own certificate, causing X1C/X1 prints to silently fail after verify_job.
Switch to transparent TCP proxying for FTP, FileTransfer, Camera, and FTP
data — only MQTT remains TLS-terminated (required for IP rewriting). The
slicer now gets end-to-end TLS directly with the printer's real certificate.
Changes:
- SlicerProxyManager uses TCPProxy for FTP (990), FileTransfer (6000),
Camera (322), and pre-listens on FTP data ports (50000-50100)
- Only MQTT (8883) uses TLSProxy for IP rewriting
- Remove debug logging from MQTT and FTP proxy code
- Fix install.sh missing AmbientCapabilities=CAP_NET_BIND_SERVICE
- Update module docstring, migration docs, README proxy description
- Add tests verifying transparent proxy architecture
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
"""TLS proxy for slicer-to-printer communication.
|
||||
"""Proxy for slicer-to-printer communication.
|
||||
|
||||
This module provides a TLS terminating proxy that forwards data between
|
||||
a slicer and a real Bambu printer, enabling remote printing over
|
||||
any network connection.
|
||||
This module provides both transparent TCP proxying and TLS-terminating
|
||||
proxying for forwarding data between a slicer and a real Bambu printer,
|
||||
enabling remote printing over any network connection.
|
||||
|
||||
Unlike a transparent TCP proxy, this terminates TLS on both ends:
|
||||
- Slicer connects to Bambuddy using Bambuddy's certificate
|
||||
- Bambuddy connects to printer using printer's certificate
|
||||
- Data is decrypted, forwarded, and re-encrypted
|
||||
Most protocols (FTP, FileTransfer, Camera) use transparent TCP proxying —
|
||||
raw bytes are forwarded without decryption, preserving end-to-end TLS
|
||||
between slicer and printer. Only MQTT is TLS-terminated so Bambuddy can
|
||||
rewrite the printer's real IP with the proxy's bind IP in MQTT payloads.
|
||||
"""
|
||||
|
||||
# ruff: noqa: N801
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
@@ -22,6 +24,44 @@ from pathlib import Path
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _SessionReuseSSLContext:
|
||||
"""Proxy around SSLContext that injects a TLS session into wrap_bio().
|
||||
|
||||
vsFTPd (used by some Bambu printers like X1C) requires TLS session reuse
|
||||
on FTP data channels — the data connection must reuse the TLS session from
|
||||
the control channel. Without this, the printer rejects the data connection
|
||||
with "522 SSL connection failed: session reuse required".
|
||||
|
||||
asyncio's open_connection() calls SSLContext.wrap_bio() internally but
|
||||
doesn't expose a session parameter. This wrapper intercepts wrap_bio()
|
||||
to inject the saved control-channel session, enabling session reuse.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: ssl.SSLContext, session: ssl.SSLSession) -> None:
|
||||
object.__setattr__(self, "_ctx", ctx)
|
||||
object.__setattr__(self, "_session", session)
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return getattr(self._ctx, name)
|
||||
|
||||
def wrap_bio(
|
||||
self,
|
||||
incoming: ssl.MemoryBIO,
|
||||
outgoing: ssl.MemoryBIO,
|
||||
server_side: bool = False,
|
||||
server_hostname: str | None = None,
|
||||
**kwargs: object,
|
||||
) -> ssl.SSLObject:
|
||||
return self._ctx.wrap_bio(
|
||||
incoming,
|
||||
outgoing,
|
||||
server_side=server_side,
|
||||
server_hostname=server_hostname,
|
||||
session=self._session,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def detect_port_redirect(port: int) -> int | None:
|
||||
"""Detect if iptables redirects a port to another port.
|
||||
|
||||
@@ -515,7 +555,6 @@ class TLSProxy:
|
||||
# Rewrite printer IP → proxy IP in MQTT PUBLISH payloads
|
||||
# to prevent the slicer from bypassing the proxy.
|
||||
if do_rewrite:
|
||||
original_len = len(data)
|
||||
extra = [(self._rewrite_old_int, self._rewrite_new_int)] if self._rewrite_old_int else None
|
||||
data, rewrite_buffer = self._rewrite_mqtt_ip(
|
||||
data,
|
||||
@@ -524,13 +563,20 @@ class TLSProxy:
|
||||
rewrite_buffer,
|
||||
extra_replacements=extra,
|
||||
)
|
||||
if not rewrite_logged and data and len(data) != original_len:
|
||||
logger.info(
|
||||
"%s proxy IP rewrite active: %s → %s",
|
||||
self.name,
|
||||
self._rewrite_old.decode(),
|
||||
self._rewrite_new.decode(),
|
||||
)
|
||||
if not rewrite_logged and data:
|
||||
if self._rewrite_old in data:
|
||||
logger.warning(
|
||||
"%s proxy IP rewrite FAILED — %s still present after rewrite!",
|
||||
self.name,
|
||||
self._rewrite_old.decode(),
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"%s proxy IP rewrite active: %s → %s",
|
||||
self.name,
|
||||
self._rewrite_old.decode(),
|
||||
self._rewrite_new.decode(),
|
||||
)
|
||||
rewrite_logged = True
|
||||
if not data:
|
||||
continue # All data buffered, waiting for more
|
||||
@@ -835,11 +881,21 @@ class FTPTLSProxy(TLSProxy):
|
||||
await client_writer.wait_closed()
|
||||
return
|
||||
|
||||
# Capture the TLS session from the control channel for data channel
|
||||
# reuse. vsFTPd (X1C) requires require_ssl_reuse — the data connection
|
||||
# must present the same TLS session as the control channel.
|
||||
ctrl_ssl_object = printer_writer.get_extra_info("ssl_object")
|
||||
ctrl_tls_session = ctrl_ssl_object.session if ctrl_ssl_object else None
|
||||
if ctrl_tls_session:
|
||||
logger.debug("%s proxy: captured TLS session for data channel reuse", self.name)
|
||||
|
||||
# Track data channel protection level per session.
|
||||
# PROT C = cleartext data, PROT P = TLS data.
|
||||
# Default to cleartext — many Bambu printers (A1, H2D) use PROT C.
|
||||
# If the slicer sends PROT P, we switch to TLS for data connections.
|
||||
session_state: dict[str, str] = {"prot": "C"}
|
||||
session_state: dict[str, str | ssl.SSLSession] = {"prot": "C"}
|
||||
if ctrl_tls_session:
|
||||
session_state["tls_session"] = ctrl_tls_session
|
||||
|
||||
# Client→Printer: intercept EPSV and replace with PASV
|
||||
# EPSV responses only contain a port (no IP), so the slicer reuses
|
||||
@@ -895,7 +951,7 @@ class FTPTLSProxy(TLSProxy):
|
||||
reader: asyncio.StreamReader,
|
||||
writer: asyncio.StreamWriter,
|
||||
direction: str,
|
||||
session_state: dict[str, str],
|
||||
session_state: dict[str, str | ssl.SSLSession],
|
||||
) -> None:
|
||||
"""Forward FTP client commands, replacing EPSV with PASV.
|
||||
|
||||
@@ -927,13 +983,8 @@ class FTPTLSProxy(TLSProxy):
|
||||
|
||||
cmd_upper = line.strip().upper()
|
||||
|
||||
# Replace EPSV with PASV so response includes an IP
|
||||
if cmd_upper == b"EPSV":
|
||||
line = b"PASV"
|
||||
logger.info("FTP command rewrite: EPSV → PASV")
|
||||
|
||||
# Track PROT level for data channel encryption
|
||||
elif cmd_upper == b"PROT P":
|
||||
if cmd_upper == b"PROT P":
|
||||
session_state["prot"] = "P"
|
||||
logger.info("FTP data protection: PROT P (TLS)")
|
||||
elif cmd_upper == b"PROT C":
|
||||
@@ -972,7 +1023,7 @@ class FTPTLSProxy(TLSProxy):
|
||||
writer: asyncio.StreamWriter,
|
||||
direction: str,
|
||||
local_ip: str,
|
||||
session_state: dict[str, str],
|
||||
session_state: dict[str, str | ssl.SSLSession],
|
||||
) -> None:
|
||||
"""Forward FTP control channel responses, rewriting PASV/EPSV.
|
||||
|
||||
@@ -1027,7 +1078,9 @@ class FTPTLSProxy(TLSProxy):
|
||||
|
||||
logger.debug("%s proxy %s: total %s bytes", self.name, direction, total_bytes)
|
||||
|
||||
async def _maybe_rewrite_pasv(self, line: bytes, local_ip: str, session_state: dict[str, str]) -> bytes:
|
||||
async def _maybe_rewrite_pasv(
|
||||
self, line: bytes, local_ip: str, session_state: dict[str, str | ssl.SSLSession]
|
||||
) -> bytes:
|
||||
"""Rewrite PASV/EPSV response to point to a local data proxy."""
|
||||
try:
|
||||
text = line.decode("utf-8")
|
||||
@@ -1076,7 +1129,9 @@ class FTPTLSProxy(TLSProxy):
|
||||
|
||||
return line
|
||||
|
||||
async def _create_data_proxy(self, printer_ip: str, printer_port: int, session_state: dict[str, str]) -> int | None:
|
||||
async def _create_data_proxy(
|
||||
self, printer_ip: str, printer_port: int, session_state: dict[str, str | ssl.SSLSession]
|
||||
) -> int | None:
|
||||
"""Create a one-shot proxy for an FTP data connection.
|
||||
|
||||
Prefers the printer's original passive port so the port number stays
|
||||
@@ -1102,10 +1157,13 @@ class FTPTLSProxy(TLSProxy):
|
||||
"TLS" if use_tls else "cleartext",
|
||||
)
|
||||
|
||||
# Get control channel TLS session for data channel reuse
|
||||
tls_session = session_state.get("tls_session") if use_tls else None
|
||||
|
||||
# Try the printer's original port first — this ensures the port
|
||||
# matches even when bounce protection or iptables REDIRECT is in play.
|
||||
try:
|
||||
await self._start_data_proxy_server(printer_port, printer_ip, printer_port, use_tls)
|
||||
await self._start_data_proxy_server(printer_port, printer_ip, printer_port, use_tls, tls_session)
|
||||
logger.info("FTP data proxy: using printer's port %s", printer_port)
|
||||
return printer_port
|
||||
except OSError as e:
|
||||
@@ -1118,7 +1176,7 @@ class FTPTLSProxy(TLSProxy):
|
||||
for _attempt in range(10):
|
||||
port = random.randint(self.PASV_PORT_MIN, self.PASV_PORT_MAX)
|
||||
try:
|
||||
await self._start_data_proxy_server(port, printer_ip, printer_port, use_tls)
|
||||
await self._start_data_proxy_server(port, printer_ip, printer_port, use_tls, tls_session)
|
||||
logger.info("FTP data proxy: using random port %s", port)
|
||||
return port
|
||||
except OSError:
|
||||
@@ -1127,7 +1185,14 @@ class FTPTLSProxy(TLSProxy):
|
||||
logger.error("Failed to bind FTP data proxy port after 10 attempts")
|
||||
return None
|
||||
|
||||
async def _start_data_proxy_server(self, port: int, printer_ip: str, printer_port: int, use_tls: bool) -> None:
|
||||
async def _start_data_proxy_server(
|
||||
self,
|
||||
port: int,
|
||||
printer_ip: str,
|
||||
printer_port: int,
|
||||
use_tls: bool,
|
||||
tls_session: ssl.SSLSession | None = None,
|
||||
) -> None:
|
||||
"""Start a one-shot server for one FTP data connection.
|
||||
|
||||
When the slicer connects, immediately connects to the printer's data
|
||||
@@ -1154,7 +1219,18 @@ class FTPTLSProxy(TLSProxy):
|
||||
# Slicer side: ALWAYS cleartext — Bambu Studio does not do TLS on
|
||||
# the data channel even after sending PROT P.
|
||||
# Printer side: TLS if PROT P, cleartext if PROT C.
|
||||
client_ssl = self._client_ssl_context if use_tls else None
|
||||
# For TLS data connections, wrap the SSL context to reuse the
|
||||
# control channel's TLS session if available. vsFTPd (X1C) requires
|
||||
# require_ssl_reuse — without this, data connections are rejected
|
||||
# with "522 SSL connection failed: session reuse required".
|
||||
if use_tls and tls_session:
|
||||
client_ssl = _SessionReuseSSLContext(self._client_ssl_context, tls_session)
|
||||
logger.debug("FTP data proxy: using TLS session reuse for port %s", port)
|
||||
else:
|
||||
client_ssl = self._client_ssl_context if use_tls else None
|
||||
|
||||
# Slicer side is ALWAYS cleartext — Bambu Studio does not do TLS on
|
||||
# the data channel even after PROT P (confirmed for both H2D and X1C).
|
||||
printer_mode = "TLS" if use_tls else "cleartext"
|
||||
|
||||
async def handle_data(
|
||||
@@ -1217,27 +1293,84 @@ class FTPTLSProxy(TLSProxy):
|
||||
pass
|
||||
|
||||
# Flush buffered slicer data to printer
|
||||
logger.info(
|
||||
"FTP data proxy port %s: buffer=%s bytes, slicer_eof=%s",
|
||||
port,
|
||||
len(slicer_buffer),
|
||||
slicer_eof,
|
||||
)
|
||||
if slicer_buffer:
|
||||
printer_writer.write(bytes(slicer_buffer))
|
||||
await printer_writer.drain()
|
||||
|
||||
if slicer_eof:
|
||||
# Slicer already closed (zero-byte upload like verify_job).
|
||||
# Close the printer write side to signal upload complete.
|
||||
if printer_writer.can_write_eof():
|
||||
printer_writer.write_eof()
|
||||
else:
|
||||
# Continue bidirectional forwarding
|
||||
c2p = asyncio.create_task(self._forward(client_reader, printer_writer, "data_c2p"))
|
||||
p2c = asyncio.create_task(self._forward(printer_reader, client_writer, "data_p2c"))
|
||||
|
||||
done, pending = await asyncio.wait([c2p, p2c], return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
# Forward remaining slicer data to printer, then close the
|
||||
# printer side to signal upload complete.
|
||||
#
|
||||
# Bambu Studio does NOT close the FTP data channel after sending
|
||||
# STOR data — it keeps the connection open and waits for the
|
||||
# printer to close its side + send 226 on the control channel.
|
||||
# A naive bidirectional proxy deadlocks here because the proxy
|
||||
# waits for the slicer EOF that never comes.
|
||||
#
|
||||
# Fix: read slicer data with an idle timeout. Once data has been
|
||||
# received and the slicer goes quiet, close the printer side so
|
||||
# the printer can send 226. For RETR (download), the printer
|
||||
# sends data and closes — the slicer reads until EOF — so this
|
||||
# unidirectional approach works for both directions.
|
||||
total_c2p = len(slicer_buffer)
|
||||
if not slicer_eof:
|
||||
# Read remaining slicer data with idle detection.
|
||||
# Must be short — Bambu Studio expects 226 almost instantly
|
||||
# after sending data. Too long and the slicer times out.
|
||||
idle_timeout = 0.3
|
||||
while True:
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass # Expected when other data direction closes
|
||||
chunk = await asyncio.wait_for(client_reader.read(65536), timeout=idle_timeout)
|
||||
except TimeoutError:
|
||||
if total_c2p > 0:
|
||||
# Slicer sent data then went idle — upload done
|
||||
logger.debug(
|
||||
"FTP data proxy port %s: slicer idle after %s bytes, closing printer side",
|
||||
port,
|
||||
total_c2p,
|
||||
)
|
||||
break
|
||||
continue # No data yet, keep waiting
|
||||
if not chunk:
|
||||
break # Slicer closed
|
||||
printer_writer.write(chunk)
|
||||
await printer_writer.drain()
|
||||
total_c2p += len(chunk)
|
||||
|
||||
logger.debug("FTP proxy data_c2p: total %s bytes", total_c2p)
|
||||
|
||||
# Close printer side to signal upload complete.
|
||||
# For TLS, close() sends close_notify which the printer treats
|
||||
# as end-of-data. The printer then sends 226 on the control
|
||||
# channel. For RETR, this is a no-op since the printer closes
|
||||
# first and we'd have exited the loop above via EOF.
|
||||
try:
|
||||
printer_writer.close()
|
||||
await printer_writer.wait_closed()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Wait for 226 response to propagate through the FTP control
|
||||
# channel before closing the slicer's data channel.
|
||||
#
|
||||
# Without this delay, the data channel FIN arrives at the
|
||||
# slicer before the 226 response on the control channel.
|
||||
# BambuStudio reacts to the data channel FIN within <1ms
|
||||
# by sending QUIT + closing the control channel — before
|
||||
# 226 arrives (~2-3ms network RTT). This causes verify_job
|
||||
# to be treated as failed and shows the login modal.
|
||||
#
|
||||
# In a direct connection, the printer sends 226 AND closes
|
||||
# the data channel simultaneously, so the slicer gets both
|
||||
# at once. The delay here emulates that timing.
|
||||
if total_c2p > 0:
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("FTP data proxy port %s: error: %s", port, e)
|
||||
finally:
|
||||
@@ -1255,8 +1388,7 @@ class FTPTLSProxy(TLSProxy):
|
||||
"0.0.0.0", # nosec B104
|
||||
port,
|
||||
# No TLS on slicer side — Bambu Studio doesn't do TLS on data
|
||||
# channel even after PROT P. The proxy terminates TLS only on
|
||||
# the printer side (inside handle_data).
|
||||
# channel even after PROT P (confirmed by connection hang test).
|
||||
)
|
||||
server_holder.append(server)
|
||||
self._data_servers.append(server)
|
||||
@@ -1325,21 +1457,33 @@ class SlicerProxyManager:
|
||||
self.bind_address = bind_address
|
||||
self.bind_identity = bind_identity
|
||||
|
||||
self._ftp_proxy: TLSProxy | None = None
|
||||
self._ftp_proxy: TCPProxy | None = None
|
||||
self._mqtt_proxy: TLSProxy | None = None
|
||||
self._file_transfer_proxy: TLSProxy | None = None
|
||||
self._rtsp_proxy: TLSProxy | None = None
|
||||
self._file_transfer_proxy: TCPProxy | None = None
|
||||
self._rtsp_proxy: TCPProxy | None = None
|
||||
self._bind_proxies: list[TCPProxy] = []
|
||||
self._bind_server = None
|
||||
self._tasks: list[asyncio.Task] = []
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start FTP and MQTT TLS proxies."""
|
||||
logger.info("Starting slicer TLS proxy to %s", self.target_host)
|
||||
# FTP passive data port range — Bambu printers typically use ports in
|
||||
# this range for EPSV/PASV data connections. We pre-listen on all of
|
||||
# them so EPSV works transparently without decrypting FTP control.
|
||||
FTP_DATA_PORT_MIN = 50000
|
||||
FTP_DATA_PORT_MAX = 50100
|
||||
|
||||
# Detect iptables port redirect (e.g. if an external redirect exists).
|
||||
# If active, connections get intercepted by iptables PREROUTING
|
||||
# and sent to the redirect target — our socket never sees them.
|
||||
async def start(self) -> None:
|
||||
"""Start proxy services.
|
||||
|
||||
Uses transparent TCP proxying for most protocols (FTP, FileTransfer,
|
||||
Camera) — raw bytes are forwarded without TLS termination, so the
|
||||
slicer gets the printer's real TLS certificate end-to-end.
|
||||
|
||||
Only MQTT is TLS-terminated because we must decrypt the payload to
|
||||
rewrite the printer's real IP with the proxy's bind IP.
|
||||
"""
|
||||
logger.info("Starting slicer proxy to %s (transparent mode)", self.target_host)
|
||||
|
||||
# Detect iptables port redirect for FTP
|
||||
ftp_listen_port = self.LOCAL_FTP_PORT
|
||||
redirect_target = detect_port_redirect(self.LOCAL_FTP_PORT)
|
||||
if redirect_target:
|
||||
@@ -1351,19 +1495,35 @@ class SlicerProxyManager:
|
||||
)
|
||||
ftp_listen_port = redirect_target
|
||||
|
||||
# Create FTP proxy with PASV/EPSV awareness for data connections
|
||||
self._ftp_proxy = FTPTLSProxy(
|
||||
# FTP control — raw TCP pass-through (end-to-end TLS with printer)
|
||||
self._ftp_proxy = TCPProxy(
|
||||
name="FTP",
|
||||
listen_port=ftp_listen_port,
|
||||
target_host=self.target_host,
|
||||
target_port=self.PRINTER_FTP_PORT,
|
||||
server_cert_path=self.cert_path,
|
||||
server_key_path=self.key_path,
|
||||
on_connect=lambda cid: self._log_activity("FTP", f"connected: {cid}"),
|
||||
on_disconnect=lambda cid: self._log_activity("FTP", f"disconnected: {cid}"),
|
||||
bind_address=self.bind_address,
|
||||
)
|
||||
|
||||
# FTP data ports — pre-listen on the entire passive port range.
|
||||
# Since FTP control is encrypted end-to-end, we can't read EPSV
|
||||
# responses to know which port the printer chose. Instead, we
|
||||
# listen on every port in the range and forward to the same port
|
||||
# on the printer. The slicer connects to bind_ip:PORT (from EPSV)
|
||||
# and we transparently relay to printer_ip:PORT.
|
||||
self._ftp_data_proxies: list[TCPProxy] = []
|
||||
for port in range(self.FTP_DATA_PORT_MIN, self.FTP_DATA_PORT_MAX + 1):
|
||||
dp = TCPProxy(
|
||||
name=f"FTP-Data-{port}",
|
||||
listen_port=port,
|
||||
target_host=self.target_host,
|
||||
target_port=port,
|
||||
bind_address=self.bind_address,
|
||||
)
|
||||
self._ftp_data_proxies.append(dp)
|
||||
|
||||
# MQTT — TLS-terminating proxy (must decrypt to rewrite IP addresses)
|
||||
self._mqtt_proxy = TLSProxy(
|
||||
name="MQTT",
|
||||
listen_port=self.LOCAL_MQTT_PORT,
|
||||
@@ -1377,30 +1537,23 @@ class SlicerProxyManager:
|
||||
rewrite_ip=(self.target_host, self.bind_address) if self.bind_address != "0.0.0.0" else None,
|
||||
)
|
||||
|
||||
# File transfer proxy — port 6000 (TLS)
|
||||
# BambuStudio connects here for verify_job and actual file uploads.
|
||||
self._file_transfer_proxy = TLSProxy(
|
||||
# File transfer — raw TCP pass-through (port 6000)
|
||||
self._file_transfer_proxy = TCPProxy(
|
||||
name="FileTransfer",
|
||||
listen_port=self.PRINTER_FILE_TRANSFER_PORT,
|
||||
target_host=self.target_host,
|
||||
target_port=self.PRINTER_FILE_TRANSFER_PORT,
|
||||
server_cert_path=self.cert_path,
|
||||
server_key_path=self.key_path,
|
||||
on_connect=lambda cid: self._log_activity("FileTransfer", f"connected: {cid}"),
|
||||
on_disconnect=lambda cid: self._log_activity("FileTransfer", f"disconnected: {cid}"),
|
||||
bind_address=self.bind_address,
|
||||
)
|
||||
|
||||
# RTSP camera proxy — port 322 (TLS)
|
||||
# X1/H2/P2 series use RTSP on port 322 for camera streaming.
|
||||
# A1/P1 series use port 6000 (already proxied via file transfer proxy).
|
||||
self._rtsp_proxy = TLSProxy(
|
||||
# RTSP camera — raw TCP pass-through (port 322)
|
||||
self._rtsp_proxy = TCPProxy(
|
||||
name="RTSP",
|
||||
listen_port=self.PRINTER_RTSP_PORT,
|
||||
target_host=self.target_host,
|
||||
target_port=self.PRINTER_RTSP_PORT,
|
||||
server_cert_path=self.cert_path,
|
||||
server_key_path=self.key_path,
|
||||
on_connect=lambda cid: self._log_activity("RTSP", f"connected: {cid}"),
|
||||
on_disconnect=lambda cid: self._log_activity("RTSP", f"disconnected: {cid}"),
|
||||
bind_address=self.bind_address,
|
||||
@@ -1450,7 +1603,7 @@ class SlicerProxyManager:
|
||||
self._bind_proxies.append(proxy)
|
||||
|
||||
# Start as background tasks
|
||||
async def run_with_logging(proxy: TLSProxy) -> None:
|
||||
async def run_with_logging(proxy: TLSProxy | TCPProxy) -> None:
|
||||
try:
|
||||
await proxy.start()
|
||||
except Exception as e:
|
||||
@@ -1488,8 +1641,20 @@ class SlicerProxyManager:
|
||||
name=f"slicer_proxy_bind_{bp.listen_port}",
|
||||
)
|
||||
)
|
||||
# FTP data port proxies (50000-50100)
|
||||
for dp in self._ftp_data_proxies:
|
||||
self._tasks.append(
|
||||
asyncio.create_task(
|
||||
run_with_logging(dp),
|
||||
name=f"slicer_proxy_ftp_data_{dp.listen_port}",
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("Slicer TLS proxy started for %s", self.target_host)
|
||||
logger.info(
|
||||
"Slicer proxy started for %s (transparent TCP + MQTT TLS, %d FTP data ports)",
|
||||
self.target_host,
|
||||
len(self._ftp_data_proxies),
|
||||
)
|
||||
|
||||
# Wait for tasks to complete (they run until cancelled)
|
||||
# This keeps the start() coroutine alive so the parent task doesn't complete
|
||||
@@ -1527,6 +1692,10 @@ class SlicerProxyManager:
|
||||
await bp.stop()
|
||||
self._bind_proxies = []
|
||||
|
||||
for dp in self._ftp_data_proxies:
|
||||
await dp.stop()
|
||||
self._ftp_data_proxies = []
|
||||
|
||||
# Cancel tasks
|
||||
for task in self._tasks:
|
||||
task.cancel()
|
||||
|
||||
Reference in New Issue
Block a user