Приветствую всех!

В этом посте я расскажу о назначении и устройстве небольшой, но полезной утилиты plexus, работа над исходной версией которой недавно завершилась. Утилита написана на C++, но этот материал будет интересен не только и не столько пишущим на плюсах, но и тем кто имеет отношение к сетевому программированию.

Небольшая ремарка

Все мы видим как радикально изменила мир цифровая революция. Весь мир, без преувеличения, переселился в Интернет. Первыми были ученые и программисты, за ними последовала продвинутая молодежь, далее пришел бизнес и все закрутилось. Реклама, бурное развитие социальных сетей, появление IT гигантов. С опозданием и скрипом, чертыхаясь, порой как слон в посудной лавке, подтянулось и государство. Однако, если в 90-х и 00-х Интернет был своеобразной территорией свободы и даже анархии, то теперь он все больше становится похожим на Матрицу, ну или некоторое множество её вариаций. Парадокс в том, что Интернет задумывался как децентрализованная система, но в социальном его воплощении движется в сторону гиперцентрализации. Торрент, Tor, криптовалюты это скорее история ярких бунтов на корабле подтверждающих неоднозначность его курса. Причин такому развитию много, но главная пожалуй в том, что Сеть при всех возможностях расширяющих социальные границы это еще и гигантские возможности по сбору и анализу информации. Как следствие торговля и манипулирование информацией и даже формирование информационных потоков и среды. И все это умножается с развитием ИИ. Даже посто чувствовать себя рыбкой в аквариуме не очень приятно, не говоря уже о прочих угрозах. Виртуальная реальность, хоть и является проекцией реальной жизни, но сильно ограничена в одной важной естественной возможности из реального мира. Это возможность устанавливать и развивать связи без посредников. Распределенные системы построенные на peer-to-peer связях в социальном сегменте Сети пока скорее нишевая экзотика. Тем временем непрерывные скандалы с утечками персональных данных, слежками, блокировками, модерациями и манипуляциями в социальных сетях, на мой взгляд формируют соответствующий социальный запрос. С чисто технической стороны, одним из препятствий тормозящим многие годы широкое развитие распределенных систем является все никак не уходящий IPv4 и порожденный им NAT. Собственно об этом мы дальше и поговорим. Кому интересно съедаем красную пилюлю и отправляемся в суровый Зион. Синяя пилюля вернет вас назад в мир сладких грез.

NAT закрыл проблему ограниченности IPv4, все к этому привыкли, а большинству пользователей публичный адрес без нужды. Голову ломают разработчики, когда это становится необходимостью. Преодоление NAT не ноухау, это давно реализовано во многих продуктах, например в IP телефонии, системах удаленного доступа, мессенджерах и т.п. Но это все решения, встроенные в соответствующие продукты, универсальных утилит, кроме коленочных скриптовых решений нужно еще поискать. Признаюсь я сильно и не искал, было интересно реализовать самому. Утилита plexus мой скромный вклад в решение этого вопроса. Если сообщество заинтересуется, будет дальнейшее развитие. Для преодоления NAT в plexus реализована хорошо известная техника UDP hole punching. Останавливаться в деталях на этом не буду, материалов на эту тему достаточно и на Хабре в частности. Дальнейшее изложение предполагает, что читатель понимает как работает NAT, что такое mapping и filtering, какие трудности создает NAT для клиент-серверной архитектуры, знаком в общих чертах с упомянутой выше техникой преодоления NAT. Либо пропускайте специфические подробности реализации, а читайте только то, что относится к использованию.

Как это работает

Если упрощенно, то инстансы plexus работающие на обеих сторонах синхронно пробивают "дыры" в NAT в направлении друг друга и передают управление заданным внешним приложениям. В приложения передаются локальный адрес-порт (далее эндпоинт) с которого пробивался NAT, эндпоинт удаленного узла на противоположном NAT и эндпоинт выданный собственным NAT. Последний передается "на всякий случай", если какому нибудь приложению он вдруг понадобится. Утилите требуется доступный в публичной сети STUN сервер для определения типа NAT и получения отображенного эндпоинта, почтовый аккаунт и адреса сторон для обмена выданными NAT эндпоинтами. На NAT политика отображения для порта источника исходящего пакета на внешний порт не должна зависеть от адреcа назначения - independent mapping. То есть при отправке двух и более пакетов на разные адреса с одного порта локальной машины должен выдаваться один и тот же внешний эндпоинт на NAT. Политика фильтрации не важна. Схема ниже показывает более подробно процедуру пробивания "коридора".

При запуске plexus посредством STUN выясняет политику отображения адресов реализованную на NAT. Если она неподходящая приложение завершает работу. Отсутствие NAT, при запуске на публичном интерфейсе не мешает работе программы. На одной из сторон plexus запускается в режиме приема запросов на соединение от соответствующего пира. Вторая сторона соответственно инициатор таких запросов.

Процедура пробивания "коридора"

  1. Инициатор делает запрос отображенного на NAT эндпоинта у STUN сервера. Тем самым он пробивает "дыру" в NAT, но пока только в направлении STUN, через которую на шаге 6 будет инициироваться "рукопожатие" c пиром, что равноценно пробиванию NAT в его направлении.

  2. Инициатор отправляет почтой запрос пиру с полученным эндпоинтом и ожидает ответа.

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

  4. Принимающая сторона посылает пакет пробивающий NAT в нужном направлении, то есть с адресом и портом назначения полученными от пира. При этом пакету устанавливается маленький ttl. Достаточный для того, чтобы пакет вышел за пределы NAT, но не добрался до NAT пира. Это необходимо потому, что на некоторых NAT встречается довольно строгая политика фильтрации, когда пакеты приходящие на открытый порт и не проходящие фильтрацию приводят к сбросу записи в таблице. В этот момент инициатор еще не пробивал свой NAT в нужном направлении. Таким образом при следующей отправке пакета он получит новое отображение на NAT. Успешное пробивание "коридора" в таком случае невозможно.

  5. Принимающая сторона отправляет почтой ответ на запрос со своим эндпоинтом полученными от STUN и переходит к ожиданию "рукопожатия", обмена специальными пакетами позволяющими завершить пробивание "коридора".

  6. Инициатор начинает обмен "рукопожатиями" отправляя пиру пакет с флагом 0. Флаг указывает видел ли отправитель хоть один пакет от пира.

  7. Принимающая сторона получив пакет от пира отправляет ответный с флагом 1.

  8. Инициатор получив пакет отправляет повторно свой с флагом 1, ожидаемый принимающей стороной.

Таким образом стороны получив и отправив пакеты с флагом 1 считают "рукопожатие" состоявшимся и передают управление своим приложениям.

В репозитории проекта https://github.com/novemus/plexus можно найти инструкции по сборке.

Пример использования

Одним из случаев когда можно и нужно использовать plexus это необходимость иметь доступ к компьютеру не имеющему публичного адреса, расположенному за NAT и не подключенному к VPN.

Запускаем инстанс plexus на удаленной машине в режиме приема запросов, с ключем --accept. При необходимости установить кастомный ttl пробивающего пакета, о чем говорилось выше, можно использовать аргумент --punch-hops. По умолчанию ttl равен 7. Определить актуальный ttl можно утилитами трассировки путей пакетов, например traceroute. Этот параметр актуален только для принимающей стороны.

plexus --accept --email-smtps=smtp.peermailer.com:xxx --email-imaps=imap.peermailer.com:xxx --email-login=peerlogin --email-passwd=peerpassword --email-from=peerhost@peermailer.com --email-to=yourhost@yourmailer.com --host-id=remote --peer-id=local --stun-ip=stun.someserver.com --bind-port=xxxx --bind-ip=xxx.xxx.xxx.xxx --exec-command=~/plexus/exec.sh

Когда необходимо подключение к удаленной машине запускаем инстанс plexus на локальной машине в режиме инициатора, то есть без без ключа --accept.

plexus --email-smtps=smtp.yourmailer.com:xxx --email-imaps=imap.yourmailer.com:xxx --email-login=yourlogin --email-passwd=yourpassword --email-from=yourhost@yourmailer.com --email-to=peerhost@peermailer.com --host-id=local --peer-id=remote --stun-ip=stun.someserver.com --bind-port=xxxx --bind-ip=xxx.xxx.xxx.xxx --exec-command=~/plexus/exec.sh

После создания "коридора" запускается приложение переданное через аргумент --exec-command со следующими аргументами:

/path/to/exec/command innerip innerport outerip outerport peerip peerport
  • innerip - локальный IP адрес переданный с аргументом --bind-ip

  • innerport - локальный порт переданный с аргументом --bind-port

  • outerip - публичный IP адрес выданный NAT

  • outerport - порт публичного интерфейса выданный NAT

  • peerip - публичный IP адрес пира

  • peerport - публичный порт пира

Для тестов можно использовать скрипты exec.sh и exec.bat из репозитория. Скрипты заточены на создание VPN туннеля между машинами, требуют установленного openvpn, который нуждается повышенных привилегиях. Либо напишите свои скрипты/приложения принимающие соответствующие параметры. Для повышения уровня безопасности имеется возможность обмениваться зашифрованными почтовыми сообщениями по протоколу S/MIME. Увидеть все имеющиеся аргументы командной строки можно через --help.

Код

Далее будет описание интерфейсной части кодовой базы для любителей "покурить" код. Код структурированный и не сложный по причине простоты утилиты и ее однопоточности. Основные компоненты это "посредник" реализующий функции SMTP и IMAP клиентов для обмена почтовыми сообщениями и "панчер" реализующий функции STUN клиента, пробивание NAT в нужном направлении и процедуру "рукопожатия".

typedef std::pair<endpoint, /* puzzle */ uint64_t> reference;

struct mediator
{
    virtual ~mediator() {}
    virtual reference receive_request() = 0;
    virtual reference receive_response() = 0;
    virtual void dispatch_response(const reference& host) = 0;
    virtual void dispatch_request(const reference& host) = 0;
};

Названия методов "посредника" говорят за себя. Тип reference содержит эндпоинт пира и пазл. Последний является случайным числом генерируемым на каждой стороне. Из пары таких чисел составляется ключ-маска используемая для верификации "рукопожатий".

enum binding
{
    unknown = 0,
    independent = 1,
    address_dependent = 2,
    address_and_port_dependent = 3
};

struct traverse
{
    unsigned int nat : 1,
                 hairpin : 1,
                 random_port : 1,
                 variable_address : 1,
                 mapping : 2, // enum binding
                 filtering : 2; // enum binding
};

struct puncher
{
    virtual ~puncher() {}
    virtual traverse explore_network() = 0;
    virtual endpoint punch_udp_hole() = 0;
    virtual endpoint punch_udp_hole_to_peer(const endpoint& peer, uint8_t hops) = 0;
    virtual void reach_peer(const endpoint& peer, uint64_t mask) = 0;
    virtual void await_peer(const endpoint& peer, uint64_t mask) = 0;
};

Методы "панчера".

explore_network - посредством STUN проверяет текущие параметры NAT и возвращает набор флагов его описывающих. Ключевой флаг mapping - политика отображения локального эндпоинта в публичный, о чем упоминалось выше.

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

punch_udp_hole_to_peer - делает тоже самое, что и предыдущий метод, но дополнительно пробивает NAT в сторону пира пакетом с укороченным ttl.

reach_peer - инициирует и выполняет процедуру "рукопожатия".

await_peer - ожидает и выполняет процедуру "рукопожатия" со своей стороны.

И в завершении фрагмент кода реализующий процедуру пробивания "коридора".

if (vm.count("accept"))
{
    plexus::reference peer = mediator->receive_request();
    plexus::reference host = std::make_pair(
        puncher->punch_udp_hole_to_peer(peer.first, (uint8_t)vm["punch-hops"].as<uint16_t>()),
        plexus::utils::random()
        );
    mediator->dispatch_response(host);
    puncher->await_peer(peer.first, peer.second ^ host.second);

    executor(host.first, peer.first);
}
else
{
    plexus::reference host = std::make_pair(
        puncher->punch_udp_hole(),
        plexus::utils::random()
        );
    mediator->dispatch_request(host);
    plexus::reference peer = mediator->receive_response();
    puncher->reach_peer(peer.first, peer.second ^ host.second);

    executor(host.first, peer.first);
}

Пожелания, замечания, отлов багов приветствуются. Но лучшее, что вы можете сделать это открыть соответсвующий тикет в репозитории с исчерпывающими комментариями, логами уровня трассировки, сессиями Wireshark. Можно предлагать свое непосредственное участие. Утилита опубликована как свободное ПО под лицензией Apache 2.0.

Спасибо за внимание.

Комментарии (14)


  1. AllKnowerHou
    08.08.2022 17:54

    А в чем заключается цель проекта? И каковы его преимущества по сравнению с другими вариантами передачи данных между пользователями?


    1. novemix Автор
      08.08.2022 18:21

      Цель проекта в создании универсального инструмента преодоления ограничений NAT.


      1. AllKnowerHou
        08.08.2022 19:15

        А что Вам не нравится в том чтобы делать проект также как и другие проекты, используя сервер - ведь даже в вашем случае он нужен - тут как бы не сэкономить на нем все равно


        1. aamonster
          09.08.2022 00:17

          Довольно-таки понятно, что автору не нравится: увеличение пути/пинга (через сервер – это плюс сколько хопов?) + на несколько порядков более высокий расход ресурсов сервера.

          Так что отправка пакетов через сервер в правильном решении должна быть фоллбэком на случай невозможности установить прямое соединение.


  1. oficsu
    08.08.2022 18:06
    +1

    Мне без сомнения нравится начинание и я поэкспериментирую с ним, если найду время, но из readme у меня появился вопрос. Почему email? Ну, точнее, почему это строгая зависимость?

    На мой взгляд – правильный дизайн в том, чтобы подготовить некоторое библиотечное решение для работы со STUN, а cli приложение должно быть маленьким дополнением к библиотеке

    А email должен быть одним из адаптеров некоторого интерфейса. Возможно, единственным стандартным, но желательно дать пользователю возможность писать и другие. Например, почему не Телеграм, почему не обычный файл?

    Файл тоже может синхронизироваться через облако или через syncthing (та же технология в основе, только не требует почтового сервиса для функционирования), разве не стоит предпочесть более простой вариант для начала?


    1. novemix Автор
      08.08.2022 18:27
      +1

      Почему email? 

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

      А email должен быть одним из адаптеров некоторого интерфейса.

      Да, примерно так и задумывалось. Первоначальный вариант решил сделать через email. Сейчас продумываю алтернативные варианты. Но важно, чтобы это не нагружало сильно пользователя дополнительными приседаниями. В том числе по этой причине первым стал email. Интегрировать любую другую службу не составит проблем. Интерфейс "посредника" обобщенный.


  1. ivan386
    08.08.2022 20:05

    Пиры могут связатся через 6to4 и передать друг другу необходимые данные для пробития NAT.


    1. Urub
      09.08.2022 14:37

      Можно ли чуть подробней рассказать как это сделать ? Пусть есть два хоста (android и ubuntu) за nat разных провайдеров, и белый ip в интернете - как их связать на примере в консоли ?


      1. ivan386
        09.08.2022 16:57

        Настраивается 6to4 на обоих концах используя внешний IPv4 своего провайдера. Далее отправляется пинг на любой IPv6 адрес и NAT провайдера откроет маршрут с локального IPv4 до 6to4 шлюза. Появляется возможность принимать и устанавливать соедения используя IPv6.


        К сожалению я не скажу как это сделать настройками в консоли. Я настраивал 6to4 на роутере. Ну и написал программку которая отправляет 6to4 пинг go-ping6to4 для проверки концепции.


        Через мобильную сеть 6to4 не работает.


        1. Urub
          09.08.2022 17:01

          эх, очень жаль что через мобильную нельзя - именно мобилу и комп соединить надо


          1. ivan386
            09.08.2022 22:23

            Некоторые мобильные операторы дают полноценный IPv6 но его надо включить в личном кабинете.


            Вот тут можно посмотреть https://version6.ru/isp


            1. Urub
              09.08.2022 22:45

              както там не густо или в тестовом режиме

              полноценный ipv6 мне наверно и не нужен, я даже никогда его не использовал )

              моя хотелка - передать на мобилы с компа и обратно немного tcp и udp, но т.к. мобилы в серой сети, то никак не могу напрямую

              есть решения, например для мегафона "Корпоративные сети", но не хотелось бы привязываться к конкретному опсосу, да и такое решение неудобно чемто еще


  1. Ingulf
    11.08.2022 12:57

    как насчет webrtc от гугла - он написан поверх (не вспомню сейчас как именно называется) другой гугловской либой, которая так же пробивает NAT'ы и обеспечивает связь пиров, они признают, что могут пробить NAT далеко не в 100% случаев, чем Ваше решение лучше\удобнее\эффективнее?


    1. novemix Автор
      11.08.2022 14:46

      Удобнее тем, что это именно утилита не нацеленная на какую-то конкретную прикладную область и вы вольны выбирать STUN сервер, почтовые сервера для обмена, тем самым полностью контролируя свои системы. Вы же приводите прикладную технологию предусматривающую преодоление NAT. Я писал в посте об этом. Сравнивать качество не имеет смысла в виду совершенно различного назначения. Эффективность же пробивания NAT зависит не столько от утилиты, сколько от настроек самого NAT. 100% нигде не будет. Другое дело, что подобные webrtc продукты предусматривают релейные решения в случае невозможности пробить NAT. В этом их плюс, но в этом же их минус, так как они привязывают пользователя к корпоративным решениям. Появляется сторона посредник, по сути контролирующая ваши коммуникации и вы этим никак не можете управлять.