feat: add one-question VPS installer
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
admin off
|
||||
}
|
||||
|
||||
{$TPROXY_HOSTNAME} {
|
||||
reverse_proxy proxy:8088
|
||||
}
|
||||
@@ -26,7 +26,9 @@ WebSocket traffic always reach `tproxy-server`.
|
||||
|
||||
## Repository contents
|
||||
|
||||
- `quick-install.sh` — fresh-VPS installation with Docker and automatic HTTPS;
|
||||
- `install.sh` — interactive installation on an existing Docker/NPM host;
|
||||
- `compose.quick.yml` and `Caddyfile` — optional Caddy frontend for the quick path;
|
||||
- `compose.yml` and `Dockerfile` — reproducible, checksum-pinned build;
|
||||
- `ingress/ingress.go` — canonical ingress used by new installations;
|
||||
- `ingress/ingress.js` — equivalent emergency implementation;
|
||||
@@ -37,6 +39,32 @@ WebSocket traffic always reach `tproxy-server`.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick installation on a fresh VPS
|
||||
|
||||
Use a clean x86_64 VPS running Ubuntu or Debian. Create one direct DNS `A`
|
||||
record for the proxy hostname. Copy the HTTPS clone URL from the Gitea **Code**
|
||||
menu, connect to the VPS over SSH, and run:
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y git
|
||||
git clone <REPOSITORY_URL>
|
||||
cd telegram-web-proxy-stack
|
||||
sudo ./quick-install.sh
|
||||
```
|
||||
|
||||
The script asks only for the hostname. It resolves the public IPv4 from DNS,
|
||||
installs Docker from Docker's official package repository, deploys the proxy and
|
||||
Caddy, obtains the TLS certificate, and prints the Telegram connection values.
|
||||
Inbound TCP ports 80 and 443 must be allowed by the VPS provider. This is a
|
||||
separate deployment mode and does not require Nginx Proxy Manager.
|
||||
|
||||
Run `quick-install.sh` again after `git pull --ff-only` to update a quick
|
||||
installation. Do not switch that deployment to `install.sh`, which is intended
|
||||
for the Nginx Proxy Manager mode below.
|
||||
|
||||
### Existing Docker host with Nginx Proxy Manager
|
||||
|
||||
Requirements:
|
||||
|
||||
- x86_64 Linux;
|
||||
|
||||
@@ -9,6 +9,75 @@
|
||||
> Telegram, в котором есть тип прокси `WEB`. Если такого пункта в настройках
|
||||
> нет, установленная версия клиента этот транспорт ещё не поддерживает.
|
||||
|
||||
## Самый простой вариант: чистый VPS и один вопрос
|
||||
|
||||
Этот вариант подходит, если вы только что купили отдельный чистый VPS и не
|
||||
хотите настраивать Nginx Proxy Manager. Скрипт сам установит Docker, запустит
|
||||
прокси вместе с Caddy и получит HTTPS-сертификат. Он спросит только домен.
|
||||
|
||||
Понадобятся:
|
||||
|
||||
- 64-битный VPS `x86_64` с Ubuntu или Debian;
|
||||
- один домен или поддомен с единственной DNS-записью `A`, которая уже указывает
|
||||
прямо на публичный IPv4 этого VPS;
|
||||
- открытые у провайдера сервера входящие TCP-порты `80` и `443`.
|
||||
|
||||
На странице проекта в Gitea нажмите **Code**, скопируйте адрес **HTTPS**, затем
|
||||
подключитесь к VPS по SSH и выполните команды ниже. Вместо текста в угловых
|
||||
скобках вставьте скопированный адрес:
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y git
|
||||
git clone <АДРЕС_РЕПОЗИТОРИЯ>
|
||||
cd telegram-web-proxy-stack
|
||||
sudo ./quick-install.sh
|
||||
```
|
||||
|
||||
После запуска нужно ввести только подготовленный домен. Пароли или токены
|
||||
Gitea для публичного репозитория не требуются.
|
||||
|
||||
Можно случайно вставить домен вместе с `https://` — установщик уберёт схему и
|
||||
лишний путь. После проверки DNS он автоматически:
|
||||
|
||||
1. установит системные компоненты и Docker из официального репозитория;
|
||||
2. соберёт Telegram WEB proxy;
|
||||
3. запустит Caddy и выпустит HTTPS-сертификат;
|
||||
4. покажет сервер, секрет и готовую ссылку подключения.
|
||||
|
||||
Для автоматического запуска домен разрешается передать сразу:
|
||||
|
||||
```sh
|
||||
sudo ./quick-install.sh --domain proxy.example.com
|
||||
```
|
||||
|
||||
Секрет сохраняется в Docker-томе и остаётся прежним при повторном запуске.
|
||||
Для обновления используйте только быстрый установщик:
|
||||
|
||||
```sh
|
||||
git pull --ff-only
|
||||
sudo ./quick-install.sh
|
||||
```
|
||||
|
||||
Если выпуск сертификата не завершился, проверьте DNS и правила входящего
|
||||
трафика у провайдера VPS, затем запустите ту же команду ещё раз. Этот вариант
|
||||
не использует Nginx Proxy Manager и не требует входа в какую-либо панель.
|
||||
|
||||
Остановить быструю установку, не удаляя секрет и сертификат:
|
||||
|
||||
```sh
|
||||
docker compose --env-file .env -f compose.yml -f compose.quick.yml down
|
||||
```
|
||||
|
||||
Повторно запустить:
|
||||
|
||||
```sh
|
||||
docker compose --env-file .env -f compose.yml -f compose.quick.yml up -d
|
||||
```
|
||||
|
||||
Ниже описан второй, отдельный вариант для сервера, где Nginx Proxy Manager уже
|
||||
установлен.
|
||||
|
||||
## Схема работы
|
||||
|
||||
```text
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2.11.7-alpine
|
||||
container_name: tg-web-proxy-caddy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
proxy:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
TPROXY_HOSTNAME: ${TPROXY_HOSTNAME:?set by quick-install.sh}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- tg_webproxy_caddy_data:/data
|
||||
- tg_webproxy_caddy_config:/config
|
||||
networks:
|
||||
- npm_network
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 128
|
||||
mem_limit: 256m
|
||||
cpus: 0.50
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
tg_webproxy_caddy_data:
|
||||
name: tg_webproxy_caddy_data
|
||||
tg_webproxy_caddy_config:
|
||||
name: tg_webproxy_caddy_config
|
||||
Executable
+294
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
domain_value="${TPROXY_HOSTNAME:-}"
|
||||
edge_network="telegram-web-proxy-edge"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Использование: sudo ./quick-install.sh [--domain ДОМЕН]
|
||||
|
||||
Быстрая установка Telegram WEB proxy и Caddy на чистый VPS.
|
||||
Без параметров скрипт спросит только домен. DNS-запись A домена должна уже
|
||||
указывать прямо на публичный IPv4 этого сервера.
|
||||
|
||||
Поддерживаются 64-битные Ubuntu и Debian на архитектуре x86_64.
|
||||
USAGE
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf 'Ошибка: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--domain)
|
||||
(($# >= 2)) || fail "после --domain нужно указать домен"
|
||||
domain_value="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Неизвестный параметр: %s\n\n' "$1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ((EUID != 0)); then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
if [[ -n "$domain_value" ]]; then
|
||||
exec sudo bash "$0" --domain "$domain_value"
|
||||
fi
|
||||
exec sudo bash "$0"
|
||||
fi
|
||||
fail "запустите скрипт от root: sudo ./quick-install.sh"
|
||||
fi
|
||||
|
||||
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
|
||||
fail "требуется 64-битный Linux x86_64"
|
||||
fi
|
||||
|
||||
repository="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
for required_file in compose.yml compose.quick.yml Caddyfile Dockerfile; do
|
||||
[[ -f "$repository/$required_file" ]] ||
|
||||
fail "рядом со скриптом отсутствует $required_file; скачайте весь репозиторий"
|
||||
done
|
||||
|
||||
if [[ ! -r /etc/os-release ]]; then
|
||||
fail "не удалось определить операционную систему"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
case "${ID:-}" in
|
||||
ubuntu | debian) ;;
|
||||
*) fail "поддерживаются только Ubuntu и Debian, получено: ${ID:-неизвестно}" ;;
|
||||
esac
|
||||
[[ -n "${VERSION_CODENAME:-}" ]] || fail "не удалось определить версию системы"
|
||||
|
||||
if [[ -z "$domain_value" ]]; then
|
||||
[[ -t 0 ]] || fail "в автоматическом режиме укажите --domain proxy.example.com"
|
||||
printf 'Введите домен прокси (например, proxy.example.com): '
|
||||
read -r domain_value
|
||||
fi
|
||||
|
||||
domain_value="$(printf '%s' "$domain_value" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed -E 's#^[[:space:]]*https?://##; s#/.*$##; s/:.*$//; s/[[:space:]]//g')"
|
||||
|
||||
valid_hostname() {
|
||||
local value=$1 label
|
||||
local -a labels
|
||||
[[ ${#value} -le 253 && "$value" == *.* ]] || return 1
|
||||
[[ "$value" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1
|
||||
local IFS=.
|
||||
read -r -a labels <<<"$value"
|
||||
for label in "${labels[@]}"; do
|
||||
[[ -n "$label" && ${#label} -le 63 ]] || return 1
|
||||
[[ "$label" != -* && "$label" != *- ]] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
valid_ipv4() {
|
||||
local ip=$1 octet
|
||||
local -a octets
|
||||
local IFS=.
|
||||
read -r -a octets <<<"$ip"
|
||||
[[ ${#octets[@]} -eq 4 ]] || return 1
|
||||
for octet in "${octets[@]}"; do
|
||||
[[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1
|
||||
((10#$octet <= 255)) || return 1
|
||||
done
|
||||
}
|
||||
|
||||
public_ipv4() {
|
||||
local ip=$1
|
||||
local -a octets
|
||||
local IFS=.
|
||||
valid_ipv4 "$ip" || return 1
|
||||
read -r -a octets <<<"$ip"
|
||||
((10#${octets[0]} != 0)) || return 1
|
||||
((10#${octets[0]} != 10)) || return 1
|
||||
((10#${octets[0]} != 127)) || return 1
|
||||
! ((10#${octets[0]} == 100 && 10#${octets[1]} >= 64 && 10#${octets[1]} <= 127)) || return 1
|
||||
! ((10#${octets[0]} == 169 && 10#${octets[1]} == 254)) || return 1
|
||||
! ((10#${octets[0]} == 172 && 10#${octets[1]} >= 16 && 10#${octets[1]} <= 31)) || return 1
|
||||
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 0)) || return 1
|
||||
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 88 && 10#${octets[2]} == 99)) || return 1
|
||||
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 168)) || return 1
|
||||
! ((10#${octets[0]} == 198 && (10#${octets[1]} == 18 || 10#${octets[1]} == 19))) || return 1
|
||||
! ((10#${octets[0]} == 198 && 10#${octets[1]} == 51 && 10#${octets[2]} == 100)) || return 1
|
||||
! ((10#${octets[0]} == 203 && 10#${octets[1]} == 0 && 10#${octets[2]} == 113)) || return 1
|
||||
((10#${octets[0]} < 224)) || return 1
|
||||
}
|
||||
|
||||
valid_hostname "$domain_value" || fail "некорректный домен: $domain_value"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
printf '\n[1/6] Устанавливаю системные компоненты...\n'
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl iproute2
|
||||
|
||||
mapfile -t resolved_ips < <(
|
||||
getent ahostsv4 "$domain_value" 2>/dev/null |
|
||||
awk '{print $1}' | sort -u
|
||||
)
|
||||
if ((${#resolved_ips[@]} == 0)); then
|
||||
fail "домен $domain_value пока не имеет DNS-записи A"
|
||||
fi
|
||||
if ((${#resolved_ips[@]} != 1)); then
|
||||
fail "для простой установки оставьте у домена одну DNS-запись A; найдено: ${resolved_ips[*]}"
|
||||
fi
|
||||
public_ip="${resolved_ips[0]}"
|
||||
public_ipv4 "$public_ip" || fail "DNS домена указывает не на публичный IPv4: $public_ip"
|
||||
printf 'DNS найден: %s -> %s\n' "$domain_value" "$public_ip"
|
||||
|
||||
install_docker() {
|
||||
local -a conflicts=()
|
||||
local package
|
||||
for package in docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc; do
|
||||
if dpkg-query -W -f='${db:Status-Abbrev}' "$package" 2>/dev/null | grep -q '^ii'; then
|
||||
conflicts+=("$package")
|
||||
fi
|
||||
done
|
||||
if ((${#conflicts[@]} > 0)); then
|
||||
fail "обнаружены конфликтующие пакеты (${conflicts[*]}). Используйте чистый VPS или удалите их вручную"
|
||||
fi
|
||||
|
||||
printf '\n[2/6] Устанавливаю Docker из официального репозитория...\n'
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl --fail --silent --show-error --location \
|
||||
--proto '=https' --proto-redir '=https' --tlsv1.2 \
|
||||
"https://download.docker.com/linux/${ID}/gpg" \
|
||||
-o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
cat >/etc/apt/sources.list.d/docker.sources <<SOURCES
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/${ID}
|
||||
Suites: ${VERSION_CODENAME}
|
||||
Components: stable
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
SOURCES
|
||||
apt-get update
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
}
|
||||
|
||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
printf '\n[2/6] Docker и Docker Compose уже установлены.\n'
|
||||
else
|
||||
install_docker
|
||||
fi
|
||||
|
||||
systemctl enable --now docker
|
||||
docker info >/dev/null 2>&1 || fail "Docker установлен, но его служба не запустилась"
|
||||
docker compose version >/dev/null 2>&1 || fail "плагин Docker Compose недоступен"
|
||||
|
||||
own_caddy_running="$(docker inspect --format '{{.State.Running}}' tg-web-proxy-caddy 2>/dev/null || true)"
|
||||
busy_ports="$(ss -H -ltn 2>/dev/null | awk '$4 ~ /:(80|443)$/ {print $4}' | sort -u)"
|
||||
if [[ -n "$busy_ports" && "$own_caddy_running" != true ]]; then
|
||||
fail "порты 80 или 443 уже заняты: ${busy_ports//$'\n'/, }. Для быстрой установки нужен чистый VPS"
|
||||
fi
|
||||
|
||||
printf '\n[3/6] Готовлю закрытую Docker-сеть и настройки...\n'
|
||||
if ! docker network inspect "$edge_network" >/dev/null 2>&1; then
|
||||
docker network create "$edge_network" >/dev/null
|
||||
fi
|
||||
|
||||
environment_file="$repository/.env"
|
||||
temporary_environment="$(mktemp "$repository/.env.new.XXXXXX")"
|
||||
trap 'rm -f "$temporary_environment"' EXIT
|
||||
cat >"$temporary_environment" <<ENV
|
||||
TPROXY_HOSTNAME=$domain_value
|
||||
MTPROXY_PUBLIC_IP=$public_ip
|
||||
NPM_NETWORK_NAME=$edge_network
|
||||
MTPROXY_WORKERS=1
|
||||
MTPROXY_MAX_CONNECTIONS=4096
|
||||
ENV
|
||||
chmod 0600 "$temporary_environment"
|
||||
mv -f "$temporary_environment" "$environment_file"
|
||||
trap - EXIT
|
||||
|
||||
compose=(
|
||||
docker compose
|
||||
--env-file "$environment_file"
|
||||
-f "$repository/compose.yml"
|
||||
-f "$repository/compose.quick.yml"
|
||||
)
|
||||
|
||||
printf '\n[4/6] Собираю Telegram WEB proxy...\n'
|
||||
"${compose[@]}" config --quiet
|
||||
"${compose[@]}" build --pull
|
||||
"${compose[@]}" pull caddy
|
||||
|
||||
printf '\n[5/6] Запускаю прокси и автоматический HTTPS...\n'
|
||||
"${compose[@]}" up -d --remove-orphans
|
||||
|
||||
proxy_health=
|
||||
for _ in $(seq 1 90); do
|
||||
proxy_health="$(docker inspect \
|
||||
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
|
||||
tg-web-proxy 2>/dev/null || true)"
|
||||
[[ "$proxy_health" == healthy ]] && break
|
||||
if [[ "$proxy_health" == unhealthy || "$proxy_health" == exited || "$proxy_health" == dead ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$proxy_health" != healthy ]]; then
|
||||
docker logs --tail 100 tg-web-proxy >&2 || true
|
||||
fail "контейнер прокси не запустился (состояние: ${proxy_health:-неизвестно})"
|
||||
fi
|
||||
|
||||
client_secret="$(docker exec tg-web-proxy sh -c 'tr -d "\\r\\n" < /data/client-secret')"
|
||||
[[ "$client_secret" =~ ^[0-9a-f]{32}$ ]] || fail "не удалось прочитать клиентский секрет"
|
||||
|
||||
printf '\n[6/6] Жду выпуска HTTPS-сертификата'
|
||||
https_ready=false
|
||||
for _ in $(seq 1 60); do
|
||||
if curl --fail --silent --show-error \
|
||||
--connect-timeout 5 --max-time 15 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"https://$domain_value/" >/dev/null 2>&1; then
|
||||
https_ready=true
|
||||
break
|
||||
fi
|
||||
printf '.'
|
||||
sleep 5
|
||||
done
|
||||
printf '\n'
|
||||
|
||||
if [[ "$https_ready" != true ]]; then
|
||||
docker logs --tail 100 tg-web-proxy-caddy >&2 || true
|
||||
cat >&2 <<ERROR
|
||||
Ошибка: HTTPS не стал доступен.
|
||||
|
||||
Проверьте, что DNS-запись A указывает на этот VPS и что у провайдера сервера
|
||||
разрешены входящие TCP-порты 80 и 443. После исправления снова запустите:
|
||||
sudo ./quick-install.sh --domain $domain_value
|
||||
ERROR
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat <<RESULT
|
||||
|
||||
Готово: Telegram WEB proxy работает через HTTPS.
|
||||
|
||||
Параметры для Telegram:
|
||||
Тип: WEB
|
||||
Сервер: $domain_value
|
||||
Секрет: $client_secret
|
||||
|
||||
Ссылка для подключения:
|
||||
tg://webproxy?server=$domain_value&secret=$client_secret
|
||||
|
||||
Не публикуйте секрет и ссылку подключения. Для обновления проекта выполните
|
||||
git pull --ff-only, затем снова запустите sudo ./quick-install.sh.
|
||||
RESULT
|
||||
@@ -52,7 +52,8 @@ scan_network_literals() {
|
||||
[[ -z "$domain" ]] && continue
|
||||
domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')"
|
||||
case "$domain" in
|
||||
github.com | core.telegram.org | example.com | *.example.com | *.localhost | \
|
||||
github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \
|
||||
example.com | *.example.com | *.localhost | \
|
||||
*.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \
|
||||
*.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;;
|
||||
*) report "unexpected hostname $domain in $context$file" ;;
|
||||
@@ -139,7 +140,8 @@ while IFS= read -r revision; do
|
||||
[[ -z "$domain" ]] && continue
|
||||
domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')"
|
||||
case "$domain" in
|
||||
github.com | core.telegram.org | example.com | *.example.com | *.localhost | \
|
||||
github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \
|
||||
example.com | *.example.com | *.localhost | \
|
||||
*.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \
|
||||
*.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;;
|
||||
*) report "unexpected hostname $domain in Git history ${revision:0:12}" ;;
|
||||
|
||||
Reference in New Issue
Block a user