Home automation field notes All concepts

protocols

Listening to a washing machine

One full cycle captured at the router: what the appliance says to its cloud, how it authenticates, and why the first local-redirect test failed for the oldest reason in networking.

The washer-dryer, an AEG L9WAN61BCE, talks to its maker's cloud and to nothing else. That much every owner knows. What we did not know, and could not find written anywhere, was how it talks: which server, which protocol, whether the conversation could be kept inside the flat. So on a Saturday with a full drum we listened to the whole cycle, three hours and twelve minutes of it, from the first packet after power-on to the last one after the door unlocked.

The machine that washes the clothes also talks to a server in another country, in a protocol it will not discuss with anyone else. We wanted to know exactly what it says. This required a router with a packet sniffer, a Saturday, a full drum, and a certain amount of humility at the end.

How to listen without a laptop in the laundry room

The router can mirror traffic. Its packet sniffer streams frames over UDP to any host, and a sixty-line Python receiver on the home server turns that stream into a standard capture file. Filter on the machine's hardware address and the firehose (four megabytes a second for the whole flat) becomes a trickle: 5,900 frames for an entire wash. The receiver, the router control and the flow classifier were written and rehearsed weeks earlier on synthetic traffic, because the machine only connects when somebody is home to start it.

Two things bit us anyway. Frames towards the machine arrived with a VLAN tag, so a parser that assumed plain Ethernet saw one direction only and confidently reported that DNS answers never came back (they did; the server answered on the same segment, past the router). And the router's sniffer stops streaming after about 100 kilobytes while still reporting running: true; a watchdog that restarts it whenever the capture file stops growing saved the day, at the cost of a few lost seconds per restart.

What the machine actually does

At power-on it asks our own DNS resolver (not a hard-coded public one, which was the first thing we checked and the best news of the day), resolves three names, and opens exactly two kinds of connection. Three short HTTPS sessions to a provisioning host on Microsoft Azure in the first minute, then nothing more from that host all day.

And one long MQTT-over-TLS session to the manufacturer's message broker, which lives on Amazon's AWS IoT Core, held open for the entire cycle: about five small records a minute from the machine, a hundred and thirty kilobytes up, twenty-eight down, a modest burst when the door unlocks. NTP once an hour. That is the whole vocabulary.

Timeline of the washer's traffic during one cycle: a burst
of HTTPS at power-on, then a steady MQTT trickle for three hours, a small bump at the end
One wash, ten-minute bins. The provisioning burst at the start, then a broker heartbeat of about seventeen kilobytes every ten minutes, and the end-of-cycle bump.

The wall, precisely measured

Reassembling the TLS handshakes by sequence number told us what the summary could not. The broker presents a certificate from AWS IoT Core and then sends a CertificateRequest; the machine answers with its own certificate and a signature. The certificate's common name is the appliance's identifier, the same string the cloud integration uses. So this is mutual TLS with a per-device key that lives inside the machine. Both channels do it. Nothing is in the clear, nothing is guessable, and there is no local port to speak of.

What follows from that is exact. Pointing the broker's hostname at a local server is trivial, since the machine trusts our resolver. Getting the machine to accept that server is the question, because it will check the server's certificate against Amazon's root, and ours cannot be signed by that.

The test is cheap: a self-signed certificate with the right name, a probe that requests the client certificate but never rejects it, and the router redirecting the machine's port 8883 to the probe. The expected result is a TLS alert, "unknown CA", and the end of the road.

The test that failed for a different reason

We ran it, and the machine's display said the Wi-Fi connection had failed. That was not the alert we were waiting for. The capture showed the machine associating normally, finishing its HTTPS provisioning, and then sending SYNs to five different broker addresses in turn, with no reply to any of them. The redirect rule had matched all twenty-one packets. The probe had accepted zero connections.

The reason is old and well documented, and we walked into it anyway: the machine and the home server sit on the same subnet. The router rewrote the destination and forwarded the packet to the server, but the server's reply went straight back to the machine over the same wire, never passing the router, never being un-rewritten.

The machine received an answer from an address it had never contacted and discarded it. A destination NAT between two hosts on one segment needs a source NAT as well, so that the reply is forced back through the box that did the rewriting. We knew this. We had simply never needed it at home.

Rolling back took one command and the machine reconnected to its real broker within seconds; the rest of the wash ran on the cloud as usual. The corrected version, with the masquerade and a probe that no longer closes after a single connection, was staged for the next load. We still expected the "unknown CA" alert. We were wrong about that in the most useful possible way.

The wall that was not there

A washer does not need a wash to phone home; the radio wakes on power-on, cycle or no cycle, so the next test cost nothing but a press of the button. The connection came back through the redirect and failed on something we had not expected: not "unknown CA" but "no shared cipher" — a snub before the introductions. The cause was mundane. This class of device offers only elliptic-curve ciphers, and we had turned up holding an RSA certificate, the wrong kind of key entirely. One certificate on the right curve later, and the very next retry — the machine knocks every seventeen seconds, a free test loop — went straight through.

And here is the punchline. The machine had gone to real trouble to prove who it was: a per-device certificate, a signature, mutual TLS, the works. Then it accepted our self-signed certificate for who we were without a second glance, and never once checked it against Amazon's root. It shows the stranger its passport and never asks to see one back. The wall everyone assumes behind mutual TLS to a cloud was only ever locked on one side, and not ours.

What it says once you answer

So we swapped the dumb probe for a hundred lines of MQTT — a broker that says yes to everything and writes down the rest — and the machine, with no username and no password to its name, told us everything in the clear. It subscribes to one command topic, the channel it waits on for orders, which is precisely the channel one would speak into to give them. It publishes its state to another. The payloads are a maker's binary format, hex in a thin JSON wrapper: little frames tagged with a module, a parameter and a value. Firmware, model, the entire menu of programs, the live state of the drum, parameter by parameter, arriving unencrypted at a broker we wrote in an afternoon.

Two things still stand between this and local control: the binary fields have to be decoded, best done by watching the numbers move during a real wash, and a command has to be composed rather than merely overheard, since the machine only ever sent its state and never an order. But the door has no lock; it simply never occurred to anyone to try the handle from inside the house.

Mutual TLS to a cloud is not a wall until you test it. A device that authenticates itself with a client certificate looks unassailable, so the reflex is to give up on local control. But two questions hide in that handshake: does the device prove itself, and does it check the server back? Here the first was locked and the second wide open, and the only way to learn that was to hold up a certificate and watch. Listen before you redirect; then redirect before you conclude.

A whole wash, decoded

Of the two things still standing between us and local control, the first fell quickly. The next real wash was three kilograms on the three-hour programme, and the binary gave up its grammar before the first rinse. A frame names a module, a field and a value; values are either plain numbers or nested lists of tag, length and value, the oldest trick in the protocol book and none the worse for it. Eighteen fields were named by the end of the evening, most of them from that one cycle: the remaining time in seconds, ticking down sixty a minute like a clock that has read the manual; the cycle phase; and the cycle counter, which went up by one at the end, as cycle counters should.

Then we turned the programme dial once round on an idle machine and got the complete dial: seventeen positions, each announcing its two-byte code. The last position is not a programme at all. It is Settings, which the cloud calls MACHINE_SETTINGS_HIDDEN_TEST and which we had been discarding as noise for about ten minutes. Six command names came along too (on, off, pause, resume, start, stop-and-reset), which is the whole vocabulary of orders one can give a drum.

The decisive trick is the one we are slightly embarrassed about. The cloud integration for Home Assistant (a community one, talking to the maker's API) publishes a diagnostics dump naming every parameter the machine has, with its current value. We had been staring at a large number in a counter field wondering what it might be; the cloud was, at the same moment, reporting the same number under the words "total washing time". Cross-referencing raw values against names resolved ten fields in one pass without a single guess. The machine had a dictionary the whole time. We had simply not asked the other half of the system for it.

The command that was ignored

With the addresses known, we composed our first order: the frame the machine itself publishes when the spin speed changes, same module, same container, same field, one byte different, and posted it to the command topic the machine subscribes to. Nothing. No reply, no change, no complaint. The machine remained in perfect health and perfect indifference.

The answer arrived days later, and not from the capture. The cloud integration keeps a history of the machine's remote-control flag, and that history shows the flag flipping to Enabled in the very second a programme starts running, with nobody near the panel, and back to a state that politely declines commands the second the cycle ends, on the one wash the cloud was allowed to see, which is a sample of one and we know it. We had sent our order to an idle machine. It had, by its own rules, no authority to take it.

The manual's button ritual for enabling remote control (press Delay, then Start) turns out to be for remote start of an idle machine, which is the one thing we do not want a script to be able to do. So the frame may well be correct; it was simply posted at the wrong hour. The test now needs a running drum, which will happen the way our best experiments happen: incidentally, and after lunch. On reflection this is the safest possible design for a machine that spins at 1,600 rpm. We would rather it ignored us than the other thing.

Twenty-one settings in forty-five minutes, no wash

The remaining unknowns were settings that never move on their own, so they had to be moved by hand. With the redirect up and the drum empty, one of us stood at the panel and changed exactly one thing at a time, and the other watched the log for the address that jumped. Brightness, chime, language, spin, steam, and sixteen more, most of them flipped back afterwards to catch the same address twice. Nobody pressed Start.

The individual fields are the least of it. Two containers matter for settings: one for the device and its clocks (brightness, language, remaining time, delay), one for the programme. Within the programme, one field holds what is selected and its neighbour holds what the programme allows: maximum temperature, maximum spin, which options exist. Switch from Cottons to Wool and the limits drop from 95 degrees and 1,600 rpm to 40 and 1,200, and the selection is clamped to fit, by the machine, before anyone has said a word.

Options are bits, and the machine publishes its own "supported" mask beside them, so there is nothing to guess. A choice is encoded in four different ways, depending on the mood of whoever wrote that part of the firmware. Dry-only sets every washing limit to "not applicable", the machine's way of agreeing that temperature is a strange thing to ask about when there is no water.

We kept a ledger of guesses made before the data arrived, because a model that cannot predict is only a description. Five hit, one was a hedge and does not count, three missed. The dryness levels we guessed as a tidy progression, one bit each; the machine used a different one, and its own mask, which we had in front of us and had not read, said so all along. The rule is short: never guess which values a field accepts. Read the mask.

Two small delights, both about time. An extra rinse costs nine minutes for the first and four for each further one, so the first presumably includes a drain and spin the others skip. And anti-crease adds thirty minutes of gentle tumbling after the cycle that the displayed duration never counts, so a "finished" notification built on the countdown would arrive half an hour early. The machine is not lying; it simply has a different opinion about when it is done.

Lesson: when a device publishes what it allows next to what it has, read the allowance first; the mask was right every time and our arithmetic only most times, which is the wrong number for a machine that heats water.

If you want to do this yourself

Everything needed to repeat this on the same family of machine is on one page, the recipe: the redirect (destination NAT plus the masquerade we forgot), the sink, the wire format, the field map, the commands and the safety rules. The same text is there as plain markdown, because recipes should be copyable. Two honest warnings go with it. While the machine is redirected the maker's app is blind, because there is one connection and it is talking to you; the wash itself runs regardless. And whether to make this permanent, with a local broker replacing the cloud for good, is a decision we have not yet made. The door has no lock. That does not oblige anyone to move in.