Compare commits
3
Commits
123cdaa19b
...
d1269162e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1269162e7 | ||
|
|
3604e843c8 | ||
|
|
2ab38bc1f8 |
@@ -5,7 +5,8 @@
|
|||||||
## Серверы
|
## Серверы
|
||||||
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан.
|
- [`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/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 клиентов, telemt), умный роутинг RU/зарубеж. ⚠️ Файл описания сверялся до переключения.
|
- [`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/rpi.md`](servers/rpi.md) — `rpi.local` (192.168.10.25, дом, за NAT). Raspberry Pi 4: бэкап личного OneDrive на HDD 4 ТБ (rclone, вечный архив версий, еженедельная сверка хэшей); SMART и статус бэкапов пушит в pushgateway fr1 по SSH. Код — `stacks/rpi-backup/`.
|
||||||
|
|
||||||
## Поддержание описаний в актуальном состоянии
|
## Поддержание описаний в актуальном состоянии
|
||||||
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
|
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
|
||||||
|
|||||||
+6
-2
@@ -15,6 +15,7 @@
|
|||||||
- ⚠️ **Группа `docker` = root-эквивалент.** `docker:x:988:rus,tgclaude` — любой её член монтирует `/` в контейнер и делает что угодно от рута (этим же способом чистились root-овы файлы 2026-09-08). Поэтому «у `tgclaude` нет sudo» **не** означает ограничения прав на хосте.
|
- ⚠️ **Группа `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).
|
- Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
|
||||||
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
|
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
|
||||||
|
- **Пуш-ключ `metrics-push@rpi`** (с 2026-10-11) в `~rus/.ssh/authorized_keys` с `restrict,command="/home/rus/bin/pushgw-ingest rpi"`: этим ключом домашняя малина может только залить метрики в pushgateway — шелла, форвардинга и pty нет. Бэкап прежнего файла — `~/.ssh/authorized_keys.bak-20261011`.
|
||||||
|
|
||||||
## SSH (`/etc/ssh/sshd_config.d/00-hardening.conf`)
|
## 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`.
|
⚠️ В основном `/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`.
|
||||||
@@ -104,8 +105,8 @@ backend=systemd banaction=ufw
|
|||||||
- Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
|
- Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
|
||||||
|
|
||||||
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
|
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
|
||||||
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager** (docker compose).
|
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager + Pushgateway** (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).
|
- **Алерты:** Alertmanager → Telegram (бот @ultimate_log_bot, chat 607015). Правила `prometheus/rules/alerts.yml`: TargetDown, HostHighCPU, HostLowMemory, HostDiskLow/Critical; группа `rpi` (с 2026-10-11) — SMART и бэкапы малины: PushMetricsStale, SmartHealthFailed, SmartBadSectors(+Growing), SmartUdmaCrcErrors, SmartSelftestFailed, SmartTemperatureHigh, BackupFailed, BackupStale. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git).
|
||||||
- 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, активных алертов нет.
|
- 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}`).
|
- 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` — пустой.
|
- **Тома:** `monitoring_prometheus_data` — **6.5 ГБ** (при retention 90d; самый крупный том на сервере, учитывать при планировании диска), `monitoring_grafana_data` — 50 МБ, `monitoring_alertmanager_data` — пустой.
|
||||||
@@ -113,6 +114,9 @@ backend=systemd banaction=ufw
|
|||||||
- **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).
|
- **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 скрыты).
|
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты).
|
||||||
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
|
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
|
||||||
|
- **Pushgateway** (с 2026-10-11) — для хостов за NAT, которых не скрейпнуть (домашняя `rpi`, см. `servers/rpi.md`). Слушает только `127.0.0.1:9091`; job `pushgateway` с `honor_labels: true` (иначе job/instance заменились бы на `pushgateway:9091`). Персистентность в томе `monitoring_pushgateway_data` — рестарт не обнуляет метрики. **Как приходят данные:** хост делает `ssh rus@fr1 <job> < metrics.prom`, forced command `~/bin/pushgw-ingest <instance>` делает `PUT /metrics/job/<job>/instance/<instance>`. instance зашит в authorized_keys, а не берётся от клиента — хост не может писать от чужого имени; имя job фильтруется по `[a-z0-9_]`. Наружу порт не открыт, nginx/ufw не трогали.
|
||||||
|
- ⚠️ Pushgateway хранит последнее значение вечно: если хост умер, метрики не исчезнут, а «застынут». Поэтому свежесть ловит отдельный алерт `PushMetricsStale` по `push_time_seconds`.
|
||||||
|
- ⚠️ Живой `~/monitoring/docker-compose.yml` расходится с репо (пароль Grafana вписан литералом), поэтому при деплое 2026-10-11 compose **не копировался**, а блок pushgateway вставлен на месте. Бэкапы: `docker-compose.yml.bak-20261011`, `prometheus/prometheus.yml.bak-20261011`.
|
||||||
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
|
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
|
||||||
|
|
||||||
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
|
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# rpi — домашняя Raspberry Pi 4 (`rpi.local`, 192.168.10.25)
|
||||||
|
|
||||||
|
Поднята 2026-10-11. Домашний сервер за NAT: первая задача — бэкап личного
|
||||||
|
OneDrive на внешний HDD. Мониторится с fr1 через pushgateway (скрейпить
|
||||||
|
её снаружи нельзя — она за домашним роутером).
|
||||||
|
|
||||||
|
## Железо и ОС
|
||||||
|
- Raspberry Pi 4, SD 64 ГБ (`mmcblk0`), питание штатное — `vcgencmd get_throttled` = `0x0`.
|
||||||
|
- Raspberry Pi OS Lite 64-bit **Trixie** (Debian 13), образ `2026-10-06-raspios-trixie-arm64-lite`.
|
||||||
|
- Headless-настройка — **cloud-init**, а не старые `ssh`/`userconf.txt`: в Trixie Pi Imager пишет `user-data`/`network-config` на `bootfs`. Наш `user-data` создал пользователя, ключ, hostname, таймзону MSK.
|
||||||
|
- Сеть: Ethernet, DHCP. MAC `dc:a6:32:db:b0:f9`, роутер `192.168.10.254`. ⏳ Резерв адреса на роутере — на пользователе (см. хвосты).
|
||||||
|
|
||||||
|
## Доступ
|
||||||
|
- С Mac: `ssh rpi` (алиас → `rpi.local`, user `rus`, ключ `~/.ssh/id_ed25519`). Резолвится через mDNS (avahi в образе по умолчанию).
|
||||||
|
- Только по ключу: `passwordauthentication no`, у `rus` пароль заблокирован, **sudo NOPASSWD** (иначе sudo был бы недоступен вовсе).
|
||||||
|
|
||||||
|
## Диски
|
||||||
|
| Устройство | Модель | Роль |
|
||||||
|
|---|---|---|
|
||||||
|
| `sda` | WD My Passport 4 ТБ (WD40NDZW, s/n WD-WX82D6228YCD) | ext4 `backup` → `/mnt/backup` |
|
||||||
|
| — | WD Elements 1 ТБ (WD10SDRW) | **не подключён**, ждёт хаба (APFS, не отформатирован) |
|
||||||
|
|
||||||
|
- fstab по UUID с `nofail,x-systemd.device-timeout=30s` (`stacks/rpi-backup/fstab.line`): без диска малина грузится, бэкап просто пропускается (`mountpoint -q`).
|
||||||
|
- ⚠️ **Два 2.5" HDD от USB Pi 4 не тянут.** Проверено 2026-10-11: при раскрутке обоих — `over-current` на всех USB3-портах; под нагрузкой 4-ТБ диск отваливался (`USB disconnect`) и залипал в `Unit Not Ready`. Один диск стабилен (20 ГБ чтения 103 МБ/с, mkfs ~60 ГБ записи без сбросов). Второй диск — только через USB3-хаб с собственным питанием.
|
||||||
|
- Залипший после отвала диск лечится power-cycle всех USB (Pi 4 переключает питание портов только разом): `sudo uhubctl -l 2 -a off; sudo uhubctl -l 1-1 -a off; sleep 8; …-a on`.
|
||||||
|
- SMART на момент ввода: 9 897 ч, 0 переназначенных/ожидающих, Start_Stop 21 946, Load_Cycle 42 530.
|
||||||
|
|
||||||
|
## Бэкап OneDrive (код — `stacks/rpi-backup/`)
|
||||||
|
- rclone (официальный .deb, v1.75.2 — в Debian 1.60, старый), remote `onedrive:` = личный OneDrive (drive_type personal). Токен — в `~rus/.config/rclone/rclone.conf` (600), только на малине. Получен через `rclone authorize onedrive` на Mac.
|
||||||
|
- `onedrive-backup` (таймер каждые 4 ч, `00/4:30`): `rclone sync` в `/mnt/backup/onedrive/current`, всё удалённое/перезаписанное в облаке — в `archive/<ts>/` **навсегда** (пользователь хочет иметь всё, что когда-либо было в OneDrive). Хэши архива — `manifests/archive-<ts>.qxh`, лог каждого прогона — `logs/sync-<ts>.log` (INFO: поштучно что скачано/убрано в архив).
|
||||||
|
- Целостность: rclone сверяет QuickXorHash каждого скачанного файла с OneDrive; `onedrive-verify` (вс 05:00) перечитывает весь HDD: `rclone check` current↔облако + `rclone checksum` archive↔манифесты. Отчёт — `logs/verify-<ts>*`.
|
||||||
|
- Оба юнита под общим `flock /run/lock/onedrive-backup.lock` — не идут параллельно и не убивают друг друга.
|
||||||
|
- ⚠️ Файл, загруженный и удалённый в облаке между прогонами (<4 ч), в бэкап не попадёт.
|
||||||
|
- ⚠️ `Personal Vault` исключён — rclone через API туда не ходит.
|
||||||
|
- ⚠️ Архив без ротации: растёт вечно. Скрипт выходит с кодом 2 при заполнении >90 % (→ алерт BackupFailed), плюс HostDisk* если когда-то поставим node-exporter.
|
||||||
|
- Первая полная выгрузка (368 ГБ) запущена 2026-10-11 ~13:13, ~27 МБ/с (упор в домашний канал).
|
||||||
|
|
||||||
|
## Мониторинг → fr1
|
||||||
|
- `smartd` (`stacks/rpi-backup/smartd.conf`): `DEVICESCAN -d removable`, `-n standby,q` (не будить диск), короткий самотест сб 03:00, длинный 1-го числа 05:00. ⚠️ `-d sat` с DEVICESCAN не работает («Not a SAT device») — тип моста пусть определяет сам.
|
||||||
|
- `smart-metrics` (таймер 15 мин): smartctl `-n standby` → метрики `smart_*` → job `smart`. Спящий диск не будится: отдаются значения из кэша `/var/lib/smart-metrics/` + `smart_disk_standby=1`.
|
||||||
|
- `backup-metrics <job>` — `ExecStopPost=+` у юнитов бэкапа: `backup_last_exit_code`, `backup_last_run_timestamp`, `backup_last_success_timestamp` (время успеха хранится локально в `/var/lib/backup-metrics/`, т.к. PUT заменяет группу целиком).
|
||||||
|
- Доставка: `metrics-push <job>` = ssh с ключом `/etc/metrics-push/id_ed25519` (root, 600) на `rus@161.97.93.252`; host key fr1 закреплён в `/etc/metrics-push/known_hosts`. На fr1 ключ ограничен forced command (`servers/fr1.md` → Мониторинг).
|
||||||
|
- Алерты — группа `rpi` в `stacks/monitoring/prometheus/rules/alerts.yml`.
|
||||||
|
|
||||||
|
## Открытые хвосты (на 2026-10-11)
|
||||||
|
- ⏳ Закрепить `192.168.10.25` за MAC `dc:a6:32:db:b0:f9` на роутере.
|
||||||
|
- ⏳ USB3-хаб с питанием → подключить и отформатировать WD Elements 1 ТБ (вторая копия архива — сейчас копия одна).
|
||||||
|
- Нет node-exporter (CPU/RAM/диск самой малины в Prometheus не видны) — пуш через pushgateway для node-метрик неудобен; решить, нужен ли.
|
||||||
|
- Первый прогон бэкапа стартовал до появления `ExecStopPost` — его итог может не дойти в метрики; `backup_last_success_timestamp` появится с первого прогона по таймеру. До этого `BackupStale` молчит (серии нет).
|
||||||
|
- Через неделю снять SMART и посчитать реальный прирост Start_Stop/Load_Cycle — прогноз ресурса пока оценочный.
|
||||||
+39
-5
@@ -1,7 +1,7 @@
|
|||||||
# ru1 — `85.198.109.86` (RU entry node)
|
# ru1 — `85.198.109.86` (RU entry node)
|
||||||
|
|
||||||
VPN entry-нода в РФ. Точка входа в топологии **RU↔DE**: умный роутинг (RU-трафик → напрямую, зарубежный → туннель AWG до de1). Полный runbook — `~/README.md` на сервере.
|
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 · РФ
|
**Провайдер:** VDSina · РФ
|
||||||
**ОС:** Ubuntu 24.04.3 LTS · kernel 6.8.0-78
|
**ОС:** Ubuntu 24.04.3 LTS · kernel 6.8.0-78
|
||||||
@@ -27,20 +27,45 @@ VPN entry-нода в РФ. Точка входа в топологии **RU↔D
|
|||||||
| OpenVPN #2 | 1195/udp (`tun1`) | RU-выход (весь трафик через ens3, DNS Yandex). Cfg `/etc/openvpn/server/ruopenvpn.conf` |
|
| 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` |
|
| 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/` |
|
| 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/` |
|
| WEB-прокси Telegram | 443 (через nginx) | `tproxy-server` (relay, systemd, `127.0.0.1:8090`, admin `:8091`) → официальный MTProxy (systemd, `2398`, stats `8888`, оба закрыты nftables для не-lo). Подробно — раздел ниже |
|
||||||
| nginx | 80, 4443 | маскировочный сайт `ru.ruzzy.dev` (Docker `nginx:alpine`) |
|
| 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)
|
## Умный роутинг (tun0 и wg1)
|
||||||
- `ipset ru_ips` (~11400 подсетей, источник ipdeny.com). До routing decision (mangle PREROUTING):
|
- `ipset ru_ips` (~11400 подсетей, источник ipdeny.com). До routing decision (mangle PREROUTING):
|
||||||
- dst ∈ ru_ips → RETURN (без метки) → main table → ens3 (RU напрямую)
|
- dst ∈ ru_ips → RETURN (без метки) → main table → ens3 (RU напрямую)
|
||||||
- иначе → MARK `0x64` → `ip rule fwmark 0x64 → table vpn (100)` → default dev wg0 → de1
|
- иначе → 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`).
|
- Обновление списка: `~/smart-routing/update-ru-ips.sh`, cron вс 03:00. Восстановление при ребуте: `ipset-restore.service` (до `awg-quick@wg0`).
|
||||||
- tun1 — без умного роутинга (всегда RU/ens3).
|
- tun1 — без умного роутинга (всегда RU/ens3).
|
||||||
|
|
||||||
## AWG Watchdog
|
## 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 отключён.
|
- OpenVPN: `~/gen_client.sh --profile de|ru|both <имя>`, PKI `~/easy-rsa/pki`, профили `~/clients/`. CRL отключён.
|
||||||
@@ -60,3 +85,12 @@ VPN entry-нода в РФ. Точка входа в топологии **RU↔D
|
|||||||
## ⚠️ Грабли (из README)
|
## ⚠️ Грабли (из README)
|
||||||
- `wg1.conf` обязан иметь `Table = off` — иначе wg-quick при рестарте сносит default route и сервер теряет SSH (восстановление: `ip route add default via 85.198.109.1 dev ens3`).
|
- `wg1.conf` обязан иметь `Table = off` — иначе wg-quick при рестарте сносит default route и сервер теряет SSH (восстановление: `ip route add default via 85.198.109.1 dev ens3`).
|
||||||
- Таблица маршрутизации — `vpn` (100), не `awg`.
|
- Таблица маршрутизации — `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`.
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ services:
|
|||||||
- '127.0.0.1:9093:9093'
|
- '127.0.0.1:9093:9093'
|
||||||
networks: [monitoring]
|
networks: [monitoring]
|
||||||
|
|
||||||
|
# pushgateway: метрики от хостов за NAT (rpi), которые не скрейпнуть.
|
||||||
|
# Порт только на 127.0.0.1 — пуш идёт по SSH через forced command ~/bin/pushgw-ingest
|
||||||
|
# (см. servers/fr1.md), наружу не торчит. Персистентность — чтобы рестарт не обнулял алерты.
|
||||||
|
pushgateway:
|
||||||
|
image: prom/pushgateway:latest
|
||||||
|
container_name: pushgateway
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- '--persistence.file=/data/metrics'
|
||||||
|
- '--persistence.interval=1m'
|
||||||
|
volumes:
|
||||||
|
- pushgateway_data:/data
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:9091:9091'
|
||||||
|
networks: [monitoring]
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:latest
|
image: grafana/grafana:latest
|
||||||
container_name: grafana
|
container_name: grafana
|
||||||
@@ -93,6 +109,7 @@ services:
|
|||||||
networks: [monitoring]
|
networks: [monitoring]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
pushgateway_data:
|
||||||
prometheus_data:
|
prometheus_data:
|
||||||
grafana_data:
|
grafana_data:
|
||||||
alertmanager_data:
|
alertmanager_data:
|
||||||
|
|||||||
@@ -35,3 +35,9 @@ scrape_configs:
|
|||||||
- job_name: openvpn
|
- job_name: openvpn
|
||||||
file_sd_configs:
|
file_sd_configs:
|
||||||
- files: ["/etc/prometheus/targets/openvpn/*.yml"]
|
- files: ["/etc/prometheus/targets/openvpn/*.yml"]
|
||||||
|
|
||||||
|
# honor_labels: сохраняем job/instance, с которыми хост запушил, а не pushgateway:9091
|
||||||
|
- job_name: pushgateway
|
||||||
|
honor_labels: true
|
||||||
|
static_configs:
|
||||||
|
- targets: ["pushgateway:9091"]
|
||||||
|
|||||||
@@ -35,3 +35,67 @@ groups:
|
|||||||
labels: {severity: critical}
|
labels: {severity: critical}
|
||||||
annotations:
|
annotations:
|
||||||
summary: "КРИТИЧНО: диск почти полон на {{ $labels.instance }} ({{ $labels.mountpoint }}): свободно {{ printf \"%.0f\" $value }}%"
|
summary: "КРИТИЧНО: диск почти полон на {{ $labels.instance }} ({{ $labels.mountpoint }}): свободно {{ printf \"%.0f\" $value }}%"
|
||||||
|
|
||||||
|
# Метрики от rpi приходят через pushgateway (SSH-пуш, см. servers/rpi.md).
|
||||||
|
# push_time_seconds — когда группа последний раз обновлялась.
|
||||||
|
- name: rpi
|
||||||
|
rules:
|
||||||
|
- alert: PushMetricsStale
|
||||||
|
expr: time() - push_time_seconds{job=~"smart|backup_.*"} > 3 * 3600
|
||||||
|
for: 10m
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: метрики {{ $labels.job }} не приходят {{ $value | humanizeDuration }} — хост/пуш лежит"
|
||||||
|
|
||||||
|
- alert: SmartHealthFailed
|
||||||
|
expr: smart_health_passed == 0
|
||||||
|
labels: {severity: critical}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: SMART FAILED на диске {{ $labels.model }} ({{ $labels.serial }}) — менять диск"
|
||||||
|
|
||||||
|
- alert: SmartBadSectors
|
||||||
|
expr: smart_attribute_raw{id=~"5|197|198"} > 0
|
||||||
|
labels: {severity: critical}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: {{ $labels.name }} = {{ $value }} на {{ $labels.model }} ({{ $labels.serial }}) — диск начал сыпаться"
|
||||||
|
|
||||||
|
- alert: SmartBadSectorsGrowing
|
||||||
|
expr: increase(smart_attribute_raw{id=~"5|197|198"}[1d]) > 0
|
||||||
|
labels: {severity: critical}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: {{ $labels.name }} растёт на {{ $labels.model }} — срочно вторую копию"
|
||||||
|
|
||||||
|
- alert: SmartUdmaCrcErrors
|
||||||
|
expr: increase(smart_attribute_raw{id="199"}[1d]) > 0
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: CRC-ошибки на {{ $labels.model }} — кабель/USB/питание"
|
||||||
|
|
||||||
|
- alert: SmartSelftestFailed
|
||||||
|
expr: smart_selftest_last_passed == 0
|
||||||
|
labels: {severity: critical}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: самотест SMART провален на {{ $labels.model }} ({{ $labels.serial }})"
|
||||||
|
|
||||||
|
- alert: SmartTemperatureHigh
|
||||||
|
expr: smart_temperature_celsius > 55
|
||||||
|
for: 15m
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: диск {{ $labels.model }} греется: {{ $value }}°C"
|
||||||
|
|
||||||
|
- alert: BackupFailed
|
||||||
|
expr: backup_last_exit_code != 0
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: {{ $labels.job }} завершился с кодом {{ $value }} — см. journalctl на хосте"
|
||||||
|
|
||||||
|
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней
|
||||||
|
- alert: BackupStale
|
||||||
|
expr: |
|
||||||
|
(time() - backup_last_success_timestamp{job="backup_onedrive"} > 12 * 3600)
|
||||||
|
or (time() - backup_last_success_timestamp{job="backup_onedrive_verify"} > 8 * 86400)
|
||||||
|
for: 10m
|
||||||
|
labels: {severity: critical}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: {{ $labels.job }} не было успешного прогона {{ $value | humanizeDuration }}"
|
||||||
|
|||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Forced command для пуш-ключей хостов за NAT (authorized_keys: command="~/bin/pushgw-ingest <instance>").
|
||||||
|
# Клиент передаёт только имя job (ssh ... <job>), метрики — в stdin в text-формате Prometheus.
|
||||||
|
# instance задаётся ключом, а не клиентом: хост не может писать метрики от чужого имени.
|
||||||
|
instance="$1"
|
||||||
|
job="$SSH_ORIGINAL_COMMAND"
|
||||||
|
case "$instance" in ""|*[!a-z0-9_-]*) echo "bad instance" >&2; exit 2;; esac
|
||||||
|
case "$job" in ""|*[!a-z0-9_]*) echo "bad job" >&2; exit 2;; esac
|
||||||
|
exec curl -sS --fail-with-body --max-time 30 -X PUT --data-binary @- \
|
||||||
|
"http://127.0.0.1:9091/metrics/job/$job/instance/$instance"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# backup-metrics <job> — из ExecStopPost юнита бэкапа: пушит итог прогона (job=backup_<name>).
|
||||||
|
# last_success хранится локально: PUT в pushgateway заменяет группу целиком,
|
||||||
|
# и после неудачи время последнего успеха иначе бы потерялось.
|
||||||
|
set -uo pipefail
|
||||||
|
job="$1"; state=/var/lib/backup-metrics; mkdir -p "$state"
|
||||||
|
now=$(date +%s)
|
||||||
|
code=${EXIT_STATUS:-1}; [ "${SERVICE_RESULT:-}" = success ] && code=0
|
||||||
|
[ "$code" = 0 ] && echo "$now" > "$state/$job.success"
|
||||||
|
last_ok=$(cat "$state/$job.success" 2>/dev/null || echo 0)
|
||||||
|
metrics-push "$job" <<M
|
||||||
|
# TYPE backup_last_exit_code gauge
|
||||||
|
backup_last_exit_code $code
|
||||||
|
backup_last_run_timestamp $now
|
||||||
|
backup_last_success_timestamp $last_ok
|
||||||
|
M
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# metrics-push <job> < metrics.prom — отправка в pushgateway на fr1.
|
||||||
|
# На fr1 ключ привязан к forced command ~/bin/pushgw-ingest rpi: можно только запушить, больше ничего.
|
||||||
|
exec ssh -i /etc/metrics-push/id_ed25519 \
|
||||||
|
-o UserKnownHostsFile=/etc/metrics-push/known_hosts -o StrictHostKeyChecking=yes \
|
||||||
|
-o BatchMode=yes -o ConnectTimeout=20 \
|
||||||
|
rus@161.97.93.252 "$1"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# OneDrive -> /mnt/backup. Ничего не удаляется навсегда:
|
||||||
|
# current/ — точное зеркало OneDrive
|
||||||
|
# archive/<время>/ — всё, что в OneDrive удалили или перезаписали (хранится вечно)
|
||||||
|
# manifests/ — quickxor-хэши архивных файлов (для onedrive-verify)
|
||||||
|
# logs/ — журнал каждого прогона: какие файлы скачаны/изменены/убраны в архив
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT=/mnt/backup/onedrive
|
||||||
|
TS=$(date +%F_%H%M)
|
||||||
|
|
||||||
|
mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; }
|
||||||
|
mkdir -p "$ROOT"/{current,archive,manifests,logs}
|
||||||
|
LOG="$ROOT/logs/sync-$TS.log"
|
||||||
|
|
||||||
|
# rclone сам сверяет хэш каждого скачанного файла с хэшем OneDrive
|
||||||
|
rclone sync onedrive: "$ROOT/current" \
|
||||||
|
--backup-dir "$ROOT/archive/$TS" \
|
||||||
|
--exclude "/Personal Vault/**" \
|
||||||
|
--transfers 4 --checkers 8 \
|
||||||
|
--retries 5 --low-level-retries 20 \
|
||||||
|
--log-file "$LOG" --log-level INFO \
|
||||||
|
--stats 10m --stats-one-line
|
||||||
|
|
||||||
|
if [ -d "$ROOT/archive/$TS" ]; then
|
||||||
|
rclone hashsum quickxor "$ROOT/archive/$TS" > "$ROOT/manifests/archive-$TS.qxh"
|
||||||
|
echo "в архив ушло: $(wc -l < "$ROOT/manifests/archive-$TS.qxh") файлов"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: current=$(du -sh "$ROOT/current" | cut -f1), диск занят на $(df --output=pcent /mnt/backup | tail -1 | tr -d " ")"
|
||||||
|
[ "$(df --output=pcent /mnt/backup | tail -1 | tr -dc 0-9)" -lt 90 ] || { echo "ВНИМАНИЕ: диск заполнен >90%"; exit 2; }
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Еженедельная проверка целостности: перечитывает весь HDD и сверяет хэши
|
||||||
|
# current/ — с хэшами OneDrive (ловит битые/недокачанные/лишние файлы)
|
||||||
|
# archive/ — с сохранёнными манифестами (ловит порчу данных на диске)
|
||||||
|
set -uo pipefail
|
||||||
|
ROOT=/mnt/backup/onedrive
|
||||||
|
TS=$(date +%F_%H%M)
|
||||||
|
LOG="$ROOT/logs/verify-$TS.log"
|
||||||
|
mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; }
|
||||||
|
rc=0
|
||||||
|
|
||||||
|
rclone check onedrive: "$ROOT/current" --exclude "/Personal Vault/**" \
|
||||||
|
--checkers 8 --combined "$ROOT/logs/verify-$TS.current.txt" \
|
||||||
|
--log-file "$LOG" --log-level NOTICE || rc=1
|
||||||
|
|
||||||
|
for m in "$ROOT"/manifests/archive-*.qxh; do
|
||||||
|
[ -e "$m" ] || continue
|
||||||
|
d=$(basename "$m" .qxh); d=${d#archive-}
|
||||||
|
rclone checksum quickxor "$m" "$ROOT/archive/$d" --log-file "$LOG" --log-level NOTICE || { echo "архив $d: несовпадение"; rc=1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
grep -vc "^=" "$ROOT/logs/verify-$TS.current.txt" | xargs echo "расхождений в current:"
|
||||||
|
[ $rc -eq 0 ] && echo "OK: целостность подтверждена" || echo "ЕСТЬ РАСХОЖДЕНИЯ — см. $LOG и verify-$TS.current.txt"
|
||||||
|
exit $rc
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# SMART всех USB/SATA-дисков → pushgateway (job=smart).
|
||||||
|
# -n standby: спящий диск НЕ будим (иначе опрос раз в 15 мин не дал бы ему спать) —
|
||||||
|
# для него отдаём последние снятые значения из кэша и smart_disk_standby=1.
|
||||||
|
set -uo pipefail
|
||||||
|
CACHE=/var/lib/smart-metrics; mkdir -p "$CACHE"
|
||||||
|
out=$(mktemp); trap 'rm -f "$out"' EXIT
|
||||||
|
echo "# TYPE smart_disk_standby gauge" >> "$out"
|
||||||
|
for dev in /dev/sd?; do
|
||||||
|
[ -b "$dev" ] || continue
|
||||||
|
json=$(smartctl -d sat -n standby,7 -j -H -i -A -l selftest "$dev"); rc=$?
|
||||||
|
serial=$(udevadm info -q property -n "$dev" | sed -n 's/^ID_SERIAL_SHORT=//p')
|
||||||
|
[ -n "$serial" ] || continue
|
||||||
|
f="$CACHE/$serial.prom"
|
||||||
|
if [ $rc -eq 7 ]; then
|
||||||
|
standby=1
|
||||||
|
else
|
||||||
|
standby=0
|
||||||
|
echo "$json" | jq -r --arg dev "${dev#/dev/}" '
|
||||||
|
(.model_name // "unknown") as $m | (.serial_number // "unknown") as $s |
|
||||||
|
"{model=\"\($m)\",serial=\"\($s)\",device=\"\($dev)\"" as $l |
|
||||||
|
"smart_health_passed\($l)} \(if .smart_status.passed then 1 else 0 end)",
|
||||||
|
"smart_temperature_celsius\($l)} \(.temperature.current // "NaN")",
|
||||||
|
"smart_power_on_hours\($l)} \(.power_on_time.hours // "NaN")",
|
||||||
|
(.ata_smart_self_test_log.standard.table[0]? // empty |
|
||||||
|
"smart_selftest_last_passed\($l)} \(if .status.passed then 1 else 0 end)"),
|
||||||
|
(.ata_smart_attributes.table[]? |
|
||||||
|
"smart_attribute_raw\($l),id=\"\(.id)\",name=\"\(.name)\"} \(.raw.value)",
|
||||||
|
"smart_attribute_value\($l),id=\"\(.id)\",name=\"\(.name)\"} \(.value)"),
|
||||||
|
"smart_last_read_timestamp\($l)} \(now|floor)"
|
||||||
|
' > "$f.tmp" && [ -s "$f.tmp" ] && mv "$f.tmp" "$f"
|
||||||
|
fi
|
||||||
|
[ -f "$f" ] && cat "$f" >> "$out"
|
||||||
|
echo "smart_disk_standby{serial=\"$serial\",device=\"${dev#/dev/}\"} $standby" >> "$out"
|
||||||
|
done
|
||||||
|
metrics-push smart < "$out"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UUID=6eea3421-da3f-4a0f-b66d-26000b9a1b01 /mnt/backup ext4 defaults,noatime,nofail,x-systemd.device-timeout=30s 0 2
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# smartd: следит за всеми дисками (DEVICESCAN подхватит и второй диск после хаба).
|
||||||
|
# -d removable — тип моста определяется сам; диска может не быть (не падать)
|
||||||
|
# -n standby,q — не будить спящий диск ради опроса
|
||||||
|
# -s — короткий самотест по субботам 03:00, длинный (~10ч на 4ТБ) 1-го числа в 05:00
|
||||||
|
# -W 4,50,55 — фиксировать скачки температуры ≥4°, порог предупреждения 50, критичный 55
|
||||||
|
# Почту не шлём (-m нет): тревоги идут через Prometheus на fr1 (smart-metrics → pushgateway).
|
||||||
|
DEVICESCAN -d removable -a -o on -S on -n standby,q -s (S/../../6/03|L/../01/./05) -W 4,50,55
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OneDrive -> HDD backup (rclone)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
RequiresMountsFor=/mnt/backup
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=rus
|
||||||
|
ExecStart=/usr/bin/flock /run/lock/onedrive-backup.lock /usr/local/bin/onedrive-backup
|
||||||
|
ExecStopPost=+/usr/local/bin/backup-metrics backup_onedrive
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
TimeoutStartSec=infinity
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OneDrive backup every 4h
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 00/4:30
|
||||||
|
RandomizedDelaySec=5m
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OneDrive backup integrity check
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target onedrive-backup.service
|
||||||
|
RequiresMountsFor=/mnt/backup
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=rus
|
||||||
|
ExecStart=/usr/bin/flock /run/lock/onedrive-backup.lock /usr/local/bin/onedrive-verify
|
||||||
|
ExecStopPost=+/usr/local/bin/backup-metrics backup_onedrive_verify
|
||||||
|
Nice=15
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
TimeoutStartSec=infinity
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Weekly OneDrive backup integrity check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=Sun *-*-* 05:00
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Push SMART metrics to pushgateway on fr1
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/smart-metrics
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=SMART metrics push every 15 min
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=15min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -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;'"
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user