Setting up a farm meant reading five fields off each printer's screen into
printers.toml, and the serial is the one that punishes a typo: 15 unspaced
alphanumerics that address Bambu's MQTT topics, so a wrong character
produces a gateway that connects to the broker, subscribes to a topic no
printer publishes, and reports nothing.
Two mechanisms, because they fail in different places:
SSDP (`listen_ssdp`) hears the announcements Bambu printers send to
239.255.255.250 unsolicited, on 2021 and 1990. It learns the most - the
model and the operator's own name for the printer come only from here - but
multicast does not cross a router, so it finds nothing from a gateway on a
different VLAN than its printers, which is an ordinary way to run a
workshop.
The TLS certificate (`identify`) is the dependable one. Every Bambu printer
puts its serial in the subject CN of the certificate it presents on 8883,
readable with no credentials, no MQTT session and across subnets. Verified
against both real printers: a P1P gives CN = 01S09B441900143 (issuer
BBL CA, TLS 1.2) and an H2C CN = 31B8BP5A1800585 (BBL Device CA O1C2-V2,
TLS 1.3), each an exact match for what printers.toml already said. A blind
sweep of their /24 found exactly those two and nothing else, in 24s.
Reading an unverifiable certificate is deliberate and worth being plain
about: it is being read as a name tag, not trusted. There is nothing to
verify it against - the serial is what is being learned, and a P1P does not
chain to the bundled Bambu CA at all. Trust is still decided by
BambuTls::resolve and the trust-on-first-connect pinning, where credentials
actually get sent. `to_string` and not the deprecated `as_utf8`, which
truncates at an interior NUL; `valid_serial` would reject the untruncated
form anyway, which is why the shape is checked rather than the name
trusted. `entries_by_nid` on the subject specifically, because an issuer's
CN sits earlier in the DER and anything scanning for the first CN reports
the certificate authority as the printer.
Three decisions that are not incidental:
It prints a [[printer]] block and does not write printers.toml. Discovery
cannot learn an access code - that code is the only thing between anyone on
the network and control of the machine - so every block is incomplete by
construction, and fleet::load rejects an empty access_code. Writing a
half-finished entry in would stop the gateway starting.
`vendor = "bambu"`, never a guessed v1/v2. That value auto-detects on
connect, and a firmware update can move a physical printer between dialects,
so a guess in a file goes stale silently. There is a tempting correlation in
the two printers to hand - the v1 machine negotiated TLS 1.2 and the v2 one
TLS 1.3 - but two samples is not evidence.
A sweep refuses anything larger than a /24. 256 addresses is every printer
anyone has on one segment, and a mistyped /8 would be sixteen million
connection attempts across somebody's network.
The SSDP path has not received a real announcement: the only host available
to test from is routed to the printers rather than on their segment, so
their multicast cannot arrive. Hence the parser is tolerant - a header
spelled differently costs one field, not the printer - and `raw` mode dumps
every datagram, so the first run on a real segment is a test that shows its
working rather than only its conclusion.
openssl is named directly for the CN read. It is already in the tree -
native-tls is openssl on Linux - so this adds nothing to the build, and
Apache-2.0 passes `cargo deny check licenses`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>