558 lines
24 KiB
Rust
558 lines
24 KiB
Rust
//! # P2P Сетевой модуль
|
||
//!
|
||
//! Распространение подписей присутствия между узлами.
|
||
//!
|
||
//! ## Русский код
|
||
//! Все идентификаторы на русском, как будто писал дух русского языка.
|
||
|
||
use montana_crypto::{sha3_256 as хеш256, secure_random_bytes as случайные_байты};
|
||
use montana_acp::PresenceProof as ДоказательствоПрисутствия;
|
||
use std::collections::{HashMap, HashSet, VecDeque};
|
||
use std::net::IpAddr;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ЛИМИТЫ СОЕДИНЕНИЙ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Лимиты соединений
|
||
/// Защита от перегрузки и Eclipse-атак
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct ЛимитыСоединений {
|
||
/// Максимум входящих соединений
|
||
pub максимум_входящих: usize,
|
||
|
||
/// Максимум исходящих соединений
|
||
pub максимум_исходящих: usize,
|
||
|
||
/// Максимум соединений на подсеть /16
|
||
pub максимум_на_подсеть: usize,
|
||
|
||
/// Минимум различных подсетей
|
||
pub минимум_подсетей: usize,
|
||
}
|
||
|
||
impl Default for ЛимитыСоединений {
|
||
fn default() -> Self {
|
||
Self {
|
||
максимум_входящих: 117,
|
||
максимум_исходящих: 11,
|
||
максимум_на_подсеть: 2,
|
||
минимум_подсетей: 4,
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// СЕТЕВОЙ АДРЕС
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Сетевой адрес узла
|
||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||
pub struct СетевойАдрес {
|
||
/// IP адрес
|
||
pub адрес: IpAddr,
|
||
|
||
/// Порт
|
||
pub порт: u16,
|
||
|
||
/// Временная метка последнего контакта
|
||
pub метка_времени: u64,
|
||
|
||
/// Сервисы узла
|
||
pub сервисы: u64,
|
||
}
|
||
|
||
impl СетевойАдрес {
|
||
/// Создать новый адрес
|
||
pub fn новый(адрес: IpAddr, порт: u16) -> Self {
|
||
let метка_времени = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_secs();
|
||
|
||
Self {
|
||
адрес,
|
||
порт,
|
||
метка_времени,
|
||
сервисы: 0,
|
||
}
|
||
}
|
||
|
||
/// Проверить роутабельность адреса
|
||
pub fn маршрутизируемый(&self) -> bool {
|
||
match self.адрес {
|
||
IpAddr::V4(ip) => {
|
||
!ip.is_private()
|
||
&& !ip.is_loopback()
|
||
&& !ip.is_link_local()
|
||
&& !ip.is_broadcast()
|
||
&& !ip.is_documentation()
|
||
&& !ip.is_unspecified()
|
||
}
|
||
IpAddr::V6(ip) => {
|
||
!ip.is_loopback() && !ip.is_unspecified()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Получить ключ группы (для бакетирования)
|
||
pub fn ключ_группы(&self) -> [u8; 4] {
|
||
match self.адрес {
|
||
IpAddr::V4(ip) => {
|
||
let октеты = ip.octets();
|
||
[октеты[0], октеты[1], 0, 0]
|
||
}
|
||
IpAddr::V6(ip) => {
|
||
let сегменты = ip.segments();
|
||
let байты1 = сегменты[0].to_be_bytes();
|
||
let байты2 = сегменты[1].to_be_bytes();
|
||
[байты1[0], байты1[1], байты2[0], байты2[1]]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// МЕНЕДЖЕР АДРЕСОВ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Менеджер адресов
|
||
/// Защита от Eclipse через криптографическое бакетирование
|
||
pub struct МенеджерАдресов {
|
||
/// Новые адреса (непроверенные)
|
||
новые: HashMap<usize, Vec<СетевойАдрес>>,
|
||
|
||
/// Проверенные адреса
|
||
проверенные: HashMap<usize, Vec<СетевойАдрес>>,
|
||
|
||
/// Секретный ключ для бакетирования
|
||
секретный_ключ: [u8; 32],
|
||
|
||
/// Количество новых бакетов
|
||
количество_новых_бакетов: usize,
|
||
|
||
/// Количество проверенных бакетов
|
||
количество_проверенных_бакетов: usize,
|
||
|
||
/// Размер бакета
|
||
размер_бакета: usize,
|
||
}
|
||
|
||
impl МенеджерАдресов {
|
||
/// Создать новый менеджер
|
||
pub fn новый() -> Self {
|
||
Self {
|
||
новые: HashMap::new(),
|
||
проверенные: HashMap::new(),
|
||
секретный_ключ: случайные_байты(),
|
||
количество_новых_бакетов: 1024,
|
||
количество_проверенных_бакетов: 256,
|
||
размер_бакета: 64,
|
||
}
|
||
}
|
||
|
||
/// Вычислить бакет для адреса
|
||
fn вычислить_бакет(&self, адрес: &СетевойАдрес, источник: &СетевойАдрес, новый: bool) -> usize {
|
||
let mut данные = Vec::new();
|
||
данные.extend_from_slice(&self.секретный_ключ);
|
||
данные.extend_from_slice(&адрес.ключ_группы());
|
||
данные.extend_from_slice(&источник.ключ_группы());
|
||
|
||
let хеш = хеш256(&данные);
|
||
let количество = if новый { self.количество_новых_бакетов } else { self.количество_проверенных_бакетов };
|
||
|
||
(u64::from_le_bytes(хеш[0..8].try_into().unwrap()) as usize) % количество
|
||
}
|
||
|
||
/// Добавить адрес в таблицу новых
|
||
pub fn добавить_новый(&mut self, адрес: СетевойАдрес, источник: &СетевойАдрес) -> bool {
|
||
if !адрес.маршрутизируемый() {
|
||
return false;
|
||
}
|
||
|
||
let бакет = self.вычислить_бакет(&адрес, источник, true);
|
||
let адреса_бакета = self.новые.entry(бакет).or_insert_with(Vec::new);
|
||
|
||
if адреса_бакета.len() >= self.размер_бакета {
|
||
if let Some(индекс_старого) = адреса_бакета
|
||
.iter()
|
||
.enumerate()
|
||
.min_by_key(|(_, а)| а.метка_времени)
|
||
.map(|(и, _)| и)
|
||
{
|
||
адреса_бакета.remove(индекс_старого);
|
||
}
|
||
}
|
||
|
||
if !адреса_бакета.contains(&адрес) {
|
||
адреса_бакета.push(адрес);
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
/// Отметить адрес как хороший
|
||
pub fn отметить_хорошим(&mut self, адрес: &СетевойАдрес) {
|
||
for адреса_бакета in self.новые.values_mut() {
|
||
if let Some(позиция) = адреса_бакета.iter().position(|а| а == адрес) {
|
||
адреса_бакета.remove(позиция);
|
||
break;
|
||
}
|
||
}
|
||
|
||
let пустой_источник = СетевойАдрес::новый(адрес.адрес, 0);
|
||
let бакет = self.вычислить_бакет(адрес, &пустой_источник, false);
|
||
|
||
let адреса_бакета = self.проверенные.entry(бакет).or_insert_with(Vec::new);
|
||
|
||
if адреса_бакета.len() < self.размер_бакета && !адреса_бакета.contains(адрес) {
|
||
адреса_бакета.push(адрес.clone());
|
||
}
|
||
}
|
||
|
||
/// Выбрать адрес для подключения
|
||
pub fn выбрать_для_подключения(&self) -> Option<СетевойАдрес> {
|
||
let использовать_проверенные = rand::random::<bool>();
|
||
|
||
if использовать_проверенные {
|
||
self.выбрать_из_проверенных().or_else(|| self.выбрать_из_новых())
|
||
} else {
|
||
self.выбрать_из_новых().or_else(|| self.выбрать_из_проверенных())
|
||
}
|
||
}
|
||
|
||
/// Выбрать из проверенных
|
||
fn выбрать_из_проверенных(&self) -> Option<СетевойАдрес> {
|
||
if self.проверенные.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
let индекс_бакета = rand::random::<usize>() % self.количество_проверенных_бакетов;
|
||
self.проверенные.get(&индекс_бакета).and_then(|адреса| {
|
||
if адреса.is_empty() {
|
||
None
|
||
} else {
|
||
let индекс = rand::random::<usize>() % адреса.len();
|
||
Some(адреса[индекс].clone())
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Выбрать из новых
|
||
fn выбрать_из_новых(&self) -> Option<СетевойАдрес> {
|
||
if self.новые.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
let индекс_бакета = rand::random::<usize>() % self.количество_новых_бакетов;
|
||
self.новые.get(&индекс_бакета).and_then(|адреса| {
|
||
if адреса.is_empty() {
|
||
None
|
||
} else {
|
||
let индекс = rand::random::<usize>() % адреса.len();
|
||
Some(адреса[индекс].clone())
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Получить количество адресов
|
||
pub fn количество(&self) -> (usize, usize) {
|
||
let новых: usize = self.новые.values().map(|v| v.len()).sum();
|
||
let проверенных: usize = self.проверенные.values().map(|v| v.len()).sum();
|
||
(новых, проверенных)
|
||
}
|
||
}
|
||
|
||
impl Default for МенеджерАдресов {
|
||
fn default() -> Self {
|
||
Self::новый()
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// СТАТИСТИКА ПИРА
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Идентификатор пира
|
||
pub type ИдПира = [u8; 32];
|
||
|
||
/// Статистика пира
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct СтатистикаПира {
|
||
/// Количество валидных сообщений
|
||
pub валидных: u64,
|
||
|
||
/// Количество невалидных сообщений
|
||
pub невалидных: u64,
|
||
|
||
/// Время последнего сообщения
|
||
pub последнее_сообщение: u64,
|
||
|
||
/// Средняя задержка (мс)
|
||
pub средняя_задержка_мс: u64,
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// РАСПРОСТРАНЕНИЕ ПОДПИСЕЙ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Протокол распространения подписей
|
||
pub struct РаспространениеПодписей {
|
||
/// Очередь подписей для отправки
|
||
очередь_исходящих: VecDeque<ДоказательствоПрисутствия>,
|
||
|
||
/// Фильтр виденных подписей
|
||
виденные: HashSet<[u8; 32]>,
|
||
|
||
/// Статистика по пирам
|
||
статистика_пиров: HashMap<ИдПира, СтатистикаПира>,
|
||
|
||
/// Локальный пул подписей
|
||
локальный_пул: Vec<ДоказательствоПрисутствия>,
|
||
|
||
/// Максимум подписей в пуле
|
||
максимум_пула: usize,
|
||
}
|
||
|
||
impl РаспространениеПодписей {
|
||
/// Создать новый протокол
|
||
pub fn новый() -> Self {
|
||
Self {
|
||
очередь_исходящих: VecDeque::new(),
|
||
виденные: HashSet::new(),
|
||
статистика_пиров: HashMap::new(),
|
||
локальный_пул: Vec::new(),
|
||
максимум_пула: 10_000,
|
||
}
|
||
}
|
||
|
||
/// Обработать входящую подпись
|
||
pub fn при_подписи(&mut self, подпись: ДоказательствоПрисутствия, от: ИдПира, текущий_τ2: u64) -> bool {
|
||
let хеш_подписи = подпись.hash();
|
||
|
||
if self.виденные.contains(&хеш_подписи) {
|
||
return false;
|
||
}
|
||
self.виденные.insert(хеш_подписи);
|
||
|
||
if !подпись.verify(текущий_τ2) {
|
||
if let Some(статистика) = self.статистика_пиров.get_mut(&от) {
|
||
статистика.невалидных += 1;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if let Some(статистика) = self.статистика_пиров.get_mut(&от) {
|
||
статистика.валидных += 1;
|
||
статистика.последнее_сообщение = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_secs();
|
||
}
|
||
|
||
if self.локальный_пул.len() < self.максимум_пула {
|
||
self.локальный_пул.push(подпись.clone());
|
||
}
|
||
|
||
self.очередь_исходящих.push_back(подпись);
|
||
true
|
||
}
|
||
|
||
/// Получить следующую подпись для отправки
|
||
pub fn следующая_исходящая(&mut self) -> Option<ДоказательствоПрисутствия> {
|
||
self.очередь_исходящих.pop_front()
|
||
}
|
||
|
||
/// Зарегистрировать пира
|
||
pub fn зарегистрировать_пира(&mut self, ид_пира: ИдПира) {
|
||
self.статистика_пиров.insert(ид_пира, СтатистикаПира::default());
|
||
}
|
||
|
||
/// Удалить пира
|
||
pub fn удалить_пира(&mut self, ид_пира: &ИдПира) {
|
||
self.статистика_пиров.remove(ид_пира);
|
||
}
|
||
|
||
/// Получить пул
|
||
pub fn пул(&self) -> &[ДоказательствоПрисутствия] {
|
||
&self.локальный_пул
|
||
}
|
||
|
||
/// Очистить пул
|
||
pub fn очистить_пул(&mut self) {
|
||
self.локальный_пул.clear();
|
||
self.виденные.clear();
|
||
}
|
||
|
||
/// Получить статистику пира
|
||
pub fn статистика_пира(&self, ид_пира: &ИдПира) -> Option<&СтатистикаПира> {
|
||
self.статистика_пиров.get(ид_пира)
|
||
}
|
||
}
|
||
|
||
impl Default for РаспространениеПодписей {
|
||
fn default() -> Self {
|
||
Self::новый()
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ЗДОРОВЬЕ СЕТИ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Метрики здоровья сети
|
||
#[derive(Clone, Debug)]
|
||
pub struct ЗдоровьеСети {
|
||
/// Количество активных соединений
|
||
pub соединений: usize,
|
||
|
||
/// Количество различных подсетей
|
||
pub подсетей: usize,
|
||
|
||
/// Среднее время отклика (мс)
|
||
pub средняя_задержка_мс: u64,
|
||
|
||
/// Количество подписей за последний τ₂
|
||
pub подписей_за_τ2: usize,
|
||
}
|
||
|
||
impl ЗдоровьеСети {
|
||
/// Оценка здоровья сети (0.0 - 1.0)
|
||
pub fn оценка(&self) -> f64 {
|
||
let лимиты = ЛимитыСоединений::default();
|
||
|
||
let оценка_соединений = (self.соединений as f64 / 10.0).min(1.0);
|
||
let оценка_подсетей = (self.подсетей as f64 / лимиты.минимум_подсетей as f64).min(1.0);
|
||
let оценка_задержки = if self.средняя_задержка_мс == 0 {
|
||
0.5
|
||
} else {
|
||
(500.0 / self.средняя_задержка_мс as f64).min(1.0)
|
||
};
|
||
|
||
(оценка_соединений + оценка_подсетей + оценка_задержки) / 3.0
|
||
}
|
||
|
||
/// Сеть здорова?
|
||
pub fn здорова(&self) -> bool {
|
||
self.оценка() > 0.6
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// P2P СООБЩЕНИЯ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
/// Типы P2P сообщений
|
||
#[derive(Clone, Debug)]
|
||
pub enum P2PСообщение {
|
||
/// Рукопожатие
|
||
Версия {
|
||
версия: u32,
|
||
сервисы: u64,
|
||
метка_времени: u64,
|
||
одноразовый_номер: u64,
|
||
},
|
||
|
||
/// Подпись присутствия
|
||
Присутствие(ДоказательствоПрисутствия),
|
||
|
||
/// Адреса узлов
|
||
Адреса(Vec<СетевойАдрес>),
|
||
|
||
/// Запрос адресов
|
||
ЗапросАдресов,
|
||
|
||
/// Пинг
|
||
Пинг(u64),
|
||
|
||
/// Понг
|
||
Понг(u64),
|
||
}
|
||
|
||
impl P2PСообщение {
|
||
/// Получить тип сообщения
|
||
pub fn тип(&self) -> &'static str {
|
||
match self {
|
||
Self::Версия { .. } => "ВЕРСИЯ",
|
||
Self::Присутствие(_) => "ПРИСУТСТВИЕ",
|
||
Self::Адреса(_) => "АДРЕСА",
|
||
Self::ЗапросАдресов => "ЗАПРОС_АДРЕСОВ",
|
||
Self::Пинг(_) => "ПИНГ",
|
||
Self::Понг(_) => "ПОНГ",
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// СОВМЕСТИМЫЕ ПСЕВДОНИМЫ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
pub use СетевойАдрес as NetAddr;
|
||
pub use МенеджерАдресов as AddrManager;
|
||
pub use РаспространениеПодписей as SignatureGossip;
|
||
pub use ЗдоровьеСети as NetworkHealth;
|
||
|
||
// Совместимость: английские методы для экономического модуля
|
||
impl МенеджерАдресов {
|
||
pub fn new() -> Self { Self::новый() }
|
||
pub fn count(&self) -> (usize, usize) { self.количество() }
|
||
}
|
||
|
||
impl РаспространениеПодписей {
|
||
pub fn new() -> Self { Self::новый() }
|
||
}
|
||
|
||
impl ЗдоровьеСети {
|
||
pub fn connections(&self) -> usize { self.соединений }
|
||
pub fn netgroups(&self) -> usize { self.подсетей }
|
||
pub fn avg_latency_ms(&self) -> u64 { self.средняя_задержка_мс }
|
||
pub fn signatures_per_tau2(&self) -> usize { self.подписей_за_τ2 }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ТЕСТЫ
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
#[cfg(test)]
|
||
mod тесты {
|
||
use super::*;
|
||
use std::net::Ipv4Addr;
|
||
|
||
#[test]
|
||
fn тест_маршрутизируемости_адреса() {
|
||
let публичный = СетевойАдрес::новый(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 8333);
|
||
let приватный = СетевойАдрес::новый(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8333);
|
||
let локальный = СетевойАдрес::новый(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8333);
|
||
|
||
assert!(публичный.маршрутизируемый());
|
||
assert!(!приватный.маршрутизируемый());
|
||
assert!(!локальный.маршрутизируемый());
|
||
}
|
||
|
||
#[test]
|
||
fn тест_менеджера_адресов() {
|
||
let mut менеджер = МенеджерАдресов::новый();
|
||
let источник = СетевойАдрес::новый(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 8333);
|
||
let адрес = СетевойАдрес::новый(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 8333);
|
||
|
||
assert!(менеджер.добавить_новый(адрес.clone(), &источник));
|
||
|
||
let (новых, _) = менеджер.количество();
|
||
assert_eq!(новых, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn тест_здоровья_сети() {
|
||
let здоровье = ЗдоровьеСети {
|
||
соединений: 10,
|
||
подсетей: 5,
|
||
средняя_задержка_мс: 100,
|
||
подписей_за_τ2: 1000,
|
||
};
|
||
|
||
assert!(здоровье.здорова());
|
||
assert!(здоровье.оценка() > 0.6);
|
||
}
|
||
}
|