Expand description
§Access
Who may reach the proxy, and how they prove it.
Two gates, decided at bind time, carried together in ProxyAccessConfig
because the router applies both at the same layer:
| Gate | Default | Answers |
|---|---|---|
| Bearer token | off | is this client authorised? |
| Host allowlist | always on | did this client know where the proxy lives? |
Everything here is pure — predicates and data. The middleware that applies it
lives in gglib-proxy, which is the crate allowed to depend on axum.
§Why a Host allowlist, when CORS already exists
CorsConfig::LocalOnly inspects Origin, and DNS rebinding does not change
Origin — it changes which IP a hostname resolves to. An attacker’s page stays
https://evil.com throughout, so the CORS predicate does reject it.
What CORS does not do is stop the request from being sent. It governs
whether the response may be read. For a preflighted request (anything
sending Content-Type: application/json, which is every /v1/chat/completions
and /mcp call) the browser asks permission first and never sends the real
request, so those are genuinely blocked. A simple GET, however, is sent, runs
to completion, and only its response is withheld — any side effect has already
happened.
The Host header is the part rebinding cannot forge: the browser sends the
name the page asked for, which is the attacker’s. Checking it closes the
simple-request gap, covers any future route that is not preflighted, and
removes the endpoint’s dependence on CORS being configured correctly. It is
enforced unconditionally, including when no token is set, because it costs a
string comparison and defends the case where the operator configured nothing.
§The allowlist
Loopback is a predicate, not a list — is_loopback_host accepts the literal
localhost and anything that parses as a loopback IP, so 127.0.0.2 and ::1
work without anyone enumerating them.
Beyond loopback, ProxyAccessConfig::new admits exactly what the operator
named:
| Bind host | Also allowed |
|---|---|
127.0.0.1, localhost, ::1 | nothing further — loopback covers it |
192.168.1.5 | 192.168.1.5 |
0.0.0.0, :: | nothing — a wildcard names no reachable address |
plus every --allowed-host value. The wildcard row is the one that breaks
existing setups, and it is deliberate: inferring the machine’s interface
addresses would re-open the hole the check exists to close, so a wildcard bind
must name its hostname explicitly.
§The token
Optional. None leaves the endpoint behaving exactly as it did before
authentication existed, which is what keeps the upgrade silent for the loopback
default. ApiKeySource records where a set token came from so the startup
banner can explain the decision instead of merely stating it — and so a token
this process generated can be printed once, while one the operator already
holds is not echoed into terminal scrollback.
bearer_matches decides whether a request presents it. The auth scheme is
matched case-insensitively, because RFC 9110 says it is a token and tokens are
case-insensitive; only the credential goes to constant_time_eq.
BearerPolicy decides which token is required, and it is a live question
rather than a bind-time one — a key rotated afterwards has to be honoured, and
a key set afterwards has to be enforced.
Modules
| Module | LOC | Complexity | Coverage |
|---|---|---|---|
access_tests.rs | |||
bearer.rs | |||
bearer_tests.rs | |||
device_keys.rs | |||
device_keys_tests.rs | |||
host.rs | |||
host_tests.rs |
Modules§
- bearer 🔒
- Matching an
Authorizationheader against the configured bearer token. - device_
keys 🔒 - The keys this machine issued to paired devices, on disk.
- host 🔒
Structs§
- Bearer
Policy - Which token a running endpoint currently requires.
- Proxy
Access Config - Who may reach the proxy, and how they prove it.
Enums§
- ApiKey
Source - Where the proxy’s bearer token came from.
Functions§
- bearer_
matches - Whether
presented— the rawAuthorizationheader value, orNonewhen the client sent none — carriesexpected_key. - constant_
time_ eq - Compare two byte strings without an early exit on the first difference, so response timing does not leak how many leading bytes of a secret a caller guessed right.
- device_
keys_ path - Where the keys live: beside the endpoint identity.
- generate_
api_ key - Mint a bearer token for an endpoint that is about to be exposed off loopback.
- is_
loopback_ host - Whether
hostrefers to the loopback interface. - is_
wildcard_ host - Whether
hostis a wildcard (“all interfaces”) address. - load_
device_ keys - Read the roster’s keys, or an empty map when nothing has been issued.
- normalize_
host - Reduce a host or authority to a bare, comparable host name.
- store_
device_ keys - Replace the stored keys,
0600, atomically.
Type Aliases§
- Device
Keys - Every device key this machine holds, by the id the tunnel edge knows it as.