From 4326fd70777d8c19a10345b03b03bd128245d5d2 Mon Sep 17 00:00:00 2001 From: "Pavel A. Oborin" Date: Sun, 19 Jul 2026 22:04:20 +0300 Subject: [PATCH] docs: add S7 Pro Ultra, document cert hostname requirement and post-handshake rejection Tested Roborock S7 Pro Ultra (model a62) with custom certs: - Actalis: working (RSA-2048, 90-day DV via ACME) - Let's Encrypt: rejected post-handshake (TLS handshake completes server-side, but vacuum sends TCP FIN without HTTP/MQTT request) Key finding documented in custom_cert_management.md and tested_vacuums.md: the vacuum uses the stack hostname (api-... value from network.stack_fqdn) for SNI and certificate hostname verification, not the value of the r field from the cfgwifi packet. A single-domain certificate on the stack hostname is therefore sufficient - no SAN for the stripped hostname required. Added troubleshooting section to onboarding.md covering the samples=0 / 'client closed before MQTT CONNECT' pattern that signals cert rejection. --- docs/custom_cert_management.md | 15 +++++++++++++++ docs/onboarding.md | 29 +++++++++++++++++++++++++++++ docs/tested_vacuums.md | 17 +++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/docs/custom_cert_management.md b/docs/custom_cert_management.md index c4db18a..17ea839 100644 --- a/docs/custom_cert_management.md +++ b/docs/custom_cert_management.md @@ -17,8 +17,23 @@ key_file = "/data/certs/privkey.pem" If you use the setup wizard and answer no to Cloudflare, it will write these values for you. You then need to place your certificate files at `data/certs/fullchain.pem` and `data/certs/privkey.pem` before starting the stack. +## Which hostname to cover + +The vacuum uses the **stack hostname** (the `api-...` value from `config.toml` → `network.stack_fqdn`) for both SNI in ClientHello and hostname verification against the certificate. It does **not** use the value of the `r` field from the cfgwifi packet (which is the stack hostname with `api-` stripped). + +In practice this means: + +- A **single-domain** certificate covering only `api-roborock.example.com` is sufficient. +- You do **not** need a SAN for the stripped hostname `roborock.example.com`, nor a wildcard. +- DNS resolution for the stripped hostname is also unnecessary — only `api-roborock.example.com` needs to resolve to the server's LAN IP. + +If you are migrating from a multi-domain cert and want to switch to a cheaper single-domain CA (e.g. Actalis free DV quota which issues one hostname per EAB), issue for the stack hostname only. + +See [Tested Vacuums](tested_vacuums.md) for a diagnosis flowchart when the vacuum rejects a cert. + ## Related Docs - [Installation](installation.md) - [Cloudflare setup](cloudflare_setup.md) - [Onboarding](onboarding.md) +- [Tested Vacuums](tested_vacuums.md) diff --git a/docs/onboarding.md b/docs/onboarding.md index f03d05e..759fac7 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -89,6 +89,35 @@ You still need to reset the vacuum's Wi-Fi manually. On many Roborock models tha Congrats! Once the script reports that the vacuum is connected to the local server, the onboarding flow is complete. +## Troubleshooting + +### `samples=0` and the vacuum never makes progress + +After sending the cfgwifi packet, the script polls the server for the vacuum's onboarding status (`samples`, `public_key`, `connected`). If `samples` stays at 0 across multiple cycles, the vacuum is making TLS connections but never sending an HTTP request that the server can use to recover the public key. The most common cause is a **certificate the vacuum rejects**. + +Check `mqtt_server.log` on the server for the post-handshake-close pattern: + +``` +TLS handshake ok from 192.168.x.y:NNNNN (TLSv1.3) +[conn N] backend connect 127.0.0.1:18830 from 192.168.x.y:NNNNN +[WARNING] [conn N] client closed before MQTT CONNECT +``` + +If you see `TLS handshake ok` immediately followed by `client closed before MQTT CONNECT` and `decompiled_http.jsonl` stays empty, the vacuum completed the TLS handshake server-side but rejected the cert during its own validation (CA chain not trusted, or hostname mismatch). See [Tested Vacuums](tested_vacuums.md) → "Diagnosing cert rejection" for the full flowchart. + +Common fixes: + +- The cert must cover the **stack hostname** (the `api-...` value from `config.toml`), not the value of the `r` field from cfgwifi. See [Custom certificate management](custom_cert_management.md) → "Which hostname to cover". +- The CA must be trusted by your vacuum model. Check [Tested Vacuums](tested_vacuums.md) for the compatibility matrix. Actalis is the safest fallback for older / pickier models. + +### `state=missing` for the selected vacuum + +The server has the vacuum in its inventory (cloud import done) but has not yet seen any onboarding traffic. Run another cycle: reset the vacuum's Wi-Fi, send the cfgwifi packet again, and wait. + +### `public_key=False` with `samples > 0` + +Recovery needs at least two query signature samples. If the count is stuck at 1, the vacuum is not making new attempts — give it a few minutes to retry, or run another cfgwifi cycle. + ## Web UI (start_onboarding_gui.py) If you would rather not use the terminal, there is a web UI version of the same flow. It runs a small local server on your machine and opens your browser automatically: diff --git a/docs/tested_vacuums.md b/docs/tested_vacuums.md index d12137f..88c31eb 100644 --- a/docs/tested_vacuums.md +++ b/docs/tested_vacuums.md @@ -46,6 +46,7 @@ Legend: |---|---:|---:|---:|---:|---:| | Roborock S5 Max | Not reported | ❌ | ✅ | ❌ | ❓ | | Roborock S7 | Not reported | ❓ | ✅ | ❌ | ✅ | +| Roborock S7 Pro Ultra (a62) | Not reported | ❓ | ✅ | ❌ | ❓ | | Roborock S7 MaxV | Not reported | ✅ | ❓ | ✅ | ❓ | | Roborock S8 | Not reported | ❓ | ❓ | ❓ | ❓ | | Roborock S8 MaxV Ultra | Not reported | ✅ | ❓ | ❓ | ❓ | @@ -57,6 +58,22 @@ Legend: | QRevo Master | Not reported | ❓ | ❓ | ❓ | ❓ | | QRevo Plus | Not reported | ✅ | ❓ | ❓ | ❓ | +## Diagnosing cert rejection + +When a vacuum rejects the TLS certificate, the failure pattern is **post-handshake close** rather than a TLS alert: + +- `mqtt_server.log` shows `TLS handshake ok from ` immediately followed by `client closed before MQTT CONNECT`. +- `decompiled_http.jsonl` stays empty (the vacuum never sends an HTTP request to `:555`). +- Packet capture shows the vacuum doing repeated short TLS connections (~30 ms) with TCP FIN and no `tls.alert` in either direction. + +This pattern indicates the vacuum completed the TLS handshake server-side but rejected the cert during its own validation (CA chain not trusted, or hostname mismatch). The vacuum then closes the TCP connection without sending any application data. + +### Hostname verification + +The vacuum uses the **stack hostname** (the `api-...` value from `config.toml` → `network.stack_fqdn`) for both SNI in ClientHello and hostname verification against the certificate. It does **not** use the value of the `r` field from the cfgwifi packet (which is the stack hostname with `api-` stripped). + +This means a single-domain certificate covering only the stack hostname (e.g. `api-roborock.example.com`) is sufficient. You do **not** need a SAN for the stripped hostname (`roborock.example.com`) or a wildcard certificate. DNS resolution for the stripped hostname is also unnecessary. + ## Unlisted Vacuums If your model is not listed, start with ZeroSSL unless it is an older model that is likely to need a different trusted chain. If onboarding fails after the DNS and server checks pass, try Actalis or a provided certificate and report the result.