👻 finalmask: маски трафика из Xray-core
finalmask — слой масок из Xray-core (в реализации форка Jolymmiles/Xray-core). Маска стоит между сокетом и TLS и меняет то, как байты выглядят на проводе: режет первые пакеты, добавляет перед данными свой «рукопожатие», перекодирует весь поток под судоку или под игровой протокол, прячет UDP в DNS-запросы или в ICMP. Для сервера Xray с теми же масками в finalmask входящего соединения Prizrak-Core — обычный клиент.
Перенесены маски:
| Тип | TCP | UDP | Что делает |
|---|---|---|---|
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
Заголовок раздела «Соответствие ключам Xray»| Xray (JSON) | Prizrak-Core (YAML) |
|---|---|
randRange | rand-range |
maxSplit | max-split |
customTable, customTables | custom-table, custom-tables |
paddingMin, paddingMax | padding-min, padding-max |
texturesValue, texturesSignature | textures-value, textures-signature |
remoteIPs, remotePorts | remote-ips, remote-ports |
lenLimit, labelLimit, extraPoll | len-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 и его эквивалент
Заголовок раздела «Пример из Xray и его эквивалент»В 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"]Маски TCP
Заголовок раздела «Маски TCP»fragment
Заголовок раздела «fragment»Режет первые отправки соединения. Реализация общая с freedom, поэтому поведение совпадает.
| Параметр | Значения | Что делает |
|---|---|---|
packets | tlshello, "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 — без ограничения |
header-custom
Заголовок раздела «header-custom»Заданный вами обмен байтами в начале соединения: клиент отправляет 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-range | rand случайных байт из диапазона 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 | Задаёт таблицу перестановки; на обеих сторонах одинаковый |
ascii | prefer_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. Профили на клиенте и сервере должны совпадать.
Маски UDP
Заголовок раздела «Маски UDP»Работают для 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> случайные цифры.
header-custom (UDP)
Заголовок раздела «header-custom (UDP)»Те же элементы, что и в 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: txidsudoku (UDP)
Заголовок раздела «sudoku (UDP)»Параметры те же, что в 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.
Первый xdns: компактная запись
Заголовок раздела «Первый xdns: компактная запись»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 — доменVLESS поверх mKCP
Заголовок раздела «VLESS поверх mKCP»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 обращается к серверу напрямую.
| Параметр | По умолчанию | Что делает |
|---|---|---|
mtu | 1350 | Наибольший пакет, байт. С xdns держите его маленьким, см. ниже |
tti | 50 | Интервал отправки, мс |
uplink-capacity | 5 | Предел отдачи, МБ/с |
downlink-capacity | 20 | Предел загрузки, МБ/с |
congestion | false | Управление перегрузкой |
write-buffer, read-buffer | 2097152 | Буферы, байт (в Xray их пишут в МБ) |
Пример: DNS-туннель
Заголовок раздела «Пример: DNS-туннель»Пакеты 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.
Сервер: входящие vless и trojan
Заголовок раздела «Сервер: входящие vless и trojan»Тот же блок 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
Заголовок раздела «Через маппер хоста Remnawave»Чтобы пользователи подписки 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 (VLESS поверх mKCP)
Заголовок раздела «Хост с kcp (VLESS поверх mKCP)»Для хоста на входящем 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); опечатка в ключе не вызывает ошибку, а маска работает без него.