Compare commits

8 Commits
Author SHA1 Message Date
gilfanovrandClaude Opus 5.5 3604e843c8 ru1: рестарт networkd снёс fwmark-правило умного роутинга — починено и защищено
После apt-get из установки WEB-прокси needrestart перезапустил
systemd-networkd, тот удалил ip rule fwmark 0x64 -> vpn: ~45 мин
зарубежный трафик клиентов OpenVPN #1 шёл через RU IP. Правило
возвращено, networkd запрещено трогать чужие правила и маршруты.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:21:42 +03:00
gilfanovrandClaude Opus 5.5 2ab38bc1f8 ru1: WEB-прокси Telegram вместо telemt
На ru.ruzzy.dev:443 поднят официальный tproxy-server (relay, carrier
websocket) перед официальным MTProxy на loopback. Фронт — nginx в
~/gost, сертификат только на ru.ruzzy.dev.

- MTProxy: --nat-info 10.8.1.4:161.97.93.252 вручную — трафик к
  Telegram выходит через wg0 с IP fr1, автоопределение install.sh
  ошиблось бы и прокси молча не отвечал бы.
- relay на 8090/8091: 8080 занят cadvisor.
- telemt удалён окончательно (контейнер, образ, конфиг, том).
- certbot до этого не работал вовсе: сертификат был просрочен с
  2026-07-01, продлён и запущен.

stacks/tproxy/: compose и nginx.conf стека ~/gost, setup-ru1.sh —
однократная root-установка relay и MTProxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:16:12 +03:00
gilfanovrandClaude Opus 5.5 123cdaa19b ru1: ключ claude-bot@ruzzy.dev в authorized_keys mik
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 16:28:06 +03:00
gilfanovrandClaude Opus 5 11531ceb16 Указатели на контракт — на самом сервере
Контракт в репозитории достаёт только агентов, у которых этот репозиторий
есть. Astra работает на fr1 как tgclaude и infra у неё не склонирован, так
что до неё не дошло бы ничего.

Заведено на сервере:
  ~rus/CLAUDE.md и ~tgclaude/CLAUDE.md — указатель на репо, правило и пять
  граблей, стоивших больше всего; AGENTS.md рядом симлинком, для агентов с
  другой конвенцией. Claude Code читает CLAUDE.md вверх по дереву, поэтому
  файл в хоме работает и при работе в ~tgclaude/imgbot.

  ~rus/MyVault/TOOLS.md, раздел SSH — то же для Astra. Файл смонтирован в
  claude-channels как /vault, изменение видно агенту сразу, в Gitea уезжает
  кроном каждые 5 минут.

MOTD и .bashrc сознательно не использованы: .bashrc выходит на строке 6 для
неинтерактивных шеллов, MOTD показывается только при интерактивном входе, а
агенты обычно дёргают `ssh host 'команда'` и не увидят ни того, ни другого.

Ограничения механизма и причина, по которой детектор дрейфа не заведён,
описаны в servers/fr1.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 17:04:08 +03:00
gilfanovrandClaude Opus 5 b56c137b5c Механизм поддержания описаний в актуальном состоянии
Сверка fr1 показала, что дока расходится с фактом по десятку пунктов —
значит нужен не разовый проход, а контракт и инструмент.

  CLAUDE.md — правило «тронул сервер, обнови servers/<хост>.md в том же
  прогоне» плюс стиль: факт + причина + что сломается молча. Лежит именно
  в CLAUDE.md, потому что AGENTS.md автоматически агенту не подгружается.

  .claude/skills/server-audit/ — метод сверки и накопленные грабли: где
  дока врёт чаще всего, почему ufw не отвечает на вопрос об открытых
  портах, почему нужен up -d --dry-run, почему «executed successfully»
  не доказывает работу задания.

  scripts/snapshot.sh — инвентаризация живого состояния за один проход:
  только чтение, без sudo, секретов не печатает, недоступное под root
  помечает и подсказывает команду.

Первый же прогон скрипта нашёл две вещи, обе описаны:

  - IPv6 не «починился», а мигает: за один день FAILED -> REACHABLE ->
    FAILED без единой правки конфига. Описан как мигающий, с выводом
    ничего на него не завязывать.
  - В основном sshd_config стоит PermitRootLogin yes (правка шаблона
    Contabo). Root-вход закрыт только потому, что Include дроп-инов
    стоит выше, а sshd берёт первое значение. Уберёшь дроп-ин —
    включится.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 16:52:22 +03:00
gilfanovrandClaude Opus 5 4308c5c7e1 fr1: сверка описания с живым сервером
Дока расходилась с фактом по десятку пунктов. Что исправлено по
результатам обхода сервера:

  - ядро 6.8.0-137 (было 134); IPv6 заработал — шлюз отвечает на NDP,
    хотя конфиг не менялся, так что может отвалиться обратно
  - Gitea 1.27.3, 1.9 ГБ, 20 репо (было 1.25.4 / 4.3 ГБ / 16)
  - апекс ruzzy.dev уже на fr1; появились vhost'ы share и www
  - мониторинг шире описанного: таргеты ru1 и job gitea, дашборды
    OpenVPN и Gitea; убраны устаревшие упоминания выведенного de1
  - vault-синк claude-channels работает (дока говорила обратное), том
    claude-home раздулся заново до 2.8 ГБ

Добавлены разделы, которых не было вовсе:
  - tgclaude — общий вход для агентов; цепочка Telegram-бот → агент в
    контейнере → SSH → группа docker → root на хосте
  - imgbot и его переезд в контейнеры
  - расписания: полная инвентаризация cron и таймеров
  - открытые хвосты, включая непогашенные токены в Gitea

Разобрано и описано попутно: ufw не управляет docker-портами (ipcam
месяцами держал 2221 и 30000-30010 наружу без единого правила);
sysstat хранил историю 7 дней при HISTORY=7 — поднято до 365 (при >28
sa1 сам переключает имена на saYYYYMMDD, иначе смысла нет); /etc/cron.
hourly/free час за часом выбрасывал страничный кэш (до -3.89 ГиБ,
подтверждено по Prometheus) — удалён.

Уборка диска: 104 → 52 ГБ занято. Ушли ipcam, sc2-parser, booty,
booty-collector, vacuum-scraper, ollama (8.1 ГБ), легаси-архив
картинок, docker build cache (43.4 ГБ).

В AGENTS.md поправлены две неверные строки: de1 значился действующей
прод-нодой, хотя выведен, а туннель ru1 — идущим до de1. Сами
servers/de1.md и servers/ru1.md не сверялись.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:26:40 +03:00
gilfanovrandClaude Opus 5 a65d6a249e imgbot: стек из двух сервисов вместо процессов на хосте
Проект агента (~tgclaude/imgbot) работал тремя голыми python-процессами,
запущенными руками из SSH-сессии: без юнита, без restart-политики, при
Linger=no. Держались только на KillUserProcesses=no и не пережили бы
перезагрузку, причём молча.

Один образ на базе playwright/python:v1.62.0-noble (версия пакета
playwright обязана совпадать с тегом, иначе разъезжается ревизия
chromium), два сервиса:
  - imgbot-daemon — APScheduler: refresh 30 мин, analyze 5 мин
  - imgbot-http   — объединил прежние mcp-http (8765) и file-server
    (8766): MCP как под-приложение Starlette плюс маршрут /photo/<id>

Неочевидное, из-за чего это ломается молча (в compose есть комментарии):
  - данные монтируются в тот же путь, что на хосте: images.file_path в
    SQLite абсолютный, и его читают обе стороны
  - корней хранения может быть больше одного — проверять все префиксы
    file_path, а не только data_dir
  - HOME обязателен: collector.py вызывает Path.home(), а uid 1001 не
    имеет записи в /etc/passwd образа
  - у MCP своя защита от DNS-rebinding: без IMGBOT_ALLOWED_HOSTS отдаёт
    421 на любой Host кроме localhost

Порт не публикуется на хост, сервис доступен из общей docker-сети с
claude-channels — агент наконец ходит по MCP, а не по SSH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:26:16 +03:00
gilfanovrandClaude Opus 5 bf4708b775 ipcam выведен; пин Gitea приведён к рабочей 1.27.3
ipcam/FTPS-приёмник удалён с fr1 целиком: контейнер, образ, том, сеть,
каталоги /srv/ipcam и /srv/ftps, deploy-хук certbot и сертификат
cam.ruzzy.dev. Видео так и не начали складываться — на момент удаления
в /srv/ipcam было 0 файлов.

Пин образа Gitea в стеке держал 1.25.4, тогда как работает 1.27.3
(обновлён 2026-08-31). Любой `docker compose up -d` откатил бы сервис
на две минорные версии назад.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:26:00 +03:00
15 changed files with 876 additions and 64 deletions
+74
View File
@@ -0,0 +1,74 @@
---
name: server-audit
description: Сверить фактическое состояние сервера с его описанием в servers/*.md и обновить описание. Использовать после любых работ на сервере (поднял/погасил сервис, поменял конфиг, удалил каталог, завёл ключ), а также когда просят проверить, соответствует ли дока реальности, актуализировать описание хоста или разобраться, что на сервере вообще происходит.
---
# Сверка описания сервера с фактом
Цель — чтобы `servers/<хост>.md` описывал сервер таким, какой он есть, и
чтобы по нему можно было принимать решения. Правила оформления — в
`CLAUDE.md` репозитория, здесь метод и грабли.
## Порядок
1. **Собери факт до чтения доки.** Сначала `./scripts/snapshot.sh <хост>`,
потом читай описание. Наоборот — начнёшь подгонять наблюдения под текст.
2. **Сверяй по пунктам, не по впечатлению.** Прогоняй утверждения доки
через факт по одному: версии, размеры, пути, порты, «включено/выключено».
При сверке fr1 2026-09-08 неверными оказались десять утверждений, включая
«IPv6 не работает» про работавший IPv6 и версию Gitea на два минора назад.
3. **Добери то, что требует root** — snapshot это помечает. Неинтерактивный
sudo не проходит: нужен `ssh -t <хост> 'sudo ...'`, то есть попроси
пользователя выполнить, а не пытайся обойти.
4. **Правь описание в том же прогоне.** Не откладывай на «потом опишу».
5. **Незакрытое — в «Открытые хвосты»** файла хоста. Токен, который не
удалось отозвать; права, которые не поправил; решение, которое за
пользователя не принимаешь.
## Где дока врёт чаще всего
- **Версии и размеры** — меняются молча при обновлении контейнера. Смотри
фактический тег у запущенного контейнера, а не запись в compose: они
расходятся, и тогда `docker compose up -d` откатывает сервис.
- **«Не работает»** — могло починиться на стороне провайдера. И наоборот:
IPv6 на fr1 за один день был `FAILED`, потом `REACHABLE`, потом снова
`FAILED` — такое надо описывать как мигающее, а не как состояние.
- **Открытые порты.** `ufw status` не отвечает на вопрос «что торчит
наружу»: docker публикует порты в iptables напрямую, минуя ufw. Только
`ss -tlnH` + `docker ps`. Непубликуемый порт контейнера всё равно
доступен с хоста по адресу контейнера — «внутренняя сеть» не периметр.
- **Процессы вне контейнеров.** `docker ps` их не покажет, а они не
переживут перезагрузку. Ищи `ps` с `PPID=1` и чужими пользователями.
- **Кроны других пользователей** — в `docker ps` и в `crontab -l` их нет.
Осиротевшее задание может годами падать молча: MTA не установлен, вывод
крона теряется.
- **Планы, записанные как факт.** «Планируется перенести DNS» при уже
перенесённом DNS. Проверяй раздел «планируется» отдельно.
## Грабли, которые стоили времени (не повторяй)
- **Пути в БД абсолютные.** Прежде чем контейнеризовать сервис, проверь
**все** префиксы путей в его базе, а не только основной каталог данных:
`select substr(file_path,1,30), count(*) from images group by 1;`
Монтируй в тот же путь, что на хосте, иначе часть данных пропадёт молча.
- **`up -d --dry-run` перед любым compose-действием.** Он показывает, что
compose собирается сделать, — например поднять забытый контейнер того же
проекта и словить конфликт портов.
- **Задание может глушить свои ошибки.** У imgbot `analyze` логирует
исключение и возвращается нормально, поэтому планировщик пишет
`executed successfully` после падения. Отсутствие ошибок не доказывает
работу — проверяй результат, а не статус.
- **Ретенция.** journald был на 500M (11 дней), sysstat на `HISTORY=7`.
Сервер, где разбирают аварии, обязан хранить историю дольше, чем живёт
проблема. У sysstat при `HISTORY>28` имена переключаются на
`saYYYYMMDD` — иначе поднятие числа бессмысленно.
- **Группа `docker` = root.** Пользователь без sudo, но в группе docker,
монтирует `/` в контейнер и делает что угодно. Учитывай при описании
доступа.
## Проверка результата
Перед отчётом прогони по получившемуся файлу: каждое ⚠️ подтверждено
наблюдением? у каждого изменения указана дата и прежнее значение? не
попали ли в текст секреты? если роль хоста изменилась — поправлена ли
строка в `AGENTS.md`?
+14 -3
View File
@@ -3,8 +3,19 @@
Описание состояния серверов.
## Серверы
- [`servers/de1.md`](servers/de1.md) — `ruzzy.dev` (213.136.74.247). Действующая прод-нода: прокси/VPN (telemt, AmneziaWG, OpenVPN), gitea, openclaw, боты. Источник для переноса.
- [`servers/fr1.md`](servers/fr1.md) — `fr1.ruzzy.dev` (161.97.93.252). Новая чистая нода (Ubuntu 24.04). Мониторинг + gitea.
- [`servers/ru1.md`](servers/ru1.md) — `85.198.109.86` (VDSina, РФ). VPN entry-нода (OpenVPN ×2, AmneziaWG до de1, WireGuard клиентов, telemt), умный роутинг RU/зарубеж.
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан.
- [`servers/fr1.md`](servers/fr1.md) — `fr1.ruzzy.dev` (161.97.93.252). Основная нода (Ubuntu 24.04): мониторинг, gitea, AmneziaWG exit для ru1, боты (claude-channels, openclaw), статика `www`/`share`. Держит апекс `ruzzy.dev`.
- [`servers/ru1.md`](servers/ru1.md) — `85.198.109.86` (VDSina, РФ). VPN entry-нода (OpenVPN ×2, AmneziaWG **до fr1** после переключения, WireGuard клиентов), умный роутинг RU/зарубеж; WEB-прокси Telegram (tproxy-server + MTProxy) на `ru.ruzzy.dev:443`, telemt удалён 2026-10-09. ⚠️ Файл описания сверялся до переключения.
## Поддержание описаний в актуальном состоянии
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
правила оформления — в [`CLAUDE.md`](CLAUDE.md), метод сверки и накопленные
грабли — в скилле [`server-audit`](.claude/skills/server-audit/SKILL.md),
инвентаризация живого состояния одной командой:
./scripts/snapshot.sh rus-161
Скрипт только читает, sudo не требует и секретов не печатает; то, что
доступно лишь под root, он помечает и подсказывает команду.
Секреты (пароли, токены) в репозитории не хранятся — только на серверах.
+49
View File
@@ -0,0 +1,49 @@
# Работа с этим репозиторием
Здесь описано состояние серверов: один файл на хост в `servers/`, стеки
docker-compose в `stacks/`, индекс в `AGENTS.md`.
## Главное правило
**Тронул сервер — обнови `servers/<хост>.md` в том же прогоне, до отчёта
пользователю.** Не «потом», не «если попросят». Расхождение описания с
фактом опаснее отсутствия описания: по доке принимают решения, и неверная
строка стоит дороже пустой.
Это относится к любому изменению состояния: поднял или погасил сервис,
поменял конфиг, удалил каталог, завёл ключ, отключил юнит. Если менял
что-то в `stacks/`, проверь, что описание сервиса в `servers/` ему не
противоречит.
## Перед тем как что-то описывать — проверь
Доке не верь, сверяй с живым сервером. При последней сверке (2026-09-08)
десяток утверждений в `servers/fr1.md` оказались неверными: версии,
размеры, «не работает» про то, что уже работало.
Подробный чеклист и метод — в скилле `server-audit`
(`.claude/skills/server-audit/`), инвентаризация одной командой —
`scripts/snapshot.sh <ssh-хост>`.
## Как писать
Стиль этого репо — не список фактов, а факт плюс причина и грабли:
- **Почему так, а не иначе.** «`node-exporter` в host-network» бесполезно
без «иначе видел бы только сеть docker-бриджа».
- **Что сломается молча.** Помечай `⚠️`. Такие места — главная ценность
файла: `ufw` не управляет docker-портами; путь монтирования обязан
совпадать с абсолютными путями в БД; `HISTORY=7` у sysstat хранит неделю.
- **Дату и прежнее значение** при изменении: «поднято до 365 (было 7)».
- **Незакрытое — в «Открытые хвосты»**, а не в никуда. Если оставил
токен, задачу или сомнительные права — запиши, иначе это потеряется.
- **Секретов здесь нет.** Токены, пароли, ключи — только на сервере.
Ссылаться на путь к секрету можно, содержимое — нельзя.
- Роль хоста поменялась — поправь и строку в `AGENTS.md`.
## Чего не делать
- Не описывай планы как факт. «Планируется» и «сделано» — разные разделы.
- Не удаляй ⚠️-заметку, потому что «выглядит устаревшей». Сначала проверь
на сервере, потом переписывай, указав дату проверки.
- Не коммить в `main` напрямую, если пользователь не просил.
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Инвентаризация живого состояния сервера — только чтение, sudo не требуется.
#
# Смысл: сверка описания с фактом стоит дорого не из-за писанины, а из-за
# выяснения, что на сервере вообще происходит. Этот скрипт собирает всё, что
# понадобилось при сверке fr1 2026-09-08, за один проход.
#
# ./scripts/snapshot.sh rus-161 # по ssh
# ./scripts/snapshot.sh rus-161 > /tmp/fr1.txt
#
# Секретов не печатает: содержимое .env, токенов и ключей не читается —
# только факт наличия файла и права.
#
# Часть данных требует root и будет помечена как недоступная: правила ufw,
# сертификаты, /var/log/syslog, кроны других пользователей. Их надо
# добирать вручную через `ssh -t <хост> sudo ...`.
set -uo pipefail
HOST="${1:-}"
[ -z "$HOST" ] && { echo "usage: $0 <ssh-host>" >&2; exit 2; }
ssh -o BatchMode=yes "$HOST" 'bash -s' <<'REMOTE'
set +e
h() { printf "\n\n===== %s =====\n" "$1"; }
h "СИСТЕМА"
uname -srm; . /etc/os-release && echo "$PRETTY_NAME"
echo "hostname: $(hostname) / $(hostname -f 2>/dev/null)"
uptime; who -b
echo "vCPU: $(nproc)"; free -h | head -2; df -h / | tail -1
ls /var/run/reboot-required 2>/dev/null && echo "ТРЕБУЕТСЯ ПЕРЕЗАГРУЗКА"
h "ПОЛЬЗОВАТЕЛИ И ДОСТУП"
getent passwd | awk -F: '$3>=1000 && $3<65000 {print $1" uid="$3" shell="$7}'
echo "группа docker (= root-эквивалент): $(getent group docker)"
echo "группа sudo: $(getent group sudo)"
grep -hE "^(PermitRootLogin|PasswordAuthentication|AllowUsers|MaxAuthTries)" \
/etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null | sort -u
h "СЕТЬ"
ip -br addr | grep -vE "^(veth|br-|docker)"
ip route | head -5
ip -6 route | grep default
echo "-- NDP шлюза (FAILED = IPv6 не работает) --"; ip -6 neigh | grep -E "fe80::1 |router"
resolvectl status 2>/dev/null | grep -A1 "Current DNS"
h "ЧТО СЛУШАЕТ НАРУЖУ (а не ufw status!)"
# ufw НЕ управляет docker-портами: docker пишет их в iptables напрямую,
# поэтому единственный честный ответ даёт ss + docker ps.
ss -tlnH | awk '{print $4}' | sort -u
echo "-- udp --"; ss -ulnH | awk '{print $4}' | sort -u
h "DOCKER"
docker --version 2>/dev/null; docker compose version 2>/dev/null
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null
echo; docker system df 2>/dev/null
echo "-- откуда запущен каждый контейнер (compose-проект и файл) --"
for c in $(docker ps -aq 2>/dev/null); do
docker inspect "$c" --format '{{.Name}} <- {{index .Config.Labels "com.docker.compose.project.config_files"}}'
done
echo "-- монтирования (host -> container) --"
for c in $(docker ps -aq 2>/dev/null); do
docker inspect "$c" --format '{{.Name}}:{{range .Mounts}} {{.Source}}->{{.Destination}}{{end}}'
done
echo "-- тома и их размер --"
for v in $(docker volume ls -q 2>/dev/null); do
printf "%s %s\n" "$(docker run --rm -v "$v":/v alpine du -sh /v 2>/dev/null | cut -f1)" "$v"
done
echo "-- сироты (dangling) --"; docker volume ls -qf dangling=true 2>/dev/null
h "ПРОЦЕССЫ ВНЕ КОНТЕЙНЕРОВ И ВНЕ SYSTEMD"
# Такие процессы не переживут перезагрузку и не видны cAdvisor.
ps -eo user:12,pid,ppid,etime:12,%cpu,rss,args --sort=-rss \
| grep -vE "\[|containerd-shim|/usr/lib/systemd|^USER" | head -25
h "РАСПИСАНИЯ"
echo "-- /etc/cron.d --"; for f in /etc/cron.d/*; do [ -f "$f" ] && { echo "# $f"; grep -vE '^#|^$|^SHELL|^PATH' "$f"; }; done
for d in hourly daily weekly monthly; do echo "-- cron.$d: $(ls /etc/cron.$d 2>/dev/null | tr '\n' ' ')"; done
echo "-- crontab текущего пользователя --"; crontab -l 2>/dev/null | grep -vE '^#|^$'
echo "-- кроны остальных пользователей: нужен root --"
sudo -n ls /var/spool/cron/crontabs 2>/dev/null || echo " (недоступно: ssh -t <хост> 'sudo ls /var/spool/cron/crontabs')"
echo "-- таймеры --"; systemctl list-timers --all --no-pager 2>/dev/null | head -20
h "NGINX / TLS"
nginx -v 2>&1; ls -l /etc/nginx/sites-enabled/ 2>/dev/null
grep -hE "server_name|proxy_pass|root |listen " /etc/nginx/sites-available/*.conf 2>/dev/null | sed 's/^ *//' | sort -u
echo "-- сертификаты: нужен root --"
sudo -n ls /etc/letsencrypt/live 2>/dev/null || echo " (недоступно)"
h "ЛОГИ И ИСТОРИЯ (для будущих разборов)"
cat /etc/systemd/journald.conf.d/*.conf 2>/dev/null | grep -vE '^#|^$'
journalctl --disk-usage 2>/dev/null | tail -1
echo "-- sysstat: глубина истории --"
grep -E '^HISTORY|^COMPRESSAFTER' /etc/sysstat/sysstat 2>/dev/null
ls /var/log/sysstat 2>/dev/null | head -3
echo " (файлы saDD = максимум месяц; saYYYYMMDD = HISTORY>28)"
h "ОБНОВЛЕНИЯ"
pro status --format json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print("Ubuntu Pro:", d["account"]["name"]); [print(" ",s["name"],s["status"]) for s in d["services"] if s["status"]=="enabled"]' 2>/dev/null
grep -hE "Update-Package-Lists|Unattended-Upgrade " /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null
snap list 2>/dev/null | tail -n +2 | awk '{print " snap:",$1,$2}'
snap refresh --time 2>/dev/null | head -2
h "ЧЕГО ЗДЕСЬ НЕТ — ДОБРАТЬ ВРУЧНУЮ ПОД ROOT"
cat <<'HINT'
ssh -t <хост> "sudo bash -c '
ufw status verbose
certbot certificates | grep -E \"Certificate Name|Expiry\"
ls /var/spool/cron/crontabs && for f in /var/spool/cron/crontabs/*; do echo \$f; grep -v ^# \$f; done
awg show 2>/dev/null | head -20
grep -c suspended /var/log/syslog
'"
HINT
REMOTE
+185 -16
View File
@@ -3,18 +3,22 @@
Снимок состояния сервера. Секреты (пароли/токены) — только на сервере, не здесь.
**Провайдер:** Contabo VPS · France / Lauterbourg (Grand Est)
**ОС:** Ubuntu 24.04 LTS · kernel 6.8.0-106-generic
**ОС:** Ubuntu 24.04.4 LTS · kernel 6.8.0-137-generic (с 2026-08-07; ранее 6.8.0-134 с 2026-07-31, до того 6.8.0-106)
**Ресурсы:** 8 vCPU / 23 GB RAM / 387 GB
**hostname:** `fr1` (FQDN `fr1.ruzzy.dev` через `/etc/hosts`)
---
## Пользователи и доступ
- Пользователь `rus` (uid 1000, группы `rus`, `sudo`, `users`), sudo по паролю.
- Пользователь `rus` (uid 1000, группы `rus`, `sudo`, `users`, `docker`, `tgclaude`), sudo по паролю.
- Пользователь `tgclaude` (uid 1001, группы `tgclaude`, `users`, `docker`) — аккаунт агента **Astra**, вход по SSH разрешён, sudo нет. `/home/tgclaude` — **21 ГБ**, крупнейший потребитель диска. Подробно — в разделе «Пользователь `tgclaude`».
- ⚠️ **Группа `docker` = root-эквивалент.** `docker:x:988:rus,tgclaude` — любой её член монтирует `/` в контейнер и делает что угодно от рута (этим же способом чистились root-овы файлы 2026-09-08). Поэтому «у `tgclaude` нет sudo» **не** означает ограничения прав на хосте.
- Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
## SSH (`/etc/ssh/sshd_config.d/00-hardening.conf`)
⚠️ В основном `/etc/ssh/sshd_config` строкой 42 стоит `PermitRootLogin yes` (правка шаблона Contabo). Побеждает всё равно дроп-ин: `Include /etc/ssh/sshd_config.d/*.conf` стоит строкой 12, а sshd берёт **первое** встреченное значение. То есть root-вход закрыт, но держится на порядке строк — уберёшь дроп-ин, и root-вход по ключу включится. ⏳ Рядом лежит `50-cloud-init.conf`, который из-под `rus` не читается: под root стоит проверить `sshd -T | grep -i permitrootlogin`.
```
PermitRootLogin no
PasswordAuthentication no
@@ -22,18 +26,34 @@ PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
MaxAuthTries 3
AllowUsers rus
AllowUsers rus tgclaude
```
Через socket-activation (`ssh.socket`).
## Сеть
- Управляется **netplan → systemd-networkd**, конфиг `/etc/netplan/50-cloud-init.yaml` (наследие cloud-init, сейчас просто статический файл).
- Матч интерфейса **по MAC `00:50:56:64:ff:15`** + `set-name: eth0`. Ядро называет карту `ens18`, netplan переименовывает её в **`eth0`** через сгенерированный `/run/systemd/network/10-netplan-eth0.link`.
- IPv4 `161.97.93.252/18`, GW `161.97.64.1`. DNS `195.179.224.53` / `209.126.15.53`.
- ⚠️ IPv6 `2a02:c207:2338:7709::1/64`, дефолт `via fe80::1 onlink` — **мигает, а не «работает» или «не работает»**. Хронология одного дня, 2026-09-08: утром `fe80::1` в `ip -6 neigh` был `REACHABLE` и `ping6` наружу шёл без потерь (до этого месяцами `FAILED`); к 15:50 снова `FAILED`, `ping6` — 100% потерь. Конфиг на сервере не менялся ни разу, то есть причина целиком на стороне Contabo. Практический вывод: **не завязывать ничего на IPv6 этого хоста** и не считать очередное «заработало» починкой. IPv4 при этом не страдает.
- ⚠️ **На имя `eth0` завязано** NAT-правило AmneziaWG (`-o eth0 MASQUERADE` в `wg0.conf`) и правила ufw. Если netplan не отработает, интерфейс останется `ens18` и VPN-выход молча сломается.
- **Страховка `net-rescue`** (заведена 2026-08-01): `/usr/local/sbin/net-rescue.sh` + `net-rescue.timer` (`OnBootSec=150`, enabled). Идемпотентно находит карту по MAC — независимо от её имени — и доставляет адрес, дефолтный маршрут и DNS, если их нет. Пишет в journal тегом `net-rescue`. Смысл: пережить повторение аварии 2026-07-31 без похода в VNC.
- **Третий механизм правки маршрута — `/etc/cron.d/staticroute`** (из образа Contabo, не наш): `@reboot root ip route replace $(link-scope сеть eth0) via $(дефолтный GW) dev eth0`. Аргументы вычисляет на ходу из текущей таблицы, поэтому если адреса на eth0 нет вообще — тихо ничего не делает. Дублирует задачу `net-rescue`, но вслепую и без привязки к MAC. Знать о нём надо: при разборе «кто поправил маршрут» это третий кандидат после netplan и net-rescue.
- cloud-init установлен, но сетевой конфиг ему запрещён: `/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg` (`network: {config: disabled}`) — иначе перезапишет netplan.
## DNS
- Зона `ruzzy.dev` — Namecheap (NS `dns1/dns2.registrar-servers.com`).
- `fr1.ruzzy.dev` A → 161.97.93.252.
- `fr1.ruzzy.dev` A → 161.97.93.252. Апекс `ruzzy.dev` A → 161.97.93.252 (переведён с de1).
- CNAME → `fr1.ruzzy.dev`: `git`, `mon`, `www`, `share`.
- ⚠️ `cam.ruzzy.dev` (CNAME → fr1) остался в Namecheap после удаления ipcam 2026-09-08 — снести руками.
- PTR (reverse DNS): дефолтный `vmi3387709.contaboserver.net` (не менялся).
## Firewall (ufw)
- Default: deny incoming, allow outgoing.
- Открыто: `22/tcp`, `80/tcp`, `443/tcp` (IPv4 + IPv6).
- Открыто (IPv4 + IPv6): `22/tcp` (OpenSSH), `80/tcp`, `443/tcp`, `2222/tcp` (gitea-ssh), `36360/udp` (awg-de-exit).
- `9100/tcp` — только `from 172.16.0.0/12` (node-exporter для Prometheus в docker).
- Роутинг туннеля: `allow FWD wg0 ↔ eth0` (в обе стороны) + `allow in on wg0`.
- Плюс динамические REJECT-правила от fail2ban.
- ⚠️ **ufw НЕ управляет docker-портами.** Публикацию `ports: - "X:Y"` docker вписывает в iptables напрямую, минуя цепочки ufw: порт открыт в мир независимо от `deny incoming` и в `ufw status` не виден. Проверено 2026-09-08 — ipcam-FTPS месяцами держал наружу `2221` и `30000-30010`, правил ufw для них не было вообще. Список выше описывает **только host-сервисы**; чтобы контейнер не светил порт, биндить надо на `127.0.0.1:` (как у grafana/prometheus/alertmanager/gitea-web). Аудит открытого наружу — `ss -tlnH` + `docker ps`, а не `ufw status`.
## fail2ban (`/etc/fail2ban/jail.local`)
```
@@ -49,37 +69,63 @@ backend=systemd banaction=ufw
## Обновления
- Ubuntu Pro подключён (аккаунт ruzzyrullezz@gmail.com): **livepatch**, **esm-infra**, **esm-apps** — enabled.
- livepatch приходит **снапом** `canonical-livepatch` (v11.0.2, rev 414); рядом только `core22` и `snapd` 2.76.3 — больше снапов на хосте нет. Состояние: `canonical-livepatch status` → ядро `6.8.0-137.137-generic`, `patchState: nothing-to-apply`, tier `updates`, поддержка этого ядра до **2027-08-17** (`upgradeRequiredDate` — после этой даты livepatch потребует апгрейда ядра).
- ⚠️ snapd обновляет себя и livepatch **сам, 4 раза в сутки, без окна обслуживания**: `snap refresh --time` → `timer: 00:00~24:00/4`, holds не выставлены. Это отдельный от apt канал изменений на хосте, и отчёт в Telegram про него не приходит (`uu-telegram-report.sh` висит только на apt).
- unattended-upgrades: `Update-Package-Lists "1"`, `Unattended-Upgrade "1"` (ежедневно, security). Авто-reboot выключен.
## Система
- swap: `/swapfile` 2 GB (в `/etc/fstab`), `vm.swappiness=10` (`/etc/sysctl.d/99-swappiness.conf`).
- journald: `Storage=persistent`, `SystemMaxUse=500M`.
- AppArmor включён; время — systemd-timesyncd.
## Логирование (для разборов) — пересобрано 2026-08-01
- journald: `Storage=persistent`; ретенция — дроп-ин `/etc/systemd/journald.conf.d/10-retention.conf`:
`SystemMaxUse=8G`, `MaxRetentionSec=1year`. **Было `SystemMaxUse=500M`** — журнал упирался в потолок и хранил всего ~11 дней, из-за чего при разборе аварии логи за 15 июля уже не существовали.
- История bash (в `.bashrc` у `rus` и у `root`): `HISTTIMEFORMAT='%F %T '`, `HISTSIZE=100000`, `HISTFILESIZE=200000`,
`HISTCONTROL=ignoredups` (было `ignoreboth` — команды с ведущим пробелом исчезали бесследно),
`PROMPT_COMMAND="history -a"` — сброс после каждой команды, чтобы жёсткий ресет не съедал сессию.
- ⚠️ Записи в `~/.bash_history` с датой **`1970-01-01` — это легаси до 2026-08-01**, реального времени у них нет. Проставлен сентинел `#1` перед каждой такой строкой, иначе bash рисовал бы им текущее время и вводил в заблуждение при разборе.
- rsyslog дублирует auth/syslog в `/var/log/*.log` (ротация ~4 недели) — это спасло разбор, когда journald уже вычистил июль.
- **sysstat / `sar`** — исторические CPU, память, диски, сеть с шагом 10 минут. Собирает `sysstat-collect.timer` (`OnCalendar=*:00/10` → `/usr/lib/sysstat/sa1`), бинарные срезы в `/var/log/sysstat/saNN` (NN — день месяца), ежедневный текстовый отчёт `sarNN` от `sysstat-summary.timer`. Читать: `sar -u -f /var/log/sysstat/sa07` (CPU), `-r` память, `-d` диски, `-n DEV` сеть. Ценность именно в том, что пишет **системный таймер, а не контейнер**: данные остаются, даже когда лежал сам docker с Prometheus.
- **Ретенция sysstat — пересобрана 2026-09-08.** Было `HISTORY=7`: история жила 7–8 дней, файлы затирались по кругу номером дня месяца — ровно та же ловушка, что была с journald на 500M (при разборе аварии месячной давности загрузки уже нет, а sysstat держат именно за этим). Стало **`HISTORY=365`**, `ENABLED="true"` в `/etc/default/sysstat`, мёртвый `/etc/cron.d/sysstat` удалён.
- Важная механика: при `HISTORY > 28` скрипт `/usr/lib/sysstat/sa1` сам добавляет `sadc -D`, и имена файлов переключаются с `saDD` на **`saYYYYMMDD`** — без этого поднятие HISTORY было бы бесполезным, срезы всё равно перезаписывались бы каждый месяц. Первый датированный файл появляется на следующем срабатывании таймера. Чистку и сжатие делает `sa2` (`find -mtime +HISTORY` → удаление, `+COMPRESSAFTER=10` → xz); при старом `HISTORY=7` сжатие не срабатывало никогда, теперь работает.
- Старые файлы эры 7 дней (`sa31`, `sa01…sa08`) остаются рядом ещё год и читаются как обычно: `sar -u -f /var/log/sysstat/sa07`.
- Почему `/etc/cron.d/sysstat` удалён без сожалений: оба его правила искали `debian-sa1`, которого в Ubuntu 24.04 нет (бинарь называется `sa1`) — то есть это был no-op. Сбором всё время занимался systemd-таймер, дублей в данных не было (срезы ровно в :00/:10/:20…). Флаг `ENABLED="false"` там же был легаси и на таймер не влиял — выставлен в `true` для соответствия факту.
- **Известные дыры:** команды без sudo (`docker stop`, `mpstat`, …) не фиксируются нигде; `sudo su` — слепая зона. `snoopy`/`auditd` и `log_output` в sudoers сознательно НЕ ставили.
- ⚠️ **Открытый баг:** `rsyslogd` периодически валится в `action 'action-0-builtin:omfile' suspended` и в эти окна теряет строки. Наблюдался и 25.07, и перед падением 31.07. Не чинилось.
## Docker
- Docker 29.6 + Compose v5.1 (официальный репозиторий). `rus` в группе docker.
## nginx + TLS
- nginx 1.24 (apt) + certbot 2.9 (`python3-certbot-nginx`), автопродление через systemd-таймер certbot.
- vhost `/etc/nginx/sites-available/mon.ruzzy.dev.conf` → проксирует на Grafana (`127.0.0.1:3000`), HTTPS-редирект, websocket.
- vhost'ы `share.ruzzy.dev.conf` и `www.ruzzy.dev.conf` — статика с **общим** корнем `/var/www/share.ruzzy.dev` (`autoindex off`, `try_files $uri =404`, `server_tokens off`), у каждого свой сертификат. Используется для раздачи клиентских конфигов OpenVPN — сейчас там `marina-de.ovpn`, `masha-de.ovpn`.
- vhost `default` — catch-all `server_name _`, `listen 80 default_server`, отдаёт дефолтную страницу `/var/www/html/index.nginx-debian.html`. На `:443` `default_server` **не задан**, поэтому TLS-запрос с неизвестным SNI попадает на первый по алфавиту vhost (`git.ruzzy.dev`) и получает его сертификат.
- Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager** (docker compose).
- **Алерты:** Alertmanager → Telegram (бот @ultimate_log_bot, chat 607015). Правила `prometheus/rules/alerts.yml`: TargetDown, HostHighCPU, HostLowMemory, HostDiskLow/Critical. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git).
- Prometheus: retention 90d, слушает `127.0.0.1:9090`. Скрейпит node-exporter + cadvisor (job'ы `node`/`cadvisor`, instance `fr1`).
- Grafana: `127.0.0.1:3000`, доступ снаружи через **https://mon.ruzzy.dev** (nginx + Let's Encrypt). Datasource Prometheus (uid `prometheus`) и дашборды (Node Exporter Full, cAdvisor, папка Infra) провижатся автоматически. Пароль admin — в `~/monitoring/docker-compose.yml` на сервере (в репо вынесен в `${GF_ADMIN_PASSWORD}`).
- Prometheus: retention 90d, слушает `127.0.0.1:9090`. Job'ы: `node` и `cadvisor` (file_sd, instance'ы **fr1 и ru1**), `openvpn` (file_sd, только ru1 — `85.198.109.86:9176`), `gitea` (static, `gitea:3000` через внешнюю docker-сеть `monitoring_monitoring`), `prometheus` (self). На 2026-09-08 все таргеты up, активных алертов нет.
- Grafana: `127.0.0.1:3000`, доступ снаружи через **https://mon.ruzzy.dev** (nginx + Let's Encrypt). Datasource Prometheus (uid `prometheus`) и дашборды (Node Exporter Full, cAdvisor, Network/Traffic, Gitea, OpenVPN — папка Infra) провижатся автоматически. Пароль admin — в `~/monitoring/docker-compose.yml` на сервере (в репо вынесен в `${GF_ADMIN_PASSWORD}`).
- **Тома:** `monitoring_prometheus_data` — **6.5 ГБ** (при retention 90d; самый крупный том на сервере, учитывать при планировании диска), `monitoring_grafana_data` — 50 МБ, `monitoring_alertmanager_data` — пустой.
- **Multi-server:** таргеты через file_sd (`prometheus/targets/{node,cadvisor}/*.yml`) — новый сервер = добавить файл + `curl -X POST .../-/reload`. См. `stacks/monitoring/README.md`.
- **node-exporter — `network_mode: host`** (видит реальные интерфейсы хоста: eth0, wg0-туннель и т.д.; иначе показывал бы только сеть docker-бриджа). Слушает host:9100, доступ ограничен ufw (`from 172.16.0.0/12 to port 9100`), Prometheus скрейпит через `host.docker.internal:9100`. nodename берётся с хоста (fr1).
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты). ⚠️ для корректной сети на `de1` его агент тоже надо перевести в host-net (пока bridge).
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты).
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
- Планируется: VPN-экспортёры (wireguard/openvpn/telemt) + подключение `de1` по туннелю.
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
- Перенесён с `de1` 2026-06-21. **Gitea 1.25.4**, БД SQLite (всё в `/srv/gitea`, 4.3 ГБ, 16 репо).
- Перенесён с `de1` 2026-06-21. **Gitea 1.27.3** (обновлён 2026-08-31 с 1.25.4), БД SQLite (всё в `/srv/gitea`, 1.9 ГБ, 20 репо).
- Пин образа в `~/gitea/docker-compose.yml` и в `stacks/gitea/` приведён к `1.27.3` **2026-09-08** — до этого оба файла держали `1.25.4`, и любой `docker compose up -d` откатил бы Gitea на две минорные версии. Бэкап прежнего файла — `~/gitea/docker-compose.yml.bak-20260908`.
- Запущенный контейнер `gitea` создан вне compose, поэтому `docker compose up -d` его **пересоздаст** — короткий простой, но данные на bind-mount не теряются. Конфликта больше нет: мешавший `gitea_old_1254` (он нёс метки того же compose-проекта и поднимался вторым, забирая порты 2222/3001) удалён 2026-09-08, `--dry-run` теперь показывает ровно одно `Container gitea Creating`.
- **Откат на 1.25.4 возможен — предмиграционный снимок есть:** `/home/tgclaude/backups/gitea-backup-20260831-045458.tar.gz`, 4.5 ГБ (`config/app.ini` + `data/`). Проверено 2026-09-08 распаковкой `data/gitea.db`: версия схемы в архиве — **323**, в живой БД — **343**, репозиториев 20, пользователей 4. То есть архив снят до миграции 1.25→1.27 и для откатa годен. Контейнер `gitea_old_1254` и образ `1.25.4` удалены 2026-09-08 (они как раз откатом **не** были — работали бы на уже миграцированных данных); при необходимости образ просто `docker pull gitea/gitea:1.25.4`.
- ⚠️ Этот снимок лежит в хоме `tgclaude` и до 2026-09-08 нигде не был описан — при уборке диска его легко снести по незнанию. Правило на будущее: перед апгрейдом Gitea снимать `/srv/gitea`, и складывать снимок туда, где он описан.
- Контейнер `gitea`: SSH `2222` (наружу), web `127.0.0.1:3001` (3000 занят Grafana → перевесил на 3001).
- nginx vhost `git.ruzzy.dev` → `127.0.0.1:3001`, TLS Let's Encrypt, `client_max_body_size 512M`.
- DNS: `git.ruzzy.dev` (CNAME → fr1) → 161.97.93.252.
- `ROOT_URL`/`SSH_DOMAIN` = `git.ruzzy.dev` (не менялись). Host-ключи SSH и user/deploy-ключи перенесены с данными — `ssh://git@git.ruzzy.dev:2222` работает как раньше.
- Docker-томов нет — все данные на bind-mount `/srv/gitea` (1.9 ГБ), это и есть точка бэкапа.
- Это origin репо `infra` и vault openclaw.
## AmneziaWG exit (для ru1) — перенос VPN, фаза 1
@@ -89,13 +135,15 @@ backend=systemd banaction=ufw
- **ufw:** `36360/udp` открыт + `ufw route allow in on wg0 out on eth0` (и обратно) + `ufw allow in on wg0` — **обязательно**, иначе ufw режет туннельный трафик (на `de1` ufw был выключен).
- Пиры: `ru1` (10.8.1.4) переключён сюда; прямые 10.8.1.1/2/3 пока на `de1`.
- Проверено: зарубежный exit RU-клиентов идёт через `fr1` (exit-IP 161.97.93.252).
- ⏳ Остаётся (фаза 2): telemt MTProxy, OpenVPN, прямые AWG-клиенты, перенос DNS `ruzzy.dev`/`www`.
- ⏳ Остаётся (фаза 2): telemt MTProxy, OpenVPN, прямые AWG-клиенты. DNS `ruzzy.dev`/`www` — **уже переведены** на fr1.
## claude-channels (Telegram-бот @iluha_rabobot)
- Перенесён с `de1` 2026-06-22. `~/claude-channels/` (Dockerfile Node22+Bun+Claude Code, compose, `.env` с токеном). Запущен, авторизован (Sonnet 4.6), подключён к Telegram.
- Volume `claude-channels_claude-home` — только нужное: `.claude.json` + `.claude/.credentials.json` (авторизация) + settings + channels (allowlist). Плагины/кэш/история НЕ копировались (на ext4 раздувались до 6.5G из-за CoW на de1).
- Перенесён с `de1` 2026-06-22. `~/claude-channels/` (Dockerfile Node22+Bun+Claude Code, compose, `.env` с токеном). Запущен, авторизован, подключён к Telegram. Claude Code 2.1.197; в `.claude.json` модели `sonnet-5[1m]`, `opus-4-8[1m]`, `opus-4-7`.
- Volume `claude-channels_claude-home` — при переносе положили только нужное: `.claude.json` + `.claude/.credentials.json` (авторизация) + settings + channels (allowlist). Плагины/кэш/история НЕ копировались (на de1 раздувались до 6.5 ГБ).
- ⚠️ Но том **раздулся заново: 2.8 ГБ** (проверено 2026-09-08) — второй по величине после prometheus_data. Экономия от аккуратного переноса съедена работой самого бота (кэш/история/плагины нарастают внутри контейнера). Если снова упрёмся в диск — смотреть сюда.
- ⚠️ В томе, помимо авторизации Claude, лежат **SSH-ключи**: `/root/.ssh/id_ed25519` (в Gitea как deploy-ключ, `Host git.ruzzy.dev`) и `/root/.ssh/astra_fr1` — ключ к самому хосту под `tgclaude` (см. раздел `tgclaude`). Том — чувствительный артефакт, обращаться как с секретом.
- **telegram-плагин переустановлен заново** в контейнере (`claude plugin install telegram@claude-plugins-official`, v0.0.6) — копировать его нельзя.
- Монтирует `~/MyVault` (`/vault`). ⚠️ Vault-синк в gitea пока НЕ активен (заведём с openclaw) — заметки бота копятся локально.
- Монтирует `~/MyVault` (`/vault`, 186 МБ). Vault-синк в gitea **работает**: cron у `rus` `*/5 * * * *` → `~/openclaw/sync.sh` — `git pull --rebase --autostash`, затем при изменениях коммит «vault sync <дата>» и push в `ruzzy/MyVault.git`. Ключ `~/.ssh/openclaw_gitea`, лог `~/.openclaw/logs/vault-sync.log`.
- Gmail MCP — без OAuth-кредов (gmail-инструменты не работают, на бот не влияет).
## Telegram-уведомления
@@ -103,5 +151,126 @@ backend=systemd banaction=ufw
- `/usr/local/bin/tg-notify.sh "msg"` — универсальная отправка (HTML, префикс hostname).
- `/usr/local/bin/uu-telegram-report.sh` — сводка автообновлений + reboot-required; повешен на `apt-daily-upgrade.service` через drop-in `/etc/systemd/system/apt-daily-upgrade.service.d/telegram.conf` (`ExecStartPost`). Молчит, если нечего сообщать.
## Пользователь `tgclaude` (агенты) и его нагрузки
Аккаунт uid 1001 — **общий вход для агентов на этот же хост**, не персональный. До 2026-09-08 был описан только в `~rus/MyVault/TOOLS.md` (шпаргалке агентов), в репо `infra` не значился вовсе. Его нагрузки не видны ни в `docker ps` (они не в контейнерах), ни при обходе `~rus`, поэтому при инвентаризации их легко пропустить. `rus` состоит в группе `tgclaude`, так что большая часть этого хома читается без sudo.
- Кто заходит: агент **Astra**, работающий **внутри контейнера `claude-channels`** (бот @iluha_rabobot). Ключ и конфиг лежат в самом контейнере: `/root/.ssh/astra_fr1` + `Host fr1-astra → HostName 161.97.93.252, User tgclaude, IdentitiesOnly yes` (созданы 2026-09-07), а публичная часть — в `authorized_keys` у `tgclaude`. То есть бот ходит по SSH на свой же хост.
- ⚠️ **Цепочка привилегий, которую стоит осознавать целиком:** Telegram-бот → агент в контейнере → SSH как `tgclaude` → группа `docker` → **root на хосте**. Ни sudo, ни пароля на этом пути не требуется. Всё, что ограничивает агента, — его собственная сдержанность.
- Ключ `astra_fr1` живёт в томе `claude-channels_claude-home`: пересоздание контейнера доступ не отберёт, а бэкап тома содержит рабочий ключ к хосту.
**imgbot** — `/home/tgclaude/imgbot` (209 МБ; python/uv, `.venv`, Playwright + chromium, есть `.git`). Headless-поиск и скачивание картинок по текстовому запросу: источники Bing, Yandex, Google и Reddit (по сохранённым кукам), Pexels API; дедуп SHA-256 + перцептивный dHash; SQLite-манифест; фоновая проверка релевантности vision-моделью через OpenRouter; MCP-сервер с инструментами `request_photos`, `list_saved`, `export_by_query`. Правится активно — README и `.git` от 2026-09-08.
- **Оба сервиса живут в контейнерах, стек — `stacks/imgbot/` в репо, на сервере `~/imgbot-daemon/`** (переведено 2026-09-08). Один образ `imgbot-daemon:local` (база `mcr.microsoft.com/playwright/python:v1.62.0-noble`), два сервиса:
- `imgbot-daemon` — APScheduler: `refresh` каждые 30 мин (докачка картинок) и `analyze` каждые 5 мин (vision-проверка релевантности, 3 запроса к OpenRouter на картинку). ~15% ядра постоянно, поэтому `cpus: 2.0`, `mem_limit: 3g`.
- `imgbot-http` — **объединил прежние `imgbot-mcp-http` (8765) и `imgbot-file-server` (8766)**: MCP отдаётся как под-приложение Starlette (`/mcp`), рядом маршрут `/photo/<id>`. Один процесс, один порт 8765.
- Ключевые решения по контейнерам (все с комментариями в compose, ломается молча, если забыть):
- **Данные монтируются в тот же путь, что на хосте** — `/home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data`. В SQLite `images.file_path` абсолютный; при монтировании в `/data` analyze падал с `FileNotFoundError` на старых строках, а новые стали бы нечитаемыми для чего угодно вне контейнера.
- ⚠️ **Корень хранения может быть не один.** При переезде был пропущен второй — легаси-архив `~tgclaude/images`, куда смотрели 1350 строк манифеста, — и `/photo/<id>` полтора часа отдавал 404 на 19.5% коллекции **молча**: в логах ничего, потому что `analyze` эти строки уже не трогал. Архив с тех пор удалён (см. «Выведено»), сейчас корень один. **Правило: перед контейнеризацией проверять все префиксы `file_path`, а не только `data_dir`** — `select substr(file_path,1,30), count(*) from images group by 1;`
- Исходники монтируются **read-only из рабочей копии** `/home/tgclaude/imgbot:/app:ro` — правки применяются `docker restart`, без пересборки. В образе только зависимости и chromium.
- `HOME=/tmp` обязателен: `collector.py` безусловно вызывает `Path.home()`, а uid 1001 не имеет записи в `/etc/passwd` образа.
- Версия `playwright` в образе (1.62.0) должна совпадать с `uv.lock` и тегом базового образа — иначе разъезжается ревизия chromium.
- `/etc/localtime` + `/etc/timezone` внутрь: иначе контейнер писал в общий `daemon.log` в UTC поверх строк в CEST.
- `shm_size: 1gb` — chromium падает на дефолтных 64 МБ.
- **Сеть и доступ:** `imgbot-http` подключён к `imgbot_default` и к внешней `claude-channels_default`, порт на хост **не публикуется**. Агент внутри `claude-channels` обращается по имени `http://imgbot-http:8765`. Проверено 2026-09-08: `MCP initialize` → 200 с `serverInfo`, `/photo/1` → 200 `image/jpeg`, несуществующий id → 404.
- ⚠️ **Авторизации нет сознательно.** Прежде токен `IMGBOT_FILE_SERVER_TOKEN` закрывал только `/photo`, а `/mcp` пускал кого угодно — проверено запросом без заголовков. Держать одну дверь на замке при открытой второй смысла не было, поэтому убрали и токен, и упоминания о нём в docstring и README. Обоснование: снаружи адреса нет, попасть можно лишь с самой машины и из подключённых docker-сетей. **Если сервис когда-нибудь выставят наружу — авторизацию вернуть первым делом:** `request_photos` тратит бюджет API и работает под живыми куками Google/Reddit, `export_by_query` копирует файлы.
- ⚠️ У MCP **своя защита от DNS-rebinding**: по умолчанию принимает только `localhost`, на `Host: imgbot-http:8765` отвечал `421 Invalid Host header`. Список задаётся `IMGBOT_ALLOWED_HOSTS` (сейчас `imgbot-http:8765,localhost:8765,127.0.0.1:8765`) — при смене имени сервиса или порта его надо править, иначе получишь загадочный 421.
- **Код проекта теперь в Gitea** — `ruzzy/imgbot` (private), создан 2026-09-08. До этого репозиторий существовал единственной копией на fr1: без remote, без бэкапа. Push идёт по deploy-ключу `~tgclaude/.ssh/imgbot_gitea` (с правом записи), remote `ssh://git@git.ruzzy.dev:2222/ruzzy/imgbot.git`. Разовая копия перед переездом — `~rus/imgbot-repo-pre-merge-20260908-133444.tar.gz`.
- ⚠️ `_job_analyze` глушит исключения через `log.exception` и возвращается нормально, поэтому APScheduler пишет `executed successfully` **даже после падения**. Мониторить надо `job failed`/`Traceback` в логе, отсутствие ошибок ничего не доказывает.
- README описывает те же `refresh`/`analyze` как задачи cron с `flock` — при живом daemon это дубль. Проверить `crontab -l -u tgclaude` (нужен root): если записи есть, работа делается дважды.
- Данные: `/home/tgclaude/imgbot-data` (2.6 ГБ) — `images/`, живой `manifest.db`, `exports/` 21 МБ, логи `daemon.log`/`refresh.log`/`bulk_fill.log`, состояние и куки Google/Reddit (mode 600).
**Прочее у `tgclaude`:**
- `/home/tgclaude/.local/lib` — 2.3 ГБ python-пакетов, `chrome-libs` — 3 МБ библиотек для headless-chromium.
- **Ещё один Telegram-бот** — `send_galleries.py`, `send_random_photo.py`, `send_retry.py`, `gallery_paths.txt`, токен в `~/.tg_bot_token` (600). Отдельный и от @ultimate_log_bot, и от @iluha_rabobot.
- `/home/tgclaude/backups/gitea-backup-20260831-045458.tar.gz` — 4.5 ГБ, предмиграционный снимок Gitea, см. раздел Gitea.
- ⚠️ `/home/tgclaude/.openclaw-auth-profile-secrets` — права **777** на каталоге, предназначенном для секретов (сейчас пуст).
- ⏳ Не проверено, нужен root: crontab пользователя `tgclaude` (README imgbot упоминает периодические задачи «run via cron» — значит, что-то запланировано), а также `.cache`, `.docker`, `.ssh`.
## Прочие нагрузки и каталоги (не оформлены в стеки)
- **openclaw — две независимые копии, не путать:**
- `/home/tgclaude/openclaw-deploy/openclaw` (947 МБ) — **именно отсюда запущены контейнеры** (по метке `com.docker.compose.project.config_files`), состояние в `/home/tgclaude/.openclaw` (135 МБ, владелец при этом `rus`, mode 700). Контейнеры `openclaw-openclaw-{gateway,cli}-1` (образ `openclaw:local`) — exited с 2026-08-31, причём у gateway `restart: unless-stopped`, то есть он остановлен явно, а не упал. Порты gateway `18789`, `18790`, `3978`; cli делит с ним netns. Сеть `openclaw_default` (172.22.0.0/16) и пустой том `openclaw_whisper_models` живы.
- `~rus/openclaw` (212 МБ, **не git-репозиторий** — просто распакованное дерево) + `~rus/.openclaw` (2.0 ГБ, из них `agents/` почти всё) + `~rus/openclaw-work` (147 МБ) + `~rus/.openclaw-auth-profile-secrets` — отдельная копия, своих контейнеров не имеет. Единственная живая часть — `sync.sh` из cron `rus` (см. claude-channels), лог пишется.
- Образ-снимок `openclaw:backup-20260831` (1.55 ГБ) — путь откатa для openclaw.
- **`~/backups` — 5.4 ГБ**, всё с уже выведенного `de1`: `openclaw-de1-2026-06-22.tgz` (3.1 ГБ), `claude-channels-de1-2026-06-22.tgz` (1.1 ГБ), `postgres-de1-2026-06-22/` (1.3 ГБ, 11 дампов — крупнейший `pers_data.dump` 1.2 ГБ), `var-www-envs/` (`.env`-ы 8 проектов), 5 pre-wipe-снимков claude-channels от 2026-07-16/18. Кандидат №1 на выселение с сервера, если понадобится место.
- `~/downloads` — пустой. `~/MyVault` (186 МБ) — см. claude-channels.
## Выведено с fr1
- **ipcam / FTPS-приёмник** (был `~/ipcam`, `fauria/vsftpd`, порты `2221:21` + `30000-30010`, домен `cam.ruzzy.dev`, данные `/srv/ipcam/videos`, сертификат `/srv/ftps/certs` + deploy-хук certbot) — **удалён 2026-09-08**: контейнер, образ, том `ipcam_ipcam_logs`, сеть, каталоги `/srv/ipcam` и `/srv/ftps`, хук и LE-сертификат `cam.ruzzy.dev`. Видео так и не начали складываться (в `/srv/ipcam` на момент удаления 0 файлов). Хвосты вне сервера: DNS-запись в Namecheap и настройка аплоада на самой камере.
- **Уборка 2026-09-08** — удалены неиспользуемые каталоги и их образы: `~/sc2-parser` (+образ `sc2-parser:latest`), `~/booty` (пустой), `~/booty-collector` (+сиротский том `booty_data`, 812 МБ), `~/vacuum-scraper` (2.0 ГБ, из них `output/` от root — дочищен через контейнер, +образ 887 МБ), `~/downloads/vacuum-scraper-images.tar.gz` (121 МБ). Код обоих проектов остался в Gitea (`booty-collector.git`, `vacuum-scraper.git`). Плюс вычищены docker build cache (43.4 ГБ), 6 анонимных томов, базовые образы `node:22`, `node:24-bookworm-slim`, `node:22-alpine`, `busybox` и связка `gitea_old_1254` + `gitea/gitea:1.25.4`. Итог: занято на `/` **104 → 58 ГБ** (27% → 15%).
- **ollama удалён 2026-09-08** — `/home/tgclaude/ollama` (3.5 ГБ: `ollama.tar.zst` 1.4 ГБ + `lib` 2.1 ГБ), `/home/tgclaude/.ollama` (4.6 ГБ моделей) и `ollama-serve.log`. Не запускался с 2026-08-25, процесса не было, юнитов и бинарей вне хома `tgclaude` не существовало. Итого 8.1 ГБ; хом `tgclaude` 21 → 13 ГБ, занято на `/` **58 → 50 ГБ** (15% → 13%).
- **Легаси-архив `~tgclaude/images` удалён 2026-09-08** — 1386 файлов, 1.9 ГБ на диске (0.84 ГиБ по сумме `size_bytes`). Это был не мусор, а второй корень хранения imgbot: `migrate_legacy.py` зарегистрировал файлы в манифесте **на месте**, поэтому вместе с каталогом удалены и 1350 строк таблицы `images` (запрос «жопки-архив»; 61 из них была признана релевантной и уже доставлена ботом). Удаление **необратимо**: у этих строк `source_url` пустой — imgbot не знал, откуда файлы взялись, и докачать их не сможет. Метаданные сохранены в `~rus/imgbot-legacy-archive-rows-20260908.json` (808 КБ) — хеши, размеры, вердикты, но не источники. Монтирование каталога убрано из compose (иначе docker пересоздал бы его пустым от root). После удаления: 5578 строк, все файлы на месте, один корень хранения.
- **Синк `vacuum-scraper → ~tgclaude/images` признан мёртвым 2026-09-08**, задание из крона root снято. Его роль забрал imgbot.
## Расписания (cron + systemd-таймеры)
Полная инвентаризация на 2026-09-08 — чтобы «кто это сделал в 3 утра» не приходилось выяснять заново.
**Наше:**
- `crontab -u rus`: `*/5 * * * *` → `~/openclaw/sync.sh` — синк MyVault в Gitea (см. claude-channels).
- `/etc/cron.d/staticroute` — `@reboot` правка маршрута, из образа Contabo (см. раздел «Сеть»).
- `certbot.timer` + `/etc/cron.d/certbot` — продление сертификатов (крон-вариант выходит no-op на systemd).
- `sysstat-collect.timer` (`*:00/10`) и `sysstat-summary.timer` (00:07) — сбор и суточный отчёт sar.
- `apt-daily.timer` / `apt-daily-upgrade.timer` — unattended-upgrades, на второй повешен `uu-telegram-report.sh`.
- `ua-timer.timer` — Ubuntu Pro. `logrotate`, `man-db`, `dpkg-db-backup`, `e2scrub_all`, `systemd-tmpfiles-clean` — штатное.
- Внутри контейнера `imgbot-daemon` — свой APScheduler (`refresh` 30 мин, `analyze` 5 мин), в системный крон не входит.
**Разобрано 2026-09-08 — что тут было и почему убрано:**
- **`crontab -u root` пуст.** Там было `*/5 * * * * rsync -a --delete … /home/rus/vacuum-scraper/output/images/ /home/tgclaude/images/` — зеркалило вывод vacuum-scraper в хом `tgclaude` каждые 5 минут. Пайплайн умер вместе с удалением `vacuum-scraper` (роль забрал imgbot), задание снято 2026-09-08. Это же объясняло, почему в `~tgclaude/images` лежали те же UUID-имена, что в удалённом `output/images`: живое зеркало, а не старая копия. Мина, которую заодно убрали: `rsync --delete` мог удалять файлы, на которые ссылался манифест imgbot, — то есть в любой момент осиротить строки в БД.
- **`/etc/cron.hourly` пуст, оба скрипта удалены 2026-09-08.** Что там было и почему убрано:
- `free` — одна строка `echo 1 > /proc/sys/vm/drop_caches`, то есть **сброс страничного кэша каждый час**. Артефакт шаблона Contabo ради «красивого» вывода `free`. Срабатывание доказано по Prometheus: `node_memory_Cached_bytes` проваливался ровно в **:17** каждого часа (именно в это время `/etc/crontab` запускает `run-parts /etc/cron.hourly`) — например 13:17 минус **3.89 ГиБ**, 10:17 минус 2.22 ГиБ. При этом платы на диске **не обнаружено**: чтение после провалов 0.14–0.48 МиБ/с, рабочий набор мал и возвращается в кэш дешёво. Убрано не ради производительности, а потому что это регулярная пила на графиках памяти, не связанная ни с чем реальным, — прямое препятствие для разборов, ради которых на этом сервере задрана ретенция журналов.
- `fstrim` — `fstrim /` **каждый час** при том, что штатный `fstrim.timer` (`OnCalendar=weekly`) на этой машине уже `enabled`. То есть trim шёл дважды; часовой убран, недельный таймер оставлен.
**Проверено и оставлено как есть:**
- `/etc/cron.daily/sysstat` — no-op: первой строкой `[ ! -d /run/systemd/system ] || exit 0`, то есть на systemd сразу выходит. Дубля с `sysstat-summary.timer` нет, трогать не надо.
- `update-notifier-download.timer` / `update-notifier-motd.timer` — десктопное наследие образа, оставлены.
**Отключено 2026-09-08 как наследие шаблона:**
- Таймеры `fwupd-refresh.timer` (раз в сутки ходил в сеть за метаданными прошивок LVFS) и `motd-news.timer` (`/etc/default/motd-news`: `ENABLED=1`, `URLS=https://motd.ubuntu.com` — раз в сутки запрашивал у Canonical новостной текст для приветствия) — `disabled`.
- Демоны `ModemManager`, `fwupd`, `multipathd`, `udisks2` — `masked`. Все четыре висели с 0.0% CPU и ~100 МБ RSS суммарно, и обслуживать им было нечего: multipath-устройств нет (`/dev/mapper` — только `control`, диск один), модемов нет (в `/sys/class/net` только `eth0`, `wg0`, `lo` и docker-интерфейсы), прошивок в KVM-гостe нет, съёмных носителей тоже.
## Открытые хвосты (на 2026-09-08)
Мелочи, оставленные сознательно, — чтобы не искать их заново.
- ⚠️ **В Gitea живут два временных токена** — `imgbot-setup` и `imgbot-setup2`, scope `write:repository,write:user`. Созданы через `docker exec -u 1000 gitea gitea admin user generate-access-token` для создания репо `ruzzy/imgbot` и deploy-ключа. Удалить через API не удалось: Gitea отвечает 401 на управление токенами по токен-авторизации, нужна basic-auth. **Отозвать вручную:** Настройки → Приложения.
- В таблице `queries` манифеста imgbot осталась строка `жопки-архив`, хотя все её 1350 картинок удалены. `refresh` может попытаться пополнить её, ища по этой строке в поисковиках, — получится мусор. Снести строку или оставить.
- В `~tgclaude/imgbot/.env` осталась строка `IMGBOT_FILE_SERVER_TOKEN` — после объединения HTTP-сервисов её никто не читает.
- `~tgclaude/.openclaw-auth-profile-secrets` — права **777** на каталоге для секретов (сейчас пуст), стоит `chmod 700`.
- Не решено, нужны ли на fr1: `~rus/backups` (5.4 ГБ архивов с выведенного de1) и спящий openclaw (947 МБ дерева + 2.0 ГБ состояния + образ-снимок 1.55 ГБ).
- Страховочные файлы, оставленные после работ: `~rus/imgbot-repo-pre-merge-20260908-133444.tar.gz` (369 КБ), `~rus/imgbot-legacy-archive-rows-20260908.json` (808 КБ), `~rus/gitea/docker-compose.yml.bak-20260908`.
- ⏳ Дроп-ин `/etc/ssh/sshd_config.d/50-cloud-init.conf` не читается из-под `rus` — под root проверить `sshd -T | grep -iE "permitrootlogin|passwordauth"`, чтобы знать эффективные значения, а не собранные из файлов вручную.
- ⚠️ Описания **других** серверов не сверялись: `servers/de1.md` и `servers/ru1.md` датируются временем до вывода de1, так что могут врать.
## Как это описание поддерживается
Механизм заведён 2026-09-08, после того как сверка нашла десяток расхождений.
**В репозитории:** `CLAUDE.md` — контракт «тронул сервер, обнови описание в
том же прогоне» и правила оформления; `.claude/skills/server-audit/` — метод
сверки и накопленные грабли; `scripts/snapshot.sh` — инвентаризация живого
состояния за один проход (только чтение, без sudo, секретов не печатает).
**На самом сервере**, для агентов, у которых репозитория нет:
- `~rus/CLAUDE.md` и `~tgclaude/CLAUDE.md` — указатель на репо, правило и
пять граблей, которые дороже всего стоили (ufw не видит docker-портов;
группа docker равна root; процесс из SSH не переживёт ребут; абсолютные
пути в чужих базах; мигающий IPv6). Рядом `AGENTS.md` — симлинк на тот же
файл, для агентов, читающих эту конвенцию. Claude Code подхватывает
`CLAUDE.md` вверх по дереву каталогов, поэтому файл в хоме виден и при
работе в подкаталогах вроде `~tgclaude/imgbot`.
- `~rus/MyVault/TOOLS.md`, раздел SSH — та же обязанность для агента Astra.
Файл смонтирован в контейнер `claude-channels` как `/vault`, так что
изменение видно ему сразу, и синкается в Gitea кроном каждые 5 минут.
⚠️ **Чего этот механизм не гарантирует.** MOTD и `.bashrc` не помогут:
`.bashrc` выходит на строке 6 для неинтерактивных шеллов, а MOTD
показывается только при интерактивном входе — то есть агент, дёргающий
`ssh fr1 'команда'`, не увидит ничего. Файлы в хомах достанут только тех,
кто читает `CLAUDE.md`/`AGENTS.md` и чей рабочий каталог оказался внутри
хома. Серверного хука «агент закончил работу» не существует.
Детектор дрейфа (таймер, сравнивающий отпечаток состояния с прошлым и
пишущий в Telegram) обсуждался и **сознательно не заводился** — если
незадокументированные изменения снова начнут накапливаться, начинать
стоит с него.
## Установленные пакеты (сверх базы)
htop, mtr-tiny, tmux, vim, curl, wget, git, unzip, net-tools, dnsutils, ncdu, rsync, fail2ban, unattended-upgrades.
htop, mtr-tiny, tmux, vim, curl, wget, git, unzip, net-tools, dnsutils, ncdu, rsync, fail2ban, unattended-upgrades, sysstat.
+41 -5
View File
@@ -1,7 +1,7 @@
# ru1 — `85.198.109.86` (RU entry node)
VPN entry-нода в РФ. Точка входа в топологии **RU↔DE**: умный роутинг (RU-трафик → напрямую, зарубежный → туннель AWG до de1). Полный runbook — `~/README.md` на сервере.
Секреты (MTProxy user-secrets, ключи WG) — только на сервере, не здесь.
Секреты (WEB-proxy secret в `/etc/tproxy-server/profiles.json` и `/etc/mtproxy/mtproxy.env`, `token.key`, ключи WG) — только на сервере, не здесь.
**Провайдер:** VDSina · РФ
**ОС:** Ubuntu 24.04.3 LTS · kernel 6.8.0-78
@@ -10,6 +10,8 @@ VPN entry-нода в РФ. Точка входа в топологии **RU↔D
## Доступ
- `ssh mik@85.198.109.86` (пользователь `mik`, вход по ключу), **passwordless sudo**.
- Ключи в `~mik/.ssh/authorized_keys` (по комментарию, сверено 2026-09-23): `mikpokrovskij@gmail.com`, `iluha_proxy`, `iluha_rabobot@ruzzy.dev`, `openclaw@ruzzy`, `claude-bot@ruzzy.dev` (добавлен 2026-09-23, SHA256:mNK/9jiwypqVHj8oOnKUcr1mqt39z4cVrWWsxxbKP+o; бэкап прежнего файла — `authorized_keys.bak-20260923`).
⚠️ У `mik` passwordless sudo, поэтому любой из этих ключей даёт root. Отдельного пользователя под ботов нет.
## Интерфейсы
- `ens3` — публичный `85.198.109.86/24` (RU интернет, default route via 85.198.109.1)
@@ -25,20 +27,45 @@ VPN entry-нода в РФ. Точка входа в топологии **RU↔D
| OpenVPN #2 | 1195/udp (`tun1`) | RU-выход (весь трафик через ens3, DNS Yandex). Cfg `/etc/openvpn/server/ruopenvpn.conf` |
| AmneziaWG `wg0` | (awg, `45451/udp`) | туннель до de1 (exit). `awg-quick@wg0`, cfg `/etc/amnezia/amneziawg/wg0.conf`, `Table=off` |
| WireGuard `wg1` | 51820/udp | VPN для клиентов (Docker `linuxserver/wireguard`), подсеть `10.8.2.0/24`, `~/wireguard/` |
| telemt (MTProxy) | 443 | FakeTLS под `ru.ruzzy.dev`, маска → nginx `127.0.0.1:4443`. API `9091`. Docker, `~/gost/` |
| nginx | 80, 4443 | маскировочный сайт `ru.ruzzy.dev` (Docker `nginx:alpine`) |
| WEB-прокси Telegram | 443 (через nginx) | `tproxy-server` (relay, systemd, `127.0.0.1:8090`, admin `:8091`) → официальный MTProxy (systemd, `2398`, stats `8888`, оба закрыты nftables для не-lo). Подробно — раздел ниже |
| nginx | 80, 443 | фронт `ru.ruzzy.dev`: весь 443 → relay. Docker `nginx:alpine`, `~/gost/` |
| certbot | — | `certbot renew` раз в 12 ч, webroot `~/gost/certbot/www` |
Контейнеры: `telemt`, `wireguard`, `nginx` (docker compose в `~/gost` и `~/wireguard`).
Контейнеры: `nginx`, `certbot` (`~/gost`), `wireguard` (`~/wireguard`), агент мониторинга (`~/monitoring-agent`). Compose, nginx.conf и скрипт установки — копия в репо `stacks/tproxy/`.
## WEB-прокси Telegram (с 2026-10-09)
Новый тип прокси в клиентах Telegram (Desktop ≥ 7.1.0; мобильные — по сообщениям с 13.0, официально не подтверждено). Клиент держит скрытый WebView к `https://ru.ruzzy.dev/` и гонит по HTTPS/WSS мультиплексированные MTProxy-потоки. Сервер — официальный [`telegramdesktop/tproxy-server`](https://github.com/telegramdesktop/tproxy-server), коммит `c8adb8b` (2026-09-29).
- **Клиенту:** server `ru.ruzzy.dev`, secret — 32 hex из `profiles.json`. Ссылка: `https://t.me/webproxy?server=ru.ruzzy.dev&secret=<secret>`; получить на сервере — последней строкой вывода `setup-ru1.sh`.
- **Цепочка:** клиент → nginx `:443` (TLS, `ru.ruzzy.dev`) → relay `127.0.0.1:8090` → MTProxy `127.0.0.1:2398` → middle-end Telegram через `wg0` → fr1.
- **Mode A, без base path:** relay — единственный шлюз хоста и сам отдаёт публичный сайт из `/srv/tproxy-site` (копия маскировочного `~/gost/nginx/html/index.html`; перечитывается только при рестарте relay). Запросы без валидной capability (`?bridge=` = HMAC от секрета и имени хоста) получают обычный сайт. Base path не включали: по документации его понимает только Desktop.
- ⚠️ **NAT для MTProxy задан вручную:** `MTPROXY_NAT_ARGS=--nat-info 10.8.1.4:161.97.93.252` в `/etc/mtproxy/mtproxy.env`. Официальный MTProxy выводит AES-ключи middle-end сессии из своего адреса; трафик к Telegram с ru1 уходит в wg0 и выходит с IP **fr1**. Автоопределение апстримного `install.sh` спросило бы ifconfig-сервис через ens3, получило 85.198.109.86 — и MTProxy принимал бы клиентов, никому не отвечая, без ошибок нигде. Сменится exit-IP fr1 или адрес wg0 — править здесь. Симптом: `journalctl -u mtproxy | grep 'Disconnected from RPC Middle-End'`.
- **Юниты:** `tproxy-firewall` (nft-таблица `inet tproxy_backend`: 2398/8888 только с lo), `mtproxy`, `tproxy-server`, `refresh-mtproxy-config.timer` (ежедневно тянет `proxy-multi.conf`, рестарт MTProxy только при изменении). Бинарник relay `/usr/local/bin/tproxy-server` собран локально (`CGO_ENABLED=0 GOOS=linux GOARCH=amd64`), MTProxy — `/opt/MTProxy`, собран `deploy/install-mtproxy.sh` (закреплённый коммит + sha256).
- **Carrier: `websocket`** (с 2026-10-09, было `https` по умолчанию) — поле `carrier_mode` профиля в `profiles.json`: один WSS-канал `/api/v1/ws` на все потоки клиента вместо POST + long poll. Клиенту менять нечего, режим выбирает сервер. Relay пингует WSS раз в 25 с простоя (`long_poll`), поэтому `proxy_read_timeout 90s` в nginx канал не рвёт. Смена режима — `systemctl restart tproxy-server`, живые сессии переподключаются. `websocket-lanes` не взят: отдельный WSS на поток, на macOS WebKit сериализует рукопожатия и подключение может не уложиться в таймаут клиента. Бэкап профиля до правки — `profiles.json.bak-20261009`.
- ⚠️ Порты relay **8090/8091**, а не дефолтные 8080/8081: `*:8080` занят cadvisor из `~/monitoring-agent`.
- ⚠️ **Логи nginx выключены** (`access_log off`): в URI bridge-страницы — capability, в `Sec-WebSocket-Protocol` — токен сессии. Не включать.
- **Проверено 2026-10-09:** `readyz` 200; 10 ME-соединений с `10.8.1.4`, `Disconnected` — 0; obfuscated2 + `req_pq_multi` на `127.0.0.1:2398` → `resPQ` за ~60 мс (3/3); capability через публичный фронт → bridge-страница (200, свой CSP). **Живой клиент не проверялся.**
- **Диагностика:** `curl -s 127.0.0.1:8091/readyz`, `curl -s 127.0.0.1:8091/metrics | grep tproxy_sessions`, `journalctl -u tproxy-server -u mtproxy`.
- **Маршрут:** весь Telegram (`91.108.4/22`, `91.108.8/22`, `91.108.56/22`, `95.161.64/20`, `149.154.160/20`) — в main-таблице `dev wg0`. Упал wg0 — прокси не работает.
- **Сертификат** `ru.ruzzy.dev` (LE, webroot, только это имя; перевыпущен 2026-10-09, до 2027-01-07). Продление: `certbot` в контейнере каждые 12 ч; nginx сам делает `reload` раз в 6 ч (цикл в `command` compose).
⚠️ До 2026-10-09 контейнера certbot **не было вовсе**, хотя он был описан в compose: сертификат истёк 2026-07-01 и три месяца отдавался просроченным.
- ⚠️ `nginx.conf` смонтирован в контейнер **одним файлом**: замена через `mv`/редактор с новым inode контейнер не видит, `nginx -s reload` перечитает старый. После такой замены — `docker compose restart nginx` (2026-10-09 так получили минуту простоя 443).
- Открытый прокси для всех, кто знает secret; лимиты — дефолтные апстрима (128 сессий, 4096 потоков).
### История 2026-10-09
1. Под «web-proxy для Telegram» сначала понят Telegram Web в браузере: nginx-прокси на `web.telegram.org` с переписыванием адресов DC (`zws*.ru.ruzzy.dev`) — **убран** в тот же день, не то.
2. telemt (MTProxy, FakeTLS под `ru.ruzzy.dev`, был на 443, API `9091` торчал наружу) снят, возвращён, затем **удалён окончательно** по решению владельца: контейнер, образ, `~/gost/telemt-config` (со старым секретом), `telemt-data`, том `gost_mtproxy-config`.
3. Поднят WEB-прокси (выше). Бэкапы compose/nginx.conf до изменений — `~/gost/*.bak-20261009*`.
## Умный роутинг (tun0 и wg1)
- `ipset ru_ips` (~11400 подсетей, источник ipdeny.com). До routing decision (mangle PREROUTING):
- dst ∈ ru_ips → RETURN (без метки) → main table → ens3 (RU напрямую)
- иначе → MARK `0x64` → `ip rule fwmark 0x64 → table vpn (100)` → default dev wg0 → de1
- ⚠️ **Правило `fwmark 0x64 → vpn` ставит `PostUp` в `wg0.conf`, и его снимает рестарт `systemd-networkd`.** По умолчанию networkd при старте удаляет «чужие» ip rule (`ManageForeignRoutingPolicyRules=yes`). 2026-10-09 в 16:36 `apt-get install` из установки WEB-прокси обновил библиотеки → needrestart перезапустил networkd → правило пропало, и ~45 мин (до 17:21) зарубежный трафик клиентов OpenVPN #1 шёл через ens3 (RU IP) вместо туннеля; жаловались пользователи VPN. Вернули вручную (`ip rule add fwmark 100 lookup vpn priority 10000`). Защита с 2026-10-09: `/etc/systemd/networkd.conf.d/10-keep-foreign-routing.conf` — `ManageForeignRoutingPolicyRules=no`, `ManageForeignRoutes=no` (действует со следующего старта networkd; networkd ради этого не перезапускали). Та же мина раньше висела на unattended-upgrades. Быстрая проверка: `ip rule | grep 0x64`.
- Обновление списка: `~/smart-routing/update-ru-ips.sh`, cron вс 03:00. Восстановление при ребуте: `ipset-restore.service` (до `awg-quick@wg0`).
- tun1 — без умного роутинга (всегда RU/ens3).
## AWG Watchdog
- `~/awg-watchdog.sh` в root-cron **каждые 3 мин**: если handshake `wg0` старше 5 мин → рестарт `awg-quick@wg0` + чистка дублей iptables. Лог `~/awg-watchdog.log`. (telemt зависит от wg0 — Telegram DC маршрутизируются через туннель до de1.)
- `~/awg-watchdog.sh` в root-cron **каждые 3 мин**: если handshake `wg0` старше 5 мин → рестарт `awg-quick@wg0` + чистка дублей iptables. Лог `~/awg-watchdog.log`. (WEB-прокси зависит от wg0: адреса Telegram с хоста идут через туннель.)
## Клиенты
- OpenVPN: `~/gen_client.sh --profile de|ru|both <имя>`, PKI `~/easy-rsa/pki`, профили `~/clients/`. CRL отключён.
@@ -58,3 +85,12 @@ VPN entry-нода в РФ. Точка входа в топологии **RU↔D
## ⚠️ Грабли (из README)
- `wg1.conf` обязан иметь `Table = off` — иначе wg-quick при рестарте сносит default route и сервер теряет SSH (восстановление: `ip route add default via 85.198.109.1 dev ens3`).
- Таблица маршрутизации — `vpn` (100), не `awg`.
## Открытые хвосты
- **Умный роутинг для `wg1` не настроен** (2026-10-09): mangle-правила есть только для `-i tun0`, для `wg1` / `10.8.2.0/24` — ни одного, хотя раздел «Умный роутинг» заявляет tun0 и wg1. В `wg1` один пир, рукопожатий не было ни разу — сервисом, похоже, никто не пользуется. Не правили.
- Drop-in `10-keep-foreign-routing.conf` фактически не проверен рестартом networkd (рестарт ens3 по SSH на этой ноде — риск). Проверить при следующей плановой перезагрузке: после неё `ip rule | grep 0x64` должен показать правило.
- **Живой клиент через WEB-прокси не проверен** (2026-10-09): серверная цепочка проверена по частям (см. раздел WEB-прокси). Поддержка в мобильных клиентах подтверждена только новостью про Telegram 13.0.
- **DNS `*.ru.ruzzy.dev`** (CNAME → `ru.ruzzy.dev`, Namecheap) остался от веб-прокси Telegram Web — больше не нужен, удалить. Имена `zws*.ru.ruzzy.dev` уже попали в CT-логи через сертификат от 2026-10-09 (Let's Encrypt) — это не отменить.
- `/tmp/tproxy-server` на сервере — исходники и `setup-ru1.sh`, нужны для `deploy/update-relay.sh`; `/tmp` чистится при ребуте — при обновлении заливать заново.
- `~/gost/manage.sh` — от прежнего gost/SOCKS5, не используется.
- Замечено при снапшоте 2026-10-09, не разбиралось: `iperf3 --server` (порт 5201 наружу, пользователь `iperf3`, PPID 1), cadvisor `unhealthy`. Остальное описание (OpenVPN, AWG, клиенты) сверялось до переключения на fr1 — см. строку в `AGENTS.md`.
+1 -1
View File
@@ -2,7 +2,7 @@ name: gitea
services:
gitea:
image: gitea/gitea:1.25.4
image: gitea/gitea:1.27.3
container_name: gitea
restart: unless-stopped
environment:
+40
View File
@@ -0,0 +1,40 @@
# Среда запуска для imgbot-daemon. Сам код — не наш: проект живёт у агента
# в /home/tgclaude/imgbot и правится им же. Здесь только окружение.
#
# База — официальный образ playwright с тегом ровно под версию из uv.lock
# проекта (playwright 1.62.0): в нём уже есть chromium и его системные
# библиотеки. Именно поэтому в контейнере не нужен костыль
# ~tgclaude/chrome-libs/extract/usr/lib/... , которым это лечилось на хосте.
FROM mcr.microsoft.com/playwright/python:v1.62.0-noble
# В этом образе лежат только браузеры (/ms-playwright, PLAYWRIGHT_BROWSERS_PATH
# уже выставлен), а python-пакета playwright нет — его ставим сами, и версию
# фиксируем ровно 1.62.0 = версия из uv.lock проекта = ревизия chromium-1234,
# вкомпилированная в этот тег. Разъезд версии пакета и образа даёт
# "Executable doesn't exist" при launch(), поэтому теги менять только парой.
#
# Остальное — по фактическим импортам обоих сервисов из этого образа:
# collector/fetcher → playwright
# storage/analyze → pillow
# daemon → apscheduler (tzlocal подтянётся зависимостью)
# app/mcp_server → mcp (с ним приходят starlette, uvicorn, httpx)
# Версии — из uv.lock проекта, чтобы образ и хостовый venv не разъезжались.
RUN pip install --no-cache-dir \
playwright==1.62.0 \
pillow==12.3.0 \
apscheduler==3.11.3 \
mcp==1.29.1
# Исходники НЕ копируются в образ — они монтируются снаружи (см. compose),
# чтобы правки в проекте применялись рестартом контейнера, без пересборки.
#
# HOME задан не случайно: collector.py безусловно вызывает Path.home(), а uid
# 1001 внутри образа не имеет записи в /etc/passwd — без HOME expanduser
# упадёт с RuntimeError.
ENV PYTHONPATH=/app/src \
PYTHONUNBUFFERED=1 \
IMGBOT_DATA_DIR=/home/tgclaude/imgbot-data \
HOME=/tmp
WORKDIR /app
CMD ["python3", "-c", "from imgbot.daemon import main; main()"]
+68
View File
@@ -0,0 +1,68 @@
# imgbot-daemon
Контейнер для фонового демона проекта **imgbot** — headless-поиска и скачивания
картинок. Проект принадлежит агенту, живёт в `/home/tgclaude/imgbot` и правится
им же; здесь только среда запуска и присмотр.
## Зачем
До этого демон работал как голый процесс на хосте: запущен руками из
SSH-сессии, `PPID=1`, без юнита и restart-политики, `Linger=no` у пользователя.
Держался только потому, что в Ubuntu `KillUserProcesses=no` — **после
перезагрузки не поднялся бы**, и сигнала об этом не было бы. Плюс его 15%
ядра не были ничем ограничены и не были видны мониторингу.
Контейнер даёт: автозапуск и `restart: unless-stopped`, потолки CPU/памяти,
видимость в cAdvisor (а значит и в Grafana), и снимает костыль с руками
распакованными библиотеками chromium.
## Что тут НЕ запускается и почему
Два других процесса проекта — `imgbot-mcp-http` (порт 8765) и
`imgbot-file-server` (8766) — оставлены вне контейнеров сознательно:
- оба **жёстко забиндены в коде на `172.22.0.1`** — адрес шлюза docker-сети
`openclaw_default`. В контейнере такой адрес не поднять, а сама привязка
ломается при пересоздании сети;
- до них всё равно никто не дотягивается: единственный живой потребитель —
агент в контейнере `claude-channels` — находится в другой docker-сети и
ходит через SSH, а не по HTTP (проверено: `curl` из контейнера даёт `000`);
- `imgbot-mcp-http` вообще без авторизации, в отличие от file-server с его
Bearer-токеном.
Их имеет смысл контейнеризовать только вместе с правкой бинда на `0.0.0.0`
(две строки: `HOST` в `file_server.py`, `host=` в `mcp_server.py`) и уже в
общей docker-сети с `claude-channels` — тогда агент получит MCP по токену
вместо SSH. Это решение владельца кода, не инфраструктуры.
## Запуск
Собрать и поднять (на сервере каталог стека — `~/imgbot-daemon/`, чтобы не путался с самим проектом в `~tgclaude/imgbot`):
docker compose build
# погасить запущенный руками демон, иначе будет два и двойной analyze:
pkill -u tgclaude -f imgbot-daemon
docker compose up -d
docker logs -f imgbot-daemon
Проверка, что расписание пошло: в `~tgclaude/imgbot-data/daemon.log` должны
появляться строки `_job_analyze ... executed successfully` каждые 5 минут и
`_job_refresh` каждые 30.
## Откат
docker compose down
# и вернуть как было, из-под tgclaude:
cd ~/imgbot && .venv/bin/imgbot-daemon >> ~/imgbot-data/daemon.log 2>&1 &
## Важно для владельца кода
Перезапуск демона теперь `docker restart imgbot-daemon`, а не `pkill` плюс
ручной старт. Если запустить процесс руками при живом контейнере — получатся
два демона, которые будут конкурировать за одни и те же строки SQLite
(в проекте это уже случалось с cron без `flock` и выглядело как зависание).
Правки кода пересборки не требуют: исходники монтируются read-only, достаточно
`docker restart imgbot-daemon`. Пересобирать нужно только при изменении
зависимостей в `pyproject.toml` — и тогда версию playwright в `Dockerfile`
надо держать совпадающей с `uv.lock`, иначе разойдётся ревизия браузера.
+112
View File
@@ -0,0 +1,112 @@
name: imgbot
services:
imgbot-daemon:
build: .
image: imgbot-daemon:local
container_name: imgbot-daemon
restart: unless-stopped
# uid:gid пользователя tgclaude: .env лежит с mode 600, данные принадлежат
# ему же. Под другим uid контейнер не прочитает ключи и не запишет картинки.
user: "1001:1001"
volumes:
# Исходники read-only: правки агента применяются `docker restart`.
- /home/tgclaude/imgbot:/app:ro
# Данные: images/, manifest.db, куки google_state/reddit_state, логи.
# ВАЖНО: путь внутри контейнера обязан совпадать с путём на хосте.
# И проверять надо ВСЕ префиксы file_path в манифесте, а не только этот
# (был второй корень — легаси-архив ~tgclaude/images, снесён 2026-09-08):
# select substr(file_path,1,28), count(*) from images group by 1;
# В SQLite `images.file_path` лежит абсолютным путём хоста, и его читают
# как этот контейнер, так и оставшийся на хосте imgbot-file-server.
# Любое другое место монтирования ломает обе стороны: analyze падает с
# FileNotFoundError на старых строках, а новые строки становятся
# нечитаемыми для хостовых процессов.
- /home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data
# Таймзона хоста: без этого контейнер пишет в общий daemon.log в UTC,
# а строки от прежнего хостового запуска — в CEST, и лог мешает два пояса.
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
IMGBOT_DATA_DIR: /home/tgclaude/imgbot-data
PYTHONPATH: /app/src
HOME: /tmp
# .env подхватывается самим приложением: config.py ищет его как
# parents[2]/.env относительно пакета, то есть /app/.env через монтирование.
# Через env_file не пробрасываем, чтобы ключи не размножать.
# chromium падает на дефолтных 64 МБ /dev/shm
shm_size: 1gb
# Демон ровно жрёт ~15% ядра постоянно (analyze каждые 5 мин, 3 vision-запроса
# на картинку). Потолок — чтобы refresh с playwright не разогнался на все 8.
cpus: "2.0"
mem_limit: 3g
# Лог остаётся там, где его читает агент, и дублируется в `docker logs`.
command: >
sh -c 'python3 -c "from imgbot.daemon import main; main()" 2>&1 | tee -a /home/tgclaude/imgbot-data/daemon.log'
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Объединённый HTTP-сервис: MCP (/mcp) и раздача картинок (/photo/<id>) в
# одном процессе. Раньше это были два процесса на двух портах, оба
# захардкоженные на 172.22.0.1 — адрес шлюза docker-сети openclaw_default.
imgbot-http:
build: .
image: imgbot-daemon:local
container_name: imgbot-http
restart: unless-stopped
user: "1001:1001"
volumes:
- /home/tgclaude/imgbot:/app:ro
- /home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
IMGBOT_DATA_DIR: /home/tgclaude/imgbot-data
PYTHONPATH: /app/src
HOME: /tmp
# URL, который сервис выдаёт в image_url: имя контейнера в общей
# docker-сети с claude-channels, откуда его и качает агент.
IMGBOT_PUBLIC_BASE: http://imgbot-http:8765
# Порт НЕ публикуется на хост: сервис доступен только из docker-сетей,
# к которым подключён контейнер (и с самой машины). Авторизации у него
# нет сознательно — см. README проекта.
expose:
- "8765"
networks:
- default
- claude-channels
# request_photos гоняет playwright внутри этого же процесса, поэтому
# лимиты и shm такие же, как у демона.
shm_size: 1gb
cpus: "2.0"
mem_limit: 3g
command: python3 -c "from imgbot.app import main; main()"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
# Сеть проекта claude-channels: агент внутри того контейнера обращается к
# imgbot по имени imgbot-http. Тот же приём, что у gitea с monitoring.
claude-channels:
external: true
name: claude-channels_default
-1
View File
@@ -1 +0,0 @@
FTP_PASS=changeme
-38
View File
@@ -1,38 +0,0 @@
name: ipcam
services:
ipcam-ftps:
image: fauria/vsftpd
container_name: ipcam-ftps
restart: unless-stopped
environment:
FTP_USER: cam
FTP_PASS: ${FTP_PASS}
PASV_ENABLE: "YES"
PASV_ADDRESS: "cam.ruzzy.dev"
PASV_ADDR_RESOLVE: "YES"
PASV_MIN_PORT: "30000"
PASV_MAX_PORT: "30010"
PASV_PROMISCUOUS: "NO"
PORT_PROMISCUOUS: "NO"
TLS_ENABLE: "YES"
TLS_FORCE_LOCAL_LOGINS_SSL: "YES"
TLS_FORCE_LOCAL_DATA_SSL: "YES"
TLS_CERT_FILE: /etc/ssl/private/vsftpd.crt
TLS_KEY_FILE: /etc/ssl/private/vsftpd.key
LOG_STDOUT: "YES"
XFERLOG_STD_FORMAT: "NO"
FILE_OPEN_MODE: "0666"
LOCAL_UMASK: "077"
REVERSE_LOOKUP_ENABLE: "YES"
ports:
- "2221:21"
- "30000-30010:30000-30010"
volumes:
- /srv/ftps/certs/vsftpd.crt:/etc/ssl/private/vsftpd.crt:ro
- /srv/ftps/certs/vsftpd.key:/etc/ssl/private/vsftpd.key:ro
- /srv/ipcam/videos:/home/vsftpd
- ipcam_logs:/var/log/vsftpd
volumes:
ipcam_logs:
+25
View File
@@ -0,0 +1,25 @@
# ru1: ~/gost/docker-compose.yml (каталог и имя проекта `gost` — исторические).
# Фронт WEB-прокси Telegram. Сам relay и MTProxy — systemd на хосте
# (setup-ru1.sh), nginx в host-network проксирует на 127.0.0.1:8090.
services:
nginx:
image: nginx:alpine
container_name: nginx
network_mode: host
restart: unless-stopped
# certbot живёт в отдельном контейнере и не может дёрнуть nginx —
# перечитываем сертификаты раз в 6 часов сами.
command: ["/bin/sh", "-c", "while :; do sleep 6h; nginx -s reload; done & exec nginx -g 'daemon off;'"]
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
certbot:
image: certbot/certbot
container_name: certbot
restart: unless-stopped
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done;'"
+67
View File
@@ -0,0 +1,67 @@
events {}
http {
server_tokens off;
# Ничего не логируем по ru.ruzzy.dev: в URI bridge-страницы — capability,
# в Sec-WebSocket-Protocol — токен сессии (требование tproxy-server).
access_log off;
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
upstream tproxy_relay {
server 127.0.0.1:8090; # tproxy-server (systemd), 8080 занят cadvisor
keepalive 32;
}
server {
listen 80;
server_name ru.ruzzy.dev;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# Mode A из tproxy-server: relay — единственный шлюз хоста, он же отдаёт
# публичный сайт (/srv/tproxy-site). Никаких отдельных location под сайт,
# иначе ответы сайта и транспорта станут различимы для зондирования.
server {
listen 443 ssl;
http2 on;
server_name ru.ruzzy.dev;
ssl_certificate /etc/letsencrypt/live/ru.ruzzy.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ru.ruzzy.dev/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
# Пачки uplink до 2 MiB; дефолтный 1 MiB ломает транспорт с 413.
client_max_body_size 4m;
location / {
proxy_pass http://tproxy_relay;
proxy_http_version 1.1;
proxy_set_header Host $host;
# Ровно один адрес: список relay отвергает.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# long poll relay — 25 с; не резать и не буферизовать.
proxy_read_timeout 90s;
proxy_send_timeout 90s;
proxy_buffering off;
proxy_request_buffering off;
}
}
}
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Однократная установка WEB-прокси Telegram (tproxy-server + официальный MTProxy)
# на ru1. Запускать от root из каталога с исходниками tproxy-server:
# sudo bash /tmp/tproxy-server/setup-ru1.sh
# Повторный запуск безопасен: секрет и token.key не перегенерируются.
#
# Отличия от deploy/install.sh апстрима:
# - без Caddy: 80/443 держит nginx в docker (~/gost), он проксирует всё в relay;
# - relay на 127.0.0.1:8090/8091 — 8080 занят cadvisor (*:8080);
# - NAT задан вручную: трафик к Telegram идёт через wg0 и выходит с IP fr1,
# а автоопределение install.sh спросило бы ifconfig-сервис через ens3
# и получило 85.198.109.86 — MTProxy молча перестал бы отвечать клиентам;
# - без base path: его пока понимает только Desktop.
set -euo pipefail
umask 077
repo="$(cd "$(dirname "$0")" && pwd)"
hostname=ru.ruzzy.dev
nat_args="--nat-info 10.8.1.4:161.97.93.252"
[[ "${EUID}" -eq 0 ]] || { echo "run as root" >&2; exit 1; }
cd "$repo"
# 1. Официальный MTProxy (закреплённый коммит, sha256) + конфиг Telegram.
bash deploy/install-mtproxy.sh
# 2. Relay.
id tproxy >/dev/null 2>&1 || useradd --system --home /nonexistent --shell /usr/sbin/nologin tproxy
install -o root -g root -m 0755 tproxy-server.bin /usr/local/bin/tproxy-server
install -d -o root -g root -m 0755 /srv/tproxy-site
install -o root -g root -m 0644 /home/mik/gost/nginx/html/index.html /srv/tproxy-site/index.html
install -d -o root -g tproxy -m 0750 /etc/tproxy-server
bash deploy/ensure-token-key.sh
if [[ ! -f /etc/tproxy-server/profiles.json ]]; then
secret="$(openssl rand -hex 16)"
printf '{"profiles":[{"name":"default","secret":"%s","backend":"127.0.0.1:2398"}]}\n' \
"$secret" > /etc/tproxy-server/profiles.json
printf 'MTPROXY_SECRET=%s\nMTPROXY_NAT_ARGS=%s\n' "$secret" "$nat_args" > /etc/mtproxy/mtproxy.env
unset secret
fi
cat > /etc/tproxy-server/config.json <<EOF
{
"public_hostname": "$hostname",
"base_path": "",
"listen": "127.0.0.1:8090",
"admin_listen": "127.0.0.1:8091",
"public_dir": "/srv/tproxy-site",
"static_routes": "exact",
"profiles_file": "/run/credentials/tproxy-server.service/profiles.json"
}
EOF
chown root:tproxy /etc/tproxy-server/config.json /etc/tproxy-server/profiles.json
chmod 0640 /etc/tproxy-server/config.json
chmod 0400 /etc/tproxy-server/profiles.json
chown root:mtproxy /etc/mtproxy/mtproxy.env
chmod 0640 /etc/mtproxy/mtproxy.env
# 3. Юниты, firewall (2398/8888 только с lo), ежедневное обновление конфига Telegram.
for unit in tproxy-server.service mtproxy.service tproxy-firewall.service \
refresh-mtproxy-config.service refresh-mtproxy-config.timer; do
install -o root -g root -m 0644 "deploy/$unit" "/etc/systemd/system/$unit"
done
install -o root -g root -m 0644 deploy/firewall.nft /etc/tproxy-server/firewall.nft
install -o root -g root -m 0755 deploy/refresh-mtproxy-config.sh /usr/local/sbin/refresh-mtproxy-config
/usr/local/bin/tproxy-server -config /etc/tproxy-server/config.json \
-profiles-file /etc/tproxy-server/profiles.json -check
systemctl daemon-reload
systemctl enable --now tproxy-firewall.service
systemctl enable --now mtproxy.service
systemctl restart mtproxy.service
systemctl enable --now tproxy-server.service
systemctl enable --now refresh-mtproxy-config.timer
for _ in $(seq 20); do
curl -fs -o /dev/null http://127.0.0.1:8091/readyz && break
sleep 1
done
curl -fsS http://127.0.0.1:8091/readyz >/dev/null && echo "relay: ready"
echo "Ссылка для клиента: sudo sed -n 's/.*\"secret\":\"\\([0-9a-f]*\\)\".*/https:\\/\\/t.me\\/webproxy?server=$hostname\\&secret=\\1/p' /etc/tproxy-server/profiles.json"