diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..665df9c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,7 @@ +{ + admin off +} + +{$TPROXY_HOSTNAME} { + reverse_proxy proxy:8088 +} diff --git a/README.md b/README.md index 6b9d948..9cbb19a 100644 --- a/README.md +++ b/README.md @@ -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 +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; diff --git a/README.ru.md b/README.ru.md index b6aa510..40cbd95 100644 --- a/README.ru.md +++ b/README.ru.md @@ -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 diff --git a/compose.quick.yml b/compose.quick.yml new file mode 100644 index 0000000..d893f97 --- /dev/null +++ b/compose.quick.yml @@ -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 diff --git a/quick-install.sh b/quick-install.sh new file mode 100755 index 0000000..ea8e219 --- /dev/null +++ b/quick-install.sh @@ -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 </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" <&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 <