•
6 min read
call.gluek.info

Видеозвонки в браузере на Bedrud: комната по ссылке, гости без регистрации. Сайт обещает ещё клиенты для Android, iOS и десктопа, но на деле их почти нет: страница в Google Play не открывается, Homebrew-тапа не существует, а в релизах на GitHub есть только десктопный клиент, причём для Mac это .dmg с голым неподписанным исполняемым файлом вместо приложения. Исходники Android- и iOS-клиентов в репозитории лежат, но готовых сборок нет. Главная особенность моей установки в том, что Bedrud не поднимает свой медиасервер, а использует LiveKit на rtc.gluek.info, который уже стоял для звонков в Matrix.

Зачем, если есть Jitsi

На meet.gluek.info у меня с июня работает Jitsi Meet, и он никуда не делся. Но это четыре контейнера (веб, XMPP-сервер Prosody, Jicofo и видеомост JVB на Java), которые в простое занимают около 460 МБ памяти, а образы весят больше 3 ГБ. На сервере, где уже работают несколько десятков контейнеров и память на счету, это заметно.

Когда для Element Call пришлось поднять LiveKit, оказалось, что на сервере теперь есть второй, куда более лёгкий медиасервер. LiveKit — это SFU на Go, тот же класс, что и JVB, только без XMPP вокруг. Оставалось найти к нему веб-интерфейс «комната по ссылке», и Bedrud подошёл: он и так построен на LiveKit и умеет работать с внешним сервером.

Сравнение по ресурсам и настройке — в отдельном посте Bedrud vs Jitsi.

Как это устроено

браузер ──https──▶ Caddy ──▶ bedrud:8090      (интерфейс, API, комнаты, пользователи)
   │
   └──wss/udp──▶ rtc.gluek.info ──▶ livekit:7880   (медиа: UDP 7882 / TCP 7881)
                                    ▲    │
             создание комнат, токены ┘    └─ вебхуки ──▶ bedrud и lk-jwt (Matrix)
  • Bedrud отвечает за всё, кроме медиа: пользователи, комнаты, чат, права. Браузеру он выдаёт токен LiveKit и адрес wss://rtc.gluek.info, а дальше звук и видео идут напрямую в LiveKit
  • LiveKit один на всех: его же использует lk-jwt-service для звонков в Element X. Новых портов открывать не пришлось, всё медиа идёт через тот же UDP 7882
  • В простое Bedrud занимает около 10 МБ памяти, после звонков около 50 МБ. LiveKit — 40–80 МБ, и эту память я уже тратил ради Matrix

Установка

Bedrud ставится одним бинарником или скриптом curl | bash, но у меня всё в Docker за общим Caddy, поэтому я собрал установку руками.

Образ

В документации указан образ ghcr.io/themadorg/bedrud, но на момент установки скачать его без авторизации нельзя (unauthorized). Docker-образы лежат в релизах на GitHub архивами, их нужно загрузить самому:

B=https://github.com/themadorg/bedrud/releases/download/v0.13.0
curl -fsSLO $B/SHA256SUMS
curl -fsSLO $B/bedrud-docker-alpine-amd64.tar.gz
grep alpine-amd64 SHA256SUMS && sha256sum bedrud-docker-alpine-amd64.tar.gz
docker load -i bedrud-docker-alpine-amd64.tar.gz   # ghcr.io/themadorg/bedrud:0.13.0-alpine

Обновление — то же самое с новой версией и сменой тега в compose.

Конфиг

Главное — livekit.external: true: тогда Bedrud не запускает встроенный LiveKit и не проксирует его через себя.

server:
  port: "8090"
  domain: "call.gluek.info"
  behindProxy: true
  trustedProxies: ["172.18.0.0/16"]   # Docker-сеть, где живёт Caddy
database:
  type: "sqlite"
  path: "/var/lib/bedrud/bedrud.db"
livekit:
  external: true
  host: "wss://rtc.gluek.info"        # куда подключаются браузеры
  internalHost: "http://livekit:7880" # API LiveKit внутри Docker-сети
  apiKey: "lk-gluek"                  # тот же ключ, что у lk-jwt (см. ниже)
  apiSecret: "…"
auth:
  jwtSecret: "…"                      # openssl rand -hex 32
  sessionSecret: "…"
  frontendURL: "https://call.gluek.info"
  tokenDuration: 24                   # обязательно, см. подводные камни
cors:
  allowedOrigins: "https://call.gluek.info"

Compose — один сервис в общей сети с Caddy и LiveKit, без опубликованных портов:

services:
  bedrud:
    image: ghcr.io/themadorg/bedrud:0.13.0-alpine
    container_name: bedrud
    command: ["run", "--config", "/etc/bedrud/config.yaml"]
    mem_limit: 256m
    restart: unless-stopped
    volumes:
      - ./config.yaml:/etc/bedrud/config.yaml:ro
      - ./data:/var/lib/bedrud
    networks: [caddy]
networks:
  caddy:
    external: true   # общая сеть с Caddy и LiveKit

LiveKit: общий ключ и второй вебхук

Bedrud узнаёт от LiveKit, что участник вышел или комната закрылась, через вебхуки. В конфиге LiveKit достаточно добавить второй адрес:

webhook:
  api_key: lk-gluek
  urls:
    - http://lk-jwt:8080/sfu_webhook            # Matrix
    - http://bedrud:8090/api/livekit/webhook    # Bedrud

Тонкость в том, что у LiveKit один webhook.api_key на все адреса: каждый вебхук подписывается этим ключом. А Bedrud проверяет подпись своим apiKey. Поэтому завести Bedrud отдельную пару ключей не получится: ему нужен тот же ключ, что и lk-jwt. На своём сервере это не страшно, но Bedrud при этом может управлять и комнатами Element Call.

Комнату в LiveKit Bedrud создаёт сам через API, поэтому room.auto_create: false, который стоит в LiveKit ради Element Call (чтобы комнаты на моём SFU не могли создавать пользователи Matrix с других серверов), ему не мешает.

Caddy

call.gluek.info {
	reverse_proxy bedrud:8090
}

После этого нужно зарегистрироваться (первый аккаунт я сделал через passkey) и выдать себе права админа:

docker exec bedrud bedrud --config /etc/bedrud/config.yaml user promote --email you@example.com

Подводные камни

tokenDuration без значения по умолчанию

Если не указать auth.tokenDuration, он равен нулю, и каждый токен входа истекает в момент выдачи (exp == iat). Вход проходит, но на любое действие интерфейс отвечает «Invalid token», через несколько секунд выкидывает на страницу входа, а при закрытой регистрации ещё и пишет, что регистрация выключена. Подпись токена при этом верная, так что искать проблему в секретах или прокси бесполезно. В примере конфига в документации tokenDuration: 24 есть, но выглядит как необязательный. Написал об этом issue #210.

Вебхуки, которые «не проходят подпись»

В логе Bedrud на каждый вход в комнату и публикацию трека пишется LiveKit webhook: invalid signature. На самом деле подпись в порядке: Bedrud разбирает JSON от LiveKit обычным encoding/json, а тот не понимает enum-поля, которые LiveKit присылает строками. В результате событие о выходе участника теряется, и он числится в комнате, пока её не закроют. От внешнего LiveKit это не зависит, встроенный присылает то же самое. Issue #209.

Ссылка на комнату живёт один звонок

Когда из комнаты выходит последний участник, LiveKit примерно через 20 секунд удаляет её у себя и присылает событие room_finished, а Bedrud по нему помечает комнату неактивной. Гость, открывший ту же ссылку позже, увидит «room is no longer active». Хозяин комнаты при входе получит предложение пересоздать её с тем же адресом. Но пересозданная комната становится приватной, и гостей в неё опять не пустит, пока в настройках не включить Public.

Галочка «постоянная комната» от этого не спасает: планировщик Bedrud постоянные комнаты не трогает, а вот обработчик room_finished выключает их так же, как и остальные. Описал это в issue #212.

Поначалу я решил, что дело в auto_create: false у LiveKit, и даже включил его. Но оказалось, что зависание на «connecting» я вызвал сам во время нагрузочного теста, когда вручную включил комнату в базе, а в LiveKit её уже не было. В обычной работе Bedrud создаёт комнату в LiveKit сам, так что auto_create можно оставить выключенным.

Единственный реальный случай, когда это мешает, — перезапуск LiveKit (например, при обновлении) посреди звонка. LiveKit держит комнаты в памяти и забывает их, не отправив room_finished. Участники не могут переподключиться, пока планировщик Bedrud примерно через минуту не погасит комнату, после чего её придётся пересоздать. Так что LiveKit лучше обновлять, когда никто не звонит.

«TLS off» в админке

На странице Overview горит красный бейдж «TLS off». Он показывает, терминирует ли HTTPS сам процесс Bedrud, а у меня этим занимается Caddy. Можно игнорировать.

Сертификат и Let’s Encrypt

Блок в Caddy я добавил раньше, чем DNS-запись, а в ту же ночь у Let’s Encrypt ещё и был инцидент с валидацией. Сертификат в итоге выдал ZeroSSL: Caddy переключается на него сам, если в глобальных опциях указан email. Без него Caddy ходит только в Let’s Encrypt (а при ошибках — в его staging). Вывод на будущее: сначала DNS, потом Caddy, и email в глобальном блоке лучше держать всегда.

Настройки

Регистрация открыта для всех, но с ограничениями. Гости без аккаунта могут только заходить в комнаты по ссылке, создавать комнаты им нельзя: это зашито в коде, настройки нет. Почту Bedrud не подтверждает, потому что SMTP пока не настроен.

Лимиты задаются через CLI и хранятся в базе, а не в config.yaml:

B="docker exec bedrud bedrud --config /etc/bedrud/config.yaml"
$B settings set maxRoomsPerUser 3              # активных комнат на пользователя (админа не касается)
$B settings set maxParticipantsLimit 20        # участников в комнате
$B settings set maxUploadBytesPerUser 52428800 # 50 МБ файлов в чате на пользователя
$B settings set globalDiskThresholdBytes 2147483648  # 2 ГБ на все загрузки

Последний лимит нужен потому, что файлы из чата удаляются только вместе с комнатой, когда её завершают или удаляют. Если все просто вышли, комната становится неактивной, но файлы остаются навсегда. Настройка chatMessageTTLHours в админке есть, но в версии 0.13.0 ни на что не влияет. Предложил это доделать в issue #211.

При создании комнаты можно включить сквозное шифрование (E2E) и комнату ожидания (Gate), где гостей впускает хозяин.

Итог

Bedrud — молодой проект (версия 0.13, на момент установки 9 звёзд на GitHub), так что Jitsi я не выключаю. Но как второй вариант для звонков он почти ничего не стоит: при уже работающем LiveKit это один контейнер на 50 МБ и пара строк в конфигах.