Files
amnezia-bot/awg/newclient.sh
T
viktor 11b52cf5ad Обновление зависимостей, устранение хардкода, оплата через Telegram
Библиотеки:
- миграция с aiogram 2.25.2 (EOL) на aiogram 3.31: aiohttp обновился
  с 3.8.6 до 3.14.3, так как aiogram 2 жёстко требовал aiohttp<3.9
- самодельный словарь состояний заменён на FSM aiogram (MemoryStorage);
  это убирает падение на user_state.startswith(), когда состояния нет
- из requirements убраны неиспользуемые пакеты (Babel 2.9.1, humanize,
  aiofiles, yookassa, YooMoney); оставлены только прямые зависимости

Хардкод:
- пути вычисляются от расположения файлов бота (awg/config.py), больше
  нет привязки к /root/amnezia-bot и к запуску из конкретного каталога
- вместо 'python3.11' используется sys.executable; awg-decode.py
  переименован в awg_decode.py и импортируется напрямую вместо запуска
  через subprocess
- newclient.sh и removeclient.sh определяют каталог по своему
  расположению, а не по cwd
- install.sh: каталог, репозиторий, ветка и Python настраиваются
  переменными окружения, зависимости ставятся из requirements.txt
- секреты можно задавать переменными окружения вместо config.json

Оплата (встроенные платежи Telegram, провайдер ЮKassa):
- счёт, pre_checkout_query и successful_payment; публичный HTTPS-адрес
  и вебхук не нужны
- обработчик платежей вынесен в отдельный роутер и подключён первым:
  successful_payment приходит без текста и иначе перехватывался бы
  обработчиком, ожидающим ввода в FSM
- выдача ключа идемпотентна по id платежа, повторная доставка апдейта
  не создаёт второй ключ
- если после оплаты ключ создать не удалось, уведомляются администраторы
- промокоды разделены на validate/consume: использование засчитывается
  только после фактической выдачи ключа, а не при проверке кода
- поддержаны скидочные промокоды, уменьшающие сумму счёта
- опциональные чеки по 54-ФЗ через provider_data

Исправленные ошибки:
- db.root_add и db.deactive_user_db передавали скриптам 2 аргумента
  вместо 4, из-за чего создание и удаление клиентов всегда падало;
  публичный ключ для удаления теперь вычисляется из конфига клиента
- в create_backup не был импортирован zipfile (NameError при бэкапе)
- планировщик запускался без задач, подписки никогда не истекали;
  добавлена ежечасная проверка с отключением доступа и уведомлением
- callback_data вида renew_period_<имя>_<период> разбирался неверно для
  имён с подчёркиваниями; разделителем стало двоеточие
- статус клиента брался из status.json, который никто не создавал;
  теперь используется wg show latest-handshakes
- продление подписки сбрасывало остаток, теперь период добавляется к нему
- Markdown-разметка ломалась на именах с подчёркиваниями; используется
  HTML с экранированием
- vpn:// ключ уходит отдельным сообщением, а не в подписи к файлу,
  где мог не поместиться в лимит 1024 символа
- блокирующие вызовы docker и скриптов вынесены в отдельные потоки
- при удалении клиента связь с Telegram ID удаляется, а не пишется None

Удалён мёртвый код: awg/wg.py (нигде не импортировался, содержал
подсеть 10.0.0.0/24 вместо используемой 10.8.1.0/24) и
handlers/add_client.py (импортировал несуществующий модуль loader).
2026-09-04 14:44:26 +00:00

123 строки
3.5 KiB
Bash
Исполняемый файл

#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "Error: CLIENT_NAME argument is not provided"
exit 1
fi
if [ -z "$2" ]; then
echo "Error: ENDPOINT argument is not provided"
exit 1
fi
if [ -z "$3" ]; then
echo "Error: WG_CONFIG_FILE argument is not provided"
exit 1
fi
if [ -z "$4" ]; then
echo "Error: DOCKER_CONTAINER argument is not provided"
exit 1
fi
CLIENT_NAME="$1"
ENDPOINT="$2"
WG_CONFIG_FILE="$3"
DOCKER_CONTAINER="$4"
if [[ ! "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Invalid CLIENT_NAME. Only letters, numbers, underscores, and hyphens are allowed."
exit 1
fi
# Каталог скрипта, а не текущий каталог: бот может быть запущен откуда угодно.
pwd="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$pwd/users/$CLIENT_NAME"
mkdir -p "$pwd/files"
key=$(docker exec -i $DOCKER_CONTAINER wg genkey)
psk=$(docker exec -i $DOCKER_CONTAINER wg genpsk)
SERVER_CONF_PATH="$pwd/files/server.conf"
docker exec -i $DOCKER_CONTAINER cat $WG_CONFIG_FILE > "$SERVER_CONF_PATH"
SERVER_PRIVATE_KEY=$(awk '/^PrivateKey\s*=/ {print $3}' "$SERVER_CONF_PATH")
SERVER_PUBLIC_KEY=$(echo "$SERVER_PRIVATE_KEY" | docker exec -i $DOCKER_CONTAINER wg pubkey)
LISTEN_PORT=$(awk '/ListenPort\s*=/ {print $3}' "$SERVER_CONF_PATH")
ADDITIONAL_PARAMS=$(awk '/^Jc\s*=|^Jmin\s*=|^Jmax\s*=|^S1\s*=|^S2\s*=|^H[1-4]\s*=/' "$SERVER_CONF_PATH")
octet=2
while grep -E "AllowedIPs\s*=\s*10\.8\.1\.$octet/32" "$SERVER_CONF_PATH" > /dev/null; do
(( octet++ ))
done
if [ "$octet" -gt 254 ]; then
echo "Error: WireGuard internal subnet 10.8.1.0/24 is full"
exit 1
fi
CLIENT_IP="10.8.1.$octet/32"
ALLOWED_IPS="$CLIENT_IP"
CLIENT_PUBLIC_KEY=$(echo "$key" | docker exec -i $DOCKER_CONTAINER wg pubkey)
cat << EOF >> "$SERVER_CONF_PATH"
[Peer]
# $CLIENT_NAME
PublicKey = $CLIENT_PUBLIC_KEY
PresharedKey = $psk
AllowedIPs = $ALLOWED_IPS
EOF
docker cp "$SERVER_CONF_PATH" $DOCKER_CONTAINER:$WG_CONFIG_FILE
docker exec -i $DOCKER_CONTAINER sh -c "wg-quick down $WG_CONFIG_FILE && wg-quick up $WG_CONFIG_FILE"
cat << EOF > "$pwd/users/$CLIENT_NAME/$CLIENT_NAME.conf"
[Interface]
Address = $CLIENT_IP
DNS = 1.1.1.1, 1.0.0.1
PrivateKey = $key
$ADDITIONAL_PARAMS
[Peer]
PublicKey = $SERVER_PUBLIC_KEY
PresharedKey = $psk
AllowedIPs = 0.0.0.0/0
Endpoint = $ENDPOINT:$LISTEN_PORT
PersistentKeepalive = 25
EOF
CLIENTS_TABLE_PATH="$pwd/files/clientsTable"
docker exec -i $DOCKER_CONTAINER cat /opt/amnezia/awg/clientsTable > "$CLIENTS_TABLE_PATH" || echo "[]" > "$CLIENTS_TABLE_PATH"
CREATION_DATE=$(date)
if [ -f "$CLIENTS_TABLE_PATH" ]; then
jq --arg clientId "$CLIENT_PUBLIC_KEY" \
--arg clientName "$CLIENT_NAME" \
--arg creationDate "$CREATION_DATE" \
'. += [{"clientId": $clientId, "userData": {"clientName": $clientName, "creationDate": $creationDate}}]' \
"$CLIENTS_TABLE_PATH" > "$CLIENTS_TABLE_PATH.tmp"
mv "$CLIENTS_TABLE_PATH.tmp" "$CLIENTS_TABLE_PATH"
else
jq -n --arg clientId "$CLIENT_PUBLIC_KEY" \
--arg clientName "$CLIENT_NAME" \
--arg creationDate "$CREATION_DATE" \
'[{"clientId": $clientId, "userData": {"clientName": $clientName, "creationDate": $creationDate}}]' \
> "$CLIENTS_TABLE_PATH"
fi
docker cp "$CLIENTS_TABLE_PATH" $DOCKER_CONTAINER:/opt/amnezia/awg/clientsTable
traffic_file="$pwd/users/$CLIENT_NAME/traffic.json"
echo '{
"total_incoming": 0,
"total_outgoing": 0,
"last_incoming": 0,
"last_outgoing": 0
}' > "$traffic_file"
echo "Client $CLIENT_NAME successfully added to WireGuard"