VFLIN
ВойтиСкачать

docs · docs/30-core/FINGERPRINT_CONFIG_CONTRACT.md · перевод от 2026-09-12 · с английской ревизии f41a1f6793ea

Настройка отпечатка

проверяемый каталог идентичностей и его правила согласованности

Назначение: определить отпечатки как структурированный проверяемый каталог с правилами согласованности, а не как случайный JSON. Этот контракт реализуем уже сейчас (схема + валидация), даже если глубокое соблюдение в движке ждёт патченого ядра. Одна и та же конфигурация ведёт каждого провайдера.

1. Почему сначала контракт

Обнаружение ведёт несогласованность между сигналами, а не отдельные «плохие» значения. Поэтому задача конфигурации не столько «выбрать случайные значения», сколько «гарантировать согласованную личность устройства». Каталог — это мозг (что показывать); Core Provider — руки (как).

2. Схема конфигурации (v0)

{
  "id": "uuid",
  "label": "win11-chrome-148-desktop",
  "os":        { "family": "windows", "version": "11", "arch": "x86_64" },
  "browser":   { "engine": "chromium", "version": "148.0.x", "uaCh": { /* derived, see §4 */ } },
  "hardware":  { "cores": 8, "memoryGb": 16, "gpu": { "vendor": "...", "renderer": "..." } },
  "screen":    { "width": 1920, "height": 1080, "dpr": 1.0, "colorDepth": 24,
                 "available": { "left": 0, "top": 0, "width": 1920, "height": 1032 } },
  "locale":    { "language": "en-US", "languages": ["en-US","en"], "timezone": "Europe/Berlin" },
  "fonts":     { "set": "win11-default", "additions": [], "removals": [] },
  "media":     { "audioContext": "coherent-noise",       // read by patch 0027 (A67: expressible since 2026-08-31)
                 "devices": "win11-typical" },       // DECLARED, READ BY NOTHING — see §2c for why
  "webrtc":    { "policy": "proxy-only", "publicIp": "from-proxy", "localIps": "masked" },
  "canvas":    { "mode": "stable-noise", "seed": "per-profile" },
  "webgl":     { "vendorMatchesGpu": true, "noise": "stable" },
  "network":   { "proxyRef": "uuid", "quic": true,
                 "tls":   { "ja3": "derived", "ja4": "derived" },           // match claimed browser
                 "http2": { "settings": "derived", "akamaiFp": "derived" } },
  "provenance":{ "source": "catalog|generated|imported", "realDeviceRef?": "..." }
}

2a. screen.available — доступный прямоугольник

screen.avail{Left,Top,Width,Height}, разрешаются каталогом и несутся целиком. Пути через точку для того, кто читает JSON: screen.available.left · .top · .width · .height.

Все четыре или ни одного. Это одно поле-объект, а не четыре ключа, чтобы наполовину заполненный прямоугольник нельзя было записать; отсутствие означает «не несётся» — конфигурация, отчеканенная до появления этого поля, — и потребитель обязан вернуться к собственному выводу, а не читать отсутствие как ноль.

Откуда берутся числа. validate::SCREEN_INSETS в vflin-fingerprint держит поля, которые занимает оформление ОС, как отступы по краям с происхождением у каждой строки (Measured / Assumed, та же дисциплина, что у SCREEN_MODES), а validate::available_rect разрешает их относительно дисплея. Windows резервирует СНИЗУ (48 CSS-пикселей на 11 — измерено на настоящем хосте, 40 на 10); macOS резервирует 25 СВЕРХУ (строка меню; Dock моделируется как автоскрывающийся); Linux — 27 сверху. При инверсии хоста (ADR-0031) РАЗМЕР берётся из собственных availWidth/availHeight хоста, когда заявляется его же дисплей; НАЧАЛО координат по-прежнему из каталога, потому что снимок не читает availLeft/availTop.

Почему это прямоугольник, а не разница. Одно «зарезервировать N px» не может сказать, у какого края эти N px. Патч 0011 держал ровно это — четыре литерала в chrome/browser/vflin/fp_config.cc (AvailHeightDeltaForOsFamily), вычитаемые из screen.height с началом координат, принудительно равным (0, 0), — так что смоделированный Mac резервировал свою строку меню и сообщал о ней ВНИЗУ экрана. Зашитые значения личности — это то, что запрещает инвариант 5; вот где они теперь живут.

2b. media.audioContext и canvas.mode — одно поле, одна поверхность

Положение дел, исправленное A67 (2026-08-31). media.audioContext теперь выразим из продукта: модель в crates/vflin-fingerprint несёт необязательный объект media, а set_audio_noise / audio_noise_on его переключают, повторяя пару для canvas. До этого поле было объявлено здесь и не имело поля в Rust вообще, так что до патча 0027 — отгруженного и работающего — можно было дотянуться только написанным вручную JSON, и именно так две сессии доказали патч, пока разрыв оставался невидимым. Отсутствие означает «выключено», и по умолчанию в продукте выключено (указание владельца: доступно, но не включено по умолчанию).

media.devices по-прежнему не читает ничто, и §2c теперь говорит, чего стоит это закрыть. A67 типизировал сторону модели и оставил смысл пресета срезу поверхности; B59 измерил эту поверхность и обнаружил, что механизм нельзя построить на одном лишь слое перечисления. Поле остаётся объявленным и непрочитанным — это честно, в отличие от наполовину реализованного.

Каждое из этих двух полей включает ровно одну поверхность, и ни одно не решает за другое. До патча 0027 (2026-08-30) эта фраза была ложью, и её стоит произнести из-за того, как именно она провалилась.

0022 (audio-noise) отгрузился завязанным на canvas.mode == "stable-noise", так что переключатель с именем canvas включал заодно и возмущение Web Audio, — а media.audioContext, объявленный в схеме выше и названный ключом конфигурации 0022 в плане серии, не читала ни одна ветвь парсера. Задокументированный орган управления не делал ничего; работу делал незадокументированный. ИЗМЕРЕНО K10 вне CreepJS голым OfflineAudioContext: отсчёт 4500 дал -0.14573481678962708 при выключенном переключателе canvas — сток до последней цифры — и -0.14574481546878815 при включённом, из конфигурации, не несущей аудиополя. CreepJS выставил это отдельным счётом как AudioBuffer: "audio is fake".

С 0027 шум аудио включается media.audioContext: "coherent-noise" и больше ничем. Доказано живьём (B55): при canvas.mode: "stable-noise" и без аудиополя отсчёт равен -0.14573481678962708, идентично стоку; при заданном media.audioContext и canvas.mode: off он сдвигается в -0.14572481811046600.

Семя по-прежнему общее, и это намеренно. Возмущение аудио ключуется на canvas.seed, свёрнутом в непересекающейся области "audio", так что две функции никогда не делят ключ: две функции на одном ключе были бы межфункциональной корреляцией, которой воспользовался бы детектор. Остаточное: поэтому аудио нельзя включить без присутствующего canvas.seed, который можно свернуть. Дать аудио собственное семя — это изменение схемы, и оно не сделано.

Всё ещё объявлено и всё ещё не читается: media.devices. B59 попытался и отозвал патч; в §2c записано, что он измерил и чего на самом деле требует закрытие этой поверхности.

2c. media.devices — измерено, испробовано, и почему всё ещё не читается

B59 построил механизм, измерил, что он работает, и отозвал его. Этот раздел существует, чтобы следующая попытка начиналась с того, чего это стоило, а не со строки в схеме.

Что эта поверхность есть на самом деле. До разрешения Chrome показывает не больше одной записи на вид, с пустыми deviceId, groupId и labelmedia_devices_manager.cc:1183 выходит из цикла по виду, когда разрешения нет. Так что единственное, что страница может прочитать, не спрашивая, — это какие виды есть; количество обратно не прочитать, а выдача нескольких записей одного вида была бы признаком, какого не даёт ни один настоящий браузер.

Что измерено на этом хосте (B59, M149, и каждая нога дважды):

нога результат
наше ядро, без личности 2 устройства — {videoinput, audiooutput}, без микрофона
наше ядро, с личностью то же самое — конфигурация не сдвинула ничего
стоковый Chrome 152 то же самое

Значит, расхождением со стоком это не было никогда. Дело в том, что каждый профиль на одной машине перечисляет эту машину, что связывает их друг с другом ровно так же, как список голосов (ISSUE-063) и возможности WebGL.

Почему патч отозван. Построены были две формы, обе несогласованные, и шестой гейт нашёл каждую:

  1. Подмена списка, только до разрешения. В момент, когда выданное разрешение открывало настоящий id устройства, проекция выключалась, и тот же origin видел другую машину, чем мгновением раньше. Кроме того, она заново наполняла список, который Chromium уже отфильтровал по Permissions-Policy, так что документу, которому микрофон запрещён, его могли выдать.
  2. Только фильтрация — убирать виды, применять всегда. Согласовано и до, и после разрешения, ничего не выдумывает, и измерилось правильно (сужающее заявление убрало камеру; контроль сохранил набор хоста; заявленный, но отсутствующий микрофон правильно не выдумывался). Но getUserMedia() её никогда не спрашивает: сайт не видит камеры в enumerateDevices(), а потом захватывает видео с камеры хоста. Скрыть вид из перечисления, когда захват его всё равно отдаёт, — это самопротиворечие в одной сессии, а скрывать — единственное, что эта форма умеет.

Что нужно, чтобы это закрыть. Перечисление и захват должны двигаться вместе — enumerateDevices(), getUserMedia() и selectAudioOutput(), отфильтрованные одним решением, а значит, проекции место в процессе браузера рядом с MediaDevicesManager, а не в колбэке результата в Blink. Добавление вида, которого у хоста нет, к тому же требует синтезировать устройство с id, а после разрешения — и с меткой, то есть нужны имена устройств с происхождением: данные каталога по ADR-0035, а не догадка движка.

До тех пор поле остаётся объявленным и непрочитанным, и это честное состояние; альтернативой предлагался механизм, единственное действие которого создавало противоречие острее той связуемости, которую он убирал.

3. Правила согласованности (валидатор)

Валидация ПАДАЕТ (а не предупреждает), когда сигналы противоречат друг другу. Примеры:

  • webgl.gpu.vendor должен быть правдоподобен для os.family (никаких GPU Apple на Windows).
  • fonts.set должен соответствовать os.family/os.version (никаких шрифтов macOS на Windows).
  • locale.timezone должен быть достижим из геолокации network.proxy (согласованность часовой пояс↔IP).
  • browser.uaCh должен быть выведен из os + browser.version (никогда не набран руками).
  • screen.dpr×базовое разрешение должно быть настоящей комбинацией устройства, а не произвольной.
  • screen.available должен помещаться в дисплей (C6_AVAIL_BOUNDS, C6_AVAIL_EMPTY) и класть свой резерв на тот край, с которого резервирует заявленная ОС (C6_AVAIL_RESERVE_SIDE). Второе — несущее: правило порядка пропускает availTop 0 рядом с availHeight = height − 25, потому что два правильно упорядоченных числа всё ещё могут описывать устройство, которого не существует. ВЕЛИЧИНА намеренно не закреплена: Dock настоящего хоста, более высокая панель задач или автоскрывающаяся строка меню реалистичны по построению согласно ADR-0031, и отказ им был бы ложным срабатыванием.
  • webrtc.publicIp должен равняться выходному IP прокси; localIps — замаскированы.
  • network.tls (JA3/JA4) и network.http2 (SETTINGS/Akamai fp) выводятся из os + browser.version и должны соответствовать заявленному браузеру: личность Chrome-148 выдаёт рукопожатие Chrome-148, а не умолчание Chromium и не переписанное прокси. (Набор C10/C11.) fingerprint.validate возвращает { valid, issues[] } со стабильным code на каждое правило.

4. Вывод, а не свободный ввод

Несколько полей вычисляются, чтобы оставаться согласованными: строка UA и UA Client Hints из os+version; languages[] из основного языка; colorDepth/dpr из профиля устройства. Интерфейс показывает выборы высокого уровня (класс устройства, регион); остальное выводит каталог.

5. Источники значений

  • Пресеты каталога: выверенные согласованные профили устройств (win11-chrome-desktop, macos-safari-…).
  • Сгенерированные: генерация с ограничениями (например, Apify Fingerprint Suite как источник значений), затем прогон через наш валидатор. (Их берём для «что»; применяем через ядро для «как».)
  • Импортированные / выведенные из настоящего устройства: наивысшее доверие; согласованность по построению. (Сюда же ложится «настоящие отпечатки устройств, а не сгенерированные» у отгруженных антидетект-продуктов.) Теперь построено для локального уровня инверсией хоста — см. §5a: аппаратные измерения узла берутся с его СОБСТВЕННОГО настоящего хоста, а не из синтезированной догадки.

Стратегия источника (различитель реализма — blocker перед Phase 2). Сгенерированные комбинации правдоподобны, но не настоящие; отгруженные антидетект-продукты выигрывают на правде настоящего устройства, так что источник данных каталога — несущее решение, а не сноска. Варианты, которые надо закрыть в ADR до Phase 2:

  • Сбор с настоящих устройств — добровольный агент или небольшой парк устройств выдаёт согласованные связки сигналов (без персональных данных): настоящие сочетания GPU+ОС+экран+шрифты+сеть → наивысший реализм; постоянные затраты; нужно покрыть матрицу ОС / GPU / экран / локаль и обновлять по мере обновления устройств. (Этот вариант с центральным пулом теперь отложенный уровень B-fleet; B-local, инверсия хоста — §5a, ADR-0031 — отгружает уровень настоящего устройства БЕЗ пула, беря собственный хост каждого узла, так что стоимость покрытия набора данных для локального применения растворяется.)
  • Лицензировать набор данных — самый быстрый старт; регулярные платежи; покрытие и качество разнятся; риск по условиям использования.
  • Генерировать и валидировать (Apify) — дешёвая широта; ниже доверие; годится для профилей с невысокими ставками. Решено (ADR-0012, Accepted 2026-06-18): основа — выверенный каталог + генератор на Rust с ограничениями сейчас (browserforge как позднейшее расширение широты за этой же схемой); конвейер сбора с настоящих устройств для уровня высокого доверия вырастает позже, под собственным ADR о приватности и согласии. Каждая личность получает оценку доверия по происхождению (§6); обновление пресетов должно следить за Chrome stable (пресеты стареют быстро). → ADR-0012 + CATALOG_SOURCING_COMPARISON.md.

Приор реализма — как тянет генератор с ограничениями (ADR-0012 realism prior, Proposed 2026-07-01). Генератор основы больше не тянет равномерно случайно по плоским таблицам; он тянет взвешенное по частоте СОВМЕСТНОЕ распределение по вручную закодированному графу согласованности (os → chrome_major → gpu → {cores, ram, screen}; region → tz/lang), детерминированно от корневого семени профиля (то же семя ⇒ тот же кортеж; другое семя ⇒ обычно другой). GPU несёт собственные правдоподобные подтаблицы железа и экрана, так что совместно невозможные комбинации (ноутбучный встроенный GPU на настольном кортеже {16 ядер, 32 ГБ, 4K}) невыразимы — рёбра закодированы, а не восстановлены из частных распределений. Это уточняет §4 (генератор выбирает согласованные и правдоподобные по частоте сочетания), не меняя ни схему §2, ни валидатор §3: веса — это const-метаданные времени генерации, а не сериализуемые поля, и каждая вытяжка по построению проходит валидатор. См. ADR-0012 realism prior (internal: docs/10-architecture/adr/0012-realism-prior-weighted-joint.md).

5a. Инверсия хоста — уровень настоящего устройства, по построению (ADR-0031, B-local)

Источник «выведено из настоящего устройства» (§5) построен для локального уровня, но инверсией, а не сбором: вместо центрального набора данных или парка, выдающего связки сигналов, каждый узел характеризует СВОЙ хост (обвязка capture-profile пишет кэш HostFingerprint), и аппаратные измерения профиля берутся с этого настоящего хоста — согласовано по построению, ничего не синтезируется. Поскольку значения настоящие, а не правдоподобные догадки, и признак приора реализма, и межинтерфейсная утечка GPU WebGPU↔WebGL (набор A8) растворяются. Меняются от профиля к профилю только свободные измерения. Разрез (как его строит host_coherent_config):

Настоящее с хоста (из снятого HostFingerprint) Свободное (на профиль — из запроса или отчеканенное)
os.family/arch (navigator.platform + arch из UA-CH)² screen.width/height/dpr/colorDepth — измерение с окном и свободное¹
browser.version и выведенные ua/uaCh (из Chrome/… в UA хоста) locale.timezone · language · выведенные languages[]
hardware.cores (hardwareConcurrency) · memoryGb (deviceMemory) network.proxyRef
hardware.gpu.vendor/renderer (WebGL UNMASKED — настоящий GPU) canvas.seedчеканится заново на каждый профиль (см. ниже)
fonts.set — набор по умолчанию настоящей ОС (не снимок) id — чеканится на профиль

TLS/HTTP2/QUIC в network принадлежат работающему ядру по построению (ADR-0027 — измерено, а не подделано), так что они автоматически совпадают с браузером хоста. provenance.source = generated с realDeviceRef = "hostinv:<platform>:<renderer>", связывающим конфигурацию обратно с хостом.

Уникальность на профиль — это правило согласованности, а не любезность. host_coherent_config выдаёт фиксированное заполнение canvas.seed; каждый путь создания делает snapshot() результата → свежее семя canvas/WebGL (и id) на профиль. Без этого два профиля, выведенных с ОДНОГО хоста, делили бы одно семя — признак связи между профилями. Поэтому снимок обязателен на пути хоста (как он уже обязателен для каталога и встроенной конфигурации).

Три источника личности во время создания. profile.createfingerprint.generate*) принимают не больше одного: fingerprintRef из каталога, встроенный проверенный fingerprint или fromHost (инверсия хоста — только свободные измерения; аппаратные приходят из кэша хоста). Больше одного ⇒ 400. Все три проходят snapshot() и по построению удовлетворяют §3. Явный fromHost на неохарактеризованном устройстве жёстко падает (NOT_FOUND) — он не должен молча вырождаться в профиль без личности, — тогда как неявный путь (личность не запрашивали) — это просто None.

¹ Честные остатки: начальный снимок делается без окна, поэтому он не может прочитать настоящий монитор (его 800×600 не проходит C6 из §3) → screen подаётся на профиль или снимком с окном (в ожидании). B-fleet (центральный пул настоящего железа) и переключатель source: real|from-fp|custom на каждое поле (инверсия хоста сегодня работает на уровне личности — вся конфигурация выводится с хоста, а не сигнал за сигналом) остаются на потом.

² os.version действительно настоящая с хоста (с 2026-07-10, ISSUE-031 c): HostFingerprint несёт снятую ua_platform_version (носитель версии ОС — navigator.platform даёт только семейство), и host_coherent_config выводит os.version из неё (мажор platform-version Win10 10 → "10", Win11 мажор ≥13 → "11"; пустой или старый кэш откатывается к умолчанию семейства). Так что хост на Windows 10 теперь выдаёт "10", а не "11". platformVersion в UA-CH тоже настоящая с хоста (ISSUE-031 c2, с 2026-07-10): конфигурация выдаёт ТОЧНОЕ значение хоста (например, Win11 "19.0.0"), а НЕ общую для парка константу из прямого отображения — то, что каждый профиль Win11 делил один Sec-CH-UA-Platform-Version, само было признаком связи. Валидатор C1 проверяет её на СТРУКТУРНОЕ правдоподобие по каждой ОС (platform_version_plausible: Windows "<major>.0.0" с мажором Win11 ∈ 13..=40 / Win10 == 10; macOS "<major>.<minor>.<patch>", мажор == os.version; Linux пусто) вместо точного равенства, так что настоящее значение хоста проходит, а значение в форме macOS, вне диапазона или неправильной формы по-прежнему отвергается; аномальный снимок откатывается к прямому отображению. browser.version тоже настоящая с хоста — разобрана из Chrome/… в UA хоста. См. ADR-0031 host-inversion (internal: docs/10-architecture/adr/0031-fingerprint-by-host-inversion.md) (аппаратные измерения инверсией хоста) и ADR-0027 network-by-inversion (internal: docs/10-architecture/adr/0027-network-fingerprint-by-inversion.md) (TLS/HTTP2/QUIC по построению).

6. Оценка доверия

Каждая материализованная личность получает оценку доверия из: происхождения, результата валидатора и capabilities работающего ядра (сколько оно на самом деле смогло соблюсти). Запуски с низким доверием помечаются, а не отгружаются молча.

Замечание по интерфейсу (дизайн): это не тщеславное число на профиль — проверенные профили на способном ядре согласованы (у всех вышло бы ~максимум). В таблице оно появляется только как флаг предупреждения о согласованности, когда профиль невозможно сделать согласованным (слабое ядро, несоответствие прокси и часового пояса). Сама оценка (её определяют в основном происхождение × возможности ядра) живёт в подробностях отпечатка, а не в колонке.

7. Отношение к патчам

Авторы патчей (Phase 7) реализуют соблюдение для полей этой схемы. Схема — это стабильный интерфейс между «конфигурацией» и «патчем на C++»: патчи читают конфигурацию и никогда не выдумывают значения. (CHROMIUM_PATCHING_POLICY.md §1.4, §7.)

8. Правка, генерация в один клик и шаблоны (планка UX)

  • 50+ отдельных параметров, каждый настраивается — редактор показывает всю схему (§2) вплоть до отдельных сигналов; каждое поле поддерживает Настоящее ↔ Из отпечатка ↔ Своё (паттерн отгруженных продуктов). Глубина соответствует лучшим на рынке (50+ настраиваемых параметров), но каждое значение по-прежнему проходит валидатор §3.
  • Умная генерация в один клик — одна кнопка производит согласованную проверенную личность из класса устройства и региона (использует генерацию §5 и правила согласованности §3). «Умная» = она подбирает правдоподобные сочетания (GPU↔ОС, шрифты↔ОС, часовой пояс↔IP, TLS↔браузер), а не случайные; результат по построению проходит набор проверок согласованности, так что оператор получает чистый профиль в один клик.
  • Шаблоны — сохранить любую конфигурацию (отпечаток + политика прокси + расширения + домашние страницы + закладки) как переиспользуемый шаблон; создать разом N профилей из шаблона и набора данных (AUTOMATION_ENGINE §4). Пресеты каталога (§5) — это шаблоны только для чтения; пользовательские шаблоны — редактируемые копии.

Открытые вопросы

Откуда берётся выверенный каталог (строить, лицензировать или собирать), как часто пресеты обновляются с выпусками Chrome, формула оценки доверия → OPEN_QUESTIONS.md.

  • source на каждое поле в схеме (показано в интерфейсе, 2026-06-21). §8 и DESIGN_DIRECTION дают каждому сигналу переключатель Настоящее ↔ Из отпечатка ↔ Своё, но схема v0 из §2 несёт только provenance.source на уровне личности. Схема v1 должна моделировать source: real|from-fp|custom на каждое поле (с пометкой, что real рискованно — оно выдаёт устройство хоста и не проходит §3, если хост случайно не окажется согласованным), чтобы переключатель опирался на контракт, а ядро знало по каждому сигналу, читать ли значение хоста, каталога или проверенное своё. Всплыло на макете выезжающей панели отпечатка. Обновление (ADR-0031, 2026-07-04): инверсия хоста (§5a) делает рискованный случай real безопасным на уровне личности — ВСЯ конфигурация согласована с хостом по построению, так что real больше не «не проходит §3, если хост случайно не окажется согласованным» (он им является, по построению). На потом остаётся гранулярность на каждое поле (смешать real GPU со custom часовым поясом в одной конфигурации) и вопрос, должна ли инверсия хоста нести собственное provenance.source (например, host-inverted), а не переиспользовать generated + realDeviceRef.