Данная статья предназначена для тех, кто хотел бы разработать собственного I2P клиента «с нуля». Предполагается знакомство с основными концепциям и понятиями I2P. На настоящий момент на это счет имеется достаточно документации и статей, в том числе и переведённых на русский язык. С другой стороны имеется официальная документация, достаточно хорошо описывающая протоколы и форматы сообщений. К сожалению она носит разрозненный характер, при этом многие неочевидные вещи там отсуствуют. Данная статья написана в первую очередь на основе изучения и отладки официального I2P джава-клиента. Конечной целью является реализация полностью на C++. Исходный код проекта в текущем состоянии располагается на github.
Для построения собственного I2P маршутизатора необходимо наличие реализации следующих алгоритмов шифрования:
I2P сеть состоит из 4-х основных уровней:
Каждый уровень добавляет свое собственное шифрование разного назначения. Шифрование транспортного уровня скрывает трафик от провайдера, туннелей — содержимое и направление от промежуточных узлов туннелей, «чеснок» — от конечных узлов тоннелей при передаче сообщений между тоннелями.
Для того чтобы установить соединение транспортного уровня требуется знать IP адрес и порт. Существует список известных узлов, называемый netDb, изменяющийся в процессе работе, информация о новых узлах поступает от других узлов. Первоначально список узлов скачивается со специальных сайтов, адреса которых явно перечислены в файле router/networkdb/Reseeder.java. Протокол, работающий поверх TCP/IP называетcя NTCP, а поверх UDP — SSU. Помимо некоторых отличий в установке соединений, SSU из-за пакетной природы поддерживает разбивку длинных сообщений на несколько фрагментов. Передаваемые сообщения состоят из заголовка, I2PN сообщения (о протоколе I2NP ниже) и контрольной суммы. Периодически передается специальное сообщение, содержащее текущее время с целью синхронизации. При установке соединения происходит обмен публичными ключами маршутизаторов, на основе которых по алгоритму Диффи-Хельмана вычисляется общий ключ для AES шифрования каждый на своей стороне.
Тоннели всегда однонаправленные — все сообщения могут передаваться только от входного узла (Gateway) к выходному узлу (Endpoint). В зависимости от того какой конец тоннеля принадлежит его владельцу, обладающему всей полнотой информации о тоннеле, тоннели делятся на входящие (владелец — выходной узел) и исходящие (владелец — входной узел). Промежуточным узлам тоннеля неизвестно, является ли тоннель входящим или исходящим, единственное действие, осуществляемое промежуточным узлом, это шифрование сообщения своим ключом шифрования и передача следующему узлу. Отсюда вытекает важное следствие: последовательное расшифрование сообщений тоннеля должно осуществляться его владельцем, поскольку только у владельца есть ключи шифрования всех промежуточных узлов. Данный факт достаточно тривиален для входящих тоннелей, т. е. получив сообщение выходной узел должен его последовательно расшифровать, однако для исходящих тоннелей оригинальное незашифрованное сообщение должно быть последовательно расшифровано перед его отправкой. Тоннели, для которых данный узел не является владельцем, называются транзитными. Транзитные тоннели передают чужой трафик и необходимы для поддержки функционирования всей сети I2P, тем самым превращая узел в маршутизатор. Узлы тоннелей используют AES шифрование с тремя различными ключами: один используется для шифрования ответа узла при создании тоннеля, а два других для передачи данных через тоннель: один ключ шифрует сами данные, а другой шифрует вектор ипициализации (IV) для шифрования данных. При этом IV шифруется тем же самым ключом дважды: до шифрования и после, называется это двойным шифрованием (double encryption). Узел получает эти два ключа в относящейся к нему записи сообщения создания тоннеля, зашифрованной его публичным ключом с помощью Эль-Гамаля.
Внутри тоннелей передаются исключительно сообщения TunnelData, вообще говоря состоящие из нескольких фрагментов. Для передачи между тоннелями используется сообщение TunnelGateway. Хотя в официальной документации написано, что для двустороннего соединения необходимо как минимум 4 тоннеля (2 входящих и 2 исходящих), на самом деле передавать сообщения через исходящие тоннели необязательно, а можно отправить сообщение TunnelGateway входному узлу нужного входяшего тоннеля.
В сообщении TunnelData контрольная сумма вычисляется из содержательных данных, следующих за нулевым байтом и присоединённым к нему не зашифрованным IV.
Обмен данными внутри сети I2P происходит с помощью I2NP сообщений разных типов. Каждое сообщение содержит заголовок с его типом и длиной, позволяющий определить границы между сообщениями. В зависимости от типа длина сообщения может варьироваться от 20 до 64К байт. Каждый уровень использует сообщения- «обертки», содержащие внутри себя другие I2NP сообщения более высокого уровня. Для тоннелей такими «обертками» являются сообщения TunnelData для передачи внутри тоннелей и TunnelGateway для передачи между тоннелями. Для «чеснока» — Garlic. Большую часть I2P трафика представляют собой следующий вложенные сообщения:
Data->Garlic->TunnelData.
Как правило сообщения передаются через тоннели, хотя могут передаваться и напрямую между маршутизаторами, в частности для первоначального создания новых тоннелей. Также маршутизаторы обмениваются сообщениями DatabaseStore сразу после установки соединения. Сообщения между точками назначения следует передавать через «чеснок», поскольку соответствующее поле присутствует только там.
Для работы в сети I2P необходим I2P клиент, состоящий из маршутизатора, обеспечивающего доступ в сеть I2P, и точек назначения для обмена содержательной информацией. Информация о маршутизаторах в том числе и об их IP адресах является общедоступной, более того, актуальный список маршутизаторов можно скачать со специальных ftp-сайтов. В то же время информация о местоположении точек назначения является конфиденциальной. Информацией о точках назаначения, расположенном на данном маршутизаторе, располагает только данный маршутизатор, для все остальных получение этой информации не представляется возможным, что является одним из основных механизмов обеспечения анонимности сети I2P.
Поскольку маршутизаторы в основном располагаются на компьютерах участников сети, то их состав все время изменяется. Поэтому маршутизаторы вынужденны постоянно поддерживать свой список другим маршутизаторов в актуальном состоянии. Этот процесс называется «зондированием» (exploratory), заключающийся в посылке запросов со случайно выбранным 32-байтным адресом специальным маршутизаторам, называемых floodfill. Предполагается что floodfill-маршутизаторы обладают всей полнотой информации о сети. Помимо все прочего floodfill-маршутизаторы постоянно сообщают друг другу информацию о найденных новых узлах.
Для запроса информации об узле используются I2NP сообщение DatabaseLookup, а для передачи самой ифмации DatabaseStore. Как правило сообщения передаются через тоннели, однако DatabaseStore передается узлом напрямую на траспортном уровне сразу после установки соединения, тем сам сообщая сети о своем существовании. В противном случае построение тоннелей для новых узлов было бы невозможно.
DatabaseStore может содержать информацию двух видов, в случае если данному адресу соотвествуют структура RouterInfo, то адрес является маршутизатором, а если LeaseSet то точкой назначения.
RouterInfo содержит публичные ключи маршутизатора, а также разнообразную служебную информацию, наиболее важной из которой являются IP адреса, порты и поддерживаемые транспортные протоколы для соединения и сведения о том является ли даныый маршутизатор floodfill-ом или нет. Поскольку RouterInfo может содержать довольно много текстовой информации, то передается заархивированным gzip-ом.
LeaseSet, содержит список входящих туннелей данной точки назначения, а также публичный ключ для шифрования «чесночных» сообщений, предназначенной данной точке назначения.
Рассмотрим содержательные действия I2P клиента: анонимный хостинг онлайн ресурсов, и, соответственно, доступ к ним. Для начала попробуем получить данные с некоторого веб-сайта, например, Флибусты. На данный момент у нас имеется только 32-байтный хэш ее I2P адреса, нашей целью же является отправка HTTP-запроса, и получение ответа.
Разумеется маршутизатор с таким адресом в базе отсутствует (иначе IP адрес ресурса был бы виден всем), поэтому единственный способ отправить запрос — это какой-нибудь входящий тоннель нужного узла, существующий в данный момент времени, для чего сначала следует запросить и получить LeaseSet. В отличие от RouterInfo, который можно запросить и получить с соседнего узла на транспортном уровне, LeaseSet можно запросить и получить только через туннели, которые предварительно нужно построить. Отсюда следует неутешительный вывод, что использовать I2P сеть «по требованию» не получится, I2P маршутизатор должен быть запущен и должен постоянно заниматься построением и поддержкой тоннелей. Из-за децентрализованности сети построение тоннелей весьма непростое дело — большинство попыток создания тоннелей оканчиваются неуспехом.
Для успешного построения тоннеля необходимо два условия:
Максимальное время жизни тоннеля 10 минут, тоннель может прекратить свое существование и досрочно, если участвующий в тоннеле узел ушел в оффлайн. Поэтому владельцы тоннелей постоянно посылают тестовые сообщения чтобы поддерживать список «живых» тоннелей в актуальном состоянии.
Итак, тоннели имеются в наличии и необходимый LeaseSet имеется в наличии. Теперь можно отправить HTTP запрос и он даже достигнет адресата, однако нам желательно также и получить ответ. Для этого мы в нашем сообщение должны указать наш собственный LeaseSet, тогда ответ будет отправлен нам через какой нибудь входящий туннель и скорее всего благополучно достигнут нашего узла. Поскольку через наш узел может одновременно работать несколько соединений, то каждому из них должен быть либо назначен собственный I2P адрес и сформирован LeaseSet из нескольких входящих туннелей, либо создан «разделяемый» адрес, мультиплексирующий соединения, используя специальный протокол соответствующими полями, являющийся «оберткой» над протоколом прикладного уровня. Такой протокол называется I2CP и в официальном I2P клиента используется исколючительно он, хотя для построения собственных служб это необязательно. Разумеется для доступа к Флибусте следует использовать I2CP, посколько она ожидает именно его. Однако для построения к примеру собственной торрентоподобной сети можно обойтись только I2P адресацией.
Протокол I2CP и построенный поверх него стек протоколов является отдельной темой, которой посвящена отдельная статья.
Используемое шифрование
Для построения собственного I2P маршутизатора необходимо наличие реализации следующих алгоритмов шифрования:
- Эль-Гамаль (ElGamal). Ассиметричное шифрование, основанное на возведении основания в степень по модулю. Основание и модуля являются фиксированными константами для всей сети I2P. Помимо блоков стандартного размера в 514 байта, также используются блоки нестандартного размера в 512 байт.
- Диффи-Хельман (Diffie-Hellman) для получения общего ключа симметричного ключа шифрования путем обмена публичными ключами. Используются те же самые ключи что и для Эль-Гамаля.
- DSA для создания и проверки электронной подписи
- AES а двух режимах: CBC с использованием ключа шифрования и вектора инициализации (IV), ECB для шифрования собственно IV длиной 16 байт
- SHA256 для вычисления хэшей
- Adler32 для вычисления контрольной суммы сообщений
Основные протоколы
I2P сеть состоит из 4-х основных уровней:
- Транспортный уровень. Это зашифрованные Интернет соединения TCP/IP или UDP. Включает в себя установку соединений и шифрование.
- Тоннели. «Окна» узлов во внешний мир, располагающиеся на других узлах и позволяющие скрывать свое истинное местоположение. Состоят из последовательности узлов, соединенных между собой протоколами транспортного уровня. Тоннель можно упрощенно представлять себе как цепочку прокси-серверов для анонимизации как клиента так и сервера.
- «Чеснок». Передача сообщений или последовательности между двумя конечными узлами произвольным маршрутом и тоннелями. Характеризуется идентификаторами сессий и асссиметричным, а, после установки сессии, симметричным шифрованием
- Протоколы прикладного уровня для передачи пользовательских данных между узлами.
Каждый уровень добавляет свое собственное шифрование разного назначения. Шифрование транспортного уровня скрывает трафик от провайдера, туннелей — содержимое и направление от промежуточных узлов туннелей, «чеснок» — от конечных узлов тоннелей при передаче сообщений между тоннелями.
Транспортный уровень
Для того чтобы установить соединение транспортного уровня требуется знать IP адрес и порт. Существует список известных узлов, называемый netDb, изменяющийся в процессе работе, информация о новых узлах поступает от других узлов. Первоначально список узлов скачивается со специальных сайтов, адреса которых явно перечислены в файле router/networkdb/Reseeder.java. Протокол, работающий поверх TCP/IP называетcя NTCP, а поверх UDP — SSU. Помимо некоторых отличий в установке соединений, SSU из-за пакетной природы поддерживает разбивку длинных сообщений на несколько фрагментов. Передаваемые сообщения состоят из заголовка, I2PN сообщения (о протоколе I2NP ниже) и контрольной суммы. Периодически передается специальное сообщение, содержащее текущее время с целью синхронизации. При установке соединения происходит обмен публичными ключами маршутизаторов, на основе которых по алгоритму Диффи-Хельмана вычисляется общий ключ для AES шифрования каждый на своей стороне.
Тоннели
Тоннели всегда однонаправленные — все сообщения могут передаваться только от входного узла (Gateway) к выходному узлу (Endpoint). В зависимости от того какой конец тоннеля принадлежит его владельцу, обладающему всей полнотой информации о тоннеле, тоннели делятся на входящие (владелец — выходной узел) и исходящие (владелец — входной узел). Промежуточным узлам тоннеля неизвестно, является ли тоннель входящим или исходящим, единственное действие, осуществляемое промежуточным узлом, это шифрование сообщения своим ключом шифрования и передача следующему узлу. Отсюда вытекает важное следствие: последовательное расшифрование сообщений тоннеля должно осуществляться его владельцем, поскольку только у владельца есть ключи шифрования всех промежуточных узлов. Данный факт достаточно тривиален для входящих тоннелей, т. е. получив сообщение выходной узел должен его последовательно расшифровать, однако для исходящих тоннелей оригинальное незашифрованное сообщение должно быть последовательно расшифровано перед его отправкой. Тоннели, для которых данный узел не является владельцем, называются транзитными. Транзитные тоннели передают чужой трафик и необходимы для поддержки функционирования всей сети I2P, тем самым превращая узел в маршутизатор. Узлы тоннелей используют AES шифрование с тремя различными ключами: один используется для шифрования ответа узла при создании тоннеля, а два других для передачи данных через тоннель: один ключ шифрует сами данные, а другой шифрует вектор ипициализации (IV) для шифрования данных. При этом IV шифруется тем же самым ключом дважды: до шифрования и после, называется это двойным шифрованием (double encryption). Узел получает эти два ключа в относящейся к нему записи сообщения создания тоннеля, зашифрованной его публичным ключом с помощью Эль-Гамаля.
Внутри тоннелей передаются исключительно сообщения TunnelData, вообще говоря состоящие из нескольких фрагментов. Для передачи между тоннелями используется сообщение TunnelGateway. Хотя в официальной документации написано, что для двустороннего соединения необходимо как минимум 4 тоннеля (2 входящих и 2 исходящих), на самом деле передавать сообщения через исходящие тоннели необязательно, а можно отправить сообщение TunnelGateway входному узлу нужного входяшего тоннеля.
В сообщении TunnelData контрольная сумма вычисляется из содержательных данных, следующих за нулевым байтом и присоединённым к нему не зашифрованным IV.
Протокол I2NP
Обмен данными внутри сети I2P происходит с помощью I2NP сообщений разных типов. Каждое сообщение содержит заголовок с его типом и длиной, позволяющий определить границы между сообщениями. В зависимости от типа длина сообщения может варьироваться от 20 до 64К байт. Каждый уровень использует сообщения- «обертки», содержащие внутри себя другие I2NP сообщения более высокого уровня. Для тоннелей такими «обертками» являются сообщения TunnelData для передачи внутри тоннелей и TunnelGateway для передачи между тоннелями. Для «чеснока» — Garlic. Большую часть I2P трафика представляют собой следующий вложенные сообщения:
Data->Garlic->TunnelData.
Как правило сообщения передаются через тоннели, хотя могут передаваться и напрямую между маршутизаторами, в частности для первоначального создания новых тоннелей. Также маршутизаторы обмениваются сообщениями DatabaseStore сразу после установки соединения. Сообщения между точками назначения следует передавать через «чеснок», поскольку соответствующее поле присутствует только там.
Маршутизаторы и точки назначения (destinations)
Для работы в сети I2P необходим I2P клиент, состоящий из маршутизатора, обеспечивающего доступ в сеть I2P, и точек назначения для обмена содержательной информацией. Информация о маршутизаторах в том числе и об их IP адресах является общедоступной, более того, актуальный список маршутизаторов можно скачать со специальных ftp-сайтов. В то же время информация о местоположении точек назначения является конфиденциальной. Информацией о точках назаначения, расположенном на данном маршутизаторе, располагает только данный маршутизатор, для все остальных получение этой информации не представляется возможным, что является одним из основных механизмов обеспечения анонимности сети I2P.
Поскольку маршутизаторы в основном располагаются на компьютерах участников сети, то их состав все время изменяется. Поэтому маршутизаторы вынужденны постоянно поддерживать свой список другим маршутизаторов в актуальном состоянии. Этот процесс называется «зондированием» (exploratory), заключающийся в посылке запросов со случайно выбранным 32-байтным адресом специальным маршутизаторам, называемых floodfill. Предполагается что floodfill-маршутизаторы обладают всей полнотой информации о сети. Помимо все прочего floodfill-маршутизаторы постоянно сообщают друг другу информацию о найденных новых узлах.
Для запроса информации об узле используются I2NP сообщение DatabaseLookup, а для передачи самой ифмации DatabaseStore. Как правило сообщения передаются через тоннели, однако DatabaseStore передается узлом напрямую на траспортном уровне сразу после установки соединения, тем сам сообщая сети о своем существовании. В противном случае построение тоннелей для новых узлов было бы невозможно.
DatabaseStore может содержать информацию двух видов, в случае если данному адресу соотвествуют структура RouterInfo, то адрес является маршутизатором, а если LeaseSet то точкой назначения.
RouterInfo содержит публичные ключи маршутизатора, а также разнообразную служебную информацию, наиболее важной из которой являются IP адреса, порты и поддерживаемые транспортные протоколы для соединения и сведения о том является ли даныый маршутизатор floodfill-ом или нет. Поскольку RouterInfo может содержать довольно много текстовой информации, то передается заархивированным gzip-ом.
LeaseSet, содержит список входящих туннелей данной точки назначения, а также публичный ключ для шифрования «чесночных» сообщений, предназначенной данной точке назначения.
Службы прикладного уровня
Рассмотрим содержательные действия I2P клиента: анонимный хостинг онлайн ресурсов, и, соответственно, доступ к ним. Для начала попробуем получить данные с некоторого веб-сайта, например, Флибусты. На данный момент у нас имеется только 32-байтный хэш ее I2P адреса, нашей целью же является отправка HTTP-запроса, и получение ответа.
Разумеется маршутизатор с таким адресом в базе отсутствует (иначе IP адрес ресурса был бы виден всем), поэтому единственный способ отправить запрос — это какой-нибудь входящий тоннель нужного узла, существующий в данный момент времени, для чего сначала следует запросить и получить LeaseSet. В отличие от RouterInfo, который можно запросить и получить с соседнего узла на транспортном уровне, LeaseSet можно запросить и получить только через туннели, которые предварительно нужно построить. Отсюда следует неутешительный вывод, что использовать I2P сеть «по требованию» не получится, I2P маршутизатор должен быть запущен и должен постоянно заниматься построением и поддержкой тоннелей. Из-за децентрализованности сети построение тоннелей весьма непростое дело — большинство попыток создания тоннелей оканчиваются неуспехом.
Для успешного построения тоннеля необходимо два условия:
- Все участвующие в тоннеле узлы должны быть доступны на транспортном уровне по крайней мере предыдущему узлу в тоннеле
- Все участвующие в тоннеле узлы должны быть согласны построить новый тоннель. Узел может отказать в создании тоннеля, например, в силу его загруженности
Максимальное время жизни тоннеля 10 минут, тоннель может прекратить свое существование и досрочно, если участвующий в тоннеле узел ушел в оффлайн. Поэтому владельцы тоннелей постоянно посылают тестовые сообщения чтобы поддерживать список «живых» тоннелей в актуальном состоянии.
Итак, тоннели имеются в наличии и необходимый LeaseSet имеется в наличии. Теперь можно отправить HTTP запрос и он даже достигнет адресата, однако нам желательно также и получить ответ. Для этого мы в нашем сообщение должны указать наш собственный LeaseSet, тогда ответ будет отправлен нам через какой нибудь входящий туннель и скорее всего благополучно достигнут нашего узла. Поскольку через наш узел может одновременно работать несколько соединений, то каждому из них должен быть либо назначен собственный I2P адрес и сформирован LeaseSet из нескольких входящих туннелей, либо создан «разделяемый» адрес, мультиплексирующий соединения, используя специальный протокол соответствующими полями, являющийся «оберткой» над протоколом прикладного уровня. Такой протокол называется I2CP и в официальном I2P клиента используется исколючительно он, хотя для построения собственных служб это необязательно. Разумеется для доступа к Флибусте следует использовать I2CP, посколько она ожидает именно его. Однако для построения к примеру собственной торрентоподобной сети можно обойтись только I2P адресацией.
Протокол I2CP и построенный поверх него стек протоколов является отдельной темой, которой посвящена отдельная статья.