What runs in your network, what it can be asked to do, and what it cannot. Every claim names the code that enforces it.
What the relay defends against:
- Perfloop, or anyone holding a tunnel, reaching anything not in your file.
A read is answered only for an upstream named in the configuration
(
relay.go,forward:r.upstreams[req.Host]), only as aGETwithout a body, and only on a documented read route of that upstream's kind (routes.go,route). There is no other handler, listener, or route. - A stolen relay token reading Perfloop data. The token is a bearer for the
tunnel route only; the Perfloop API refuses it everywhere else. The relay
sends it to the configured
apiorigin and nowhere else: redirects are not followed (relay.go,New,CheckRedirect). - Your credentials leaving your network. Upstream headers are read from your
file and set on the upstream request only (
relay.go,forward,up.Headers); they are never printed (routes.go,Routes) or logged, and Perfloop's own headers cannot override them, because yours are set last.
What the relay trusts:
- The Perfloop API's validation of each read. The relay does not parse
queries or check scopes; it forwards the query string unchanged
(
relay.go,forward,target.RawQuery = req.URL.RawQuery). - TLS to the configured origin: the system roots, or only
api_cawhen set, TLS 1.2 or later (relay.go,New,TLSClientConfig). A wrong certificate ends the handshake (relay_test.go,TestTunnelRefusesUntrustedAPICertificate). - Your upstream not echoing request headers into responses. Responses are
forwarded as they are (
relay.go,forward, theio.Copy).
What stays your job:
- Custody and rotation of the relay token, in the Secret your file references, and revoking it in Perfloop Setup when it may have leaked.
- A read-only, query-scoped token for each upstream in its
headers. - The NetworkPolicy: egress to the Perfloop API host on 443 and to your
upstreams, nothing else, and no ingress (
examples/kubernetes/). - Choosing when to roll a new image, after reading its commit and verifying
its signature (
docs/image.md). Nothing updates the relay for you.
The relay opens a WebSocket to the Perfloop API host over HTTPS
(relay.go, open):
- The target is
wss://<api host>/v1/relays/tunnelwith the relay token as aAuthorization: Bearerheader and theperfloop-relaysubprotocol (link/link.go). The API host is theapifield of the configuration and nothing else: a redirect, to any host, ends the handshake instead of being followed, so the token is sent only to the configured origin. - TLS 1.2 or later, trusting the system roots or, when
api_cais set, only that certificate authority.HTTPS_PROXYin the environment is honored for the tunnel, so an egress proxy that carries WebSockets is enough; it is not applied to upstream requests, which are local. - Inside the socket the roles turn around: Perfloop is the HTTP/2 client and
the relay is the HTTP/2 server (
relay.go,serve). Every read is an ordinary HTTP request from Perfloop to the relay. The relay never opens a stream of its own. - The relay keeps two tunnels open so the loss of one never leaves you without a tunnel, reopens a tunnel that ends with a backoff of at most ten seconds, and pings a tunnel that has been silent for thirty seconds; a ping with no answer within ten seconds ends the tunnel. At most eight reads are in flight on one tunnel; the tunnel's HTTP/2 settings enforce it.
- On
SIGINTorSIGTERMthe relay closes every tunnel at once and exits (relay.go,Runandserve;cmd/perfloop-relay/main.go). A read in flight on a closed tunnel fails; Perfloop retries a read whose response had not started on another live tunnel of the tenant, which is why the relay keeps two, and reports the read as unavailable when none is left. There is no drain.
Every read is checked in relay.go, forward, before any upstream request is
made. The route table it checks against is routes.go, and it is the same
table that -print-routes prints, so the printed listing is what the relay
does.
A read is forwarded only when all of these hold:
- Its host names an upstream in the configuration. Any other host answers
404 unknown upstream. - It is a
GETwith no body. Anything else answers405 only GET without a body is relayed. - Its path is clean: absolute, with no
.,.., or repeated slashes. Otherwise400 path is not clean. - Its path is the provider's API base followed by exactly one documented read
route, with nothing before the base and nothing after the route. Otherwise
403 route is not a documented read.
The documented read routes, per upstream kind:
| Kind | Base | Read routes |
|---|---|---|
prometheus, victoriametrics |
/api/v1/ |
query, query_range, labels, label/{name}/values, series, metadata |
loki |
/loki/api/v1/ |
query, query_range, labels, label/{name}/values |
pprof |
/<target>/debug/pprof/ |
profile, heap, allocs, goroutine, mutex, block |
{name} is one non-empty path segment. <target> is a name in the pprof
upstream's targets; any other answers 404 unknown target. A pprof read's
query must be empty or exactly one seconds from 1 to the upstream's
max_seconds, and profile must carry it; anything else answers 400. A
read past the upstream's max_concurrent answers 429. Admin, write, push, and delete routes
are not in the table and are refused before any upstream request; the tests in
routes_test.go and relay_test.go include the cases admin/tsdb/delete_series,
admin/tsdb/snapshot, write, push, and a .. in the path.
A forwarded request is built as: the upstream url plus the read's path,
the read's query string unchanged, and, of the request headers Perfloop
sent, exactly Accept, Accept-Encoding, User-Agent, and X-Scope-OrgID
(link.ForwardedHeaders; -print-routes prints them). Every other request
header, the Perfloop-Request-Id included, stops at the relay. Then the
upstream's configured headers are set and replace any header of the same
name: a customer who wants to pin the Loki tenant sets X-Scope-OrgID in
headers:, and the value Perfloop sent never reaches the upstream. The
upstream response's status, headers, and body are returned to Perfloop as
they are, except that hop-by-hop headers and every Perfloop-* header the
upstream sent are removed, so an upstream cannot speak as Perfloop. A
redirect from the upstream is not followed: the read fails with 502 upstream request failed, like any other upstream error.
Bounds on one read: two minutes end to end, and a response body of at most 32 MiB. A larger body aborts the read so that Perfloop sees a broken read, never a short body that looks complete.
- It does not inspect or rewrite upstream responses. There is no scanning
of response bodies for credentials and no redaction; the body Perfloop
receives is the body the upstream sent (
relay.go,forward, theio.CopyafterWriteHeader). Your protections are the ones you already own: give each upstream a read-only, query-scoped token in itsheaders, and use an upstream that does not echo request headers into responses. Every supported kind satisfies this by default. - It has no inbound listener. It does not bind a port. The only network
connections it opens are the tunnels to the configured
apiand the requests to the configured upstreams. - It takes no configuration push, no remote update, and no command. There is no shell, no exec route, and no route that changes its state. A new relay version is a new image you choose to roll.
- It cannot widen what Perfloop may read. The route table above is a control you own; Perfloop's own validation of each read happens before the read reaches the relay, and the relay does not repeat or replace it.
- When the relay token is revoked in Perfloop Setup, the Perfloop API ends
its open tunnels within 40 seconds (the API checks each tunnel's key on a
schedule; the relay takes no part in it), and every reconnect after that
is refused with
401 Unauthorized, which the relay logs asperfloop refused tunnel. Stopping the relay ends its tunnels at once.
A relay token is a Perfloop API key with the relay scope. A tenant admin
creates it in Setup, under Telemetry, Relay tokens, and revokes it there.
The plaintext is shown once; Perfloop stores only its hash. The token lets a
relay answer that tenant's validated reads and nothing else: the Perfloop API
refuses it as a bearer everywhere but the tunnel route, so a token held in
your network reads no Perfloop data. More than one relay may run with the
same token; the API uses any live tunnel of the tenant and accepts a limited
number of tunnels per tenant, refusing another with status 429 until one
ends.