Пока пользователь решает captcha вручную, поднимаем сессионный captchaActive:
watchdog не считает подключение зависшим (двигает дедлайн) и отдаёт StateCaptcha
вместо connecting, чтобы UI не показывал ошибку.
Экспортируем SetManualCaptcha(bool); при включении прокидываем ManualOnly в
vk.Config, и решатель идёт ручным путём с первой попытки (минуя авто-решатель).
Когда авто-решатель captcha не справился, прокидываем в vk-провайдер
ManualSolverFunc, который переиспользует SolveViaProxy и сигналит UI через
gomobile-интерфейс CaptchaPresenter (Show/Hide). Без презентера поведение
прежнее. Файлы библиотеки не затронуты - мостик в новом файле vk/iosmanual.go.
- безопасная конверсия uint64→int64 (clampToInt64) для счётчиков,
экспортируемых через gomobile, чтобы не ловить G115;
- анонимный ресивер у bufLogger.write (unused-receiver).
udpParams заполнял только ObfKey, но не Profile/ObfTiming, как это
делает CLI-клиент. Без профиля клиент обфусцировал трафик не так, как
ждёт сервер (OBF_PROFILE=rtpopus): VK-аутентификация и TURN-аллокация
проходили, но дальше Tx рос, Rx оставался 0 и стримы отваливались.
без RTP-профиля паковать нечего, а в tcp-режиме pacing ломает RTT/конгешн
KCP. Раньше флаг в tcp молча игнорировался. Валидация симметрична на
клиенте и сервере.
Флаг -browser chrome|firefox (default firefox, Chrome - escape-hatch), client-local для VK-auth, в freeturn:// URI не идёт. browserprofile.ForKind даёт канонные UA/JA3/Accept-Language; ApplyFhttp шлёт sec-ch-ua* только для Chrome (Firefox их не отправляет). dial.go выбирает profiles.Firefox_147 / Chrome_146 под UA.
Control-plane TLS-dial переведён на MultiSplitWriteConn (фрагментация SNI).
MultiSplitWriteConn парсит TLS ClientHello, находит SNI host_name и режет первый сегмент внутри него на ~трети с джиттером - registrable-домен гарантированно пересекает границу TCP-сегмента, DPI без реассемблинга не собирает hostname. Фоллбэк на одиночный рез offset 6, если SNI не найден.
turndial: STUN-split offset рандомизирован в [5,7] (рез внутри magic cookie), убран статический фингерпринт фикс-offset.
rtpopus2 несёт RTP header extension (RFC 8285 one-byte: X=1,
ssrc-audio-level + transport-cc) - ближе к современному WebRTC, чем
rtpopus (X=0). Wire несовместим с rtpopus, overhead 52 против 40.
wire.Codec - общий контракт профиля; NewClientCodec/Listen диспатчат
по -obf-profile, RelayPacketConn обобщён по Codec. Профиль тянется
в pipeline (раньше шёл только ключ obf).
Попутно: tcpfwd на 1-based streamID (единый базис с udprelay для
CacheID); cmd/client задаёт probe-host login.vk.ru для vk.
auto-режим пробил UDP/53 нейтральным dns.google и залипал на UDP
навсегда. При выборочной фильтрации (dns.google резолвится, целевой
хост - нет) это вело к таймаутам резолва. Проба теперь валидирует
резолв реального хоста; при провале UDP уходит на DoH. Добавлен
рантайм-recovery в обе стороны: серия неудач UDP -> DoH, периодическая
перепроба DoH -> UDP.
CacheID делил streamID/streamsPerCache, но streamID 1-based: поток
с номером, кратным streamsPerCache, переваливал в соседний bucket
и тянул лишнюю VK-авторизацию (заметно при -n == -streams-per-cred).
Теперь (streamID-1)/streamsPerCache - блок 1..N делит один кэш.
freeturn:// теперь несёт base64url(json) вместо query-string. Поля:
v, provider, peer, transport, mode, bond, obf, key, n, spc, cid,
listen, dns, dnss, mcap, name. Версионирован полем v=1.
config.PeekSubURL: pre-parse -sub до ParseClient, URI первой ноды
подсовывается позиционным аргументом - применение общим путём.
ParseClient применяет все новые поля URI (N, StreamsPerCred,
ClientID, Listen, DNSMode, DNSServers, ManualCaptcha, transport).