Forward-only Telegram proxy for XuiDesk. Sits between XuiDesk and api.telegram.org, with HMAC authentication on the XuiDesk ↔ relay leg.
Deploy targets (same /api/v1/* contract):
| Target | Status | Notes |
|---|---|---|
| Global VPS + CDN | Recommended | Docker (install.sh) or native Go binary |
| Cloudflare Worker | Optional fallback | Same routes in src/ — see deploy/worker/README.md |
One-liner (Docker, default):
curl -fsSL https://raw.githubusercontent.com/mrchatam/tg-relay/main/install.sh | sudo bashNon-interactive example:
sudo NON_INTERACTIVE=1 \
RELAY_SECRET="$(openssl rand -hex 32)" \
XUIDESK_WEBHOOK_BASE=https://local.domain1.com \
RELAY_PUBLIC_URL=https://global.domain1.com \
SETUP_NGINX=1 \
RELAY_DOMAIN=global.domain1.com \
DEPLOY_MODE=docker \
bash install.shInstall directory: /opt/tg-relay (override with TG_RELAY_INSTALL_DIR). Relay binds localhost only (default 127.0.0.1:8080; the installer prompts for LISTEN_PORT if 8080 is taken). Nginx on the same VPS proxies your Cloudflare subdomain to that port.
docker pull ghcr.io/mrchatam/tg-relay:latest
cp deploy/vps/env.example .env # edit RELAY_SECRET, XUIDESK_WEBHOOK_BASE, RELAY_PUBLIC_URL
docker compose up -d
curl -sS http://127.0.0.1:8080/api/v1/healthAfter install, manage from anywhere:
tg-relay # interactive menu
tg-relay update # pull latest GHCR image and recreate
tg-relay menu # same as bare tg-relay
tg-relay health | logs | restart | status | refresh-installer | help- Point
global.domain1.com(orange cloud) → global VPS origin IP - TLS terminates at Cloudflare; origin nginx serves HTTP on port 80
- Install script adds
/etc/nginx/sites-available/tg-relay.confonly — other vhosts are untouched - See deploy/vps/nginx/README.md for manual merge into an existing
server {}
Post-deploy hardening: docs/99-REVIEW-AND-HARDEN.md.
Both Iran VPS (XuiDesk) and Global VPS (tg-relay) sit behind CDN. Not all four subdomains are required at once — configure per path.
| Subdomain | CDN | Origin | Role |
|---|---|---|---|
global.domain1.com |
Cloudflare | Global VPS | Relay public URL — Telegram webhooks + XuiDesk outbound |
local.domain1.com |
Cloudflare | Iran VPS | XuiDesk inbound — relay forwards here |
global.domain2.ir |
Arvancloud | Global VPS | Relay fallback / Iran→global path |
local.domain2.ir |
Arvancloud | Iran VPS | XuiDesk fallback inbound |
flowchart LR
TG[Telegram]
CFg[global.domain1.com CF]
CFl[local.domain1.com CF]
Relay[Global VPS tg-relay]
XD[Iran VPS XuiDesk]
TG -->|webhook| CFg --> Relay
Relay -->|POST /telegram/webhook| CFl --> XD
XD -->|POST /api/v1/out| CFg --> Relay
Relay -->|HTTPS| TG
Direct IP — optional origin bypass in nginx/relay config for debugging only (not for Telegram webhook registration).
sequenceDiagram
participant XD as XuiDesk
participant R as tg-relay
participant TG as api.telegram.org
XD->>R: POST /api/v1/out/{method}
R->>TG: POST /bot{token}/{method}
TG-->>R: JSON
R-->>XD: pass-through
TG->>R: POST /api/v1/in/{botPublicID}
R->>XD: POST /telegram/webhook/{publicID}
XD-->>R: 200
R-->>TG: pass-through
| Route | Direction | Auth |
|---|---|---|
POST /api/v1/out/{method} |
XuiDesk → Telegram | X-Relay-Signature (HMAC-SHA256 of raw body) |
POST /api/v1/in/{botPublicID} |
Telegram → XuiDesk | Relay signs body when forwarding to XuiDesk |
GET /api/v1/health |
Health probe | None |
Headers on authenticated routes:
X-Relay-API-Version: 1X-Relay-Signature: <hex>—HMAC-SHA256(relay_secret, raw_body)
Outbound body must include bot_token; the relay strips it before calling Telegram.
| Variable | Required | Description |
|---|---|---|
RELAY_SECRET |
yes | Shared HMAC key (must match XuiDesk App Settings relay secret) |
XUIDESK_WEBHOOK_BASE |
yes* | XuiDesk inbound CDN URL (no trailing slash), e.g. https://local.domain1.com |
XUIDESK_WEBHOOK_BASE_FALLBACK |
no | Retry inbound forward on connection error or 5xx (e.g. Arvan path) |
RELAY_PUBLIC_URL |
no | Public relay URL for docs / webhook registration (https://global.domain1.com) |
LISTEN_ADDR |
no | Listen address (default :8080) |
TELEGRAM_API_BASE |
no | Override Telegram API base (default https://api.telegram.org/bot) |
* Also accepts XUIDESK_BASE_URL (Worker alias) if XUIDESK_WEBHOOK_BASE is unset.
go build -o tg-relay ./cmd/tg-relay
export RELAY_SECRET=your-secret
export XUIDESK_WEBHOOK_BASE=https://local.domain1.com
./tg-relayAlternative to install.sh:
- Build and install binary to
/usr/local/bin/tg-relay-bin - Copy deploy/vps/env.example →
/etc/tg-relay/envand edit - Install deploy/vps/systemd/tg-relay.service (set
ExecStartto the binary path) - Configure nginx from deploy/vps/nginx/tg-relay.conf.example
- Point CDN
global.domain1.com→ global VPS origin
- Deploy Go relay on global VPS
- Point CDN
global.domain1.com→ global VPS - XuiDesk App Settings → relay URL =
https://global.domain1.com(not workers.dev) - Ensure XuiDesk
PUBLIC_BASE_URL=https://local.domain1.com(CDN inbound) - Run E2E checklist below
- Worker deploy remains optional — see deploy/worker/README.md
- App Settings → Telegram transport → Cloudflare Worker relay (or
relayafter XuiDesk prompt 14) - Set Relay base URL to your CDN URL (e.g.
https://global.domain1.com) - Set Relay secret to the same value as
RELAY_SECRET - Use Test connectivity (
ProbeRelayHealth) - Register bot webhooks — XuiDesk uses
{relay_base}/api/v1/in/{publicID}automatically
-
curl https://global.domain1.com/api/v1/health - XuiDesk settings → Test connectivity (
ProbeRelayHealth) - Outbound
getMevia relay from XuiDesk - Register webhook → Telegram → global CDN → relay → local CDN → XuiDesk
- Fallback: repeat with Arvan subdomains if CF path blocked from Iran
export RELAY_BASE="https://global.domain1.com"
export RELAY_SECRET="your-secret"Health
curl -sS "$RELAY_BASE/api/v1/health"Outbound (mirrors XuiDesk workerTransport)
BODY='{"bot_token":"BOT_TOKEN","chat_id":123,"text":"hi"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$RELAY_SECRET" | awk '{print $2}')
curl -sS -X POST "$RELAY_BASE/api/v1/out/sendMessage" \
-H 'Content-Type: application/json' \
-H 'X-Relay-API-Version: 1' \
-H "X-Relay-Signature: $SIG" \
-d "$BODY"Inbound (simulate Telegram → relay)
curl -sS -X POST "$RELAY_BASE/api/v1/in/BOT_PUBLIC_ID" \
-H 'Content-Type: application/json' \
-H 'X-Telegram-Bot-Api-Secret-Token: WEBHOOK_SECRET' \
-d '{"update_id":1,"message":{"text":"hi"}}'go test ./...
npm test- Phase VPS (primary) — Go binary on global VPS + CDN (this repo)
- Phase Worker (optional) — Cloudflare Worker in
src/ - Phase B — Request counters, latency, error rate
- Phase C — Admin GUI at
/admin(easier on VPS) - Phase D — Log ring buffer
Post-deploy: run docs/99-REVIEW-AND-HARDEN.md.
Private / use per your deployment policy.