Вся боль p2p разработки

    Добрый день, хабрасообщество! Сегодня я хотел бы рассказать о волшебном и чудесном проекте компании Тензор — удаленном помощнике. Это система удаленного доступа, связывающая миллионы клиентов и операторов в рамках общей клиентской базы СБИС. Удаленный помощник уже сейчас тесно интегрирован с online.sbis.ru. Каждый день мы регистрируем более десяти тысяч подключений и десятки часов сессионного времени в сутки.В этой статье мы расскажем о том, как мы устанавливаем p2p соединения, и что делать, если этого сделать не удается.



    Опыт — сын ошибок трудных


    Систем удаленного доступа существует достаточно много. Это и всевозможные вариации бесплатных VNC, и достаточно мощные и предлагающие широкий набор функционала платные решения.Изначально наша компания использовала адаптацию одного из таких решений — UltraVNC. Это отличная бесплатная система, которая позволяет подключиться к другому ПК, зная его IP. Вариант того, как стоит поступать, если ПК имеет непрямой доступ к сети интерне, уже мелькал на просторах Хабра, и мы не будем затрагивать эту тему. Этого решения будет достаточно только до достижения сравнительно небольшого количества одновременных подключений. Шаг влево, шаг вправо, и начинается головняк с масштабированием, удобством использования, интеграцией в систему и сложностью доработок, которые, конечно, появляются в процессе жизненного цикла ПО, с чем мы и столкнулись.

    Итак, было принято решение изобрести свой велосипед создать свою систему управления удаленными рабочими столами, которую можно было бы интегрировать в общую экосистему СБИС. Конечно, самый простой способ связать 2 ПК, который не использует только ленивый — по числовому идентификатору. В нашей реализации мы используем рандомные 6-и знаковые номера без привязки к конкретному клиенту.

    Один очень известный человек однажды сказал:

    Теория — это когда все известно, но ничего не работает.
    Практика — это когда все работает, но никто не знает почему.
    Мы же объединяем теорию и практику: ничего не работает…
    и никто не знает почему!

    В самом начале нашего пути, эта цитата была очень похожа на правду: было понимание каким образом можно «познакомить» друг с другом клиента и оператора. Но на практике все оказалось не совсем тривиально.

    Введение в p2p


    Для связи 2х устройств мы используем сигнальный сервер — посредник, доступ к которому есть у обеих сторон. Его роль заключается в регистрации и возможности обмена информацией между участниками в режиме реального времени. Через него без лишних хлопот мы производим обмен endpoint’ами (связка IP-адрес и порт, точка доступа) с целью установки соединения.



    Этот сигнальный сервер, именуемый у нас remote helper manager(RHM) — пул написанных на nodejs систем, обеспечивающих отказоустойчивую работу всего сервиса. Нууу, точнее, как «отказоустойчивую» … мы на это надеемся :). Подключение к одному из серверов происходит по принципу round-robin. Таким образом клиент и оператор могут быть подключены к разным серверам, и вся механика по их синхронизации и координации полностью снята с десктопного приложения.

    Вся работа сводится к обмену служебными пакетами, при помощи которых стороны могут однозначно идентифицировать друг друга и выполнять какие-либо действия относительно синхронно, например, начать процедуру сбора кандидатов на подключение или же начало самой попытки подключения.

    Кстати, не поступайте как мы – не стреляйте себе в ногу: если используете 443 TCP порт — используйте TLS, а не чистый трафик. Все больше и больше брандмауэров его блокируют и разрывают соединение, причем, нередко на стороне провайдера.


    Самые распространенные в сети интернет протоколы обмена информацией — это UDP и TCP. UDP — быстр и легок, однако лишен нативной возможности гарантировать доставку пакетов и их очередность. TCP лишен этих недостатков, однако чуть более сложен в процессе установки p2p соединения. А с последними тенденциями, как мне кажется, прямое tcp соединение и вовсе может кануть в лету.

    Далеко не всегда установка p2p соединения зависит от умения работать с сетевыми протоколами. По большей части эта возможность зависит от конкретных сетевых настроек, чаще: типа NAT(Network address translation) и/или настроек файрвола.

    Принято разделять NAT на 4 типа, каждый из которых отличаются правилами трансляции пакетов из внешней сети конечному пользователю:

    • Symmetric NAT
    • Cone/Full Cone NAT
    • Address restricted cone NAT
    • Port restricted cone NAT

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

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

    Чтобы узнать свой IP-адрес и порт на внешнем устройстве (для простоты назовем его маршрутизатором), мы используем STUN (Session traversal utilities for NAT) и TURN (Traversal using relay NAT) сервера. STUN – для определения внешних IP: порт(endpoint) на UDP протокола, TURN – для TCP.

    Почему так, ведь гораздо проще было бы получить внешний IP с нашего же сигнального сервера?

    Здесь имеется как минимум 4 аргумента «за»:

    1. Возможность прозрачного расширения списка серверов (как своих, так и общедоступных) для сбора endpoint’ов, таким образом повысить отказоустойчивость системы.
    2. Взаимодополняемость и широкое распространение протоколов STUN и TURN позволяет уделять минимум внимания на сбор endpoint’ов и ретрансляцию трафика.
    3. STUN и TURN протоколы очень похожи. Разобравшись с архитектурой STUN пакетов, TURN идет уже по «накатанной». А использование TURN дают нам возможность ретрансляции трафика при провале попытки установить прямое подключение.
    4. У нас уже использовался STUN/TURN сервер «coturn» в проекте видеозвонков, а значит можно было «заюзать» их мощности с минимальными вливаниями в «железо».

    Coturn — это opensource реализация TURN и STUN сервера. Его использование, как показала практика, совсем не ограничивается WebRTC. На мой взгляд, это достаточно гибкий инструмент, не сильно требовательный. Да, у него нет встроенной возможности горизонтального масштабирования, но все решаемо, например, при помощи сигнального сервера.

    Как же строится общение с сервером по STUN/TURN протоколу


    Этапы получения endpoint’ов задокументированы в RFC #3489, #5389, #5766 и #6062.
    Все сообщения к STUN или TURN протоколу имеют следующий вид:



    Соответственно:

    1. 12 байта на тип сообщения
    2. 22 байта на его длину (размер всех последующих атрибутов)
    3. 12 байт — для рандомного идентификатора для TURN и 16 байт — для STUN пакетов. Их размер отличается на 4 байта — эти данные зарезервированы для TURN пакета под константный MagicCookie.

    В целом служебная информация заключена в первых 20 байтах пакета.
    Атрибуты также состоят из:

    1. 2 байта на тип атрибута
    2. 2 байта на его длину
    3. самого значения атрибута

    Важно, что общая длина атрибута должна быть кратна 4 байтам. Если, скажем, значение длины атрибута, например 7, то в конце необходимо доукомплектовать: (2 + 2 + 7) % 4 байтами пустых данных.

    Как выглядит сбор endpoint’а для UDP протокола:

    1. Коннект к серверу
    2. Отправка пакета, содержащего binding request:
    3. Получение пакета, содержащего binding response:
    4. Парсинг пакета и извлечение mapped-address:
      0x00 0x01 — Тип атрибута, соответствующий MAPPED-ADDRESS
      0x00 0x08 — Совокупная длина атрибута
      0x00 0x01 — Версия протокола, соответствующая IPv4
      0x30 0x39 – Порт, со значением 12345

    Далее каждый байт соответствует своему октету ipv4 адреса: 123.123.123.123

    Сбор endpoint’а для TCP несколько отличается, т.к. получаем мы его по TURN протоколу. Почему именно так? Все объясняется минимизацией количества сокетов, подключенных к TURN-серверу, а значит, потенциально большее количество людей смогут «висеть» на одном сервере ретрансляции трафика.

    Для сбора кандидата по TURN протоколу необходимо:

    1. Подключиться к серверу.
    2. Отправить пакет, содержащий allocation request.
    3. При необходимости авторизации на TURN сервере в ответ мы получим allocate failure с 401 ошибкой. В таком случае необходимо будет повторить allocation request с указанием имени пользователя и атрибута Message Integrity, генерируемого на основании самого сообщения, имени пользователя, пароля и атрибута realm, взятого из полученного от сервера ответа.
    4. Далее сервер в случае успешной регистрации присылает allocate success response с атрибутом выделенного порта на TURN-сервере, а также XOR-MAPPED-ADDRESS – тем самым публичным endpoint’ом на TCP протоколе. Для дальнейшей работы с IP каждый октет надо «заксорить» (XOR — операция логического исключения ИЛИ) аналогичным байтом из константного атрибута MagicCookie: 0x21 0x12 0xA4 0x42
    5. В случае дальнейшей работой с этим TURN соединением необходимо каждый раз продлять регистрацию, отправляя refresh request. Сделано это для отбрасывания «мертвых» коннектов.

    Итак, мы имеем сервер, через который мы обменялись с удаленной стороной собранными endpoint’ами.

    Конечно, это сейчас кажется простым и понятным, но оглядываясь назад, когда смотришь в RFC и понимаешь, что без подсказок wireshark’а дальше дело не сдвинется с мертвой точки — готовишься к погружению в… В общем, вспоминается один бородатый анекдот:

    Учись пацан, а то так и будешь ключи подавать…


    Как установить соединение?


    Самое простое – это организация UDP hole punch’а.
    Для этого необходимо искусственно создать правила маршрутизации на своем NAT.



    Достаточно просто организовать серию передачи пакетов на удаленный endpoint и дождаться от него ответ. Несколько пакетов необходимы для создания соответствующего правила на NAT’е и избавления от «гонки», кто кому первым доставит соответствующий пакет. Ну и потерю на UDP никто не отменял.

    Далее обменялись контрольными фразами и можно считать, что соединение установлено.

    Чуть-чуть сложнее – организация TCP hole punch, хотя общая идеология остается точно такой же.

    Сложность заключается в том, что только 1 сокет по умолчанию может занимать свой локальный endpoint, а попытка подключения к другому адресу приведет к автоматическому разрыву соединения с первым. Однако существуют опции сокета, это ограничение снимающие: REUSE_ADDRESS и EXCLUSIVEADDRUSE. После взведения первой и сбрасывания второй опции на сокете другие сокеты смогут занимать тот же самый локальный endpoint.

    Ну и остается сущий пустяк – забиндиться на локальный endpoint, открытый сокетом при коннекте с TURN’ом, ну и попытаться подключиться к endpoint’у удаленной стороны.

    Ну и еще чуть сложнее, но не менее важная для стабильной установки соединения – ретрансляция трафика.

    1. Т.к. регистрация на TURN’е у нас уже имеется, все, что нам необходимо – это добавить в разрешения на TURN’е регистрацию удаленной стороны. Для этого отправляем пакет CreatePermission с указанием удаленной регистрации.
    2. Инициатор соединения отправляет пакет ConnectRequest с указанием «заксоренного» endpoint’а удаленной регистрации и подписывает пакет MessageIntegrity.
    3. Если все хорошо и удаленная сторона отправляла CreatePermission с вашей регистрацией, то инициатору придет connect success response, а клиенту – connection attempt. И в том, и в другом случае во входящем пакете будет присутствовать атрибут connection-id.
    4. Далее за непродолжительный промежуток времени необходимо новым сокетом подключиться к тому же IP и порту TURN сервера, что и первоначальный сокет (в классическом исполнении TURN сервера могут слушать как 3478, так и 443 tcp порты) и отправить пакет ConnectionBind с нового сокета с указанием connection-id, полученного ранее.
    5. Дождаться пакета, содержащего connection bind success response, и вуаля – соединение установлено. При этом да, используется 2 сокета — управляющий, который отвечает за поддержание соединения, и транспортный, с которым можно работать как при прямом соединении – все, что будет отправлено или получено, должно обрабатываться как есть.

    По приоритету использования у нас выстроилась такая иерархия: прямое tcp > прямое udp > релей (ретрансляция)

    Почему мы унесли прямое udp на второе место?


    Что ж, UDP при всей своей легкости и скорости обладает существенным недостатком: отсутствием гарантии доставки и очередности. И если с видеопотоком еще как-то с этим можно было бы смириться (наличие графических артефактов), то вот с передачей файлов тут несколько серьезней.

    Для обеспечения гарантии и очередности был реализован механизм, схожий с reliable UDP, который да, потребляет несколько больше ресурсов, но и дает желаемое.
    Как же мы вышли из ситуации? Для начала необходимо узнать MTU (maximum transmission unit) – то есть максимально большой размер udp пакета, который может быть отправлен без фрагментирования на проходящих узлах.

    Для этого принимаем за максимальный размер пакета 512 байт и выставляем сокету опцию IP_DONTFRAGMENT. Отправляем пакет и ждем его подтверждения. Если в течение фиксированного времени мы получили ответ, то увеличиваем максимальный размер и повторяем итерацию. Если же в конечном итоге подтверждения мы не дождались, то начинаем процедуру уточнения размера MTU: начинаем не существенно понижать максимальный размер блока и ожидаем стабильного подтверждения в течение 10 раз. Не получили подтверждение – снизили MTU и по новой запускаем цикл.
    Оптимальный размер MTU найден.

    Далее проводим сегментирование: нарезаем весь большой блок на множество маленьких с указанием начального номера сегмента и конечного номера сегмента, характеризующего пакет. После разбиения добавляем сегменты в очередь отправки. Отправка сегмента производится до тех пор, пока удаленная сторона не сообщит нам о том, что получила его. Интервал повторной отправки используем как 1.2*максимальный размер ping’а, полученного при нахождении MTU.
    На принимающей стороне смотрим полученный сегмент, добавляем во входящую очередь и пробуем собрать ближайший пакет. Если получилось – чистим очередь и пробуем собрать следующий.

    Тут, конечно, самые внимательные из вас, кто «дожил» до этого абзаца, могут смело заметить: а почему не использовать кодек x264 или x265? — и будут частично правы. Честно говоря, мы тоже склонны его заюзать, тогда можно поступиться этим велосипедом на udp. Но как быть, скажем, с передачей бинарных файлов? В этом случае мы опять возвращаемся к необходимости гарантии доставки и очередности пакетов.

    В заключение хочется отметить, что с такой организацией подключений мы имеем не более 2-3% несостоявшихся подключений в день, большую часть из которых составляют неверные настройки прокси или файрвола, настроив которые соединение осуществляется без проблем.

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

    Автор: Владислав Яковлев asmsa
    Тензор
    37,00
    Разработчик системы СБИС
    Поделиться публикацией

    Похожие публикации

    Комментарии 22

      +1
      Интересно, что мешает использовать DHT, и ту же систему, что применяется в tox?
        +1
        DHT не отменяет необходимость в STUN, TURN и hole punching
          0
          Если я не ошибаюсь, сеть DHT подразумевает наличие постоянно запущенного приложения. В наших реалиях — средство управление запускается по мере необходимости оказать или получить помощь.
          Плюс торрентоподобные сети подразумевают возможность ретрансляции трафика через других участников сетей. Уверен клиентам это понравилось бы.
          Ну и да, как отметили чуть выше, STUN или аналогичный способ определения своего внешнего IP все же необходим.
            0
            1. сеть tox и opendht существуют независимо, при этом ежеминутно активны десятки тысяч хостов.
            2. DHT не требует передачи трафика через клиентов, а является только таблицей адресов, точнее хешей. Своего рода адресная книга
            3. github.com/TokTok/c-toxcore/tree/master/toxcore здесь живая реализация. И да, пробивка NAT через STUN/TURN здесь может и работает, но явно без задействования внешних, дополнительных серверов.
            4. в вашей реализации требуется отдельная сущность «remote helper manager(RHM)». Вопрос в том, зачем плодить сущностей когда все придумано до нас?
              0
              В случае если в любой момент есть хотя бы одно запущенное приложение DHT сети — согласен, проблем нет. Что если запуск происходит в момент недоступности последних известных узлов?
              «Без задействования дополнительных серверов» — да, когда есть внешний хост DHT сети — вопрос о своем внешнем IP решается сам собой, бесспорно. Но если такой возможности нет, что делать, с чего начать? На это и была нацелена статья.
              Наша структура построена на централизованном типе p2p. Ей и проще управлять и собирать статистику соединений, а так же интегрировать в панель ожидающих подключения клиентов.
                0
                1. повторюсь, единовременно в сети существуют десятки тысяч нод DHT/OpenDHT… Чтобы не было ни одной надо полностью закрыть весь доступ во внешнуюю сеть.
                2. > Наша структура построена на централизованном типе p2p.
                в таком случае здесь и речи быть не может о p2p. В вашем случае это обычная клиент-серверная структура. Где есть сервер и есть клиенты.
                  0
                  1. средство управление запускается по мере необходимости оказать или получить помощь — оно не висит в памяти без необходимости. Таким образом, скажем в часы минимальной нагрузки(скажем ночью или ближе к утру по Москве) — да, есть такая вероятность, что ни одного приложения не будет запущено вовсе. А вероятность доступности последнего известного узла — еще меньше.
                  2. В рамках коннекта к rhm — да, согласен. Но дальнейшее подключение между оператором и клиентом идет непосредственно по p2p
                    0
                    Кажется, понял. Предлагается завязаться не на свою DHT, а на некую общую. Да, как вариант. Но есть 2 момента:
                    1. Придется уделить огромное внимание безопасности
                    2. Как быть с клиентами у которых для бух.компов выход в сеть только по белым спискам? Их в топку?
                      0
                      1. Какой? Там только хеши.
                      2. А кто даст гарантию, что для rhm администраторы будут создавать отдельные белые списки?
                      Опять же, если rhm не единичный, и/или будет масштабироваться, то кто будет обновлять постоянно белые списки?

                      >2. В рамках коннекта к rhm — да, согласен. Но дальнейшее подключение между оператором и клиентом идет непосредственно по p2p
                      Здесь противоречие с белыми списками. Если знакомы с VoIP SIP, то вспомните к чему приводят подобные вещи без проксирования.
                        0
                        1. Любая открытая сеть — потенциально опасна
                        2. Гарантий нет, но есть возможность, а это уже +, чего не скажешь о dht
                        3. Абсолютно нет противоречия. Перелистайте статью — в случае, когда прямое соединение установить не удается, мы идем через релей, т.е сервер ретрансляции трафика.

                        Но Ваша позиция мне понятна, спасибо!
            +1

            А бенчмарки будут? Фпс хотя бы и лэтэнси по обычному интернет каналу и по локалке?

              –1
              Бенчмарки чего? Самой системы управления? Или установки соединения? Если вопрос касается только установки соединения — не более пары секунд, в зависимости от канала.
              Если говорить о качестве самой работы, то очень многое зависит от клиентского аппаратного обеспечения. Но в среднем достаточно комфортно.
              0
              Кстати, про reliable UDP — эдак вы еще и до управления congestion window дойдёте и получится почти полноценный TCP :-)
                0
                К сожалению, до полноценного TCP дойти точно не удастся, т.к. вся нагрузка по гарантии доставки и очередности сборки пакетов лежит все-таки на приложении, а не драйвере.
                0
                Работаю с online.sbis && reg — было интересно почитать, что там под капотом, спасибо.
                Среди наболевшего:
                — API развивать будете в сторону работы с ЭЦП/сертификатами?
                — планируете как-нибудь ограничить аппетиты sbis plugin в плане оперативки? В диспетчере постоянно наблюдаю значение в 700-800 Мб, пару раз за гигабайт переваливало. Причем периодически не может переварить и давится, помогает только «тремя пальцами», что грустно.
                  0
                  А нам в радость рассказать об этом. В ближайшее время удаленный помощник портируется на linux и os x системы. Дальнейшее развитие проекта еще на стадии обсуждения.
                  Про СБИС Плагин — это, конечно, не по теме статьи, однако могу лишь намекнуть, что разрабатывается его новая реализация. Надеюсь, вы оцените! ;)
                    0
                    В дополнение к вопросу выше. Будет ли работать полноценно sbis в Linux/Mac?
                  0

                  А есть статистика по процентам tcp/udp/relay?

                    +1
                    Добрый вечер! В среднем на релей уходит около 37%, прямое udp 58% ну и оставшиеся 5% — прямые tcp по данным за январь месяц.
                    0
                    Как понял тут описан транспортный уровень, а интересна именно реализация захвата видео и управления, что для этого используется?
                      0

                      Спасибо за интерес! этот вопрос заслуживает отдельной статьи, парой фраз тут не рассказать :)

                      0
                      Недавно связался с Тензором, дабы получить электронную подпись и после всего установленного шлака для этих целей, я обнаружил, что ваш софт установил в доверенные корневые центры сертификации самоподписной сертификат на адрес 127.0.0.1. Право даже не знаю, в голову одни маты лезут. Это тоже от боли при p2p разработки?

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое