Настройка Element Call

Настройка Element Call

Короче.

Есть инструкция: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md

Я думал, что можно, читая сколько-то по диагонали, поставить это всё. Увы. Оказалось, сервис сделан айтишниками для айтишников, а я, похоже, ненастоящий. Поэтому с помощью ChatGPT и такой-то матери в течение нескольких дней (прям рабочих дней; на что я трачу свои выходные?) получилось поставить.

Что меня больше всего расстраивает - это отсутствие объяснения взаимосвязей. Ребята написали опенсорс тут, заюзали опенсорс там, а единственное место, где всё преднастроено "из коробки" - их Docker-образ. Однако, Docker пока не по мою душу - я решил настроить каждый элемент ))) отдельно. К слову, там есть какие-то графики этих взаимосвязей, но оно то ли поверхностно, то ли абстрактно, то ли для кого-то ещё.

Возможно, я просто тупой

Что за гайд

Гайд очень жёстко граничит с "уплыло всё нахуй в море", ибо оПеНсОрС - для любого такого продукта ставятся сначала зависимости, потом зависимости зависимостей, потом зависимости зависимостей зависимостей, а спустя трое суток ты просыпаешься в холодном поту, поняв, что сам продукт ты поставить забыл.

Я постараюсь сводить длинные секции балабольства воедино к концу каждого раздела, ибо я сам читал те доки посредственно, но схема с "эксперимент → пипец → чтение инструкции" здесь не сработала, потому что вместо экспериментов пришлось искать, в каком шкафу лежат очередные зависимости.

Записано всё на горячую тёплую руку по истории команд терминала и поисковых запросов, поэтому, возможно, я что-то забыл. Но весь процесс достаточно поджёг мою задницу, чтобы я вообще всё это написал. Айтишники опять придумали хуйню.

Благодарности и ссылки сразу

  1. Вот этому господину. По его статье можно было хоть примерно сориентироваться, что происходит, и как оно работает (но я всё равно понял не сразу) (и до сих пор не всё).
  2. И вот этому блогу (внезапно, ещё один блог .de) за примеры.
  3. И разрабам, что ну хоть как-то добрались и написали доку. А то четыре месяца назад там вообще ничего не было - в 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- и веб-версии поддерживают оба типа звонков из одного клиента.

У меня он порядошно работает, но у меня не особо много клиентов, поэтому напрягов нет. Единственное, что с моего айфона видеопоток почему-то не принмается (но передаётся остальным участникам беседы) - так и не понял пока, в чём дело.