Перейти к содержимому

👻 finalmask: маски трафика из Xray-core

finalmask — слой масок из Xray-core (в реализации форка Jolymmiles/Xray-core). Маска стоит между сокетом и TLS и меняет то, как байты выглядят на проводе: режет первые пакеты, добавляет перед данными свой «рукопожатие», перекодирует весь поток под судоку или под игровой протокол, прячет UDP в DNS-запросы или в ICMP. Для сервера Xray с теми же масками в finalmask входящего соединения Prizrak-Core — обычный клиент.

Перенесены маски:

ТипTCPUDPЧто делает
fragment✅—Режет первые отправки соединения (ClientHello) на куски
header-custom✅✅Заданный вами обмен байтами в начале TCP-потока; заголовок перед каждой UDP-датаграммой или отдельный обмен перед первой
sudoku✅✅Перекодирует поток в байты-подсказки судоку
xmc✅—Поток выглядит как вход клиента Minecraft на сервер и игра
noise—✅Перед первым пакетом к адресату отправляет несколько пакетов шума
udphop—✅Периодически меняет локальный сокет и/или адрес и порт сервера
xdns—✅Датаграммы идут как DNS-запросы и ответы через резолверы
xicmp—✅Датаграммы идут как ICMP echo (ping)

Не перенесены: mkcp-legacy, salamander (в том числе gecko) и realm. Если они встретятся в конфигурации, прокси не создастся с понятной ошибкой.


  • Только у прокси vless и trojan и у одноимённых входящих (listeners).
  • TCP-маски оборачивают соединение под TLS, REALITY, ShadowTLS, Restls и JLS и работают со всеми транспортами: tcp, ws, grpc, h2, http, xhttp (включая download-settings).
  • UDP-маски оборачивают сокет VLESS поверх mKCP (network: mkcp, см. ниже) и QUIC-сокет xhttp по HTTP/3 (network: xhttp, alpn: [h3]). На других транспортах они не используются, и такая конфигурация отклоняется с ошибкой.
  • У входящих (listeners) поддерживаются только TCP-маски: у них нет сокета HTTP/3 или mKCP.
  • Если блока finalmask нет — прокси работает как обычно, без каких-либо обёрток.

Порядок масок такой же, как в Xray: первая в списке стоит ближе всего к TLS, последняя — ближе всего к сети, именно её видно на проводе. Маски, которые открывают сеть сами (udphop, xdns, xicmp), должны быть последними в списке udp.

Маски должны совпадать на обеих сторонах. Если на сервере маска есть, а у клиента нет (или отличается хотя бы один параметр, например пароль sudoku), соединение не установится.


Блок finalmask у прокси содержит списки tcp и udp. Каждый элемент — type и параметры маски рядом с ним:

proxies:
- name: vless-finalmask
type: vless
server: example.com
port: 443
uuid: <uuid>
tls: true
servername: example.com
client-fingerprint: chrome
finalmask:
tcp:
- type: fragment
packets: tlshello
lengths: ["1-1"]
delays: ["0"]
- type: sudoku
password: <password>
  • Ключи пишутся в стиле mihomo, через дефис: rand-range, max-split, custom-tables, padding-min.
  • Принимается и запись Xray с вложенным settings: — блок можно скопировать из конфигурации клиента Xray, переименовав ключи (таблица ниже).
  • Диапазоны пишутся как в Xray: "10-20" или просто число. Верхняя граница у length/delay не входит, как в Xray.
Xray (JSON)Prizrak-Core (YAML)
randRangerand-range
maxSplitmax-split
customTable, customTablescustom-table, custom-tables
paddingMin, paddingMaxpadding-min, padding-max
texturesValue, texturesSignaturetextures-value, textures-signature
remoteIPs, remotePortsremote-ips, remote-ports
lenLimit, labelLimit, extraPolllen-limit, label-limit, extra-poll

Остальные ключи (packets, length, delay, lengths, delays, clients, servers, errors, client, server, mode, capture, reuse, transform, password, ascii, hostname, profiles, reset, noise, interval, domains, resolvers, names, types, edns0, addrs, dgram, ips) совпадают.

В Xray блок finalmask исходящего соединения выглядит так:

{
"tcp": [
{
"type": "fragment",
"settings": {
"packets": "tlshello",
"lengths": ["1-1"],
"delays": ["0"]
}
}
]
}

В Prizrak-Core это:

finalmask:
tcp:
- type: fragment
packets: tlshello
lengths: ["1-1"]
delays: ["0"]

Запись с settings: тоже сработает:

finalmask:
tcp:
- type: fragment
settings:
packets: tlshello
lengths: ["1-1"]
delays: ["0"]

Режет первые отправки соединения. Реализация общая с freedom, поэтому поведение совпадает.

ПараметрЗначенияЧто делает
packetstlshello, "N-M" или пустоtlshello переписывает первый ClientHello в несколько TLS-записей. "1-3" режет первые три отправки на TCP-сегменты, пусто — все отправки
length"N" или "N-M", байтРазмер куска для всех кусков
delay"N" или "N-M", мсПауза после каждого куска. При tlshello и 0 все записи уходят одной отправкой
lengthsсписок диапазоновРазмер i-го куска; последний элемент действует на все следующие куски
delaysсписок диапазоновПауза после i-го куска; последний элемент действует на все следующие
max-split"N" или "N-M"Не больше стольких кусков, последний забирает остаток. 0 — без ограничения

Заданный вами обмен байтами в начале соединения: клиент отправляет clients[0], сервер отвечает servers[0], затем clients[1], servers[1] и так далее. Только после этого идёт TLS. Если последовательность клиента не совпала, сервер отвечает errors[i] (только на серверной стороне).

- type: header-custom
clients:
- - packet: "GET /api HTTP/1.1\r\nHost: cdn.example.com\r\nX-Nonce: "
type: str
- rand: 8
rand-range: "97-122" # буквы a-z
capture: nonce
- packet: "\r\n\r\n"
type: str
servers:
- - packet: "HTTP/1.1 101 Switching Protocols\r\nX-Nonce: "
type: str
- reuse: nonce
- packet: "\r\n\r\n"
type: str

Каждый элемент последовательности — ровно одно из:

ПолеЧто делает
packet + typeФиксированные байты. type: str (как есть), hex, base64, или список чисел 0–255 без type
rand + rand-rangerand случайных байт из диапазона rand-range (по умолчанию 0-255)
reuseЗначение, ранее сохранённое через capture
transformВычисленные байты: {op: ..., args: [...]}

Дополнительно: capture: имя сохраняет принятые или отправленные байты элемента под именем; delay: "a-b" (только TCP) — пауза в мс перед элементом.

Операции transform: concat, slice, be16, be32, le16, le32, le64, xor16, xor32, pad, truncate, add, sub, and, or, shl, shr. Аргументы: {bytes: ..., type: hex}, {u64: 42}, {reuse: имя}, {metadata: local_port} (local_port, remote_port, local_ip4_u32, remote_ip4_u32, src_port_u16, dst_port_u16, src_ip4_u32, dst_ip4_u32), вложенный {transform: ...}.

Перекодирует весь поток в байты-подсказки судоку 4×4. По TCP клиент отправляет классическим кодированием (4 подсказки на байт), сервер — упакованным (6 бит на байт). Рукопожатия нет, маска — чистое преобразование внешнего вида.

ПараметрЧто делает
passwordЗадаёт таблицу перестановки; на обеих сторонах одинаковый
asciiprefer_entropy (по умолчанию) — байты выглядят случайными; prefer_ascii — печатные ASCII-символы
custom-tableРаскладка битов из 8 букв: ровно 2 x, 2 p и 4 v, например xpxvvpvv (только в режиме entropy)
custom-tablesНесколько раскладок, которые чередуются
padding-min, padding-maxВероятность (в процентах) вставить байт-заполнитель

Трафик вырастает в 4–9 раз по TCP от клиента и примерно в 1,4 раза от сервера.

Поток выглядит как вход клиента Minecraft 26.1.2 на сервер: рукопожатие, Login Start, шифрование с RSA-ключом, выведенным из пароля, Login Success с подписанным профилем, затем «загрузка мира» нужной длины и сами данные в пакетах Custom Payload с keep-alive.

- type: xmc
password: <password> # одинаковый на обеих сторонах
hostname: mc.example.com # адрес сервера в пакете рукопожатия
profiles:
- username: Steve
uuid: 069a79f4-44e9-4726-a5be-fca90e38aaf5
textures-value: <value>
textures-signature: <signature>

uuid берётся из https://api.mojang.com/users/profiles/minecraft/{username}, а textures-value и textures-signature — из https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false. Профили на клиенте и сервере должны совпадать.


Работают для VLESS поверх mKCP (network: mkcp) и для xhttp по HTTP/3 (alpn: [h3]).

Перед первой датаграммой к адресату отправляет несколько пакетов шума и повторяет их через reset секунд.

- type: noise
reset: "30-60" # секунды; 0 — шум только один раз
noise:
- type: exp
packet: "<b 0d0a0d0a> <t> <r 24>"
delay: "10-20" # пауза после пакета, мс
- rand: "40-60"
rand-range: "0-255"

Пакет шума — одно из: rand (столько случайных байт из rand-range), packet + type (str, hex, base64, список байт) или type: exp с выражением из сегментов: <b hex> фиксированные байты, <t> 4-байтная метка времени, <c> 4-байтный счётчик, <n> 8-байтный nonce, <r a-b> случайные байты, <rc a-b> случайные буквы, <rd a-b> случайные цифры.

Те же элементы, что и в TCP, но два режима:

  • mode: prefix (по умолчанию) — заголовок client стоит перед каждой датаграммой клиента, server — перед каждой датаграммой сервера;
  • mode: standalone — заголовки отправляются один раз отдельными датаграммами перед первым пакетом (например, имитация STUN-запроса и ответа).
- type: header-custom
mode: standalone
client:
- packet: "000100002112a442"
type: hex
- rand: 12
capture: txid
server:
- packet: "010100002112a442"
type: hex
- reuse: txid

Параметры те же, что в TCP; каждая датаграмма кодируется отдельно классическим кодированием. Датаграмма вырастает в 4–9 раз: пакет QUIC в 1200 байт превращается в 5–10 КиБ. Учитывайте лимиты UDP на вашем стеке.

Клиент периодически меняет сокет и/или адрес сервера. Серверу Xray ничего настраивать не нужно для смены локального сокета; для смены портов сервер должен принимать все порты из списка (обычно через правило DNAT на одном сокете, как у Hysteria).

- type: udphop
mode: intervalLocal # intervalLocal, intervalRemote, perConnRemote — через запятую
interval: "30-60" # секунды, не меньше 5
# remote-ips: ["203.0.113.0/24"]
# remote-ports: ["20000-30000"]
  • intervalLocal — новый локальный сокет каждые interval секунд;
  • intervalRemote — новый адрес/порт сервера из remote-ips / remote-ports каждые interval секунд;
  • perConnRemote — адрес/порт выбирается один раз на соединение.

Должна быть последней в списке udp.

Датаграммы передаются как DNS-запросы и ответы для зоны, за которую отвечает сервер, через указанные резолверы. На проводе — обычный DNS, поэтому маска должна быть последней в списке udp.

- type: xdns
domains:
- names: ["t.example.com"] # зоны сервера
edns0: 4096 # обязательно для QUIC: в 512 байт ответ не влезет
types: [16] # 1 A, 5 CNAME, 16 TXT (по умолчанию), 28 AAAA
# len-limit: 255
# label-limit: 63
resolvers:
- addrs: ["udp://1.1.1.1", "tcp://8.8.8.8:53"] # без схемы — udp, без порта — 53
# extra-poll: 0 # дополнительных опросов после каждой отправки, 0–3

Ёмкость ответа зависит от типа записи: с edns0: 4096 TXT вмещает ~3800 байт, AAAA и CNAME — около 2000, A — не больше 764 байт. Для QUIC используйте TXT.

Xray-core с v26.1.31 по v26.9.9 записывал резолвер одной строкой: зона, тип записи и резолвер.

- type: xdns
resolvers:
- "t.example.com:aaaa+udp://1.1.1.1:53" # тип записи: txt (по умолчанию), a, aaaa
- "t.example.com:aaaa+udp://8.8.8.8:53"

У того xdns свой формат на проводе, который нынешний xdns не читает, а панели и серверы того времени им пользуются до сих пор. Маска в такой записи получает клиент того времени и работает с такими серверами. Две записи в одной маске не смешиваются. Перенесена только клиентская сторона.

Пакет первого xdns несёт не больше 223 байт и должен поместиться в имя запроса: около 120 байт при короткой зоне. Он рассчитан на mKCP с маленьким MTU.

Датаграммы передаются как ICMP echo. Нужны raw-сокеты или, с dgram: true, непривилегированные ICMP-сокеты (Linux с net.ipv4.ping_group_range, macOS, iOS). На Android без root, как правило, недоступно.

- type: xicmp
dgram: true
ips: ["203.0.113.1"] # адреса сервера; обязательно, если server — домен

network: mkcp (или kcp) передаёт прокси VLESS поверх mKCP так, как этого ждёт входящее kcp в Xray-core. Кадры такие же, как в Xray-core после finalmask (v26.1.31): у mKCP нет своего заголовка и аутентификации, потому что прежние seed и header стали масками finalmask. Поэтому у VLESS в mkcp-opts нет seed и header. Список udp блока finalmask стоит под mKCP. Без него mKCP обращается к серверу напрямую.

ПараметрПо умолчаниюЧто делает
mtu1350Наибольший пакет, байт. С xdns держите его маленьким, см. ниже
tti50Интервал отправки, мс
uplink-capacity5Предел отдачи, МБ/с
downlink-capacity20Предел загрузки, МБ/с
congestionfalseУправление перегрузкой
write-buffer, read-buffer2097152Буферы, байт (в Xray их пишут в МБ)

Пакеты mKCP идут как DNS-запросы к зоне сервера через публичные резолверы. server и port пишутся как в конфигурации Xray: туннель доходит до сервера только через резолверы.

proxies:
- name: dns-tunnel
type: vless
server: 77.88.8.8
port: 53
uuid: <uuid>
udp: true
packet-encoding: xudp
network: mkcp
mkcp-opts:
mtu: 77
tti: 100
congestion: false
finalmask:
udp:
- type: xdns
resolvers:
- "t.example.com:aaaa+udp://77.88.8.8:53"
- "t.example.com:aaaa+udp://8.8.8.8:53"

Пакет должен поместиться в один DNS-запрос, поэтому MTU остаётся маленьким: панели Remnawave ставят 77. Такой туннель даёт десятки КБ/с, этого хватает мессенджерам в текстовом режиме. Резолверы здесь записаны в компактной записи первого xdns, как их отдают такие панели.

Prizrak-Core сам превращает в такой прокси подписки Xray JSON (их получает Happ) и ссылки vless:// с type=kcp, mtu, tti и fm. VMess поверх mKCP сохраняет mKCP до finalmask, с seed и header в mkcp-opts.


Тот же блок finalmask (только tcp) принимают входящие vless и trojan. Маски оборачивают принятые соединения под TLS, REALITY, ShadowTLS, Restls или JLS на всех транспортах. Соединение, не прошедшее маску, закрывается, остальные обслуживаются дальше. Так Prizrak-Core может быть сервером для клиента Xray с finalmask или для другого Prizrak-Core.

listeners:
- name: vless-in
type: vless
port: 443
listen: 0.0.0.0
users:
- uuid: <uuid>
certificate: ./server.crt
private-key: ./server.key
finalmask:
tcp:
- type: header-custom
clients: [[{ packet: "GET / HTTP/1.1\r\n", type: str }, { rand: 4, capture: n }]]
servers: [[{ packet: "HTTP/1.1 101\r\n", type: str }, { reuse: n }]]
errors: [[{ packet: "HTTP/1.1 404 Not Found\r\n\r\n", type: str }]]
- type: sudoku
password: <password>

Чтобы пользователи подписки Remnawave получили finalmask, добавьте его к прокси хоста через маппер хоста, в список mihomo. Пример для fragment из начала страницы:

{
"xrayJson": [],
"mihomo": [
{
"op": "set",
"value": {
"tcp": [
{
"type": "fragment",
"packets": "tlshello",
"lengths": ["1-1"],
"delays": ["0"]
}
]
},
"to": "['finalmask']"
}
],
"base64": []
}

В подписке Mihomo прокси получит блок finalmask с этим fragment. Маски должны совпадать с finalmask входящего соединения на сервере Xray этого хоста. Клиенты на обычном Mihomo ключ finalmask проигнорируют и подключатся без масок, то есть к серверу с масками они не подключатся: включайте finalmask только для хостов, которыми пользуются клиенты на Prizrak-Core.

Для хоста на входящем kcp, например DNS-туннеля через xdns, маппер собирает весь прокси из настроек самого хоста: сеть, mkcp-opts и finalmask.

{
"xrayJson": [],
"mihomo": [
{ "op": "set", "to": "network", "value": "mkcp" },
{ "op": "copy", "from": "$host.transportOptions.clientMtu", "to": "mkcp-opts.mtu" },
{ "op": "copy", "from": "$host.transportOptions.clientTti", "to": "mkcp-opts.tti" },
{ "op": "copy", "from": "$host.transportOptions.congestion", "to": "mkcp-opts.congestion" },
{ "op": "copy", "from": "$host.streamOverrides.finalMask", "to": "finalmask" },
{ "op": "unset", "to": "client-fingerprint" }
],
"base64": []
}

Прокси получится таким же, как в примере DNS-туннеля.


  • xicmp на стороне Prizrak-Core проверен только модульными тестами: серверу нужны raw-сокеты.
  • udphop в режимах intervalRemote / perConnRemote требует, чтобы сервер принимал все порты из списка.
  • xdns без edns0 непригоден для QUIC; тип A ограничен 764 байтами на ответ.
  • Первый xdns (компактная запись) перенесён только для клиента; его пакет несёт не больше 223 байт.
  • VLESS поверх mKCP использует кадры Xray-core после finalmask. Серверы с seed или header у mKCP (до v26.1.31) не поддерживаются: маски mkcp-original и mkcp-aes128gcm не перенесены.
  • sudoku и xmc заметно нагружают процессор и увеличивают трафик.
  • Параметр, неизвестный маске, игнорируется (как и везде в mihomo); опечатка в ключе не вызывает ошибку, а маска работает без него.