47290175aa945e213b90b86b11bd6e473809ba11
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9061d39684
|
Don't let a missing serial listing discard an 'all' discovery on Windows
Windows CI (run 415) ran the discovery tests: 48 passed and one failed, because port listing reads Linux sysfs and the test assumed it exists. That is a real limitation, not just a bad test - and in the example it meant `all` would listen and sweep and then abort on the serial step, throwing the results away. The serial step in `all` is now a printed note on failure; the standalone `serial` command still errors. The Linux-only test is gated to Linux and a counterpart asserts that off Linux the listing is an error, not a clean empty result. |
||
|
|
c6cf9bcb5e
|
Report discovered printers to the cloud, off by default
The gateway could already find printers; only a human reading a terminal could see the result. So adding a printer in the web console still began with walking to a machine to read its IP address off the screen. Now a background task scans on a timer and sends a `discovered_printers` frame. It cannot finish the job - the credential is on that screen and no probe can read it - but it removes the address, the serial and the guesswork about which vendor the thing is. OFF by default, which is the load-bearing part of this commit. A /24 sweep is 254 TLS handshakes and up to three HTTP requests per host, and nothing watching can tell that from a port scan; on a corporate network, being indistinguishable from a port scan is itself the problem. A gateway that did this by default would scan networks its operator never meant to point it at, and the first they would hear of it is from their own security team. So: - `discovery.enabled` is false unless set, per gateway, in that gateway's own config. The cloud cannot switch it on. A setting whose whole purpose is to bound what this process does to the local network has to be decided where that network is understood, which is not in a data centre. - `subnets` is explicit, never derived from this host's interfaces, so a second NIC, a VPN or a container bridge cannot widen the sweep. Anything wider than a /24 is still refused, now at config-import time rather than when the timer first fires. - A flag that is neither `true` nor `false` is an error, not a shrug. The two readings of a typo are not equally safe. - Naming a subnet without setting `enabled` scans nothing - the safe reading of an incomplete instruction - and `enabled` with nowhere to look is refused as the mistake it is. - The env var can switch it *off* over a file that enables it, which is the direction that has to work: a Proxy Box's config is signed in the cloud, so an operator who finds their gateway probing cannot just edit it. What a scheduled scan will not do, unlike the example CLI: send `M115`. Opening a serial port toggles DTR, which reboots an Arduino-based board and ruins a print in progress. The example can do it behind `--yes-reset-the-printer` because a human is standing there; a timer has nobody to ask, so it does not get the option and reads sysfs instead. Two things that took more care than expected: `Scan::looked()`. An empty report means "this gateway looked and its network has no unconfigured printers", so sending one after every probe failed would state something untrue about the farm. A partial failure is still evidence, though - withholding a good sweep because a container can never bind SSDP would suppress it forever - so the test is "did anything succeed", not "did everything". `fleet::configured()` reads the fleet file at parse level rather than through `load`, because the entry it most needs to recognise is the one `load` refuses: a printer discovery suggested, pasted in, with the access code not yet typed in. Re-offering that printer as a fresh find is noise at the one moment the operator is mid-way through adding it. Verified against the real LAN: a sweep found both printers by TLS certificate with correct serials and no credentials, and the frame it produced validates against the backend's EdgeClientMessageSchema. The `wire` subcommand on the example prints exactly that frame, and the capture is pinned as a backend fixture. SSDP remains untestable here - this container is a router hop from the printers and multicast does not cross one. |
||
|
|
a401773406
|
Discover Klipper, PrusaLink and serial printers too
Discovery found only Bambu printers, so a mixed farm still meant typing most of printers.toml by hand. It now covers the four vendors fleet.rs accepts, over both transports. Detection splits in two, and keeping the halves apart is what makes this honest. "Something at this address speaks X" needs no credential from any vendor, and is most of the value - it yields the address and the vendor, which is two of the three or four fields an entry needs. "What is it called, what firmware does it run" only has an answer where the vendor publishes one unauthenticated: Bambu always, Moonraker usually, PrusaLink never. So an HTTP 401 counts as a find rather than a failure. A password prompt from /api/version is proof that something is serving /api/version, which is the question being asked. It also makes the Moonraker probe correct without having to know how a given host configured [authorization], which is not knowable from outside. Klipper is `GET :7125/printer/info`, Moonraker's own identity endpoint, which returns hostname and software_version. PrusaLink is `GET :80/api/version`; its OpenAPI specification puts digestAuth on every operation including /api/v1/info, so there is no unauthenticated identity endpoint at all and a 401 is the best available answer - which is enough, since the only field still missing is the API key no probe could supply. This cannot distinguish PrusaLink from OctoPrint, and that is not fixable from outside: PrusaLink deliberately implements OctoPrint's API. A wrong guess an operator can see and correct beats skipping a real printer. Two guards against claiming things that are not printers, both now tested against what a real LAN actually answers. Three devices on the test segment answer /api/version with HTTP 200 and a body that is not JSON - a router, a device with a logon page, and one serving gzip - so a 200 must carry OctoPrint's guaranteed `api` field, and Moonraker's must carry the `result` wrapper every Moonraker reply has. And a bare 401 is not enough either: routers, NASes and cameras all password protect port 80, so a 401 must carry a *Digest* challenge, which PrusaLink's spec guarantees and the Basic-auth crowd does not send. A sweep of the real /24 still finds exactly the two Bambu printers and nothing else. Serial detection is passive by default, and that is a safety property rather than a limitation. Opening a serial port toggles DTR, which reboots an Arduino-based board - marlin_serial.rs documents the same thing from the other direction - so a firmware handshake against a machine that happens to be printing ruins the print, and discovery is exactly what someone runs while a farm is busy. So list_ports opens nothing: it reads USB descriptors out of sysfs, which is where udev reads them from, and gets the manufacturer, product and USB serial without touching the device. That is enough to render an entry, since a serial printer's block needs only a path and a baud rate. probe_m115 is the opt-in extra that learns the firmware and the exact machine type, and it refuses to run without --yes-reset-the-printer, the same shape as bambu_print_probe's --yes-start-a-real-print. sysfs rather than tokio-serial's libudev feature: a gateway appliance should not need libudev-dev at build time to list its own ports, and reading the four files udev would read is thirty lines and no dependency. The walk climbs to the USB device holding idVendor rather than counting parents, because a ttyACM link points at an interface and a ttyUSB link at a usb-serial port, each a different depth down. Only Prusa's own USB vendor id counts as evidence of a printer. Everything else in the table is a generic bridge chip - FTDI, CH340, CP210x - that a printer is likely to be behind but that a GPS or an Arduino project could equally be, so those are reported with "could be a printer or any other serial device" rather than asserted. Entries now carry exactly the fields each vendor's loader demands and no others, with a test that fails if to_toml_entry and fleet.rs drift apart, and a rendered entry for a vendor whose client is still a stub says so - otherwise an operator pastes in a Klipper printer and gets a gateway that connects and does nothing. Found while testing the M115 parser: a value ran to end-of-input instead of end-of-line, so the last field on a line swallowed what followed and UUID read as "cede2a2f-...\nok". Unverified, and untestable here: the Klipper and PrusaLink probes (no such hardware) and all serial detection (no serial device attached, and /sys/class/tty holds only virtual consoles). Every parser those depend on is unit tested against recorded protocol text, including the real non-printer responses above. |
||
|
|
e4344b208b
|
Find printers on the network instead of retyping their details
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`. |