# Развёртывание 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}`). Подключение обычным ` ``` Обязателен ПОЛНЫЙ 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 — маршрут ``, пакет не проходит через 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//test.txt ; GET → 200, содержимое цело ``` Файлы хранятся: `/opt/icq/data/http_upload//` (в контейнере /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"") print(port, "->", s.recv(4096)[:200]) # features: s.send(b"") print(port, "->", s.recv(4096)[:100]) # 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=)` | ✅ работает; 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:` — ключи конкретного устройства; - `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` | Ad-hoc админ-команды по XMPP (adminsub, список сессий) — НЕ веб-панель | 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` проходит). ✅ Проверено рестартом 2026-08-29: сервер стартовал без error, модуль загрузился, BOSH поднялся — падения нет. ⚠️ **УТОЧНЕНИЕ**: это XMPP-модуль ad-hoc админ-команд (adminsub, список сессий через subscription-уведомления), а НЕ HTTP-веб-панель. HTTP `/admin` на :5280 даёт **404**. Настоящей веб-админки из коробки в Prosody 0.11 нет (ставят отдельные UI-проекты). Поэтому не переживаем нас сёрфа на /admin и внешних прокси-блоков к нему. - **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`. ### 10.4 Push-уведомления (APNs/FCM) — варианты реализации **Статус (2026-08-29): ОТЛОЖЕНО.** Особой потребности в push нет; реализуем когда понадобится или когда исчерпается список задач. Ниже — честная картина с учётом нашей версии Prosody. Как устроен push в XMPP (коротко): - **XEP-0357** — стандарт push-уведомлений. «Перевёрнутая» схема: не сервер шлёт в APNs/FCM, а мобильный клиент регистрирует у Prosody «push-сервис и токен», и сервер при новом offline-сообщении зовёт push-сервис, а тот уже доставляет через APNs/FCM. - Нативные клиенты Conversations (Android) и Monal (iOS) ходят на **свои публичные push-шлюзы** (conversations.im, push.monal.im) — для них достаточно включить XEP-0357-модули на сервере, свой APNs/FCM-мост поднимать НЕ нужно. **Варианты:** **(1) XEP-0357 (рекомендуемый, работает на 0.11)** — mod_push + mod_push_offline (+ mod_push_muc для комнат): ```bash # Модули НЕ в Debian-пакете prosody-modules — берутся из официального репозитория prosody-modules (Mercurial): # https://hg.prosody.im/prosody-modules/ (каталоги mod_push, mod_push_offline, mod_push_muc) # Положить в /opt/icq/modules/, затем в prosody.cfg.lua добавить в modules_enabled: # "push"; "push_offline"; # и "push_muc" для MUC ``` Плюс: нативный push для Conversations/Monal через их бесплатные публичные шлюзы, без своей инфраструктуры. **(2) UnifiedPush (mod_unified_push)** — только после апгрейда Prosody до 0.12: ```bash # mod_unified_push уже есть в apt-prosody-modules (скопирован был из /usr/lib/prosody/modules/mod_unified_push/) # НО требует util.jwt — это API Prosody 0.12+, в нашем контейнере (0.11.9) его НЕТ → модуль не грузится: # modulemanager error: module 'util.jwt' not found # Поэтому включить его смысл есть только вместе с переходом prosody на 0.12. ``` **Вывод:** пока остаёмся на 0.11.9 без push. Когда появится мобильный клиент или потребность — идти по варианту (1): скачать 3 модуля из hg, положить в modules/, включить в конфиг (это под 0.11). ## 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. Slidgram: регистрация telegram-аккаунта (пошагово) > Мост уже подключён к Prosody (внешний компонент telegram.nixg.ru, трафик TG → SOCKS5 > 172.27.0.1:1080 → VPS01). Осталось привязать аккаунт. Вся механика — из кода slidgram > (в контейнере icq-slidgram: /venv/.../slidgram/gateway.py) и офлайн-доков docs/slidgram/. ### 12.1 Что понадобится - **api_id и api_hash** — свои, с https://my.telegram.org/apps (завести приложение Telegram). ⚠️ HTTP my.telegram.org из РФ открывается в обычном браузере на вашей машине (туннель для этого не нужен). - XMPP-клиент под `admin@nixg.ru`: веб-чат **https://chat.nixg.ru** (Converse) или Gajim. Converse поддерживает ad-hoc команды; либо просто отправить сообщение `register` — оба способа ниже. ### 12.2 Способ A: ad-hoc команда «Register» (Converse/Gajim) 1. Открыть https://chat.nixg.ru, войти как admin@nixg.ru. 2. Начать чат с контактом **telegram.nixg.ru** (JID компонента — без @ и логина). 3. В Converse: открыть карточку контакта (info) → там пункт «Команды» (adhoc) → выбрать **Register**. В Gajim: Правка → Команды (или иконка) → Register. 4. Заполнить форму: **phone** (в международном формате `+7...`), **api_id**, **api_hash**. 5. На ваш Telegram придёт SMS/код подтверждения → ввести его в форму (поле «Confirmation code»). 6. Если у TG-аккаунта включён 2FA-пароль — мост спросит пароль (поле password). ### 12.3 Способ B: текстовое сообщение «register» (Conversations и др.) 1. Отправить сообщение `register` на адрес **telegram.nixg.ru**. 2. Мост ответит формой-запросом: ввести номер телефона, api_id, api_hash (и код). 3. Далее как в способе A: код из SMS + (опционально) 2FA-пароль. ### 12.4 Что происходит под капотом (для диагностики) - Мост сначала `Client.connect()` — если уже залогинен, сообщает «two factor» / завершает; - затем `send_code(phone)` → возвращает `phone_code_hash` (80 сек на ввод кода, `REGISTRATION_AUTH_CODE_TIMEOUT` в slidgram/config.py); - `sign_in(phone, code_hash, code)` — при 2FA ловит `SessionPasswordNeeded` → запрашивает пароль; - после успеха аккаунт привязан: контакты появляются в ростере XMPP как `123456789@telegram.nixg.ru` (puppet-JID), сообщения Telegram дублируются в XMPP. - Всё это идёт через **MTProto поверх нашего SOCKS5** (SLIDGRAM_PROXY) — из РФ работает. ### 12.5 Проверка после регистрации ```bash docker logs icq-slidgram --since 10m | grep -iE 'register|login|session|avatar|error' docker exec icq-prosody prosodyctl list 2>/dev/null # зарегистрированные XMPP-аккаунты ``` - В Converse после входа появится список контактов Telegram (появятся с задержкой синхронизации ростера). - Переписка: сообщения, пришедшие в Telegram, будут дублироваться в XMPP-диалог с puppet-контактом. ### 12.6 Известные грабли - **Код/пароль 2FA** — вводить быстро: таймаут ввода кода 60 сек (по умолчанию). - **api_id/api_hash** нельзя «забыть» ввести: без них мост сам запросит их в форме (если не заданы в конфиге `API_ID`/`API_HASH`). - **На одном номере** — только один аккаунт на сервер (при повторной регистрации с тем же номером мост ответит «Someone is already using this phone number on this server»). - **Аватары** могут не подтягиваться (URL-загрузка web.telegram.org не ходит через SOCKS5) — это отдельная задача в STATUS.md; на приём сообщений не влияет. ### 12.7 Засады 2026-08-29 (после регистрации estorozhenko@nixg.ru) - **mod_privilege (XEP-0356) ломает Prosody 0.11.9**: community-модуль из prosody-modules рассчитан на 0.12 и вызывает `module:send_iq()` (API 0.12), которого в 0.11 нет → traceback `attempt to call method 'send_iq' (a nil value)` при каждом privileged IQ. Последствия: слайдж висит на IqTimeout при создании MDS-узла / записи bookmarks, после рестартов контакты TG пропадали из ростера (на сервере рostер цел, клиент просто не получал push). - **Фикс (0.11-совместимый стаб)**: в `/opt/icq/modules/mod_privilege.lua` вызов `module:send_iq(wrapped_iq, newsession):next(...)` заменён на стаб-ACK — модуль отвечает слайджу `st.reply(stanza)` (ACK) и НЕ пересылает IQ от имени юзера. Prosody перестаёт падать, слайдж не перезапускается в цикле. Цена: bookmarks/MDS через XEP-0356 не работают, слайдж шлёт приглашения в группы вручную. Коммит: 770c9fa. - **«Миллион запросов на подключение к чату» в Pidgin** — следствие двух вещей: 1) `always_invite_when_adding_bookmarks: true` в preferences пользователя (slidge шлёт gateway-приглашение на КАЖДУЮ группу при добавлении закладки); 2) bookmarks не записываются (mod_privilege сломан) → слайдж падает в fallback и шлёт приглашения вручную на каждый чат. Временное решение: смириться до апгрейда на 0.12 (там XEP-0356 нативный); альтернатива — `always_invite_when_adding_bookmarks: false` в preferences (запись: slidge.core.session — user.preferences, изменение в SQLite `user_account`). - **Flood wait от Telegram при первом старте**: слайдж с большим аккаунтом (1602 контакта) при старте тянет инфу о каждом чате/контакте → Telegram банит частые запросы (`WARNING:slidgram.telegram:Flood in get_chat(...), sleep for N seconds`). Не баг: после кэширования паузы исчезают. Проверка БД: `docker exec icq-slidgram python3 -c "import sqlite3;c=sqlite3.connect('/var/lib/slidge/slidge.sqlite');print(c.execute('select count(*) from contact').fetchone())"`. - **Контакты «пропали» после рестарта сервера** — на самом деле рostер цел на сервере (проверка: slixmpp roster get → 1602 telegram-контакта). Клиент (Pidgin) при переподключении должен сам запросить рostер (Disable/Enable аккаунта). Если нет — проблема в клиентском кэше, не в сервере. ### 12.8 Планируется: апгрейд Prosody 0.11.9 → 0.12.x (система сборки) - **Зачем**: 0.12 имеет нативный XEP-0356 (mod_privilege больше не нужен), util.jwt (mod_unified_push → push-уведомления), исправления безопасности. - **Проблема**: официальный Docker-образ `prosody/prosody` не обновлён до 0.12 (latest = 0.11.9; теги только 0.9–0.11 + trunk). Стабильный 0.12.6 доступен только исходниками: https://prosody.im/downloads/source/prosody-0.12.6.tar.gz - **Сторонние образы**: `prosodyim/prosody:0.12` (nightly 236 от 2026-04-29) и `prosodyim/prosody:13.0` (nightly 98 от 2026-06-07) — НЕ для продакшена. - **Решение**: собрать свой образ из исходников 0.12.6 (Dockerfile на базе prosody/prosody:0.11.9 + tar.gz 0.12.6). См. STATUS.md «Сборка Prosody 0.12». ### 12.9 Каналы и папки Telegram в рostере (2026-08-29 вечер) **Вопрос пользователя**: «как найти каналы из Телеграм в рostере? а ещё папки были в телеге — как вытащить?» **Ответ (проверено) — каналов в рostере XMPP НЕТ, и это нормально:** - XMPP-рostер (`jabber:iq:roster`) — это **только личные контакты**: 1602 записи, все вида `число@telegram.nixg.ru` (пример: `1646170373@telegram.nixg.ru` = «Наталья Рыбий»). Контактов с префиксом `group-` в рostере — **0**. - Каналы/группы живут **отдельно**, в БД моста (таблица `room` в slidge.sqlite): **91 канал** (`muc_type='CHANNEL'`, jid_localpart `group-`) + **40** групп/прочих. - В XMPP каналы должны показываться как **закладки комнат (XEP-0402 bookmarks)** → в Pidgin «Комнаты → Закладки», откуда заходят. **Сейчас bookmarks сломаны** из-за mod_privilege-стаба (XEP-0356 не пересылает MDS/pubsub-запросы), поэтому слайдж вместо закладок шлёт **приглашения** в каждую комнату — «миллион запросов». - **Обходной путь сейчас**: Pidgin → Комнаты → Присоединиться → адрес `group-@telegram.nixg.ru`, ник `estorozhenko` (пример `group-1001698123474@telegram.nixg.ru`). Полный список каналов: `SELECT jid_localpart, name FROM room WHERE muc_type='CHANNEL'` в БД моста. - **Папки Telegram**: слайдж **не поддерживает** (`session.py:292`, обработчик `DialogPeerFolder` — `# TODO: investigate what that is; return`). В XMPP аналога «папок чатов» нет (рostер плоский). Pyrogram `get_folders()` (`messages.GetDialogFilters`) технически умеет читать папки, но отдельный MTProto-клиент на той же сессии выкинет работающий мост → вариант только «остановить мост на 30 сек, прочитать, запустить». Практической ценности нет — в XMPP-клиенте папки всё равно некуда вывести. **Диагностика рostера (полезный навык, не повторять ошибку):** - Признак «контакты пропали»: клиент (slixmpp/Pidgin) показывает пусто. **Проверять НЕ через `bot.roster`/`roster_update`** (slixmpp читает свой кэш и событие срабатывает раньше полного ответа — ложно показывает 1 контакт), а **сырым IQ**: `bot.make_iq_get(queryxmlns='jabber:iq:roster').send()` → `.findall('.//{jabber:iq:roster}item')`. Верифицировано: вернул **1602 item'а**. - Рostер-файл: `/var/lib/prosody/nixg%2eru/roster/estorozhenko.dat` (445 КБ, 1602 записи, версия 1607) — читается через `datamanager.load("estorozhenko","nixg.ru","roster")` только при установленном `data_path` (вне Prosody-контекста он nil → ложный ENOENT). - Debug-логирование mod_privilege: добавить `debug = ".../prosody.log"` в блок `log` конфига (временно; после отладки вернуть) — в логе видно `Entity is privileged` на обоих хостах, `load_roster: asked for` / `save_roster: saving roster (all contacts)`. - Замечание: каталог данных называется `nixg%2eru` (URL-кодирование точки), а не `nixg.ru` — `ls /var/lib/prosody/nixg.ru` даёт «No such file» — это норма, не ошибка. **Причина «миллиона приглашений» подтверждена**: в БД моста `user_account.preferences` JSON содержит `"always_invite_when_adding_bookmarks": true` — слайдж шлёт приглашение на каждую группу при синхронизации. Кандидат на `false`, когда bookmarks заработают (после апгрейда 0.12). Пока слайдж-стаб не чинит bookmarks, приглашения — единственный способ «увидеть» каналы, их можно игнорировать в Pidgin. ## 13. TODO (следующие шаги) - [x] ~~OMEMO (XEP-0384)~~ — ВЫПОЛНЕНО: раздел 9; скрипт scripts/omemo_check.py. - [ ] Бот-книгоискатель (slixmpp + OPDS) — Этап 3 PRD. venv уже создан (/opt/icq/.venv: aiohttp, slixmpp). - [ ] Мосты mautrix-whatsapp — Этап 4 (нужен Prosody mod_component / external component). [Telegram уже на Slidge] - [ ] Push-уведомления APNs/FCM — ОТЛОЖЕНО, варианты в разделе 10.4. - [ ] Продление LE-серта nixg.ru — 2026-10-15 10:00 (cron Hermes, job ca2a23a34905); процедура в разделе 4.