# Чем нас связывает чужой стенд: перепись поверхностей 15 августа 2026

Проверка враждой, не самопроверкой. Владелец построил на своём сайте
`rareamour` антифрод-стенд (`~/Desktop/rareamour`, `docs/antifraud.md`) — сайт
с намеренно злой системой связывания, которая снимает всё технически доступное
и пытается доказать, что «разные люди» — одна машина. Мы натравили на него
шесть профилей (три первого уровня, три второго) и отдельно провели **перепись
поверхностей отпечатка**: 18 агентов по восьми областям, каждое утверждение
проверено по коду стенда. Полный свод — `~/Desktop/rareamour/docs/antifraud-census-2026-08-15.md`.

Этот файл — выжимка того, что важно **для Mantia**: чем нас можно связать, что
из этого мы уже честно показываем, и что было новостью.

## Что стенд подтвердил своим замером

Его самопроверка (сетка «антидетект»: пять профилей с подменёнными canvas,
WebGL, звуком, экраном и часовым поясом) собрала все пять в **один кластер**.
Связь удержалась на том, что принадлежит машине: **набор шрифтов, адаптер
WebGPU и физическая раскладка клавиатуры**. Ровно те три признака, которые мы
за день до этого нашли у себя замером и внесли в карту покрытия
([[coverage]] — `machine_apis`). Чужая логика подтвердила нашу.

## Измеренная атака: шесть профилей против стенда

Прогнали шесть профилей (3 Chromium — уровень 1, 3 Camoufox/Firefox —
уровень 2) через регистрацию на стенде. Два искажения опыта поставлены
сознательно и названы прямо: все профили на **одной физической машине** и через
**один прокси** (стенд не связывает по IP, поэтому общий выход ответ ему не
подсказывает). Ценность — не в том, что связал (на одной машине это неизбежно),
а в том, ЧЕМ.

Стенд собрал все шесть в **один кластер, 292 бита**, ложных срабатываний ноль
(ни одного живого клиента рядом). Разложение по коду стенда:

* **Мост между движками — ровно один признак: `webgl_render`** (14 бит, 5 из 6
  профилей). Строка видеокарты у Chromium и Firefox на одном физическом GPU
  совпадает. Это и склеило уровень 1 с уровнем 2. Вывод жёсткий: **смешение
  движков на одной машине связку не рвёт** — оба называют одну карту.
* **Внутри первого уровня** три профиля связались всеми машинными
  константами: `canvas_text`, `fonts`, `canvas_geo`, `audio`, `webgl_params`,
  **`webgpu`, `battery`, `keyboard`** (три последних — ровно те, что добавлены
  в карту покрытия днём раньше; подтверждено чужой логикой), плюс сетевые
  `ja4`, `h2`.
* **Внутри второго уровня** связали `canvas_geo` (патч холста не в поставке —
  ожидаемо), `webgl_params`, `ja3`, `ja4`, `h2` и `rtc_ip` (утечка адреса
  прокси через WebRTC — следствие общего прокси). А вот `fonts` и `canvas_text`
  в связку **не попали**: три Firefox-профиля различились по набору шрифтов —
  значит закрепление шрифтов за профилем работает, и текстовый холст следом за
  ним тоже разошёлся. Единственное, чем связались, — то, что от шрифтов не
  зависит.
* `webgpu`, `keyboard`, `battery` у второго уровня **отсутствуют вовсе** — в
  его сигналах их нет, что ещё раз подтверждает запись в карте покрытия.

Итог честный: стенд воспроизвёл наш собственный разбор «две машины из одной»,
ничего сверх него не сломалось, и один машинный признак — строка видеокарты —
оказался достаточным мостом даже между разными браузерами. Это довод не против
второго уровня (внутри него шрифты и текстовый холст разошлись), а против
надежды, что смешение движков на ОДНОЙ машине даёт независимые личности. Не
даёт: GPU один.

## Второй опыт: свои прокси — и найденный из-за него дефект

Владелец дал четыре резидентских прокси, и опыт повторили честно: четыре
профиля второго уровня, у каждого свой прокси (Нидерланды, Малайзия, Чехия,
Германия — четыре страны, четыре автономные системы) и своя география.

Стенд снова собрал их в кластер (178 бит), и **самой тяжёлой уликой оказался
не отпечаток, а наша ошибка**: `rtc_ip`, 16 бит, «один адрес по WebRTC» у всех
четырёх. Причём адрес был чужой — выход **польского** прокси из предыдущего
опыта, к этим профилям не привязанного вовсе.

Разбор занял три замера:

1. профиль **в одиночку**, свежий процесс — объявляет свой выход правильно;
2. два профиля **подряд** в одном процессе — второй объявляет адрес первого;
3. в исходниках Camoufox функция `public_ip(proxy)` помечена
   `@lru_cache(maxsize=None)`, а ключ — строка прокси. Наш форвардер даёт
   браузеру `socks5://127.0.0.1:<порт>`, порты переиспользуются, ключ у всех
   профилей совпадает — первый вычисленный адрес возвращается всем.

Тем же кэшем портились часовой пояс и координаты: профиль получал географию
чужого прокси. То есть беда была шире, чем WebRTC.

**Починено**: вместо флага «спроси сам» движку передаётся ЗАМЕРЕННЫЙ выход
прокси этого профиля (`geoip=<адрес>`), который у нас и так лежит в базе после
проверки прокси. Ни сетевого запроса при запуске, ни кэша, ни зависимости от
порядка. На запасном пути (прокси ещё не проверяли) их кэш сбрасывается перед
запуском. Проверено замером: два профиля в одном процессе объявили разные
адреса, каждый свой.

Отсюда правило, которое стоит помнить: **чужая библиотека может кэшировать то,
что у нас обязано быть разным на каждый профиль.** Ищется это не чтением, а
запуском двух профилей подряд и сравнением.

## Третий опыт, чистый: что осталось после починки

Свежие каталоги профилей, три разных прокси, правка WebRTC в деле. Первые два
опыта были испорчены каждый по-своему — общий прокси в первом и
переиспользованные каталоги во втором (стенд справедливо склеил новые аккаунты
со старыми визитами по метке в хранилище; для альфа-тестеров отсюда правило:
**один профиль — один аккаунт**).

Итог по двум дошедшим профилям (третий не дошёл: прокси оборвал соединение):

| | было (общий прокси) | стало (чисто) |
|---|---|---|
| вес кластера | 178 бит | **126 бит** |
| `rtc_ip` — один адрес по WebRTC | 16 бит | **исчез** |
| балл подмены | 50 | **0** |

**Балл подмены ноль** — это отдельная победа, и она не про связывание. Раньше
стенд насчитывал 50: часовой пояс и координаты профиль получал от ЧУЖОГО
адреса (тот же кэш), и его правило «зона не сходится с адресом» срабатывало.
Теперь география берётся от замеренного выхода своего прокси, противоречий
нет, и стенд видит обычного человека, а не подделку.

Осталось шесть признаков, все — константы машины и сборки:

* `webgl_render`, `webgl_params`, `gpu` — физическая видеокарта;
* `ja3`, `ja4`, `h2` — одна сборка Camoufox: рукопожатие TLS и настройки HTTP/2;
* `canvas_geo` — **наш патч холста**: написан, собран, проверен, в поставку не
  вошёл.

Из них на площадке размером с настоящий маркетплейс первые две группы
обесцениваются механизмом гашения популярных значений (у стенда он есть, но
гасить нечего — там восемь аккаунтов): строка видеокарты и рукопожатие Firefox
одинаковы у десятков тысяч живых людей. А `canvas_geo` не обесценится — он
уникален для машины.

**Значит по отпечатку остался ровно один незакрытый пункт, и он уже написан.**

## Три класса, по убыванию опасности

### 1. Машинные константы — связывают профили одной машины сами по себе

Это подтверждение и расширение того, что уже сказано в
[fingerprint-diversity.md](fingerprint-diversity.md): «две машины из одной». Их
нельзя подделать шумом или подменой строки — только патч C++ или отдельное
железо. Первый уровень отдаёт их одинаковыми у всех профилей машины; второй
уровень часть из них не отдаёт вовсе (у Firefox нет WebGPU, раскладки,
батареи — измерено).

Новое сверх того, что было в карте покрытия:

* **ICC-профиль монитора через отрисовку** — картинка с вшитым профилем,
  `drawImage`, чтение пикселей: система пересчитала цвет под конкретный
  дисплей. На macOS выражен сильно. Один на машину;
* **Screen Details API** (`getScreenDetails`) — метки мониторов содержат
  модель, плюс геометрия нескольких экранов. Chrome. Один на машину;
* **WebGPU-compute и GPU-таймер** — не строка адаптера (её мы уже учли), а
  результат реального вычисления на видеокарте и время отрисовки. Считает само
  железо;
* **`queryLocalFonts`** — полный список семейств с PostScript-именами, а не
  перебор словаря. Chrome. Точнее нашего измерения ширины;
* **настоящее число ядер замером** — N фоновых потоков со счётным тестом
  показывают реальную параллельность, даже когда `hardwareConcurrency`
  подменён;
* **декодирование медиа** (`MediaCapabilities.decodingInfo`,
  `powerEfficient`; WebCodecs `prefer-hardware`; EME robustness и HDCP) —
  аппаратный контур целиком.

### 2. Сеть и транспорт — принадлежат машине и прокси, не профилю

Расширяет строку `tcp` карты покрытия. Всё это одинаково у профилей за одним
каналом:

* **отпечаток стека TCP/IP** (p0f по SYN) — уже честно помечен как
  неуправляемый;
* **HTTP/2**: порядок SETTINGS и WINDOW_UPDATE, дерево PRIORITY — жёстко
  привязаны к настоящей сборке; расхождение с заявленным UA выдаёт подмену;
* **QUIC/HTTP-3**: Initial и transport parameters — второй, независимый от
  TLS путь; часто забывают выключить QUIC, подменив остальное;
* **TLS `key_share`** — перечень групп прямо выдаёт настоящую версию Chromium
  под чужим UA;
* **утечка DNS с ECS, пара IPv4/IPv6, латентная геолокация по RTT** — приходят
  мимо профиля и показывают настоящую сеть машины. Наш fail-closed закрывает
  HTTP, но не DNS, не UDP/QUIC и не IPv6.

### 3. Поведение оператора — общий почерк на все профили

Самый прямой путь к связыванию, и он **наш**, а не браузера: диграфы печати,
профиль движения мыши, паузы, порядок полей задаёт один и тот же агент.
Отдельно назван `getCoalescedEvents`: у настоящей мыши на `pointermove`
приходят склеенные точки с частотой опроса устройства, у `Input.dispatch*` их
нет, а координаты целые — это одновременно детект CDP и метка, общая для всех
профилей. Наша моторика разносит тайминги, но склейку точек не эмулирует.

## Что из этого делать нам — по убыванию пользы

1. **Второй уровень для флота — подтверждён враждой.** Часть машинных констант
   (WebGPU, раскладка, батарея) у Firefox отсутствует, и стенд связал сетку
   «антидетект» слабее, чем «одну машину» (108 бит против 198). Это довод в
   карте покрытия, теперь измеренный чужой логикой.
2. **Проверить, что клонирование профиля не тащит сеть.** Супер-cookie вне
   хранилищ (кэш favicon, HSTS, TLS-билеты, ETag, кэш DNS) переезжают с копией
   каталога и склеивают личности намертво при полностью разных отпечатках.
   Наш клон рождается с чистым каталогом ([[editing]]) — но это надо
   **замерить**, а не считать очевидным.
3. **Расширить fail-closed за пределы HTTP** — на DNS, QUIC/UDP и IPv6. Сейчас
   умерший апстрим глушит HTTP-трафик, а запрос DNS или QUIC может уйти мимо.
4. **Эмуляция `getCoalescedEvents`** — если когда-нибудь понадобится проходить
   стенд такого класса. Пока вне границ задачи: это уже не отпечаток, а
   поведение.

## Граница

Стенд — не наша цель и не рынок. Он инструмент проверки: показывает список
дыр, которые площадка **способна** увидеть, чтобы мы не шлифовали известное и
не упирались внезапно в неизвестное. Обход капчи, проверок личности и живости
по-прежнему вне задачи ([[границы]] в CLAUDE.md).
