# iStat Menus replacement

> Prime time for vibecoding products for your own needs

Published 2026-09-18 by Zsombor Paróczi · tags: saaspocalypse, iStat Menus, vibecoding
Source: https://blog.stormregion.hu/posts/istat-menus-replacement/

I did love iStat Menus. I had (and I guess still have) a license for it, but something has felt off lately. It started getting laggy, hogging resources, and feeling somewhat limited now that I use multiple Macs daily — which also made the licensing troublesome.

After searching for an alternative, I found [eul](https://github.com/gao-sun/eul), which looked awesome for my use case. The only thing was that its last release was years old, so it needed some love for today's Macs. GitHub is awesome in this sense: you can check out the forks through the network graph, so I could go fishing for the best version to use as a development base. :D

In this case, that meant multiple forks:

- https://github.com/miaoweiwei/eul
- https://github.com/chrsomle/eul
- https://github.com/StoneOlo/eul
- https://github.com/sclarkca/eul

Everyone had added something useful, and after the great merge of different branches, [version 2.1.0 was born](https://github.com/realhidden/eul/commit/c9fc4bb3e788c7431a6a47860586f5183a419468). Testing it for a week gave me the confidence that it would work well for my use case. Simple yet powerful stats, a modern-ish build of the old app, and some fixes here and there.

![eul](https://blog-files.stormregion.hu/eul/panel.png)

## Act Two — Connecting multiple machines

Ad-hoc networks, connections, and clever networking tricks have always had a special place in my random projects. I even managed to sell Ábel on the idea of creating an ad-hoc tracing service in Node.js five years ago. Check out the project; it's still awesome: [peek](https://github.com/abelptvts/peek).

The idea is the same as it was back then: give a service a shared secret, and it automagically becomes part of a distributed network where different machines find each other automatically.

> **What's a shared secret?**
> In this case, it's a pairing code you enter on each machine you want to connect. Machines with the same code can find each other and read each other's stats. No account or central registration needed. Anyone with that code can join the group, so treat it like a password.

Since I know more about networks and the technologies involved now, I split the job into two parts:

- **Local discovery:** same Wi-Fi / LAN, talk directly.
- **Remote discovery:** different networks, meet somewhere on the internet.

Both run at the same time, all the time.

## One secret, three keys

Everything starts with the shared secret. A generated one looks like `k7mqp-x3ndr-h9wza-2vtfe`: 4 groups of 5 characters, from an alphabet without look-alikes (no 0/o, no 1/l/i), so you can type it on the other machine by hand without cursing. That's roughly 99 bits of randomness, assuming the characters are chosen uniformly — plenty for this use case.

The secret itself never goes on the wire. It gets stretched with [PBKDF2](https://www.rfc-editor.org/rfc/rfc8018) (200k rounds), then [HKDF](https://www.rfc-editor.org/rfc/rfc5869) derives separate values, each with its own label. [This is the way.](https://www.starwars.com/series/the-mandalorian)

> **PBKDF2 and HKDF, without the alphabet soup**
> PBKDF2 turns the secret into key material through repeated calculations. That makes each password guess more expensive for someone trying to crack it.
>
> HKDF then derives separate values for separate jobs. Think of it as making three different keys from the same starting material: one for local discovery, one for the internet meeting room, and one for encryption. Seeing a discovery value doesn't give you the encryption key.

Actual copy-paste, starting with the master key material produced by PBKDF2:

```swift
let ikm = SymmetricKey(data: master)

func derive(_ label: String) -> SymmetricKey {
    HKDF<SHA256>.deriveKey(
        inputKeyMaterial: ikm,
        info: Data(label.utf8),
        outputByteCount: 32
    )
}

func hex(_ key: SymmetricKey) -> String {
    key.withUnsafeBytes {
        $0.prefix(16).map { String(format: "%02x", $0) }.joined()
    }
}

return Keys(
    lanToken: hex(derive("lan")),
    topic: "eul-peer/\(hex(derive("topic")))",
    seal: derive("seal")
)
```

I do not code Swift, but Opus does. :D

- `lan` — a token to recognize my peers on the local network.
- `topic` — the meeting room name on the internet.
- `seal` — the ChaChaPoly key that encrypts every snapshot.

A snapshot is simply one sample of a machine's current stats, packaged into a message.

Why the PBKDF2 part? Because the topic isn't private. The broker can see `eul-peer/3f9a...`, and someone who learns that value can test guesses to see which secret produces it.

With a strong generated secret, that is hopeless anyway. But people do type `password123` into things, so the slow stretching is the seatbelt (and the UI warns below 12 characters). It makes guessing slower; it doesn't turn a terrible password into a good one.

### Local discovery: Bonjour

Apple gave us Bonjour years ago, and the Network framework makes using it a couple of lines. Yet understanding what's going on underneath can take an afternoon. It's a pretty clever way to advertise services on the local network, and something I think everyone should know a little about.

> **What's Bonjour?**
> Bonjour is Apple's implementation of zero-configuration networking. Its local discovery uses multicast DNS (mDNS) and DNS Service Discovery (DNS-SD).
>
> Instead of asking a central server where a service lives, machines send discovery messages to a shared address on the local network. Nearby machines can announce what they offer and answer requests. That's how an app can find another app without you typing an IP address.
>
> A service announcement includes a service type, connection details, and optional metadata in a **TXT record**. Here, that metadata contains the pairing token. Discovery generally stays within the local network, and the network has to allow multicast traffic.

Every eul instance does two things:

- Advertises an `_eul-peer._udp` service, with the `lan` token in the TXT record.
- Browses for the same service type and keeps only the results where the token matches.

```swift
var txt = NWTXTRecord()
txt[Self.tokenKey] = token
listener.service = NWListener.Service(
    type: Self.serviceType,
    txtRecord: txt
)
```

When a match shows up, it opens a UDP flow to that peer, and every 5 seconds a sealed snapshot goes straight to it. No broker, no internet.

> **Why UDP?**
> UDP sends individual packets without setting up the reliable, ordered stream you get with TCP. A packet can get lost or arrive late, but for stats refreshed every 5 seconds, the next update is usually more useful than retrying an old one.

The token is a derived value, not the secret. Broadcasting it doesn't directly reveal the key needed to read the stats. It does let someone recognize machines in the same group and test password guesses, which is another reason to use the generated secret.

Two small gotchas I ran into:

- Results come back once per interface (Wi-Fi + Ethernet + whatever VPN you have), so the first endpoint per service name wins. Otherwise, you send everything 3 times.
- You will find yourself. Bonjour can also rename your service after a name conflict, so the "is this me?" check uses the name the system actually registered, not the host name.

### Remote discovery: someone else's MQTT broker

Different networks are where it gets interesting. NAT, firewalls, no local multicast, and it's always a pain in the ass to provide a public service on the internet for an open-source project.

> **Why can't the machines just connect directly?**
> Your home router usually uses NAT: multiple devices share one public IP address, while each device has its own private address inside the network. Outgoing connections are straightforward, but an incoming connection generally needs port forwarding or some other way through.
>
> A relay avoids that problem because both machines connect outward to a server they can reach.

Then it clicked: public MQTT brokers. Free, open, and built exactly for "many clients publish and subscribe to a topic."

> **What's MQTT?**
> MQTT is a lightweight messaging protocol. Clients connect to a server called a **broker**, which passes messages between them.
>
> A client **publishes** a message to a named **topic**, and clients **subscribed** to that topic receive it. Think of the topic as a meeting room: everyone joins the same room, and the broker forwards whatever gets posted there.
>
> The broker doesn't need to understand the message contents. That makes it useful as a relay for encrypted data.

The topic is the derived `eul-peer/<hex>`. Everyone with the same secret subscribes to the same room and publishes a sealed snapshot into it every 5 seconds.

The broker sees encrypted bytes on a random-looking topic. Without the key, it can't read the stats or create a new message that passes the authentication check. It can still see connection details and message timing, and it can drop, delay, or replay messages.

> **What's ChaChaPoly?**
> CryptoKit's `ChaChaPoly` combines ChaCha20 encryption with Poly1305 authentication. Encryption hides the contents; authentication checks that the message was created by someone with the key and hasn't been altered.
>
> A modified message, or one created without the key, fails that check and gets dropped. An unchanged copy of an old valid message still passes the cryptographic check, so the app also needs duplicate and freshness checks.
>
> Since all peers share the key, this proves membership in the group, not the identity of a particular machine.

The first version used one broker with a fallback list, which was suboptimal. :P Machine A falls back to broker #2, machine B is happy on broker #1, and they will never meet.

So now every instance joins all of them at once:

```swift
private static let brokers = [
    MQTTClient.Broker(host: "broker.emqx.io", port: 8883),
    MQTTClient.Broker(host: "broker.hivemq.com", port: 8883),
    MQTTClient.Broker(host: "test.mosquitto.org", port: 8886),
]
```

The MQTT client itself is a minimal, hand-written MQTT 3.1.1 implementation: QoS 0 only, one topic. Pulling in a full MQTT library for publish + subscribe felt like overkill.

> **What's QoS 0?**
> MQTT has different delivery levels, called Quality of Service (QoS). QoS 0 sends messages without MQTT-level acknowledgements or retries, so delivery isn't guaranteed.
>
> That's a reasonable tradeoff for periodically refreshed stats: if one snapshot goes missing, another should arrive a few seconds later.

These are public shared brokers, so I'm relying on someone else's infrastructure. Handy for this project, but not something I'd treat as a guaranteed service.

## Same snapshot, many times

With Bonjour plus three brokers, the same snapshot can arrive up to 4 times. Plus, the brokers can echo your own publications back to you.

Each snapshot carries a per-launch instance ID and the sender's timestamp, so the receiver:

- Drops its own messages (same instance ID).
- Drops anything older than 60 seconds, limiting how long an old snapshot can be replayed.
- Keeps only the newest `sentAt` per peer. Duplicates from the slower paths just update "last seen" and the "via LAN / via relay" badge.

The instance ID is a temporary identifier generated when the app starts. It lets the receiver distinguish running peers without needing a central registry.

The timestamp check also assumes the machines' clocks are reasonably in sync. It isn't complete replay protection: a recent valid message can still be repeated inside that window, although it won't replace a newer snapshot.

After roughly three missed updates — a 16-second timeout — the peer disappears from the list. A restarted peer simply shows up as a new one, and the old entry expires on its own. No goodbye messages, no state to clean up.

I do love this solution, so I think I'll use it next time as well. Elegant, small(ish), and simple enough to have just a couple of moving parts.

## The Linux side

Once I had the Mac solution tested, I wanted to create a Linux CLI port. Plain C99, runs on a Pi, and speaks the same protocol, so it shows up in the Mac panel like any other peer.

It only does the broker part, over plain TCP on port 1883, using the same brokers and topic. Adding TLS to a dependency-free C binary was not a yak I wanted to shave. The Bonjour portion would also need mDNS/DNS-SD support, so I left that out as well.

> **TCP, TLS, and the encrypted payload**
> TCP provides a reliable, ordered connection between the client and broker. TLS adds encryption to that connection and, with proper certificate validation, verifies the broker's identity.
>
> The snapshots are already encrypted end-to-end: the sending machine encrypts them, and the receiving peers decrypt them. So even over plain TCP, the stats themselves remain encrypted.
>
> TLS still adds something useful, though. Without it, someone watching the connection can see the MQTT topic and protocol traffic, and an active attacker can interfere with the connection. Payload encryption protects the stats; TLS protects the connection to the broker.

So skipping TLS is a tradeoff in the Linux port, even though the payload is already encrypted.

The crypto had to be byte-compatible with CryptoKit, so I asked Opus to write PBKDF2, HKDF, and ChaChaPoly in C and test them against the RFC vectors. On macOS, there's also a cross-check that seals a message on one side and opens it on the other.

> **What's an RFC test vector?**
> It's a published example with known inputs and expected outputs. Feeding those inputs into your implementation should produce exactly the documented result.
>
> The cross-check then tests whether the C implementation and CryptoKit can actually exchange encrypted messages. Useful checks, though passing them isn't the same as a full security review.

## Open-source project

This started as an open-source project, and I'm keeping it that way, so you can see my exact modifications:

[github.com/realhidden/eul](https://github.com/realhidden/eul)

And just to clarify: a **FREE and OPEN-SOURCE iStat Menus replacement** that needs no server of your own, no accounts, and no monthly bill to check on your other machines remotely.

Local peers talk directly. Remote peers use public MQTT brokers as relays. Same secret, same group, stats in your menu bar.

Feel free to contribute, fork, or just use it. I hope it helps you as much as it helps me.
