diff --git a/CHANGELOG.md b/CHANGELOG.md index 720093433..ffcf3b743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ All notable changes to Bambuddy will be documented in this file. - **User Notification Ruff/Lint Fixes** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — Fixed missing `timezone` import in email timestamp, unused lambda argument, PEP 8 blank line spacing for `mark_printer_stopped_by_user`, and SQLAlchemy forward reference in `UserEmailPreference` model. - **Carbon Rod Lubrication Maintenance Task Incorrect** ([#755](https://github.com/maziggy/bambuddy/issues/755)) — X1/P1 series printers showed a "Lubricate Carbon Rods" maintenance task, but carbon rods use plain bearings and should never be lubricated — doing so degrades print quality. Removed the lubrication task; only "Clean Carbon Rods" remains. Existing "Lubricate Carbon Rods" entries are automatically removed on next startup. Reported by @RosdasHH. - **Ntfy Notifications Fail With Non-ASCII Characters** ([#742](https://github.com/maziggy/bambuddy/issues/742)) — Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters (e.g. accented letters, CJK). The `Title` and `Message` HTTP headers were passed as Python strings, causing httpx to reject them with `UnicodeEncodeError`. Fixed by encoding header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. Reported by @user. -- **Virtual Printer Proxy Mode Printing Fails on Isolated Networks** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — When the slicer and printer are on different VLANs/subnets, Bambu Studio could not send prints through the virtual printer proxy because: (1) the printer's real IP leaked through MQTT payloads (`rtsp_url`, `net.info[].ip`), causing BS to bypass the proxy; (2) the bind/detect protocol (port 3000/3002) was forwarded to the real printer, leaking its identity and name; (3) the file transfer tunnel (port 6000) used by BS for verify_job and uploads was not proxied; (4) FTP data connections for zero-byte uploads (verify_job) failed due to a TLS handshake race condition. Fixed by: rewriting IP addresses in MQTT PUBLISH payloads (both string and integer formats) with proper MQTT framing preservation, responding to bind/detect with the VP's own identity via BindServer, adding a TLS proxy for port 6000, buffering slicer data during FTP data proxy connection setup, and advertising the configured VP name in SSDP. Also added cross-subnet SSDP support via a wildcard listener for VPN/multi-subnet setups. Reported by @Utility9298. +- **Virtual Printer Proxy Mode Printing Fails on Isolated Networks** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — When the slicer and printer are on different VLANs/subnets, Bambu Studio could not send prints through the virtual printer proxy because: (1) the printer's real IP leaked through MQTT payloads (`rtsp_url`, `net.info[].ip`), causing BS to bypass the proxy; (2) the bind/detect protocol (port 3000/3002) was forwarded to the real printer, leaking its identity and name; (3) the file transfer tunnel (port 6000) used by BS for verify_job and uploads was not proxied; (4) FTP data connections for zero-byte uploads (verify_job) failed due to a TLS handshake race condition. Fixed by: rewriting IP addresses in MQTT PUBLISH payloads (both string and integer formats) with proper MQTT framing preservation, responding to bind/detect with the VP's own identity via BindServer, adding TLS proxies for port 6000 (file transfer) and port 322 (RTSP camera), buffering slicer data during FTP data proxy connection setup, and advertising the configured VP name in SSDP. Also added cross-subnet SSDP support via a wildcard listener for VPN/multi-subnet setups. Reported by @Utility9298. - **UserEmailPreference Model Not Registered** — The `UserEmailPreference` SQLAlchemy model was not imported in `models/__init__.py`, causing mapper initialization failures when the `User` model's relationship resolved the string reference before the model class was registered with Base metadata. ### Added diff --git a/backend/app/services/virtual_printer/tcp_proxy.py b/backend/app/services/virtual_printer/tcp_proxy.py index 1c28380e5..6c9164f08 100644 --- a/backend/app/services/virtual_printer/tcp_proxy.py +++ b/backend/app/services/virtual_printer/tcp_proxy.py @@ -540,7 +540,6 @@ class TLSProxy: await writer.drain() total_bytes += len(data) - logger.debug("%s proxy %s: %s bytes", self.name, direction, len(data)) except asyncio.CancelledError: pass # Expected when the other forwarding direction closes first @@ -1289,6 +1288,7 @@ class SlicerProxyManager: PRINTER_FTP_PORT = 990 PRINTER_MQTT_PORT = 8883 PRINTER_FILE_TRANSFER_PORT = 6000 + PRINTER_RTSP_PORT = 322 # X1/H2/P2 series camera (A1/P1 use port 6000) PRINTER_BIND_PORTS = [3000, 3002] # Local listen ports - must match what Bambu Studio expects @@ -1328,6 +1328,7 @@ class SlicerProxyManager: self._ftp_proxy: TLSProxy | None = None self._mqtt_proxy: TLSProxy | None = None self._file_transfer_proxy: TLSProxy | None = None + self._rtsp_proxy: TLSProxy | None = None self._bind_proxies: list[TCPProxy] = [] self._bind_server = None self._tasks: list[asyncio.Task] = [] @@ -1390,6 +1391,21 @@ class SlicerProxyManager: 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( + 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, + ) + # Bind/auth — respond with VP identity instead of proxying to printer. # The detect response contains the printer name, serial, model, and # bind status. Proxying it would leak the real printer's identity and @@ -1453,6 +1469,10 @@ class SlicerProxyManager: run_with_logging(self._file_transfer_proxy), name="slicer_proxy_file_transfer", ), + asyncio.create_task( + run_with_logging(self._rtsp_proxy), + name="slicer_proxy_rtsp", + ), ] if self._bind_server: self._tasks.append( @@ -1495,6 +1515,10 @@ class SlicerProxyManager: await self._file_transfer_proxy.stop() self._file_transfer_proxy = None + if self._rtsp_proxy: + await self._rtsp_proxy.stop() + self._rtsp_proxy = None + if self._bind_server: await self._bind_server.stop() self._bind_server = None diff --git a/backend/tests/unit/services/test_virtual_printer.py b/backend/tests/unit/services/test_virtual_printer.py index 8cc8353cd..798e506ce 100644 --- a/backend/tests/unit/services/test_virtual_printer.py +++ b/backend/tests/unit/services/test_virtual_printer.py @@ -1089,6 +1089,8 @@ class TestSlicerProxyManager: assert proxy_manager.LOCAL_MQTT_PORT == 8883 assert proxy_manager.PRINTER_FTP_PORT == 990 assert proxy_manager.PRINTER_MQTT_PORT == 8883 + assert proxy_manager.PRINTER_FILE_TRANSFER_PORT == 6000 + assert proxy_manager.PRINTER_RTSP_PORT == 322 # Bind ports: both 3000 and 3002 for slicer compatibility assert proxy_manager.PRINTER_BIND_PORTS == [3000, 3002]