Приветствую всех!
В этом посте я расскажу о назначении и устройстве небольшой, но полезной утилиты 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 запускается в режиме приема запросов на соединение от соответствующего пира. Вторая сторона соответственно инициатор таких запросов.
Процедура пробивания «коридора»
-
Инициатор делает запрос отображенного на NAT эндпоинта у STUN сервера. Тем самым он пробивает «дыру» в NAT, но пока только в направлении STUN, через которую на шаге 6 будет инициироваться «рукопожатие» c пиром, что равноценно пробиванию NAT в его направлении.
-
Инициатор отправляет почтой запрос пиру с полученным эндпоинтом и ожидает ответа.
-
Принимающая сторона получив запрос получает через STUN отображеный эндпоинт на собственном NAT, с которого далее будет пробиваться в сторону пира модифицируя таблицу фильтрации NAT для возможности получения пакетов с нужного адреса.
-
Принимающая сторона посылает пакет пробивающий NAT в нужном направлении, то есть с адресом и портом назначения полученными от пира. При этом пакету устанавливается маленький ttl. Достаточный для того, чтобы пакет вышел за пределы NAT, но не добрался до NAT пира. Это необходимо потому, что на некоторых NAT встречается довольно строгая политика фильтрации, когда пакеты приходящие на открытый порт и не проходящие фильтрацию приводят к сбросу записи в таблице. В этот момент инициатор еще не пробивал свой NAT в нужном направлении. Таким образом при следующей отправке пакета он получит новое отображение на NAT. Успешное пробивание «коридора» в таком случае невозможно.
-
Принимающая сторона отправляет почтой ответ на запрос со своим эндпоинтом полученными от STUN и переходит к ожиданию «рукопожатия», обмена специальными пакетами позволяющими завершить пробивание «коридора».
-
Инициатор начинает обмен «рукопожатиями» отправляя пиру пакет с флагом 0. Флаг указывает видел ли отправитель хоть один пакет от пира.
-
Принимающая сторона получив пакет от пира отправляет ответный с флагом 1.
-
Инициатор получив пакет отправляет повторно свой с флагом 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 туннеля между машинами. Либо напишите свои скрипты/приложения принимающие соответствующие параметры. Для повышения уровня безопасности имеется возможность обмениваться зашифрованными почтовыми сообщениями по протоколу SMIME. Увидеть все имеющиеся аргументы командной строки можно через —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.
Спасибо за внимание.
ссылка на оригинал статьи https://habr.com/ru/post/681432/
Добавить комментарий