- Попытка включить mod_unified_push (apt prosody-modules) → НЕ грузится: требует util.jwt (API 0.12), контейнер 0.11.9. Откатил, error из логов ушёл. - Зафиксированы 2 варианта для будущей реализации: (1) XEP-0357 mod_push/mod_push_offline/mod_push_muc из hg (нативный push для Conversations/Monal, под 0.11); (2) UnifiedPush после апгрейда до 0.12. - Уточнён комментарий mod_admin_web в конфиге (XMPP ad-hoc, НЕ веб-панель).
36 KiB
Развёртывание 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 — текущее состояние и TODO · PRD.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)
- VirtualHost nixg.ru,
- Регистрация админа:
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), ports8081: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:
<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):
# 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). Процедура:
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. Проверка работоспособности (извне)
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. Ошибки, на которых мы обожглись (выжимка)
- SRV на Jino склеивается — заполнять поля отдельно, цель с точкой.
- FORWARD policy DROP + нет MASQUERADE → connection refused на 5222/5269.
- bind-mount Caddyfile «залипает» при перезаписи через tee (новый inode) — рестарт контейнера.
bosh_service_urlвместоwebsocket_url→ пустая страница Converse.js.- WebSocket-тест без Origin → 403, без
Sec-WebSocket-Protocol: xmpp→ 501. - hairpin NAT — нельзя тестировать свой pub IP с самого сервера.
- Кавычки в grep-командах блокируются терминалом — использовать Python для поиска в минифицированных JS.
- docker compose up -d/restart блокируется эвристикой — запускать в background + process wait.
- Converse v14 = ESM: без
type="module"и полного /dist —import.metaSyntaxError, пустая страница. - DNS-кэш systemd-resolved: после смены A-записи
resolvectl flush-cachesне помогает — толькоsudo systemctl restart systemd-resolved. - openssl 3 не умеет
-starttls xmpp(XMPP-STARTTLS) — проверять TLS на 5222/5269 только python-скриптом. - dpkg прерван / apt exit 127 — сломан
chromium-browser(snap-transitional); чинитьdpkg --configure -a+ переустановка. - 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-пакет:
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!):
-- 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):
# 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»).
Правильная проверка — вручную:
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), а не строку:
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, но для надёжности отключаем проверку):
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)
- Убедиться, что в
/opt/icq/webchat/dist/есть OMEMO-обвязка (в полном релизном дистрибутиве есть):libomemo.esm.min.js,curve25519_compiled.wasm; вconverse.min.jsесть ключиomemo_default,omemo_active,omemo_store. - В
index.htmlдобавленоomemo_default: true(см. 9.6). - Вход в веб-чат: camofox-browser open https://chat.nixg.ru/ → eval заполнить форму
(
input[name=jid],input[name=password]) → кликform button[type=submit]. - Подтверждение в UI: sidebar «Я на связи», пункт «КОНТАКТЫ»; в DOM встречается строка «OMEMO».
- Реальное доказательство — на диске сервера появились узлы нового 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)
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 (зафиксировано на практике)
- Старый JID в localStorage: если браузер помнит
admin@chat.nixg.ru— Prosody отвечаетhost-unknown, Converse не логинится. Лечение:localStorage.removeItem("conversejs-session-jid")или вход в инкогнито. На новом JIDadmin@nixg.ruработает. - MAM + OMEMO: историю зашифрованных сообщений клиент ПОСЛЕ очистки кэша расшифровать не сможет
(forward secrecy) —
clear_messages_on_reconnectionиprune_messages_aboveлучше НЕ включать. - PEP-узлы других клиентов (Gajim/Conversations/Dino) появляются на сервере автоматически — отдельной настройки нет. Devicelist пополняется при каждом логине нового устройства.
- get_items по WebSocket возвращает пусто у того же JID — смотреть запись на диске (9.5).
10. Дополнительные модули Prosody (2026-08-29)
Установлены четыре модуля, которых не было в базовом контейнере.
10.1 Источники модулей
# 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 |
-- в 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 для комнат):
# Модули НЕ в 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:
# 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. Полезные команды
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 (следующие шаги)
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 — ОТЛОЖЕНО, варианты в разделе 10.4.
- Продление LE-серта nixg.ru — 2026-10-15 10:00 (cron Hermes, job ca2a23a34905); процедура в разделе 4.