Skip to main content
  1. Posts/

Eight Sleep Tried to Bypass My DNS Filtering with Google DNS

My firewall log lit up this morning. One device, over and over, a new block every few seconds: an Eight Sleep pod on my untrusted IoT network, throwing DNS and pings at Google’s 8.8.8.8 and getting rejected each time.

All Flows · eight-pod (Untrusted / IoT)Outgoing · Policy: Untrusted Reject DNS9:31:13Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:31:13Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:31:12Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:30:59Eight Sleep → 8.8.8.8 (ICMP echo)BLOCK9:30:49Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:30:43Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:30:09Eight Sleep → 8.8.8.8 (dns.google)BLOCK9:28:59Eight Sleep → 8.8.8.8 (dns.google)BLOCKA new block every few seconds · dozens per minute, around the clock
The pod hammering Google Public DNS, every entry rejected by the firewall. Device identifiers anonymized.

The device shipped with its own idea of which resolver to use, and my network was not it. This is the story of catching that, and of a better answer than a wall of red: stop blocking the traffic and start redirecting it, so the mattress keeps working while every lookup runs through my filtered, logged pipeline. The device never knows the difference.

The pod brought its own DNS #

Every device on my network gets a resolver by DHCP. That resolver is not optional; it is how I filter, log, and reason about what my network is doing. The Eight Sleep pod took the address, ignored it, and dialed 8.8.8.8 directly.

Two behaviors show up in the log. The first is plain DNS to Google Public DNS on UDP and TCP port 53. Hardcoding a public resolver is how a lot of consumer hardware sidesteps whatever the local network prefers, and it is precisely the move my filtering exists to stop. The second is an ICMP echo to the same address, one packet, 121 bytes: a captive-portal-style heartbeat the firmware uses to decide whether the internet is reachable at all.

My firewall already had an answer, and it is sharper than a port block. The policy named “Untrusted Reject DNS” never looks at port 53. It matches DNS by application signature: UniFi’s deep-packet inspection fingerprints the traffic itself, so one rule rejects five DNS applications leaving the untrusted zone for the internet, plain DNS, DNS over HTTPS, DNS over TLS, Multicast DNS, and the NSTX DNS tunnel. Port-hopping does not help, and encryption does not either, because DoH on 443 and DoT on 853 are recognized and rejected with the rest. The action is Reject, so each attempt fails fast and the pod knocks again seconds later, which is why the log fills the way it does. It works, and it still tells me nothing. A reject is a closed door with no peephole. I could see that the pod wanted Google, and that it wanted the internet badly, but not the one thing that matters: which names was it trying to resolve? That question is the whole game.

Why DNS is the control worth owning #

Nearly everything a connected device does starts with a name. Before the pod talks to its cloud, streams your sleep data, or checks for firmware, it asks a resolver to turn a hostname into an address. Own that step and you own the ledger: every destination, per device, in one place. Filter that step and you decide what resolves at all.

I built this deliberately. My gateway runs UniFi DNS Shield, and it forwards every query upstream over DNS over HTTPS to Cloudflare Zero Trust, where category filtering and full query logging live. I wrote up that setup in DNS over HTTPS with Ubiquiti and Cloudflare Zero Trust: encrypted lookups my ISP can no longer read, ad and malware domains blocked before they load, and a log of who asked for what. A device that hardcodes 8.8.8.8 walks straight past all of it.

There is a sharper reason to watch this particular device. Security researchers at Truffle Security found that Eight Sleep pods shipped with an SSH backdoor granting the company’s engineers remote root on every customer’s unit, plus hardcoded AWS credentials baked into the firmware. The AWS keys are the same lesson I keep coming back to: a device that sits in the customer’s hands cannot keep a secret, for exactly the reason an API key does not belong in a mobile app. Ship the credential inside the thing you hand to the world and you have already leaked it. A bed with a vendor backdoor and extractable cloud keys is not a device I extend any trust to. It is a device I quarantine and watch.

Segmentation first: the untrusted IoT VLAN #

Before any of the DNS work, the reason a backdoored mattress does not keep me up at night is where it lives. I run a dedicated IoT VLAN for every smart device in the house, wired and wireless alike, and I put that VLAN in an untrusted firewall zone. The default posture there is simple: these devices are guests I do not trust, and they get to reach the internet on my terms and nothing else.

Trusted LAN zoneLaptopsNASPhones · workstationsUntrusted · IoT VLANwired & wireless IoTeight-podSSH backdoor · hardcoded keyscameras · plugs · TVsTrusted → Untrusted: ALLOWcast, print, manageUntrusted → Trusted: DENYpod's outbound DNSUXG-Prozone firewall + DNSRejected by app signatureDPI match, any port · action Reject• DNS over HTTPS (DoH)• DNS over TLS• Multicast DNS• NSTX DNS Tunnelredirect plaintext DNS (DNAT)DNS ShieldDoH upstreamCloudflareZero Trust log + filterreply spoofed as 8.8.8.8 · pod sees GoogleContainment, by default.Trusted devices reach IoT when I choose; IoT never reaches trusted, so a vendorbackdoor cannot pivot. Encrypted and tunneled DNS are rejected by signature;plaintext DNS is redirected into my filtered, logged pipeline.
Zone segmentation with the DNS decision. Trusted reaches IoT by choice; IoT never reaches trusted. Encrypted and tunneled DNS are rejected by application signature, and plaintext DNS is redirected into the filtered, logged pipeline.

UniFi’s zone-based firewall makes this the natural shape rather than a pile of hand-written rules. Zones are groups of networks, and policy is written between zones: untrusted to trusted is denied, untrusted to internet is allowed but constrained, trusted to untrusted is permitted so I can still cast to a TV or print. The Truffle Security finding is a live test of that design. An attacker with root on the pod, or an Eight Sleep engineer using the backdoor, is sitting inside my untrusted zone. The zone firewall denies untrusted-to-LAN by default, so that foothold cannot reach a laptop, a NAS, or a phone. The lateral hop that turns one compromised gadget into a compromised house is not on the table.

Segmentation contains the blast radius. It does not, on its own, tell me what the contained device is trying to say to the outside world. That is the gap the DNS redirect closes.

Block, or redirect #

Here is the shift in thinking. Blocking 8.8.8.8 protects me and blinds me at the same time. The better move is to accept the pod’s packets aimed at Google and route them to my own resolver instead. The pod thinks it is talking to Google. It is talking to me. And because my resolver forwards over DoH to Cloudflare, its lookups land back in the same filtered, logged pipeline as everything else.

This is destination NAT, and the important question is whether it is transparent. It is, and the reason is worth understanding, because it decides how much machinery you need.

Before · blocked, invisibleeight-podUntrusted / IoTUDP/TCP :53 → 8.8.8.88.8.8.8Google Public DNSAfter · redirected, logged, still transparenteight-podUntrusted / IoT:53UXG-ProDNAT :53 → local+ conntrackDNS ShieldDoH upstreamCloudflareZero Trustreply rewritten to source 8.8.8.8 — the pod never noticesNo proxy rewrites the DNS payload. NAT + connection tracking do it all for plain port 53.DoH, DoT, and DNS tunneling stay rejected by app signature; the pod falls back to plaintext DNS.
Destination NAT plus connection tracking. The reply's source address is restored to 8.8.8.8 on the way back.

You do not need a transparent DNS proxy that rewrites UDP packets to impersonate Google. Plain DNS on port 53 carries no authentication, so there is nothing to forge. When the gateway rewrites the destination of the pod’s query from 8.8.8.8 to my resolver, Linux connection tracking remembers the swap and undoes it on the reply: the answer goes back to the pod with its source address restored to 8.8.8.8. From the pod’s point of view, Google answered. The same mechanism handles the ICMP heartbeat, so the pod’s connectivity check succeeds without a single packet leaving my network.

A userspace proxy that rewrites DNS payloads only earns its keep when the client validates who it is talking to. DNS over TLS on 853 and DNS over HTTPS on 443 do exactly that: they check a certificate you cannot forge, so there is no transparent way to intercept them. I do not need one. The reject rule already recognizes DoH, DoT, and the NSTX tunnel by signature and turns them away, which leaves the pod a single working option: plaintext DNS, the escape hatch that is also the peephole I redirect and read.

The build on a UniFi UXG-Pro #

My gateway is a UniFi UXG-Pro running the zone-based firewall, and the whole thing is three rules in the UI. No SSH, no scripts that a firmware update will erase.

  1. Redirect the plaintext DNS. Under Settings, Routing, NAT, create a rule of type Destination. Protocol TCP/UDP, source the untrusted IoT zone, destination port 53, translated address the resolver on my gateway, translated port 53. NAT matches ports, not application signatures, so this rule owns plaintext DNS; the encrypted and tunnel apps stay with the reject rule, since TLS cannot be transparently redirected anyway. I leave the destination address empty on purpose, so the rule catches any external resolver: if the pod gives up on 8.8.8.8 and reaches for 1.1.1.1, it lands in the same net. Exclude the resolver’s own address so it never redirects to itself.

  2. Let the redirected query through. This is the step that trips people up after the zone-firewall migration. DNAT happens in pre-routing, before the firewall picks a zone, so once the destination is rewritten to the gateway, the packet is no longer headed to the internet zone; it is headed to the gateway’s local zone. My existing “Untrusted Reject DNS” rule targets untrusted-to-internet and no longer matches, so I add an explicit allow for untrusted-to-local on UDP and TCP 53, ordered above any broad local deny. Skip this and the redirect silently drops every query.

  3. Answer the heartbeat locally. A second destination-NAT rule sends ICMP echo aimed at public DNS to the gateway, with a matching untrusted-to-local allow for ICMP. The gateway replies, connection tracking restores the source, and the pod decides the internet is up. It is, on my terms.

Keep the application reject rule as the backstop. What the redirect cannot touch, encrypted DNS and the tunnel apps, still meets a closed door by signature. Redirect the plaintext you can read; reject the rest.

What the pod was asking for #

The moment the redirect went live, the flow log stopped bleeding red. The pod’s entries flipped from blocked to allowed, the mattress kept working, and the lookups it had been hiding started showing up where I could read them: in UniFi’s per-client DNS activity, attributed to the pod, and in the Cloudflare Zero Trust DNS logs downstream. Every hostname the firmware reaches for, its telemetry endpoints, its update servers, its analytics, now resolves through a path I filter and record. What used to be a wall of denied Google traffic is a clean list of names.

That is the payoff of controlling DNS. Segmentation kept the backdoored device off my real machines. The redirect turned its last act of independence into a transparent record. The pod believes it slipped past me on 8.8.8.8, and it will keep believing that.

On my network, DNS is mine. A device does not get to opt out.

George Tsiokos
Author
George Tsiokos

Leave a comment

Preview

Comments are reviewed before publishing.