Обзор
BEP 6 (расширение Fast Extension) включает пять функций: Have All / Have None, Reject Requests, Suggestions и Allowed Fast. Протокол передачи данных — бит согласования, идентификаторы сообщений и семантика блокировки — не зависит от транспорта и работает без изменений поверх I2P-потоков. Единственная часть BEP 6, которую нельзя напрямую сопоставить с I2P, — это генерация набора Allowed Fast, поскольку она определяется через IPv4-адрес пира. Пиры I2P не имеют IP-адресов; они идентифицируются хэшами назначения длиной 32 байта.
Данный стандарт предлагает унифицированный способ формирования набора разрешённых быстрых соединений (Allowed Fast), нативный для I2P, чтобы все торрент-клиенты I2P генерировали идентичные наборы разрешённых быстрых соединений для одного и того же пира и торрента, что делает эту функцию полезной (и проверяемой) в разных реализациях.
Мотивация
Новым пирам необходимы первые несколько кусочков, прежде чем механизм BitTorrent’а «за кусок — кусок» сможет набрать обороты. В I2P этот процесс происходит медленнее, чем в открытой сети: установка соединения и доставка кусочков проходят через несколько высокозадержечных туннелей, из-за чего интервал между подключением и первым взаимным разблокированием становится длиннее. Возможность «Быстрой раздачи» напрямую решает эту проблему — стартующему пирам разрешается получить небольшое количество кусочков, даже будучи заблокированным, получать данные сразу и начинать взаимный обмен раньше.
Справочный BEP 6 вычисляет разрешённый быстрый набор на основе IPv4-адреса пира, чтобы гарантировать, что отправитель может выбирать фрагменты, уникальные для получателя (один пользователь с множеством IP не может собрать много наборов). В I2P хеш назначения пира выполняет ту же привязывающую функцию и доступен обоим концам каждого соединения, что делает набор детерминированным и локально проверяемым — чего схема, основанная на IP, предложить не может.
Модификации BEP 6
Согласование расширения Fast и все четыре типа сообщений принимаются без изменений:
- Переговоры: третий наименее значащий бит последнего зарезервированного байта,
reserved[7] |= 0x04, с обеих сторон - Have All
<len=0x0001><op=0x0E>, Have None<len=0x0001><op=0x0F> - Suggest Piece
<len=0x0005><op=0x0D><index> - Reject Request
<len=0x000D><op=0x10><index><begin><length> - Allowed Fast
<len=0x0005><op=0x11><index> - Каждый запрос приводит ровно к одному ответу (piece или reject); choke больше не отклоняет неявно ожидающие запросы
Единственное отклонение заключается в генерации набора разрешённых быстрых соединений, где байты IP-адреса заменяются байтами хэша назначения пира.
Отклонение: хеш-байты вместо маскированного IP
См. BEP 6, шаг (1):
x = 0xFFFFFF00 & ip
Это занимает три байта IPv4-адреса пира и обнуляет 4-й байт. Это эвристика подсети: пользователи, которые могут получить несколько IP-адресов в одной и той же подсети /24, не должны получать несколько наборов разрешённых быстрых узлов.
Наша версия I2P заменяет это на первые четыре байта хэша назначения узла длиной 32 байта:
x = first 4 bytes of peer destination hash
Отличие от эталонной реализации:
«Это 3 байта IP-адреса, за которыми следует ноль. Вы — 4 байта хэша. Отличается от BEP 6, потому что здесь нет IP-адреса, и 4-й байт не обнуляется.»
Оба конца соединения I2P уже знают хэш назначения узла (это адрес, к которому/от которого было установлено соединение), поэтому не требуется дополнительный обмен, обнаружение NAT или определение внешнего IP-адреса — ничего из этого в I2P не существует.
Разрешенный быстрый алгоритм генерации
Пусть hash — это 32-байтовый хэш назначения принимающего узла, infohash — 20-байтовый infohash торрента, sz — количество частей в торренте, k — конечное количество частей в разрешённом быстром наборе (10, как в BEP 6), а a — выходной набор:
x = hash[0:4] ++ infohash (1)
while |a| < k:
x = SHA1(x) (2)
for i in [0:5] and |a| < k: (3)
y = x[i*4 : i*4+4] (4)
index = y % sz (5)
if index not in a: (6)
add index to a (7)
Примечания:
- 4 байта хэша назначения заменяют 3 замаскированных байта IP-адреса. Все четыре байта содержат энтропию хэша; ни один из них не обнуляется.
- Как и в BEP 6, цепочка SHA1 генерирует длинную псевдослучайную последовательность, разделяемую на индексы кусков; значение
k = 10соответствует значению по умолчанию в эталонной реализации. - Сообщение Allowed Fast носит рекомендательный характер: получатель НЕ ДОЛЖЕН интерпретировать его как признак наличия у отправителя куска — только как указание, что отправитель будет раздавать этот кусок, даже находясь в состоянии подавления (choked).
Преимущества
| Область | Преимущество |
|---|---|
| Задержка запуска | Новые пиры получают первые части, находясь в состоянии блокировки, что сокращает медленный процесс tit-for-tat, особенно затягивающийся при использовании многошаговых туннелей I2P |
| Детерминированность | Набор — это чистая функция от хеша назначения + infohash, поэтому любая реализация вычисляет один и тот же набор — в отличие от IP-ориентированного BEP 6, где представление отправителя об IP-адресе получателя может отличаться (из-за NAT) |
| Проверяемость | Получающий пириент знает собственный хеш назначения и может локально пересчитать и проверить набор, обнаруживая недобросовестных отправителей |
| Отсутствие IP-механизмов | Не требуется преодоление NAT, определение внешнего IP, эвристики подсетей — всё это невозможно или бессмысленно в I2P |
| Привязка к идентификатору | Разрешён только один быстрый набор на одно назначение. Пользователь с множеством адресов получает по одному набору на каждый — та же защита от злоупотреблений, которую IP-маска обеспечивает в открытом интернете |
| Конфиденциальность | Ни при вычислениях, ни в передаче никогда не участвует IP-адрес |
| Пропускная способность | «Имею всё» / «Имею ничего» заменяет полное битовое поле в крупных торрент-файлах; команда Reject устраняет избыточные повторные запросы |
Соображения по реализации
- Идентификация пира: хэш назначения пира берётся из потокового соединения (назначение сессии) и является одинаковым значением для обеих сторон. Для исходящих соединений используйте назначение, с которым установили соединение; для входящих — назначение, откуда пришло соединение.
- Согласование: отправляйте
reserved[7] |= 0x04при рукопожатии; посылайте сообщения расширения Fast только если у пира в рукопожатии также установлен этот бит; если пир присылает сообщения Fast без согласования, закрывайте соединение. - Have All / Have None: сразу после рукопожатия отправляйте ровно одно из: битовое поле / Have All / Have None. Используйте Have All для сидов, Have None — до получения первого блока.
- Сторона отправки Allowed Fast: объявляйте только те блоки, которые у вас действительно есть; получатель может запрашивать их даже при подавлении. Ограничивайте обслуживаемый набор (например, отклоняйте запросы allowed-fast от пира, у которого уже больше
kблоков, согласно рекомендациям BEP 6). - Сторона получения Allowed Fast: сохраняйте полученный набор; разрешайте запросы на эти блоки даже при подавлении; опционально проверяйте набор, пересчитывая его из собственного хэша назначения и infohash, и игнорируйте блоки, отсутствующие в вычисленном наборе.
- Reject: каждый запрос ДОЛЖЕН получить ровно один ответ; при подавлении отклоняйте всё, что не входит в набор allowed fast, вместо того чтобы молча игнорировать пира.
- Размер набора: для совместимости используйте
k = 10; пиры могут выбирать меньшееkпри высокой нагрузке, но обе стороны должны объявлять только то, что реально готовы отдавать. - Ограничение по блокам:
index = y % szдолжен использовать общее количество блоков в торрентеsz; игнорируйте индексы >= sz (защитная мера), поскольку цепочка хэшей не ограничивается диапазоном блоков. - Обратная совместимость: клиенты, не согласующие бит fast, просто никогда не видят этих сообщений; другие изменения протокола не требуются.
Референсные реализации
Алгоритм небольшой и автономный — всего несколько десятков строк на любом языке. Все три приведённых ниже примера вычисляют одинаковый набор при одинаковых входных данных (hash[0:4] ++ infohash, цепочка SHA1, y % sz, с ограничением k = 10).
Java
// I2P: peer.getPeerID().getDestHash() is the 32-byte destination hash.
// Big-endian word reads build each candidate piece index from the SHA1 chain.
import java.security.MessageDigest;
import java.util.HashSet;
import java.util.Set;
public static Set<Integer> generateAllowedFastSet(byte[] destHash, byte[] infohash, int pieces) {
Set<Integer> rv = new HashSet<>(10);
if (destHash == null || infohash == null || pieces <= 0) {
return rv;
}
byte[] x = new byte[24];
System.arraycopy(destHash, 0, x, 0, 4); // 4 hash bytes, no IP, no zeroed 4th byte
System.arraycopy(infohash, 0, x, 4, Math.min(20, infohash.length));
MessageDigest md = MessageDigest.getInstance("SHA-1");
while (rv.size() < 10) {
x = md.digest(x);
for (int i = 0; i < 5 && rv.size() < 10; i++) {
long y = ((x[i * 4] & 0xFFL) << 24) | ((x[i * 4 + 1] & 0xFFL) << 16)
| ((x[i * 4 + 2] & 0xFFL) << 8) | (x[i * 4 + 3] & 0xFFL);
rv.add((int) (y % pieces));
}
}
return rv;
}
C++
// Peer identity input is the 32-byte destination hash available on the connection.
#include <cstdint>
#include <set>
#include <vector>
extern std::vector<uint8_t> sha1(const std::vector<uint8_t>& in); // e.g. OpenSSL SHA1()
std::set<int> generate_allowed_fast_set(const std::vector<uint8_t>& dest_hash,
const std::vector<uint8_t>& infohash,
int pieces) {
std::set<int> rv;
if (dest_hash.size() < 4 || infohash.size() < 20 || pieces <= 0) { return rv; }
std::vector<uint8_t> x(dest_hash.begin(), dest_hash.begin() + 4); // 4 hash bytes,
// no IP mask
x.insert(x.end(), infohash.begin(), infohash.begin() + 20);
while (rv.size() < 10) {
x = sha1(x);
for (int i = 0; i < 5 && rv.size() < 10; i++) {
uint32_t y = (uint32_t(x[i * 4]) << 24) | (uint32_t(x[i * 4 + 1]) << 16) |
(uint32_t(x[i * 4 + 2]) << 8) | uint32_t(x[i * 4 + 3]);
rv.insert(int(y % uint32_t(pieces)));
}
}
return rv;
}
Python
import hashlib
def generate_allowed_fast_set(dest_hash: bytes, infohash: bytes, pieces: int) -> set:
"""4 bytes of the destination hash stand in for the masked IP; no byte is zeroed."""
rv = set()
if len(dest_hash) < 4 or len(infohash) < 20 or pieces <= 0:
return rv
x = dest_hash[:4] + infohash[:20]
while len(rv) < 10:
x = hashlib.sha1(x).digest()
for i in range(5):
if len(rv) >= 10:
break
y = int.from_bytes(x[i * 4 : i * 4 + 4], "big")
rv.add(y % pieces)
return rv
Совместимость
- Совместимо по протоколу: бит переговоров и форматы сообщений байт-идентичны clearnet BEP 6; различается только входные данные для генерации набора.
- Не взаимодействует между сетями: клиент I2P и клиент обычной сети всё равно не могут соединиться друг с другом; отклонение затрагивает только байты идентичности пира, но никогда — формат передачи по проводу.
- Внутри I2P: любой клиент, реализующий это предложение, вычисляет идентичные разрешённые быстрые наборы и может одинаково обслуживать и проверять их. Клиенты, игнорирующие Allowed Fast, просто воспринимают его как рекомендательную команду без действия и теряют только преимущество быстрого запуска.
Открытые вопросы
- Должен ли размер набора
kоставаться фиксированным на уровне 10 или быть адаптивным в зависимости от нагрузки (например, меньше при высокой нагрузке запросов), как это разрешено в BEP 6? - Должны ли получатели проверять набор по собственному хешу назначения и отбрасывать несовпадающие индексы (защита от неисправных или вредоносных отправителей)? Рекомендуется: да.
- Выбрать 4-байтовый префикс (байты 0–3), как показано, или последние 4 байта — любое фиксированное 4-байтовое окно обеспечивает одинаковые свойства; использование префикса сохраняет естественный порядок байтов в опорном коде (
hash[0:4]).
Предшествующий уровень техники
- Справка: Расширение BEP 6 Fast
- Референсная реализация I2PSnark:
PeerState.sendAllowedFast()/generateAllowedFastSet()вapps/i2psnark/java/src/org/klomp/snark/PeerState.java(@since 0.9.71+) - Работает совместно с BEP 40 (канонический приоритет пиров) и BEP 21 (частичные сиды), оба поддерживаются I2PSnark