Abusing Apple’s Find My network

Some months ago Apple was running a commercial on Spanish TV about the iPhone’s privacy, mocking users with Android devices.

Probably, iPhone users do not know that it is really easy to physically track them.

Apple’s “Find My” network was launched in 2019 and the AirTag in 2021. The AirTag emits BLE beacons with a public key that, when received by another Apple device, are sent along with the location of the device that received them to Apple servers, encrypted with the AirTag public key. The information on the Apple servers needs to be decrypted with the AirTag’s private key.

However, when the AirTag location is updated, the owner of the AirTag also knows that there is an Apple device nearby.

In theory it’s required to have an iPhone/iPad/iPod to activate an AirTag, but it’s very easy to build an AirTag clone with ESP32.

The Macless Haystack project, based on the original OpenHaystack, provides

  • The python utility to generate AirTag key pairs
  • The firmware to flash an ESP32-WROOM-32 and convert it to an AirTag clone
  • An Android app to check the location of your fake AirTags
  • Two Docker containers needed to retrieve the info from the Apple servers, they need to be accesible by the Android app

And, of course, I built my own one:

These ESP clones are much bigger than AirTags but they are ok to track cars, suitcases or bags. There is a PR to the Macless Haystack repo alowing to use an ESP32-C3 Supermini, that would make a smaller device.

Google launched the “Find My Device” network in April 2024, and the tags supporting it (i.e. the Chippolo Point or the Pebblebee) are slowly reaching the market. But the default security option only shares the location information if there are other Android devices nearby. That is much better for the privacy of Android device owners, but much worse for the owners of the tags.

So, if you own an Android device, it is still better to use AirTag clones, abusing the lack of privacy of the Apple devices.

Back to OpenWrt

Some years ago, my network provider (O2 – Spain) installed a router for me (Mitrastar HGU GPT-2541GNAC) with much better specs than my old OpenWrt router (TP-Link TL-WDR4300). So, I ditched OpenWrt and started using the company’s router.

But the Mitrastar needed to be factory reset every 6 months because it had some problems with the DHCP in my network. Recently, I offloaded the DHCP to an OrangePi 3B, but now my home network was relying too much on the availability of this device. Also, the Mitrastar is starting to show its age without features like WiFi 6…

A couple of weeks ago I found on Aliexpress a new Xiaomi AX3000T router. It has an amazing set of specs:

  • 2 ARMv8 cores @ 1.3GHz (MediaTek MT7981B)
  • 128 MB ROM
  • 256 MB RAM
  • WiFi 6 (AX) in the 2.4 GHz and 5 GHz bands

And it’s compatible with the lastest snapshot of OpenWrt:

I was able to get it during the AliExpress ChoiceDay (this happens the first days of each month) for 26 EUR (including a 4 EUR coupon).

When it arrived, I found it a bit smaller than what I expected, and I liked its minimalist look. It has only one button for WPS (+ the reset pinhole), and one LED in the front panel (i.e. it does not have LEDs on the ethernet ports).

Everything was in Chinese, but it’s easy to read it using the Google Translate camera, only for the steps necessary to set up OpenWrt. I did a simple installation without the U-Boot boot loader.

So, I put again an OpenWrt router back in my home network moving the DHCP and WireGuard services to the router. I had forgotten the beauty and simplicity of OpenWrt.

I also tested adblock-lean and it works quite well on the router with big lists like It’s able to manage the 660K domains of both oisd big and osid nsfw lists. But at the moment I’ll continue using the Pi-hole on the OrangePi. I still need the OrangePi to run Home Assistant and the NAS.

CO2 sensor with LCD and ESP32-C3 Supermini

I wanted to add an LCD screen to my CO2 sensor, so I bought a white LCD 1602 with an I2C controller. The I2C controller needs to be soldered to the LCD, but my basic soldering skills were sufficient for the task.

I also wanted to place it in a box, so I purchased this plastic enclosure but I cannot recommend it. It required a lot of glue from a glue gun to install the LCD and the ESP. I also had to use the soldering iron to create space for the ESP and a hole for the USB connector. I installed the plastic buttons but they are only decorative.

I made room for the sensors inside the box, but finally left them outside because they are more precise that way.

The ESP32-WROOM-32 was too large for the enclosure, so I used a ESP32-C3 Supermini with an expansion board. This is a really amazing board with a 32-bit RISC-V 160MHz microcontroller, WiFi, Bluetooh, I2C and UART. It’s not as powerful as the ESP32-WROOM-32 with a dual core 32-bit Xtensa 240Mhz, but it’s more than capable to control the sensors and the LCD.

This is the ESPHome configuration, which includes a switch to control the LCD backlight and a clock synchronizing the time with Home Assistant:

  name: co2sensor
  friendly_name: CO2 Sensor

  board: esp32-c3-devkitm-1
    type: arduino

  level: ERROR

  password: ""

  platform: esphome
  password: ""

  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 90s
    ssid: ${friendly_name} Hotspot
    password: !secret wifi_password


  port: 80


  - platform: homeassistant
    id: homeassistant_time
    timezone: "Europe/Madrid"

  rx_pin: GPIO20
  tx_pin: GPIO21
  baud_rate: 9600

  sda: GPIO8
  scl: GPIO9

  - platform: uptime
    name: Uptime
      - lambda: return x / 60.0;
    unit_of_measurement: minutes

  - platform: wifi_signal
    name: "WiFi signal sensor"
    update_interval: 60s

  - platform: dht
    model: AM2302
    pin: GPIO10
      id: "dht22_temperature"
      name: "DHT22 Temperature"
      id: "dht22_humidity"
      name: "DHT22 Humidity"
    update_interval: 50s

  - platform: mhz19
      id: "mhz19_co2"
      name: "MH-Z19 CO2"
      id: "mhz19_temperature"
      name: "MH-Z19 Temperature"
    update_interval: 60s
    automatic_baseline_calibration: false

  - platform: lcd_pcf8574
    dimensions: 16x2
    address: 0x27
    update_interval: 1s
    lambda: |-
        auto time =  id(homeassistant_time).now();
        it.printf(11, 0, "%02d:%02d", time.hour, time.minute);
        it.printf(11, 1, "%02d/%02d", time.day_of_month, time.month);
        auto co2 = id(mhz19_co2).state;
        if (!isnan(co2)) {
          it.printf(0, 0, "%.0fppm", co2);
        it.printf(0, 1, "%.1fC", id(dht22_temperature).state);
        it.printf(6, 1, "%.0f%%", id(dht22_humidity).state);

  - platform: gpio
    pin: GPIO7
    name: "LED Backlight"
    id: led_backlight
    restore_mode: ALWAYS_OFF

And the full BOM:

MH-Z19B CO2 sensor

To keep a healthy environment at home or at the workplace, one of the important things to control is the carbon dioxide (CO2) level.

It’s measured in ppm (parts per million), indicating how many parts of CO2 there are in one million parts of air. As a reference:

  • Less than 1000 ppm are healthy levels
  • Between 1000 ppm and 2000 ppm, we need to reduce the CO2 levels
  • Levels greater than 2000 ppm are associated with headaches, sleepiness, poor concentration, loss of attention…

To reduce the CO2 level, we need to ventilate the room. It can be manually done (opening the windows) or it can be automated with a ventilation system.

To measure it we need a proper CO2 sensor, and one of the most reliables sensors is the MH-Z19B. It is not cheap for the Aliexpress standards (it costs around 20 EUR), but other cheap sensors announced as “air quality” sensors or “eCO2” sensors are not really measuring the CO2 level (i.e. the MQ135).

I bought this MH-Z19B from Aliexpress and hooked it to an ESP32-WROOM-32 board. This board is going to be also purposed as a temperature and humidity sensor, so I also attached a DHT22 sensor. I bought this DHT22 sensor but it is not an original one, and the measures do not seem very correct, so I ordered again an original AM2302 (=DHT22). The MH-Z19B includes a temperature sensor, but it’s mainly used for calibration and it lacks precision, as it does not report decimals. I’m also using an expansion board to simplify the connections.

The ESP32-WROOM-32 on the expansion board connected to the MH-Z19B (gold) and to the DHT22 (red)
  • The jumper in the expansion board needs to be set to 5V (because both of these sensors need 5V).
  • Connected VCC and GND of both sensors to the expansion board
  • Connected the RX and TX of the MH-Z19B to the TX and RX (GPIO1 and GPIO3) of the ESP
  • Connected GPIO16 to the DAT of the DHT22

Finally, I installed ESPHome to the board with this configuration:

devicename: co2sensor
friendly_name: CO2 sensor

name: ${devicename}
friendly_name: ${friendly_name}
platform: ESP32
board: nodemcu-32s


password: ""

password: ""

ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 90s
ssid: ${friendly_name} Hotspot
password: !secret wifi_password


port: 80


- platform: homeassistant
id: homeassistant_time

rx_pin: GPIO3
tx_pin: GPIO1
baud_rate: 9600

- platform: uptime
name: Uptime
- lambda: return x / 60.0;
unit_of_measurement: minutes

- platform: wifi_signal
name: "WiFi signal sensor"
update_interval: 60s

- platform: dht
model: DHT22
pin: GPIO16
id: "dht22_temperature"
name: "DHT22 Temperature"
id: "dht22_humidity"
name: "DHT22 Humidity"
update_interval: 60s

- platform: mhz19
id: "mhz19_co2"
name: "MH-Z19 CO2"
id: "mhz19_temperature"
name: "MH-Z19 Temperature"
update_interval: 60s
automatic_baseline_calibration: false

And It’s working nicely, this is the ESPHome web interface:

Keeping the CO2 levels under control is helping me create a healthier workplace environment and improve performance.

Improving WiFi reception with an ESP32-WROOM-32U

I’m using a ESP32 with ESPHome connected to my heating system for climate control, as expained in a previous post.

The heating system is in a different builng than the router and I was experiencing some WiFi coverage issues (the WiFi signal needs to cross two metallic window blinds…).

To diagnose the WiFi coverage is very useful the wifi_signal sensor in ESPHome:

  - platform: wifi_signal
    name: Wifi Signal
    update_interval: 60s

It was showing a WiFi signal of -95 dBm in the board: This is very low, and it was experiencing some disconnections.

Usually the ESP32 boards have an antenna integrated in the board, but the ESP32-WROOM-32U has an IPEX connector for an external antenna:

So, I spent less than 10 EUR in Aliexpress buying (affiliate links):

And replaced the previous ESP32-WROOM-32 module with an ESP32-WROOM-32U, installing the external antenna. This is how it looks now:

ESP32-WROOM-32U mounted on an expasion board connected to the 4 relay module

The WiFi signal shown in ESPHome increased from -95 dBm to –75 dBm and it’s no longer experiencing any interruptions.


I always used OpenVPN in my servers, but now WireGuard is a better option:

  • It’s simpler
  • It’s more efficient
  • It’s faster
  • It uses modern cryptography algorithms

I’m using it to remotely access private services in my home server. I setup a star topology, where all the VPN clients connect to the home server and they can only see the server.

So I need a dynamic DNS and an open port in the router, I already have them for Home Assistant.

Eloy Coto recommended Tailscale, it is an amazing mesh VPN based in WireGuard. It’s much simpler to set up, and you do not need to open public ports, but it’s commercial and a bit overkill for my needs.

Generating the WireGuard configurations

The most tedious part of WireGuard is to generate the configurations, but there are some nice tools to ease that, like:

The tool generates the configuration for the server and for the requested number of clients. It does everything in the frontend, so it is not leaking the VPN keys.

As I’m only acessing the server, I have removed the IP forwarding options in the Post-Up and Post-Down rules.

Installing and configuring the WireGuard server

WireGuard is in the official Ubuntu repos, so to install it in the server it’s enough to do:

sudo apt install wireguard

Then I needed to put the config in the /etc/wireguard/wg0.conf file and do:

sudo systemctl enable wg-quick@wg0.service
sudo systemctl start wg-quick@wg0.service

Installing and configuring the clients

WireGuard has clients for almost any OS:

To setup the client in the phones, the WireGuard Config web tool generates QR codes. In other devices you’ll need to create a file with it or paste the config contents.

Using Pi-hole from the VPN clients

To use the Pi-hole hosted in the same VPN server from the VPN clients, you can specify a DNS property in the client config, i.e. if the server is and the client is

PrivateKey = <client-private-key>
Address =

PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = <my-home-server>:51820
AllowedIPs =, ::/0
PersistentKeepalive = 25

Every time that you connect the VPN, the DNS server in the client changes to and it is reverted to the previous DNS server when the VPN is disconnected.

Additionally, Pi-hole needs to be listening in the wg0 interface, I explained how to make Pi-hole listen on multiple interfaces in the Pi-hole post.

The LLM revolution

I took this photo of three Llamas in Machu Picchu some years ago…

ChatGPT was launched in November 2022, and it changed our world as we knew it. Since then, Large Language Models (LLMs) have integrated into our daily workflows enhancing our productivity and the quality of our work.

Another interesting milestone happened in February 2023, when Meta released the Llama LLM under a noncommercial license:

This sparked the enthusiasm among numerous developers dedicated to advancing LLMs, leading to a increase in collaborative efforts and innovation within the field. A good example is the Hugging Face Model Hub where new models are constantly published:

Developers started creating improved models and optimizing performance for local execution of LLMs on consumer-grade hardware.

Llama.cpp is a port of Llama to C++, started in March 2023 with a strong emphasis on performance and portability. It includes a web server and an API:

Mistral 7B was released in October 2023, achieving better performance than larger Llama models and demonstrating the effectiveness of LLMs in compressing knowledge.:

And now it’s easier than ever to locally execute LLMs, especially since November 2023, with the Llamafile project that packs Llama.cpp and a full LLM into a multi-OS single executable file:

The llama.cpp web interface running Mistral 7B Instruct in local via a llamafile

It’s even possible to run LLMs in a Raspberry Pi 4, like the TinyLlama-1.1B used from a llamafile in this project:

And about using LLMs for code generation (Github’s Copilot has been available since 2021), there are IntelliJ plugins like CodeGPT (with its first release in February 2023) that now allows you to run the code generation against a local LLM (running under llama.cpp):

Google is a bit late to the party. In December 2023 they announced Gemini. In February 2024, they launched the Gemma open models, based on the same technology than Gemini:

They also released a gemma.cpp inference engine:

And finally, if you are lost among so many LLM models, an interesting resource is the Chatbot Arena, released in August 2023. It allows humans to compare the results from different LLMs, keeping a leaderboard with chess-like ELO ratings:

And according to this leaderboard, at the moment GPT-4 is still the king.

Opening Home Assistant to Internet

To make Google Assistant work with your Home Assistant, you need to provide a public URL with HTTPS access to HA. Here are the full instructions:

But something that seems trivial, like publicly accessing services in your home server, has some complications, and you usually need to worry about dynamic IPs and security.

What do we need:

  • An ISP not using CG-NAT
  • Redirect ports in the router
  • A dynamic DNS provider and a client to update the IP (or a static IP)
  • An SSL certificate to securely access the HTTP services

ISP providers with CG-NAT

Some ISPs use CG-NAT (Carrier-Grade NAT), sharing the same IPv4 among multiple customers. In that case the only way to expose your services is using reverse proxy services such as ngrok.

Ngrok allows you to generate one static domain and it also automatically generates a SSL certificate, so most steps in this post do not apply.

My ISP (O2 Spain) assigns me a dynamic IP, and I prefer to not rely on these reverse proxy services, so I remotely access my home server redirecting ports in the router.

Dynamic DNS provider

Usually, and unless you have a static IP service (not very common, and not available in my ISP), you need to setup a dynamic DNS service.

I have been using the free Now-DNS service for years:

And to update the IP in my home server, I setup ddclient with this /etc/ddclient.conf file:

mail=root                               # mail all msgs to root
mail-failure=root                       # mail failed update msgs to root
pid=/var/run/               # record PID in file.
use=web,             # get ip from server.                      # default server

Some of these dynamic DNS domains are blocked in the Pi-hole blocking lists, so, if you are using Pi-hole or other DNS blocking service, you’ll probably need to whitelist your domain.

SSL certificate

With the amazing Certbot you can obtain free SSL certificates:

There is extensive documentation in the Certbot site about how to use it. I simply install certbot from apt and do a:

certbot certonly --webroot -w /var/www/html/ -d <your-dynamic-domain> --email <my-email> --non-interactive --agree-tos

But in order to make that work, you need a domain name (available from the dynamic DNS provider in the previous section).

HTTP Server

And to verify that the domain points to your server, Certbot is going to do an HTTP request to that domain, so you also need to have an HTTP server in the port 80 and open the port 80 in the router. This is also needed for the certificate renewals.

You may encounter numerous attacks on this port, so it is crucial to have a reliable web server that is consistently updated and properly configured. I personally use nginx as my HTTP server, and it has never failed me so far.

Home Assistant

To use the SSL certificate from the HA container, we need to share the folder where certificates are stored passing a “-v /etc/letsencrypt:/etc/letsencrypt” to the docker command and setting in the HA configuration.yaml:

  ssl_certificate: /etc/letsencrypt/live/<your-dynamic-domain>/fullchain.pem
  ssl_key: /etc/letsencrypt/live/<your-dynamic-domain>/privkey.pem

You can also use your public HA URL to remotely access it and to configure in the HA Android application.

Orange Pi 3B

I’ve never been a fan of the Raspberry Pi. In my opinion, it occupies an intermediate position where it is too underpowered for desktop use and too overpowered for IoT projects:

  • To use them as a desktop, there are great X86 alternatives available at about the same price than a RPi 5 but much more powerful, such as the Intel N100.
  • And for IoT projects, the ESP32 is the king, with amazing boards with Wifi, Bluetooth, etc., all at a price of less than 5 euros.

So it’s place may be TV boxes (where I prefer a Chromecast with Android) or small servers where the power consumtion is important because they are always on.

I bought an Orange Pi 3B: 4 cores, 4GB RAM, 64GB eMMC (~50 euros in Aliexpress) to replace my old X86 home server (Intel N450: 2 cores, 2 GB RAM, 64GB SSD):

The Orange Pi 3B shares the form factor with the Raspberry Pi 3B but it is almost as powerful as the Raspberry Pi 4. Notably, the Orange Pi 3B comes with several advantages over the RPi 4:

  • Support for eMMC (much faster and reliable than SD cards)
  • A power button
  • A full-size HDMI port
  • External antenna
  • And it’s cheaper

I installed the Ubuntu Jammy server image in the eMMC following the OPi manual. It needs to use a USB-A male to USB-A male cable and the RKDevTool (it’s in Chinese) that runs only in Windows.

And, as this machine is going to be exposed to internet, I hardened a bit the security:

  • Changed the APT repositories to
  • Regenerated SSH server keys
  • Removed SSH root access
  • Changed passwords
  • Renamed the orangepi user
  • Removed the local autologin

To remove the local autologin we need to edit:

  • /lib/systemd/system/getty@.service.d/override.conf: For the display console autologin
  • /lib/systemd/system/serial-getty@.service.d/override.conf: For the serial console autologin
ExecStartPre=/bin/sh -c 'exec /bin/sleep 10'
ExecStart=-/sbin/agetty --noissue --autologin orangepi %I $TERM

Removing the “–autologin orangepi”. If you rename the orangepiuser but you want to keep the autologin, you’ll also need to change the username here.

Then I moved the docker containers and other services from my old X86 server:

  • Home Assistant (docker container)
  • ESPHome dashboard (docker container)
  • Pi-hole (docker container)
  • nginx (for certbot and DNS DoT for Pihole)
  • certbot (to maintain the SSL certificate for Home Assistant)
  • ddclient (dynamic DNS updater)
  • NAS (do not expect anything fancy, I access a USB disk via SSH, it’s enough for Kodi & backups)

Everything seems to work smoothly now.

Pi-hole as home DNS and DHCP server

I encountered numerous issues with my network provider’s router DHCP. Since I haven’t yet decided to acquire another router, I opted to offload the DHCP server to another machine, which is currently running my Home Assistant and NAS.

I was in search of a DHCP server with a web UI. During my exploration, I came across Pi-hole, a DNS server specifically designed to block DNS queries to domains that serve ads and do tracking. Interestingly, Pi-hole also incorporates an integrated DHCP server (dnsmasqd) that can be configured through its admin UI.

I presume the integration of the DHCP server aimed to simplify the setup of clients’ DNS servers, yet it proves highly convenient for home networks. And forget about the “Pi” in the name, it can be run in any linux server, not necessarily in a Raspberry Pi.

I’m still an addict to running everything in Docker containers. So I set up the Docker Pi-hole container ( using this script localed at /usr/local/pihole/

cd $(dirname $(readlink -f $0))
docker stop pihole
docker rm pihole
docker pull pihole/pihole:latest
docker run -d \
	--name pihole \
	--privileged \
	--restart=unless-stopped \
	--network=host \
	-e TZ=Europe/Madrid \
        -e FTLCONF_LOCAL_IPV4= \
        -e WEB_PORT=8081 \
	-e WEBPASSWORD=admin \
	-e INTERFACE=eth0 \
	-e DNSMASQ_USER=root \
	-v ./etc-pihole:/etc/pihole \
	-v ./etc-dnsmasq.d:/etc/dnsmasq.d \
	--cap-add=NET_ADMIN \
docker image prune --all
  • Every time that you run the script, it updates the container with the last Pi-hole version
  • It didn’t work without setting FTLCONF_LOCAL_IPV4 to the local IP
  • I needed to set up WEB_PORT to not override with the nginx running in that machine (for Certbot)
  • Setting WEBPASSWORD is the easiest way to initially setup an admin password
  • I couldn’t make the DHCP server work with port mappings, it needed a –network=host
  • There is an image prune at the end to save space by removing old docker images

I also had some problems because Ubunt’s systemd-resolved includes a DNS server, and I needed to disable it:

And of course, you need to disable also the DHCP server on the router, it’s a very bad idea to have two DHCP servers working in the same network…

It is now functioning smoothly, and the included ad-blocking feature is a definite plus. Although it doesn’t currently block ads on YouTube and Twitch, its still great.

I’m also using it in my phone with a Wireguard VPN (it maybe a topic for another post). To make it listen in multiple interfaces like in the local and the VPN interfaces, I needed to create a /usr/local/pihole/etc-dnsmasq.d/99-interfaces.conf adding there:


Another similar alternative worth exploring is AdGuard Home, but I haven’t had the time to test it yet: