mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
[Fix] Virtual Printer FTP routed to wrong VP with different access codes (#735)
When running multiple virtual printers with different access codes on separate bind IPs, FTP connections were always routed to the wrong VP. Root cause: the iptables REDIRECT rule (990→9990) rewrites the destination IP to the incoming interface's primary address. With Linux's weak host model (arp_filter=0), packets for secondary IPs arrive on the primary interface, and REDIRECT sends them all to the first VP's FTP server. MQTT was unaffected because port 8883 had no redirect. Fix: FTP server now binds directly to port 990 (standard implicit FTPS), eliminating the iptables redirect entirely. Requires CAP_NET_BIND_SERVICE (already set in the systemd service file and Docker image). Also removed a global asyncio set_exception_handler() in the MQTT server that was overwritten by each VP instance, causing spurious "Unhandled exception in client_connected_cb" errors on startup. Changes: - FTP_PORT: 9990 → 990 (ftp_server.py) - Removed set_exception_handler() from MQTT server - Updated Dockerfile, docker-compose.yml port mappings - Deprecated --redirect-990 in install script - Updated wiki: removed iptables instructions for all platforms - Added migration guide (docs/migration-vp-ftp-port.md) - Added unit tests for port constant and no-global-state invariant
This commit is contained in:
@@ -17,8 +17,12 @@ from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default FTP port for Bambu printers (implicit FTPS)
|
||||
FTP_PORT = 9990
|
||||
# Default FTP port for Bambu printers (implicit FTPS).
|
||||
# Must be 990 (same as real printers) to avoid iptables REDIRECT,
|
||||
# which rewrites the destination IP to the incoming interface's primary
|
||||
# address — breaking multi-VP setups with different bind IPs.
|
||||
# Requires CAP_NET_BIND_SERVICE or root.
|
||||
FTP_PORT = 990
|
||||
|
||||
|
||||
class FTPSession:
|
||||
@@ -568,7 +572,7 @@ class VirtualPrinterFTPServer:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
logger.info("Starting virtual printer implicit FTPS on port %s", self.port)
|
||||
logger.info("[%s] Starting virtual printer implicit FTPS on %s:%s", self.vp_name, self.bind_address, self.port)
|
||||
|
||||
# Ensure upload directory exists
|
||||
self.upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -689,7 +689,7 @@ class VirtualPrinterManager:
|
||||
)
|
||||
self._instances[vp.id] = instance
|
||||
await instance.start_proxy()
|
||||
logger.info("Started proxy VP: %s → %s", instance.name, target_ip)
|
||||
logger.info("Started proxy VP: %s → %s (bind=%s)", instance.name, target_ip, instance.bind_ip)
|
||||
else:
|
||||
instance = VirtualPrinterInstance(
|
||||
vp_id=vp.id,
|
||||
|
||||
@@ -262,20 +262,6 @@ class SimpleMQTTServer:
|
||||
except Exception as e:
|
||||
logger.error("MQTT connection handler error: %s", e)
|
||||
|
||||
# Custom protocol factory to log raw connection attempts
|
||||
logger.info("Setting up MQTT server with SSL error handling...")
|
||||
|
||||
# Add SSL handshake error callback
|
||||
def handle_ssl_error(loop, context):
|
||||
exception = context.get("exception")
|
||||
message = context.get("message", "")
|
||||
if "ssl" in str(exception).lower() or "ssl" in message.lower():
|
||||
logger.error("SSL error: %s - %s", message, exception)
|
||||
else:
|
||||
logger.debug("Asyncio error: %s", message)
|
||||
|
||||
asyncio.get_event_loop().set_exception_handler(handle_ssl_error)
|
||||
|
||||
self._server = await asyncio.start_server(
|
||||
connection_handler,
|
||||
self.bind_address,
|
||||
|
||||
@@ -232,7 +232,13 @@ class VirtualPrinterSSDPServer:
|
||||
try:
|
||||
msg = self._build_notify_message()
|
||||
self._socket.sendto(msg, (SSDP_BROADCAST_ADDR, SSDP_PORT))
|
||||
logger.debug("Sent SSDP NOTIFY for %s", self.name)
|
||||
logger.debug(
|
||||
"Sent SSDP NOTIFY for %s (Location=%s, USN=%s, bind=%s)",
|
||||
self.name,
|
||||
self._get_local_ip(),
|
||||
self.serial,
|
||||
self._bind_ip,
|
||||
)
|
||||
except OSError as e:
|
||||
logger.debug("Failed to send NOTIFY for %s: %s", self.name, e)
|
||||
|
||||
@@ -278,7 +284,13 @@ class VirtualPrinterSSDPServer:
|
||||
try:
|
||||
response = self._build_response_message()
|
||||
self._socket.sendto(response, addr)
|
||||
logger.info("Sent SSDP response to %s for virtual printer '%s'", addr[0], self.name)
|
||||
logger.info(
|
||||
"Sent SSDP response to %s for '%s' (Location=%s, USN=%s)",
|
||||
addr[0],
|
||||
self.name,
|
||||
self._get_local_ip(),
|
||||
self.serial,
|
||||
)
|
||||
except OSError as e:
|
||||
logger.debug("Failed to send SSDP response for %s: %s", self.name, e)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
def detect_port_redirect(port: int) -> int | None:
|
||||
"""Detect if iptables redirects a port to another port.
|
||||
|
||||
When iptables NAT REDIRECT rules exist (e.g. 990→9990), connections
|
||||
When iptables NAT REDIRECT rules exist (e.g. port redirects), connections
|
||||
to the original port never reach our socket because iptables intercepts
|
||||
them in PREROUTING. We must listen on the redirect target instead.
|
||||
|
||||
@@ -1087,9 +1087,9 @@ class SlicerProxyManager:
|
||||
"""Start FTP and MQTT TLS proxies."""
|
||||
logger.info("Starting slicer TLS proxy to %s", self.target_host)
|
||||
|
||||
# Detect iptables port redirect (e.g. 990→9990 for non-root installs).
|
||||
# If active, connections to port 990 get intercepted by iptables PREROUTING
|
||||
# and sent to the redirect target — our socket on 990 never sees them.
|
||||
# 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.
|
||||
ftp_listen_port = self.LOCAL_FTP_PORT
|
||||
redirect_target = detect_port_redirect(self.LOCAL_FTP_PORT)
|
||||
if redirect_target:
|
||||
|
||||
Reference in New Issue
Block a user