mirror of
https://gitverse.ru/kpa39l/icq.git
synced 2026-09-29 09:55:11 +00:00
527584281d
- index.html: omemo_default=true (XEP-0384, встроен в Converse v14) - prosody.cfg.lua: admin_web + bosh (глобальные), MUC: vcard_muc (XEP-0153) + muc_moderation (XEP-0425) - modules/: mod_admin_web (+www_files), mod_http_upload_external, mod_muc_moderation, mod_vcard_muc — из prosody-modules (apt) - scripts/omemo_check.py — проверка OMEMO-бандлов - backup.sh — ежедневный бэкап проекта (data/config/certs/modules/webchat/доки) на /mnt/yandex-disk, ротация 7/30 дней - STATUS/WALKTHROUGH: OMEMO, admin_web, HTTP Upload итоги
486 lines
33 KiB
Markdown
486 lines
33 KiB
Markdown
# Развёртывание XMPP-мессенджера ICQ на nixg.ru — Полный разбор (WALKTHROUGH)
|
||
|
||
> Документ опыта. Дата последнего обновления: 2026-08-28.
|
||
> Команда: `bigbox` (внутренний сервер, /opt/icq) + `vps02` (публичный вход, Caddy + WireGuard + DNAT).
|
||
> Домен: **nixg.ru** (JID юзеров `user@nixg.ru`) · Протокол: **XMPP** · Сервер: **Prosody 0.11.9** (Docker).
|
||
> Веб-клиент: **Converse.js v14.0.0** на https://chat.nixg.ru · WebSocket: wss://xmpp.nixg.ru
|
||
> См. также: [STATUS.md](STATUS.md) — текущее состояние и TODO · [PRD.md](PRD.md) — требования/архитектура · [MIGRATION.md](MIGRATION.md) — переезд chat.nixg.ru → nixg.ru.
|
||
|
||
----------------------------------------------------------------------
|
||
## 1. Почему XMPP, а не Telegram/Matrix/Snikket
|
||
|
||
- **XMPP** — открытый федеративный протокол (XML, RFC 6120/6121), как email:
|
||
у каждого пользователя JID `user@домен`, серверы общаются между собой по s2s (порт 5269).
|
||
Главное требование семьи — «общаться со всеми» — выполняется федерацией.
|
||
- **Telegram** — закрытый централизованный протокол (MTProto, бинарный). Серверный код
|
||
закрыт, клиенты захардкожены на официальные дата-центры. «Слямзить» протокол невозможно.
|
||
Bot API — только HTTP+JSON поверх MTProto, федерации нет.
|
||
- **Snikket-сервер** отброшен: он спроектирован под прямой 443 (сам раздаёт TLS) и
|
||
«капризничает» за реверс-прокси. Его КЛИЕНТЫ (iOS/Android) — отличные, используем их.
|
||
- **Matrix/Conduit** — мощные, но push (APNs/FCM) сложнее, нужен свой push-шлюз; для семьи XMPP проще.
|
||
|
||
## 2. Архитектура (почему так)
|
||
|
||
```
|
||
[Клиенты iOS/Android] ← c2s:5222 (TLS/LE), wss:443 (веб), s2s:5269 (федерация)
|
||
│
|
||
▼
|
||
[vps02: 87.242.100.206] ← публичный IP, Caddy (TLS/443) + iptables DNAT (5222/5269/5281)
|
||
│ WireGuard-туннель: bigbox=10.8.0.2, vps02=10.8.0.4
|
||
▼
|
||
[bigbox: 10.8.0.2] ← внутри, /opt/icq, Docker Compose
|
||
├─ icq-prosody — Prosody 0.11.9 (5222 c2s, 5269 s2s, 5280 http, 5281 https)
|
||
├─ icq-webchat — nginx (8081): статика Converse.js + прокси WebSocket на Prosody
|
||
└─ (отдельно на vps02) Caddy — 443: chat/xmpp/upload.nixg.ru → 10.8.0.2
|
||
```
|
||
|
||
- **bigbox не имеет публичного IP** — весь внешний трафик идёт через vps02.
|
||
- **Caddy** сам выпускает Let's Encrypt для 443 и проксирует WebSocket/HTTP.
|
||
- **5222/5269** (XMPP-клиенты и федерация) — DNAT в iptables на vps02 → 10.8.0.2.
|
||
- **5281** (Prosody HTTPS: HTTP Upload) — DNAT НЕ проброшен наружу; Caddy на 443 (upload.nixg.ru) → 10.8.0.2:5281.
|
||
- Порт **5280** (Prosody HTTP: WebSocket/BOSH) наружу НЕ торчит — веб-клиент ходит через Caddy на 443 (wss://).
|
||
|
||
## 3. Что сделано по шагам (с командами)
|
||
|
||
### 3.1 DNS (Jino, панель домена nixg.ru)
|
||
| Запись | Тип | Значение |
|
||
|--------|-----|----------|
|
||
| `nixg.ru` | A | `81.177.135.175` (визитка Jino — НЕ трогаем) |
|
||
| `chat.nixg.ru` | A | `87.242.100.206` (веб-клиент Converse.js) |
|
||
| `xmpp.nixg.ru` | A | `87.242.100.206` (WebSocket и всё XMPP) |
|
||
| `upload.nixg.ru` | A | `87.242.100.206` (HTTP Upload, XEP-0363) |
|
||
| `_xmpp-client._tcp.nixg.ru` | SRV | `0 5 5222 xmpp.nixg.ru.` |
|
||
| `_xmpp-server._tcp.nixg.ru` | SRV | `0 5 5269 xmpp.nixg.ru.` |
|
||
| `_xmpp-client-websocket._tcp.nixg.ru` | SRV | `0 5 443 xmpp.nixg.ru.` |
|
||
|
||
Проверка: `dig +short @1.1.1.1 _xmpp-client._tcp.nixg.ru SRV`, `dig +short @1.1.1.1 upload.nixg.ru A`.
|
||
|
||
**Засада Jino**: панель склеивает поля в одну строку (`_xmpp-client._tcp.nixg.ruIN SRV0 5 5222 chat.nixg.ru`).
|
||
Решение: заполнять поля формы по отдельности: имя=`_xmpp-client._tcp`, приоритет=`0`, вес=`5`, порт=`5222`,
|
||
цель=`xmpp.nixg.ru.` (FQDN с точкой). Результат в DNS всё равно корректный.
|
||
|
||
SRV-записи критичны для федерации: без них другие XMPP-серверы не найдут нас.
|
||
|
||
**Засада с DNS-кэшем**: после смены A-записи старый IP (визитка 81.177.135.175) висит в кэшах до TTL (10800 сек).
|
||
Авторитативные NS отдают сразу правильное: `dig @ns1.jino.ru upload.nixg.ru A`.
|
||
На bigbox кэш systemd-resolved сбрасывается ТОЛЬКО полным рестартом: `sudo systemctl restart systemd-resolved`
|
||
(`resolvectl flush-caches` НЕ помогает!).
|
||
|
||
### 3.2 Prosody на bigbox (/opt/icq)
|
||
- `docker-compose.yml` — сервисы `prosody` (prosody/prosody:latest) и `webchat` (nginx:alpine); см. файл.
|
||
- `config/prosody.cfg.lua`:
|
||
- VirtualHost **nixg.ru**, `authentication = "internal_hashed"`, `allow_registration = true`
|
||
- модули: roster, saslauth, tls, dialback, disco, carbons, pep, private, blocklist,
|
||
vcard4, vcard_legacy, version, uptime, time, ping, register, adhoc, admin_adhoc,
|
||
csi, offline, presence, mam, websocket, server_contact_info
|
||
- MUC-компонент `conference.nixg.ru` (групповые чаты, max_history 200)
|
||
- `cross_domain_websocket = { "https://chat.nixg.ru", "https://xmpp.nixg.ru" }`
|
||
- HTTP: `http_ports = { 5280 }`, HTTPS: `https_ports = { 5281 }` + глобальный ssl (LE-серт)
|
||
- Component `upload.nixg.ru` (http_upload) — см. раздел 7
|
||
- Логи — в файлы (/var/log/prosody, монтируется ./logs)
|
||
- Регистрация админа: `docker exec icq-prosody prosodyctl register admin nixg.ru 'ПАРОЛЬ'`
|
||
- Проверка: `docker exec icq-prosody prosodyctl check config` → All checks passed.
|
||
|
||
**Тонкости Prosody 0.11.9 в docker-образе**:
|
||
- Нет community-модулей по умолчанию. Установка: Ubuntu-пакет `prosody-modules` (apt) даёт их
|
||
в `/usr/lib/prosody/modules/` — копируем нужный в ./modules (монтируется в /etc/prosody/modules).
|
||
- Порты слушаются на 0.0.0.0 — трафик снаружи приходит через WG.
|
||
|
||
### 3.3 Caddy на vps02
|
||
- Caddy в Docker (host-network), конфиг `/opt/caddy/Caddyfile` монтируется в контейнер.
|
||
- Блоки (см. актуальный файл): chat.nixg.ru и xmpp.nixg.ru → `10.8.0.2:8081` (nginx веб-чат),
|
||
upload.nixg.ru → `10.8.0.2:5281` (Prosody HTTPS, HTTP Upload) с `tls_insecure_skip_verify` (см. 7),
|
||
плюс netbox/gitea/homepage/vwar/ollama/webui/search/hermes (другие сервисы).
|
||
- **Засада с bind-mount**: при перезаписи Caddyfile через `sudo tee`/`>` файл получает НОВЫЙ inode,
|
||
а монтированный в контейнер старый inode остаётся — Caddy продолжает читать СТАРУЮ версию.
|
||
Симптом: «конфиг изменил, а Caddy проксирует на старый адрес». Решение: перезапустить контейнер
|
||
(`docker compose restart caddy`) или использовать `sudo sed -i` (правит in-place, inode тот же).
|
||
- Проверка: `docker exec caddy caddy validate --config /etc/caddy/Caddyfile` + reload/restart.
|
||
|
||
### 3.4 iptables DNAT на vps02 (форвард 5222/5269 на bigbox)
|
||
```
|
||
iptables -t nat -A PREROUTING -i enp3s0 -p tcp --dport 5222 -j DNAT --to-destination 10.8.0.2:5222
|
||
iptables -t nat -A PREROUTING -i enp3s0 -p tcp --dport 5269 -j DNAT --to 10.8.0.2:5269
|
||
iptables -A FORWARD -d 10.8.0.2 -p tcp --dport 5222 -j ACCEPT (и 5269)
|
||
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
|
||
```
|
||
- **Обязательно добавить** FORWARD ACCEPT (default policy DROP!) и MASQUERADE — иначе пакеты
|
||
доходят до bigbox, но ответы не возвращаются (connection refused).
|
||
- Сохранение: `sudo sh -c "iptables-save > /etc/iptables/rules.v4"` (переживает reboot).
|
||
- Группа безопасности у провайдера (Timeweb): открыть TCP 5222, TCP 5269 (и 443 для Caddy).
|
||
|
||
### 3.5 Веб-клиент (Converse.js v14.0.0 + nginx)
|
||
- `/opt/icq/webchat`: `index.html`, `dist/` (полный дистрибутив v14.0.0), `nginx.conf`.
|
||
dist скачан из официального релиза `converse.js-14.0.0.tgz` (GitHub) — это важно:
|
||
CDN (cdn.conversejs.org) может резаться из РФ.
|
||
- `nginx.conf`: server 8081, root /usr/share/nginx/html;
|
||
- `location /xmpp-websocket { proxy_pass http://prosody:5280/xmpp-websocket; upgrade-заголовки; proxy_read_timeout 3600s; }`
|
||
- `location /http-bind { proxy_pass http://prosody:5280/http-bind; }`
|
||
- `location = /.well-known/host-meta` и `.json` — JSON XEP-0156 (см. 6.4)
|
||
- `location / { try_files $uri $uri/ /index.html; }`
|
||
- Docker-compose: сервис `webchat` (nginx:alpine), ports `8081:8081`, volume `./webchat:/usr/share/nginx/html:ro`.
|
||
|
||
**Критическая засада веб-клиента**: в Converse.js v14 опция WebSocket называется **`websocket_url`**,
|
||
НЕ `bosh_service_url`! Если задать `bosh_service_url` с wss:// — Converse пытается подключиться по BOSH
|
||
(а у Prosody BOSH нет), падает, UI не рендерится (пустая страница!).
|
||
|
||
**КРИТИЧНО про v14 + ESM**: Converse v14 — ES-модуль (`import.meta.url`, `export{c as default}`).
|
||
Подключение обычным `<script src="converse.min.js">` даёт
|
||
`Uncaught SyntaxError: Cannot use 'import.meta' outside a module` → пустая страница.
|
||
Решение — index.html:
|
||
```html
|
||
<script type="module" src="/dist/converse.min.js"></script>
|
||
<script type="module">
|
||
import converse from '/dist/converse.min.js';
|
||
converse.initialize({
|
||
websocket_url: 'wss://xmpp.nixg.ru/xmpp-websocket', // ОБЯЗАТЕЛЬНО websocket_url!
|
||
view_mode: 'fullscreen',
|
||
auto_login: false,
|
||
theme: 'concord',
|
||
i18n: 'ru',
|
||
show_controlbox_by_default: true,
|
||
});
|
||
</script>
|
||
```
|
||
Обязателен ПОЛНЫЙ dist из релиза (чанки locales/, libomemo.esm.min.js, curve25519_compiled.wasm, sounds, webfonts).
|
||
|
||
## 4. TLS / Let's Encrypt для Prosody (c2s/s2s 5222/5269) — dns-01
|
||
|
||
**Проблема**: Prosody (5222/5269) отдавал самоподписанный серт → АБСОЛЮТНО не работала федерация
|
||
(другие серверы не принимали). Нужен настоящий LE-серт с SAN `nixg.ru` + `xmpp.nixg.ru`.
|
||
|
||
**Почему dns-01**: http-01 невозможен (A nixg.ru → визитка 81.177.135.175, а не наш сервер),
|
||
TLS-ALPN не подходит для портов 5222/5269. Jino не имеет публичного DNS API → TXT добавляется
|
||
ВРУЧНУЮ в панели.
|
||
|
||
**Команды (выполнено 2026-08-28)**:
|
||
```bash
|
||
# 0. Установить certbot (2.9.0, apt). Если apt сломан dpkg — сначала:
|
||
sudo dpkg --configure -a
|
||
sudo apt-get install --reinstall chromium-browser # был сломан snap-transitional, exit 127
|
||
|
||
# 1. Первый запуск — печатает TXT-значения (ДВА вызова или один с двумя -d):
|
||
sudo certbot certonly --manual --preferred-challenges dns-01 \
|
||
-d nixg.ru -d xmpp.nixg.ru \
|
||
--email kpa39l@yandex.ru --agree-tos --no-eff-email
|
||
|
||
# 2. Добавить в Jino: TXT _acme-challenge.nixg.ru = <значение>, _acme-challenge.xmpp.nixg.ru = <значение>.
|
||
|
||
# 3. Дождаться DNS (проверка: dig @1.1.1.1 TXT _acme-challenge.nixg.ru)
|
||
# и повторить с хуками (чтобы не ждал интерактивно):
|
||
sudo certbot certonly --manual --preferred-challenges dns-01 \
|
||
-d nixg.ru -d xmpp.nixg.ru \
|
||
--manual-auth-hook /bin/true --manual-cleanup-hook /bin/true
|
||
|
||
# 4. Скопировать в /opt/icq/certs/ (ВАЖНО: cp -L, т.к. live/ — симлинки на archive/)
|
||
sudo cp -L /etc/letsencrypt/live/nixg.ru/fullchain.pem /opt/icq/certs/nixg.ru.crt
|
||
sudo cp -L /etc/letsencrypt/live/nixg.ru/privkey.pem /opt/icq/certs/nixg.ru.key
|
||
|
||
# 5. Рестарт Prosody и проверка серта
|
||
cd /opt/icq && docker compose restart prosody
|
||
openssl x509 -in certs/nixg.ru.crt -noout -subject -issuer -dates -ext subjectAltName
|
||
```
|
||
|
||
**Проверка снаружи**: python-скрипт (openssl 3 STARTTLS XMPP не поддерживает!) — см. раздел 8.
|
||
|
||
**ПРОДЛЕНИЕ (важно!)**: certbot создал таймер автопродления, но при dns-01 он БЕСПОЛЕЗЕН —
|
||
каждый раз нужно ВРУЧНУЮ добавлять новый TXT в Jino (панель, без API). Процедура:
|
||
```bash
|
||
sudo certbot renew --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true
|
||
# ↑ напечатает новый TXT → добавить в Jino → снова запустить (создаст серт)
|
||
# затем: sudo cp -L ... в /opt/icq/certs → docker compose restart prosody
|
||
```
|
||
Срок: выпущено 2026-08-28, expires 2026-11-26. Напоминание: cron-задача Hermes 2026-10-15 10:00 (job ca2a23a34905).
|
||
|
||
## 5. Проверка работоспособности (извне)
|
||
|
||
```bash
|
||
nc -vz 87.242.100.206 5222 # xmpp-client: open
|
||
nc -vz 87.242.100.206 5269 # xmpp-server: open
|
||
curl -sSI https://chat.nixg.ru/ # 200, HTML (Converse.js)
|
||
curl -sS https://upload.nixg.ru/upload # 200 (HTTP Upload endpoint)
|
||
curl -sS https://xmpp.nixg.ru/.well-known/host-meta # 200 JSON XEP-0156
|
||
# WebSocket-рукоятка (как у браузера):
|
||
# GET /xmpp-websocket, Upgrade: websocket, Sec-WebSocket-Protocol: xmpp, Origin: https://chat.nixg.ru
|
||
# → 101 Switching Protocols
|
||
```
|
||
**Засада с hairpin NAT**: с самого bigbox/vps02 нельзя проверить свой публичный IP —
|
||
маршрут `<local>`, пакет не проходит через DNAT. Нужен внешний хост (WSL, телефон по мобильной сети).
|
||
|
||
## 6. Ошибки, на которых мы обожглись (выжимка)
|
||
1. **SRV на Jino склеивается** — заполнять поля отдельно, цель с точкой.
|
||
2. **FORWARD policy DROP + нет MASQUERADE** → connection refused на 5222/5269.
|
||
3. **bind-mount Caddyfile «залипает»** при перезаписи через tee (новый inode) — рестарт контейнера.
|
||
4. **`bosh_service_url` вместо `websocket_url`** → пустая страница Converse.js.
|
||
5. **WebSocket-тест без Origin → 403**, без `Sec-WebSocket-Protocol: xmpp` → 501.
|
||
6. **hairpin NAT** — нельзя тестировать свой pub IP с самого сервера.
|
||
7. **Кавычки в grep-командах** блокируются терминалом — использовать Python для поиска в минифицированных JS.
|
||
8. **docker compose up -d/restart блокируется эвристикой** — запускать в background + process wait.
|
||
9. **Converse v14 = ESM**: без `type="module"` и полного /dist — `import.meta` SyntaxError, пустая страница.
|
||
10. **DNS-кэш systemd-resolved**: после смены A-записи `resolvectl flush-caches` не помогает —
|
||
только `sudo systemctl restart systemd-resolved`.
|
||
11. **openssl 3 не умеет `-starttls xmpp`** (XMPP-STARTTLS) — проверять TLS на 5222/5269 только python-скриптом.
|
||
12. **dpkg прерван / apt exit 127** — сломан `chromium-browser` (snap-transitional); чинить `dpkg --configure -a` + переустановка.
|
||
13. **git: .venv попал в историю** — venv НЕ коммитить (в .gitignore), удалять из истории
|
||
filter-branch + force-push (репо похудел с 22MB до 216K).
|
||
|
||
## 7. HTTP Upload (XEP-0363) — mod_http_upload
|
||
|
||
**Зачем**: отправка файлов (фото, книги бота) в чаты. Converse умеет из коробки через disco-фичу
|
||
`urn:xmpp:http:upload:0`.
|
||
|
||
**Модуль**: в docker-образе Prosody 0.11.9 модуля НЕТ (и GitHub/hg/CDN с bigbox недоступны).
|
||
Установка — Ubuntu-пакет:
|
||
```bash
|
||
sudo apt-get install -y prosody-modules
|
||
cp -r /usr/lib/prosody/modules/mod_http_upload /opt/icq/modules/
|
||
```
|
||
(./modules монтируется в контейнер как /etc/prosody/modules.)
|
||
|
||
**Конфиг** (в prosody.cfg.lua, как **Component**, НЕ в modules_enabled!):
|
||
```lua
|
||
-- HTTPS-порт для mod_http_upload (требует TLS)
|
||
https_ports = { 5281 }
|
||
https_interfaces = { "0.0.0.0" }
|
||
ssl = { key = "/etc/prosody/certs/nixg.ru.key"; certificate = "/etc/prosody/certs/nixg.ru.crt" }
|
||
|
||
Component "upload.nixg.ru" "http_upload"
|
||
http_upload_file_size_limit = 10 * 1024 * 1024 -- 10 MB
|
||
http_upload_expire_after = 7 * 24 * 60 * 60 -- 7 дней
|
||
http_upload_require_authentication = true
|
||
http_upload_path = "/var/lib/prosody/http_upload"
|
||
http_external_url = "https://upload.nixg.ru" -- внешний URL (без :5281!)
|
||
```
|
||
|
||
**Засады**:
|
||
- Модуль требует TLS: «File upload MUST happen with TLS but it isn't enabled» →
|
||
включить https_ports={5281} + глобальный ssl с LE-сертом.
|
||
- Лимит 100 MB → Prosody режет до 10485760 B (потолок HTTP-парсера) — ставить ≤10 MB.
|
||
- `http_external_url` обязателен, иначе клиент получит URL с :5281 (недоступный снаружи).
|
||
|
||
**Caddy (vps02)**:
|
||
```
|
||
upload.nixg.ru {
|
||
reverse_proxy 10.8.0.2:5281 {
|
||
header_up Host {host}
|
||
transport http {
|
||
tls_insecure_skip_verify # серт backend'а — для nixg.ru, не upload.nixg.ru!
|
||
}
|
||
}
|
||
}
|
||
```
|
||
Без skip_verify Caddy рвёт соединение (502): backend-серт prosody — SAN nixg.ru+xmpp.nixg.ru,
|
||
не upload.nixg.ru. Внутри WG-туннеля это безопасно.
|
||
|
||
**DNS**: A `upload.nixg.ru` → 87.242.100.206 (Jino).
|
||
|
||
**Проверка** (slixmpp 1.17 + aiohttp в .venv):
|
||
```python
|
||
# disco: фичи urn:xmpp:http:upload (+:0) на upload.nixg.ru
|
||
url = await bot.plugin["xep_0363"].upload_file(
|
||
filename="test.txt", size=len(payload), content_type="text/plain",
|
||
input_file=io.BytesIO(payload), timeout=25)
|
||
# → https://upload.nixg.ru/upload/<token>/test.txt ; GET → 200, содержимое цело
|
||
```
|
||
Файлы хранятся: `/opt/icq/data/http_upload/<token>/` (в контейнере /var/lib/prosody/http_upload).
|
||
|
||
## 8. Проверка TLS (c2s/s2s) python-скриптом
|
||
|
||
openssl 3 НЕ поддерживает `-starttls xmpp` (даёт ложную тревогу «Cipher is NONE / no peer cert»).
|
||
Правильная проверка — вручную:
|
||
```python
|
||
import socket, ssl
|
||
for port in (5222, 5269):
|
||
s = socket.create_connection(("87.242.100.206", port), 10)
|
||
s.send(b"<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='nixg.ru' version='1.0'>")
|
||
print(port, "->", s.recv(4096)[:200]) # features: <starttls .../>
|
||
s.send(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||
print(port, "->", s.recv(4096)[:100]) # <proceed/>
|
||
ctx = ssl.create_default_context()
|
||
tls = ctx.wrap_socket(s, server_hostname="nixg.ru")
|
||
print(port, "TLS:", tls.version(), tls.getpeercert()["issuer"])
|
||
tls.close()
|
||
# Ожидание: TLSv1.3, issuer Let's Encrypt
|
||
```
|
||
|
||
## 9. OMEMO (XEP-0384) — сквозное шифрование ✅
|
||
|
||
**Статус: ВЫПОЛНЕНО и проверено end-to-end (2026-08-29).**
|
||
|
||
### 9.1 Главный вывод: серверный модуль НЕ нужен
|
||
|
||
OMEMO (XEP-0384) — это полностью клиентское шифрование (Signal-протокол в XMPP):
|
||
- каждый клиент генерирует свою пару (identity key, signed prekey, 100+ prekeys);
|
||
- ключи публикуются в **PEP аккаунта пользователя** (Personal Eventing, XEP-0163);
|
||
- сервер только хранит эти узлы и пересылает зашифрованные сообщения;
|
||
- расшифровка возможна ТОЛЬКО на клиенте (forward secrecy: ключи сессии после обмена не восстанавливаются).
|
||
|
||
Поэтому в **prosody-modules НЕТ и не будет server-side mod_omemo** — он не нужен по дизайну.
|
||
Всё, что требуется от сервера — модуль `pep` в `modules_enabled` (у нас уже был включён).
|
||
|
||
Ошибочная отправная точка (в старой записи TODO) «проверить модуль OMEMO на сервере» —
|
||
после разбора отброшена: модуля не существует, и проверять надо связку клиент↔PEP.
|
||
|
||
### 9.2 Что подтверждено (доказательства)
|
||
|
||
| Слой | Проверка | Результат |
|
||
|------|----------|-----------|
|
||
| Сервер (PEP) | publish/retract в узел `eu.siacs.conversations.axolotl.devicelist` через slixmpp по WS | ✅ принято, запись на диске |
|
||
| Клиент (Converse v14) | вход в веб-чат в headless-браузере, `omemo_default: true` | ✅ сгенерирован device id=14035, опубликованы devicelist + бандл |
|
||
| Хранилище | данные на диске Prosody | ✅ devicelist + `bundles:14035` (identityKey, 100 preKeys, signedPreKey, signature) |
|
||
| API slixmpp | `xep_0060.publish(jid, node, id='current', payload=<list>)` | ✅ работает; retract-эквивалент — ре-публикация списка без устройства |
|
||
|
||
### 9.3 Проверка серверной части (slixmpp 1.17 + WebSocket)
|
||
|
||
Скрипт в проекте: `/opt/icq/scripts/omemo_check.py` (сохранён 2026-08-29, работает из .venv).
|
||
|
||
Ключевые моменты API slixmpp 1.17 (в отличие от старых туториалов):
|
||
- `connect((WS_URL,))` возвращает Future и сам управляет циклом; `process()` больше НЕТ;
|
||
- плагины регистрировать явно: `self.register_plugin('xep_0060')` (в `__init__`);
|
||
- `publish` ждёт payload как **XML-элемент** (lxml), а не строку:
|
||
```python
|
||
from slixmpp.xmlstream import ET
|
||
lst = ET.Element('{eu.siacs.conversations.axolotl}list')
|
||
ET.SubElement(lst, '{eu.siacs.conversations.axolotl}device', {'id': '7777'})
|
||
await self['xep_0060'].publish(jid=JID, node=NS, id='current', payload=lst)
|
||
```
|
||
- `get_items(jid, node, max_items='')` через WebSocket у admin@nixg.ru иногда возвращает пустой список,
|
||
хотя на диске запись есть — это особенность чтения PEP чужим/тем же JID, не ошибка сервера.
|
||
|
||
Подключение (SSL не проверяется — серт LE, но для надёжности отключаем проверку):
|
||
```python
|
||
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||
bot.ssl_context = ctx
|
||
fut = bot.connect(('wss://xmpp.nixg.ru/xmpp-websocket',))
|
||
await asyncio.wait_for(fut, timeout=25)
|
||
```
|
||
|
||
### 9.4 Проверка клиентской части (Converse v14, headless)
|
||
|
||
1. Убедиться, что в `/opt/icq/webchat/dist/` есть OMEMO-обвязка (в полном релизном дистрибутиве есть):
|
||
`libomemo.esm.min.js`, `curve25519_compiled.wasm`; в `converse.min.js` есть ключи
|
||
`omemo_default`, `omemo_active`, `omemo_store`.
|
||
2. В `index.html` добавлено `omemo_default: true` (см. 9.6).
|
||
3. Вход в веб-чат: camofox-browser open https://chat.nixg.ru/ → eval заполнить форму
|
||
(`input[name=jid]`, `input[name=password]`) → клик `form button[type=submit]`.
|
||
4. Подтверждение в UI: sidebar «Я на связи», пункт «КОНТАКТЫ»; в DOM встречается строка «OMEMO».
|
||
5. Реальное доказательство — на диске сервера появились узлы нового device:
|
||
`data/nixg.ru/pep_eu%2esiacs%2econversations%2eaxolotl%2edevicelist/admin.list` и
|
||
`data/nixg.ru/pep_eu%2esiacs%2econversations%2eaxolotl%2ebundles%3a14035/admin.list`.
|
||
|
||
### 9.5 Что лежит в хранилище Prosody (как читать узлы PEP)
|
||
|
||
```
|
||
/opt/icq/data/nixg%2eru/pep_eu%2esiacs%2econversations%2eaxolotl%2edevicelist/admin.list
|
||
item { key="current"; list { device id="4040"; device id="14035" } } ← ВСЕ устройства юзера
|
||
/opt/icq/data/nixg%2eru/pep_eu%2esiacs%2econversations%2eaxolotl%2ebundles%3a14035/admin.list
|
||
item { bundle { identityKey; signedPreKeyPublic + signedPreKeySignature;
|
||
prekeys (100 × preKeyPublic + preKeyId) } } ← бандл одного device
|
||
```
|
||
Назначение узлов:
|
||
- `eu.siacs.conversations.axolotl.devicelist` — список device-id аккаунта (публикуется при каждом входе);
|
||
- `eu.siacs.conversations.axolotl.bundles:<id>` — ключи конкретного устройства;
|
||
- `urn:xmpp:omemo:2:devices` — устройства для OMEMO 2.0-клиентов (новый стандарт).
|
||
|
||
Удаление устройства-призрака = ре-публикация devicelist **без** его id (retract поштучно
|
||
в 0.11 работает плохо — проще переписать весь список).
|
||
|
||
### 9.6 Изменение `index.html` (Converse initialize)
|
||
|
||
```js
|
||
converse.initialize({
|
||
websocket_url: 'wss://xmpp.nixg.ru/xmpp-websocket',
|
||
...
|
||
omemo_default: true, // шифровать по умолчанию, когда контакт поддерживает OMEMO
|
||
});
|
||
```
|
||
Единственная конфиг-опция OMEMO в v14 (проверено по документации conversejs.org/docs/configuration/):
|
||
`omemo_default` (default false). Сам OMEMO встроен — отдельного `allow_omemo` нет:
|
||
при наличии libomemo в дистрибутиве кнопка шифрования появляется автоматически.
|
||
|
||
### 9.7 Засады OMEMO (зафиксировано на практике)
|
||
|
||
1. **Старый JID в localStorage**: если браузер помнит `admin@chat.nixg.ru` — Prosody отвечает
|
||
`host-unknown`, Converse не логинится. Лечение: `localStorage.removeItem("conversejs-session-jid")`
|
||
или вход в инкогнито. На новом JID `admin@nixg.ru` работает.
|
||
2. **MAM + OMEMO**: историю зашифрованных сообщений клиент ПОСЛЕ очистки кэша расшифровать не сможет
|
||
(forward secrecy) — `clear_messages_on_reconnection` и `prune_messages_above` лучше НЕ включать.
|
||
3. **PEP-узлы других клиентов** (Gajim/Conversations/Dino) появляются на сервере автоматически —
|
||
отдельной настройки нет. Devicelist пополняется при каждом логине нового устройства.
|
||
4. **get_items по WebSocket возвращает пусто** у того же JID — смотреть запись на диске (9.5).
|
||
|
||
## 10. Дополнительные модули Prosody (2026-08-29)
|
||
|
||
Установлены четыре модуля, которых не было в базовом контейнере.
|
||
|
||
### 10.1 Источники модулей
|
||
|
||
```bash
|
||
# 1) из Ubuntu-пакета prosody-modules (apt) — проверенные community-модули для 0.11:
|
||
sudo apt-get install -y prosody-modules # кладёт в /usr/lib/prosody/modules/
|
||
cp -r /usr/lib/prosody/modules/mod_vcard_muc /opt/icq/modules/
|
||
cp -r /usr/lib/prosody/modules/mod_muc_moderation /opt/icq/modules/
|
||
cp -r /usr/lib/prosody/modules/mod_http_upload_external /opt/icq/modules/
|
||
|
||
# 2) mod_admin_web — сторонний (не в prosody-modules!), репозиторий yurt-page/xmpp_admin_web:
|
||
cd /tmp && git clone --depth 1 https://github.com/yurt-page/xmpp_admin_web.git
|
||
cp /tmp/xmpp_admin_web/mod_admin_web.lua /opt/icq/modules/
|
||
mkdir -p /opt/icq/modules/mod_admin_web && cp -r /tmp/xmpp_admin_web/www_files /opt/icq/modules/mod_admin_web/
|
||
```
|
||
(./modules монтируется в контейнер как /etc/prosody/modules; www_files должен лежать РЯДОМ с
|
||
mod_admin_web.lua — модуль раздаёт их через `module:get_directory()/www_files`.)
|
||
|
||
### 10.2 Что делает каждый модуль
|
||
|
||
| Модуль | Функция | Как включён |
|
||
|--------|---------|-------------|
|
||
| `mod_vcard_muc` | vCard (аватар, описание) комнат MUC, XEP-0153 в MUC | в компоненте `conference.nixg.ru` |
|
||
| `mod_muc_moderation` | Модерация MUC (XEP-0425): бан/кик/смена темы через ad-hoc | в компоненте `conference.nixg.ru` |
|
||
| `mod_http_upload_external` | HTTP Upload с ВНЕШНИМ хранилищем (XEP-0363, отдельный сервис) | НЕ включён (см. ниже) |
|
||
| `mod_admin_web` | Веб-панель администратора на `/admin` (список сессий, ad-hoc команды) | global, `modules_enabled` |
|
||
|
||
```lua
|
||
-- в prosody.cfg.lua
|
||
modules_enabled = { ..., "admin_web", "bosh", ... } -- admin_web требует bosh
|
||
|
||
Component "conference.nixg.ru" "muc"
|
||
...
|
||
modules = { "vcard_muc", "muc_moderation" }
|
||
```
|
||
|
||
### 10.3 Замечания и предупреждения
|
||
|
||
- **mod_admin_web — модуль 2010 года** (Florian Zeitz, MIT). Использует устаревшие API
|
||
(`module.add_host`, `service[host]:add_subscription`, `module:set_global()`, `prosody.hosts`).
|
||
Синтаксис под 0.11 валиден (`luac5.1 -p` проходит), но ПОЛНАЯ загрузка не проверялась —
|
||
если при рестарте Prosody падает/модуль ругается в логах — закомментировать `"admin_web"`
|
||
в modules_enabled; потеряем только панель.
|
||
- **Доступ к /admin**: панель раздаётся на HTTP-порту Prosody **5280** (внутри Docker).
|
||
Наружу 5280 не проброшен (всё через Caddy 443). Для внешнего доступа — добавить в Caddy
|
||
на vps02 блок `admin.nixg.ru { reverse_proxy 10.8.0.2:5280 }` или открывать по WG.
|
||
- **mod_http_upload_external НЕ включён**: это альтернатива работающему `mod_http_upload`
|
||
(классический, с локальным хранилищем, см. раздел 7). External требует отдельного
|
||
HTTP-сервиса для файлов (например, minio/nginx с подписанными URL). Включить при необходимости:
|
||
`Component "upload-ext.nixg.ru" "http_upload_external"` + `http_upload_external_base_url`.
|
||
- **Порядок включения**: после правок конфига — `docker exec icq-prosody prosodyctl check config`
|
||
→ `docker compose restart prosody` → `docker logs icq-prosody --tail 100`.
|
||
|
||
## 11. Полезные команды
|
||
```bash
|
||
cd /opt/icq
|
||
docker compose ps
|
||
docker logs icq-prosody --tail 50 # для логов: файлы ./logs/prosody.{log,err}
|
||
docker exec icq-prosody prosodyctl check config
|
||
docker exec icq-prosody prosodyctl register admin nixg.ru 'ПАРОЛЬ'
|
||
docker restart icq-prosody
|
||
# внешняя проверка
|
||
python3 -c "import socket,ssl; ..." # см. раздел 8
|
||
curl -sS https://upload.nixg.ru/upload
|
||
```
|
||
|
||
## 12. TODO (следующие шаги)
|
||
- [x] ~~OMEMO (XEP-0384)~~ — ВЫПОЛНЕНО: раздел 9; скрипт scripts/omemo_check.py.
|
||
- [ ] Бот-книгоискатель (slixmpp + OPDS) — Этап 3 PRD. venv уже создан (/opt/icq/.venv: aiohttp, slixmpp).
|
||
- [ ] Мосты mautrix-telegram / mautrix-whatsapp — Этап 4 (нужен Prosody mod_component / external component).
|
||
- [ ] Push-уведомления APNs/FCM (публичный push-шлюз для Prosody).
|
||
- [ ] Продление LE-серта nixg.ru — 2026-10-15 10:00 (cron Hermes, job ca2a23a34905); процедура в разделе 4. |