Личный сервер Matrix на базе Tuwunel, легковесного homeserver на Rust. Мой адрес в Matrix теперь @gluek:gluek.info, написать мне можно по ссылке matrix.to/#/@gluek:gluek.info.
Зачем свой сервер, если есть matrix.org
Аккаунт @gluek:matrix.org я завёл довольно давно, чтобы читать несколько публичных групп, но активно им так и не пользовался.
Главный довод за свой сервер: в Matrix нельзя перенести аккаунт с одного сервера на другой. Часть адреса после двоеточия навсегда привязывает его к серверу, и если однажды условия на matrix.org перестанут устраивать, остаётся только завести новый аккаунт и заново собирать контакты и комнаты. А раз переносить почти нечего, лучше сделать это сейчас.
Повод задуматься дал и сам matrix.org: для бесплатных аккаунтов там появились лимиты: не больше 10 МБ на вложение и 500 МБ трафика вложений за 28 дней, а в примечании честно сказано, что лимиты будут пересматриваться. Мне этого пока хватает, но это тот же сценарий, что и с 1Password: зависимость от чужой ценовой политики.
При этом привязка к домену, а не к серверу, работает в мою пользу: сервер можно сколько угодно переносить между машинами и даже реализациями, а адрес @gluek:gluek.info останется тем же, пока у меня есть домен.
Отдельно стоит сказать про приватность: свой сервер на одного человека её почти не прибавляет. Остальные серверы федерации видят, в каких комнатах я состою и когда пишу, и на сервере с одним пользователем вся его активность — это я. На matrix.org я хотя бы терялся в толпе. Так что дело здесь в контроле над адресом и данными, а не в анонимности.
Почему Tuwunel
Эталонный сервер Synapse на Python требует PostgreSQL и заметно больше памяти, а для одного аккаунта это перебор. Dendrite фактически в режиме поддержки. Остаются потомки conduwuit на Rust: Tuwunel и continuwuity. Оба ставятся одним контейнером со встроенной RocksDB и поддерживают sliding sync для Element X.
Выбрал Tuwunel: его основной спонсор — правительство Швейцарии, разработку ведут штатные сотрудники, и для сервера, который должен прожить долго, это выглядит надёжнее. continuwuity развивает сообщество, и проект тоже живой. По возможностям они сейчас близки.
Стек
- Tuwunel через Docker Compose на том же VPS, что и остальные сервисы. В простое занимает около 80 МБ памяти
- Caddy в роли reverse proxy (с автоматическим HTTPS от Let’s Encrypt)
- Делегирование:
server_name— этоgluek.info, а сам сервер живёт наmatrix.gluek.info. Порт 8448 и SRV-записи не нужны, федерация идёт через 443 - Регистрация закрыта: аккаунт создан по одноразовому токену, после чего регистрация выключена. Новые аккаунты я могу завести вручную через админ-комнату
- Бэкапы каждую ночь: контейнер на секунду останавливается, база RocksDB копируется целиком, и с неё снимаются копии вместе с остальным сервером. Подписывающий ключ сервера лежит внутри базы, так что эта копия и есть весь сервер
- Обновления вручную по уведомлению в админ-комнате: скрипт сначала делает бэкап, потом подтягивает новый образ и ждёт, пока сервер пройдёт health-check
- Клиенты: Element Desktop на Mac (есть в Homebrew:
brew install --cask element) и Element X на Android. Возможно, ещё попробую FluffyChat
Сервер личный, но если мы знакомы и вам нужен аккаунт в Matrix, пишите: заведу. Если нет, можно поднять свой сервер, ориентируясь на этот разбор, или выбрать один из публичных серверов.
Делегирование через .well-known
Чтобы адрес был @gluek:gluek.info, а не @gluek:matrix.gluek.info, сервер и клиенты должны найти настоящий хост через два файла на основном домене: /.well-known/matrix/server и /.well-known/matrix/client. Tuwunel умеет отдавать их сам, если указать в конфиге:
[global]
server_name = "gluek.info"
[global.well_known]
client = "https://matrix.gluek.info"
server = "matrix.gluek.info:443"
А в Caddy достаточно прокинуть эти пути с основного сайта:
gluek.info {
handle /.well-known/matrix/* {
reverse_proxy tuwunel:8008
}
# ... остальной сайт
}
matrix.gluek.info {
request_body {
max_size 100MB
}
reverse_proxy tuwunel:8008
}
Подводный камень: двойной CORS-заголовок
По многим инструкциям для .well-known/matrix/client нужно руками добавить Access-Control-Allow-Origin: *, иначе веб-клиенты не смогут прочитать файл. Но если файлы отдаёт сам Tuwunel, он уже ставит этот заголовок, и с ручным добавлением в ответе оказываются два одинаковых заголовка. Браузеры такой ответ отвергают как некорректный CORS, хотя curl и федерационный тестер ничего подозрительного не показывают. Проверить так:
curl -sI https://example.com/.well-known/matrix/client | grep -ci access-control-allow-origin
Должно быть 1.
Проверить федерацию целиком можно через Matrix Federation Tester.
Звонки: два несовместимых протокола
Самое неочевидное во всей истории. В Matrix сейчас две разные системы звонков, и друг с другом они не соединяются.
- Старые звонки 1:1 (события
m.call.*) — обычный WebRTC: устройства соединяются напрямую, а если мешает NAT, идут через TURN-сервер. Их умеют Element Desktop/Web, классический Element Android и FluffyChat. Групповые звонки так не сделать (каждый соединяется с каждым), поэтому в Element они годами работали через встроенный Jitsi-виджет. - MatrixRTC / Element Call — новая схема: все участники шлют медиа на SFU-сервер LiveKit, а он раздаёт потоки остальным. Звонок 1:1 и групповой устроены одинаково, сквозное шифрование работает и в группах. Цена — ещё один сервер рядом с homeserver.
Element X умеет только новую схему. Поэтому если позвонить из FluffyChat человеку в Element X, он звонка просто не увидит, и наоборот. Обе стороны должны пользоваться клиентами с одной и той же схемой. И отдельно: на сервере без LiveKit в Element X звонки не работают вовсе, никакой TURN тут не поможет.
У себя я поднял обе:
- TURN для старых звонков: coturn, который уже стоял на сервере для другого сервиса. Tuwunel выдаёт клиентам временные учётные данные к нему через общий секрет (
turn_urisиturn_secret_fileв конфиге) - LiveKit для Element Call: сам LiveKit и маленький lk-jwt-service, который выдаёт токены участникам Matrix, на поддомене
rtc.gluek.info. Клиенты узнают о нём из того же.well-known/matrix/client(в Tuwunel это одна строкаlivekit_url). Медиа идёт через один UDP-порт, так что отдельный диапазон портов не нужен. Звонок в Element X между@gluek:gluek.infoи моим старым аккаунтом на matrix.org работает в обе стороны, со звуком и видео
Подводный камень: TURN как дыра во внутреннюю сеть
TURN по своей природе пересылает трафик туда, куда попросит клиент. Если не запретить частные адреса, то любой, у кого есть учётные данные (а с Matrix их получает каждый залогиненный клиент), может через coturn достучаться до внутренних сервисов: баз данных в Docker-сетях, localhost и всего, что не торчит наружу. Закрывается это списком --denied-peer-ip для всех частных диапазонов (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 и т.д.), а заодно --no-tcp-relay.
Но в Docker это ломает сам TURN: в bridge-режиме ретранслятор живёт на внутреннем 172.x-адресе, и звонок, где обе стороны идут через один и тот же TURN, упирается в только что запрещённый диапазон. Помог перевод coturn в network_mode: host с явным публичным адресом (--listening-ip и --relay-ip). Тогда, правда, порты перестают пробрасываться в обход файрвола, и релейный диапазон нужно открыть в UFW.
Проверить можно утилитой turnutils_uclient из того же образа coturn: запрос к внутреннему адресу должен получать 403, а обычная ретрансляция — проходить без потерь.