Serein
Rust로 만든 가볍고 성능 좋은 Discord 전용 네이티브 클라이언트
Self-hosted, operator-blind HTTP proxy toolkit (OHTTP relay + attested gateway + commons cache). Non-commercial.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A self-hostable HTTP proxy toolkit for personal use, built so that no operator can link who made a request to what was fetched. Privacy derives from what each party is structurally unable to observe, not from a policy commitment.
Columbia is named after Apollo 11's Command and Service Module, which stayed in orbit without visibility into the lunar surface operations below.README 전문 보기 ↗A self-hostable HTTP proxy toolkit for personal use, built so that no operator can link who made a request to what was fetched. Privacy derives from what each party is structurally unable to observe, not from a policy commitment.
Columbia is named after Apollo 11's Command and Service Module, which stayed in orbit without visibility into the lunar surface operations below.
HTTP content is fetched through a split-trust path built on OHTTP (RFC 9458). Three services carry the request path (relay, gateway, commons cache), and a fourth optional service (the token issuer) gates who may use the relay without identifying them. Each runs as a self-hosted service on Docker, on any host. The issuer and relay include locked dependencies for cryptography or optional shared storage; a managed cloud is not required for local development.
No single operator ever holds both client identity and request content at the same time. The relay sees the client IP but only opaque ciphertext. The gateway decrypts and fetches but never sees the client IP. Run the two as separate operators and neither can link a client to the content it fetched.
This is a toolkit, not a hosted service; it requires self-provided servers and keys. It is released for personal, non-commercial use (see LICENSE).
The OHTTP path. No single hop holds both client identity and request content.
flowchart LR
client["Your client<br/>HPKE-seal (on device)"]
subgraph relayop["Relay operator"]
relay["RELAY<br/>sees your IP,<br/>not content"]
end
subgraph gatewayop["Gateway operator"]
gateway["GATEWAY<br/>sees content,<br/>not your IP"]
commons["COMMONS CACHE<br/>fetch once,<br/>serve many"]
end
upstream["upstream"]
client -->|"sealed bytes + your IP"| relay
relay -->|"opaque ciphertext, NO IP"| gateway
gateway -->|"plaintext request"| commons
commons -->|"fetch once"| upstream
X-Forwarded-For, so the gateway cannot learn the client's identity.ALLOWED_TARGET_ORIGINS). Set that allowlist tightly. The gateway does NOT fail closed on its own: if ALLOWED_TARGET_ORIGINS is empty or unset, it becomes an open, anonymous proxy and an SSRF pivot that anyone can point at any origin. Matching is by exact Host string, so scheme, port, and subdomain are all literal (example.com does not cover api.example.com).The relay thus holds identity without content, and the gateway holds content without identity. As long as different operators run them and the two do not collude, no party can reconstruct a client's reading history. Running both under a single operator is acceptable for testing, but it collapses the non-collusion property (see SELFHOSTING.md).
The full trust and threat model, the attestation chain, and the observability design are in ARCHITECTURE.md.
ALLOWED_TARGET_ORIGINS allowlist can be reached by sealing a message/bhttp GET that carries Authorization and User-Agent; the gateway forwards those inner headers to the target. The target must be reachable from the GATEWAY's egress IP, since the gateway makes the outbound fetch, not the client.Columbia is a read-path privacy layer, not a general tunnel: it is not a VPN. Whether it will route a request depends on what the credential identifies, not merely on whether the request is "authenticated":
Authorization header travels inside the HPKE-sealed body, so the relay never sees it and only the gateway does; because it links to the app and not to a client identity, the relay-holds-identity / gateway-holds-content split still holds. Columbia still does not pool or share user credentials; a shared app-level credential is already common to everyone by definition.A liveness or reachability probe from the client (can I reach the relay? is the path up?) is not the same thing as routing a fetch through it. The former only confirms the hop answers; the latter seals a real request end to end. Health-checking the relay tells you nothing about whether a given target is allowlisted or reachable from the gateway; that is exercised only by an actual sealed request the gateway forwards.
A sealed read can fail: the target is not on ALLOWED_TARGET_ORIGINS (the gateway returns a BHTTP 403), the gateway's shared upstream budget is momentarily spent (429 with Retry-After), or the path is down. When it does, the client decides how to degrade. Fail-open: fall back to a direct fetch over its own session, trading the split-trust property for availability. Fail-closed: surface the error and route nothing, keeping the privacy property even when the path is unavailable. Columbia takes no position; the choice is the client's policy.
/ohttp-configs vs /ohttp-keysThe gateway publishes its HPKE key material at two endpoints. GET /ohttp-configs returns a single classical X25519 config (DHKEM(X25519, HKDF-SHA256)): the one a simple RFC 9458 client pins, and the only one the relay proxies. GET /ohttp-keys returns the full list (the draft post-quantum X25519+Kyber768-draft00 hybrid plus the classical X25519 config); a client that wants the hybrid selects from this list. The relay currently passes through /ohttp-configs only, so a hybrid-capable client behind a relay-only public surface reaches /ohttp-keys on the gateway's own host.
| Directory | Role | What it can see |
|---|---|---|
ohttp-relay/ | Strips your IP and all headers, forwards opaque ciphertext to the gateway | your IP plus opaque bytes, never content |
ohttp-gateway/ | Decapsulates the request, fetches the allowlisted target, re-encapsulates the response | request content, never your IP |
commons-cache/ | Fetches each public item once and serves all; TTL plus stale-while-revalidate plus single-flight | public content only, no user identity |
token-issuer/ | App Attest gated, blind-signs anonymous unlinkable tokens the relay verifies offline | the device id at issuance, never the spent token or the content fetched |
ohttp-relay/ is a Node service with an optional locked Azure Table state adapter. It forwards message/ohttp-req bodies to the gateway in a fresh request that carries no client headers and no X-Forwarded-For. As the one public surface it also carries the abuse controls (per-IP rate limiting, a concurrency cap, a strict request shape, and a pluggable client-auth hook), with transient IP limits and optional durable anonymous spend hashes; neither is logged. For the hardened topology it can run as the sole public hop, with the gateway and cache on internal ingress, and pass the gateway's public key config through to clients. See SELFHOSTING.md. Its logs are RED metrics only.ohttp-gateway/ is a vendored copy of Cloudflare's privacy-gateway-server-go (BSD-3, the RFC 9458 reference gateway). Local modifications include relay authentication, an endpoint guard, an optional outbound rate limiter, response-side error mapping, a bounded upstream timeout, bounded operational summaries and corrected Prometheus accounting. Request diagnostics exclude caller-derived content. Runtime environment variables configure deployment-specific behavior. Provenance, the full list of additions, and the required attribution live in ohttp-gateway/VENDORED.md.commons-cache/ is another dependency-free Node service, an optional cache origin for public, sessionless content. It serves X-Cache: HIT|MISS|STALE with CDN-ready Cache-Control and Age headers.token-issuer/ is its own service. It runs Apple App Attest verification and blind-signs (RFC 9474) anonymous tokens so the relay can require a genuine, rate-limited client without a login. It is the one component that learns a device identity; blinding prevents that identity from being linked to any finished token or to the content it is spent on. Verifying a spent token at the relay is offline against the issuer's published epoch public key. See token-issuer/PROTOCOL.md for the wire format and .The relay is the only service that has to be public. The gateway and the commons cache run on internal ingress, reachable only from inside the environment, and the issuer is its own public service alongside the relay. Because the gateway is internal, clients cannot fetch its key config directly, so the relay proxies that one read at GET /ohttp-configs and returns the gateway's public key-config bytes verbatim. That is public material clients are meant to pin, so the passthrough leaks nothing.
The relay's rate and concurrency controls use transient memory. Optional shared redemption storage retains only anonymous spend claims, public-key fingerprints and signature hashes, without request content or client IP. Issuer registration/counter/quota state uses a separate store and operator credential; it must never be shared with the relay. These stores do not remove timing-correlation or operator-collusion risks.
POST /relay with Content-Type: message/ohttp-req is served.X-Columbia-Relay-Auth) so the gateway rejects traffic that did not come through the relay.off, secret, and token. In token mode the relay verifies an anonymous issuer-signed token offline and enforces spend-once.The optional Azure Table adapters preserve issuer counters/quotas and relay spend decisions across replicas and restarts. Memory mode is limited to one process. Enabling token enforcement requires a separate compatible rollout, durable state, reviewed epoch keys and real-device App Attest acceptance; installing a newer image does not complete those steps. See the issuer deployment requirements, relay token mode and roadmap.
A CDN or WAF (for example a managed front door) can sit in front of the public relay and issuer to absorb DDoS and rate-limit at the edge. When the relay and issuer are configured to require it, they reject any request that did not arrive through that front door, so the origins cannot be reached directly. See SELFHOSTING.md.
brew tap wbsmolen/columbia
brew install columbia
columbia commons # or: relay | gateway | issuer
One columbia launcher runs any of the four services; each is configured by
the environment variables in its service README (and SELFHOSTING.md).
Running everything on one machine is for development only — the operator-blind
guarantee requires the relay and gateway under separate, non-colluding operators.
Prerequisites: Docker and OpenSSL. To run the full path on one machine:
# 0. Create a shared network so the containers can reach each other by name.
docker network create columbia
# 1. Generate the gateway's HPKE seed (32-byte hex) and a local test certificate.
# The self-signed cert + NODE_TLS_REJECT_UNAUTHORIZED below are local-testing
# only, never production — see SELFHOSTING.md for a real deployment.
export SEED_SECRET_KEY="$(openssl rand -hex 32)"
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout certs/key.pem -out certs/cert.pem -subj "/CN=gateway"
# 2. Build and run the gateway. ALLOWED_TARGET_ORIGINS restricts what it may fetch.
cd ohttp-gateway
docker build -t columbia-gateway .
docker run -d --name gateway --network columbia -p 8080:8080 \
-v "$(pwd)/../certs:/certs:ro" \
-e PORT=8080 \
-e SEED_SECRET_KEY="$SEED_SECRET_KEY" \
-e LOG_SECRETS=false \
-e ALLOWED_TARGET_ORIGINS="http://commons:8080" \
-e CERT=/certs/cert.pem \
-e KEY=/certs/key.pem \
columbia-gateway
# 3. Build and run the relay, pointed at the gateway.
# GATEWAY_URL must be https: the relay hard-exits on a plain-http value.
cd ../ohttp-relay
docker build -t columbia-relay .
docker run -d --name relay --network columbia -p 8081:8080 \
-e PORT=8080 \
-e GATEWAY_URL="https://gateway:8080/gateway" \
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \
columbia-relay
# 4. (Optional) Build and run the commons cache as the gateway's upstream target.
cd ../commons-cache
docker build -t columbia-commons .
docker run -d --name commons --network columbia -p 8082:8080 -e PORT=8080 columbia-commons
For the full walkthrough (generating and pinning the gateway key config, configuring the cache, and running the relay and gateway as separate operators so non-collusion holds), see SELFHOSTING.md.
An OHTTP client (any RFC 9458 library, for example one built on Apple CryptoKit or ohttp) seals a request against the gateway's published key config and POSTs the message/ohttp-req body to the relay. The relay hands back the encapsulated message/ohttp-res, which the client opens locally. The gateway publishes two key configs: a primary draft post-quantum suite (KEM 0x30, X25519+Kyber768-draft00) and a legacy classical suite (KEM 0x20, DHKEM(X25519, HKDF-SHA256)). A classical-only RFC 9458 client must select the legacy config; the primary config is a draft, non-RFC suite that not every client supports.
The privacy property depends on logs not recording what the crypto hides, so the rule is RED metrics only (rate, errors, duration).
{ts, route, status, durationMs[, cache]}. The route is always a template (/relay, /v1/commons), never a path that contains a target, a query value, or a body. Cardinality stays bounded because there's nothing high-cardinality to write.reason and error code describing the relay-to-gateway hop, so a 502 can be attributed to a cause without naming a client or a target. The reason vocabulary is fixed, so cardinality stays bounded.LOG_SECRETS=false the HPKE seed never reaches stdout.The privacy property is designed to be verified rather than taken on trust.
flowchart TD
root["columbia/"]
readme["README.md: you are here"]
arch["ARCHITECTURE.md: trust/threat model, attestation chain, observability"]
self["SELFHOSTING.md: host-agnostic build & run with Docker"]
roadmap["ROADMAP.md: hardening / decentralization direction"]
license["LICENSE: PolyForm Noncommercial 1.0.0"]
gitignore[".gitignore"]
security["SECURITY.md: reporting + the guarantee in scope"]
commons["commons-cache/: optional public-content cache origin (Node)"]
commonsfiles["server.js, Dockerfile, deploy-ghcr.sh, README.md"]
gateway["ohttp-gateway/: VENDORED Cloudflare OHTTP gateway (Go, BSD-3)"]
vendored["VENDORED.md: upstream repo + pinned commit + local modifications"]
gatewaysrc["… (mostly-upstream source + vendored Go deps)"]
relay["ohttp-relay/: OHTTP relay (Node)"]
relayfiles["server.js, Dockerfile, README.md"]
issuer["token-issuer/: App Attest + blind-RSA token issuer (Node)"]
issuerfiles["server.js, appattest.js, PROTOCOL.md, Dockerfile, README.md"]
deploy["deploy/: optional reference deploy config, one example among many"]
root --> readme
root --> arch
root --> self
root --> roadmap
root --> license
root --> security
root --> gitignore
root --> commons
commons --> commonsfiles
root --> gateway
gateway --> vendored
gateway --> gatewaysrc
root --> relay
relay --> relayfiles
root --> issuer
issuer --> issuerfiles
root --> deploy
| RFC | Used for |
|---|---|
| RFC 9458 | OHTTP, the relay/gateway split-trust transport |
| RFC 9292 | Binary HTTP, the inner message/bhttp request/response |
| RFC 9180 | HPKE. The gateway publishes two key configs (see note below). |
| RFC 9576 | Privacy Pass architecture, the issuer's Attester and Issuer roles |
| RFC 9474 | Blind RSA (RSABSSA-SHA384-PSS), the token blind-signature suite |
| RFC 9578 | Privacy Pass Token Type 2, the Apple Private Access Token construction the issuer mints |
The gateway publishes two HPKE key configs:
X25519+Kyber768-draft00 (KEM id 0x30), a draft, non-RFC, post-quantum hybrid of X25519 and Kyber768. Treat it as experimental: it is a draft suite, not an RFC-registered one.DHKEM(X25519, HKDF-SHA256) (KEM id 0x20), the classical RFC 9180 suite (DHKEM(X25519, HKDF-SHA256) / HKDF-SHA256 / AES-128-GCM).A classical-only client picks the legacy config; a client that wants the post-quantum hybrid picks the primary config. Both use HKDF-SHA256 and AES-128-GCM.
Columbia is under the PolyForm Noncommercial License 1.0.0. Personal and non-commercial use is allowed; commercial use is not. The full text is in LICENSE.
The vendored ohttp-gateway/ is Cloudflare's privacy-gateway-server-go under BSD 3-Clause, and its LICENSE is kept unmodified (see ohttp-gateway/VENDORED.md). The PolyForm license covers the parts written here: commons-cache/, ohttp-relay/, and the top-level docs.
token-issuer/README.md