295 lines
10 KiB
Bash
Executable File
295 lines
10 KiB
Bash
Executable File
#!/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
|