Назначение: определить отпечатки как структурированный проверяемый каталог с правилами согласованности, а не как случайный 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 и label — media_devices_manager.cc:1183 выходит из цикла по виду, когда
разрешения нет. Так что единственное, что страница может прочитать, не спрашивая, — это какие виды есть;
количество обратно не прочитать, а выдача нескольких записей одного вида была бы признаком, какого не даёт ни
один настоящий браузер.
Что измерено на этом хосте (B59, M149, и каждая нога дважды):
| нога | результат |
|---|---|
| наше ядро, без личности | 2 устройства — {videoinput, audiooutput}, без микрофона |
| наше ядро, с личностью | то же самое — конфигурация не сдвинула ничего |
| стоковый Chrome 152 | то же самое |
Значит, расхождением со стоком это не было никогда. Дело в том, что каждый профиль на одной машине перечисляет эту машину, что связывает их друг с другом ровно так же, как список голосов (ISSUE-063) и возможности WebGL.
Почему патч отозван. Построены были две формы, обе несогласованные, и шестой гейт нашёл каждую:
- Подмена списка, только до разрешения. В момент, когда выданное разрешение открывало настоящий id устройства, проекция выключалась, и тот же origin видел другую машину, чем мгновением раньше. Кроме того, она заново наполняла список, который Chromium уже отфильтровал по Permissions-Policy, так что документу, которому микрофон запрещён, его могли выдать.
- Только фильтрация — убирать виды, применять всегда. Согласовано и до, и после разрешения, ничего не
выдумывает, и измерилось правильно (сужающее заявление убрало камеру; контроль сохранил набор хоста;
заявленный, но отсутствующий микрофон правильно не выдумывался). Но
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.create (и fingerprint.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, если хост случайно не окажется согласованным» (он им является, по построению). На потом остаётся гранулярность на каждое поле (смешатьrealGPU соcustomчасовым поясом в одной конфигурации) и вопрос, должна ли инверсия хоста нести собственноеprovenance.source(например,host-inverted), а не переиспользоватьgenerated+realDeviceRef.