refactor(virtual-printer): drop Tailscale LE cert path, keep toggle informational

The Tailscale toggle was supposed to obtain a publicly-trusted Let's Encrypt
cert via `tailscale cert` so users wouldn't need to import Bambuddy's CA into
the slicer. End-to-end testing showed this was always going to fail:

  - Bambu Studio and OrcaSlicer refuse hostname input in the Add Printer
    dialog (IP-only).
  - Their printer-MQTT trust path validates only against the bundled BBL CA
    store (`printer.cer`), NOT the system trust store. Confirmed against
    ClusterM/open-bambu-networking's clean-room reimplementation:
    `mosquitto_tls_set(BBL_CA)` + `verify_peer=1` + `tls_insecure=true` —
    chain validation against BBL CA only, hostname check intentionally
    skipped (because Bambu's printer cert CN is the device serial).
  - LE certs don't chain to BBL CA, so the slicer rejects with the
    well-known "-1" before any hostname/IP logic runs.

The cert-import step is unavoidable; LE provisioning was dead code for slicer
connections. Pivot:

  - Toggle stays as an informational marker — when ON, the VP card surfaces
    the host's Tailscale IP + MagicDNS hostname so users know what to paste
    into the slicer.
  - Cert is always self-signed (signed by `bbl_ca`).
  - Tailscale exposure is via the existing bind_ip dropdown, which already
    includes `tailscale0` IPs.
  - Tailscale's role is strictly network reach — same trust burden as LAN.

Backend cuts:

  - `tailscale.py`: `provision_cert`, `ensure_cert`, `cert_needs_renewal`,
    `_FQDN_RE`, `_HTTPS_DISABLED_RE`, `TS_CERT_EXPIRY_THRESHOLD_DAYS`,
    `cryptography` import. Keep `get_status` and `TailscaleStatus`.
  - `certificate.py`: `ts_cert_path`, `ts_key_path`, `use_tailscale_cert`.
  - `manager.py`: `tailscale_fqdn` field, `_cert_renewal_task`,
    `_cert_restart_task`, `_cert_renewal_loop`, `_restart_for_cert_renewal`,
    `_cancel_renewal_task`, `_cancel_restart_task`. Simplify
    `_resolve_cert_and_advertise` to a sync method that just generates the
    self-signed cert. Drop `tailscale_disabled` from the change-detection
    diff (toggle is informational — no service restart needed).
  - `routes/virtual_printers.py` + `routes/settings.py`: drop the
    `tailscale_not_available` 409 guard on toggle-enable.

Frontend cuts:

  - `VirtualPrinterCard.tsx`: FQDN/IP display sourced from
    `multiVirtualPrinterApi.getTailscaleStatus()` (host-level) when toggle
    is ON, instead of `printer.status.tailscale_fqdn` (cert side-effect,
    no longer populated). Drop the `tailscale_not_available` toast handler.
  - `api/client.ts`: drop `tailscale_fqdn` from the VP status type.
  - i18n: rewrite `tailscaleDisabled.description` in all 8 locales to drop
    the "no cert import" promise. Remove `toast.tailscaleNotAvailable` key.

Docs:

  - Wiki `features/virtual-printer.md`: rewrite the entire Tailscale section
    — remove the LE-cert + HTTPS-Certs-toggle + tailscale-cert-operator
    steps, document the toggle as informational, keep the Docker socket
    mount + LXC TUN troubleshooting (those still apply for daemon
    reachability).
  - README: drop "the Tailscale benefit here is the tunnel, not cert-import
    elimination" framing in favour of "surfaces the IP for paste into
    slicer; CA import unchanged because BBL CA store, not system trust
    store, is what gets validated".

Tests:

  - `test_tailscale.py`: reduced to surviving `get_status` cases (binary
    missing, command fails, success, empty DNSName, malformed JSON).
  - `test_virtual_printer.py::test_sync_from_db_restarts_on_tailscale_disabled_change`
    → `test_sync_from_db_does_not_restart_on_tailscale_toggle` (toggle is
    informational; `remove_instance` must NOT be called).
  - `test_virtual_printer_api.py::TestVirtualPrinterTailscaleGuardAPI` →
    `TestVirtualPrinterTailscaleToggleAPI` (single test asserts both
    directions succeed and daemon is never consulted).
  - `VirtualPrinterCard.test.tsx`: mock now stubs `getTailscaleStatus`;
    FQDN-copy block drives data through that query.

DB column `tailscale_disabled` is kept (persists toggle state) — Postgres-
safe column drop is harder; future cleanup can remove if the toggle goes
away entirely. LE cert files on disk (`virtual_printer_ts.{crt,key}`) are
left in place per VP — harmless residue, manual cleanup if desired.

Verified: ruff clean, 2484 backend unit tests pass, 17 frontend VP-card
tests pass, frontend build succeeds, live service restart confirms VPs
serve `issuer=CN=Virtual Printer CA` on the Tailscale interface — slicer
trusts the user-imported bambuddy CA and skips hostname checks, so MQTT
connection succeeds end-to-end.
This commit is contained in:
maziggy
2026-05-03 14:58:29 +02:00
parent 556d666889
commit 64899a8ca4
23 changed files with 246 additions and 1377 deletions
+20 -11
View File
@@ -50,9 +50,21 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [fqdnCopied, setFqdnCopied] = useState(false);
// Host-level Tailscale identity (same for every VP) — shown inline on the card when
// the user has marked this VP as "exposed over Tailscale". Cert handling does NOT
// depend on this toggle; the slicer trusts the bambuddy CA the user imports once.
const { data: tailscaleStatus } = useQuery({
queryKey: ['tailscale-status'],
queryFn: multiVirtualPrinterApi.getTailscaleStatus,
enabled: !localTailscaleDisabled,
staleTime: 60_000,
});
const tailscaleFqdn = tailscaleStatus?.available ? tailscaleStatus.fqdn : '';
const tailscaleIp = tailscaleStatus?.available ? tailscaleStatus.tailscale_ips?.[0] ?? '' : '';
const handleCopyFqdn = async (e: React.MouseEvent) => {
e.stopPropagation();
const fqdn = printer.status?.tailscale_fqdn;
const fqdn = tailscaleFqdn;
if (!fqdn) return;
let ok = false;
// Modern API — only works in secure contexts (HTTPS / localhost).
@@ -126,13 +138,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
setPendingAction(null);
},
onError: (error: Error) => {
// Specific: the backend rejected "enable Tailscale" because the binary isn't installed.
// Surface a clear reason instead of the raw error code.
if (error.message === 'tailscale_not_available') {
showToast(t('virtualPrinter.toast.tailscaleNotAvailable'), 'error');
} else {
showToast(error.message || t('virtualPrinter.toast.failedToUpdate'), 'error');
}
showToast(error.message || t('virtualPrinter.toast.failedToUpdate'), 'error');
setLocalEnabled(printer.enabled);
setLocalMode((printer.mode === 'queue' ? 'review' : printer.mode) as LocalMode);
setLocalTargetPrinterId(printer.target_printer_id);
@@ -301,12 +307,15 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
</button>
</div>
{/* Tailscale FQDN (when active) + serial — compact info row */}
{/* Tailscale identity (host-level) + serial — compact info row.
Shown only when this VP is marked Tailscale-exposed AND the daemon is up. */}
<div className="flex items-center gap-2 -mt-2">
{printer.status?.tailscale_fqdn && (
{tailscaleFqdn && (
<span className="flex items-center gap-1 text-green-400/70 min-w-0">
<ShieldCheck className="w-3.5 h-3.5 flex-shrink-0" />
<span className="font-mono text-xs truncate">{printer.status.tailscale_fqdn}</span>
<span className="font-mono text-xs truncate">
{tailscaleIp ? `${tailscaleIp} (${tailscaleFqdn})` : tailscaleFqdn}
</span>
<button
onClick={handleCopyFqdn}
className="p-0.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors flex-shrink-0"