Hiding IPs from my own relay is harder than it sounds
I wanted my relay to never see IPs. I ended up changing the sentence instead.
While I was writing down what TrueSeal promises and I called it "private and anonymous" (something I like to do detached from implementing stuff, to make myself accountable) I realised that the threat model I'd already accepted said that the relay sees IP addresses and timing, and push fan-out lets it guess who shares a group. So anonymous was out not really true unless you mask your own I not really true unless you mask your own IP.
Pretty simple tho, I'll change the relay to never see IPs.
The relay software already never reads, stores, logs or keys anything on an IP. But a TCP connection hands the source address to whoever hosts the thing. The host OS sees it, the VPS provider sees it, and so does any reverse proxy you put in front. So "never see IPs" really means adding a network hop the relay host doesn't control.
# The options
My first idea was a proxy in front of the relay, and it's useless against the one party I care about. The proxy sees the IP, the relay sees the traffic, and the same person holds both logs. The Oblivious HTTP RFC (9458) says the two sides can't be run by the same entity. With a self-hosted relay, "the operator" is whoever runs the Docker compose, so the hop has to live outside their hands.
A Tor onion service works. The relay host never learns client IPs, and Noise runs unchanged inside the Tor stream. But that is a bigger more expensive change:
- About 430 ms median to get a first response from an onion service, on Tor's own desktop measurements, and that's without the time Tor takes to boot.
- Every self-hoster runs a Tor daemon next to the relay and keeps a second secret key around.
- iOS can take the socket away when the app is suspended, and a background push gives you 30 seconds. So the phone has to boot Tor again and reach the onion service inside that window. idk, for a library that's supposed to be boring on purpose that sounds like a lot of new ways to break.
A proxy hop run by someone else (how iCloud Private Relay works) is the cheapest fit. About one extra round trip, and iOS 17+ supports it natively. It only helps if an independent operator exists and doesn't collude with the relay, and I don't have one.
Oblivious HTTP doesn't fit at all. It's built for requests that carry no state between them, and TrueSeal keeps a long-lived receive session open.
Nym is the only option that also hides timing. It's heavy and poor on mobile tho.
# My own code was leaking too
A device's receiving connection and its sending connections come from the same IP. The push session uses Noise NK on purpose so the relay doesn't learn who sent a message. But if the host sees the same IP on both, it can re-identify the sender anyway. That goes into the threat model whatever I decide about Tor.
# What I did instead (for now)
For now I changed the docs so they don't lie. They now say: "The relay never logs, stores or uses your IP address. The server it runs on still sees the connection, as with any internet service. To hide your IP from the server too, use a VPN or Tor."
"Doesn't record or use" and "can't see" sound almost the same to a user, and they're very different promises.
"Can't see your IP" is banned from all public copy, along with "zero-knowledge". And the claim has a test behind it: an e2e scenario runs the real relay in normal and dev mode and fails if any client address shows up in the logs or in inbox.db.
Real IP hiding is out of the preview, and not required for 1.0 either. What I did keep is one place in the client that opens sockets, so Tor or a proxy hop can go in later without touching anything else.
I'm also thinking about making the sync library refuse to talk to a relay that isn't running my code. It stays self-hostable, but if someone modifies the relay, the sync won't talk to it. (via checksum or something like that, have not investigated these type of mechanisms yet)