# INCY Developer Documentation — Full documentation > Technical documentation for integrating with the INCY app: subscription format, share-link parameters, full Xray configs, routing, deep links, HWID, Premium API, provider settings, and examples. Bilingual (RU default, EN copies live at /en/). # Главная Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/README.md # Документация для разработчиков Техническая документация по интеграции с приложением INCY. ## Содержание ### Подписки и серверы - [Формат подписки](subscription-format.md) — поддерживаемые форматы, протоколы, HTTP-заголовки - [Параметры share-ссылок](share-links.md) — VLESS, VMess, Trojan, Shadowsocks, Hysteria2, SOCKS5, WireGuard - [Полные Xray-конфигурации](full-xray-config.md) — конфиги с балансировщиками и обсерваториями - [Управление приложением](app-management.md) — HTTP-заголовки и параметры подписки (`hide-url`, баннер провайдера через `banner-*` заголовки, и др.) ### Маршрутизация - [Маршрутизация (Routing)](routing.md) — профили маршрутизации, геофайлы, правила, обрезка chunk-файлов - [Автообновляемая маршрутизация (Autorouting)](autorouting.md) — профили с автообновлением по URL ### Интеграция - [Deep Links](deep-links.md) — управление приложением через ссылки (включая шифрованные `incy://crypt1/` для анти-grep пересылок) - [HWID](hwid.md) — аппаратный идентификатор устройства - [Premium API](premium-api.md) — зашифрованный API конфигурации провайдера, лимиты устройств, резервные домены (`fallbackHosts`) ### Настройки провайдера - [Пресет-иконки](icon-presets.md) — иконки для ссылок в Lite Mode (бот / канал / поддержка) - [Админ-доступ по HWID](admin-hwids.md) — правка конфигов на устройстве + auto-approve push-уведомлений - [Push-уведомления провайдера](provider-notifications.md) — таргетинг, модерация, отмена ### Premium и биллинг - [Premium биллинг](premium-billing.md) — тарифы, способы оплаты, привязка аккаунта, управление подпиской - [VPN Аукцион](auction.md) — еженедельный аукцион за размещение в канале рекомендаций ### Примеры - [Примеры ссылок и параметров](examples.md) — готовые примеры для интеграции # Примеры Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/examples.md # Примеры ссылок и параметров Готовые примеры для интеграции маршрутизации и автообновляемой маршрутизации в подписки INCY. --- ## Deeplink-ссылки ### Статический профиль (base64) Добавляет и активирует профиль из base64-данных: ``` ://routing/onadd/eyJOYW1lIjoiUm9zY29tVlBOIiwiR2xvYmFsUHJveHkiOiJ0cnVlIiwiUmVtb3RlRE5TVHlwZSI6IkRvSCIsIlJlbW90ZUROU0RvbWFpbiI6Imh0dHBzOi8vY2xvdWRmbGFyZS1kbnMuY29tL2Rucy1xdWVyeSIsIlJlbW90ZUROU0lQIjoiMS4xLjEuMSIsIkRvbWVzdGljRE5TVHlwZSI6IkRvSCIsIkRvbWVzdGljRE5TRG9tYWluIjoiaHR0cHM6Ly9kbnMuZ29vZ2xlL2Rucy1xdWVyeSIsIkRvbWVzdGljRE5TSVAiOiI4LjguOC44IiwiRG9tYWluU3RyYXRlZ3kiOiJJUElmTm9uTWF0Y2gifQ== ``` Добавляет профиль без активации: ``` ://routing/add/eyJOYW1lIjoiUm9zY29tVlBOIn0= ``` ### Автообновляемый профиль (URL) Скачивает профиль по URL и устанавливает автообновление: ``` ://autorouting/onadd/https://raw.githubusercontent.com/user/repo/main/profile.json ``` Через `routing/onadd/` с URL (URL обнаруживается автоматически): ``` ://routing/onadd/https://raw.githubusercontent.com/user/repo/main/profile.json ``` ### GitHub blob URL Обычные GitHub-ссылки конвертируются автоматически: ``` ://autorouting/onadd/https://github.com/user/repo/blob/main/INCY/DEFAULT.JSON ``` Приложение автоматически преобразует в: ``` https://raw.githubusercontent.com/user/repo/main/INCY/DEFAULT.JSON ``` --- ## HTTP-заголовки подписки ### Autorouting — автообновляемый профиль ``` HTTP/2 200 content-type: text/plain autorouting: https://raw.githubusercontent.com/user/repo/main/profile.json vless://uuid@server1:443?security=tls&type=ws#Server 1 vless://uuid@server2:443?security=tls&type=ws#Server 2 ``` ### Routing — статический профиль (base64) ``` HTTP/2 200 content-type: text/plain routing: ewogICJOYW1lIjogIlJvc2NvbVZQTiIsCiAgIkdsb2JhbFByb3h5IjogInRydWUiCn0= vless://uuid@server1:443?security=tls#Server1 ``` ### Routing — статический профиль (полная ссылка) ``` HTTP/2 200 content-type: text/plain routing: ://routing/onadd/ewogICJOYW1lIjogIlJvc2NvbVZQTiIsCiAgIkdsb2JhbFByb3h5IjogInRydWUiCn0= vless://uuid@server1:443?security=tls#Server1 ``` ### Комбинация заголовков ``` HTTP/2 200 content-type: text/plain profile-title: My VPN support-url: https://t.me/support_bot profile-web-page-url: https://my-vpn.com subscription-userinfo: upload=0;download=536870912;total=10737418240;expire=1735689600 autorouting: https://raw.githubusercontent.com/user/repo/main/profile.json vless://uuid@server1:443?security=tls#NL vless://uuid@server2:443?security=tls#DE vless://uuid@server3:443?security=tls#FI ``` --- ## Тело подписки (body) ### Autorouting в body ``` vless://uuid@server1:443?security=tls#Server1 vless://uuid@server2:443?security=tls#Server2 ://autorouting/onadd/https://raw.githubusercontent.com/user/repo/main/profile.json ``` ### Routing в body ``` vless://uuid@server1:443?security=tls#Server1 vless://uuid@server2:443?security=tls#Server2 ://routing/onadd/ewogICJOYW1lIjogIlJvc2NvbVZQTiIsCiAgIkdsb2JhbFByb3h5IjogInRydWUiCn0= ``` ### Inline-метаданные в body (статический файл) Все метаданные через `#` комментарии — полезно при раздаче подписок как статических файлов (nginx), где нет возможности задать кастомные HTTP-заголовки: ``` #profile-update-interval: 1 #profile-title: Обход белых списков #support-url: https://t.me/+example #announce: base64:0J/Qu9Cw0L3QvtCy0L7QtSDQvtCx0YHQu9GD0LbQuNCy0LDQvdC40LU= #announce-url: https://example.com/news #profile-web-page-url: https://example.com vless://uuid@server1:443?security=tls#NL vless://uuid@server2:443?security=tls#DE ://autorouting/onadd/https://raw.githubusercontent.com/user/repo/main/profile.json ``` ### Комбинация заголовков и body-метаданных HTTP-заголовки имеют приоритет. Body-метаданные используются как fallback: ``` HTTP/2 200 content-type: text/plain profile-title: VPN Pro #support-url: https://t.me/support #profile-update-interval: 6 vless://uuid@server1:443?security=tls#NL vless://uuid@server2:443?security=tls#DE ``` В этом примере `profile-title` берётся из заголовка (`VPN Pro`), а `support-url` и `profile-update-interval` — из тела подписки. --- ## Примеры JSON-профилей ### Минимальный профиль ```json { "Name": "Minimal", "GlobalProxy": "true", "DomainStrategy": "AsIs" } ``` ### Профиль для обхода блокировок РФ ```json { "Name": "RoscomVPN", "GlobalProxy": "true", "RemoteDNSType": "DoH", "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", "RemoteDNSIP": "1.1.1.1", "DomesticDNSType": "DoH", "DomesticDNSDomain": "https://dns.google/dns-query", "DomesticDNSIP": "8.8.8.8", "Geoipurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "Geositeurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "DnsHosts": { "cloudflare-dns.com": "1.1.1.1", "dns.google": "8.8.8.8" }, "DirectSites": ["geosite:ru"], "DirectIp": ["geoip:ru", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255"], "ProxySites": [], "ProxyIp": [], "BlockSites": ["geosite:category-ads-all"], "BlockIp": [], "DomainStrategy": "IPIfNonMatch", "FakeDNS": "false" } ``` ### Профиль для Китая ```json { "Name": "China", "GlobalProxy": "true", "RemoteDNSType": "DoH", "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", "RemoteDNSIP": "1.1.1.1", "DomesticDNSType": "DoU", "DomesticDNSDomain": "", "DomesticDNSIP": "8.8.8.8", "Geoipurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "Geositeurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "DnsHosts": { "cloudflare-dns.com": "1.1.1.1" }, "DirectSites": ["geosite:cn", "geosite:geolocation-cn"], "DirectIp": ["geoip:cn", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255"], "ProxySites": ["geosite:cn"], "ProxyIp": ["geoip:amazon"], "BlockSites": ["geosite:ads"], "BlockIp": ["geoip:ads"], "DomainStrategy": "IPIfNonMatch", "FakeDNS": "false" } ``` --- ## Размещение геофайлов с хеш-проверкой Для оптимизации трафика размещайте SHA-256 хеш-файлы рядом с геофайлами: ``` https://example.com/geo/geoip.dat https://example.com/geo/geoip.dat.sha256 https://example.com/geo/geosite.dat https://example.com/geo/geosite.dat.sha256 ``` Содержимое `.sha256` файла — hex-строка SHA-256 хеша (64 символа): ``` 38c25fea171323e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4 ``` Генерация хеша: ```bash sha256sum geoip.dat | awk '{print $1}' > geoip.dat.sha256 sha256sum geosite.dat | awk '{print $1}' > geosite.dat.sha256 ``` Приложение скачивает `.sha256` перед полным файлом. Если хеш не изменился — скачивание полного файла пропускается (даже при ручном обновлении). > **Примечание:** Начиная с версии 2.0.3, базовые геофайлы Loyalsoldier вшиты в приложение. Первый запуск не требует загрузки из интернета. Обновление геофайлов происходит при обновлении подписки или вручную. --- ## Примеры share-ссылок с транспортами ### mKCP с кастомными MTU и TTI ``` vless://uuid@server:443?security=none&type=kcp&headerType=srtp&seed=myseed&mtu=1400&tti=20#mKCP Server ``` | Параметр | Значение | Описание | |---|---|---| | `type` | `kcp` | Транспорт mKCP | | `headerType` | `srtp` | Маскировка под SRTP | | `seed` | `myseed` | Обфускация | | `mtu` | `1400` | MTU (по умолчанию 1350) | | `tti` | `20` | TTI в мс, 10–5000 (по умолчанию 50) | ### XHTTP ``` vless://uuid@server:443?security=tls&type=xhttp&mode=auto&path=/xhttp#XHTTP Server ``` ### VLESS + REALITY ``` vless://uuid@server:443?security=reality&type=tcp&flow=xtls-rprx-vision&sni=example.com&fp=chrome&pbk=PUBLIC_KEY&sid=SHORT_ID&spx=%2F#REALITY Server ``` # Формат подписки Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/subscription-format.md # Формат подписки Описание форматов подписок, поддерживаемых протоколов и HTTP-заголовков. ## Поддерживаемые протоколы | Протокол | Схема | Описание | |---|---|---| | VLESS | `vless://` | Основной протокол | | VMess | `vmess://` | JSON-based конфигурация в base64 | | Trojan | `trojan://` | Парольная аутентификация | | Shadowsocks | `ss://` | SIP002 и современный формат | | Hysteria2 | `hysteria2://`, `hy2://` | Мульти-портовая поддержка | | SOCKS5 | `socks://`, `socks5://` | Проксирование через SOCKS5 | | HTTP-proxy | `http://user:pass@host:port` | HTTP-прокси (со `@` — иначе строка считается ссылкой на подписку) | | WireGuard | `wireguard://`, `wg://` | Туннелирование WireGuard | | AmneziaWG | `amneziawg://`, `awg://`, `.conf` в теле | Обфусцированный WireGuard. Одиночный `.conf` или несколько серверов в одной подписке — см. раздел «AmneziaWG / WireGuard .conf в теле» ниже | > Схемы `ssr://`, `tuic://`, `hysteria://` распознаются приложением, но **не парсятся** — серверы с этими схемами будут пропущены. ## Форматы тела подписки ### 1. Base64-закодированные ссылки Наиболее распространённый формат. Тело ответа — base64, при декодировании содержит ссылки по одной на строку: ``` base64( vless://uuid@server1:443?security=tls#Server1 vless://uuid@server2:443?security=tls#Server2 ) ``` Поддерживается URL-safe Base64 (`-` → `+`, `_` → `/`). ### 2. Открытые ссылки (plain text) Ссылки в открытом виде, по одной на строку: ``` vless://uuid@server1:443?security=tls#Server1 vmess://eyJhZGQiOiJzZXJ2ZXIyIn0= trojan://password@server3:443#Server3 socks://user:pass@server4:1080#Server4 wireguard://secretKey@server5:51820?publickey=KEY&address=10.0.0.2#Server5 amneziawg://#Server6 ``` ### 3. JSON-форматы **Массив полных xray-конфигов:** ```json [ { "outbounds": [...], "routing": {...} }, { "outbounds": [...], "routing": {...} } ] ``` **Полный xray-конфиг** (одиночный объект с `inbounds` и `outbounds`): ```json { "inbounds": [...], "outbounds": [...], "routing": {...}, "dns": {...} } ``` Подробнее: [full-xray-config.md](full-xray-config.md). ### 4. Смешанный формат Ссылки серверов + строки маршрутизации + метаданные в одном теле: ```text vless://uuid@server1:443?security=tls#Server1 vless://uuid@server2:443?security=tls#Server2 ://autorouting/onadd/https://example.com/routing.json #announce: Плановое обслуживание завтра ``` **Поддерживаемые специальные строки в теле:** | Паттерн | Описание | |---|---| | `://autorouting/onadd/{url}` | Автообновляемый профиль маршрутизации (URL, с `sourceURL`) | | `://autorouting/onadd/{base64}` | Профиль маршрутизации inline (base64) | | `://autorouting/add/{url}` | Автообновляемый профиль маршрутизации (URL, с `sourceURL`) | | `://routing/onadd/{url}` | Одноразовый импорт профиля по URL (без автообновления) | | `://routing/onadd/{base64}` | Статический профиль маршрутизации | | `://routing/add/{base64}` | Статический профиль маршрутизации | | `://onadd/{url или base64}` | Сокращённая форма (без автообновления) | | `://routing/{base64}` | Сокращённая форма | | `#announce: текст` | Объявление (поддерживает `base64:...`) | | `#profile-title: текст` | Имя подписки (поддерживает `base64:...`) | | `#support-url: URL` | Ссылка на поддержку | | `#profile-web-page-url: URL` | Ссылка на сайт провайдера | | `#announce-url: URL` | Ссылка на объявление | | `#profile-update-interval: число` | Интервал обновления (часы) | Специальные строки извлекаются из тела и не попадают в список серверов. > **Приоритет:** значения из HTTP-заголовков имеют приоритет над значениями из тела. Inline-метаданные в теле используются как fallback, если соответствующий заголовок отсутствует. ### 5. AmneziaWG / WireGuard .conf в теле Тело подписки может быть **сырым `.conf`-файлом** WireGuard или AmneziaWG (многострочный INI с секциями `[Interface]` и `[Peer]`). Приложение распознаёт его по наличию `[Interface]` + `PrivateKey` и роутит в тот же парсер, что файл/«Вставить». AmneziaWG определяется по наличию обфускационных параметров (`Jc`, `Jmin`, `Jmax`, `S1`–`S4`, `H1`–`H4`, `I1`–`I5`). Поддерживаются также новые параметры **AmneziaWG 3.0** (движок обновлён до amneziawg-go v3). Все они опциональны и прокидываются в движок как есть: | Ключ `.conf` | Описание | |---|---| | `HeaderProtectionKey` | Ключ защиты заголовков пакетов (задаётся сервером; для работы `S1`–`S4` должны быть ≥ 8) | | `ContentPaddingAddition` | Диапазон случайного паддинга контента (`uint32` или диапазон `min-max`) | | `RekeyAfterTime` | Время (сек), после которого клиент инициирует пересогласование | | `RekeyTimeout` | Таймаут (сек) повтора рукопожатия | | `RejectAfterTime` | Время (сек), после которого клиент принудительно пересогласовывается и отклоняет входящие данные | | `KeepaliveTimeout` | Время (сек) от последней отправки данных до отправки keepalive | | `MaxHandshakeAttempts` | Максимальное число повторов рукопожатия | ```ini [Interface] PrivateKey = Address = 10.8.0.2/32 DNS = 1.1.1.1 Jc = 4 Jmin = 40 Jmax = 70 S1 = 86 S2 = 574 H1 = 1234567890 [Peer] PublicKey = PresharedKey = AllowedIPs = 0.0.0.0/0 Endpoint = server.example.com:51820 ``` Тело может быть как plain-text, так и base64-обёрнутым. То же самое можно доставить через deep-link `incy://import/{base64-conf}` — см. [deep-links.md](deep-links.md). > Сырой `.conf` в теле парсится как **одна** серверная запись; обфускация AmneziaWG применяется движком на этапе подключения (клиент хранит `.conf` дословно). #### Несколько AmneziaWG-серверов в одной подписке > **Только iOS и Android.** Desktop-клиент AmneziaWG не поддерживает — `.conf` в теле там парсится как обычный WireGuard, а схемы `amneziawg://`/`awg://` и JSON-контейнер игнорируются. Одиночный `.conf` = один сервер. Чтобы отдать несколько AmneziaWG-локаций одной подпиской, используйте один из двух форматов. В обоих каждый `.conf` кодируется в **url-safe base64** (`-`→`+`, `_`→`/`, паддинг необязателен), а имя сервера берётся из `#`-фрагмента или поля `name`. **Формат 1 — построчный (`amneziawg://` / `awg://`).** По одной ссылке на строку, можно мешать с `vless://` и другими протоколами в том же теле: ``` amneziawg://#Германия awg://#Нидерланды ``` `amneziawg://` — канонический, `awg://` — короткий алиас (эквивалентны). Всё после `#` — отображаемое имя. **Формат 2 — JSON-контейнер.** Тело — JSON-объект с `type: "amneziawg"`: ```json { "type": "amneziawg", "version": 1, "servers": [ { "name": "Германия", "config": "" }, { "name": "Нидерланды", "config": "" } ] } ``` Каждый элемент `servers[]` → отдельный сервер. `name` необязателен (при отсутствии имя берётся из `# Name=` внутри `.conf` или из хоста `Endpoint`). Поле `version` зарезервировано и сейчас не проверяется. > **Поведение в обоих форматах:** битая base64-запись **пропускается** — остальные серверы всё равно загружаются (одна плохая локация не ломает всю подписку). Дубликаты по одинаковому `.conf` схлопываются в один сервер. При обновлении подписки серверы добавляются/обновляются/удаляются без задвоения. HTTP-заголовки остаются на уровне подписки (общие для всех серверов). --- ## HTTP-заголовки ### Заголовки подписки | Заголовок | Тип | Описание | |---|---|---| | `profile-title` | string | Имя подписки (до 25 символов). Поддерживает base64 | | `subscription-name` | string | Альтернатива `profile-title` (fallback) | | `profile-description` | string | Описание подписки. Поддерживает base64 | | `profile-update-interval` | int | Интервал обновления в часах | | `subscription-userinfo` | string | Статистика трафика и срок действия | | `support-url` | URL | Ссылка на поддержку | | `support-email` | email | Email поддержки — показывает кнопку «Email» в карточке подписки. Без заголовка кнопка не показывается | | `profile-web-page-url` | URL | Ссылка на сайт провайдера. Альтернатива: `homepage` | | `homepage` | URL | Fallback для `profile-web-page-url` | | `announce-url` | URL | Ссылка на объявление | | `announce` | string | Текст объявления (до 200 символов). Поддерживает base64 | | `autorouting` | URL | URL-источник профиля маршрутизации с автообновлением | | `routing` | string | Профиль маршрутизации (base64 или полная ссылка) | | `sort-order` | string | Порядок сортировки серверов: `ping`, `name`, `none` | | `content-disposition` | string | Fallback для имени подписки (расширения `.txt`, `.yaml` удаляются) | | `premium-url` | URL | Ссылка кнопки «Премиум» в карточке подписки | | `hide-url` | `1`/`0`/`true`/`false` | Скрыть URL подписки от Share/Copy/QR/backup | | `banner-text` | string | Текст баннера (base64). Перебивает панель | | `banner-button-text` | string | Текст кнопки баннера | | `banner-button-url` | URL | Ссылка кнопки баннера | | `banner-bg-color` | hex | Цвет фона баннера (`#RRGGBB`) | | `banner-button-color` | hex | Цвет кнопки баннера (`#RRGGBB`) | | `fragmentation-enable` | `1`/`0` | TCP-фрагментация | | `fragmentation-length` | `min-max` | Диапазон длины фрагмента | | `fragmentation-interval` | `min-max` | Диапазон задержки между фрагментами | | `fragmentation-packets` | `tlshello` / `1-3` / `all` | На какие пакеты применять | | `noises-enable` | `1`/`0` | Отправка шумовых пакетов до handshake | | `noises-type` | `rand` / `str` / `hex` | Тип шумового контента | | `noises-packet` | string | Payload шума (формат зависит от `type`) | | `noises-delay` | `min-max` мс | Диапазон задержки между шумами | | `server-address-resolve-enable` | `1`/`0` | Предварительный DNS-резолв адреса сервера через DoH | | `server-address-resolve-dns-domain` | URL | URL DoH-сервера | | `server-address-resolve-dns-ip` | IP | IP DoH-сервера (bootstrap) | | `no-limit-enabled` | `1`/`0` | (iOS) Память-экономный режим Network Extension (держит фоновый процесс под лимитом iOS 50 МБ). Только включает | | `per-app-proxy-enable` | `1`/`0` | (только Android) Включить per-app режим | | `per-app-proxy-mode` | `bypass` / `proxy` | (только Android) Режим per-app | | `per-app-proxy-list` | CSV / URL | (только Android) Список package names | > Это справочник заголовков подписки. Полное описание каждого (форматы значений, условия показа, приоритет «заголовок → панель», поддержка `#`-фрагмента в теле) — в [Управление приложением](app-management.md), где сводная таблица является авторитетным источником. ### Profile Title Поддерживает два формата: **Открытый текст:** ``` profile-title: My VPN ``` **Base64 с описанием:** ``` profile-title: base64:TWVNdiBWUE4KV2VsY29tZSB0byBvdXIgc2VydmljZQ== ``` При base64-декодировании: первая строка — имя, остальные — описание. ### Subscription User Info ``` subscription-userinfo: upload=0;download=1073741824;total=10737418240;expire=1735689600 ``` | Поле | Тип | Описание | |---|---|---| | `upload` | int | Исходящий трафик (байт) | | `download` | int | Входящий трафик (байт) | | `total` | int | Лимит трафика (байт) | | `expire` | int | Дата истечения (Unix timestamp, секунды) | > Если `expire` > 32000000000 — значение интерпретируется как миллисекунды и конвертируется в секунды. **Скрытие блока трафика:** Если сервер возвращает `subscription-userinfo: 0`, блок трафика на главном экране полностью скрывается. Используйте это, когда статистика трафика не предоставляется. ### Announce Текст объявления отображается на главном экране в виде баннера. Поддерживается до **5 строк** текста, после чего текст обрезается с многоточием. ``` announce: Обновление серверов 15 марта ``` ``` announce: base64:0J7QsdC90L7QstC70LXQvdC40LUg0YHQtdGA0LLQtdGA0L7Qsg== ``` ### Sort Order Задаёт порядок сортировки серверов. На iOS и Android значение применяется **к этой подписке** (у каждой подписки свои настройки пинга/сортировки/стиля, как и профиль маршрутизации); пользователь может переопределить его в настройках подписки. На Desktop применяется к глобальной настройке сортировки. ``` sort-order: ping ``` | Значение | Описание | |---|---| | `none` | Порядок по умолчанию (как в подписке) | | `ping` | По пингу (самые быстрые первыми) | | `name` | По алфавиту | --- ## Заголовки запроса (клиент → сервер) При обновлении подписки приложение отправляет: | Заголовок | Описание | |---|---| | `User-Agent` | `INCY//` | | `Accept` | `*/*` | | `Accept-Language` | Language-tag устройства (напр. `ru-RU`) | | `Accept-Encoding` | Только iOS: `gzip, deflate, br` | | `x-app-version` | Версия приложения | | `x-device-locale` | Язык устройства | | `x-client` | `INCY` | При включённой отправке HWID дополнительно: | Заголовок | Описание | |---|---| | `x-hwid` | Аппаратный идентификатор ([подробнее](hwid.md)) | | `X-Device-ID` | Alias для `x-hwid` на Android (некоторые сервер-стеки ожидают именно этот заголовок) | | `x-device-os` | Платформа (`iOS`, `Android`, `Linux`, `Windows`) | | `x-ver-os` | Версия ОС | | `x-device-model` | Модель устройства | > Все заголовки HTTP регистронезависимы. Сервер может смотреть на `x-hwid` либо `X-HWID` — придут одни и те же байты. --- ## Резервные хосты (fallback) Если основной хост подписки недоступен (сеть/таймаут/5xx/429), клиент перебирает резервные хосты — тот же путь и токен, меняется только хост. На `404`/`410` (подписка удалена провайдером) перебор не выполняется. Список резервных хостов приходит не через заголовок, а в теле premium-конфига (`settings.fallbackHosts`) — см. [premium-api.md](premium-api.md). # Параметры share-ссылок Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/share-links.md # Параметры share-ссылок Описание параметров для протокольных ссылок серверов (VLESS, VMess, Trojan, Shadowsocks, Hysteria2, SOCKS5, WireGuard). --- ## VLESS ``` vless://uuid@host:port?params#name?serverDescription=base64 ``` ### Основные параметры | Параметр | Поле | По умолчанию | Описание | |---|---|---|---| | `encryption` | encryption | `"none"` | Шифрование | | `flow` | flow | | XTLS flow (напр. `xtls-rprx-vision`) | | `type` | network | `"tcp"` | Транспорт: `tcp`, `ws`, `grpc`, `xhttp`, `kcp`, `quic` | | `security` | security | `"none"` | Безопасность: `none`, `tls`, `reality` | ### TLS | Параметр | Поле | Описание | |---|---|---| | `sni` | SNI | Server Name Indication | | `fp` | fingerprint | uTLS fingerprint (значения — ниже) | | `alpn` | ALPN | Протоколы через запятую (напр. `h2,http/1.1`) | **Допустимые значения `fp`** (uTLS из xray-core): `chrome`, `firefox`, `safari`, `ios`, `android`, `edge`, `360`, `qq`, `random` (случайный реальный браузер), `randomized` (рандомизированный с ALPN), `randomizednoalpn`. Также принимаются закреплённые версии: `hellochrome_120/131/133`, `hellofirefox_120/148`, `helloios_13/14`, `helloedge_106`, `hellosafari_26_3`, `hello360_11_0`, `helloqq_11_1`, `hellogolang` (Go-default, без имперсонации), `unsafe`. Пусто/не задано → `chrome`. Значение передаётся в xray как есть. ### Reality | Параметр | Поле | Описание | |---|---|---| | `pbk` | publicKey | Публичный ключ Reality | | `sid` | shortId | Short ID | | `spx` | spiderX | Spider X path | ### Transport (WebSocket) | Параметр | Поле | Описание | |---|---|---| | `path` | path | Путь WebSocket (URL-decoded) | | `host` | host | Host-заголовок | ### Transport (gRPC) | Параметр | Поле | Описание | |---|---|---| | `serviceName` | serviceName | Имя gRPC-сервиса | | `authority` | authority | HTTP authority | ### Transport (xHTTP / SplitHTTP) | Параметр | Поле | Описание | |---|---|---| | `path` | path | Путь | | `host` | host | Host-заголовок | | `mode` | xhttpMode | Режим: `auto`, `packet`, `connect` | | `extra` | xhttpExtra | Дополнительные данные (URL-encoded JSON) | | `sit` | xhttpSessionIDTable | Размер таблицы session-ID (int, xray 26.x) | | `sil` | xhttpSessionIDLength | Длина session-ID (int, xray 26.x) | > `type=splithttp` автоматически нормализуется в `xhttp`. > `sit`/`sil` также принимаются на VLESS и Trojan (не только на xHTTP-транспорте). ### Transport (mKCP) | Параметр | Поле | Описание | |---|---|---| | `headerType` | headerType | Тип заголовка (none, srtp, utp, wechat-video, и др.) | | `seed` | kcpSeed | KCP seed | | `mtu` | kcpMtu | MTU (Maximum Transmission Unit), по умолчанию 1350 | | `tti` | kcpTti | TTI (Transmission Time Interval) в мс, 10–5000, по умолчанию 50 | ### Transport (QUIC) | Параметр | Поле | Описание | |---|---|---| | `quicSecurity` | quicSecurity | `none`, `aes-128-gcm`, `chacha20-poly1305` | | `key` | quicKey | Ключ QUIC | ### Расширенные параметры безопасности | Параметр | Поле | Описание | |---|---|---| | `fm` | finalmask | Маска uTLS (URL-encoded JSON) | | `pcs` | pinnedPeerCertSha256 | SHA-256 пин сертификата (URL-encoded) | | `vcn` | verifyPeerCertByName | Проверка сертификата по имени (URL-encoded) | | `ech` | echConfigList | Encrypted Client Hello конфиг (URL-encoded) | | `pqv` | mldsa65Verify | Post-quantum ML-DSA-65 верификация | ### TCP-фрагментация (per-server) Параметры фрагментации можно задать прямо в share-link, и они применятся только к этому серверу (отдельно от провайдерских `fragmentEnabled` в [Premium API](premium-api.md)). | Параметр | Поле | Описание | |---|---|---| | `fragmentPackets` | fragmentPackets | На какие пакеты применять: `tlshello`, `1-3`, `1`, `all` | | `fragmentLength` | fragmentLength | Диапазон длины фрагмента в байтах (напр. `10-30`) | | `fragmentInterval` | fragmentInterval | Диапазон задержки между фрагментами в мс (напр. `10-30`) | Эти же параметры можно передать на любом VLESS/VMess/Trojan-сервере — парсер добавит их в модель `VLESSConfig` и xray применит фрагментацию при активной проверке `security` (tls/reality). ### Фрагмент (после `#`) ``` #ServerName?serverDescription=SGlnaC1TcGVlZA== ``` - Всё до первого `?` — имя сервера (URL-decoded) - `serverDescription` — base64-закодированное описание (до 30 символов) --- ## VMess ``` vmess://base64({ json }) ``` Base64-закодированный JSON-объект: | Поле | Тип | По умолчанию | Описание | |---|---|---|---| | `add` | string | | Адрес сервера | | `port` | int | | Порт | | `id` | string | | UUID | | `ps` | string | | Имя / remark | | `scy` | string | `"auto"` | Шифрование | | `aid` | int | `0` | Alter ID | | `net` | string | `"tcp"` | Транспорт | | `tls` | string | | `"tls"` или пусто | | `sni` | string | | SNI | | `fp` | string | | TLS fingerprint | | `path` | string | | WebSocket/gRPC путь | | `host` | string | | Host-заголовок | | `type` | string | | Header type (игнорируется если `"none"`) | | `alpn` | string | | ALPN через запятую | Дополнительно для полных xray-конфигов: | Поле | Тип | Описание | |---|---|---| | `meta.serverDescription` | string | Описание сервера | --- ## Trojan ``` trojan://password@host:port?params#name?serverDescription=base64 ``` Параметры аналогичны VLESS со следующими отличиями: - Пароль передаётся в userInfo (вместо UUID) - `security` по умолчанию `"tls"` (не `"none"`) - Дополнительный параметр `peer` — fallback для SNI (если `sni` не указан) - Поддерживает расширенные параметры: `fm`, `pcs`, `vcn`, `ech` --- ## Shadowsocks Два формата: **Современный:** ``` ss://base64(method:password)@host:port#name?serverDescription=base64 ``` **SIP002:** ``` ss://base64(method:password@host:port)#name?serverDescription=base64 ``` | Компонент | Описание | |---|---| | `method` | Метод шифрования (напр. `aes-256-gcm`, `chacha20-ietf-poly1305`) | | `password` | Пароль | --- ## Hysteria2 ``` hysteria2://password@host:port1,port2-port3?params#name?serverDescription=base64 hy2://password@host:port?params#name?serverDescription=base64 ``` ### Параметры | Параметр | Поле | Описание | |---|---|---| | `insecure` | security | `"1"` → без проверки сертификата | | `sni` | SNI | Server Name Indication | | `fp` | fingerprint | TLS fingerprint | | `pinSHA256` | pinnedPeerCertSha256 | SHA-256 пин сертификата (URL-encoded) | | `alpn` | ALPN | Протоколы через запятую | | `obfs` | hy2Obfs | Тип обфускации | | `obfs-password` | hy2ObfsPassword | Пароль обфускации (URL-encoded) | | `up` | hy2UpMbps | Скорость загрузки (Mbps) | | `down` | hy2DownMbps | Скорость скачивания (Mbps) | | `mport` | portHopping | Port hopping: список портов/диапазонов, напр. `443,5000-6000` (алиас: `ports`) | | `mportHopInt` | portHoppingInterval | Интервал переключения портов, сек (алиас: `portHopInt`) | > Desktop-клиент port hopping не поддерживает — использует только первый порт. ### Мульти-портовый формат ``` hysteria2://password@server:443,8443-8445#Server ``` - Первый порт (`443`) используется для подключения - Формат: `порт1,порт2-порт3` (диапазоны поддерживаются) ### IPv6 ``` hysteria2://password@[::1]:443#Server ``` --- ## SOCKS5 ``` socks://username:password@host:port#name?serverDescription=base64 ``` | Компонент | Описание | |---|---| | `username:password` | Учётные данные SOCKS5 (опционально, может быть анонимным) | | `host:port` | Адрес прокси-сервера | SOCKS5 — простой протокол проксирования, дополнительные query-параметры не требуются. Фрагмент (`#`): всё до первого `?` — имя сервера (URL-decoded), `serverDescription` — base64-закодированное описание (до 30 символов). ### Пример SOCKS5 ``` socks://pkg-private2-country-us-city-new_york_city:w0e20i55uuq6pxqg@quality.proxywing.com:1080#title?serverDescription=SGFwcCB0aGUgYmVzdA== ``` --- ## WireGuard ``` wireguard://secretKey@host:port?publickey=KEY&address=ADDR&mtu=1500&reserved=1,22,33#name?serverDescription=base64 ``` | Компонент | Описание | |---|---| | `secretKey` | Приватный (секретный) ключ в userInfo | | `host:port` | Адрес эндпоинта WireGuard | ### Параметры WireGuard | Параметр | Обязательный | Описание | |---|---|---| | `publickey` | Да | Публичный ключ пира | | `address` | Нет | Локальный туннельный адрес (через запятую для нескольких) | | `mtu` | Нет | MTU (по умолчанию 1500) | | `reserved` | Нет | Зарезервированные байты (через запятую: `1,22,33`) | | `allowinsecure` | Нет | `1` = не проверять сертификат (информационный) | Фрагмент (`#`): всё до первого `?` — имя сервера (URL-decoded), `serverDescription` — base64-закодированное описание (до 30 символов). ### Пример WireGuard ``` wireguard://password2key@123.123.123.2:10803?publickey=asd33d223d33&address=dom.ru&allowinsecure=1&mtu=1500&reserved=1,22,33#title?serverDescription=SGFwcCB0aGUgYmVzdA== ``` # Полные Xray-конфигурации Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/full-xray-config.md # Полные Xray-конфигурации Подписка может возвращать полные конфигурационные файлы xray-core с балансировщиками, обсерваториями и кастомной маршрутизацией. Такие конфигурации передаются в xray-core практически без изменений. ## Определение Конфигурация считается «полной», если JSON содержит **оба** поля: - `inbounds` — входящие подключения - `outbounds` — исходящие подключения Любой JSON-объект, содержащий и `inbounds`, и `outbounds`, распознаётся как полная конфигурация — независимо от наличия балансировщиков, обсерваторий или метаданных. ## Формат ### Одиночная конфигурация ```json { "log": { "loglevel": "warning" }, "dns": { "servers": [...] }, "inbounds": [ { "tag": "socks-in", "protocol": "socks", "port": 10808, "listen": "127.0.0.1" }, { "tag": "http-in", "protocol": "http", "port": 10809, "listen": "127.0.0.1" } ], "outbounds": [ { "tag": "proxy-1", "protocol": "vless", "settings": {...} }, { "tag": "proxy-2", "protocol": "vless", "settings": {...} }, { "tag": "direct", "protocol": "freedom" }, { "tag": "block", "protocol": "blackhole" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [...], "balancers": [ { "tag": "balancer-1", "selector": ["proxy-1", "proxy-2"], "strategy": { "type": "leastPing" } } ] }, "burstObservatory": { "subjectSelector": ["proxy-1", "proxy-2"], "pingConfig": { "destination": "http://www.google.com/generate_204", "connectivity": "http://www.google.com/generate_204", "interval": "30s", "sampling": 2, "timeout": "5s" } }, "stats": {} } ``` ### Массив конфигураций ```json [ { "outbounds": [...], "routing": { "balancers": [...] }, "burstObservatory": {...} }, { "outbounds": [...], "routing": { "balancers": [...] }, "burstObservatory": {...} } ] ``` Каждый элемент массива — отдельная полная конфигурация, импортируется как отдельный «сервер». ## Автоматический патчинг При запуске приложение автоматически патчит конфигурацию (`patchFullConfigInbounds`): ### 1. Логирование - `log.loglevel` — устанавливается из настроек пользователя - `log.access` и `log.error` — устанавливается путь к лог-файлу приложения > iOS: `group.llc.itdev.incy/logs/xray.log` > Android/Desktop: передаётся через параметр `logFilePath` ### 2. Inbound'ы - У всех не-internal `socks` / `mixed` / `http` inbound'ов `listen` принудительно ставится в `127.0.0.1` (порт **не** меняется — конфиг должен сам слушать `10808` для SOCKS/mixed и `10809` для HTTP, это точки входа клиента). - Если в конфиге нет ни одного socks-подобного inbound'а — добавляется `mixed` inbound на `127.0.0.1:10808` со sniffing. - На точечные inbound'ы без своей авторизации клиент «накрашивает» креды, чтобы порт не оставался открытым релеем. ### 3. Stats Если конфигурация содержит `burstObservatory` или `observatory`, но не содержит `stats` — автоматически добавляется пустой объект `"stats": {}`. ### 4. DNS Direct Routing (предотвращение циклической зависимости) **Условие:** конфигурация содержит **и** observatory, **и** balancers. **Проблема:** Observatory проверяет серверы → для проверки нужен DNS → DNS идёт через балансировщик → балансировщик зависит от результатов Observatory → цикл. **Решение:** IP-адреса DNS-серверов из `dns.servers` добавляются в начало `routing.rules` с outbound `"direct"`: ```json { "type": "field", "ip": ["8.8.8.8", "1.1.1.1"], "outboundTag": "direct" } ``` Если outbound с тегом `"direct"` или протоколом `"freedom"` отсутствует — добавляется автоматически. ## Отображение в UI - Полные конфигурации отображаются как один сервер в списке - Бейджи безопасности и транспорта скрываются (информация внутри конфига) - Если в `meta` есть `serverDescription` — отображается как описание сервера - Имя сервера берётся из первого proxy-outbound'а ## Особенности работы с полными конфигурациями ### MPH Cache Для полных конфигураций **MPH cache не используется**. Кэш (`mph_cache.dat`) предназначен для сериализации DomainMatcher, но несовместим с полными JSON-конфигурациями. При обнаружении полной конфигурации: - Существующий файл `mph_cache.dat` удаляется - Xray-core строит матчеры в рантайме - Флаг `isFullConfig` передаётся из основного приложения в Network Extension (iOS) через `providerConfiguration` и `sharedDefaults` ### Геофайлы (Geo Trimming) Для полных конфигураций **обрезка геофайлов пропускается**. Полные конфигурации поставляются с собственными кастомными геофайлами от подписки, которые используются как есть. При подключении: - `GeoTrimmer` не вызывается - Обрезанные файлы удаляются (`deleteTrimmedGeoFiles`) - Xray-core использует оригинальные геофайлы ### DNS Поведение зависит от того, содержит ли конфигурация **собственные** DNS-серверы (непустой `dns.servers`): - **Если `dns.servers` задан** — приложение **не трогает** DNS. Ваши записи с фильтрами `domains`/`expectIPs` сохраняются как есть; клиент не подставляет свои DNS-серверы. Именно так работает большинство полных конфигураций (Remnawave и т.п.). - **Если `dns.servers` пуст или отсутствует** — конфигурация не приносит своих DNS, поэтому клиент подставляет свой `queryStrategy` (по настройке IPv4/IPv6/auto) и `hosts`, иначе DNS-запросы утекали бы напрямую (DNS-leak). Независимо от этого: - Клиент **сужает** переданный `fakedns`-пул до узкой подсети, чтобы IP из широкого fakedns-пула провайдера не маршрутизировались как реальные адреса. - Добавляется Direct-правило для DNS-серверов (предотвращает циклическую зависимость с Observatory). ## Хранение Полная JSON-конфигурация сохраняется в поле `fullConfigJson` модели `VLESSConfig`. При запуске xray-core конфиг патчится и передаётся целиком, без генерации из отдельных полей. # Управление приложением Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/app-management.md # Управление приложением Параметры для управления поведением приложения через HTTP-заголовки подписки и строки в теле ответа. ## Способы передачи Все параметры можно передавать двумя способами: **1. HTTP-заголовок:** ``` HTTP/2 200 profile-title: Мой VPN support-url: https://t.me/support ``` **2. Строка в теле подписки (комментарий с `#`):** ``` #profile-title: Мой VPN #support-url: https://t.me/support #profile-update-interval: 6 #announce: Текст объявления vless://... ``` > **Приоритет:** HTTP-заголовки имеют приоритет. Строки в теле подписки используются как fallback, когда соответствующий заголовок отсутствует. Это особенно полезно при раздаче подписок через статические файлы (nginx), где нет возможности задать кастомные HTTP-заголовки. --- ## Стандартные параметры ### Имя подписки Название профиля подписки. Максимум 25 символов. Можно передавать как текст или base64 (UTF-8). **Заголовок:** ``` profile-title: Мой VPN ``` **В теле подписки:** ``` #profile-title: Мой VPN ``` **Base64 с описанием** (первая строка — имя, остальные — описание): ``` profile-title: base64:0JzQvtC5IFZQTgrQlNC+0LHRgNC+INC/0L7QttCw0LvQvtCy0LDRgtGM ``` **Альтернативные заголовки** (fallback, если `profile-title` отсутствует): - `subscription-name` — альтернативное имя подписки - `content-disposition` — имя файла из заголовка (расширения `.txt`, `.yaml`, `.yml` удаляются автоматически) ### Описание подписки Отдельный заголовок для описания, если оно не включено в `profile-title`: ``` profile-description: Быстрые серверы в Европе ``` Поддерживает base64: `profile-description: base64:...` ### Интервал обновления подписки Интервал автоматического обновления подписки в часах. Значение должно быть кратно одному часу. **Заголовок:** ``` profile-update-interval: 6 ``` **В теле подписки:** ``` #profile-update-interval: 6 ``` ### Статус подписки Информация о балансе, объёме использованного трафика и сроке действия подписки. Поля разделяются точкой с запятой. ``` subscription-userinfo: upload=0;download=1073741824;total=10737418240;expire=1700000000 ``` | Поле | Описание | |---|---| | `upload` | Исходящий трафик (байт) | | `download` | Входящий трафик (байт) | | `total` | Лимит трафика (байт) | | `expire` | Дата истечения (Unix timestamp, секунды) | > Если `expire` > 32 000 000 000 — значение интерпретируется как миллисекунды и конвертируется в секунды. ### Ссылка на поддержку Кнопка перехода на страницу поддержки. Если ссылка ведёт в Telegram — отображается иконка Telegram. **Заголовок:** ``` support-url: https://t.me/your_support_bot ``` **В теле подписки:** ``` #support-url: https://t.me/your_support_bot ``` ### Ссылка на сайт Кнопка перехода на сайт подписки. **Заголовок:** ``` profile-web-page-url: https://your-site.com ``` **В теле подписки:** ``` #profile-web-page-url: https://your-site.com ``` Альтернативный заголовок: `homepage` ### Объявление Текстовое объявление (до 200 символов). Можно передавать как текст или base64. **Заголовок:** ``` announce: Плановое обслуживание 15 марта с 03:00 до 05:00 MSK ``` **Base64:** ``` announce: base64:0J/Qu9Cw0L3QvtCy0L7QtSDQvtCx0YHQu9GD0LbQuNCy0LDQvdC40LU= ``` **URL объявления** (ссылка, не текст): ``` announce-url: https://example.com/news ``` **В теле подписки:** ``` #announce: Плановое обслуживание 15 марта #announce-url: https://example.com/news vless://uuid@server:443#Server ``` --- ## Сводная таблица параметров | Заголовок | Альтернативы | Формат | Body (`#`) | Описание | | --- | --- | --- | --- | --- | | `profile-title` | `subscription-name`, `content-disposition` | текст / `base64:...` | ✅ | Имя подписки | | `profile-description` | — | текст / `base64:...` | — | Описание подписки | | `profile-update-interval` | — | число (часы) | ✅ | Интервал обновления | | `subscription-userinfo` | — | `key=value;...` | — | Статистика трафика | | `sort-order` | — | `none` / `ping` / `name` | — | Порядок сортировки серверов (на iOS/Android — для этой подписки; на Desktop — глобально) | | `support-url` | — | URL | ✅ | Ссылка на поддержку | | `support-email` | — | email | ✅ | Email поддержки — показывает кнопку «Email» в карточке подписки (запасной контакт). Без заголовка кнопка не показывается | | `profile-web-page-url` | `homepage` | URL | ✅ | Ссылка на сайт | | `announce` | — | текст / `base64:...` | ✅ | Текст объявления | | `announce-url` | — | URL | ✅ | Ссылка на объявление | | `autorouting` | — | URL | ✅ | Автообновляемый профиль маршрутизации | | `routing` | — | base64 / ссылка / `off` | ✅ | Профиль маршрутизации; значение `off` отключает встроенную маршрутизацию | | `premium-url` | — | URL | — | Ссылка «Премиум» (в карточке подписки, см. ниже) | | `banner-text` | — | текст / `base64:...` | — | Текст баннера (перебивает панель, см. ниже) | | `banner-button-text` | — | текст / `base64:...` | — | Текст кнопки баннера | | `banner-button-url` | — | URL | — | Ссылка кнопки баннера | | `banner-bg-color` | — | hex (`#RRGGBB`) | — | Цвет фона баннера | | `banner-button-color` | — | hex (`#RRGGBB`) | — | Цвет кнопки баннера | | `hide-url` | — | `1` / `0` / `true` / `false` | ✅ | Скрыть URL подписки от Share/Copy/QR/backup (см. ниже) | | `hide-check` | — | `1` / `0` / `true` / `false` | ✅¹ | Скрыть кнопку «Проверить» на главном экране (см. ниже) | | `per-app-proxy-enable` | — | `1` / `0` | — | Включить per-app режим (только Android) | | `per-app-proxy-mode` | — | `bypass` / `proxy` | — | Режим per-app | | `per-app-proxy-list` | — | CSV / URL | — | Список package names | | `fragmentation-enable` | — | `1` / `0` | — | TCP-фрагментация | | `fragmentation-length` | — | `min-max` | — | Диапазон длины фрагмента | | `fragmentation-interval` | — | `min-max` | — | Диапазон задержки между фрагментами | | `fragmentation-packets` | — | `tlshello` / `1-3` / `all` | — | На какие пакеты применять | | `noises-enable` | — | `1` / `0` | — | Отправка шумовых пакетов до handshake | | `noises-type` | — | `rand` / `str` / `hex` | — | Тип шумового контента | | `noises-packet` | — | строка | — | Payload шума (формат зависит от `type`) | | `noises-delay` | — | `min-max` мс | — | Диапазон задержки между шумами | | `server-address-resolve-enable` | — | `1` / `0` | — | Предварительный DNS-резолв адреса сервера через DoH | | `server-address-resolve-dns-domain` | — | URL | — | URL DoH-сервера | | `server-address-resolve-dns-ip` | — | IP | — | IP DoH-сервера (bootstrap) | | `no-limit-enabled` | — | `1` / `0` | — | (iOS) Включить память-экономный режим Network Extension — держит фоновый процесс под лимитом iOS 50 МБ, чтобы xhttp-транспорт не убивался системой (VPN не отваливается). Только включает (не выключает ручной выбор пользователя) | > Все заголовки регистронезависимы (`profile-title` = `Profile-Title`). > > Параметры из тела подписки (body) используются как fallback — HTTP-заголовки всегда имеют приоритет. > > Все заголовки со значениями поддерживают префикс `base64:` для передачи UTF-8 данных без проблем с Latin-1 / non-ASCII (применяется к `announce`, `profile-title`, `profile-description`, `subscription-name`, `per-app-proxy-list`, `banner-text`, `banner-button-text`). > > Android и Desktop дополнительно понимают gzip-цепочки в значениях (`base64:gzip:`, `gzip:base64:`, `gzip:`) — полезно для длинных `per-app-proxy-list`. **iOS поддерживает только `base64:`** (без gzip). > > ¹ `hide-check` как HTTP-заголовок и через тело подписки (`#hide-check:`) работает так же, как `hide-url`, **но** маркер в теле `#hide-check:` читают только Android и iOS — Desktop применяет `hide-check` только через заголовок и Premium-конфиг. > > ⚠️ **Кириллица в баннере.** HTTP-заголовки не передают non-ASCII напрямую. Чтобы текст баннера или кнопки был на русском, кодируйте его в base64: `banner-text: base64:` и `banner-button-text: base64:`. --- ## Маршрутизация ### Профиль маршрутизации Статический профиль в base64. Подробнее — [routing.md](routing.md). ``` routing: ://routing/onadd/ewog...base64... ``` Значение `off` (или ссылка `://routing/off`) полностью отключает встроенную маршрутизацию: ``` routing: off ``` ### Автообновляемый профиль маршрутизации URL-источник профиля маршрутизации с периодическим обновлением. Подробнее — [autorouting.md](autorouting.md). ``` autorouting: https://raw.githubusercontent.com/user/repo/main/profile.json ``` ### Приоритет источников маршрутизации Если профиль маршрутизации указан в нескольких местах, используется **первый найденный** по приоритету: | Приоритет | Источник | |---|---| | 1 (высший) | Заголовок `autorouting` | | 2 | Body — строка с URL (`://autorouting/onadd/`, `://autorouting/add/`) | | 3 | Заголовок `routing` | | 4 (низший) | Body — строка с base64 (`://routing/onadd/`, `://routing/add/`, `://routing/`) | > **Важно:** только `://autorouting/` устанавливает `sourceURL` и включает автообновление. Строки `://routing/onadd/{url}` в теле подписки импортируют профиль одноразово, без привязки к источнику. --- ## Описание сервера Дополнительная подпись, отображаемая под именем сервера (максимум 30 символов). Добавляется после `title` через разделитель `?`: ``` vless://uuid@server:443#Сервер1?serverDescription=base64-text ``` --- ## Per-app proxy (управление из подписки — только Android) Android VpnService позволяет пропускать через VPN только **выбранные приложения** либо наоборот — **исключать** их из туннеля. Провайдер может форсировать этот режим из подписки через три заголовка (только Android). На Desktop сама фича есть, но управляется пользователем — см. подраздел ниже. ``` per-app-proxy-enable: 1 per-app-proxy-mode: bypass per-app-proxy-list: com.android.chrome,org.telegram.messenger ``` ### Параметры | Заголовок | Значение | Описание | | --- | --- | --- | | `per-app-proxy-enable` | `1` / `0` | Включить режим per-app | | `per-app-proxy-mode` | `bypass` \| `proxy` | `bypass` — указанные приложения **обходят** VPN. `proxy` — **только** они идут через VPN | | `per-app-proxy-list` | CSV или URL | Список package names через запятую, перенос строки, или URL до текстового файла | ### Формат списка **Inline (CSV или строчный):** ``` per-app-proxy-list: com.android.chrome,org.telegram.messenger,com.google.android.youtube ``` **Base64 (для длинных списков):** ``` per-app-proxy-list: base64:Y29tLmFuZHJvaWQuY2hyb21lCm9yZy50ZWxlZ3JhbS5tZXNzZW5nZXI= ``` **Удалённый URL:** ``` per-app-proxy-list: https://example.com/myapps.txt ``` Файл по URL — обычный plain text с package names по одному на строку или через запятую. Клиент скачивает его при применении подписки и при каждом обновлении. ### Поведение - Если ни одно из трёх полей не задано — настройки пользователя в приложении **не переопределяются**. - Если `per-app-proxy-enable` задан в `0` — per-app режим выключается, даже если пользователь его включил локально. - Эти **заголовки** читает только Android (список — по package names). ### Per-app на Desktop (Windows / macOS / Linux) Десктоп-клиент **тоже поддерживает** per-app split-tunnel, но **не через эти заголовки** — а как **пользовательскую** настройку в приложении (экран «Split-tunneling»). Провайдер задать её из подписки не может; список формирует пользователь по именам/путям исполняемых файлов, а не по package names. Под капотом Desktop не трогает системный firewall — правило пишется прямо в маршрутизацию xray через поле `process` (сопоставление по PID сокета, sniffing не нужен). Работает и для обычных, и для **full-config** подписок. Поддержаны Windows, macOS и Linux (движок ядра xray определяет процесс-инициатор: `GetExtendedTcpTable` на Windows, `/proc` на Linux, `libproc` на macOS). !!! note "Только в режиме TUN" Per-app на Desktop работает только когда включён режим **TUN** (xray сам держит TUN-устройство и видит процесс-инициатор). В режимах System Proxy / Proxy Only процесс не определяется, и per-app не применяется. --- ## TCP-фрагментация Перезаписывает глобальные настройки фрагментации пользователя для данной подписки. ``` fragmentation-enable: 1 fragmentation-packets: tlshello fragmentation-length: 10-30 fragmentation-interval: 10-30 ``` | Заголовок | Значение | Описание | | --- | --- | --- | | `fragmentation-enable` | `1` / `0` | Включить фрагментацию | | `fragmentation-packets` | `tlshello` \| `1-3` \| `1` \| `all` | На какие TCP-пакеты применять фрагментацию | | `fragmentation-length` | `min-max` | Диапазон длины фрагмента в байтах | | `fragmentation-interval` | `min-max` | Диапазон задержки между фрагментами в мс | Те же параметры доступны через [Premium API](premium-api.md#domain-fronting-и-фрагментация) как `fragmentEnabled / fragmentPackets / fragmentLength / fragmentInterval` — при Premium-подписке HTTP-заголовки игнорируются в пользу API-значений. --- ## Шумовые пакеты (noises) Отправка случайных UDP-пакетов перед VPN-handshake'ом для маскировки. Актуально в первую очередь для WireGuard и Hysteria2 в сетях с глубокой инспекцией трафика. ``` noises-enable: 1 noises-type: rand noises-packet: 10-20 noises-delay: 10-50 ``` | Заголовок | Значение | Описание | | --- | --- | --- | | `noises-enable` | `1` / `0` | Включить шумы | | `noises-type` | `rand` \| `str` \| `hex` | Формат payload'а | | `noises-packet` | строка | Содержимое шумового пакета; для `rand` — диапазон длины `min-max` | | `noises-delay` | `min-max` мс | Диапазон задержки между шумовыми пакетами | --- ## Резолв адреса сервера через DoH Bootstrap-резолв домена сервера через DNS-over-HTTPS до установки туннеля. Полезно когда провайдерский DNS подменяет адрес VPN-сервера. ``` server-address-resolve-enable: 1 server-address-resolve-dns-domain: https://common.dot.dns.yandex.net/dns-query server-address-resolve-dns-ip: 77.88.8.8 ``` | Заголовок | Значение | Описание | | --- | --- | --- | | `server-address-resolve-enable` | `1` / `0` | Включить DoH-резолв | | `server-address-resolve-dns-domain` | URL | DoH endpoint (обычно `/dns-query`) | | `server-address-resolve-dns-ip` | IP | IP DoH-сервера для bootstrap'а — используется до резолва его домена | Те же поля передаются через Premium API как `serverAddressResolveEnable` / `serverAddressResolveDnsDomain` / `serverAddressResolveDnsIp`. --- ## Ссылка «Премиум» Дополнительная кнопка в карточке подписки — ведёт на страницу покупки или личный кабинет провайдера. ``` premium-url: https://example.com/pricing ``` | Заголовок | Значение | Описание | | --- | --- | --- | | `premium-url` | URL | URL кнопки «Премиум» в карточке подписки | Если не задан — кнопка скрыта. --- ## Баннер провайдера Баннер — заметная плашка в карточке подписки на главном экране (текст + опциональная кнопка). Настраивается в премиум-панели, но текст и кнопку можно **переопределить через заголовки подписки** — удобно для динамических объявлений без захода в панель. ### Условия показа Баннер показывается, когда провайдер — **премиум** (активная премиум-подписка) **и** выполнено хотя бы одно из: 1. пришёл заголовок **`banner-text`** в ответе подписки (динамический баннер — например, через Remnawave Response Rules, без захода в премиум-панель INCY); **или** 2. баннер **включён в премиум-панели** (`bannerEnabled`). То есть заголовок `banner-text` сам по себе включает баннер (при условии, что провайдер премиум) — заходить в панель и ставить `bannerEnabled` не обязательно. Если `banner-text` не пришёл, баннер показывается по старому пути (панельный `bannerEnabled` + текст из панели). > Слать баннер через заголовки могут **только премиум-провайдеры**: для не-премиум подписок `banner-*` заголовки игнорируются. ### Приоритет: заголовок → панель Каждое поле резолвится как `заголовок ?? панель`: если соответствующий `banner-*` заголовок пришёл в ответе подписки — берётся он, иначе — значение из панели. ``` banner-text: base64:0JDQutGG0LjRjyEg0KHQutC40LTQutCwIDUw0Js= banner-button-text: Подробнее banner-button-url: https://example.com/promo banner-bg-color: #E53E3E banner-button-color: #38A169 ``` | Заголовок | Формат | Переопределяет (панель) | | --- | --- | --- | | `banner-text` | текст / `base64:...` | `bannerText` | | `banner-button-text` | текст / `base64:...` | `bannerButtonText` | | `banner-button-url` | URL | `bannerButtonUrl` | | `banner-bg-color` | hex `#RRGGBB` | `bannerBgColor` | | `banner-button-color` | hex `#RRGGBB` | `bannerButtonColor` | > Текст (`banner-text`) и текст кнопки (`banner-button-text`) поддерживают `base64:` — для кириллицы/UTF-8 это **обязательно** (см. предупреждение выше). > > URL кнопки резолвится как `premium-url` → `banner-button-url` → панель: заголовок `premium-url` имеет наивысший приоритет (исторически). > > Если после резолва текст баннера пуст — баннер не показывается. > > Поддерживается на iOS, Android и Desktop. --- ## Скрытие URL подписки от пользователя Параметр `hide-url` блокирует «утечку» URL подписки за пределы устройства: при включении приложение **не показывает и не разрешает экспортировать** URL ни через Share, ни через Copy URL, ни через QR-код, ни в резервные копии. Имя подписки, серверы и весь остальной UX работают как обычно — скрывается только сам URL. **Заголовок:** ``` hide-url: 1 ``` **В теле подписки:** ``` #hide-url: 1 vless://... ``` **Premium API (JSON):** ```json { "hide_url": true, "settings": { ... } } ``` ### Принимаемые значения | Что прислано | Поведение | | --- | --- | | `1`, `true`, `yes` (без учёта регистра) | URL скрыт | | `0`, `false`, `no`, пустая строка | URL открыт (по умолчанию) | | Любое другое значение в заголовке | Игнорируется, URL открыт | ### Приоритет источников 1. HTTP-заголовок `hide-url` имеет наивысший приоритет 2. Строка `#hide-url:` в теле подписки используется как fallback при отсутствии заголовка 3. Поле `hide_url` в Premium API JSON — независимый источник, применяется параллельно > Если хотя бы один источник говорит «скрыть» — URL скрывается. Чтобы снять скрытие, отключите его **во всех** источниках (либо обновите подписку с обновлённым значением). ### Что блокируется - Кнопка **Share** в карточке подписки - Кнопка **Copy URL** - Показ **QR-кода** подписки - Включение подписки в **резервную копию** (даже зашифрованную паролем) - Кнопка экспорта в редакторе серверов подписки ### Что НЕ блокируется - Просмотр и редактирование настроек серверов внутри подписки (на устройствах с **admin HWID** — см. [admin-hwids.md](admin-hwids.md)) - Подключение к серверам, ping, traffic stats, обновление подписки - Резервные копии **отдельных серверов** (если они импортированы вручную, вне этой подписки) ### Правила для Premium-провайдеров | Premium | Admin HWID | `hide-url` | Видит URL | URL в backup | | --- | --- | --- | --- | --- | | нет | — | нет | да | да | | нет | — | да | нет | нет | | да | да | нет | да | да | | да | да | да | да | **нет** (admin видит, но экспортировать через backup нельзя) | | да | нет | — | нет | нет | Без `hide-url` обычные (не-premium) подписки всегда экспортируются и копируются — `hide-url` это **единственный** способ запретить это у не-premium. --- ## Скрытие кнопки «Проверить» Параметр `hide-check` убирает с главного экрана кнопку **«Проверить»** (проверка соединения при активном VPN). Работает по той же схеме, что и [`hide-url`](#скрытие-url-подписки-от-пользователя): tri-state (задано «скрыть» / задано «показать» / не задано → fallback). По умолчанию кнопка показывается. Полезно провайдерам, чей сервер отвечает на check-запрос нестандартно (или блокирует его), из-за чего кнопка вводит пользователя в заблуждение. **Заголовок:** ``` hide-check: 1 ``` **В теле подписки** (только Android и iOS — Desktop игнорирует маркер в теле): ``` #hide-check: 1 vless://... ``` **JSON-тело подписки:** ```json { "hide_check": true } ``` **Premium API (JSON):** ```json { "settings": { "hideCheck": true } } ``` ### Принимаемые значения | Что прислано | Поведение | | --- | --- | | `1`, `true`, `yes` (без учёта регистра) | Кнопка «Проверить» скрыта | | `0`, `false`, `no`, пустая строка | Кнопка показана (по умолчанию) | | Заголовок отсутствует | Fallback на тело / Premium-конфиг, иначе показана | ### Приоритет источников 1. HTTP-заголовок `hide-check` — наивысший приоритет 2. Маркер `#hide-check:` в теле подписки — fallback (Android/iOS) 3. Поле `settings.hideCheck` в Premium API — независимый источник ### Платформы Android, iOS и Desktop — все три скрывают кнопку. Отличие только в источнике «тело подписки»: маркер `#hide-check:` Desktop не читает (используйте заголовок или Premium-конфиг). --- ## Настройки, которые подписка **не может** задать HTTP-заголовками Эти параметры хранятся в [Premium API](premium-api.md) конфигурации провайдера и применяются клиентом только если домен подписки принадлежит Premium-провайдеру. Включить их «через подписку без аккаунта в панели» нельзя — это by design. | Группа | Что настраивается | Ссылка | |-----------------------|-----------------------------------------------------------------------------------|----------------------------------------------------------| | Lite Mode | Упрощённый интерфейс, ссылки на бот/канал/поддержку + [пресет-иконки](icon-presets.md) | [premium-api.md § Lite Mode](premium-api.md#lite-mode) | | Баннер подписки | Текст + цвета + кнопка в карточке подписки | [premium-api.md § Баннер подписки](premium-api.md#баннер-подписки) | | Кастомная тема | Плоские цвета и градиенты для аккаунта / фона | [premium-api.md § Кастомная тема](premium-api.md#кастомная-тема-theme) | | Force-настройки | `forceConnectionStyle`: классическая круглая кнопка vs compact-toggle | [premium-api.md § Принудительные настройки](premium-api.md#принудительные-настройки) | | Ping и сортировка | `defaultPingProtocol`, `defaultSortOrder`, `pingOnUpdate` | [premium-api.md § Ping и сортировка](premium-api.md#ping-и-сортировка) | | Domain fronting | `resolveAddress` / `hostHeader` (связка SNI ↔ Host для маскировки) | [premium-api.md § Domain fronting и фрагментация](premium-api.md#domain-fronting-и-фрагментация) | | Админ-доступ по HWID | `adminHwids` + auto-approve пушей | [admin-hwids.md](admin-hwids.md) | | Push-уведомления | Модерация, таргетинг, отмена | [provider-notifications.md](provider-notifications.md) | > **Фрагментация, шумы и DoH-резолв** доступны через **оба** канала: HTTP-заголовками подписки (см. выше) или через Premium API. При Premium-подписке API-значения приоритетнее заголовков. # Routing Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/routing.md # Маршрутизация (Routing) Приложение поддерживает настройку маршрутизации трафика через профили маршрутизации. В приложении предустановлены геофайлы для работы сразу после установки. Обновление происходит при обновлении профиля или вручную. ## Добавление профилей Профили маршрутизации можно добавлять через: - Буфер обмена - Deeplink-ссылки - QR-коды - HTTP-заголовки (`routing`) - Тело подписки (body) ## Типы ссылок | Формат ссылки | Описание | |---|---| | `incy://routing/add/{base64}` | Добавляет профиль; активируется после успешной загрузки геофайлов | | `incy://routing/onadd/{base64}` | Добавляет и сразу активирует профиль | | `incy://routing/off` | Полностью отключает встроенную маршрутизацию (без данных) | > `{base64}` — JSON-профиль, закодированный в base64. > **`incy://routing/off`** выключает встроенную маршрутизацию целиком — эквивалент выключения тумблера профилей маршрутизации в настройках: конфиг генерируется без правил роутинга. Выбранный профиль сохраняется (обратимо: провайдер может снова включить через `onadd`). > Обратная совместимость: ссылки `://routing/add/` и `://routing/onadd/` также поддерживаются. ## Обработка ошибок Менеджер загрузки геофайлов работает в фоновом режиме: - Загрузки, превышающие 3 минуты, прерываются - Сообщения об ошибках отображаются на главном экране - Проблемные профили отмечаются красным восклицательным знаком в списке - Проблемы исчезают после успешной загрузки файлов или удаления профиля ## HTTP-заголовок Профиль передаётся в заголовке `routing` в base64-формате. Поддерживаются два формата: **Формат 1 — base64 напрямую:** ``` HTTP/2 200 routing: ewogICJOYW1lIjogIlJvc2NvbVZQTiIs... ``` **Формат 2 — полная ссылка (любая схема):** ``` HTTP/2 200 routing: ://routing/onadd/ewogICJOYW1lIjogIlJvc2NvbVZQTiIs... ``` **Формат 3 — отключение маршрутизации:** Значение `off` (или ссылка `://routing/off`) отключает встроенную маршрутизацию целиком — провайдер выключает роутинг прямо из ответа подписки, без действий пользователя: ``` HTTP/2 200 routing: off ``` ## Тело подписки Строка маршрутизации размещается в теле подписки наряду с серверными конфигурациями: ``` vless://uuid@server1:443?security=tls#Server1 vmess://eyJhZGQiOiAic2VydmVyMi... incy://routing/onadd/ewogICJOYW1lIjogIlJvc2NvbVZQTiIs... ``` ## Обновление существующих профилей - Профили с одинаковым полем `Name` обновляются, а не дублируются - Поле `LastUpdated` с Unix timestamp контролирует актуальность — обновление происходит при значении большем, чем у сохранённого профиля --- ## Структура профиля ### Пример профиля ```json { "Name": "RoscomVPN", "GlobalProxy": "true", "RemoteDNSType": "DoH", "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", "RemoteDNSIP": "1.1.1.1", "DomesticDNSType": "DoH", "DomesticDNSDomain": "https://dns.google/dns-query", "DomesticDNSIP": "8.8.8.8", "Geoipurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "Geositeurl": "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "DnsHosts": { "cloudflare-dns.com": "1.1.1.1", "dns.google": "8.8.8.8" }, "DirectSites": ["geosite:ru"], "DirectIp": ["geoip:ru", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255"], "ProxySites": [], "ProxyIp": [], "BlockSites": ["geosite:category-ads-all"], "BlockIp": [], "DomainStrategy": "IPIfNonMatch", "FakeDNS": "false" } ``` ### Описание полей #### Основные настройки | Поле | Тип | По умолчанию | Описание | |---|---|---|---| | `Name` | string | `"Default"` | Имя профиля | | `GlobalProxy` | string | `"true"` | `"true"` — весь трафик через прокси; `"false"` — прямое соединение. Определяет поведение при отсутствии совпадений в правилах маршрутизации | | `LastUpdated` | string | | Unix timestamp. Контролирует принудительное обновление геофайлов при первом сохранении или при обновлении профиля с более новой временной меткой | #### Настройки DNS Система разделяет DNS-запросы на удалённые (Remote, через прокси) и локальные (Domestic, прямое соединение). **Удалённый DNS** (для прокси-ресурсов): | Поле | Тип | По умолчанию | Описание | |---|---|---|---| | `RemoteDNSType` | string | `"DoH"` | Протокол: `DoH`, `DoU` | | `RemoteDNSDomain` | string | `"https://cloudflare-dns.com/dns-query"` | Адрес DNS-сервера (обязателен для DoH) | | `RemoteDNSIP` | string | `"1.1.1.1"` | IP DNS-сервера | | `RemoteDns` | string | | Альтернативное поле для `RemoteDNSIP` (обратная совместимость) | **Локальный DNS** (для прямых ресурсов): | Поле | Тип | По умолчанию | Описание | |---|---|---|---| | `DomesticDNSType` | string | `"DoU"` | Протокол: `DoH`, `DoU` | | `DomesticDNSDomain` | string | `""` | Адрес DNS-сервера (по умолчанию пусто — используется `DomesticDNSIP` по UDP) | | `DomesticDNSIP` | string | `"8.8.8.8"` | IP DNS-сервера | | `DomesticDns` | string | | Альтернативное поле для `DomesticDNSIP` (обратная совместимость) | **Дополнительные DNS-настройки:** | Поле | Тип | Описание | |---|---|---| | `DnsHosts` | object | Ручные DNS-записи (аналог файла hosts). Формат: `{"domain": "ip"}` | | `FakeDNS` | string | `"true"` — подставляет виртуальные IP вместо реальных, чтобы Xray обрабатывал все запросы согласно конфигурации | #### Правила маршрутизации Трафик распределяется по трём категориям: | Поле | Тип | Описание | |---|---|---| | `DirectSites` | string[] | Домены/категории для прямого доступа (без прокси) | | `DirectIp` | string[] | IP-адреса и подсети для прямого доступа | | `ProxySites` | string[] | Домены/категории, направляемые через прокси | | `ProxyIp` | string[] | IP-адреса и подсети через прокси | | `BlockSites` | string[] | Блокируемые домены/категории (реклама, трекеры) | | `BlockIp` | string[] | Блокируемые IP-адреса и подсети | Правила поддерживают гео-категории (`geosite:ru`, `geoip:ru`), конкретные домены и IP/CIDR-подсети. #### Геофайлы | Поле | Тип | Описание | |---|---|---| | `Geoipurl` | string | URL для скачивания `geoip.dat` | | `Geositeurl` | string | URL для скачивания `geosite.dat` | #### Стратегия маршрутизации Поле `DomainStrategy` определяет порядок проверки правил (по умолчанию `AsIs`): | Значение | Описание | |---|---| | `AsIs` | Домены передаются как есть, без DNS-резолва (по умолчанию) | | `IPIfNonMatch` | Сначала проверка по домену; если не совпало — резолв DNS и проверка по IP-правилам | | `IPOnDemand` | Всегда резолвит домены в IP перед проверкой правил | --- ## Геофайлы: оптимизированное скачивание Геофайлы (`geoip.dat`, `geosite.dat`) скачиваются с URL, указанных в профиле. Для экономии трафика используется хеш-проверка. ### Алгоритм 1. Скачивается `{filename}.sha256` (несколько байт) с того же URL 2. Если хеш совпадает с сохранённым **и** файл существует локально → скачивание пропускается (даже при ручном обновлении) 3. Если файл `.sha256` недоступен → fallback на сравнение временных меток `LastUpdated` 4. Полный файл скачивается → вычисляется SHA-256 → файл заменяется 5. Новый хеш сохраняется в профиле ### Рекомендация для провайдеров Размещайте файл `geoip.dat.sha256` и `geosite.dat.sha256` рядом с геофайлами. Файл должен содержать только hex-строку SHA-256 хеша (64 символа). Это позволит клиентам пропускать скачивание неизменившихся файлов и экономить трафик. Пример содержимого `geoip.dat.sha256`: ``` 38c25fea171323e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4 ``` --- ## Обрезка геофайлов (chunk files) Полные geoip/geosite файлы весят десятки мегабайт. Если профиль использует только ограниченное подмножество `geoip:`/`geosite:` тегов, клиент может обрезать скачанные файлы до реально нужных записей. ### Поле профиля | Поле | Тип | Описание | |-----------------|---------|---------------------------------------------------------------------------------------------------------------------| | `useChunkFiles` | boolean | Если `true` — клиент вызывает внутреннее `CutGeoData()` после скачивания и оставляет только упомянутые в правилах теги | - **По умолчанию** `false` на всех платформах — обратная совместимость со старыми профилями. - **Android / iOS** реализуют обрезку через встроенный Go-модуль `incycore.CutGeoData()` (protobuf-парсинг geoip/geosite и выброс лишних записей). - **Desktop (Linux / Windows)** пока использует полные файлы — `useChunkFiles` игнорируется; процесс xray работает с неукороченными `.dat`. - После обрезки хеш пересчитывается локально и сохраняется в профиле — повторное скачивание не запустится, пока исходный `.sha256` на сервере не сменится. ### Когда использовать Включите, если: - В `proxy` / `direct` / `block` используется мало геотегов (например, только `geoip:ru`, `geoip:cn`, `geosite:google`). - Пользователи жалуются на память при загрузке тяжёлых списков. Не включайте, если: - Профиль использует много тегов — обрезка не даст выигрыша. - Есть кастомные geosite-файлы с нестандартным layout — клиент может не распознать структуру. # Autorouting Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/autorouting.md # Автообновляемая маршрутизация (Autorouting) Расширение функционала [маршрутизации](routing.md), позволяющее привязать профиль к удалённому URL-источнику. Профиль периодически скачивается и обновляется автоматически. ## Отличие от Routing | | Routing | Autorouting | |---|---|---| | Данные | Base64-профиль передаётся один раз | URL-источник, профиль скачивается по нему | | Обновление | Только при обновлении подписки | Автоматически по интервалу (по умолчанию 24ч) | | `sourceURL` | Не устанавливается | Устанавливается — профиль привязан к URL | | Индикатор | Нет | Иконка облака в списке профилей | ## Типы ссылок | Формат ссылки | Описание | |---|---| | `://autorouting/onadd/{url}` | Скачивает профиль по URL, устанавливает автообновление и активирует | | `://autorouting/add/{url}` | Скачивает профиль по URL, устанавливает автообновление | > **Важно:** `://routing/onadd/{url}` **не** является авторутингом — это одноразовый импорт без автообновления. Только схема `://autorouting/` устанавливает `sourceURL` и включает автообновление. > `{url}` — прямая ссылка на JSON-файл профиля (начинается с `http://` или `https://`). ### Определение типа Тип определяется **по схеме ссылки**: - `://autorouting/` → **autorouting** (устанавливается `sourceURL`, включается автообновление) - `://routing/` → **routing** (одноразовый импорт, без `sourceURL`, без автообновления) Примеры: ```text ://autorouting/onadd/https://example.com/profile.json → autorouting (автообновление) ://routing/onadd/https://example.com/profile.json → routing (одноразовый импорт) ://routing/onadd/ewogICJOYW1lIjogIlRlc3QiCn0= → routing (base64) ``` ## HTTP-заголовок Заголовок `autorouting` содержит URL, по которому доступен JSON-профиль маршрутизации: ``` HTTP/2 200 autorouting: https://raw.githubusercontent.com/user/repo/main/profile.json ``` Профиль скачивается при обновлении подписки, сохраняется с привязкой к URL-источнику и периодически обновляется. ## Тело подписки Строка autorouting размещается в теле подписки наряду с серверными конфигурациями: ``` vless://uuid@server1:443?security=tls#Server1 vless://uuid@server2:443?security=tls#Server2 ://autorouting/onadd/https://raw.githubusercontent.com/user/repo/main/profile.json ``` ## Приоритет источников При наличии нескольких источников маршрутизации используется **первый найденный**: | Приоритет | Источник | Тип | |---|---|---| | 1 (высший) | Заголовок `autorouting` | Автообновляемый | | 2 | Body — строка с URL | Автообновляемый | | 3 | Заголовок `routing` | Статический | | 4 (низший) | Body — строка с base64 | Статический | --- ## Конвертация GitHub URL Ссылки на файлы в GitHub-репозиториях автоматически конвертируются из «blob» формата в «raw»: ``` https://github.com/user/repo/blob/main/path/profile.json → https://raw.githubusercontent.com/user/repo/main/path/profile.json ``` Это происходит прозрачно при импорте и автообновлении. Можно использовать обычные GitHub-ссылки — приложение само подставит правильный URL. --- ## Контент по URL Файл по URL может содержать: **1. Вложенная deep link ссылка:** Если контент начинается с `incy://` — приложение автоматически извлекает данные профиля из ссылки. Это позволяет размещать `.deeplink` файлы: ```text incy://routing/onadd/ewogICJOYW1lIjogIlJvc2NvbVZQTiIKfQ== ``` Приложение извлечёт base64-данные и декодирует профиль. **2. JSON-профиль (рекомендуемый):** ```json { "Name": "RoscomVPN", "GlobalProxy": "true", "RemoteDNSType": "DoH", "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", "RemoteDNSIP": "1.1.1.1", "Geoipurl": "https://example.com/geoip.dat", "Geositeurl": "https://example.com/geosite.dat", "DirectSites": ["geosite:ru"], "DirectIp": ["geoip:ru"], "DomainStrategy": "IPIfNonMatch" } ``` **3. Base64-закодированный JSON:** ```text ewogICAgIk5hbWUiOiAiUm9zY29tVlBOIiwKICAgICJHbG9iYWxQcm94eSI6ICJ0cnVlIgp9 ``` Приложение пробует: вложенную deep link → JSON → base64 (в порядке приоритета). Структура полей профиля описана в [routing.md](routing.md#структура-профиля). --- ## Автообновление ### Механизм - Приложение проверяет все профили с `sourceURL` каждые 30 минут - Если с момента последнего обновления (`sourceLastUpdated`) прошло больше `updateInterval` — профиль скачивается заново - При обновлении сохраняются: ID профиля, `sourceURL`, `updateInterval`, хеши геофайлов - Если URL геофайлов изменились после обновления — геофайлы перекачиваются автоматически ### Интервалы обновления | Значение (сек) | Отображение | |---|---| | `43200` | 12 часов | | `86400` | 24 часа **(по умолчанию)** | | `259200` | 3 дня | | `604800` | 7 дней | ### Управление в UI Профили с `sourceURL` отображают в списке иконку облака. В редакторе профиля доступна секция «Источник обновлений»: - **URL** — можно просмотреть и изменить URL-источник - **Частота обновления** — выбор интервала обновления - **Обновлено** — время последнего обновления - **Обновить сейчас** — ручное обновление профиля - **Удалить источник** — отвязать профиль от URL (превращает в статический) --- ## Обновление существующих профилей - Профили с одинаковым `Name` обновляются, а не дублируются - При обновлении автообновляемого профиля сохраняются: `sourceURL`, `updateInterval`, хеши геофайлов - Если URL геофайлов (`Geoipurl`, `Geositeurl`) изменились — геофайлы перекачиваются автоматически # Deep Links Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/deep-links.md # Deep Links Приложение поддерживает deep link ссылки для управления VPN, импорта конфигураций и настройки маршрутизации. ## Поддерживаемые схемы Приложение обрабатывает ссылки с любой зарегистрированной схемой (`incy://`, и др.). Также поддерживаются прямые протокольные ссылки. ## Управление VPN | Ссылка | Описание | |---|---| | `://connect` или `://open` | Подключить VPN | | `://disconnect` или `://close` | Отключить VPN | | `://toggle` | Переключить состояние VPN | | `://status` | Открыть приложение (показать статус) | ## Импорт конфигураций | Ссылка | Описание | | --- | --- | | `://import/{data}` | Автоопределение типа данных (URL подписки, конфигурация сервера, несколько URL, сырой WireGuard/AmneziaWG `.conf`) | | `://add/{url}` | Добавить подписку или конфигурацию напрямую | | `://crypt1/{payload}` | Зашифрованный (обфусцированный) вариант `://add/` — см. [Шифрованные ссылки crypt1](#шифрованные-ссылки-crypt1) ниже | `://import/{data}` также принимает **сырой `.conf`** WireGuard/AmneziaWG (многострочный INI с `[Interface]`/`[Peer]`). Для надёжной передачи оберните `.conf` в base64: `incy://import/{base64-conf}` (plain-text тоже поддерживается). AmneziaWG определяется по обфускационным параметрам (`Jc`, `S1`–`S4`, `H1`–`H4`, `I1`–`I5`). Подробнее о `.conf` — [subscription-format.md](subscription-format.md). ### Протокольные ссылки Прямое добавление серверов через протокольные ссылки: ``` vless://uuid@server:443?security=tls&type=ws&sni=example.com#Server Name vmess://eyJhZGQiOiJzZXJ2ZXIiLCJwb3J0Ijo0NDN9 trojan://password@server:443?security=tls&sni=example.com#Server Name ss://method:password@server:8388#Server Name hysteria2://password@server:443?sni=example.com#Server Name socks://user:pass@server:1080#Server Name wireguard://secretKey@server:51820?publickey=KEY&address=10.0.0.2#Server Name ``` ## Маршрутизация | Ссылка | Описание | |---|---| | `://routing/add/{base64}` | Добавить профиль маршрутизации | | `://routing/onadd/{base64}` | Добавить и сразу активировать профиль | | `://routing/onadd/{url}` | Скачать профиль по URL (одноразовый импорт, без автообновления) | | `://autorouting/onadd/{url}` | Скачать профиль по URL и установить автообновление | | `://autorouting/add/{url}` | Скачать профиль по URL и установить автообновление | | `://onadd/{url}` | Сокращённая форма (одноразовый импорт, без автообновления) | | `://routing/off` | Полностью отключить встроенную маршрутизацию (без данных). Эквивалент выключения тумблера профилей маршрутизации в настройках: конфиг генерируется без правил роутинга. Выбранный профиль сохраняется — обратимо через `onadd` | Подробнее: [routing.md](routing.md), [autorouting.md](autorouting.md). ### Query-параметр Для совместимости поддерживается передача данных через query-параметр `data` (Android, iOS): ``` ://routing/add?data={base64} ://routing/onadd?data={base64} ``` ### Определение типа данных Тип определяется **по схеме ссылки**: - `://autorouting/` — автообновление (`sourceURL` устанавливается) - `://routing/` — одноразовый импорт (без `sourceURL`, без автообновления) Если данные после `onadd/` — URL (`http://`/`https://`), профиль скачивается по этому URL. Если base64 — декодируется напрямую. ## Примеры ### Подключение VPN ``` incy://connect ``` ### Импорт подписки ``` incy://import/https://example.com/api/subscription/abc123 ``` ### Добавление сервера ``` incy://add/vless://uuid@server:443?security=tls#MyServer ``` ### Добавление маршрутизации из GitHub ```text incy://autorouting/onadd/https://github.com/user/repo/blob/main/profile.json ``` --- ## Шифрованные ссылки crypt1 `incy://crypt1/` — обфусцированный (зашифрованный) вариант `://add/`. Внутри лежит та же ссылка на подписку, что в обычном `://add/`, но base64url-payload снаружи не позволяет регэкспам и сканерам распознать VPN-URL. ### Когда использовать | Сценарий | Рекомендация | | --- | --- | | Отправка ссылки в Telegram-чат / канал | crypt1 — Telegram-модерация не распознаёт VPN-паттерн | | Публикация на сайте / в FAQ | crypt1 — снижает вероятность авто-блокировки по содержимому | | Внутренний обмен ссылками в админ-панели | plain `://add/` — нет смысла шифровать | | Скриншоты / документация / служебные сообщения | crypt1 — пользователь не «выдаёт» URL даже случайно | ### Wire format ```text incy://crypt1/ ``` Расшифрованный payload — компактный UTF-8 JSON: ```json { "url": "https://sub.example.com/abc123token", "v": 1, "n": "MyProvider VPN" } ``` | Поле | Тип | Описание | | --- | --- | --- | | `url` | string | URL подписки (тот же, что в `://add/{url}`) | | `v` | integer | Версия схемы payload'а, сейчас `1` | | `n` | string? | Опциональное имя провайдера — приложение покажет его в окне подтверждения импорта и предзаполнит поле «Имя» | ### Шифрование - **AES-256-GCM** - Ключ K1 «зашит» в клиенты iOS / Android / Desktop и публикуется в виде [NPM-пакета `@incy/link-encoder`](https://www.npmjs.com/package/@incy/link-encoder) — благодаря этому провайдеры могут собирать crypt1-ссылки из своих ботов и сайтов - При компрометации ключа в новой версии клиентов будет введён `crypt2/` со свежей keymat. **Существующие `crypt1/` ссылки продолжают работать неограниченно долго** — старые схемы из декодера никогда не удаляются. > ⚠️ **Это обфускация, а не криптография.** Цель — не дать автоматическим сканерам распознать VPN-URL. Реверс-инженер с Frida извлечёт ключ из клиента примерно за час. Не используйте crypt1 в задачах, где нужна реальная защита секрета. ### Как генерировать ссылки #### Браузер / онлайн-инструмент Лендинг [incy.cc/encrypt](https://incy.cc/encrypt) делает crypt1 полностью на клиенте через Web Crypto API — ничего не отправляется на сервер. #### NPM-пакет (Node.js, бэкенд, боты) ```bash npm install @incy/link-encoder ``` ```js import { encryptLink } from '@incy/link-encoder'; const link = encryptLink('https://sub.your-provider.example/abc123token', { name: 'My Provider VPN', }); console.log(link); // → incy://crypt1/AAECAwQFBgcICQoLNyIQL3rDwRZqnyoD8pGK… ``` API: ```ts encryptLink(url: string, opts?: { name?: string }): string decryptLink(link: string): { url: string; name?: string } ``` Подробнее — [README пакета](https://github.com/INCY-DEV/incy-link-encoder). ### Поведение приложения при импорте | Шаг | Что делает клиент | | --- | --- | | 1 | Пользователь тапает / сканирует / вставляет `incy://crypt1/` | | 2 | Клиент декодирует payload, восстанавливает `url` | | 3 | Открывается окно подтверждения импорта (как у `://add/`), URL подписки **скрыт** под `••••` чтобы не «утечь» в скриншоте | | 4 | Если в payload есть `n` — отображается как имя провайдера в этом окне и пред-заполняется в поле «Имя» | | 5 | После подтверждения — обычный flow импорта подписки | | 6 | Внутренний флаг `importedViaCrypt1=true` сохраняется у созданной подписки. При последующем Share/Copy/QR клиент **снова** выдаёт crypt1, не обнажая URL | ### Сохранение wire-format'а при шеринге При нажатии **Share** / **Copy URL** / **Show QR** на карточке подписки клиент эмитит тот же формат, в котором подписка была добавлена: - Добавлена через `https://...` → Share выдаёт `https://...` - Добавлена через `incy://crypt1/...` → Share выдаёт `incy://crypt1/...` Это гарантирует, что обфускация не теряется по цепочке пересылок: если провайдер изначально опубликовал crypt1-ссылку, все пересылки её копии в Telegram-чатах остаются crypt1-овыми. ### Совместимость | Версия INCY | Поддержка | | --- | --- | | iOS / Android / Desktop ≥ июнь 2026 | Да, нативный handler `incy://crypt1/` | | Более старые версии | Ссылка не открывается — пользователю нужно обновиться | | Сторонние VPN-клиенты (V2Box, Shadowrocket, Happ) | Не поддерживается, scheme зарегистрирован только для INCY | ### Примеры crypt1 #### Простая ссылка ```text incy://crypt1/FZEVXuV39UEX1yHB3nkrgdPdrJ3syVxcQm_Y-lY0oKWAT5yRn00xe6ohg06aVWjWRrGJ7BAeEzuoFzv8XBosLnqnqqCMbnAJmR7EN2hII4Yyql1FtWlLlLs ``` #### С брендингом провайдера в QR-коде QR-код, сгенерированный из crypt1-ссылки с полем `n`, при сканировании в INCY покажет пользователю «MyProvider VPN — Подтвердить импорт?» — провайдер брендится в окне подтверждения без серверного round-trip. # Передача подписки на ТВ Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/tv-relay.md # Передача подписки на ТВ Перенос подписки на Apple TV / Android TV с телефона — из любой сети, даже если устройства находятся на разных концах города. Ключевое свойство: **релей не может прочитать то, что через него передаётся**. Ссылка на подписку шифруется на телефоне и расшифровывается только на телевизоре. Сервер видит лишь два непрозрачных блока байтов. - **Адрес:** `https://check.incytv.com` - **Исходный код:** [INCY-DEV/incy-tv-relay](https://github.com/INCY-DEV/incy-tv-relay) - **Образ:** [ghcr.io/incy-dev/incy-tv-relay](https://github.com/orgs/INCY-DEV/packages/container/package/incy-tv-relay) Код открыт целиком — можно убедиться, что сервер действительно ничего не расшифровывает, и при желании поднять свой релей. --- ## Как это работает На телевизоре появляется 8-значный код. Вы вводите его на телефоне (или сканируете QR) — и подписка переезжает. ``` Телевизор Релей Телефон │ │ │ │ занимает код │ │ │───────────────────────>│ │ │ │ │ │ показывает │ читает параметры │ │ K7M2XQ4P + QR │<───────────────────────│ │ │ │ │ │ шифрует подписку │ │ │ ключом из кода │ │ │ │ │ │<─── шифротекст ────────│ │<─── шифротекст ────────│ │ │ │ │ │ расшифровывает │ запись удаляется │ ``` Обе стороны выводят общий ключ шифрования **из одного лишь кода** — по протоколу CPace. Релей, наблюдая весь обмен целиком, этот ключ вывести не может: он видит только публичные значения, из которых секрет не восстанавливается. !!! info "Почему 8 знаков — это достаточно" Обычно короткий код означает слабую защиту. Здесь иначе: перехватив трафик, злоумышленник **не может подбирать код у себя на компьютере**. Каждая попытка требует обращения к серверу, а там лимит — 5 попыток, после чего код уничтожается. Поэтому ручной ввод кода так же надёжен, как QR — в обоих случаях передаётся только код, а ключ вычисляется на устройствах. --- ## Что видит сервер | Данные | Хранит | Комментарий | |---|---|---| | Ссылка на подписку | **нет** | зашифрована, ключа у сервера нет | | Название провайдера | **нет** | внутри того же шифротекста | | IP-адреса устройств | **нет** | не пишутся ни в базу, ни в логи | | История переносов | **нет** | запись удаляется сразу после доставки | | Два блока байтов | 5 минут | затем исчезают автоматически | Тела запросов не логируются — там шифротекст, но логи имеют свойство утекать, поэтому их просто нет. --- ## Локальный перенос без интернета Если телефон и телевизор в одной сети Wi-Fi, приложение использует **прямое соединение** (Bonjour, `_incy-tv._tcp`) — данные вообще не покидают домашнюю сеть и не идут через сервер. Релей включается только тогда, когда прямое соединение невозможно. Разница пользователю не показывается — работает и так, и так. --- ## API для интеграции Своим ботам и панелям можно передавать подписку на телевизор пользователя тем же способом. Все тела — JSON, бинарные поля — base64url без паддинга. ### Занять код Вызывает телевизор. ```http POST /pair/init Content-Type: application/json { "code": "K7M2XQ4P", "sid": "<16 байт>", "ya": "<32 байта>" } ``` | Ответ | Значение | |---|---| | `204` | принято | | `409` | код занят — сгенерировать новый и повторить | | `400` | неверный формат кода или точки | ### Прочитать параметры Вызывает отправитель. ```http GET /pair/K7M2XQ4P ``` ```json { "sid": "<16 байт>", "ya": "<32 байта>" } ``` | Ответ | Значение | |---|---| | `200` | параметры получены | | `404` | кода нет или истёк | | `429` | превышен лимит попыток, код уничтожен | !!! warning "Каждый вызов считается попыткой" Это и есть защита от перебора. После 5 обращений код сгорает — не опрашивайте эндпоинт в цикле. ### Отправить подписку ```http POST /pair/K7M2XQ4P/send Content-Type: application/json { "yb": "<32 байта>", "ct": "" } ``` | Ответ | Значение | |---|---| | `204` | принято | | `404` | кода нет или истёк | | `409` | подписка уже отправлена (одноразово) | | `413` | тело больше 64 КиБ | ### Забрать подписку Вызывает телевизор. Запрос висит до 30 секунд, ожидая отправителя. ```http GET /pair/K7M2XQ4P/result ``` | Ответ | Значение | |---|---| | `200` | `{ "yb": …, "ct": … }`, запись сразу удаляется | | `204` | пока ничего — повторить запрос | | `404` | код истёк | ### Служебные `GET /healthz` — жив ли сервис. `GET /readyz` — готов ли принимать запросы. --- ## Ограничения | Что | Лимит | |---|---| | Попыток на один код | 5, дальше код уничтожается | | Запросов с одного IP | 300 в минуту | | Размер тела | 64 КиБ | | Время жизни кода | 5 минут | Лимит на код важнее лимита на IP: смена адреса от него не спасает, поэтому перебор бессмыслен. Отдельного лимита на создание кодов нет: за одним внешним адресом (CGNAT оператора, общежитие) могут быть сотни устройств, и почасовой лимит отрезал бы часть пользователей. --- ## Что передавать Внутри шифротекста — JSON: ```json { "v": 1, "type": "subscription", "url": "https://example.com/sub", "name": "Мой провайдер" } ``` Поле `ct` — это `nonce ‖ шифротекст ‖ тег` одним куском: раскладка `combined` у CryptoKit, которую libsodium понимает напрямую. Отдельного поля `nonce` нет. Передавайте **ссылку на подписку, а не развёрнутый конфиг**: телевизор сам её загрузит и получит актуальный список серверов. Так объём на порядки меньше, и конфигурация всегда свежая. --- ## Криптография Для совместимости с приложением параметры должны совпадать байт в байт. | Параметр | Значение | |---|---| | Группа | Ristretto255 | | Протокол | CPace | | Шифрование | ChaCha20-Poly1305 | | Вывод ключа | HKDF-SHA256 | | Алфавит кода | `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` (без `I`, `O`, `0`, `1`) | ``` generator = ristretto255_from_hash( SHA-512(dsi ‖ code ‖ sid) ) scalar = ristretto255_scalar_reduce( 64 случайных байта ) Y = ristretto255_scalarmult( scalar, generator ) K = ristretto255_scalarmult( scalar, Y_peer ) key = HKDF-SHA256( K ‖ min(Ya,Yb) ‖ max(Ya,Yb), salt=sid, info=dsi, 32 ) dsi = "CPace255-INCY-TVRELAY-v1" ``` Точки сортируются лексикографически — чтобы обе стороны хешировали их в одном порядке, не договариваясь, кто «первый». Контрольный вектор для проверки реализации: ``` generator("TESTCODE", sid = 000102…0f) = def51453cb5cfdb7d78e667cf7575060841474e063f5e39ea28f14fd9340042f ``` Библиотеки: **libsodium** (iOS/tvOS — `swift-sodium`), **lazysodium** (Android — `com.goterl:lazysodium-android`). Ristretto255 доступен в обеих без рукописных биндингов. !!! tip "Проверяйте совместимость тестом" Расхождение ключей между платформами означает, что перенос просто не сработает. Сверьте контрольный вектор выше — это быстрее, чем отлаживать «не расшифровывается». --- ## Пример: бот на Python Отправка подписки на телевизор пользователя по введённому коду. ```python import base64, json, secrets, httpx from nacl.bindings import ( crypto_core_ristretto255_from_hash, crypto_core_ristretto255_scalar_random, crypto_scalarmult_ristretto255, ) from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes import hashlib BASE = "https://check.incytv.com" DSI = b"CPace255-INCY-TVRELAY-v1" b64d = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) b64e = lambda b: base64.urlsafe_b64encode(b).decode().rstrip("=") def send_subscription(code: str, url: str, name: str) -> None: # 1. Читаем параметры телевизора. Это считается попыткой — # повторять при неудаче нельзя, после 5 раз код сгорит. r = httpx.get(f"{BASE}/pair/{code}", timeout=10) if r.status_code == 404: raise ValueError("код не найден или истёк") if r.status_code == 429: raise ValueError("код заблокирован из-за неверных попыток") r.raise_for_status() sid, ya = b64d(r.json()["sid"]), b64d(r.json()["ya"]) # 2. Выводим общий ключ из кода — сервер этого сделать не может. generator = crypto_core_ristretto255_from_hash( hashlib.sha512(DSI + code.encode() + sid).digest() ) scalar = crypto_core_ristretto255_scalar_random() yb = crypto_scalarmult_ristretto255(scalar, generator) shared = crypto_scalarmult_ristretto255(scalar, ya) lo, hi = sorted([ya, yb]) # порядок точек одинаков у обеих сторон key = HKDF(hashes.SHA256(), 32, sid, DSI).derive(shared + lo + hi) # 3. Шифруем и отправляем. payload = json.dumps( {"v": 1, "type": "subscription", "url": url, "name": name} ).encode() nonce = secrets.token_bytes(12) # nonce идёт первыми 12 байтами ct — отдельного поля нет. ct = nonce + ChaCha20Poly1305(key).encrypt(nonce, payload, None) resp = httpx.post( f"{BASE}/pair/{code}/send", json={"yb": b64e(yb), "ct": b64e(ct)}, timeout=10, ) if resp.status_code == 409: raise ValueError("на этот код уже отправляли подписку") resp.raise_for_status() ``` Зависимости: `pynacl`, `cryptography`, `httpx`. --- ## Пример: бот на Node.js ```js import sodium from 'libsodium-wrappers-sumo'; import { createHash, hkdfSync, randomBytes, createCipheriv } from 'node:crypto'; const BASE = 'https://check.incytv.com'; const DSI = Buffer.from('CPace255-INCY-TVRELAY-v1'); const b64e = (b) => Buffer.from(b).toString('base64url'); const b64d = (s) => Buffer.from(s, 'base64url'); export async function sendSubscription(code, url, name) { await sodium.ready; // 1. Параметры телевизора (одна попытка — лимит 5 на код). const r = await fetch(`${BASE}/pair/${code}`); if (r.status === 404) throw new Error('код не найден или истёк'); if (r.status === 429) throw new Error('код заблокирован'); if (!r.ok) throw new Error(`релей вернул ${r.status}`); const { sid: sidB64, ya: yaB64 } = await r.json(); const sid = b64d(sidB64); const ya = b64d(yaB64); // 2. Общий ключ выводится из кода на устройствах, не на сервере. const generator = sodium.crypto_core_ristretto255_from_hash( createHash('sha512').update(Buffer.concat([DSI, Buffer.from(code), sid])).digest(), ); const scalar = sodium.crypto_core_ristretto255_scalar_random(); const yb = sodium.crypto_scalarmult_ristretto255(scalar, generator); const shared = sodium.crypto_scalarmult_ristretto255(scalar, ya); // Лексикографический порядок точек — одинаковый у обеих сторон. const [lo, hi] = [Buffer.from(ya), Buffer.from(yb)].sort(Buffer.compare); const key = Buffer.from(hkdfSync( 'sha256', Buffer.concat([Buffer.from(shared), lo, hi]), sid, DSI, 32, )); // 3. Шифруем и отправляем. const payload = Buffer.from(JSON.stringify( { v: 1, type: 'subscription', url, name }, )); const nonce = randomBytes(12); const cipher = createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 }); // nonce первыми 12 байтами ct — отдельного поля нет. const ct = Buffer.concat([ nonce, cipher.update(payload), cipher.final(), cipher.getAuthTag(), ]); const resp = await fetch(`${BASE}/pair/${code}/send`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ yb: b64e(yb), ct: b64e(ct) }), }); if (resp.status === 409) throw new Error('на этот код уже отправляли'); if (!resp.ok) throw new Error(`релей вернул ${resp.status}`); } ``` Зависимость: `libsodium-wrappers-sumo` (обычная сборка `libsodium-wrappers` не содержит функций Ristretto255). --- ## Типичные ошибки **`404` на все запросы.** Код истёк — он живёт 5 минут. Попросите пользователя открыть экран добавления на телевизоре заново. **`429` при первом же обращении.** Кто-то уже перебирал этот код, и он уничтожен. Нужен новый код с телевизора. **Телевизор пишет «неверный код».** MAC не сошёлся: либо код введён с ошибкой, либо реализация криптографии расходится. Проверьте контрольный вектор. **`409` при отправке.** На этот код уже отправляли подписку — она одноразовая. **Пустой ответ `204` от `/result`.** Это норма: отправитель ещё не прислал данные. Повторите запрос, соединение держится до 30 секунд. --- ## Свой релей Сервер не хранит ничего постоянного, поэтому его легко поднять самостоятельно: ```bash git clone https://github.com/INCY-DEV/incy-tv-relay cd incy-tv-relay npm install REDIS_URL=redis://localhost:6379 npm start ``` Нужен только Redis. В репозитории есть готовые манифесты Kubernetes и `Dockerfile`. Приложение по умолчанию использует `check.incytv.com`; адрес своего релея задаётся в настройках сборки. # HWID Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/hwid.md # HWID (Hardware ID) Аппаратный идентификатор устройства, используемый для привязки подписок к конкретным устройствам и отслеживания активных подключений. ## Формат HWID — строка в формате UUID (`8-4-4-4-12`, верхний регистр), производная от аппаратных характеристик устройства. Считается SHA-256 хеш от характеристик (с солью `incy_hwid_`), и его hex-дайджест форматируется в UUID. На всех платформах одинаково — 36 символов с дефисами, например `270DD26E-160D-4257-B8AC-654800E12F24`. ## Генерация по платформам ### Android **Компоненты** (объединяются через `|`): | Компонент | Источник | |---|---| | Android ID | `Settings.Secure.ANDROID_ID` | | Производитель | `Build.MANUFACTURER` | | Модель | `Build.MODEL` | | Бренд | `Build.BRAND` | | Устройство | `Build.DEVICE` | | Продукт | `Build.PRODUCT` | | Плата | `Build.BOARD` | | Оборудование | `Build.HARDWARE` | **Алгоритм:** ``` deviceId = SHA256("androidId|manufacturer|model|brand|device|product|board|hardware") hwid = hashToUuid(SHA256("incy_hwid_" + deviceId)) // hex → UUID 8-4-4-4-12 ``` **Результат:** UUID (36 символов, верхний регистр) **Хранение:** EncryptedSharedPreferences (сохраняется при переустановке) ### Desktop (Linux) **Компоненты** (объединяются через `|`): | Компонент | Источник | |---|---| | Machine ID | `/etc/machine-id` | | Hostname | `InetAddress.getLocalHost().hostName` | | OS | `System.getProperty("os.name")` | | Архитектура | `System.getProperty("os.arch")` | | Пользователь | `System.getProperty("user.name")` | ### Desktop (Windows) **Компоненты** (объединяются через `|`): | Компонент | Источник | |---|---| | Machine GUID | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` | | Hostname | `InetAddress.getLocalHost().hostName` | | OS | `System.getProperty("os.name")` | | Архитектура | `System.getProperty("os.arch")` | | Пользователь | `System.getProperty("user.name")` | **Алгоритм (Linux/Windows):** ``` deviceId = SHA256("machineId|hostname|os|arch|user") hwid = hashToUuid(SHA256("incy_hwid_" + deviceId)) // hex → UUID 8-4-4-4-12 ``` **Результат:** UUID (36 символов, верхний регистр) **Хранение:** файл `{configDir}/device_id` ### iOS **Компоненты:** | Компонент | Источник | |---|---| | Vendor ID | `UIDevice.current.identifierForVendor` | | Имя устройства | Модель устройства | **Алгоритм:** ``` raw = "incy_hwid_{vendorID}-{deviceName}" hwid = hashToUuid(SHA256(raw)) // hex → UUID 8-4-4-4-12 ``` **Результат:** UUID (36 символов, верхний регистр) **Хранение:** Keychain (сохраняется даже при удалении приложения) > Формат одинаков на всех платформах — UUID из SHA-256 хеша. `shortHWID` (первые 8 символов) используется только для отображения. ## Использование ### HTTP-заголовки запроса При включённой отправке HWID приложение добавляет заголовки к запросам подписки: ```http x-hwid: x-device-os: x-ver-os: x-device-model: ``` ### Premium API — проверка лимита устройств При запросе конфигурации провайдера клиент передаёт хеш HWID: ```http GET /api/subscription/config?h=&hwid= ``` Сервер использует `hwidHash` для проверки, зарегистрировано ли устройство у данного провайдера, и принятия решения о выдаче premium-статуса в рамках [лимита устройств](premium-api.md#лимиты-устройств). ### Поле `hwidHash` в регистрации При регистрации устройства помимо сырого `hwid` сервер хранит `hwidHash = SHA256(hwid)`. Это позволяет находить устройство по хешу без знания исходного идентификатора. ### Принудительная отправка Провайдер может включить принудительную отправку HWID через Premium API: ```json { "settings": { "alwaysHwidEnable": true } } ``` При этом пользователь не может отключить отправку HWID для данной подписки. ## Short HWID Для отображения в UI используются первые 8 символов HWID. # Premium API Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/premium-api.md # Premium API Система управления подписками с шифрованием на основе доменных хешей. Позволяет провайдерам настраивать внешний вид приложения, параметры серверов и уведомления для своих пользователей. ## Обзор Приложение при добавлении подписки извлекает домен из URL и запрашивает конфигурацию провайдера через зашифрованный API. Домен никогда не передаётся в открытом виде — используется SHA-256 хеш. --- ## API ### Endpoint ``` GET /api/subscription/config?h=&hwid= ``` ### Запрос | Параметр | Тип | Обязательный | Описание | | --- | --- | --- | --- | | `h` | string | да | SHA-256 хеш домена (64 hex-символа) | | `hwid` | string | нет | SHA-256 хеш HWID устройства (64 hex-символа). Для проверки лимита устройств | Хеш вычисляется от домена в нижнем регистре: ``` SHA256("example.com") → "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947" ``` ### Ответ Сервер всегда возвращает зашифрованный ответ: ```json { "encrypted": true, "iv": "", "data": "", "tag": "" } ``` ### Поля ответа (после расшифровки) | Поле | Тип | Описание | | --- | --- | --- | | `success` | boolean | Успешность запроса | | `domain` | string | Домен подписки | | `isPremium` | boolean | Активен ли Premium у провайдера | | `deviceLimitExceeded` | boolean | Лимит устройств провайдера превышен (см. [Лимиты устройств](#лимиты-устройств)) | | `logoUrl` | string? | URL логотипа провайдера | | `settings` | object | Настройки провайдера (см. ниже) | | `theme` | object | Кастомная тема (см. ниже) | ### Rate Limit 30 запросов в минуту на IP-адрес. При превышении — ответ `429`. --- ## Шифрование Ответы API зашифрованы алгоритмом AES-256-GCM. Ключ шифрования деривируется на основе домена — только клиент, знающий оригинальный домен и комбинацию, может расшифровать ответ. Компоненты ответа: | Поле | Описание | |---|---| | `iv` | Initialization vector (base64, 12 байт) | | `data` | Зашифрованные данные (base64) | | `tag` | Authentication tag (base64, 16 байт) | > Детали деривации ключа и реализация шифрования являются внутренними и не публикуются. --- ## Структура конфигурации ### Настройки провайдера (`settings`) #### Базовые | Поле | Тип | Описание | |---|---|---| | `serverDescription` | string | Описание сервера (до 30 символов) | | `alwaysHwidEnable` | boolean | Принудительная отправка HWID (пользователь не может отключить) | | `expiryNotifications` | boolean | Локальные уведомления об истечении подписки | | `showServerDescription` | boolean | Показывать описание серверов. По умолчанию `true` | #### Domain fronting и фрагментация | Поле | Тип | Описание | |---|---|---| | `resolveAddress` | string | IP для domain fronting | | `hostHeader` | string | Host-заголовок для domain fronting | | `fragmentEnabled` | boolean | TCP-фрагментация в hev-socks5-tunnel | | `fragmentLength` | string | Диапазон длины фрагментов (напр. `"10-30"`) | | `fragmentInterval` | string | Диапазон интервала (напр. `"20-40"` мс) | | `fragmentPackets` | string | Количество фрагментов (напр. `"5-10"`) | #### DNS-резолв адреса сервера (DoH) | Поле | Тип | Описание | |---|---|---| | `serverAddressResolveEnable` | boolean | Предварительный резолв адреса сервера через DoH | | `serverAddressResolveDnsDomain` | string | URL DoH-сервера (напр. `https://common.dot.dns.yandex.net/dns-query`) | | `serverAddressResolveDnsIp` | string | IP DoH-сервера (используется до резолва его домена) | #### Lite Mode Упрощённый интерфейс с ссылками на бот, канал, поддержку и (опционально) премиум. Подробнее про иконки — [icon-presets.md](icon-presets.md). | Поле | Тип | Описание | |---|---|---| | `liteMode` | boolean | Включить упрощённый режим | | `botUrl` | string? | Ссылка на Telegram-бот | | `channelUrl` | string? | Ссылка на Telegram-канал | | `supportUrl` | string? | Ссылка на поддержку | | `botIconKey` | string? | Ключ иконки бота из пресет-набора (см. [icon-presets.md](icon-presets.md)) | | `channelIconKey` | string? | Ключ иконки канала | | `supportIconKey` | string? | Ключ иконки поддержки | > `null` или неизвестный ключ → клиент использует дефолтную иконку (`send` для бота, `megaphone` для канала, `help` для поддержки). #### Админ-доступ (admin HWIDs) Устройства в списке `adminHwids` могут: - Просматривать и редактировать конфиги серверов прямо в приложении. - Отправлять провайдерские уведомления **без модерации** (auto-approve при совпадении с `targetSegment.hwid`). Подробности — [admin-hwids.md](admin-hwids.md). | Поле | Тип | Описание | |--------------|------------|-----------------------------------------| | `adminHwids` | `string[]` | Список HWID устройств с админ-правами | #### Баннер подписки Баннер произвольного цвета внутри карточки подписки. Используется для анонсов и технических работ («тех. работы на сервере N»). Показывается **всем** пользователям подписки. | Поле | Тип | Описание | |---|---|---| | `bannerEnabled` | boolean | Показывать баннер | | `bannerText` | string? | Текст баннера (белым по `bannerBgColor`) | | `bannerButtonText` | string? | Текст кнопки (если пусто — кнопка скрыта) | | `bannerButtonUrl` | string? | URL кнопки | | `bannerBgColor` | string? | Цвет фона баннера (hex, напр. `"#E53E3E"`, по умолчанию красный) | | `bannerButtonColor` | string? | Цвет кнопки (hex, по умолчанию зелёный) | #### Баннер об истечении подписки Отдельный баннер, который показывается **только** пользователям, у которых до истечения подписки осталось **3 дня или меньше** (дни считаются на устройстве по дате истечения подписки). Приглашает продлить подписку. Набор полей независим от баннера выше. | Поле | Тип | Описание | |---|---|---| | `expiryBannerEnabled` | boolean | Показывать баннер об истечении | | `expiryBannerText` | string? | Текст баннера | | `expiryBannerButtonText` | string? | Текст кнопки (если пусто — кнопка скрыта) | | `expiryBannerButtonUrl` | string? | URL кнопки (если не задан — кнопка не показывается) | | `expiryBannerBgColor` | string? | Цвет фона (hex, по умолчанию `"#D97706"`) | | `expiryBannerButtonColor` | string? | Цвет кнопки (hex, по умолчанию зелёный) | **Приоритет:** если включён баннер «тех. работы» (`bannerEnabled`), показывается **только он** (всем). Баннер об истечении показывается лишь когда `bannerEnabled` выключен, `expiryBannerEnabled` включён и подписка пользователя истекает в течение 3 дней. Платформы: iOS, Android, Desktop (incy-linux). #### Принудительные настройки Переопределяют выбор пользователя в настройках приложения, пока подписка активна. | Поле | Тип | Описание | |------------------------|-----------|-------------------------------------------------------------------------------------------------------| | `forceConnectionStyle` | `string?` | Стиль кнопки подключения: `"classic"` (крупная круглая кнопка) или `"compact"` (узкий toggle внизу) | #### Ping и сортировка Применяются при первом подключении подписки. На iOS и Android эти значения задаются **per-подписку** (у каждой подписки свои пинг/протокол/сортировка/стиль); пользователь может переопределить их в настройках подписки. На Desktop переопределяют глобальные настройки приложения. | Поле | Тип | Описание | |---|---|---| | `defaultPingProtocol` | string? | Тип пинга: `incy`, `tcp`, `proxy_head`, `proxy_get`, `icmp` | | `pingTestUrl` | string? | URL для HTTP-пинга (используется при `incy` / `proxy_head` / `proxy_get`) | | `defaultSortOrder` | string? | Сортировка серверов: `none`, `ping`, `name` | | `defaultPingDisplayFormat` | string? | Вид отображения пинга: `time` (цифры, мс), `bars` (шкала-полоски), `both` (цифры + шкала) или `dots` (зелёные точки — ответил/нет). Режим `dots` поддерживается только на iOS; Android/Desktop показывают `time`/`bars`/`both` | | `pingOnUpdate` | boolean? | Авто-пинг всех серверов после обновления подписки | `incy` — **INCY Ping**: реальный HTTP GET через прокси, но значение делится на ~3.3, чтобы читаться как привычный пинг (а не полный проксированный round-trip). Метод по умолчанию в приложении. #### Контент | Поле | Тип | Описание | |---|---|---| | `announceUrl` | string? | URL для объявлений провайдера в приложении | | `webPageUrl` | string? | Ссылка на веб-страницу провайдера | #### Резервные домены (`fallbackHosts`) | Поле | Тип | Описание | |---|---|---| | `fallbackHosts` | string[] | Список запасных хостов. Если основной домен подписки не отвечает при обновлении, клиент повторяет тот же запрос, подставляя эти хосты по очереди (меняется только хост, путь/токен те же). Перебираются сверху вниз. | > Клиент кэширует `fallbackHosts` на подписке, поэтому перебор работает даже когда основной домен полностью недоступен. На `404`/`410` (подписка удалена) перебор не выполняется. В премиум-панели задаётся кнопкой «Резервные» рядом с доменом. ### Кастомная тема (`theme`) Расширенная палитра, применяется только когда пользователь видит premium-подписку. Поддерживает плоские цвета и 2-4-стоповые градиенты. #### Плоские цвета | Поле | Тип | Описание | |---|---|---| | `enabled` | boolean | Кастомная тема включена | | `darkMode` | boolean | Тёмный режим | | `accent` | string | Основной акцентный цвет (hex, напр. `"#FF6B6B"`) | | `accentSecondary` | string | Вторичный акцентный цвет | | `background` | string | Цвет фона | | `card` | string | Цвет карточек | | `surface` | string | Цвет поверхности | | `textPrimary` | string | Основной цвет текста | | `textSecondary` | string | Вторичный цвет текста | #### Градиенты Если задан, перекрывает плоские `accent`/`accentSecondary` для акцентных элементов и `background` для фона. | Поле | Тип | Описание | |---|---|---| | `accentGradient.stops` | GradientStop[] | 2-4 стопа (`{ color, position }`), `position` в диапазоне `0.0..1.0` | | `accentGradient.angle` | number | Угол градиента в градусах `0..360` | | `backgroundGradient.enabled` | boolean | Включить градиентный фон | | `backgroundGradient.stops` | GradientStop[] | 2-3 стопа | | `backgroundGradient.angle` | number | Угол градиента | Каждый стоп: ```json { "color": "#B8D94A", "position": 0.0 } ``` --- ## Лимиты устройств Premium-провайдеры имеют тарифы с ограничением по количеству устройств. Лимит задаётся полем `maxDevices` провайдера. ### Как работает 1. Клиент отправляет `hwid=SHA256(rawHwid)` в запросе конфигурации 2. Сервер проверяет, зарегистрировано ли устройство с таким `hwidHash` для доменов данного провайдера 3. Если устройство **уже зарегистрировано** — всегда получает premium (существующие устройства не блокируются) 4. Если устройство **не зарегистрировано** — сервер считает общее количество устройств провайдера: - Если `totalDevices < maxDevices` — premium выдаётся - Если `totalDevices >= maxDevices` — возвращается `isPremium: false` + `deviceLimitExceeded: true` ### Поведение клиента при `deviceLimitExceeded: true` - Premium-функции отключены (как при `isPremium: false`) - Кастомная тема провайдера не применяется - Premium-настройки (фрагментация, fronting и т.д.) не используются - VPN продолжает работать в базовом режиме - Устройство регистрируется (но без premium) ### Тарифы | Лимит | Описание | | --- | --- | | `1000` | Стандартный тариф | | `3000` | Средний тариф | | `6000` | Максимальный тариф | | `null` | Безлимитный (нет ограничений) | --- ## Регистрация устройства При добавлении подписки приложение регистрирует устройство для отслеживания активных подключений и push-уведомлений. ### Данные регистрации | Поле | Тип | Описание | | --- | --- | --- | | `hwid` | string | Аппаратный идентификатор устройства ([подробнее](hwid.md)) | | `hwidHash` | string | SHA-256 хеш HWID (для серверной проверки лимитов) | | `uid` | string | Анонимный UID устройства | | `platform` | string | `"android"`, `"ios"`, `"linux"`, `"windows"`, `"macos"` | | `appVersion` | string | Версия приложения | | `osVersion` | string | Версия ОС | | `locale` | string | Язык устройства | | `subscriptionDomainHash` | string | SHA-256 хеш домена подписки | | `fcmToken` | string | Push-токен для уведомлений (FCM / APNs) | | `lastActive` | timestamp | Время последней активности | > Поле `hwidHash` добавляется при регистрации и используется сервером для проверки лимита устройств без знания исходного HWID. ### Синхронизация подписки Сервер может установить флаг `subscriptionNeedsSync = true` на устройстве. При обнаружении этого флага приложение: 1. Читает поле `subscriptionNewDomain` из документа устройства 2. Обновляет URL подписки на новый домен 3. Сбрасывает флаг `subscriptionNeedsSync` Это позволяет провайдеру мигрировать пользователей на новый домен при блокировке старого. --- ## Кеширование - **In-memory кеш:** конфигурация хранится в памяти на время работы приложения - **Disk кеш:** сохраняется в SecureStorage для offline-доступа - **Fallback:** при ошибке `503` используется кешированная конфигурация - **Очистка:** конфигурация удаляется из кеша, если домен больше не является premium --- ## Certificate Pinning Запросы к Premium API на Android и Desktop защищены certificate pinning (SHA-256 пины TLS-сертификата). Это предотвращает MITM-атаки на канал связи с API. # Документация для ИИ (MCP) Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/ai-mcp.md # Документация для ИИ (MCP / llms.txt) Документацию INCY можно подключить к ИИ-ассистентам (Claude Code, Codex, Cursor и др.), чтобы они автоматически подтягивали актуальные форматы подписок, заголовки, deep-links и Premium API — и писали интеграцию правильнее и быстрее, без ручного копипаста. Есть два способа: **MCP-сервер** (инструмент для ИИ) и **llms.txt** (машинно-читаемый индекс). ## Способ 1. MCP через GitMCP (рекомендуется) [GitMCP](https://gitmcp.io) — бесплатный публичный MCP-сервер, который читает документацию прямо из нашего GitHub-репозитория. Ничего устанавливать не нужно — только подключить URL. **URL сервера:** ``` https://gitmcp.io/INCY-DEV/incy-docs ``` ### Claude Code ```bash claude mcp add --transport http incy-docs https://gitmcp.io/INCY-DEV/incy-docs ``` ### Cursor / Codex / другие клиенты Добавьте сервер в MCP-конфиг клиента: ```json { "mcpServers": { "incy-docs": { "url": "https://gitmcp.io/INCY-DEV/incy-docs" } } } ``` После подключения ИИ получает инструменты для поиска и чтения документации INCY. Например, на запрос «сделай интеграцию: скрой URL подписки и добавь баннер» ассистент сам найдёт заголовки `hide-url`, `banner-text` в документации и применит их корректно. ## Способ 2. llms.txt Для инструментов, поддерживающих стандарт [llms.txt](https://llmstxt.org), мы публикуем два файла: | Файл | Что внутри | | --- | --- | | [`llms.txt`](https://docs.incy.cc/llms.txt) | Индекс: список всех разделов документации со ссылками и кратким описанием | | [`llms-full.txt`](https://docs.incy.cc/llms-full.txt) | Полная документация одним файлом (весь текст) | Просто дайте ассистенту ссылку `https://docs.incy.cc/llms-full.txt` — он получит всю документацию сразу. ## Что можно спросить у ИИ - «Какие HTTP-заголовки подписки поддерживает INCY и что делает каждый?» - «Покажи формат deep-link для импорта подписки» - «Как настроить фрагментацию и domain fronting через Premium API?» - «Собери пример полного Xray-конфига с маршрутизацией для INCY» Документация двуязычная (RU по умолчанию, EN — версии с суффиксом), и llms.txt/GitMCP обновляются автоматически при каждом изменении документации. # Пресет-иконки Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/icon-presets.md # Пресет-иконки ссылок Иконки, которые отображаются рядом с ссылками **Бот / Канал / Поддержка** в карточке подписки (режим `liteMode`). Провайдер выбирает иконку по ключу из пресет-набора — каждая платформа (Android, iOS, Desktop) маппит этот ключ на свою нативную иконку. Зачем так, а не URL картинки: - **Единый внешний вид на всех платформах** — иконки из системного набора (Material Icons, SF Symbols) подстраиваются под тему. - **Ноль сетевых запросов в клиенте** — ключ это просто строка в `settings`. - **Поддержка в офлайне** — приложение работает даже без связи с сервером. --- ## Где задаётся Провайдер выбирает иконку в [веб-панели](https://web.incy-panel.com) для каждого домена в блоке **Lite Mode**. Значения сохраняются в `SubscriptionSettings`: | Поле | Тип | Описание | |-------------------|-----------|-------------------------------------| | `botIconKey` | `string?` | Ключ иконки для `botUrl` | | `channelIconKey` | `string?` | Ключ иконки для `channelUrl` | | `supportIconKey` | `string?` | Ключ иконки для `supportUrl` | Возвращаются клиенту через [Premium API](premium-api.md) в объекте `settings`. --- ## Поведение клиента - **Ключ задан и известен** → клиент рендерит соответствующую нативную иконку. - **Ключ пустой (`null` / `""`)** → fallback на дефолтную иконку слота: - `botUrl` → `send` (бумажный самолётик) - `channelUrl` → `megaphone` (рупор) - `supportUrl` → `help` (вопросительный знак) - **Ключ задан, но неизвестен клиенту** (старая версия приложения + новый ключ) → fallback на тот же дефолт. --- ## Полный список пресетов Всего 20 ключей. Имя ключа **стабильно** — один раз опубликованный ключ не переименовывается. ### Бот / сообщения | Ключ | Превью | Назначение | |--------------|:------:|------------------------------------| | `send` | ✈️ | Бумажный самолётик (по умолчанию для `botUrl`) | | `bot` | 🤖 | Робот | | `chat` | 💬 | Речевое облачко | | `message` | ✉️ | Конверт (открытое сообщение) | | `mail` | 📧 | Почта | ### Новости / вещание | Ключ | Превью | Назначение | |--------------|:------:|------------------------------------| | `megaphone` | 📢 | Рупор (по умолчанию для `channelUrl`) | | `bell` | 🔔 | Колокольчик | | `newspaper` | 📰 | Газета | | `rss` | 📡 | RSS | | `broadcast` | 📻 | Антенна / радио | ### Поддержка / справка | Ключ | Превью | Назначение | |--------------|:------:|------------------------------------| | `help` | ❓ | Вопрос (по умолчанию для `supportUrl`) | | `support` | 🎧 | Агент поддержки | | `lifebuoy` | 🛟 | Спасательный круг | | `info` | ℹ️ | Информация | | `book` | 📖 | Книга / FAQ | ### Акцентные | Ключ | Превью | Назначение | |----------|:------:|-------------------------| | `crown` | 👑 | Корона | | `star` | ⭐ | Звезда | | `gem` | 💎 | Алмаз | | `rocket` | 🚀 | Ракета | | `heart` | ❤️ | Сердце | --- ## Маппинг на нативные иконки Для справки — какие нативные иконки рендерятся на каждой платформе. Добавлять / менять маппинг нужно одновременно в трёх клиентах + web-панели. | Ключ | Material Icons (Android / Desktop) | SF Symbols (iOS) | |--------------|------------------------------------|------------------------------------------------| | `send` | `Send` | `paperplane.fill` | | `bot` | `SmartToy` | `cpu` | | `chat` | `Chat` | `bubble.left.fill` | | `message` | `Message` | `message.fill` | | `mail` | `Email` | `envelope.fill` | | `megaphone` | `Campaign` | `megaphone.fill` | | `bell` | `Notifications` | `bell.fill` | | `newspaper` | `Newspaper` | `newspaper.fill` | | `rss` | `RssFeed` | `dot.radiowaves.left.and.right` | | `broadcast` | `Podcasts` | `mic.fill` | | `help` | `HelpOutline` | `questionmark.circle` | | `support` | `SupportAgent` | `headphones` | | `lifebuoy` | `MedicalServices` | `lifepreserver` | | `info` | `Info` | `info.circle` | | `book` | `MenuBook` | `book.fill` | | `crown` | `EmojiEvents` | `crown.fill` | | `star` | `Star` | `star.fill` | | `gem` | `Diamond` | `diamond.fill` | | `rocket` | `RocketLaunch` | `flame.fill` | | `heart` | `Favorite` | `heart.fill` | --- ## Пример ```json { "settings": { "liteMode": true, "botUrl": "https://t.me/my_bot", "botIconKey": "bot", "channelUrl": "https://t.me/my_channel", "channelIconKey": "megaphone", "supportUrl": "https://t.me/my_support", "supportIconKey": "lifebuoy" } } ``` В карточке подписки пользователь увидит три кнопки: робот (бот), рупор (канал), спасательный круг (поддержка). # Админ-доступ по HWID Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/admin-hwids.md # Админ-доступ по HWID `adminHwids` — список HWID устройств, которым провайдер выдал **админ-привилегии** в рамках конкретного домена подписки. Привилегий две: просмотр/правка конфигов серверов прямо в приложении и отправка push-уведомлений в обход модерации. --- ## Где задаётся Провайдер добавляет HWID в [веб-панели](https://web.incy-panel.com) в настройках домена, в разделе **Админ-доступ**. Поле хранится в `SubscriptionSettings` и возвращается клиенту через [Premium API](premium-api.md): | Поле | Тип | Описание | |--------------|------------|-------------------------------------------------------------------| | `adminHwids` | `string[]` | Массив HWID в формате, идентичном тому, что клиент отправляет как `x-hwid` | Формат HWID — см. [hwid.md](hwid.md). Сравнение идёт **посимвольно** (без нормализации регистра), т. е. нужно скопировать HWID ровно в том виде, в каком его видит приложение в своих настройках. --- ## Привилегия 1: просмотр и правка конфигов серверов Если HWID устройства входит в `adminHwids` текущей подписки: - На карточке сервера в приложении появляется кнопка редактирования (обычно скрыта для остальных пользователей). - Устройство может править параметры VLESS/VMess/Trojan/… прямо в UI: адрес, порт, UUID, transport-настройки. - Правки действуют **только локально** на этом устройстве — они не уезжают обратно в подписку. При следующем `refresh` подписки сервер восстановится до версии провайдера. Используется при отладке: провайдер в роли «своего» устройства может проверить работу конкретного ключа или параметра транспорта, не переписывая конфиг на стороне сервера. --- ## Привилегия 2: отправка уведомлений без модерации По умолчанию любое уведомление, которое провайдер отправляет из панели, ждёт модерации INCY (статус `pending`). Если в таргетинге указан **конкретный HWID** и этот HWID присутствует в `adminHwids` какого-либо верифицированного домена провайдера: - Уведомление сразу получает статус `approved` (auto-approve по admin-HWID). - Рассылка запускается немедленно — без ожидания модерации. Это нужно для отладки собственных уведомлений: провайдер шлёт тест на своё личное устройство и мгновенно видит результат. > **Важно:** auto-approve срабатывает только когда `targetSegment.hwid` задан **и** совпадает с одним из `adminHwids`. Уведомления без HWID-таргета или на чужой HWID всё равно проходят модерацию. Подробнее — [provider-notifications.md](provider-notifications.md). --- ## Гигиена - Храните в `adminHwids` только собственные устройства. Админ-доступ даёт обход модерации push-уведомлений — случайный HWID чужого пользователя сможет спамить остальных подписчиков. - HWID устройства меняется при заводской очистке или переустановке ОС — список `adminHwids` нужно обновлять. - Удаление HWID из списка вступает в силу при следующем получении конфига устройством (typical 1–5 минут, зависит от кеша). # Push-уведомления провайдера Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/provider-notifications.md # Push-уведомления провайдера Провайдеры с активным Premium могут рассылать push-уведомления своим подписчикам. Все сообщения проходят модерацию в INCY, кроме случая отправки на собственное устройство через [admin HWID](admin-hwids.md). --- ## Жизненный цикл уведомления 1. **Создание.** Провайдер жмёт «Отправить» в панели → уведомление встаёт в очередь со статусом `pending`. 2. **Модерация.** Уведомление проходит модерацию INCY: «Одобрить» либо «Отклонить с комментарием». 3. **Fan-out.** При одобрении сервер выбирает устройства по `targetSegment` и шлёт пуш на каждое устройство, у которого есть push-токен. 4. **Статусы.** Провайдер видит в панели статус и счётчики `deliveredCount` / `failedCount`. ### Статусы записи | Статус | Значение | |--------------|---------------------------------------------------------------------| | `pending` | Ждёт модерации | | `approved` | Одобрено модератором (или auto-approved); FCM-рассылка выполнена | | `rejected` | Отклонено модератором. `moderatorComment` содержит причину | | `cancelled` | Провайдер отменил до одобрения. Рассылка не выполнялась | | `failed` | Одобрено, но fan-out упал (нет верифицированных доменов / FCM error) | --- ## Отправка: поля уведомления | Поле | Тип | Обязательное | Описание | |-----------------|-----------|:------------:|-------------------------------------------------------------| | `title` | `string` | да | Заголовок, ≤ 100 символов | | `body` | `string` | да | Текст, ≤ 500 символов | | `image` | `string?` | нет | URL большого изображения (отрисовывается в развёрнутом пуше) | | `url` | `string?` | нет | URL для кнопки / тапа по уведомлению | | `urlButtonName` | `string?` | нет | Подпись кнопки. По умолчанию «Открыть» | | `forceTimer` | `number?` | нет | Enterprise-only: показать модальный диалог на N сек (1-10) | | `targetSegment` | `object` | да | Параметры таргетинга (ниже) | ### Таргетинг (`targetSegment`) | Поле | Тип | Описание | |--------------|-------------|--------------------------------------------------------------------------| | `platform` | `string` | `all` \| `ios` \| `android` \| `linux` \| `windows` \| `macos` | | `region` | `string[]?` | Локаль устройства (например `["ru", "by"]`). Сравнение case-insensitive | | `domain` | `string?` | Конкретный домен подписки провайдера (если у провайдера их несколько) | | `activeDays` | `number?` | Только устройствам, видимо активным за последние N дней | | `appVersion` | `string?` | Только конкретная версия приложения (например `"2.5.6"`) | | `hwid` | `string?` | Конкретный HWID. Включает [admin-HWID auto-approve](#auto-approve-через-admin-hwid) | Все фильтры объединяются логическим **И**. Пустые / неуказанные поля не ограничивают рассылку. ### Доставка - **Android / iOS** получают пуш напрямую (FCM / APNs). - **Desktop (Linux / Windows)** забирает уведомление при следующей синхронизации (десктоп без пуш-канала) — текст, ссылку, кнопку, изображение и таймер. ### Ограничения - Только для верифицированных доменов провайдера. - Только при активном Premium. - `forceTimer` > 0 работает только на Enterprise-тарифе. --- ## Auto-approve через admin HWID Если `targetSegment.hwid` совпадает с одним из `adminHwids` любого верифицированного домена провайдера — запись создаётся сразу со статусом `approved`, модерация пропускается, FCM уходит немедленно. Подробности: [admin-hwids.md](admin-hwids.md). Это единственный путь, по которому провайдер может отправить уведомление в обход модерации. --- ## Отмена рассылки Пока статус `pending`, провайдер может отменить уведомление в панели. В результате: - Статус меняется на `cancelled`. - Если модератор в этот момент решит «Одобрить» — бот получит ответ «Провайдер отменил рассылку» и FCM не выполнится. После перевода в любой терминальный статус (`approved` / `rejected` / `cancelled` / `failed`) — операция необратима. --- ## Аудит Каждая запись в `pendingNotifications` хранит: | Поле | Описание | |---------------------|-----------------------------------------------------------| | `providerId` | UID провайдера | | `providerEmail` | email провайдера на момент отправки | | `createdAt` | Когда провайдер нажал «Отправить» | | `moderatedAt` | Когда модератор решил / система auto-approved | | `moderatorComment` | Текст отказа (для `rejected`) | | `autoApproved` | `true` если миновало модерацию | | `autoApproveReason` | Причина, например `"adminHwid"` | | `cancelledAt` | Для `cancelled` — когда отменил провайдер | | `cancelledBy` | Источник отмены (сейчас только `"provider"`) | | `deliveredCount` | Число успешно отправленных пушей + desktop-polling устройств | | `failedCount` | FCM-ошибки | # Premium биллинг Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/premium-billing.md # Premium биллинг Система оплаты и управления подписками INCY Premium для провайдеров. ## Обзор Premium даёт провайдерам доступ к расширенным функциям: управление доменами, push-уведомления, аналитика, кастомная тема и логотип. --- ## Тарификация | Параметр | Значение | | --- | --- | | Базовая ставка | **$0.06** за устройство в месяц | | Минимум | 100 устройств | | Самообслуживание | до 10 000 устройств | | Enterprise | от 10 000+ устройств (индивидуальные условия) | ### Периоды оплаты | Период | Скидка | | --- | --- | | 1 месяц | — | | 3 месяца | 3% | | 6 месяцев | 8% | ### Enterprise Для провайдеров с более 10 000 устройств доступны индивидуальные условия: - Кастомная ставка (ниже $0.06) - Приоритетная поддержка Для подключения: [premium@incy.cc](mailto:premium@incy.cc) --- ## Способы оплаты ### СБП (Overpay) Оплата через Систему Быстрых Платежей в рублях. Курс USD→RUB фиксированный. ### Криптовалюта (Heleket) Оплата в USD через криптовалюту (USDT, BTC и др.). --- ## Привязка аккаунта Для оплаты через Telegram-бота провайдер привязывает аккаунт: 1. Перейти в [веб-панель → Биллинг](https://web.incy-panel.com/billing) 2. Нажать кнопку **Войти через Telegram** (Telegram Login Widget) 3. Подтвердить привязку После привязки оплата доступна через бота: `/premium` или [t.me/incyhelperbot?start=premium](https://t.me/incyhelperbot?start=premium) --- ## Управление подпиской ### Продление Провайдер выбирает период (1/3/6 мес) → способ оплаты → оплачивает. Время добавляется к текущему сроку (не с момента оплаты). ### Докупка устройств Доступно только при активной подписке. Доплата рассчитывается пропорционально оставшемуся времени: ``` доплата = (новых_устройств - текущих) × ставка × (оставшихся_дней / 30) ``` Срок подписки при докупке не меняется. Уменьшить количество устройств нельзя. ### Уведомления об истечении Автоматические напоминания в Telegram: за 7 дней, 3 дня, 1 день и в день истечения. --- ## Веб-панель: страница биллинга Доступна по адресу `/billing` в веб-панели: - Текущий план (устройства, стоимость, дата истечения, ставка) - Привязка Telegram - Калькулятор стоимости (слайдер 100–10 000 устройств) - Enterprise блок (для >10 000) - История платежей --- ## Администрирование ### Через Telegram-бота `/admin` → **Биллинг провайдера** → ввод Telegram ID: - Просмотр карточки провайдера - Установка количества устройств (без ограничений) - Установка кастомной ставки - Включение/выключение Enterprise ### Через веб-панель `/admin/providers` → выбор провайдера: - Установка Premium статуса и даты истечения - Установка лимита устройств - Просмотр привязки Telegram - Просмотр тарифа и последнего платежа # VPN Аукцион Source: https://raw.githubusercontent.com/INCY-DEV/incy-docs/main/ru/dev-docs/auction.md # VPN Аукцион Еженедельный аукцион за размещение рекламы VPN-сервисов в канале «Рекомендованные VPN». ## Обзор Провайдеры VPN-сервисов делают ставки за попадание в топ-10 канала рекомендаций. Победители получают размещение своего объявления на неделю. --- ## Как это работает ### Для участников 1. Открыть бота: [@incyhelperbot](https://t.me/incyhelperbot) 2. `/start` → **VPN Аукцион** 3. Пополнить баланс (СБП через Kassai/FreeKassa или криптовалюта через CryptoPay) 4. Сделать ставку 5. Создать объявление (название, описание, ссылка) 6. Дождаться окончания аукциона ### Цикл аукциона | Этап | Описание | | --- | --- | | Понедельник | Открывается новый аукцион | | В течение недели | Участники делают ставки | | Воскресенье | Аукцион завершается в случайное время с 20:00 до 22:00 (МСК) | | После завершения | Топ-10 получают размещение, остальным — возврат на баланс бота для последующего участия в аукционах | Время завершения выбирается случайно для защиты от снайпинга (ставки в последнюю секунду). Баланс аукциона нельзя использовать для оплаты Premium --- ## Ставки ### Правила - Одна активная ставка на пользователя за неделю - Ставка блокирует средства на балансе (эскроу) - Можно повысить ставку — разница списывается с баланса - При вытеснении из топ-10 — полный возврат на баланс бота - Минимальный шаг повышения настраивается администратором ### Топ-10 Рейтинг формируется по размеру ставки (от большей к меньшей). При заполнении всех 10 мест новая ставка должна быть выше минимальной в топе. При вытеснении пользователь получает: - Уведомление в Telegram - Полный возврат заблокированных средств --- ## Объявления Каждый участник может создать объявление: | Поле | Ограничение | | --- | --- | | Название | до 100 символов | | Описание | до 200 символов | | Ссылка | HTTP/HTTPS или t.me/ | Объявление отображается в канале при победе. Можно изменить в любое время. --- ## Результаты ### В канале После завершения аукциона в канал публикуется витрина с объявлениями победителей: - Топ-3 — с медалями (🥇🥈🥉) - Места 4–10 — с номерами - Каждая строка — кликабельная ссылка на сервис Формат: `🥇 Название - Описание` (вся строка — ссылка) ### Во время аукциона - В канал ничего не публикуется - Участники видят рейтинг только в боте - ID участников замаскированы (первые 4 цифры) --- ## Баланс ### Пополнение - **Kassai/FreeKassa** (СБП) — пользователь вводит сумму в долларах, оплата списывается в рублях по курсу - **CryptoPay** (Telegram Crypto Bot) — в криптовалюте ### Движение средств | Операция | Баланс | | --- | --- | | Пополнение | + | | Ставка | − (блокировка) | | Повышение ставки | − (доплата разницы) | | Вытеснение из топ-10 | + (возврат) | | Проигрыш аукциона | + (возврат) | | Победа аукциона | средства списываются окончательно | --- ## Уведомления Участники получают уведомления в Telegram: - Вытеснение из топ-10 (с суммой возврата) - Победа (место и сумма) - Проигрыш (с возвратом) --- ## Администрирование Через бота (`/admin`): - Просмотр статистики аукциона (участники, ставки, заблокированные средства) - Досрочное завершение аукциона - Корректировка баланса пользователей --- ## Кто увидит рекламу Пользователи приложения INCY без premium-подписки видят раздел «Где мне взять сервера?» в настройках приложения (экран «О приложении»). По нажатию они переходят в канал с результатами аукциона. Канал: [@recommended_vpn](https://t.me/recommended_vpn)