# udpcheck: протокол сети узлов (v2) Цель: понять, режет ли провайдер UDP, в какую сторону и устойчиво ли это во времени. Много узлов у разных провайдеров периодически проверяют несколько опорных серверов (в протоколе и в API они называются целями, `target`); результаты копятся на хабе. Обычному узлу не нужны имена, сертификаты и открытые порты. Узел, у которого порты всё же открыты, становится ещё и опорным сервером. ## Роли | Роль | Что делает | Что ей нужно | |---|---|---| | **Хаб** (`dagart.xyz`) | Регистрирует узлы, раздаёт задания, принимает результаты, хранит и отдаёт статистику | домен, сертификат (Caddy), SQLite | | **Опорный сервер / цель** (серверы проекта) | Отвечает на подписанные UDP-пакеты и TCP-запросы, шлёт хабу счётчики «что дошло» | публичный IP, UDP/TCP-порты | | **Узел** (ПК, роутер, VPS у добровольца) | Раз в N минут получает задание, шлёт поток на цели, отправляет результат | только исходящие соединения | | **Опорный сервер участника** (узел с `--ref`) | То же, что цель, но только для сравнения: в вывод по замеру не входит | публичный IPv4, UDP 47321, 47322, TCP 47321 | Узел ходит только на хаб по HTTPS (443) и на IP целей из задания (UDP/TCP). Хаб к узлу не подключается (кроме одной проверки доступности опорного сервера, см. ниже). ## Поток замера 1. Узел: `POST /api/v2/node/poll` (long-poll до 25 с). Хаб создаёт задание, если пришло время (раз в 15 минут со сдвигом) или нажата «проверить сейчас». 2. Задание: список целей (код, IP, порты) и для каждой `taskid` (8 байт) и `mac` (4 байта). `mac = HMAC-SHA256(секрет_цели, "UDPC2" + taskid)[:4]`. Узел сам цели выбрать не может. 3. Узел шлёт на каждый порт каждой цели 100 пакетов по 1200 байт (22-байтный заголовок + заполнитель). Цель проверяет `mac`, отвечает пакетом той же длины, запоминает номера дошедших. 4. Через ~4 с тишины цель сообщает хабу: `POST /api/v2/target/report` с подписью HMAC тела. 5. Узел присылает хабу, сколько ответов получил: `POST /api/v2/node/result`. 6. Хаб сводит «дошло на цель» и «вернулось на узел» по каждому порту, считает класс и вывод, пишет в БД, отвечает узлу итогом. ## Опорные серверы участников Установщик запускает агент с `--ref` (отключается `UDPCHECK_REF=0`). Агент занимает порты UDP 47321, 47322 и TCP 47321 (каких-то может не хватить: берём свободные) и работает как цель по заданиям хаба: 1. `POST /api/v2/node/ref` (токен узла): `{udp_ports, tcp_port, verify}`. Адрес берётся из соединения, указать чужой нельзя. Порты 1024..65535, не больше трёх UDP. Один адрес держит один опорный сервер. Хаб выдаёт `code` (`REF-`) и секрет. 2. С `verify=true` хаб сам шлёт подписанные UDP-пакеты на заявленные порты и один TCP-запрос; ответили, значит сервер «проверен». Ответ должен прийти с того же адреса. Проверку агент повторяет раз в 3 часа и после смены адреса (`ref_verified:false` в ответе на `node/poll`); при неудаче интервал растёт до 6 часов. 3. В каждое задание хаб добавляет до двух проверенных опорных серверов участников, которые на связи: не свой, не с адресом узла и не из его сети (ASN). Поле цели `community:true`. 4. Опорный сервер отвечает на подписанные пакеты и шлёт отчёт `POST /api/v2/target/report` с заголовками `X-Target: REF-`, `X-Auth: HMAC(секрет, тело)`. Секрет у каждого свой, у хаба хранится в SQLite. 5. Результаты по таким серверам пишутся в БД (`tres`, `meas`, цель `REF-`), но **в вывод по замеру и в статистику по провайдерам не входят**, а в публичном списке узлов показываются только как число проверок за сутки и доля чистых. Трассировки до адреса посетителя серверам участников не передаются. **Проверка посетителя из браузера.** Хаб добавляет в `GET /api/v2/web/stun` до двух проверенных серверов участников (на связи, не из сети посетителя, лучше у других хостеров, чем наши). Агент отвечает на STUN Binding Request на своих UDP-портах (не больше 60 запросов за 10 секунд на адрес). Такие серверы это независимые свидетели: если до серверов проекта UDP не идёт, а до сервера участника идёт, вывод смягчается до «выборочно» (`FOREIGN_PARTIAL`); если до сервера участника не идёт, а до серверов проекта идёт, это не блокировка; «режется» (`FOREIGN_IN_CUT`) решают только серверы проекта. В стране, где серверов проекта нет, сервер участника из этой страны становится контролем «своя страна». Подтверждения приёма запросов (`stunseen`) даёт только проект: у серверов участников направление потери не доказывается. Если серверы увидели посетителя с разных внешних адресов (раздельная маршрутизация, VPN, прокси), проверка помечается ненадёжной и в статистику не идёт. Сервер участника видит IP узлов, которые его проверяют, а узлы получают его IP в задании. Ответ ограничен: только на подписанные пакеты, не больше размера запроса, не больше 1500 ответов на сессию и 500 в секунду. ## Роли машины Агент работает в одной из ролей (`--role node|ref|both`, поле `role` в опросе хаба): **узел** сам шлёт пакеты и проверяет свою сеть (домашний ПК, роутер, машина за NAT), **опорный сервер** только отвечает на проверки (VPS и серверы с публичным IPv4), заданий на проверку хаб ему не выдаёт. Установщик выбирает роль сам: хаб отдаёт запрашивающему его адрес (`GET /api/v2/ip`), если такой адрес есть на интерфейсах машины, это сервер (`ref`), иначе `node`; OpenWrt всегда `node`. Вручную: `UDPCHECK_ROLE=node|ref|both`. IP опорного сервера участника хаб выдаёт только узлам, работающим не меньше суток и сделавшим не меньше 3 замеров, и не больше 5 разных серверов в сутки на узел. ## Подтверждение приёма запросов серверами (браузерная проверка) После STUN-проверки браузер просит `POST /api/v2/web/stunseen`; серверы получают задание (вместе с опросом хаба), отвечают, сколько STUN-запросов с адреса посетителя они получили за последнюю проверку (в памяти, 2 минуты), и браузер видит, дошли ли запросы до каждого сервера. Если запросы дошли, а ответы нет, потерян путь к посетителю (входящее); если запросы не дошли, режется исходящее. Адрес посетителя получают только серверы проекта. ## Страны: «своя страна» и «зарубеж» У каждого опорного сервера есть страна (`country` в targets.json, иначе по базе IP). Для узла и для посетителя сайта хаб сравнивает её со страной проверяемого: тот же код страны означает `kind: home` («своя страна», контроль), другой означает `foreign`. Вывод строится так: контроль в своей стране должен быть чистым, зарубежные серверы сравниваются с ним. Если в стране проверяемого нет ни одного опорного сервера проекта, вывод `NO_ANCHOR` («не на что опираться») и просьба добавить сервер. Пока опорных серверов проекта в стране нет, проверенные серверы участников из этой страны (из разных сетей) становятся контролем (`control: true`): число дошедших пакетов для них берётся не меньше числа ответов, реально вернувшихся узлу, так что такой сервер может только забраковать замер (`CONTROL_BAD`), но не обвинить провайдера. Старое значение `kind: ru` читается как `home`. ## Формат UDP-пакета (v2) `!4s B B 8s I 4s` = 22 байта: `UDPC`, версия=2, флаг (0 запрос, 1 ответ), `taskid`, `seq`, `mac`. ## Классы и вывод Порт: по доле доставленных пакетов в каждую сторону. «Дошло на цель» / отправлено и «вернулось» / дошло на цель. Менее 5% считается обрывом, 5–50% частичным, дальше: потери менее 1% чисто, 1–5% небольшие, более 5% плохо. Небольшие потери блокировкой не считаются. Статус цели: `OK`, `INCUT` (на цель доходит, ответы нет), `OUTCUT` (на цель не доходит), `PART` (на части портов проходит), `MIXED`, `NOREPORT` (цель не прислала счётчики). Вывод по замеру сравнивает контроль в РФ с зарубежными целями в один момент: `NO_BLOCK`, `FOREIGN_IN_CUT`, `FOREIGN_PARTIAL`, `FOREIGN_OUT_CUT`, `CONTROL_BAD`, `CONTROL_DOWN`, `NO_FOREIGN`. ## Что хранится и что публикуется - **Не хранится:** IP узлов (только в памяти на время замера, чтобы проверить, что UDP и HTTPS шли с одного адреса), имя пользователя, имя компьютера, пути. - **Хранится:** для опорных серверов участников ещё IP, порты и секрет (таблица `refs`), пока узел не удалён; токен узла (только хэш), позывной, ASN и страна провайдера (по базе DB-IP), город приблизительный, результаты по портам и целям, время. Сырые данные 60 дней. - **Публикуется:** агрегаты по ASN (число узлов, число замеров, доли выводов по часам) и список узлов: позывной, ASN, страна, система, на связи ли, число замеров, последний итог, признак опорного сервера. Адресов и городов нет. ## Защита от злоупотреблений - Задания только из белого списка (UDP-поток и TCP-контроль на цели хаба). Адреса целей задаёт хаб, а не узел. - Лимиты: регистрация, опрос, «проверить сейчас», публичная статистика. - Цели отвечают пакетом той же длины, только на подписанные пакеты, с ограничением числа ответов. - Токен узла хранится в виде хэша. Результаты узла не проверяются на честность, поэтому в выводах учитывается число узлов на ASN. ## Версии Протокол v1 (`/udpcheck/api/v1`, `udpcheck_probe.bat`) удалён (2026-10-08); остался только `/api/v1/health` для проверки, что процесс жив. ## Длина пакета в браузерной проверке Кроме обычных STUN-портов (777, 47321, ответ ~32 байта) серверы проекта слушают порты длинных ответов (по умолчанию UDP 3478 -> 600 байт и UDP 19302 -> 1200 байт, переменная `UDPCHECK_PAD_PORTS="порт:длина,..."`, пусто = выключено). Ответ дополняется необязательным STUN-атрибутом 0xC057 со случайными байтами; браузеры его пропускают. Чтобы сервер не стал усилителем трафика, на этих портах он отвечает только адресам, которые разрешил хаб: `GET /api/v2/web/stun` создаёт задание `pad` (адрес посетителя подтверждён по TCP), цели получают его при опросе и подтверждают в следующем (`ack`); ответ хаба содержит `pad: [{port, size}]` только для подтвердивших целей. Разрешение живёт 120 с, не больше 150 длинных ответов в минуту на адрес. Цель сообщает хабу занятые порты в теле опроса (`pad`). Страница проверяет длинные порты вторым этапом, после обычных, и показывает результат отдельно; в общий вывод и в статистику он не входит.