A rewrite proxy that lets Mercurygram receive Telegram's legacy WebPush notifications through a UnifiedPush distributor.
Telegram encrypts WebPush payloads with the pre-RFC aesgcm scheme
(draft-ietf-webpush-encryption-04), which carries the Encryption and
Crypto-Key parameters in HTTP headers. UnifiedPush distributors forward
only the request body and drop those headers, so the device can never decrypt
the payload. This proxy folds the headers into the body before forwarding, so
the client can reconstruct and decrypt the notification.
(RFC 8291 aes128gcm needs no such proxy — it already carries its parameters
in the body. This proxy exists solely for the legacy aesgcm scheme.)
| Method | Path | Description |
|---|---|---|
POST |
/aesgcm?e=<url> |
WebPush: serializes the Encryption/Crypto-Key headers into the body (aesgcm\nEncryption: ...\nCrypto-Key: ...\n<ciphertext>), forwards to the UnifiedPush endpoint, stamps a correlation-cache entry |
PUT |
/<url> |
Simple Push (token_type=4): waits 200 ms for a matching POST; suppresses the wake-up if found (already delivered as an encrypted payload), else forwards the body as a synthetic wake-up |
POST |
/fcm/<token> |
Same folding as /aesgcm, but the destination is FCM and the request is VAPID-signed here (see below) |
PUT |
/fcm/<token> |
Simple Push leg of the FCM route, with the same correlation logic as PUT /<url> |
The correlation window (200 ms wait, 2 s cache age) prevents duplicate wake-ups for regular messages while still letting secret-chat pushes reach the app.
Google Play Services hands an app a plain WebPush endpoint without any Google
library on the client, but FCM only accepts pushes to it that carry a
VAPID authorization, which Telegram
does not send. The /fcm/<token> routes sign on Telegram's behalf: the app
registers https://<proxy>/fcm/<token> as its endpoint and passes the proxy's
VAPID public key to Play Services, binding the subscription to this proxy.
Mint a keypair once:
aesgcm-proxy --generate-vapidPut VAPID_PRIVATE_KEY in the environment of the service and the printed public
key in the client. Leaving the variable unset or empty disables /fcm (503) and
leaves every other route working; a malformed value aborts startup.
FCM caps a WebPush body at 4096 bytes. When the folded payload would exceed that, the proxy sends an empty push instead, so the app still wakes and fetches the message over MTProto rather than losing the notification to a 400.
cargo build --releaseThe default native-tls feature links the system OpenSSL. For a fully static
binary with no OpenSSL dependency, build with rustls:
cargo build --release --no-default-features --features rustlsWithout socket activation it binds 127.0.0.1:8001. Override with LISTEN_ADDR:
LISTEN_ADDR=0.0.0.0:8001 ./target/release/aesgcm-proxyaesgcm-proxy.service + aesgcm-proxy.socket provide a hardened,
socket-activated deployment (the socket is passed via listenfd, so
LISTEN_ADDR is unused). Install the binary to /usr/local/bin/aesgcm-proxy
and enable the socket.
The image is published to ghcr.io/mercurygram/aesgcm-proxy for linux/amd64
and linux/arm64:
podman run --rm -p 8001:8001 ghcr.io/mercurygram/aesgcm-proxy:latestBuild it locally:
podman build -f Containerfile -t aesgcm-proxy .CI rebuilds on every push, on a weekly cron (base-image / crate security updates), and on manual dispatch.
- Non-http/https schemes and URLs with credentials are rejected.
- Literal IP addresses are checked before forwarding (private, loopback, CGNAT, link-local, ULA, NAT64 and similar ranges are blocked).
- Hostnames are filtered by
SafeResolver(a custom reqwest DNS resolver) at connection time — a single resolution feeds both the safety check and the connection, eliminating the TOCTOU gap. Redirects are disabled.
Licensed under either of MIT or
Apache-2.0 at your option. The repository is
REUSE compliant: every file carries an SPDX header
(or is covered by REUSE.toml).