// my projects | whoami.zerotrust.nz
This is part of an ongoing series of notes on tools I build for my own infrastructure. Not polished products. Working software.
how it started#
It started as a what-is-my-ip page.
That is how these things start. You want something simple, you build it in an afternoon, and then you start pulling on the thread. Every pull reveals something you did not know you were missing. The afternoon becomes a week. The what-is-my-ip becomes something else entirely.
The original intent was straightforward. I wanted a page I could point people to, something that lived on my own infrastructure, that would show them their IP address without routing the request through someone else’s analytics. A small sovereignty gesture. No third-party APIs if avoidable. No Google-hosted fonts if possible. Nothing that called home.
I got that. And then I kept going.
what it became#
The real question was never what your IP is. That is the least interesting thing your connection reveals.
The more interesting question is: what does a web server actually see when you connect? Not what your browser tells you. Not what the VPN provider claims. What does the raw request look like at the other end?
Once you frame it that way, the scope expands naturally. The server sees your IP, yes. But it also sees the exact order your browser sent its headers. That order is a fingerprint. Chrome, Firefox and Safari each have a characteristic pattern, and that pattern does not change when you switch to incognito, clear your cookies, or rotate your IP. It changes only when you change the browser software itself.
The server also sees your TLS handshake. The negotiated cipher. Whether the IP maps to a known VPN exit range or a Tor relay.
On the client side, once you load the page, the picture gets worse. The GPU renderer string. The canvas hash. The audio fingerprint. The font set. The screen resolution, pixel ratio, hardware concurrency. Each of these is a signal. Combined, they form something closer to a unique identity than most people expect.
The page now shows all of it. Grouped by what it is. Explained in plain language. No jargon for its own sake.
the architecture#
The backend is Rust. Axum, tokio, the usual stack. It runs as a systemd service on the web-server LXC, the same container that handles several of my other vhosts. The binary is small. Memory at idle sits around 16MB, most of that is the GeoIP databases loaded into memory at startup.
Nginx terminates TLS and proxies to the Rust backend over loopback. The key architectural decision was bypassing the firewall WAF for this specific vhost. Every other site on the server goes through the WAF reverse proxy, which means the firewall terminates TLS, rewrites headers, and SNATs the connection. That is fine for most purposes. For a fingerprinting tool it defeats the point entirely. If the firewall terminates TLS, the server never sees the real handshake. It sees the firewall’s handshake.
The solution was a plain DNAT rule, forwarding port 10001 directly to the LXC. No WAF, no TLS interception, no SNAT. The visitor’s TCP connection arrives at nginx intact. The real cipher, the real protocol, the real IP.
visitor -> firewall (dnat only, no proxy) -> nginx:10001 -> rust:8080
That path matters. The passive HTTP hash is computed from the actual browser headers before any proxy has touched them. The TLS data is real. The peer IP is the visitor’s real address.
what the server detects#
Three layers of IP intelligence.
First, MaxMind GeoLite2. Two local databases loaded at startup: City and ASN. No outbound call per request. The lookup is a memory read. Country, city, coordinates, timezone, ASN, organisation. All of it resolved server-side, which means it works regardless of what the browser shields block.
Second, VPN and Tor detection. X4BNet maintains community CIDR lists for VPN exit ranges. The Tor Project publishes an official bulk exit list, updated every thirty minutes. Both load at startup into in-memory prefix sets. A connection check is a linear scan across roughly ten thousand VPN ranges and twelve hundred Tor exits. Fast enough. If the CIDR check misses, the code falls through to ASN and organisation string matching. Mullvad exits, for example, often come through Host Universal Pty Ltd on AS136557. That ASN is now in the hardcoded map. The provider name appears in the hero, not a generic label.
Third, the passive HTTP fingerprint. The header order, stripped of any nginx-injected headers, hashed with SHA-256. The result is a stable identifier for the browser build. It will be the same hash every time that browser on that machine makes a request, regardless of IP rotation, cookie clearing, or private mode.
what the client detects#
The full list is visible on the page. The highlights:
The canvas and audio fingerprint are the stickiest signals. A hidden canvas draw and a silent OfflineAudioContext render produce hashes that vary by GPU, driver, and audio hardware. They survive everything except a browser that actively randomises them. Brave and GrapheneOS Vanadium do this. The page detects it and notes it.
The font enumeration reveals the OS and installed software by measuring whether the browser renders test strings differently with each font substituted against known base fonts. Sixteen Windows fonts confirmed on the work laptop. Fourteen Linux fonts on skycity. Different machines, different operating systems, same logic.
The WebRTC leak probe is the one that matters most for VPN users. WebRTC can expose your real local IP address even when you are behind a tunnel, by using the STUN protocol to gather ICE candidates. The probe fires a STUN request at Google’s server and watches what comes back. If a private RFC1918 address appears, the tunnel has a bypass. Hardened browsers and VPN kill switches prevent this. Standard Chrome does not.
the exposure score#
The page generates an exposure score. Zero to one hundred. Low, Moderate, or High.
Four components: network exposure, fingerprint uniqueness, tracking API surface, and privacy posture. Each weighted. Combined into a single number.
The thing worth saying about this score is what it cannot tell you. It is an estimate, not a security audit. A low score is not anonymity. The weightings are a judgement call, not a standard. It only sees this one browser visit. It knows nothing about your accounts, your habits, or what you did before you landed here.
It is useful for one specific thing: understanding which factors are exposing you and how they trade off. The paradox it surfaces is real. The more you harden your browser, the more unique you become. On a street full of people in ordinary clothes, the one person in a hooded coat and sunglasses stands out. Tor Browser works hard to avoid this by standardising what all its users expose. Brave goes some way. No setup is perfect.
The score tells you where you sit. It does not tell you where you need to be.
what it does not do#
No storage. No logging of visitor data. No analytics. The binary reads the request, generates the inspection, returns the JSON, forgets everything. The Rust process holds no state between requests.
The GeoIP and CIDR lookups are read-only against files loaded at startup. Nothing writes to disk at request time.
The page itself uses no external fonts at request time that could be used for tracking. The CDN references for icons and fonts are the one compromise. Acceptable for a public demo tool. A hardened version would inline everything.
current state#
Running. Stable.
The lists refresh weekly via cron, pulling the latest VPN ranges from X4BNet and the latest Tor exits from the Tor Project. The service restarts after each refresh.
The next meaningful addition is proper JA4 fingerprinting. That requires terminating TLS inside the Rust process rather than delegating it to nginx, so the raw ClientHello bytes are accessible before decryption. The backend is structured to accept a ja4 field in the inspection struct. The work is in wiring rustls to capture the handshake. That is a future session.
The page lives at whoami.zerotrust.nz:10001. No login, no account, nothing stored. Load it from whatever browser you want to test. The data it returns is the data you leave behind everywhere you go.
what it taught me#
Most people have no idea what their browser reveals. That is not a failure of intelligence. It is a failure of visibility.
The common mental model is: VPN on, location hidden, private. That model is incomplete in ways that matter. The VPN hides your IP. It does not touch your fingerprint. Your canvas hash is the same on Mullvad as it is without Mullvad. Your font set does not change. Your GPU renderer string does not change. You are recognisable.
Clearing cookies helps less than people think. Private mode helps less than people think. The signals that persist are the ones rooted in hardware and browser software, not in stored state.
Building this tool made that concrete in a way that writing about it abstractly does not. The data is real. The test is immediate. The gap between what people assume and what the server actually sees is the whole point.
That gap is the conversation worth having.