A speed test that runs inside the machine it measures
A throughput number is a claim about a path, and a path has two ends. If both
ends are the same host, or if one end is a one-vCPU machine that is busy running
everything else, the number is mostly a measurement of the machine. It will
happily report a fast link over a bad one. The header of
internal/api/speedtest.go puts it bluntly:
1
// a speed test running inside the machine it is measuring is not a speed test
Where the bytes actually travel
The browser in the tunnel moves bytes to and from the tunnel endpoint — the
host every client tunnel terminates on, served by this same binary under its
clientsv probe role. The downlink is streamed from there; the uplink is
counted there.
The portal is deliberately the other half. It serves the page, owns the session, mints the short-lived proof the far end requires, and writes the one result row. It does not move the bytes. That distinction is the design: the edge host is one vCPU that also serves the site, the admin console, monitoring and CI, and its load moves the throughput measured through it by a factor of several.
The far end is a deliberately minimal role of the same binary: no database, no session, no cookies, no templates, no history, no write path. Two routes carry the load, and both are pure copy work:
1
2
3
block := speedBlock() // 1 MiB, incompressible, repeated
n, err := io.Copy(io.Discard, io.LimitReader(r.Body, cap+1)) // the uplink
Every byte the test moves is a byte the host copied: a one-vCPU host can saturate on the copy before the link does, so the ceiling is often the host, not the network. A finished test stores one row — two rates, the byte counts, the latency, the device and tier, and the time.
Auth without a usage log
An endpoint anyone can hit has to be authenticated, and the easy way is to give it an account and log it. A session cookie is host-only, so a browser cannot carry the portal’s session to another name — the far end needs its own credential, and it gets a short-lived bearer token instead of an account.
The portal mints it and the far end verifies it statelessly: HMAC-SHA256 over an expiry and a nonce, keyed by a secret both hosts read from the same sops file. The nonce makes two tokens minted in the same second different, so a leaked one is not replayable. The shipped TTL is 3 minutes, the far end accepts nothing with an expiry more than 15 minutes out, and 30 seconds of clock skew is absorbed.
There is no access log entry for any of it. The far end writes nothing, the
fleet’s caddy skips the probe paths on the endpoint, and on the edge
/api/speedtest/* is log_skipped next to a comment explaining why. One
request line carries a byte count and a timestamp, and a few dozen of them are
a per-test throughput measurement tied to an address.
The induced load is itself a denial-of-service surface on a small edge host, which is why the credential is short-lived and per-request rather than a long-lived key, and why there is a concurrency ceiling of 12 requests in flight. Over the ceiling the far end answers 503 and the page falls back to measuring at the portal — a stalling client degrades the test instead of parking the host. Stealing a token buys bandwidth, not access, and it buys it for minutes.
What a throughput figure needs next to it
Never present one number on its own. Say, at minimum:
- What the two endpoints were. “Client to tunnel endpoint” and “client to the site’s front door” are different paths and different numbers.
- What the transfer size was. The test streams a 1 MiB block repeatedly and caps a single request at 512 MiB per direction; a short transfer measures ramp, not throughput.
- What the host was doing. On a one-vCPU edge, a backup job or a deploy moves the number by a factor of several.
What you can check
- The headers, in
internal/api/speedtest.goandinternal/api/probe.go: where the bytes travel, what is stored, and the sentence about the machine measuring itself. - The token.
MintProbeTokenandprobeTokenValidshow the HMAC over expiry and nonce, the 3-minute TTL and the 15-minute ceiling; the key comes from the same sops file both hosts read. - The absence of a log line.
/api/speedtest/*islog_skipped in the public vhost’s caddy config, with the reasoning written next to it, and the probe paths are skipped on the endpoint. - The one row. Its fields and its five-row retention are in the same file as the code that writes them.
A short-lived token protects against replay, against correlation by value, and bounds the blast radius of a leak — it expires in minutes and grants a bounded transfer to one far end. It does not protect the host from a concurrent test saturating its CPU, which the concurrency ceiling caps as a degradation control rather than a defence.
What is recorded and what is not is on what we log; the policy it belongs to is what “no logs” can and cannot promise.