P2P общего назначения
IT-индустрия совершила интересный виток развития в последние десятилетия. На заре IT-революции, когда компьютеры были дорогими, их ресурсы, хоть и скромные по нынешним временам, нужно было использовать эффективно. Это привело к появлению многопользовательских OS, например, семейства UNIX, созданию компьютерных сетей и появлению протоколов удаленного доступа.
Стремительное развитие производительности чипов и удешевление железа привело к революции персональных компьютеров. Однако опережающий рост вычислительных потребностей запустил развитие на новый круг. Сейчас найдется немного компаний и организаций, которые обходятся исключительно персональными компьютерами и сервисами в локальной сети. Когда-то диковинные IaaS, PaaS, SaaS, распределенные вычислительные технологии плотно вошли в жизнь. Доступ к удаленным системам — обыденная потребность не только для администраторов и программистов. Все знают о протоколах ssh, rdp, vnc, многие пользуются TeamViewer, AnyDesk, Remmina, X2Go и т. п. Ввиду того, что IPv4 сети и порожденный ими NAT пока еще более чем актуальны, не каждое из перечисленных средств позволяет подключиться к машине, находящейся за NAT, если у вас нет возможности пробросить порты.
Работая удаленно, постепенно обрастая девайсами, время от времени я стал ощущать потребность подключаться к собственным устройствам удаленно через интернет. Я арендовал VDS на минималках и поднял виртуальную сеть. С технической точки зрения решение избыточное для моих скромных и нечастых нужд. Казалось бы, такая простая потребность — подключиться к службе удаленной машины, а NAT мешается под ногами. В мире P2P разнообразные торренты, криптовалюты, VoIP, WebRTC, некоторые мессенджеры давно справляются с NAT с помощью протоколов STUN/TURN/ICE.
Напомню, эти протоколы позволяют исследовать NAT и его свойства, «приоткрыть» его для входящих соединений на внутренний ресурс, а если невозможно, то использовать релейные решения. Однако реализации данных протоколов встроены в конечные продукты, а в OpenSource мне не удалось найти решений, которые можно было бы просто взять и использовать «из коробки» по своему усмотрению.
Родилась идея восполнить этот недостаток и написать инструмент, который позволял бы удаленно подключаться через NAT к любому сервису. Так появился проект plexus, а позже — еще пара вспомогательных. Инструмент еще не реализовывает в полной мере всего нужного, но в большинстве случаев позволяет связывать приложения, запущенные на машинах, находящихся за NAT. Дальнейшее повествование подразумевает, что читатель понимает принципы NAT, протоколы TCP/UDP и читал что-нибудь про технику преодоления NAT с помощью протокола STUN называемую UDP-hole-punching.
NAT и его свойства
Про NAT написано много, поэтому коротко и упрощенно обозначим его характеристики. Когда NAT видит первый исходящий пакет с некоторого внутреннего адреса, он выделяет порт на публичном интерфейсе, подменяет адрес/порт источника исходящего пакета на выделенную пару, делает запись в таблице, где связывает адрес/порт внутреннего ресурса, адрес/порт внешнего ресурса и выделенный публичный адрес/порт, затем отправляет пакет по назначению. В остальных исходящих пакетах с теми же параметрами подменяет адрес/порт источника и передает их дальше. Для входящих пакетов на выделенную пару NAT подменяет адрес/порт назначения на адрес/порт внутреннего ресурса и передает пакет во внутреннюю сеть. Не каждый входящий пакет может быть передан во внутреннюю сеть. За это отвечает политика filtering:
Full Cone NAT — пропускаются пакеты из любого источника.
Port Restricted Cone NAT — пропускаются только пакеты, порт источника которых совпадает с портом назначения первого исходящего пакета.
Address Restricted Cone NAT — пропускаются только пакеты, адрес источника которых совпадает с адресом назначения первого исходящего пакета.
Symmetric NAT — пропускаются пакеты только из источника, к которому было исходное обращение.
Свойство hairpin определяет, допускается ли передача во внутреннюю сеть пакетов, пришедших на внешний интерфейс из внутренней же сети.
Еще одна важная политика — это mapping. Она отвечает за то, как происходит отображение внутреннего источника на внешний интерфейс, будет ли создаваться новое отображение, если источник очередного исходящего пакета остался прежним, а назначение поменялось. В теории здесь возможны те же варианты, что и с filtering.
Свойство preserve port определяет, будет ли NAT стремиться сохранять номер порта при отображении.
UDP-hole-punching
При определенных условиях подключение к службе, скрытой за NAT, возможно. Для этого служба каким-то образом должна узнать о желании клиента подключиться и его внешний адрес/порт, суметь создать нужное отображение на внешний интерфейс NAT, узнать адрес/порт этого отображения, сообщить его клиенту. При этом адрес/порт клиента должен соответствовать политике фильтрации NAT.
Для решения этих задач прежде всего необходим STUN. Это UDP протокол обмена сообщениями со специальным сервисом, имеющим два публичных адреса и два порта, по которым он доступен на каждом из них. Он позволяет выяснить политики NAT c помощью нескольких запросов, обращаясь на разные адреса и порты сервера и запрашивая ответы с разных адресов и портов, а также получить адрес/порт созданного отображения на публичный интерфейс NAT. Для обмена сообщениями между клиентом и сервисом используется сигнальная служба, так называемый rendezvous. Он может быть реализован разными способами. Например, через расшариваемый в сети интернет файл, с помощью email или SMS, через сети DHT или IPFS и т.п. Схема преодоления NAT показана ниже.
Клиент, желая подключиться к скрытой за NAT службе, сначала с помощью STUN отображает себя на внешний интерфейс собственного NAT и выясняет его политики (1).
Для клиента важна исключительно политика mapping, так как позже при отправке пакетов на публичный адрес удаленной службы, спрятанной за NAT, необходимо, чтобы его внешнее отображение, полученное после общения со STUN сервером, не поменялось. Если все в порядке, то через сигнальную службу клиент отправляет сообщение сервису, прописав в него собственный публичный адрес/порт (2).
Сервис, получив сообщение, также отображает себя на внешний интерфейс NAT с помощью STUN и выясняет политики собственного NAT (3).
Для сервиса имеет значение политика filtering, так как он реализует серверное приложение, то есть слушает соединения. Как правило, NAT-провайдеры реализуют симметричную фильтрацию, что для p2p не очень хорошо, но еще не безнадежно. В целом симметричная фильтрация — это нормально, так как отправлять во внутреннюю сеть пакеты, пришедшие откуда попало, небезопасно. В этом случае на помощь приходит политика mapping. Здесь провайдеры, как правило, позволяют сохранять отображение при смене адреса/порта назначения исходящих пакетов. И это тоже нормально, так как новое отображение инициируется легальным способом по инициативе внутреннего хоста. Теперь наша служба должна направить «пробивающий» NAT пакет на адрес/порт, полученный от клиента, тем самым создав новую запись в таблице NAT, и наличие симметричной фильтрации никак не будет мешать (4).
Далее посредством сигнальной службы сервис сообщает клиенту свое внешнее отображение (5).
С этого момента клиент и сервис могут установить соединение обычным способом, NAT уже не будет этому препятствовать (6). В этом заключается суть техники UDP-hole-punching.
Почему UDP, а не TCP? Потому что UDP мультиадресный и дейтаграммный, не имеет состояний, стороны соединения не имеют формальных ролей в отличие от TCP, где всё это сильно мешает программной реализации и преодолению NAT. UDP-отображение на NAT удаляется по таймеру, что очень удобно из-за предсказуемости. Реализация для TCP тоже возможна, но работать будет плохо. Для TCP нужно пробивать NAT syn-пакетами, но клиент также будет соединяться с помощью syn, а настройка firewall может рассматривать в качестве допустимых только syn|ack. Также TCP — это протокол с состояниями, и NAT об этом знает, а гонки при установлении соединения через «пробитый» NAT могут провоцировать reset ответы или icmp ошибки на что NAT отреагирует сбросом записи в таблице.
Практические эксперименты показали, что для TCP преодоление NAT работает не везде и крайне нестабильно. К тому же для манипуляций отдельными TCP пакетами нужны повышенные привилегии. Тем не менее большинство сетевых приложений работает по протоколу TCP. Решить проблему можно пробросом TCP-потока через UDP-туннель, что будет показано позже.
Plexus
Вышеописанный алгоритм реализован в утилите plexus с небольшим дополнением. Во время обмена сообщениями стороны дополнительно формируют проверочный ключ, а на последнем этапе, прежде чем передать управление основному приложению, идентифицируют друг друга встречными пакетами с использованием ключа. В качестве сигнальной службы можно использовать службу email (smtp и imap) или DHT-сеть. В github-репозитории можно найти инструкцию по сборке утилиты или скачать готовые пакеты.
Для использования утилиты необходимо создать репозиторий приложения, разместить в нем каталоги всех участников, согласно схеме apprepo/owner@mailer.com/pin, где
apprepo — каталог приложения;
owner@mailer.com — идентификатор владельца удаленного или локального эндпоинта;
pin — идентификатор эндпоинта, уникальный для владельца.
Каждый участник должен сгенерировать приватный ключ и X509-сертификат и обменяться сертификатами с остальными участниками. В каталоге локального хоста нужно разместить его сертификат, именованный cert.crt, и приватный ключ, именованный private.key. В каталоге удаленного хоста нужно разместить его сертификат cert.crt. На других машинах необходимо проделать то же самое.
В plexus стороны идентифицируют друг друга с помощью открытого ключа в случае использования DHT в качестве сигнальной службы, а сообщения шифруются. Верификация отправителя выполняется с помощью цифровой подписи. Идентификатор владельца может быть любой строкой, в DHT-сети он не используется. Однако, если вы планируете использовать email как сигнальную службу, то идентификаторы должны быть реальными почтовыми адресами, через которые будет выполняться обмен сообщениями. Идентификаторы эндпоинтов должны быть согласованы, так как они используются для уточнения получателя и фильтрации сообщений. Здесь шифрование сообщений опционно. Если вы полностью доверяете email службе, то просто не размещайте ключи и сертификаты в каталогах участников.
Пример
Следующий пример демонстрирует подключение к удаленному хосту, находящемуся за NAT, через ssh. Как я упоминал выше, для TCP приложений нужно создавать UDP туннель через пробитый NAT. Для этих целей была написана вспомогательная утилита wormhole. С ее помощью можно отобразить удаленный сервис на порт локального интерфейса через UPD туннель. Примерно так же как проброс портов через же ssh. Подробнее расскажу об этом в следующей статье. Скачайте/соберите и установите пакеты plexus и wormhole на удаленной и локальной машинах. Создайте и наполните репозитории на серверной и клиентской машинах:
Конфигурация для серверной машины, файл ssh-export.conf:
accept=1
app-id=ssh-service
app-repo=/home/server/plexus
host-info=server-owner@mailer.com/ssh-server
peer-info=client-owner@mailer.com/ssh-client
dht-bootstrap=bootstrap.jami.net:4222
stun-server=stun.ekiga.net
exec-command=wormhole
exec-args=--purpose=export --service=127.0.0.1:22 --gateway=%innerip%:%innerport% --faraway=%peerip%:%peerport% --log-file=export%p.log
Запускаем…
$ cd ~/plexus
$ plexus --config=ssh-export.conf
Конфигурация для клиентской машины, файл ssh-import.conf:
accept=0
app-id=ssh-service
app-repo=/home/client/plexus
host-info=client-owner@mailer.com/ssh-client
peer-info=server-owner@mailer.com/ssh-server
dht-bootstrap=bootstrap.jami.net:4222
stun-server=stun.ekiga.net
exec-command=wormhole
exec-args=--purpose=import --service=127.0.0.1:2222 --gateway=%innerip%:%innerport% --faraway=%peerip%:%peerport% --log-file=import%p.log
Запускаем…
$ cd ~/plexus
$ plexus --config=ssh-import.conf
После того как plexus пробьет NAT, утилита wormhole создаст UDP туннель и удаленный ssh-сервер будет «отображен» локально на адрес 127.0.0.1:2222. Если все прошло штатно, в логах отобразится запись c PID запущенной команды. Можно подключаться к ssh…
$ ssh -p 2222 127.0.0.1
Аргументы в plexus можно передавать как конфигурационным файлом, так и ключами. Так как один репозиторий может использоваться для разных приложений, ключом --app-id необходимо указать целевое приложение. Как не трудно догадаться, ключи --host-info и --peer-info идентифицируют участников и являются относительными путями к каталогам участников в репозитории. Ключ --accept указывает, что данная сторона будет работать в режиме ожидания подключений. Если вы хотите принимать подключения с нескольких хостов, то просто не указывайте ключ --peer-info, будут приниматься подключения от всех хостов описанных в репозитории. Аргументом --exec-command можно передать любую команду или скрипт на ваше усмотрение. Для передачи параметров вашему приложению нужно использовать ключ --exec-args со следующими подстановочными шаблонами:
%innerip% — локальный адрес привязки приложения, может задаваться ключом --stun-client
%innerport% — локальный порт привязки приложения, может задаваться ключом --stun-client
%outerip% — адрес отображения на публичный интерфейс NAT
%outerport% — порт отображения на публичный интерфейс NAT
%peerip% — адрес публичного интерфейса удаленного хоста, полученный от сигнальной службы
%peerport% — порт публичного интерфейса удаленного хоста, полученный от сигнальной службы
%hostmail% — идентификатор владельца локального хоста, префикс аргумента --host-info
%peermail% — идентификатор владельца удаленного хоста, префикс аргумента --peer-info
%hostpin% — идентификатор локального хоста, суфикс аргумента --host-info
%peerpin% — идентификатор удаленного хоста, суфикс аргумента --peer-info
%secret% — сформированный сторонами сессионный 64-битный ключ
Сталкивался со случаем, когда «пробивающий» в сторону клиента пакет (шаг 4), достигнув клиентский NAT, приводил к сбросу серверного отображения. В этот момент такой пакет еще не проходит политику фильтрации. Вероятно, в этом была причина такой реакции NAT. Для купирования подобных проблем нужно использовать ключ –punch-hops, по умолчанию 7. Этот аргумент предназначен для установки ttl «пробивающего» пакета так, что бы он мог выйти за пределы серверного NAT, создав на нем нужное отображение, но не достиг клиентского. Для использования email в качестве сигнальной службы определите ключи --email-smtps, --email-imaps, --email-login и --email-password. Запустите утилиту с ключом --help, чтобы увидеть все возможные аргументы.
В каких случаях plexus не поможет
В случае, если на NAT политика отображения реализует зависимость от адреса/порта назначения, то есть создается новое отображение при перенаправлении пакетов к новому ресурсу даже при сохранении адреса/порта источника.
В случае, если обе машины находятся за одним NAT и он не поддерживает функцию hairpin.
Обнаружение и соединение приложений в локальных сетях пока не реализовано, как и возможность использования релейных решений.
Проект развивается и для данных случаев уже намечены решения.
Библиотека
Утилита plexus так же реализована в виде библиотеки. API описан в заголовочном файле plexus.h, по функционалу идентичен описанному выше. C++ программист сможет без труда разобраться. Дополнительно имеется возможность создавать UDP потоки, в том числе с SSL шифрованием. Если будет интерес, то об этом я тоже расскажу в отдельной статье.
Copyright © 2024 Novemus
Автор статьи: Сергей Девяткин
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS(кроме тарифа Прогрев) — HABRFIRSTVDS