Skip to content

mrchatam/tg-relay

Repository files navigation

tg-relay

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

Quick install (global VPS)

One-liner (Docker, default):

curl -fsSL https://raw.githubusercontent.com/mrchatam/tg-relay/main/install.sh | sudo bash

Non-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.sh

Install 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 quick start (manual)

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/health

CLI

After 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

Nginx + Cloudflare

  1. Point global.domain1.com (orange cloud) → global VPS origin IP
  2. TLS terminates at Cloudflare; origin nginx serves HTTP on port 80
  3. Install script adds /etc/nginx/sites-available/tg-relay.conf only — other vhosts are untouched
  4. See deploy/vps/nginx/README.md for manual merge into an existing server {}

Post-deploy hardening: docs/99-REVIEW-AND-HARDEN.md.

CDN topology (VPS primary)

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
Loading

Direct IP — optional origin bypass in nginx/relay config for debugging only (not for Telegram webhook registration).

Architecture

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
Loading

API contract

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: 1
  • X-Relay-Signature: <hex>HMAC-SHA256(relay_secret, raw_body)

Outbound body must include bot_token; the relay strips it before calling Telegram.

Environment variables (Go VPS)

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.

See deploy/vps/env.example.

Build and run (Go VPS)

go build -o tg-relay ./cmd/tg-relay
export RELAY_SECRET=your-secret
export XUIDESK_WEBHOOK_BASE=https://local.domain1.com
./tg-relay

Deploy on global VPS (manual)

Alternative to install.sh:

  1. Build and install binary to /usr/local/bin/tg-relay-bin
  2. Copy deploy/vps/env.example/etc/tg-relay/env and edit
  3. Install deploy/vps/systemd/tg-relay.service (set ExecStart to the binary path)
  4. Configure nginx from deploy/vps/nginx/tg-relay.conf.example
  5. Point CDN global.domain1.com → global VPS origin

Migration from Worker

  1. Deploy Go relay on global VPS
  2. Point CDN global.domain1.com → global VPS
  3. XuiDesk App Settings → relay URL = https://global.domain1.com (not workers.dev)
  4. Ensure XuiDesk PUBLIC_BASE_URL = https://local.domain1.com (CDN inbound)
  5. Run E2E checklist below
  6. Worker deploy remains optional — see deploy/worker/README.md

XuiDesk configuration

  1. App Settings → Telegram transport → Cloudflare Worker relay (or relay after XuiDesk prompt 14)
  2. Set Relay base URL to your CDN URL (e.g. https://global.domain1.com)
  3. Set Relay secret to the same value as RELAY_SECRET
  4. Use Test connectivity (ProbeRelayHealth)
  5. Register bot webhooks — XuiDesk uses {relay_base}/api/v1/in/{publicID} automatically

E2E test checklist

  • curl https://global.domain1.com/api/v1/health
  • XuiDesk settings → Test connectivity (ProbeRelayHealth)
  • Outbound getMe via relay from XuiDesk
  • Register webhook → Telegram → global CDN → relay → local CDN → XuiDesk
  • Fallback: repeat with Arvan subdomains if CF path blocked from Iran

Manual tests (curl)

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"}}'

Tests

go test ./...
npm test

Roadmap

  • 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.

License

Private / use per your deployment policy.

About

realy for xuidesk

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages