Личный сервер 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в конфиге). Кроме обычного порта 3478 (UDP и TCP) клиенты получают ещёturns:turn.gluek.info:443— TURN по TLS на 443-м порту, на случай сетей, где открыт только HTTPS (как он сделан, ниже) - 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.gluek.info:443LiveKit раздаёт клиентам как запасной путь (rtc.turn_serversс тем же общим секретом), если не проходят ни UDP 7882, ни TCP 7881
Подводный камень: 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, а обычная ретрансляция — проходить без потерь.
TURN по TLS на 443-м порту рядом с Caddy
В сетях с жёсткой фильтрацией открыт бывает только 443-й порт. TURN умеет работать и там: по TLS, так что снаружи соединение выглядит как обычный HTTPS. Но 443-й порт у меня занят Caddy, через который работают все сайты.
Решил это модулем caddy-l4, который добавляет в Caddy обработку на уровне TCP. Он смотрит на имя сервера (SNI) в начале TLS-соединения: соединения с turn.gluek.info Caddy расшифровывает сам и отдаёт обычным TURN в coturn на порт 3478, а всё остальное идёт дальше, к сайтам. Свой TLS и свои сертификаты coturn при этом не нужны, сертификат для turn.gluek.info получает и продлевает Caddy:
{
servers :443 {
listener_wrappers {
layer4 {
@turn tls sni turn.gluek.info
route @turn {
tls
proxy turn.gluek.info:3478 # coturn в host-сети, поэтому по внешнему имени, а не по имени контейнера
}
}
tls
}
}
}
# нужен только для того, чтобы Caddy получал и продлевал сертификат;
# TLS-соединения с этим именем до HTTP-сервера не доходят
turn.gluek.info {
respond 404
}
Модуль не входит в официальный образ, поэтому Caddy теперь своя сборка:
FROM caddy:2.11.6-builder AS builder
RUN xcaddy build v2.11.6 --with github.com/mholt/caddy-l4
FROM caddy:2.11.6
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Цена — Caddy больше не обновляется сам вместе с тегом latest: для новой версии нужно пересобрать образ.
Раз временные логины к TURN теперь раздаёт и Jitsi, а его может открыть кто угодно, на coturn я поставил квоты: не больше ~15 Мбит/с на сессию в каждую сторону (--max-bps), не больше 16 сессий на одного пользователя (--user-quota) и 100 сессий всего (--total-quota).