Ночной деплой с iPhone — как перезапустил VPN в 3 ночи
Когда сервер падает в 3 ночи, а под рукой только iPhone
3:17 AM. Уведомление в Telegram: "VPN Server DOWN". Я лежу в кровати, половина России спит, а моя инфраструктура решила устроить себе внеплановый отпуск. Под рукой только iPhone 13 Pro и твёрдое намерение не вставать к ноутбуку.
Спойлер: за 47 минут удалось полностью восстановить VPN-сервер, перезапустить все сервисы и даже пофиксить конфиг, который косячил уже неделю. Всё это — с экрана 6.1 дюйма.
Анатомия катастрофы
Мой setup: VPS в Варшаве (4 CPU, 8GB RAM, Ubuntu 22.04), на нём крутится Xray с VLESS+REALITY. Управление через Telegram-бота на aiogram 3.7, который мониторит статус и может выполнять базовые команды. Плюс классический мониторинг через Uptime Kuma.
В 3:17 прилетает уведомление: "🔴 VPN Server is DOWN". Открываю Telegram, пробую команду /status — бот молчит. Плохо. Очень плохо.
Первая мысль: "Может, просто перезагрузка?" Но нет, через 10 минут статус тот же. Сервер либо завис намертво, либо что-то с сетью. В любом случае, нужно лезть по SSH.
Инструменты для мобильного хаоса
На iPhone у меня установлен Termius — лучший SSH-клиент для iOS. Платный, но $10/месяц за возможность деплоить с дивана — это копейки.
Открываю Termius, подключаюсь к серверу:
ssh root@vpn-server.example.com -p 22
Первое, что бросается в глаза — экран крошечный. В Termius можно настроить шрифт, но даже на минимальном размере помещается максимум 80 символов в ширину и 25 строк. Это не vim на 27-дюймовом мониторе, это мобильная хирургия.
Но подключение прошло успешно. Сервер жив, SSH отвечает. Значит, проблема в сервисах.
Диагностика в темноте
Первым делом — общий статус системы:
# Проверяем загрузку
htop
На iPhone экран htop выглядит как иероглифы, но основное видно: CPU загружен на 2%, RAM свободно 4.2GB из 8GB. Сервер не перегружен.
# Проверяем сервисы
systemctl status xray
systemctl status nginx
systemctl status telegram-bot
И тут начинается веселье. Xray — active (running), nginx — active (running), а telegram-bot — failed. Первый косяк найден.
# Смотрим что случилось с ботом
journalctl -u telegram-bot --since "2 hours ago"
Лог показывает:
Dec 15 03:12:47 vpn-server systemd[1]: telegram-bot.service: Main process exited, code=killed, signal=KILL
Dec 15 03:12:47 vpn-server systemd[1]: telegram-bot.service: Failed with result 'signal'.
Бот был убит системой. Скорее всего, OOM Killer сработал, хотя памяти вроде достаточно.
Реанимация по частям
Сначала перезапускаем бота:
systemctl restart telegram-bot
systemctl status telegram-bot
Статус: active (running). Проверяю в Telegram — /status отвечает. Но основная проблема остаётся: VPN всё ещё не работает.
Проверяем Xray подробнее:
# Статус процесса
ps aux | grep xray
# Порты
netstat -tlnp | grep :443
netstat -tlnp | grep :80
Процесс висит, порты слушаются. Но что-то не так. Смотрим логи Xray:
tail -f /var/log/xray/error.log
И тут я вижу проблему:
2023/12/15 03:11:23 [Warning] failed to handler mux client connection > proxy/vless/outbound: failed to find an available destination > transport/internet/reality: x509: certificate signed by unknown authority
Сертификаты Reality протухли. Классика жанра.
Мобильный vim — это боль
Нужно править конфиг Xray. На десктопе это 30 секунд, на iPhone — квест на выживание.
vim /etc/xray/config.json
Vim на мобильном экране — это отдельный вид пытки. Курсор прыгает, экранная клавиатура закрывает половину экрана, а привычные хоткеи работают через раз.
Но есть лайфхак: в Termius можно включить "Hardware Keyboard Support" и подключить внешнюю клавиатуру. У меня её нет, но есть другой трюк — использовать nano вместо vim:
nano /etc/xray/config.json
Nano на мобильном — гораздо более предсказуем. Ищу секцию с Reality настройками:
{
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "www.microsoft.com:443",
"serverNames": ["www.microsoft.com"],
"privateKey": "old_expired_key_here",
"shortIds": ["abcd1234"]
}
}
}
Проблема в dest и privateKey. Microsoft поменял сертификаты, и Reality больше не может маскироваться под их сайт.
Генерим новые ключи с iPhone
Нужно сгенерировать новую пару ключей для Reality. Обычно для этого есть специальная утилита, но на сервере её нет. Придётся использовать openssl:
# Генерируем приватный ключ
openssl genpkey -algorithm X25519 -out /tmp/private.key
# Извлекаем публичный ключ
openssl pkey -in /tmp/private.key -pubout -out /tmp/public.key
# Конвертируем в base64 для Reality
openssl pkey -in /tmp/private.key -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' \n:' | xxd -r -p | base64
Команда выдаёт новый приватный ключ в base64. Копирую его (на iPhone это тоже квест — нужно выделить текст пальцем, надеясь не промазать).
Обновляю конфиг:
{
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "www.cloudflare.com:443",
"serverNames": ["www.cloudflare.com"],
"privateKey": "новый_ключ_тут",
"shortIds": ["ef012345"]
}
}
}
Поменял dest на Cloudflare — их сертификаты стабильнее, и они реже меняют конфигурацию SSL.
Перезапуск и тестирование
Сохраняю конфиг (Ctrl+X, Y, Enter в nano), перезапускаю Xray:
systemctl restart xray
systemctl status xray
Статус: active (running). Проверяю логи:
tail -f /var/log/xray/access.log
Ошибок нет. Теперь нужно обновить клиентские конфиги. У меня есть скрипт, который генерирует новые ссылки:
/opt/scripts/generate_client_configs.sh
Скрипт выдаёт новые VLESS-ссылки с обновлёнными ключами. Копирую одну из них и тестирую подключение прямо с iPhone.
В iOS есть несколько VPN-клиентов, поддерживающих VLESS. Использую FoXray — бесплатный и работает стабильно.
Вставляю новую ссылку, подключаюсь — работает! Пинг до Google 45ms, скорость нормальная.
Автоматизация для следующего раза
Пока я исправлял проблему, в голове уже крутился план: как сделать так, чтобы в следующий раз это можно было пофиксить одной командой.
Создаю скрипт автоматического восстановления:
nano /opt/scripts/emergency_fix.sh
#!/bin/bash
# Автоматическое восстановление VPN-сервера
# Для использования в экстренных ситуациях
LOG_FILE="/var/log/emergency_fix.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE
}
log "Starting emergency VPN fix..."
# Проверяем статус сервисов
log "Checking services status..."
systemctl is-active --quiet xray || {
log "Xray is down, restarting..."
systemctl restart xray
}
systemctl is-active --quiet nginx || {
log "Nginx is down, restarting..."
systemctl restart nginx
}
systemctl is-active --quiet telegram-bot || {
log "Telegram bot is down, restarting..."
systemctl restart telegram-bot
}
# Проверяем Reality ключи
log "Checking Reality configuration..."
if ! xray -test -config /etc/xray/config.json; then
log "Config test failed, regenerating Reality keys..."
# Бэкап старого конфига
cp /etc/xray/config.json /etc/xray/config.json.backup.$(date +%s)
# Генерируем новые ключи
NEW_PRIVATE=$(openssl genpkey -algorithm X25519 | openssl pkey -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' \n:' | xxd -r -p | base64)
# Обновляем конфиг
sed -i "s/\"privateKey\": \".*\"/\"privateKey\": \"$NEW_PRIVATE\"/" /etc/xray/config.json
sed -i 's/"dest": ".*"/"dest": "www.cloudflare.com:443"/' /etc/xray/config.json
sed -i 's/"serverNames": \[".*"\]/"serverNames": ["www.cloudflare.com"]/' /etc/xray/config.json
# Перезапускаем с новым конфигом
systemctl restart xray
log "Reality keys regenerated and Xray restarted"
fi
# Генерируем новые клиентские конфиги
log "Regenerating client configs..."
/opt/scripts/generate_client_configs.sh > /tmp/new_configs.txt
# Отправляем уведомление в Telegram
log "Sending notification..."
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d chat_id="$ADMIN_CHAT_ID" \
-d text="🟢 VPN Server recovered automatically at $(date)"
log "Emergency fix completed successfully"
Делаю скрипт исполняемым:
chmod +x /opt/scripts/emergency_fix.sh
Теперь в следующий раз достаточно будет запустить одну команду:
/opt/scripts/emergency_fix.sh
Настройка Telegram-бота для экстренных случаев
Обновляю бота, добавляю команду экстренного восстановления:
nano /opt/telegram-bot/main.py
import asyncio
import subprocess
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
import logging
# Версия aiogram 3.7
bot = Bot(token="YOUR_BOT_TOKEN")
dp = Dispatcher()
@dp.message(Command("emergency_fix"))
async def emergency_fix(message: types.Message):
"""Экстренное восстановление VPN-сервера"""
if message.from_user.id != ADMIN_USER_ID:
await message.answer("❌ Access denied")
return
await message.answer("🔧 Starting emergency fix...")
try:
# Запускаем скрипт восстановления
result = subprocess.run(
['/opt/scripts/emergency_fix.sh'],
capture_output=True,
text=True,
timeout=300 # 5 минут максимум
)
if result.returncode == 0:
await message.answer(f"✅ Emergency fix completed!\n\n```\n{result.stdout[-1000:]}\n```", parse_mode="Markdown")
else:
await message.answer(f"❌ Emergency fix failed!\n\n```\n{result.stderr[-1000:]}\n```", parse_mode="Markdown")
except subprocess.TimeoutExpired:
await message.answer("⏰ Emergency fix timed out")
except Exception as e:
await message.answer(f"💥 Error: {str(e)}")
@dp.message(Command("status"))
async def status(message: types.Message):
"""Проверка статуса сервисов"""
services = ['xray', 'nginx', 'telegram-bot']
status_text = "📊 Services Status:\n\n"
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service], capture_output=True, text=True)
status = "🟢 Active" if result.stdout.strip() == "active" else "🔴 Inactive"
status_text += f"{service}: {status}\n"
except:
status_text += f"{service}: ❓ Unknown\n"
# Добавляем информацию о нагрузке
try:
load_result = subprocess.run(['uptime'], capture_output=True, text=True)
status_text += f"\n💻 Load: {load_result.stdout.strip()}"
except:
pass
await message.answer(status_text)
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
Перезапускаю бота:
systemctl restart telegram-bot
Тестирую новую команду — /emergency_fix работает, /status показывает, что все сервисы в порядке.
Мобильный набор для экстренного деплоя
За время этого ночного приключения я понял, что нужно подготовить "аварийный чемоданчик" для мобильного администрирования.
Приложения для iOS:
- Termius ($10/мес) — лучший SSH-клиент
- Working Copy ($20) — Git-клиент для iOS, поддерживает SSH-ключи
- FoXray (бесплатно) — VPN-клиент для тестирования VLESS
- Telegram — для управления через бота
- 1Password — для хранения SSH-ключей и паролей
Настройки Termius для мобильного деплоя:
# В настройках Termius:
- Font Size: 10pt (минимальный читаемый)
- Theme: Dark (меньше батарейки ночью)
- Hardware Keyboard: включено
- SSH Key Agent: включено
- Auto-lock: 30 минут
SSH-ключи на мобильном:
Главная проблема — как безопасно хранить SSH-ключи на iPhone. Termius поддерживает импорт ключей из 1Password, что очень удобно:
- Генерируем ключ на десктопе:
ssh-keygen -t ed25519 -C "mobile-emergency-key"
- Добавляем публичный ключ на сервер:
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
-
Приватный ключ сохраняем в 1Password как Secure Note
-
В Termius импортируем ключ из 1Password
Шпаргалка команд для экстренных случаев:
Создаю файл с часто используемыми командами:
# /opt/mobile-cheat-sheet.txt
# === EMERGENCY COMMANDS ===
# Services restart
systemctl restart xray nginx telegram-bot
# Check status
systemctl status xray
journalctl -u xray --since "1 hour ago"
# Test config
xray -test -config /etc/xray/config.json
# Emergency fix
/opt/scripts/emergency_fix.sh
# Generate new Reality keys
openssl genpkey -algorithm X25519 | openssl pkey -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' \n:' | xxd -r -p | base64
# Check connections
netstat -tlnp | grep :443
ss -tlnp | grep :443
# Disk space
df -h
du -sh /var/log/*
# Memory usage
free -h
ps aux --sort=-%mem | head
# Network test
curl -I https://www.google.com
ping -c 4 8.8.8.8
Результаты ночного марафона
4:04 AM — всё работает. Время восстановления: 47 минут. Не рекорд, но для работы с iPhone — вполне достойно.
Что было исправлено:
- Перезапущен упавший Telegram-бот
- Обновлены Reality-ключи для Xray
- Изменён dest-сервер с Microsoft на Cloudflare
- Созданы скрипты автоматического восстановления
- Добавлена команда /emergency_fix в бота
Метрики производительности после фикса: - Ping через VPN: 45ms (было 120ms) - Скорость загрузки: 85 Mbps (было 12 Mbps) - Стабильность соединения: 99.8% uptime
Выводы
Деплой с iPhone — это реально. Больше того, при правильной подготовке это может быть даже удобно для экстренных случаев.
Что я понял за эту ночь:
-
Termius стоит своих денег. $10/месяц — копейки за возможность чинить серверы с дивана. Встроенный SFTP, синхронизация между устройствами, поддержка SSH-ключей — всё работает.
-
Nano лучше vim на мобильном. Vim на 6-дюймовом экране — это мазохизм. Nano предсказуем, понятен, и работает без танцев с бубном.
-
Автоматизация решает. Скрипт
/opt/scripts/emergency_fix.shтеперь может решить 80% проблем одной командой. Следующая цель — запуск через Telegram-бота. -
Шпаргалка обязательна. В 3 ночи мозг работает на 30%. Файл с готовыми командами спасает время и нервы.
-
Reality требует внимания. Это не "поставил и забыл". Сертификаты меняются, dest-серверы обновляют конфигурации. Нужен мониторинг и автоматическое обновление ключей.
Планы на будущее:
- Настроить автоматическое обновление Reality-ключей раз в месяц
- Добавить в бота команды для просмотра логов и метрик
- Создать Ansible-плейбук для полного восстановления сервера
- Настроить алерты в Telegram при падении любого сервиса
Мобильный набор админа 2024:
- iPhone с Termius
- SSH-ключи в 1Password
- Telegram-бот для управления
- Шпаргалка с командами
- Скрипты автоматического восстановления
В следующий раз, когда в 3 ночи что-то упадёт, я буду готов. А пока — спать. Завтра нужно на работу, анализировать кровь людей, которые тоже не спали всю ночь, но по другим причинам.