v0.5.0’s diagnostic gate produced exactly the kind of byte I’d been hoping for. The pre-flight read of mirrorExternalState came back .error (0x9), the follow-on read of mirrorError came back .noInternet (0x3), and for the first time in the entire project’s history I had a specific, named, actionable failure cause. Then I spent the next two hours finding out the answer required a layer of access I didn’t have.
This post is about what I learned in those two hours, why opening the case is now the path, and what I’m picking up tomorrow.
The verdict the DeviceState gate produced
The relevant lines from the log:
17:42:23.829 INFO HTTP 200 body={"status":"ok","version":"0.5.0"}17:42:23.878 INFO wifiConnectionStatus = .connected (0x1 = 1 dec)…18:30:25.065 INFO Pre-flight mirrorExternalState = .error (0x9 = 9 dec)18:30:25.127 ERR DeviceState = .error. mirrorError = .noInternet (0x3).Two bytes, three sentences of meaning:
- WiFi association is fine.
wifiConnectionStatus = .connected. The mirror’s radio is talking to the access point. - The mirror itself thinks it has no internet.
mirrorError = .noInternet. Whatever the firmware’s check is, it’s failing. - The mirror refuses all scan actions until that flag clears. This is why every
alignScalefor the past two weeks has been rejected at the device-state layer with no accompanyingscanError.
The fix is: make the mirror’s internet check pass. Which means: figure out what the check is.
What the network actually shows
I pulled out Firewalla, a Firewalla Gold sits on my LAN, and looked at the mirror’s full outbound profile. With nothing else running on the device, just a fresh boot and an idle scan-init UI, the mirror reaches out to four services in the first sixty seconds:
12:45 PM tunnel.us.ngrok.com (4 separate flows in 60 seconds, retry pattern)12:45 PM dns.google.com (DoH, DNS over HTTPS)12:46 PM s3.amazonaws.com (single hit, small)12:46 PM api.logdna.com (telemetry)What’s not in the list is the surprise: api.nakedfit.net. The domain I’d been planning the entire intercept strategy around, the one I’d written into Ref/SCAN_WORKFLOW.md as the upload-coordination endpoint, the mirror does not contact on its own. That domain is touched by the Android app during scan initiation (the token fetch step), not by the mirror’s firmware at idle.
The mirror’s internet check is something else, and the retry pattern on tunnel.us.ngrok.com is the clue. Four separate flows to the same host in sixty seconds is failure-and-retry behavior. A successful tunnel registration would establish one persistent connection that stays open. Four short-lived flows means: TLS handshake, application-layer rejection, reconnect.
Naked Labs almost certainly used ngrok to maintain inbound tunnels into the fleet for remote support, a common pattern in industrial IoT. The tunnel registration requires an auth token tied to a specific ngrok account. Their account is now expired or deauthorized. The mirror’s firmware keeps trying to register, the registration keeps failing, and the firmware concludes: no internet.
It’s the right verdict from the firmware’s perspective (“I can’t reach the place I’m supposed to reach”). It just happens to be wrong about what kind of internet failure is happening.
sequenceDiagram
participant Mirror
participant Ngrok as tunnel.us.ngrok.com
loop every ~15s, 4x in 60s
Mirror->>Ngrok: TLS handshake + tunnel registration
Ngrok-->>Mirror: rejected (account expired/deauthorized)
end
Note over Mirror: concludes "no internet"
Note over Mirror: mirrorError = .noInternet, all scan actions blockedWhy the obvious intercept doesn’t work
My plan from earlier that week was: override DNS for api.nakedfit.net at the LAN layer, run a local HTTPS server, capture and replay the request. Firewalla supports per-device DNS overrides. Simple.
Three problems with that plan, in increasing severity:
api.nakedfit.netis the wrong target. The mirror doesn’t contact it. Overriding it does nothing.- DNS overrides on the LAN don’t apply. The mirror uses DoH to
dns.google.com, encrypted DNS queries over HTTPS that bypass the LAN resolver entirely. To force the mirror onto LAN DNS, I’d have to blockdns.google.comfirst at the firewall. - The real target is ngrok, and ngrok’s tunnel protocol is proprietary, binary, and certificate-pinned. I can’t just stand up a local server pretending to be
tunnel.us.ngrok.com, the mirror’s ngrok client will reject my cert during the TLS handshake. Reproducing the protocol meaningfully is a 1-2 week project at minimum, and I’d still have to decide what shape of response satisfies the firmware’s “is the tunnel registered” check.
None of these are solvable with what I have on the outside of the mirror.
What’s behind the SSH prompt
The mirror has SSH open on port 22. OpenSSH 7.6, both publickey and password accepted. I checked, methodically, in this order:
- Existing SSH keys on my Mac (any prior trust). None matched.
- Eight standard usernames (root, naked, admin, support, ubuntu, pi, nakedlabs, squirrel) via pubkey-only, all rejected.
- Twenty common default logins (
rootwithroot,rootwith an empty password,nakedwithnaked,supportwithsupport,piwithraspberry, and others in that spirit), all rejected. - Thirteen project-specific guesses (
rootwithsquirrelfit,rootwithnakedfit,rootwith the device’s own identifier,servicewithnakedfit, and similar), all rejected.
Thirty-three attempts. Nothing. Naked Labs did basic security hygiene on their fleet: no default logins, no obvious patterns, no shipped-in-APK service accounts (I grepped the decompile for SSH/RSA/key literals; there are none).
I also asked the same question of public sources. There’s a Reddit thread on r/3DScanning from 2 years ago where a user named aDutchofMuch posted that they had successfully logged in to the Poky Yocto OS on the Naked scanner and had been looking through run files. They didn’t say how. Bob Free, Naked Labs’ founding VP of Engineering, employee #1, who personally wrote the cloud/web/IoT/mobile code, replied saying he can’t help with reverse-engineering until the IP owner authorizes. The IP owner is Prism Labs (Naked rebranded, pivoted to iPhone-camera scanning, indifferent to the legacy hardware). The thread is dormant.
So I got the most informative possible result from the SSH attempts: the mirror’s root login is non-trivial, and the community has the answer but it’s locked behind people who can’t share it.
That ends the non-invasive path. I’m not finding the login by guessing; I’m not finding it on the internet; I’m not getting it from Bob Free.
Two corrections to my model
Two beliefs I’d been operating on got revised today:
The mirror runs Poky Yocto Linux, not Android. aDutchofMuch’s comment said “the Poky Yocto OS.” The OpenSSH 7.6 + dnsmasq 2.78 fingerprint matches Yocto Pyro (April 2017) or Rocko (October 2017), both shipped exactly those versions. The mirror is a Linux box, not an Android one. That changes the toolbox once I’m in: standard systemd, standard ssh, standard /etc/hosts. No adb, no Dalvik VM, no Android-specific quirks.
The compute board is an H110-SF100. Visible in whoamarcos’s teardown photos as a labeled mini-ITX-class industrial board: Intel H110 chipset, LGA 1151 socket (so a 6th/7th gen Celeron CPU), DDR4 SO-DIMM RAM, M.2 NVMe/SATA SSD storage. Commodity x86 hardware. Boots with a standard UEFI/BIOS, almost certainly GRUB-based.
Both corrections matter for the path forward.
And one architectural finding that does not fix itself
The DisplayPort cable visible in the teardown photos is not driving a consumer-accessible display. The mirror has no screen on its outer surface, just the LED ring. The DP cable is going to some internal service/dev port I haven’t characterized. Which means the boot-interrupt plus GRUB-edit path is blocked: I’d be typing kernel-cmdline edits without being able to see the console. Possible, maybe 20-30% reliable, but not the path of first resort.
What’s left
The remaining viable path is also the most surgical: pull the M.2 SSD, mount it externally on my Mac (via a UTM Linux VM, since macOS can’t write ext4 natively), add my public key to /root/.ssh/authorized_keys, put it back, SSH in.
I’ve documented the full procedure in Ref/PHYSICAL_ACCESS.md: teardown steps from whoamarcos’s notes, what to look for inside, how to identify the SSD form factor, the M.2-to-USB adapter needed (combo NVMe+SATA, since I don’t know which interface this one uses), and the Mac-side mount-and-edit procedure. The procedure is reversible until the moment I write to the filesystem, and even then the SSD just goes back in and the device boots normally, no irreversible commit.
Once I have root, the project unblocks substantially. I can:
- Find what code path sets
MirrorError.NO_INTERNET(some systemd unit, probably tied to ngrok-client status), disable it, or make it always succeed. - Locate the actual scan-upload script, probably a Python or shell wrapper that PUTs to a hardcoded S3 bucket. Read it. Understand its request shape. Either rewrite its target locally or redirect S3 to my LAN coordinator with my own cert installed in the mirror’s trust store.
- Replace the inferred-from-Android-client model in
Ref/SCAN_WORKFLOW.mdwith observed reality.
What I learned
A few takeaways for the series:
- The right answer is sometimes also the most involved answer. A week of careful protocol audit, instrumentation, version stamping, and streaming pipelines all paid off, but the last 20% of the problem still required physical access. Refusing to open the device would have stranded the project at a wall built out of someone else’s encrypted business logic.
- The byte-level signal that closes a hypothesis is worth weeks of speculation.
mirrorError = .noInternet (0x3)is two bytes. Before v0.5.0 landed those bytes, I’d been building plan scenarios around the wrong endpoint. After they landed, the path forward shifted within an hour. The streaming-pipeline-plus-precondition-gate was specifically designed to make this kind of byte legible, and it earned its keep. - Public sources aren’t always alive. Sometimes the community has the answer, has posted it, and has moved on, and the trail goes cold not because the answer is wrong but because the people who knew it have left the room. Plan around dormant communities the way you plan around defunct vendors.
- Naked Labs did basic security hygiene. No shipped-in logins, no default passwords. That’s inconvenient from where I’m sitting, but it speaks reasonably well of how the original engineering team built the fleet. The thing that broke this product was the cloud shutdown, not the device’s posture.
What’s next
Tomorrow:
- Open the mirror per
whoamarcos’s teardown notes. - Pull the M.2 SSD.
- Add my public key, reassemble.
- Get a shell.
- Find the ngrok-check code and disable it.
- Watch DeviceState transition from
.errorto.idlefor the first time in this project’s history.
If everything goes right, that unblocks alignScale. Then I find out whatever’s behind the next door.
Follow along
Procedure 13-AThis post is a dead-end-into-a-plan post: the SSH path closes, the physical-access path opens. If you’re facing the same wall on your own mirror:
- 1Confirm the device-state error before chasing a network theory. Read
mirrorExternalStateandmirrorErrorfirst. Knowing it says.noInternettells you the firmware itself believes it’s disconnected, which is a very different problem from “my BLE write isn’t working.” - 2Profile the mirror’s actual outbound traffic with a device-aware firewall or packet capture before assuming which domain matters. My assumption (
api.nakedfit.net) was wrong; the real dependency (an ngrok tunnel) wasn’t on my radar until I looked. - 3Rule out DNS-layer tricks early if the device uses DoH. A LAN DNS override is useless against a client that queries
dns.google.comdirectly over HTTPS. - 4Recognize when a vendor’s proprietary, cert-pinned protocol is the actual blocker, and stop trying to spoof it from outside. At that point, the economical move is local access to the device, not more cleverness on the network.
- 5Treat physical access as the next tool, not a last resort to feel bad about. It’s your hardware. Pulling a drive to add your own key is no more invasive than opening a laptop to replace a battery.
Agent prompts Procedure 13-A as prompts for a coding agent · 4 prompts
Paste into Claude Code or a similar agent, one at a time, from the root of your iOS project. Each prompt maps to the step with the same number above.