Настройка Element Call

Короче.
Есть инструкция: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md
Я думал, что можно, читая сколько-то по диагонали, поставить это всё. Увы. Оказалось, сервис сделан айтишниками для айтишников, а я, похоже, ненастоящий. Поэтому с помощью ChatGPT и такой-то матери в течение нескольких дней (прям рабочих дней; на что я трачу свои выходные?) получилось поставить.
Что меня больше всего расстраивает - это отсутствие объяснения взаимосвязей. Ребята написали опенсорс тут, заюзали опенсорс там, а единственное место, где всё преднастроено "из коробки" - их Docker-образ. Однако, Docker пока не по мою душу - я решил настроить каждый элемент ))) отдельно. К слову, там есть какие-то графики этих взаимосвязей, но оно то ли поверхностно, то ли абстрактно, то ли для кого-то ещё.
Возможно, я просто тупой
Что за гайд
Гайд очень жёстко граничит с "уплыло всё нахуй в море", ибо оПеНсОрС - для любого такого продукта ставятся сначала зависимости, потом зависимости зависимостей, потом зависимости зависимостей зависимостей, а спустя трое суток ты просыпаешься в холодном поту, поняв, что сам продукт ты поставить забыл.
Я постараюсь сводить длинные секции балабольства воедино к концу каждого раздела, ибо я сам читал те доки посредственно, но схема с "эксперимент → пипец → чтение инструкции" здесь не сработала, потому что вместо экспериментов пришлось искать, в каком шкафу лежат очередные зависимости.
Записано всё на горячую тёплую руку по истории команд терминала и поисковых запросов, поэтому, возможно, я что-то забыл. Но весь процесс достаточно поджёг мою задницу, чтобы я вообще всё это написал. Айтишники опять придумали хуйню.
Благодарности и ссылки сразу
- Вот этому господину. По его статье можно было хоть примерно сориентироваться, что происходит, и как оно работает (но я всё равно понял не сразу) (и до сих пор не всё).
- И вот этому блогу (внезапно, ещё один блог .de) за примеры.
- И разрабам, что ну хоть как-то добрались и написали доку. А то четыре месяца назад там вообще ничего не было - в README.md репозитория просто было написано "а вот такая штука у нас есть))) Хотите свою такую же? Ебитес))". К слову, графики митоза в их документации мой мозг отказывается воспринимать до сих пор (и отдельно благодар очка за подставу с неактуальной версией в гайде установки lk-jwt-service - 0.1.1 вместо 0.2.3, рррряяяяяя; было весело обнаружить).

Мои исходные
"Железо"
- отдельная виртуалка под nginx reverse proxy. На ней висит не только Matrix, поэтому я просто докидываю туда конфиги;
- отдельная виртуалка (ВМ) под сам Matrix;
- и ещё отдельная под PostgreSQL;
- всё это на Ubuntu 24.04;
- виртуалки очень скромные по ресурсам (1 ядро, 2ГБ RAM, 20/40ГБ SSD), сам Matrix Synapse для тестов был успешно запущен в пределах одной ВМ - вместе с базой и nginx. Требуемая мощность будет диктоваться вашими объёмами, ✨htop в помощь✨.
Домены
- основой, второго уровня (указан как
realm_name
везде в Matrix и TURN/coturn):hahaha.lol
(.lol
- это реальный TLD, кстати; может, всё-таки использовать в статье example.com?) - домен Matrix:
matrix.hahaha.lol
- домен звонков (тоже третьего уровня):
call.hahaha.lol
Веб-сервер
Я привык работать с nginx, поэтому везде, где фигурирует веб-сервер, речь идёт именно о нём. В гайдах Matrix мелькают другие веб-сервера и примеры для них (например, гайд по Reverse Proxy)
Element Call поставлен на той же ВМ, что и reverse proxy, ибо там ходит внешний трафик, а TURN-сервера, как я понимаю, не любят проксироваться - нужен прямой доступ к клиентам через соответствующие порты сервера.
⚠️ Дебаг рекомендую проводить, залогинившись через https://app.element.io (или ваш собственный Element Web), в обнимку с консолью разработчика - там хорошо видны пакеты, летающие от веб-сервера и к нему. Там как раз и было хорошо видно проблемы с CORS и с анонсом эндпоинтов для клиентов.
Я
Преимущественно виндовый админ, но регулярно ковыряю селф-хостинг, ибо интересно поднять что-то своё. Скорее всего, по тексту есть ошибки конфигурации, но оно работает. А всё, что ниже – это на сколько меня хватило залезть вглубь, чтобы поднять Element Call и написать эту статью. Буду рад исправить, если есть какие-то оглушительные косяки.
Статья довольно неорганизованная, по моим ощущениям, поэтому настоятельно рекомендую Ctrl/Cmd+F для поиска инфы, если вдруг случаются слишком большие разрывы в повествовании.
Предварительно
Сертификаты
Скорее всего, у вас уже есть сертификаты на matrix.hahaha.lol
и/или hahaha.lol
Теперь нужно получить и на call.hahaha.lol
. У меня есть свой playbook для Ansible под эти цели, но получение сертификатов явно за пределами этой статьи (может, будет статья по соседству). Сертификаты беру в Let's Encrypt (у них вообще есть прекрасный certbot), можно также рассмотреть ZeroSSL.
Пока что с этим могу отправить только в Google или Яндекс или Bing.
Matrix Synapse
Matrix Synapse (и, думаю, любая другая реализация их протоколов) должен быть в курсе, что на сервере будут проводиться звонки. Сумма изменений [в моём] конфиге чуть ниже, пока что секции с пояснениями. Для звонков понадобилось в homeserver.yaml
прописать следующее (по их инструкции):
### Element Call
experimental_features:
# MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
max_event_delay_duration: 24h
rc_message:
# This needs to match at least e2ee key sharing frequency plus a bit of headroom
# Note key sharing events are bursty
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
# This needs to match at least the heart-beat frequency plus a bit of headroom
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
per_second: 1
burst_count: 20
Хотелось бы, чтобы это было всё, но нет. Element Call и их самописная служба авторизации (lk-jwt-service
) будет использовать Synapse как источник учётных записей (что-то вроде Identity Provider). Поэтому загадочной фразой в документации висит фраза, упоминающая о необходимости заиметь listener на федерацию или OpenID. Когда строка в документации ссылается на другую документацию, которая ссылается на третью, я начинаю понимать, почему мои коллеги заявляли мне о херовости плохом качестве моих собственных доков.
Оказывается, можно включить федерацию только на собственный сервер и всё ещё запрещать чужие подключения:
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: [<...>]
resources:
- names: [client, federation] # в resources была добавлена federation
compress: false
### Federation settings
allow_public_rooms_over_federation: false # нельзя публичные комнаты через федерацию
federation_domain_whitelist: [ "hahaha.lol" ] # только свой домен
federation_sender_enabled: false
В моём конфиге 8448 даже не фигурирует и не проксируется через nginx.
Домен второго уровня (SLD)
Если Matrix - не единственное, что хостится на вашем домене, то "корневой домен" (hahaha.lol
, например; SLD) обязан анонсировать для клиентов Matrix (Element, Element X, Cinny, Nheko и т.д.) адреса, куда клиентам (и другим серверам - в случае федерации) нужно обращаться за дополнительной информацией.
В моём случае Synapse хостится на соседнем от SLD-домене: в этом случае логины имеют формат @shrek:hahaha.lol
, а сам Synapse работает из-под matrix.hahaha.lol
Это если вы хотите нормальные логины. Никто не запрещает хостить иметь realm_name
вида matrix.hahaha.lol
с логинами @shrek:matrix.hahaha.lol
, однако такое считается bad practice.
Суммарный конфиг на hahaha.lol
для nginx reverse proxy:
location /.well-known/matrix/client {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
default_type application/json;
return 200 '{"m.homeserver":{"base_url":"https://matrix.hahaha.lol"},"m.identity_server":{"base_url":"https://matrix.hahaha.lol"},"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://call.hahaha.lol/livekit"
},
{
"type": "nextgen_new_foci_type",
"props_for_nextgen_foci": "val"
}
]}';
В ответный JSON добавлена секция rtf_foci
(foci - оказывается, не аббревиатура, это множественное число слова "focus"; куда Matrix RTC будет "фокусироваться")
call.hahaha.lol
- соответствует адресу https://matrix-rtc.example.com/
из инструкции
Также (в случае отдельного от SLD домена - третьего уровня и глубже) обязательны заголовки Access-Control, ибо Delegation - делегирование операций с основным доменом (SLD) домену третьего уровня (hahaha.lol
-> matrix.hahaha.lol
.
Сами звонки
Понадобятся два репозитория и отдельная софтина (чудеса опенсорса): livekit, а также Element Call и lk-jwt-service от самих Matrix. Последнее - та самая самописная служба, которая будет генерить JWT-токены. С ней будет прикол, но о нём позднее
Сначала Element Call
⚠️ Он нужен, если хочется иметь свою морду звонков типа https://call.element.io (по секрету признаюсь, что такую морду я пока не сумел заставить работать; скорее всего, что-то не доложил). По факту, эту секцию можно пропустить (только про Element Call, до "А теперь звоночки"), я так чувствую (но я с этим долго долбался, поэтому оно здесь).
"Если верить документации", Element Call использует маршрутизацию [запросов] со стороны клиента ("client-side routing"), а в связи с этим несуществующие пути стоит заворачивать на /index.html
(а не отдавать 404).
Процесс билда должен завестись прям по докам:
git clone https://github.com/element-hq/element-call.git
cd element-call
corepack enable
yarn
yarn build
🎸 Если у вас нету yarn-а 🎶 Если у вас нет yarn или вдруг вылезла ошибка:
This project's package.json defines "packageManager": "yarn@4.7.0". However the current global version of Yarn is 1.22.22.
Нужно провернуть фокус
Если нет даже npm:
sudo apt install npm
Потом yarn:
sudo npm install --global yarn
Потом подняться наверх из element-call/
, ибо изнутри директории активация corepack не срабатывает, и запустить
cd ..
corepack prepare yarn@4.7.0 --activate
yarn set version 4.7.0
А вот после этого уже билдить по документации.
Всё слово целиком:
cd ~
sudo apt install npm
sudo npm install --global yarn
corepack prepare yarn@4.7.0 --activate
yarn set version 4.7.0
git clone https://github.com/element-hq/element-call.git
cd element-call/
corepack enable
yarn
yarn build
Corepack - это, вроде как, менеджер менеджеров пакетов для node.js (yarn
- как раз менеджер пакетов; а в веб 2.0, прекрасная маркиза, всё хорошо, всё ха-ра-шо). Скорее всего, он у меня стоял с самим node.js
Node.js очень хорошо ставится из NodeSource (настоятельно рекомендую версию LTS, если софт не рекомендует иное. Element Call не рекомендует)
Установка node.js v22 LTS для Ubuntu:
sudo apt-get install -y curl
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt-get install -y nodejs
node -v # проверить установленную версию
После всех этих действий в element-call/
появится директория dist/
, которую и нужно будет отдавать клиентам через веб-сервер
В директории также, предположительно, должен присутствовать config.json
. Поэтому. Изнутри директории element-call/
:
cp config/config.sample.json dist/config.json
sudo vi dist/config.json
В нём:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.hahaha.lol",
"server_name": "hahaha.lol"
}
},
"livekit": {
"livekit_service_url": "https://call.hahaha.lol/livekit/sfu"
},
"features": {
"feature_use_device_session_member_events": true
},
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf"
}
⚠️ Строго говоря, я так и не понял, кто и в каком случае к нему обращается. Сам файл по структуре похож на сформированный JSON в /.well-known/matrix/client
для SLD. Но он захосчен вместе с остальной статикой по той же документации.
После этого относим весь element-call/dist
туда, где его будет отдавать nginx:
sudo mkdir /var/www/elementcall
sudo cp -r dist/* /var/www/elementcall
И прописываем локейшн в конфиг для call.hahaha.lol
(конфиг целиком будет ниже):
location / {
root /var/www/elementcall;
try_files $uri $uri/ /index.html;
}
А теперь звоночки
Сам Element Call особо ничего не делает - технической стороной занимается отдельная утилита livekit.
curl -sSL https://get.livekit.io | bash
livekit-server --version
sudo mkdir -p /etc/livekit
livekit-server generate-keys
Последняя команда сгенерирует необходимые API-ключи для обращений к livekit (этим будет заниматься lk-jwt-service). Возьмите эти ключи с собой - они понадобятся в конфиге.
Вообще, кстати, я запредельно озадачен либо своим умением гуглить, либо документацией всех этих сервисов, ибо это было совершенно неочевидно - редко когда приходится что-нибудь гуглить дольше получаса
sudo vi /etc/livekit/config.yaml
В самом конфиге:
# Main port for LiveKit signaling and API
port: 7880
# Logging level: debug, info, warn, error
log_level: info
# WebRTC configuration
rtc:
# UDP ports for client traffic; ensure these are open in your firewall
# можно диапазон поменьше - зависит от числа клиентов
port_range_start: 50000
port_range_end: 60000
# TCP port for WebRTC fallback when UDP isn't available
tcp_port: 7881
# Discover public IP via STUN; useful in cloud environments
use_external_ip: true
interfaces:
includes:
- eth0 # интерфейс с внешним ip
# turn, встроенный в livekit
turn:
enabled: true
tls_port: 5350
relay_range_start: 50000
relay_range_end: 60000
external_tls: true
domain: call.hahaha.lol
# optional (set only if not using external TLS termination)
cert_file: /etc/letsencrypt/certs/fullchain_call.hahaha.lol.crt
key_file: /etc/letsencrypt/keys/call.hahaha.lol.key
# Redis configuration for distributed deployments
redis:
address: 10.0.0.6:6379
# Uncomment and set if Redis requires authentication
# username: your_redis_username
# password: your_redis_password
keys:
<API-ключ>: <секрет>
Если с конфигом всё в порядке, то команда
/usr/local/bin/livekit-server --config /etc/livekit/config.yaml
расскажет логами, куда на какие порты прикрепился livekit и что, в целом, он работает. Если нет, то расскажет, где сломалась. livekit логгирует в stdout, логи в /var/log придётся делать самому. Я пока не делал. Слишком хотелось заставить это работать хоть как-нибудь.
Если всё успешно, для удобства можно создать службу (daemon в systemd):
sudo vi /etc/systemd/system/livekit.service
В ней:
[Unit]
Description=LiveKit SFU Server
After=network.target
[Service]
ExecStart=/usr/local/bin/livekit-server --config /etc/livekit/config.yaml
Restart=always
RestartSec=3
User=root
WorkingDirectory=/etc/livekit
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Перезагрузка демонов, активация, включение службы и запрос её статуса:
sudo systemctl daemon-reload
sudo systemctl enable livekit
sudo systemctl start livekit
sudo systemctl status livekit
lk-jwt-service
Служба генерации временных токенов для работы с livekit. Именно она будет использовать API-ключ и секрет из livekit и именно для неё нужен был listener на федерацию в Matrix Synapse.
По ней вполне толковая инструкция, за исключением некорректной версии в README.md
Для службы понадобится заиметь golang (если ещё не):
sudo apt install golang
Установка и перемещение в рабочую директорию:
wget https://github.com/element-hq/lk-jwt-service/archive/refs/tags/v0.2.3.tar.gz
tar -xvf v0.2.3.tar.gz
mv lk-jwt-service-0.2.3/ lk-jwt-service
cd lk-jwt-service
go build -o lk-jwt-service .
cd ..
sudo cp ~/lk-jwt-service/ /opt/lk-jwt-service
sudo chown -R www-data:www-data /opt/lk-jwt-service
И создание демона:
sudo vi /etc/systemd/system/lk-jwt.service
Демона sspaeth (блог №1 в "Благодарностях") как раз рекомендует запускать его из-под ограниченного пользователя nginx:
[Unit]
Description=LiveKit JWT Service
After=network.target
[Service]
Restart=always
User=www-data
Group=www-data
WorkingDirectory=/opt/lk-jwt-service
Environment="LIVEKIT_URL=wss://call.hahaha.lol/livekit/"
Environment="LIVEKIT_KEY=<API-ключ от livekit>"
Environment="LIVEKIT_SECRET=<secret от livekit>"
Environment="LIVEKIT_JWT_PORT=8080"
ExecStart=/opt/lk-jwt-service/lk-jwt-service
[Install]
WantedBy=multi-user.target
В файле демона выставляются переменные среды, из которых служба берёт все необходимое для работы
sudo systemctl daemon-reload
sudo systemctl enable lk-jwt.service
sudo systemctl start lk-jwt.service
sudo systemctl status lk-jwt.service
Посмотреть, чего творит служба можно с помощью:
sudo journalctl -u lk-jwt.service -f
Если что-то не заработает, служба отпишет в журнал
Веб-сервер (и iptables)
Осталось только начать принимать подключения к этим всем вот сервисам (и объяснить клиентам, куда ходить
Конфиг nginx для hahaha.lol
(SLD), включая ответы для федерации и клиентов, в каком месте хостится сам Matrix Synapse:
server {
listen 80;
server_name hahaha.lol;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name hahaha.lol;
location ~ ((/_matrix.*)|(config.*json)) {
return 404;
}
location /.well-known/matrix/server {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
default_type application/json;
return 200 '{"m.server": "matrix.hahaha.lol:443"}';
}
location /.well-known/matrix/client {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
default_type application/json;
return 200 '{"m.homeserver":{"base_url":"https://matrix.hahaha.lol"},"m.identity_server":{"base_url":"https://matrix.hahaha.lol"},"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://call.hahaha.lol/livekit"
},
{
"type": "nextgen_new_foci_type",
"props_for_nextgen_foci": "val"
}
]}';
}
ssl_certificate /etc/letsencrypt/certs/fullchain_hahaha.lol.crt;
ssl_certificate_key /etc/letsencrypt/keys/hahaha.lol.key;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/nginx/dhparams.pem;
access_log /var/log/nginx/matrix_tld.log;
}
Конфиг nginx для hahaha.lol (SLD)
[Максимально дезорганизованный] конфиг nginx для call.hahaha.lol
:
server {
listen 80;
server_name call.hahaha.lol;
return 301 https://$host$request_uri;
}
server {
server_name call.hahaha.lol;
location / {
root /var/www/elementcall;
try_files $uri $uri/ /index.html;
}
location /livekit/ {
proxy_pass http://127.0.0.1:7880/; # LiveKit media server port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
}
location ~ ^/livekit/(jwt/|healthz/) {
proxy_pass http://127.0.0.1:8080; # Your JWT service
proxy_set_header Host $host;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
}
location = /livekit/sfu/get {
proxy_pass http://127.0.0.1:8080/sfu/get; # Your JWT service
proxy_set_header Host $host;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
# Proxy WebSocket connections to LiveKit
location /livekit/sfu {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Authorization';
# LiveKit SFU websocket connection running at port 7880
proxy_pass http://localhost:7880/;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/certs/fullchain_call.hahaha.lol.crt;
ssl_certificate_key /etc/letsencrypt/keys/call.hahaha.lol.key;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/nginx/dhparams.pem;
access_log /var/log/nginx/call.hahaha.lol.log;
}
Конфиг nginx для call.hahaha.lol
Отдельное внимание на пути к /livekit/sfu/get
- я не разобрался, как по этому локейшну отправлять запрос корректно в lk-jwt-service, поэтому к ip:порт в proxy_pass
приписан ещё и путь (коллеги подсказали, что так делать нормально)
livekit/healthz
создан по рекомендации sspaeth, но он у меня в таком виде не работает. Опечатка? Возможно, та же проблема с путями
Прикол с lk-jwt-service
В конфиге для call.hahaha.lol
помимо добавления заголовков есть строки, где заголовки скрываются:
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
Сделано это затем, что служба lk-jwt-service прямо в своём коде добавляет CORS-заголовки - Access-Control-Allow-Origin, -Methods и -Headers. Но в процессе передачи пакетов обратно клиенту через веб-сервер, если веб-сервер добавляет свои CORS, то заголовки дублируются.
В процессе конфигурации я наткнулся на непонятный мне фокус, при котором, если я убираю добавление CORS в конфиге nginx, то lk-jwt-service тоже не добавляет заголовки, а браузер сыпет ошибками "Cross-Origin not Allowed".
Конструкция с proxy_hide_header
и add_header
следом гарантирует, что заголовки добавляет только nginx и Access-Control-Allow-Origin
(и остальные) не дублируются.
iptables
И для этого всего нужны правила в iptables.
Для livekit/turn:
-A INPUT -i eth0 -p tcp -m state --state NEW,ESTABLISHED -m tcp --dport 5350 -m comment --comment "livekit" -j ACCEPT
-A INPUT -i eth0 -p udp -m state --state NEW,ESTABLISHED -m udp --dport 50000:60000 -m comment --comment "livekit" -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m state --state ESTABLISHED -m tcp --sport 5350 -m comment --comment "livekit" -j ACCEPT
-A OUTPUT -o eth0 -p udp -m state --state ESTABLISHED -m udp --sport 50000:60000 -m comment --comment "livekit" -j ACCEPT
У вас, вполне вероятно, разрешён весь OUTPUT и/или есть отдельное правило для ESTABLISHED соединений в INPUT (чтобы не напрягать сервер разбором пакетов), поэтому логика может отличаться, но конкретно к этим портам должен быть доступ
5350
- порт из конфига livekit, на нём работает встроенный TURN-сервер
7881
- "запасной" ("fallback") порт, на случай, если udp не работает.
50000:60000
- диапазон UDP-портов, тоже для TURN-сервера. Можно поменьше, я думаю. По-моему, это будет определяться потенциальным числом клиентов, ибо каждый из них будет занимать порт на аудио-поток и видео-поток.
Порт 7880 проксируется через nginx.
И всё
После этого Element Call должен успешно завестись. С одним нюансом, что на мобильниках для него нужен Element X, а не "просто" Element". Win-, Mac- и веб-версии поддерживают оба типа звонков из одного клиента.
У меня он порядошно работает, но у меня не особо много клиентов, поэтому напрягов нет. Единственное, что с моего айфона видеопоток почему-то не принмается (но передаётся остальным участникам беседы) - так и не понял пока, в чём дело.