Что: 4a521b9d638a8d23487ff6c36ac3be97c8c464b3 Когда: 2024-04-10 00:47:05+03:00 ------------------------------------------------------------------------ Темы: crypto go multimedia ------------------------------------------------------------------------ Моё новое поделие: VoRS http://www.vors.stargrave.org/ Как-то я бросался словами что надо написать свой собственный VoIP клиент (+сервер). А то из всего VoIP есть только Mumble, который хоть как-то ещё можно собрать и который хоть как-то но работает. Хоть как-то -- это значит что всё равно со сторонними реализациями всё плохо (Mumble на Go): 7dac01b0761a750312eef3765d3131e36fac95aa 6bf8ec6fda4ba9a2ee54819e4a6613ff33d8effe ecf0bbd8f4f25d6039438e1c6756c518e6979cfb Отдельная боль в Mumble -- его Murmur сервер, который хоть и не GUI, но требует Qt. За три последних дня, не забывая про работу, я осилил написать своё решение. Когда-то я думал что это вообще на Си стоило бы, но увидел, что Go-шная wrapper библиотека для libopus существует, в ней почти нет кода (буквально просто обёртки), замечательно работает. В итоге Си как то сам собою отпал. Около экрана кода достаточно чтобы P2P Opus закодированный трафик по UDP передавать. Я было обрадовался как оказалось всё просто. Но вот если захочется больше чем два человека, то тогда что делать? Всем перезапускаться и указывать ещё один дополнительный IP адрес? Геморрой. Да и я не планировал и яростно против любых NAT-traversal технологий, но отдельный сервер всё же вполне себе был бы и решением до сих пор остающихся неподключёнными к Интернету людей (которые за NAT). И с его появлением всё сразу как-то сразу усложнилось. Но я постарался сделать так, чтобы всё же проще было некуда. VoRS: Vo(IP) Really Simple. * каждый клиент подключается по TCP к серверу * на нём заранее генерируется X.509 сертификат самоподписанный, ed25519 алгоритм, хэш от SPKI выдаётся клиентам * инициируется TLS 1.3 с curve25519 DH. Проверяется SPKI хэш сервера * далее внутри TLS следует текстовый построчный протокол: сервер отсылает 128-бит challenge; мы отвечаем BLAKE2s(пароль, challenge) и username-ом. Серверу и клиентам заранее указываются пароли подключения. Факт успешного расчёта MAC-а над challenge означает что клиент аутентифицирован и авторизован к подключению * если с паролем всё ok, если username не сдублирован, то сервер или отвечает "OK <SID>" или сообщением с ошибкой. SID это stream identifier -- 8-бит число, по сути просто идентификатор подключённого клиента * далее по этому TLS-у раз в 10сек бегают PING/PONG, с отключением если долго от противоположной стороны ничего не было X.509 -- потому что из коробки в Go есть. X.509+пароль -- точно так же это устроено и в Mumble. То бишь для Mumble-пользователей привычно. После успешной авторизации, сервер и клиент вырабатывают симметричный ключ шифрования UDP трафика, используя встроенную возможность TLS 1.3 в виде Export Keying Material. Аудио читается кусками по 20мс -- рекомендованное значение Opus-а. 48kHz, 1 канал, 16-бит S-LE. Натравливается функция кодирования Opus, получается несколько десятков байт пакет. К нему добавляется 32-бит заголовок: 8-бит SID и 24-бита счётчик пакетов. 24-бита достаточно для многодневной беседы без остановки, учитывая что отсылается 50pps. Счётчик используется для обнаружения переупорядочивания и потерь пакетов. Используются PLC (Packet Loss Concealment) возможности libopus для сглаживания потерь. Bitrate выставлен в 32Kbps. Изначально выставлял 24Kbps, как рекомендовано. Но... 4-байт заголовок VoRS, 40-байт заголовок IPv6, 16-байт на MAC, 8-байт на UDP... выходит что размер payload-а меньше чем overhead на передачу! Поэтому пускай будет 32Kbps, чтобы всё же чуть больше чем overhead быть, и суммарно получить 64Kbps трафика. Раз в секунду отправляется пакет с 1-им байтом SID-а, чисто для UDP hole punching-а stateful firewall-а. Собственно, ключ EKM используется для ChaCha20-Poly1305. В качестве nonce которого используется счётчик пакетов. Пока используются все 128-бит Poly1305, но я думаю что имеет смысл сократить в два раза. UDP трафик от клиента отправляется на сервер, который только смотрит на первый байт SID-а и UDP IP:порт. Клиент шлёт UDP трафик с такого же номера порта по которому он подключился для TCP. А дальше сервер просто буквально рассылает копии пакета всем остальным. Да -- это пока самое неприятное место, ибо микшированием аудиопотоков сервер не занимается и поэтому объём трафика растёт пропорционально кол-ву участников. Но на моей практике, людей в Mumble буквально не больше 4-5, и это речь про 64Kbps поток от каждого. Как же дешифровать то трафик могут другие, ведь у них же свои TLS соединения со своим state-ом. Сервер по TLS-у просто сообщает текстовой строкой о факте подключения нового участника: ADD SID USERNAME KEY. Если кто отключается, то: DEL SID. Безусловно пока есть race между тем как дойдёт ADD/DEL по TCP до клиентов и параллельно с этим идущим UDP трафик. Но да и фиг с ним: речь про доли секунды возможно ещё не дешифрующегося трафика. Когда сервер научится микшировать аудиопотоки, то от него будет идти ровно один stream с audio. Можно будет избавиться от SID-а в принципе. Если дойдут до этого руки, ибо задача вроде бы отнюдь не тривиальна. Сервер сейчас даже не работает с криптографией UDP пакетов, хотя мог бы проверять MAC например (ключ же он знает). Самая жопа это ввод и вывод аудио. Ничего портабельного, кроме говна типа PulseAudio или очередных его заменителей -- нет. OSS4 это мир BSD. ALSA это Linux. JACK из коробки не стоит, да и я не знаю адекватно ли с ним работать. Видел и даже трогал софт с OpenAL -- но на Go как-то оно всё не то чтобы стабильно работало (может быть это софт был говно, а не с OpenAL дело). Пока решил поступить по тупому: SoX-овый rec для того, чтобы из него просто забирать поток PCM байт. Его же play для воспроизведения. Приятно то, что он не требует чтобы я поток постоянно выдавал. Если мне не приходят UDP/Opus пакеты, то в play я байты никакие не подаю и это не проблема. Для каждого клиента/stream-а я запускаю ещё один "play". Насколько знаю, возможности микшировать потоки с разных приложений зависят от драйверов и вообще звуковой подсистемы. Но вроде бы и ALSA и OSS давно без проблем это всё из коробки умеют уже давно. У меня play прекрасно запускается в большом количестве и асинхронно к ним подаются кусочки звуковых данных -- всё тип-топ работает. Вместо play/rec можно использовать всё что угодно другое. Это просто команда которая или с stdout или на stdin должна принимать PCM данные. Хоть ffmpeg засунуть -- должно быть пофиг. И по идее это нигде не должно создавать проблем. Проверял на какой-то Ubuntu не самой свежей (с LiveCD), ну и на своих FreeBSD, как со встроенными Intel HDA звуковухами, так и подключёнными через USB или даже virtual_oss. Если я доберусь до микширования звука на сервере, то этот же код можно будет использовать и на клиенте, для того чтобы ровно один "play"/whatever запускать, если будет в этом смысл. Ну и оно должно быть удобно для использования. Я люблю бегающие чиселки. Mumble настолько ничего не показывает, что частенько он говорит что типа всё ok, мы работаем, вот только звуковое устройство отвалилось, как и сервер. Решил сразу сбацать TUI интерфейс. Экран для произвольных логов (ошибки, события подключения), и для каждого участника по окошечку, где перечисляются кол-во принятых/переданных пакетов (на сервере, а на клиенте только принятых от остальных, или переданных от себя), размеры в байтах (только payload конечно же), потерянные или переупорядоченные пакеты, Если последний UDP пакет был принят более секунды назад -- считается что пользователь молчит. В противном случае показывается "TALK" зелёным цветом. Как на сервере, так и на клиентах. Нажатием Enter можно включать/выключать mute локальный -- UDP просто не будет отсылаться. Не знаю зачем, но всегда хотелось иметь прыгающую полосочку громкости когда человек говорит. Вычисляю RMS 200мс отрезков и рисую bar. С такой же периодичностью обновляю экран. В планах не было делать Voice Activity Detection, но раз я уже умею вычислять RMS, то что мешает добавить VAD? Я не очень понял про его значения, не вдавался в подробности, но просто написал утилитку, которая выводит RMS значение для звука из rec-а. На глаз можно оценить какой порог надо задать и передать его в клиента -- тихий звук до него будет считаться тишиной и никакой UDP передаваться не будет. Вроде работает отлично. В "бою" я это ещё не проверял -- только в домашних условиях. Но с задержками проблем не увидел, собралось под старой Ubuntu без проблем, работает прям отлично как-будто. Запросто где-то фатальные косяки, ибо делал на скорую руку. Но по идее оно полностью покрывает функционал Mumble для просто общения (вне игр, где всякие positional audio). Нет разделения по "комнатам". Конечно можно и добавить, но думаю что проще поднять ещё один сервер на соседнем порту. У него из аргументов то пути только bind, путь-к-pem и пароль. Плюс он не требует libopus или чего-то подобного: обычная статически собранная Go программа. ------------------------------------------------------------------------ оставить комментарий: mailto:comment@blog.stargrave.org?subject=Re:%20%D0%9C%D0%BE%D1%91%20%D0%BD%D0%BE%D0%B2%D0%BE%D0%B5%20%D0%BF%D0%BE%D0%B4%D0%B5%D0%BB%D0%B8%D0%B5:%20VoRS%20%284a521b9d638a8d23487ff6c36ac3be97c8c464b3%29 ------------------------------------------------------------------------ Сгенерирован: SGBlog 0.34.0