Skip to content

Add dynamic cert noise calibration for FakeTLS handshake#409

Merged
9seconds merged 2 commits into9seconds:masterfrom
dolonet:cert-noise-calibration
Mar 28, 2026
Merged

Add dynamic cert noise calibration for FakeTLS handshake#409
9seconds merged 2 commits into9seconds:masterfrom
dolonet:cert-noise-calibration

Conversation

@dolonet
Copy link
Copy Markdown
Contributor

@dolonet dolonet commented Mar 26, 2026

Closes #408

Summary

The hardcoded noise range (2500-4700 bytes) in the FakeTLS ServerHello doesn't match the real certificate chain sizes for many popular fronting domains. This makes the proxy detectable by DPI that compares ApplicationData size with the actual cert chain size for the SNI domain.

Measured sizes for popular domains:

Domain Real size (bytes) In 2500-4700?
github.com 2932 yes
www.google.com 3953 yes
cdn.jsdelivr.net 5488 no (+17%)
dl.google.com 6480 no (+38%)
www.microsoft.com 8169 no (+74%)
microsoft.com 13004 no (+177%)

Changes

  • On proxy startup, probe the fronting domain via real TLS connections to measure the actual encrypted handshake size (ApplicationData bytes between CCS and application data)
  • Use measured mean ± jitter instead of the static 2500-4700 range
  • Falls back to legacy range if probe fails (domain unreachable, etc.)
  • Optional caching of probe results between restarts (noise-cache-path, noise-cache-ttl)
  • Configurable probe count (noise-probe-count, default 15)
  • Fully backward compatible: zero-value NoiseParams{} preserves legacy behavior

New config options under [defense.doppelganger]:

noise-probe-count = 15       # number of TLS probes (default)
noise-cache-path = "/etc/mtg/noise-cache.json"  # optional
noise-cache-ttl = "24h"      # cache validity (default)

Test plan

  • All existing tests pass with updated SendServerHello signature
  • New TestCalibratedNoiseSize verifies noise is within mean ± jitter
  • Tested in production with dl.google.com — probe correctly returns mean=6480
  • Cache file is created and used on restart
  • Fallback to legacy range works when probe fails

The hardcoded noise range (2500-4700 bytes) in the FakeTLS ServerHello
does not match the real certificate chain sizes of many popular fronting
domains (e.g., dl.google.com ≈ 6480 bytes, microsoft.com ≈ 13004 bytes).
This makes the proxy detectable by DPI systems that compare the
ApplicationData size with the real cert chain size for the SNI domain.

On startup, probe the fronting domain's actual TLS handshake size and
use the measured value ± jitter instead of the static range. Falls back
to the legacy 2500-4700 range if the probe fails.

Also adds optional caching of probe results between restarts
(noise-cache-path, noise-cache-ttl) and a configurable probe count
(noise-probe-count) under [defense.doppelganger].

Closes 9seconds#408
@zzlyns
Copy link
Copy Markdown

zzlyns commented Mar 27, 2026

А подскажите, по итогу какие домены лучше использоваться по FakeTLS? Я взял mail.ru и не понимаю, хороший это выбор или нет. Обязательно ли [defense.doppelganger] прописывать связанный с mail.ru в этом случае?

Уже пару дней есть жалобы от друзей с Мегафона (Москва) - на нем через раз подключается к mtg клиент.

Copy link
Copy Markdown
Owner

@9seconds 9seconds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Спасибо большое, хороший PR. Самое основное - тут идет дублирование логики уже существующего компонента, который прямо и создавался с целью поизмерить что-то в TLS фронта. Давайте попробуем туда перенести.

Кроме того, хотелось бы ничего на диске не хранить. Уже в первом приближении можно получить достаточно хорошие результаты, чтобы не возится с дисковым состоянием

@@ -0,0 +1,261 @@
package fake
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мне кажется, это лучше перенести в doppelganger/scout. Скаут как раз и задумывался как некоторый такой паук, который ходит к фронту и собирает с него некую статистику. Там уже есть периодичность.

Да, сейчас он только про сбор задержек по списку урлов, но, возможно, имеет смысл именно его модифицировать, чтобы он ходил на / по умолчанию.

Хочется просто не распылять сервисные обращения к фронту с целью получить какую-то стастистку, а хранить все в одном месте.

return CertProbeResult{Mean: cache.Mean, Jitter: cache.Jitter}, true
}

// SaveCachedProbe writes a probe result to path as JSON.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я не уверен, стоит ли хранить это состояние на диске. Буквально в первую секунду работы прокси первым же запросом можно получить довольно хорошее приближение (а скорее всего, даже нормальное число) этого размера. Мне кажется, стоит таким состоянием пренебречь. Все-таки отсутствие внешнего стейта сильно всегда упрощает дело, не надо возиться с устареваним и так далее.

return os.WriteFile(path, data, 0o644) //nolint: gosec
}

// ProbeCertSize connects to hostname:port via TLS multiple times and measures
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, тут прямо дублируется логика скаута. Давайте эту функциональность туда перенесем?

return capture.appDataBytes, nil
}

// recordCapture wraps a net.Conn and parses the raw TLS record stream to
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, посмотрите в сторону скаута, пожалуйста. Там даже такая логика есть

Instead of a separate cert_probe.go that duplicates the scout's TLS
connection logic, measure the cert chain size directly from the same
HTTPS connections the scout already makes.

Changes:
- Extend ScoutConnResult with payloadLen field
- Add Write interception to ScoutConn for handshake boundary detection
- Scout.learn() now computes cert size (sum of ApplicationData between
  CCS and first client Write) alongside inter-record durations
- Ganger aggregates cert sizes across raids and exposes NoiseParams()
  via atomic pointer for lock-free reads from proxy goroutines
- Proxy reads NoiseParams from Ganger on each handshake instead of
  probing at startup
- Remove cert_probe.go, disk cache, and related config options
  (noise-cache-path, noise-cache-ttl, noise-probe-count)

Falls back to legacy 2500-4700 range until the first scout raid
completes (typically within 1-2 seconds of startup).
@dolonet dolonet force-pushed the cert-noise-calibration branch from 7a2a3fc to 9dfd992 Compare March 27, 2026 13:34
@dolonet
Copy link
Copy Markdown
Contributor Author

dolonet commented Mar 27, 2026

Переработал по замечаниям.

  1. Перенёс измерение cert size в скаут. ScoutConnResult теперь хранит payloadLen, ScoutConn перехватывает Write для определения границы handshake (когда клиент пишет post-CCS — все server handshake records уже получены). Scout.learn() считает cert size как сумму ApplicationData между CCS и первым Write, параллельно с существующим сбором таймингов.

  2. Ganger агрегирует cert sizes из scout raids и хранит NoiseParams через atomic.Pointer — lock-free чтение из proxy-горутин. Proxy читает doppelGanger.NoiseParams() при каждом handshake вместо фиксированного значения.

  3. Убрал дисковый кэш — скаут запускается при старте и результат доступен через 1-2 секунды. До первого результата используется fallback 2500-4700.

  4. Удалил cert_probe.go и конфиг-опции noise-cache-path, noise-cache-ttl, noise-probe-count.

Все тесты проходят, включая -race.

Copy link
Copy Markdown
Owner

@9seconds 9seconds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Круто, очень круто, спасибо!

@9seconds 9seconds merged commit cc4b6ce into 9seconds:master Mar 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Размер шума в FakeTLS ServerHello не соответствует реальным сертификатам

3 participants