Files
2026-08-29 13:06:55 +04:00

220 lines
9.1 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
# One-command installer for Telegram's experimental WEB proxy server.
# Intended for a clean, dedicated Ubuntu 22.04+/Debian 12+ x86_64 VPS.
readonly TPROXY_COMMIT="52a5feb7fac38f68da5afef9cedd9b3bfc8473ca"
readonly TPROXY_ARCHIVE_URL="https://github.com/telegramdesktop/tproxy-server/archive/${TPROXY_COMMIT}.tar.gz"
readonly RESULT_FILE="/root/telegram-web-proxy-access.txt"
HOSTNAME_ARG=""
EMAIL=""
WORK_DIR=""
say() { printf '\n==> %s\n' "$*"; }
die() { printf '\nERROR: %s\n' "$*" >&2; exit 1; }
cleanup() {
if [[ -n "${WORK_DIR}" && -d "${WORK_DIR}" ]]; then
rm -rf -- "${WORK_DIR}"
fi
}
trap cleanup EXIT
trap 'printf "\nInstallation stopped at line %s.\n" "$LINENO" >&2' ERR
usage() {
cat <<'EOF'
Usage:
sudo bash install.sh [--hostname proxy.example.com] [--email admin@example.com]
Options:
--hostname HOST Dedicated lowercase DNS name pointed to this VPS.
--email EMAIL ACME/Let's Encrypt contact email.
-h, --help Show this help.
If hostname or email is omitted, the installer asks interactively.
The WEB proxy secret is generated locally and never placed in shell history.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--hostname) HOSTNAME_ARG="${2:-}"; shift 2 ;;
--email) EMAIL="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1" ;;
esac
done
[[ "${EUID}" -eq 0 ]] || die "Run this script as root (sudo bash install.sh)."
[[ "$(uname -m)" == "x86_64" ]] || die "The official MTProxy build currently requires x86_64."
[[ -r /etc/os-release ]] || die "Cannot identify the operating system."
# shellcheck disable=SC1091
source /etc/os-release
OS_MAJOR="${VERSION_ID%%.*}"
[[ "${OS_MAJOR}" =~ ^[0-9]+$ ]] || die "Cannot parse OS version: ${VERSION_ID:-unknown}."
if [[ "${ID:-}" == "ubuntu" ]]; then
(( OS_MAJOR >= 22 )) || die "Ubuntu 22.04 or newer is required."
elif [[ "${ID:-}" == "debian" ]]; then
(( OS_MAJOR >= 12 )) || die "Debian 12 or newer is required."
else
die "Supported systems: Ubuntu 22.04+ or Debian 12+ (detected ${PRETTY_NAME:-unknown})."
fi
if [[ -z "${HOSTNAME_ARG}" ]]; then
read -r -p "Proxy hostname (for example proxy.example.com): " HOSTNAME_ARG
fi
HOSTNAME_ARG="${HOSTNAME_ARG,,}"
HOSTNAME_ARG="${HOSTNAME_ARG%.}"
if [[ ! "${HOSTNAME_ARG}" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ || "${HOSTNAME_ARG}" != *.* ]]; then
die "Hostname must be a lowercase ASCII DNS name, for example proxy.example.com."
fi
[[ "${HOSTNAME_ARG}" != *..* ]] || die "Hostname contains an empty DNS label."
IFS='.' read -r -a DNS_LABELS <<<"${HOSTNAME_ARG}"
for label in "${DNS_LABELS[@]}"; do
[[ ${#label} -le 63 && "${label}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] ||
die "Invalid DNS label in hostname: ${label}"
done
if [[ -z "${EMAIL}" ]]; then
read -r -p "Email for HTTPS certificate notices: " EMAIL
fi
[[ "${EMAIL}" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || die "Invalid email address."
command -v curl >/dev/null 2>&1 || {
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates
}
command -v openssl >/dev/null 2>&1 || {
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssl
}
say "Checking DNS and the VPS"
PUBLIC_IPV4="$(curl --fail --silent --show-error --max-time 10 -4 https://api.ipify.org || true)"
[[ "${PUBLIC_IPV4}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || die "Could not determine this VPS public IPv4."
DNS_IPV4S="$(getent ahostsv4 "${HOSTNAME_ARG}" 2>/dev/null | awk '{print $1}' | sort -u || true)"
[[ -n "${DNS_IPV4S}" ]] || die "${HOSTNAME_ARG} has no visible A record yet."
if ! grep -Fxq "${PUBLIC_IPV4}" <<<"${DNS_IPV4S}"; then
printf 'Public VPS IPv4: %s\nDNS currently returns:\n%s\n' "${PUBLIC_IPV4}" "${DNS_IPV4S}" >&2
die "Point the hostname A record to this VPS and wait for DNS propagation."
fi
if command -v ss >/dev/null 2>&1; then
OCCUPIED="$(ss -H -lnt '( sport = :80 or sport = :443 )' 2>/dev/null || true)"
if [[ -n "${OCCUPIED}" ]]; then
printf '%s\n' "${OCCUPIED}" >&2
die "Ports 80/443 are already in use. Use a clean VPS or integrate manually; the official installer replaces Caddy configuration."
fi
fi
if [[ -s /etc/caddy/Caddyfile ]]; then
die "An existing Caddy configuration was found. This safe installer will not overwrite it."
fi
say "Downloading the pinned official tproxy-server source"
WORK_DIR="$(mktemp -d /tmp/telegram-web-proxy.XXXXXXXX)"
ARCHIVE="${WORK_DIR}/tproxy-server.tar.gz"
curl --fail --silent --show-error --location \
--proto '=https' --proto-redir '=https' --tlsv1.2 \
--output "${ARCHIVE}" "${TPROXY_ARCHIVE_URL}"
tar -tzf "${ARCHIVE}" >/dev/null
tar -xzf "${ARCHIVE}" -C "${WORK_DIR}"
SOURCE_DIR="${WORK_DIR}/tproxy-server-${TPROXY_COMMIT}"
[[ -x "${SOURCE_DIR}/deploy/install.sh" ]] || die "Official installer is missing from the downloaded archive."
say "Creating a unique public cover site"
SITE_DIR="${WORK_DIR}/site"
mkdir -p "${SITE_DIR}"
SITE_ID="$(openssl rand -hex 6)"
cat >"${SITE_DIR}/index.html" <<EOF
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Northwind Status</title>
<link rel="stylesheet" href="/styles.css">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
</head>
<body><main><p class="tag">SERVICE NODE ${SITE_ID}</p><h1>Systems operational</h1><p>This endpoint provides encrypted application transport and status information.</p><a href="/about">Service information</a></main></body>
</html>
EOF
cat >"${SITE_DIR}/about.html" <<EOF
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Service information</title><link rel="stylesheet" href="/styles.css"></head><body><main><p class="tag">NODE ${SITE_ID}</p><h1>Service information</h1><p>This privately operated endpoint is monitored for availability.</p><a href="/">Return to status</a></main></body></html>
EOF
cat >"${SITE_DIR}/404.html" <<EOF
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Not found</title><link rel="stylesheet" href="/styles.css"></head><body><main><p class="tag">404</p><h1>Page not found</h1><a href="/">Return to status</a></main></body></html>
EOF
cat >"${SITE_DIR}/styles.css" <<'EOF'
:root{color-scheme:dark;font-family:ui-sans-serif,system-ui,sans-serif;background:#07111f;color:#e8f0f8}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 20% 20%,#173b5c 0,transparent 35%),#07111f}main{width:min(680px,calc(100% - 40px));padding:48px;border:1px solid #294158;border-radius:20px;background:#0b1827cc;box-shadow:0 28px 80px #0008}h1{font-size:clamp(2rem,7vw,4rem);letter-spacing:-.04em;margin:.2em 0}p{line-height:1.7;color:#a9bbcb}.tag{font-size:.75rem;letter-spacing:.18em;color:#61d3a5}a{color:#7ed7ff}
EOF
cat >"${SITE_DIR}/favicon.svg" <<EOF
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#0b1827"/><path d="M17 34l10 10 21-25" fill="none" stroke="#61d3a5" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/></svg>
EOF
SECRET="$(openssl rand -hex 16)"
say "Installing Caddy, the relay and official MTProxy"
printf '%s\n' "${SECRET}" | "${SOURCE_DIR}/deploy/install.sh" \
--hostname "${HOSTNAME_ARG}" \
--email "${EMAIL}" \
--site-dir "${SITE_DIR}"
say "Verifying services and HTTPS"
for unit in caddy tproxy-firewall mtproxy tproxy-server; do
systemctl is-active --quiet "${unit}" || {
systemctl --no-pager --full status "${unit}" || true
die "Service ${unit} is not running."
}
done
curl --fail --silent --show-error --max-time 10 http://127.0.0.1:8081/healthz >/dev/null
curl --fail --silent --show-error --max-time 10 http://127.0.0.1:8081/readyz >/dev/null
HTTPS_OK=0
for _ in 1 2 3 4 5 6; do
if curl --fail --silent --show-error --location --max-time 15 "https://${HOSTNAME_ARG}/" >/dev/null; then
HTTPS_OK=1
break
fi
sleep 5
done
[[ "${HTTPS_OK}" -eq 1 ]] || die "Services are running, but public HTTPS verification failed. Check DNS, provider firewall and Caddy logs."
WEB_LINK="tg://webproxy?server=${HOSTNAME_ARG}&secret=${SECRET}"
HTTPS_LINK="https://t.me/webproxy?server=${HOSTNAME_ARG}&secret=${SECRET}"
cat >"${RESULT_FILE}" <<EOF
Telegram WEB proxy
Created: $(date --iso-8601=seconds)
Hostname: ${HOSTNAME_ARG}
Secret: ${SECRET}
Desktop link: ${WEB_LINK}
HTTPS-form link: ${HTTPS_LINK}
Public cover site: https://${HOSTNAME_ARG}/
Pinned tproxy-server commit: ${TPROXY_COMMIT}
EOF
chmod 0600 "${RESULT_FILE}"
cat <<EOF
============================================================
Telegram WEB proxy is ready.
Hostname : ${HOSTNAME_ARG}
Secret : ${SECRET}
Open in Telegram Desktop:
${WEB_LINK}
Alternative link form:
${HTTPS_LINK}
Saved root-only at: ${RESULT_FILE}
Public site: https://${HOSTNAME_ARG}/
============================================================
Important: WEB proxy support is experimental. Use a compatible
Telegram Desktop build; normal mobile releases may not support it.
EOF