mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
style: ASCII-пунктуация
This commit is contained in:
+2
-2
@@ -40,7 +40,7 @@ func main() {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
os.Exit(0)
|
||||
}
|
||||
// логгер ещё не создан — единственный fatal до его инициализации.
|
||||
// логгер ещё не создан - единственный fatal до его инициализации.
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
logger := logx.New(cfg.Log.Debug)
|
||||
@@ -180,7 +180,7 @@ func handleAccepted(ctx context.Context, logger logx.Logger, registry *bondserve
|
||||
}
|
||||
logger.Debugf("Handshake done")
|
||||
|
||||
// Client ID читается всегда (клиент всегда шлёт его первой app-record) —
|
||||
// Client ID читается всегда (клиент всегда шлёт его первой app-record) -
|
||||
// wire-контракт симметричен. -clients-file включает только enforce по allowlist.
|
||||
clientID, err := clientsdb.ReadClientID(dtlsConn)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,7 +24,7 @@ func AppDialer(mode string) net.Dialer {
|
||||
}
|
||||
|
||||
// InstallGlobalResolver выставляет net.DefaultResolver на тот же DNS-транспорт,
|
||||
// что и AppDialer — чтобы сторонние библиотеки, собирающие свой http.Client
|
||||
// что и AppDialer - чтобы сторонние библиотеки, собирающие свой http.Client
|
||||
// без нашего Dialer, тоже шли через DoH / auto-fallback, а не OS resolver.
|
||||
func InstallGlobalResolver(mode string) {
|
||||
d := AppDialer(mode)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package dnsdial владеет DNS-резолвингом и net.Dialer'ом, прокинутым во все
|
||||
// outbound HTTP/TLS клиенты. По Mode выбирает UDP/53, DNS-over-HTTPS или auto
|
||||
// (UDP-probe → sticky DoH fallback).
|
||||
// (UDP-probe -> sticky DoH fallback).
|
||||
package dnsdial
|
||||
|
||||
import (
|
||||
@@ -50,12 +50,12 @@ const (
|
||||
autoDoHProbeBudget = 6 * time.Second
|
||||
|
||||
autoReevalInterval = 30 * time.Second // мин. интервал авто-перепроб (recovery)
|
||||
autoUDPFailThreshold = 2 // подряд неудачных UDP-резолвов → перепроба
|
||||
autoUDPFailThreshold = 2 // подряд неудачных UDP-резолвов -> перепроба
|
||||
dnsMinResponseBytes = 12 // размер DNS-заголовка; меньше = не ответ
|
||||
|
||||
// defaultProbeHost - нейтральный дефолт auto-пробы, если провайдер не задал
|
||||
// свой через SetProbeHost. Провайдеру стоит указывать реальную control-plane
|
||||
// цель (VK → login.vk.ru), чтобы проба мерила резолв нужного домена.
|
||||
// цель (VK -> login.vk.ru), чтобы проба мерила резолв нужного домена.
|
||||
defaultProbeHost = "dns.google"
|
||||
)
|
||||
|
||||
@@ -496,7 +496,7 @@ func SetProbeHost(host string) {
|
||||
// рантайме (не one-shot): на UDP серия неудачных резолвов уводит на DoH; на DoH
|
||||
// периодическая перепроба возвращает на UDP, когда цель снова резолвится. Решение
|
||||
// принимается по резолву реального control-plane хоста (probeHost), а не
|
||||
// нейтрального домена: ТСПУ фильтрует выборочно, «UDP жив на постороннем домене»
|
||||
// нейтрального домена: ТСПУ фильтрует выборочно, "UDP жив на постороннем домене"
|
||||
// ничего не гарантирует.
|
||||
type autoState struct {
|
||||
udpDial dialFunc
|
||||
@@ -524,10 +524,10 @@ func autoDial(r *DohResolver) dialFunc {
|
||||
}
|
||||
|
||||
// decide пробует резолвить цель и обновляет useDoH:
|
||||
// 1. UDP оператора резолвит цель в валидный адрес → UDP;
|
||||
// 2. уже на DoH и цель по UDP всё ещё недоступна → остаёмся (без лишней DoH-пробы);
|
||||
// 3. иначе DoH резолвит цель → DoH;
|
||||
// 4. ни то ни другое → DoH (больше шансов на recovery).
|
||||
// 1. UDP оператора резолвит цель в валидный адрес -> UDP;
|
||||
// 2. уже на DoH и цель по UDP всё ещё недоступна -> остаёмся (без лишней DoH-пробы);
|
||||
// 3. иначе DoH резолвит цель -> DoH;
|
||||
// 4. ни то ни другое -> DoH (больше шансов на recovery).
|
||||
//
|
||||
// quiet подавляет лог перехода - для стартового решения, логируемого в dial().
|
||||
func (a *autoState) decide(quiet bool) {
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestAutoDial_StickyAfterUDPFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
// startFakeUDPDNS поднимает локальный UDP DNS, отвечающий на A-запрос фиксированным
|
||||
// answer (nil → NOERROR без записей). Возвращает адрес ip:port.
|
||||
// answer (nil -> NOERROR без записей). Возвращает адрес ip:port.
|
||||
func startFakeUDPDNS(t *testing.T, answer net.IP) string {
|
||||
t.Helper()
|
||||
pc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
@@ -146,7 +146,7 @@ func startFakeUDPDNS(t *testing.T, answer net.IP) string {
|
||||
func TestProbeResolves(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
answer net.IP // nil → NOERROR без A-записей (домен дропнут/отфильтрован)
|
||||
answer net.IP // nil -> NOERROR без A-записей (домен дропнут/отфильтрован)
|
||||
want bool
|
||||
}{
|
||||
{"valid public A", net.ParseIP("93.184.216.34"), true},
|
||||
@@ -172,7 +172,7 @@ func failDial(context.Context, string, string) (net.Conn, error) {
|
||||
return nil, errors.New("dial unavailable")
|
||||
}
|
||||
|
||||
// UDP не резолвит цель и DoH-проба тоже падает → auto залипает на DoH.
|
||||
// UDP не резолвит цель и DoH-проба тоже падает -> auto залипает на DoH.
|
||||
func TestAutoState_FailsOverToDoH(t *testing.T) {
|
||||
a := &autoState{udpDial: udpDNSDial, dohDial: failDial, host: func() string { return "login.vk.ru" }}
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestAutoState_FailsOverToDoH(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Залипли на DoH, UDP снова резолвит цель → перепроба возвращает на UDP (recovery).
|
||||
// Залипли на DoH, UDP снова резолвит цель -> перепроба возвращает на UDP (recovery).
|
||||
func TestAutoState_RecoversToUDP(t *testing.T) {
|
||||
a := &autoState{udpDial: udpDNSDial, dohDial: failDial, host: func() string { return "login.vk.ru" }}
|
||||
a.useDoH.Store(true) // имитируем залипание на DoH
|
||||
|
||||
@@ -46,10 +46,10 @@ func (l *listener) Accept() (net.Conn, error) {
|
||||
addrlen := uintptr(128)
|
||||
|
||||
// i386 сетевые syscall'ы мультиплексируются через socketcall (102).
|
||||
// SYS_ACCEPT — subcall 5.
|
||||
// SYS_ACCEPT - subcall 5.
|
||||
args := [3]uintptr{uintptr(l.fd), uintptr(unsafe.Pointer(&addr[0])), uintptr(unsafe.Pointer(&addrlen))}
|
||||
|
||||
// Syscall6 — чтобы хватило регистров аргументов на этой платформе.
|
||||
// Syscall6 - чтобы хватило регистров аргументов на этой платформе.
|
||||
r1, _, errno := syscall.Syscall6(102, 5, uintptr(unsafe.Pointer(&args)), 0, 0, 0, 0)
|
||||
if errno != 0 {
|
||||
if errno == syscall.EINTR {
|
||||
@@ -63,7 +63,7 @@ func (l *listener) Accept() (net.Conn, error) {
|
||||
_ = syscall.SetsockoptInt(nfd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, 256*1024)
|
||||
_ = syscall.SetsockoptInt(nfd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, 256*1024)
|
||||
|
||||
// избегаем net.FileConn — она регистрирует fd в Go epoll poller'е, что в
|
||||
// избегаем net.FileConn - она регистрирует fd в Go epoll poller'е, что в
|
||||
// iSH стабильно падает с EEXIST. Возвращаем кастомный blocking net.Conn.
|
||||
conn := &ishConn{fd: nfd}
|
||||
return conn, nil
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//go:build !(linux && 386)
|
||||
|
||||
// Package ish — TCP listener shim для iSH (https://ish.app), Linux user-mode
|
||||
// Package ish - TCP listener shim для iSH (https://ish.app), Linux user-mode
|
||||
// эмулятор на iOS. iSH таргетит linux/386 и не имеет современного accept4 /
|
||||
// epoll-poller Go, поэтому там нужен sandbox-aware accept loop. На любом
|
||||
// другом GOOS/GOARCH WrapListener — no-op passthrough.
|
||||
// другом GOOS/GOARCH WrapListener - no-op passthrough.
|
||||
package ish
|
||||
|
||||
import "net"
|
||||
|
||||
@@ -143,7 +143,7 @@ func (db *DB) save() error {
|
||||
}
|
||||
|
||||
tmpFile := db.path + ".tmp"
|
||||
err = os.WriteFile(tmpFile, b, 0o600) // содержит Client ID (auth-токены) — не world-readable
|
||||
err = os.WriteFile(tmpFile, b, 0o600) // содержит Client ID (auth-токены) - не world-readable
|
||||
if err == nil {
|
||||
err = os.Rename(tmpFile, db.path)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Package logx — минимальный уровневый логгер поверх stdlib log.
|
||||
// Package logx - минимальный уровневый логгер поверх stdlib log.
|
||||
// Вызывающий получает Logger и зовёт Debugf/Infof/Warnf/Errorf;
|
||||
// Debugf управляется debug-флагом, остальные уровни всегда пишут.
|
||||
package logx
|
||||
|
||||
import "log"
|
||||
|
||||
// Logger — интерфейс уровневого логирования.
|
||||
// Logger - интерфейс уровневого логирования.
|
||||
// Debugf гейтится debug-флагом конструктора; остальные уровни печатают всегда.
|
||||
type Logger interface {
|
||||
Debugf(format string, v ...any)
|
||||
@@ -22,7 +22,7 @@ type stdLogger struct {
|
||||
debug bool
|
||||
}
|
||||
|
||||
// New возвращает Logger поверх stdlib log. При debug=false Debugf — no-op.
|
||||
// New возвращает Logger поверх stdlib log. При debug=false Debugf - no-op.
|
||||
func New(debug bool) Logger {
|
||||
return &stdLogger{debug: debug}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package netconn — мелкие net.Conn / transport.Net адаптеры для клиента
|
||||
// Package netconn - мелкие net.Conn / transport.Net адаптеры для клиента
|
||||
// и сервера: passthrough transport.Net (для pion turn), ConnectedUDPConn
|
||||
// (WriteTo поверх dialed UDPConn) и SplitFirstWriteConn (обход DPI через
|
||||
// разбиение первого сегмента).
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// BiCopy запускает io.Copy в обе стороны между c1 и c2; возвращает
|
||||
// (bytesC1FromC2, bytesC2FromC1). Как только одна копия завершилась
|
||||
// (EOF/ошибка/cancel) — обоим концам выставляется deadline=time.Now(),
|
||||
// (EOF/ошибка/cancel) - обоим концам выставляется deadline=time.Now(),
|
||||
// чтобы разблокировать парную горутину. Ошибки уходят в errf, если он не nil.
|
||||
func BiCopy(ctx context.Context, c1, c2 net.Conn, errf func(format string, v ...any)) (int64, int64) {
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
//
|
||||
// Реализации (internal/provider/vk, ...) поставляют
|
||||
// короткоживущие user/pass/addr для TURN-allocate. Pipeline (proxy/udprelay,
|
||||
// proxy/tcpfwd) работает только через этот интерфейс — никаких VK-specific
|
||||
// proxy/tcpfwd) работает только через этот интерфейс - никаких VK-specific
|
||||
// типов в общем коде.
|
||||
//
|
||||
// Sentinel-ошибки описывают provider-нейтральные состояния, которые pipeline
|
||||
// обрабатывает однотипно: ErrBackoffActive — провайдер просит подождать,
|
||||
// ErrFatalNoStreams — фатально, дальше пробовать бессмысленно.
|
||||
// обрабатывает однотипно: ErrBackoffActive - провайдер просит подождать,
|
||||
// ErrFatalNoStreams - фатально, дальше пробовать бессмысленно.
|
||||
package provider
|
||||
|
||||
import (
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Credentials — выданные провайдером TURN-реквизиты для одного стрима.
|
||||
// ServerAddrs — кандидаты host:port в порядке предпочтения (первый —
|
||||
// Credentials - выданные провайдером TURN-реквизиты для одного стрима.
|
||||
// ServerAddrs - кандидаты host:port в порядке предпочтения (первый -
|
||||
// предпочтительный для этого streamID). Pipeline пробует их по очереди при
|
||||
// allocate: если первый не проходит (DPI-дроп/RST), берёт следующий.
|
||||
type Credentials struct {
|
||||
@@ -27,7 +27,7 @@ type Credentials struct {
|
||||
ExpiresAt time.Time // 0 если провайдер не сообщает дедлайн
|
||||
}
|
||||
|
||||
// Provider — источник TURN-реквизитов для клиентского pipeline.
|
||||
// Provider - источник TURN-реквизитов для клиентского pipeline.
|
||||
// Реализации обязаны быть thread-safe: pipeline вызывает GetCredentials и
|
||||
// HandleAuthError из N разных горутин одновременно (по числу стримов).
|
||||
type Provider interface {
|
||||
@@ -35,7 +35,7 @@ type Provider interface {
|
||||
// Реализация отвечает за throttling, кеширование, ретраи к upstream-API.
|
||||
GetCredentials(ctx context.Context, streamID int) (Credentials, error)
|
||||
|
||||
// IsAuthError — провайдер сам решает, какие ошибки от TURN-allocate
|
||||
// IsAuthError - провайдер сам решает, какие ошибки от TURN-allocate
|
||||
// (401/403/stale-nonce/etc.) считать auth-related и приводить к
|
||||
// инвалидации кеша через HandleAuthError.
|
||||
IsAuthError(err error) bool
|
||||
@@ -47,24 +47,24 @@ type Provider interface {
|
||||
// ResetErrors обнуляет счётчик ошибок (вызывать после успешного allocate).
|
||||
ResetErrors(streamID int)
|
||||
|
||||
// BackoffUntilUnix — unix-секунда, до которой провайдер просит pipeline
|
||||
// не звать GetCredentials. 0 — нет активного бэк-оффа.
|
||||
// BackoffUntilUnix - unix-секунда, до которой провайдер просит pipeline
|
||||
// не звать GetCredentials. 0 - нет активного бэк-оффа.
|
||||
// Используется только при возврате ошибки, обёрнутой ErrBackoffActive.
|
||||
BackoffUntilUnix() int64
|
||||
|
||||
// Name — короткое имя провайдера для логов ("vk", ...).
|
||||
// Name - короткое имя провайдера для логов ("vk", ...).
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Sentinel-ошибки, которые провайдер оборачивает (через errors.Join или
|
||||
// fmt.Errorf("%w: ...", ErrXxx, inner)) для сигнализации pipeline.
|
||||
var (
|
||||
// ErrBackoffActive — провайдер просит подождать. Pipeline вызывает
|
||||
// BackoffUntilUnix() для получения точного дедлайна; если 0 — sleep по
|
||||
// ErrBackoffActive - провайдер просит подождать. Pipeline вызывает
|
||||
// BackoffUntilUnix() для получения точного дедлайна; если 0 - sleep по
|
||||
// дефолтному backoff (60s).
|
||||
ErrBackoffActive = errors.New("provider backoff active")
|
||||
|
||||
// ErrFatalNoStreams — провайдер не может выдать креды и ни один стрим
|
||||
// ErrFatalNoStreams - провайдер не может выдать креды и ни один стрим
|
||||
// ещё не поднят. Pipeline возвращает фатальную ошибку наружу
|
||||
// (хост-процесс делает os.Exit без вмешательства pipeline).
|
||||
ErrFatalNoStreams = errors.New("provider fatal: no streams alive")
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/randx"
|
||||
)
|
||||
|
||||
// Log — пакетный логгер. По умолчанию no-op; main устанавливает его через
|
||||
// Log - пакетный логгер. По умолчанию no-op; main устанавливает его через
|
||||
// SetLogger, чтобы captcha-вывод подчинялся глобальному -debug. Solve также
|
||||
// принимает logger параметром для DI.
|
||||
var Log logx.Logger = logx.Nop()
|
||||
@@ -125,7 +125,7 @@ func (s *captchaSession) logger() logx.Logger {
|
||||
}
|
||||
|
||||
// Solve запускает авторешение captcha против VK captchaNotRobot API
|
||||
// и возвращает success-токен при успехе. log может быть nil — тогда
|
||||
// и возвращает success-токен при успехе. log может быть nil - тогда
|
||||
// используется пакетный Log.
|
||||
func Solve(
|
||||
ctx context.Context,
|
||||
@@ -479,7 +479,7 @@ func solveCaptchaPoW(ctx context.Context, input string, difficulty int) string {
|
||||
return ""
|
||||
}
|
||||
target := strings.Repeat("0", difficulty)
|
||||
// ctx-check каждые 1024 итерации — задержка отмены в пределах нескольких мс
|
||||
// ctx-check каждые 1024 итерации - задержка отмены в пределах нескольких мс
|
||||
// даже на слабом ARM (было 4096).
|
||||
buf := make([]byte, 0, len(input)+20)
|
||||
buf = append(buf, input...)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package manual реализует ручное решение CAPTCHA — fallback, когда автосолвер
|
||||
// Package manual реализует ручное решение CAPTCHA - fallback, когда автосолвер
|
||||
// (internal/provider/vk/internal/captcha) не справился или пользователь дал -manual-captcha.
|
||||
// Поднимает локальный HTTP-сервер 127.0.0.1:8765, проксирующий страницу VK
|
||||
// CAPTCHA (с переписыванием абсолютных URL и gzip), открывает её в браузере и
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
// браузерного трафика. Ставится из main после разбора cfg.
|
||||
var Debug bool
|
||||
|
||||
// Log — пакетный логгер; по умолчанию no-op. main устанавливает его через
|
||||
// Log - пакетный логгер; по умолчанию no-op. main устанавливает его через
|
||||
// SetLogger, чтобы вывод подчинялся -debug.
|
||||
var Log logx.Logger = logx.Nop()
|
||||
|
||||
@@ -217,7 +217,7 @@ var htmlStyleContentRe = regexp.MustCompile(`(?is)(<style[^>]*>)(.*?)(</style>)`
|
||||
|
||||
// rewriteHTMLAttrsServerSide переписывает абсолютные и protocol-relative URL
|
||||
// в src/href/action HTML на стороне сервера. URL, совпадающие с upstream origin,
|
||||
// идут на localhost; остальные — через /generic_proxy, чтобы cross-domain
|
||||
// идут на localhost; остальные - через /generic_proxy, чтобы cross-domain
|
||||
// ресурсы (st.vk.ru, userapi.com и т.д.) грузились через прокси.
|
||||
func rewriteHTMLAttrsServerSide(html string, targetURL *neturl.URL) string {
|
||||
localOrigin := localCaptchaOrigin()
|
||||
@@ -235,7 +235,7 @@ func rewriteHTMLAttrsServerSide(html string, targetURL *neturl.URL) string {
|
||||
if strings.HasPrefix(absURL, localOrigin) {
|
||||
return rawURL
|
||||
}
|
||||
// прочие абсолютные URL → через generic_proxy
|
||||
// прочие абсолютные URL -> через generic_proxy
|
||||
return "/generic_proxy?proxy_url=" + neturl.QueryEscape(absURL)
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
|
||||
|
||||
// Шаг 2: серверный rewrite остальных абсолютных URL в HTML-атрибутах.
|
||||
// Критично: браузер начинает грузить <script src> / <link href> / <img src>
|
||||
// сразу при парсинге HTML — раньше любых инжектированных JS-перехватов.
|
||||
// сразу при парсинге HTML - раньше любых инжектированных JS-перехватов.
|
||||
html = rewriteHTMLAttrsServerSide(html, targetURL)
|
||||
|
||||
script := fmt.Sprintf(`
|
||||
@@ -490,7 +490,7 @@ func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
|
||||
</script>
|
||||
`, localOrigin, upstreamOrigin)
|
||||
|
||||
// Шаг 3: инжектируем клиентский скрипт как можно раньше — после <head>,
|
||||
// Шаг 3: инжектируем клиентский скрипт как можно раньше - после <head>,
|
||||
// чтобы XHR/fetch-перехваты были активны до любого inline <script> в <head>.
|
||||
switch {
|
||||
case strings.Contains(html, "<head>"):
|
||||
@@ -556,7 +556,7 @@ func runCaptchaServerAndWait(ctx context.Context, handler http.Handler, captchaU
|
||||
}
|
||||
|
||||
defer func() { //nolint:contextcheck // shutdown intentionally uses fresh context after parent is cancelled
|
||||
// best-effort shutdown. На iSH SetDeadline — no-op, Shutdown может
|
||||
// best-effort shutdown. На iSH SetDeadline - no-op, Shutdown может
|
||||
// таймаутить при сливе listener'ов; результат всё равно прокидываем.
|
||||
shutCtx, shutCancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer shutCancel()
|
||||
@@ -709,7 +709,7 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) (
|
||||
|
||||
if res.StatusCode >= 300 && res.StatusCode < 400 {
|
||||
if loc := res.Header.Get("Location"); loc != "" {
|
||||
// не логируем полный redirect URL — шум в консоли
|
||||
// не логируем полный redirect URL - шум в консоли
|
||||
if rewritten, ok := rewriteProxyRedirectLocation(loc, targetURL); ok {
|
||||
res.Header.Set("Location", rewritten)
|
||||
} else {
|
||||
@@ -838,7 +838,7 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) (
|
||||
|
||||
// captchaNotRobot.check уходит на api.vk.ru (другой хост, чем
|
||||
// upstream vk.ru), поэтому идёт через /generic_proxy. Извлекаем
|
||||
// success_token здесь — серверный путь работает на iOS даже если
|
||||
// success_token здесь - серверный путь работает на iOS даже если
|
||||
// JS-callback в браузере не сработал.
|
||||
if strings.Contains(targetAuthURL, "captchaNotRobot.check") {
|
||||
bodyBytes, readErr := io.ReadAll(res.Body)
|
||||
@@ -860,7 +860,7 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) (
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
Log.Debugf("[Captcha Proxy] HTTP %s %s", r.Method, r.URL.Path)
|
||||
if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" {
|
||||
// не логируем полный redirect URL — шум в консоли
|
||||
// не логируем полный redirect URL - шум в консоли
|
||||
http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
@@ -883,11 +883,11 @@ func openBrowser(url string) {
|
||||
func browserOpenCommands(goos string, url string) []browserCommand {
|
||||
switch goos {
|
||||
case "windows":
|
||||
// 'rundll32 url.dll,FileProtocolHandler' надёжнее 'cmd /c start' —
|
||||
// 'rundll32 url.dll,FileProtocolHandler' надёжнее 'cmd /c start' -
|
||||
// не задействует shell (cmd.exe), нет проблем с '&' и спец-символами.
|
||||
return []browserCommand{
|
||||
{name: "rundll32", args: []string{"url.dll,FileProtocolHandler", url}},
|
||||
// fallback с пустым title для 'start' — обход проблем с кавычками
|
||||
// fallback с пустым title для 'start' - обход проблем с кавычками
|
||||
{name: "cmd", args: []string{"/c", "start", "", url}},
|
||||
}
|
||||
case "darwin":
|
||||
|
||||
@@ -37,7 +37,7 @@ func parseLines(s string) []string {
|
||||
}
|
||||
|
||||
// feminizeSurname переводит мужскую фамилию в женскую форму.
|
||||
// Работает по рунам — кириллица многобайтовая в UTF-8.
|
||||
// Работает по рунам - кириллица многобайтовая в UTF-8.
|
||||
// Фамилии с не-русскими суффиксами возвращаются без изменений
|
||||
// (иностранные / мемные).
|
||||
func feminizeSurname(surname string) string {
|
||||
@@ -55,7 +55,7 @@ func feminizeSurname(surname string) string {
|
||||
return surname
|
||||
}
|
||||
|
||||
// SurnameProbability — вероятность того, что Generate вернёт имя с фамилией.
|
||||
// SurnameProbability - вероятность того, что Generate вернёт имя с фамилией.
|
||||
const SurnameProbability = 0.7
|
||||
|
||||
// Generate возвращает случайное "Имя" или "Имя Фамилия".
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// DefaultAutoSolve запускает in-page widget flow. Загружает сохранённый профиль
|
||||
// браузера, если доступен, — капча видит стабильные fingerprints на этапе
|
||||
// браузера, если доступен, - капча видит стабильные fingerprints на этапе
|
||||
// загрузки страницы и POST-запроса.
|
||||
func DefaultAutoSolve(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -15,7 +15,7 @@ type StreamCredentialsCache struct {
|
||||
lastErrorTime atomic.Int64
|
||||
}
|
||||
|
||||
// Store отображает cache-id ((streamID-1) / streamsPerCache) → StreamCredentialsCache.
|
||||
// Store отображает cache-id ((streamID-1) / streamsPerCache) -> StreamCredentialsCache.
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
caches map[int]*StreamCredentialsCache
|
||||
|
||||
@@ -53,10 +53,10 @@ func TestIsAuthError(t *testing.T) {
|
||||
func TestStoreCacheGrouping(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// streamID 1-based (как в pipeline): потоки 1..10 → cache 0, 11..20 → cache 1.
|
||||
// streamID 1-based (как в pipeline): потоки 1..10 -> cache 0, 11..20 -> cache 1.
|
||||
s := NewStore(10)
|
||||
if s.CacheID(1) != 0 || s.CacheID(10) != 0 || s.CacheID(11) != 1 {
|
||||
t.Fatalf("unexpected cache grouping: 1→%d 10→%d 11→%d", s.CacheID(1), s.CacheID(10), s.CacheID(11))
|
||||
t.Fatalf("unexpected cache grouping: 1->%d 10->%d 11->%d", s.CacheID(1), s.CacheID(10), s.CacheID(11))
|
||||
}
|
||||
a := s.Get(3)
|
||||
b := s.Get(7)
|
||||
@@ -68,16 +68,16 @@ func TestStoreCacheGrouping(t *testing.T) {
|
||||
t.Error("streams 3 and 13 should not share cache (groups 0 vs 1)")
|
||||
}
|
||||
|
||||
// streams-per-cred == n: все потоки 1..n делят один кэш — поток n не должен
|
||||
// streams-per-cred == n: все потоки 1..n делят один кэш - поток n не должен
|
||||
// переваливать в отдельный bucket (регрессия на -n 7 -streams-per-cred 7).
|
||||
s7 := NewStore(7)
|
||||
for id := 1; id <= 7; id++ {
|
||||
if s7.CacheID(id) != 0 {
|
||||
t.Errorf("streams-per-cred=7: stream %d → cache %d, want 0", id, s7.CacheID(id))
|
||||
t.Errorf("streams-per-cred=7: stream %d -> cache %d, want 0", id, s7.CacheID(id))
|
||||
}
|
||||
}
|
||||
if s7.CacheID(8) != 1 {
|
||||
t.Errorf("streams-per-cred=7: stream 8 → cache %d, want 1", s7.CacheID(8))
|
||||
t.Errorf("streams-per-cred=7: stream 8 -> cache %d, want 1", s7.CacheID(8))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestOrderAddrs(t *testing.T) {
|
||||
t.Errorf("orderAddrs mutated input: %v", addrs)
|
||||
}
|
||||
|
||||
// Один адрес — копия из одного элемента.
|
||||
// Один адрес - копия из одного элемента.
|
||||
if got := orderAddrs([]string{"x"}, 5); !slices.Equal(got, []string{"x"}) {
|
||||
t.Errorf("single addr: got %v", got)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
)
|
||||
|
||||
// CaptchaSolveMode — выбор между автоматическим решением и ручным браузерным fallback.
|
||||
// CaptchaSolveMode - выбор между автоматическим решением и ручным браузерным fallback.
|
||||
type CaptchaSolveMode int
|
||||
|
||||
const (
|
||||
@@ -55,7 +55,7 @@ type AutoSolveFunc func(
|
||||
) (token string, err error)
|
||||
|
||||
// ManualSolveFunc открывает локальный браузерный fallback. Возвращает либо
|
||||
// success_token (token != ""), либо captcha_key — в зависимости от пути
|
||||
// success_token (token != ""), либо captcha_key - в зависимости от пути
|
||||
// ошибки VK.
|
||||
type ManualSolveFunc func(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
)
|
||||
|
||||
// fetchAnonToken — шаг 1 цепочки: обменивает app client_id/client_secret
|
||||
// fetchAnonToken - шаг 1 цепочки: обменивает app client_id/client_secret
|
||||
// на анонимный access token из login.vk.ru.
|
||||
func (c *Client) fetchAnonToken(ctx context.Context, httpClient tlsclient.HttpClient, profile browserprofile.Profile, creds VKCredentials) (string, error) {
|
||||
data := fmt.Sprintf("client_id=%s&token_type=messages&client_secret=%s&version=1&app_id=%s",
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
)
|
||||
|
||||
// fetchCallToken — шаг 2 цепочки: вызывает calls.getAnonymousToken и ведёт
|
||||
// fetchCallToken - шаг 2 цепочки: вызывает calls.getAnonymousToken и ведёт
|
||||
// цикл retry captcha до получения call-токена или исчерпания всех режимов решения.
|
||||
func (c *Client) fetchCallToken(
|
||||
ctx context.Context,
|
||||
@@ -104,7 +104,7 @@ func (c *Client) solveCaptcha(
|
||||
break
|
||||
}
|
||||
c.log.Infof("[STREAM %d] [Captcha] Triggering manual captcha fallback", streamID)
|
||||
// Ручной решалке выделяется свой 3-минутный бюджет — жёсткий parent-deadline
|
||||
// Ручной решалке выделяется свой 3-минутный бюджет - жёсткий parent-deadline
|
||||
// не обрезает время пользователя. Отмена parent (завершение приложения)
|
||||
// всё равно propagate, горутина не переживает процесс.
|
||||
manualCtx, manualCancel := context.WithTimeout(ctx, 3*time.Minute)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
)
|
||||
|
||||
// fetchTurnCreds — шаг 4 цепочки: вызывает vchat.joinConversationByLink
|
||||
// fetchTurnCreds - шаг 4 цепочки: вызывает vchat.joinConversationByLink
|
||||
// и извлекает TURN username, credential и список адресов из ответа.
|
||||
func (c *Client) fetchTurnCreds(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
)
|
||||
|
||||
// fetchOkRuSession — шаг 3 цепочки: получает анонимный ok.ru session_key
|
||||
// fetchOkRuSession - шаг 3 цепочки: получает анонимный ok.ru session_key
|
||||
// через auth.anonymLogin.
|
||||
func (c *Client) fetchOkRuSession(ctx context.Context, httpClient tlsclient.HttpClient, profile browserprofile.Profile) (string, error) {
|
||||
sessionData := fmt.Sprintf(`{"version":2,"device_id":"%s","client_version":1.1,"client_type":"SDK_JS"}`, uuid.New())
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/provider"
|
||||
)
|
||||
|
||||
// VKCredentials — пара app_id/app_secret для получения анонимных токенов.
|
||||
// VKCredentials - пара app_id/app_secret для получения анонимных токенов.
|
||||
type VKCredentials struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
// TurnCredentials — разрешённые TURN-реквизиты для группы потоков.
|
||||
// TurnCredentials - разрешённые TURN-реквизиты для группы потоков.
|
||||
type TurnCredentials struct {
|
||||
Username string
|
||||
Password string
|
||||
@@ -22,9 +22,9 @@ type TurnCredentials struct {
|
||||
Link string
|
||||
}
|
||||
|
||||
// DefaultCredentials — публичные app_id/secret VK SDK, извлечённые из
|
||||
// DefaultCredentials - публичные app_id/secret VK SDK, извлечённые из
|
||||
// официальных VK-клиентов (web/mobile/video). Это НЕ приватные креды
|
||||
// пользователя — VK раздаёт их в JS-бандле страницы калла. Клиент перебирает
|
||||
// пользователя - VK раздаёт их в JS-бандле страницы калла. Клиент перебирает
|
||||
// по порядку при ошибках авторизации.
|
||||
//
|
||||
//nolint:gosec // public VK SDK app credentials, not user secrets
|
||||
@@ -48,7 +48,7 @@ const (
|
||||
// Sentinel-ошибки auth-потока. Строковые формы стабильны (используются в логах).
|
||||
//
|
||||
// ErrCaptchaWaitRequired и ErrFatalCaptchaNoStreams также матчатся через
|
||||
// provider.ErrBackoffActive / provider.ErrFatalNoStreams — pipeline проверяет
|
||||
// provider.ErrBackoffActive / provider.ErrFatalNoStreams - pipeline проверяет
|
||||
// generic-sentinels, vkauth-внутренний код может проверять и старые.
|
||||
var (
|
||||
ErrCaptchaWaitRequired = errors.Join(provider.ErrBackoffActive, errors.New("CAPTCHA_WAIT_REQUIRED"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package bondclient реализует клиентскую сторону bonded VLESS lane:
|
||||
// одно принятое TCP-соединение, распределённое (round-robin) по всем активным
|
||||
// smux-сессиям в tcpfwd.SessionPool. Wire-формат фреймов — internal/wire/bondframe;
|
||||
// smux-сессиям в tcpfwd.SessionPool. Wire-формат фреймов - internal/wire/bondframe;
|
||||
// пакет соединяет copy-loop локального TCP ↔ lanes.
|
||||
package bondclient
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"github.com/xtaci/smux"
|
||||
)
|
||||
|
||||
// Deps — зависимости хост-процесса для bond-клиента.
|
||||
// Deps - зависимости хост-процесса для bond-клиента.
|
||||
type Deps struct {
|
||||
Log logx.Logger
|
||||
}
|
||||
@@ -41,7 +41,7 @@ type Handler struct {
|
||||
Deps Deps
|
||||
}
|
||||
|
||||
// lane — один smux-поток внутри bonded TCP-соединения.
|
||||
// lane - один smux-поток внутри bonded TCP-соединения.
|
||||
type lane struct {
|
||||
ps *tcpfwd.PooledSession
|
||||
stream *smux.Stream
|
||||
@@ -58,7 +58,7 @@ func (h *Handler) Handle(ctx context.Context, tcpConn net.Conn, connID uint64, c
|
||||
defer cancel()
|
||||
|
||||
// Фаза 1: открыть потоки на доступных сессиях. Фаза 2: отправить Hello с
|
||||
// реальным числом lane — bondserver.waitForInitialLanes не будет ждать
|
||||
// реальным числом lane - bondserver.waitForInitialLanes не будет ждать
|
||||
// lane, которые не были открыты.
|
||||
type pending struct {
|
||||
ps *tcpfwd.PooledSession
|
||||
@@ -210,7 +210,7 @@ func (h *Handler) copyTCPToBond(ctx context.Context, connID uint64, tcpConn net.
|
||||
|
||||
// writeBondFrameToNextLane пишет в следующий живой lane в порядке round-robin.
|
||||
// В отличие от bondserver.writeToNextLane (который ждёт новые lane), набор
|
||||
// lane клиента фиксирован на время жизни Handle — нечего ждать, fail fast.
|
||||
// lane клиента фиксирован на время жизни Handle - нечего ждать, fail fast.
|
||||
func writeBondFrameToNextLane(ctx context.Context, lanes []*lane, typ byte, seq uint64, data []byte, laneIdx *uint64) (*lane, error) {
|
||||
for range lanes {
|
||||
idx := *laneIdx % uint64(len(lanes))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package bondserver реализует серверную сторону bonded VLESS lane:
|
||||
// одно backend TCP-соединение, мультиплексированное по N smux-потокам с общим
|
||||
// ConnID. Wire-формат фреймов — internal/wire/bondframe; пакет соединяет
|
||||
// ConnID. Wire-формат фреймов - internal/wire/bondframe; пакет соединяет
|
||||
// copy-loop backend TCP ↔ lanes и реестр per-ConnID.
|
||||
package bondserver
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/xtaci/smux"
|
||||
)
|
||||
|
||||
// laneStream — подмножество *smux.Stream, нужное serverLane. Определено как
|
||||
// laneStream - подмножество *smux.Stream, нужное serverLane. Определено как
|
||||
// интерфейс, чтобы юнит-тесты могли подменять in-memory pipe без смоделированной
|
||||
// smux-сессии.
|
||||
type laneStream interface {
|
||||
@@ -26,7 +26,7 @@ type laneStream interface {
|
||||
SetDeadline(time.Time) error
|
||||
}
|
||||
|
||||
// Deps — зависимости хост-процесса для bond-сервера.
|
||||
// Deps - зависимости хост-процесса для bond-сервера.
|
||||
type Deps struct {
|
||||
Log logx.Logger
|
||||
}
|
||||
@@ -283,8 +283,8 @@ func (c *serverConn) copyBackendToBond(backendConn net.Conn) {
|
||||
for {
|
||||
n, err := backendConn.Read(buf)
|
||||
if n > 0 {
|
||||
// writeToNextLane — синхронная запись (WriteFrame возвращается до
|
||||
// переключения lane), поэтому buf[:n] передаётся напрямую — аналог bondclient.
|
||||
// writeToNextLane - синхронная запись (WriteFrame возвращается до
|
||||
// переключения lane), поэтому buf[:n] передаётся напрямую - аналог bondclient.
|
||||
if writeErr := c.writeToNextLane(seq, buf[:n], &laneIdx); writeErr != nil {
|
||||
c.deps.log().Errorf("[bond %d] lane write data error: %v", c.id, writeErr)
|
||||
return
|
||||
|
||||
@@ -33,7 +33,7 @@ func DialTURN(ctx context.Context, host, port string, udp bool, peer *net.UDPAdd
|
||||
if len(rawURLs) == 0 {
|
||||
return nil, fmt.Errorf("no TURN candidates")
|
||||
}
|
||||
// HostOverride (-turn) принудительно задаёт host → все кандидаты резолвятся
|
||||
// HostOverride (-turn) принудительно задаёт host -> все кандидаты резолвятся
|
||||
// в одну цель, гонять их нет смысла; пробуем только первого.
|
||||
if host != "" {
|
||||
rawURLs = rawURLs[:1]
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/xtaci/smux"
|
||||
)
|
||||
|
||||
// PooledSession — одна TURN+DTLS+KCP+smux сессия в пуле с lifetime-счётчиками.
|
||||
// PooledSession - одна TURN+DTLS+KCP+smux сессия в пуле с lifetime-счётчиками.
|
||||
// Поля экспортированы для учёта per-lane трафика в bondclient;
|
||||
// атомики изменять только через их методы.
|
||||
type PooledSession struct {
|
||||
@@ -26,7 +26,7 @@ type PooledSession struct {
|
||||
FromSession atomic.Uint64
|
||||
}
|
||||
|
||||
// SessionPool — конкурентно-безопасный round-robin пул активных smux-сессий.
|
||||
// SessionPool - конкурентно-безопасный round-robin пул активных smux-сессий.
|
||||
type SessionPool struct {
|
||||
mu sync.RWMutex
|
||||
sessions []*PooledSession
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
const inboundQueueCap = 2000
|
||||
|
||||
// Packet — пулированная UDP-датаграмма, передаваемая из listener'а в per-stream
|
||||
// DTLS-воркер. N — заполненный префикс Data.
|
||||
// Packet - пулированная UDP-датаграмма, передаваемая из listener'а в per-stream
|
||||
// DTLS-воркер. N - заполненный префикс Data.
|
||||
type Packet struct {
|
||||
Data []byte
|
||||
N int
|
||||
@@ -24,7 +24,7 @@ var packetPool = sync.Pool{
|
||||
|
||||
// runListener читает пакеты из listenConn, обновляет кэш active-peer
|
||||
// и публикует каждый пакет в inboundChan. При переполнении канала пакет
|
||||
// отбрасывается — цикл чтения остаётся wait-free.
|
||||
// отбрасывается - цикл чтения остаётся wait-free.
|
||||
func runListener(ctx context.Context, listenConn net.PacketConn, activeLocalPeer *atomic.Value, inboundChan chan<- *Packet) {
|
||||
// Pointer-кэш последнего виденного адреса local peer. Позволяет избежать
|
||||
// per-packet аллокации addr.String() на горячем WG ingest пути:
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
// DTLSLoop поддерживает единственное DTLS-подключение для streamID, перезапуская
|
||||
// его при сбое с backoff 10-30s (пропускается при активном provider-backoff,
|
||||
// если предыдущая ошибка — дедлайн). connchan получает свежую половину
|
||||
// если предыдущая ошибка - дедлайн). connchan получает свежую половину
|
||||
// AsyncPacketPipe на каждой попытке; okchan (non-nil только для потока 1)
|
||||
// сигнализирует о первом успешном handshake.
|
||||
func DTLSLoop(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *Packet, connchan chan<- net.PacketConn, okchan chan<- struct{}, streamID int) {
|
||||
@@ -236,7 +236,7 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
relayConn := stream.Relay
|
||||
deps.log().Debugf("[STREAM %d] TURN server IP: %s", streamID, stream.ServerUDPAddr.IP)
|
||||
|
||||
// Инкремент до ResetErrors — конкурентные наблюдатели HandleAuthError видят
|
||||
// Инкремент до ResetErrors - конкурентные наблюдатели HandleAuthError видят
|
||||
// поток подключённым до сброса счётчика ошибок.
|
||||
deps.ConnectedStreams.Add(1)
|
||||
deps.Auth.ResetErrors(streamID)
|
||||
@@ -270,13 +270,13 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
|
||||
const maxPayload = 1600
|
||||
|
||||
// PermDead закрывается при блэкхоле data-path (см. turndial/permwatch.go) —
|
||||
// PermDead закрывается при блэкхоле data-path (см. turndial/permwatch.go) -
|
||||
// отменяем turnctx, TURNLoop делает свежий allocate.
|
||||
wg.Go(func() {
|
||||
select {
|
||||
case <-turnctx.Done():
|
||||
case <-stream.PermDead:
|
||||
deps.log().Warnf("[STREAM %d] TURN channel-bind умер — рецикл allocation", streamID)
|
||||
deps.log().Warnf("[STREAM %d] TURN channel-bind умер - рецикл allocation", streamID)
|
||||
turncancel()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package udprelay реализует UDP-режим прокси: терминирует DTLS от локального
|
||||
// пира (WireGuard) и ретранслирует пакеты через per-stream TURN-аллокацию
|
||||
// обратно к удалённому пиру. Run — точка входа; владеет локальным listener,
|
||||
// обратно к удалённому пиру. Run - точка входа; владеет локальным listener,
|
||||
// fan-in входящего dispatch и per-stream DTLS/TURN циклами.
|
||||
package udprelay
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// GetCredsFunc реэкспортирован из common, чтобы вызывающие не выходили за пределы импортов пакета.
|
||||
type GetCredsFunc = common.GetCredsFunc
|
||||
|
||||
// AuthHandler — подмножество provider.Provider, необходимое пакету.
|
||||
// AuthHandler - подмножество provider.Provider, необходимое пакету.
|
||||
// Определено как локальный интерфейс, чтобы тесты могли подменять fake без
|
||||
// импорта реализации провайдера. Sentinel-ошибки auth-флоу проверяются через
|
||||
// provider.ErrXxx.
|
||||
@@ -32,7 +32,7 @@ type AuthHandler interface {
|
||||
BackoffUntilUnix() int64
|
||||
}
|
||||
|
||||
// Params — per-stream конфигурация TURN/wrap, общая для DTLS и TURN циклов.
|
||||
// Params - per-stream конфигурация TURN/wrap, общая для DTLS и TURN циклов.
|
||||
type Params struct {
|
||||
Host string
|
||||
Port string
|
||||
@@ -43,14 +43,14 @@ type Params struct {
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// streamStartBarrier — максимум, который стримы 2..N ждут прогрева кэша
|
||||
// streamStartBarrier - максимум, который стримы 2..N ждут прогрева кэша
|
||||
// credentials стримом 1 перед стартом. Защита от вечного стопора, если
|
||||
// стрим 1 не поднимается.
|
||||
const streamStartBarrier = 20 * time.Second
|
||||
|
||||
// ErrFatal возвращается из Run, когда поток встречает условие, требующее
|
||||
// завершения всего приложения (см. provider.ErrFatalNoStreams). Вызывающий
|
||||
// должен проверить через errors.Is и вызвать os.Exit сам — udprelay не
|
||||
// должен проверить через errors.Is и вызвать os.Exit сам - udprelay не
|
||||
// вмешивается в хост-процесс.
|
||||
var ErrFatal = errors.New("udprelay: fatal error")
|
||||
|
||||
@@ -63,7 +63,7 @@ type Deps struct {
|
||||
Log logx.Logger
|
||||
ActiveLocalPeer *atomic.Value
|
||||
ConnectedStreams *atomic.Int32
|
||||
// fatalCh — внутренний сигнальный канал; устанавливается Run, пишется
|
||||
// fatalCh - внутренний сигнальный канал; устанавливается Run, пишется
|
||||
// TURNLoop, читается Run для проброса фатальной ошибки наверх.
|
||||
fatalCh chan error
|
||||
}
|
||||
@@ -75,12 +75,12 @@ func (d *Deps) log() logx.Logger {
|
||||
return d.Log
|
||||
}
|
||||
|
||||
// Run — точка входа UDP-режима. Биндит listenAddr, распределяет входящие пакеты
|
||||
// Run - точка входа UDP-режима. Биндит listenAddr, распределяет входящие пакеты
|
||||
// в общую очередь и запускает numStreams пар (DTLSLoop, TURNLoop).
|
||||
// connectedStreams принадлежит вызывающему (provider может читать через свой
|
||||
// StreamsAlive-аналог) и инкрементируется/декрементируется в oneTURN.
|
||||
// Возвращается после выхода всех потоков (т.е. при отмене ctx).
|
||||
// При фатальной provider-ошибке возвращает ErrFatal — вызывающий делает
|
||||
// При фатальной provider-ошибке возвращает ErrFatal - вызывающий делает
|
||||
// os.Exit без вмешательства udprelay в хост-процесс.
|
||||
func Run(ctx context.Context, dtlsDialer *dtlsdial.Dialer, auth AuthHandler, logger logx.Logger, connectedStreams *atomic.Int32, params *Params, peer *net.UDPAddr, listenAddr string, numStreams int) error {
|
||||
listenConn, err := (&net.ListenConfig{}).ListenPacket(ctx, "udp", listenAddr)
|
||||
@@ -125,7 +125,7 @@ func Run(ctx context.Context, dtlsDialer *dtlsdial.Dialer, auth AuthHandler, log
|
||||
// Остальные стримы ждут этот сигнал (или ctx/safety-timeout), чтобы не бить
|
||||
// по VK API одновременно: стрим 1 прогревает кэш credentials
|
||||
// (streams-per-cred), после чего 2..N переиспользуют тёплый кэш вместо N
|
||||
// параллельных запросов к VK — это снимает thundering herd на старте
|
||||
// параллельных запросов к VK - это снимает thundering herd на старте
|
||||
// (частая причина rate-limit/captcha при одновременном подъёме всех стримов).
|
||||
okchan := make(chan struct{}, 1)
|
||||
{
|
||||
@@ -138,7 +138,7 @@ func Run(ctx context.Context, dtlsDialer *dtlsdial.Dialer, auth AuthHandler, log
|
||||
})
|
||||
}
|
||||
|
||||
// Барьер: ждём первый успешный стрим, отмену ctx, либо safety-timeout —
|
||||
// Барьер: ждём первый успешный стрим, отмену ctx, либо safety-timeout -
|
||||
// если стрим 1 не поднимается, не стопорим остальные навсегда (liveness).
|
||||
select {
|
||||
case <-okchan:
|
||||
|
||||
@@ -65,7 +65,7 @@ func Handle(ctx context.Context, logger logx.Logger, conn net.Conn, connectAddr
|
||||
}
|
||||
|
||||
// copyOne читает из src и пишет в dst до отмены ctx или ошибки любой стороны.
|
||||
// Каждый read/write сбрасывает idle timeout — зависший конец закрывается, а не висит.
|
||||
// Каждый read/write сбрасывает idle timeout - зависший конец закрывается, а не висит.
|
||||
func copyOne(ctx context.Context, logger logx.Logger, src, dst net.Conn, count func(int)) {
|
||||
buf := make([]byte, udpRelayBufSize)
|
||||
for {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package randx — тонкая обёртка над crypto/rand с API в стиле math/rand.
|
||||
// Package randx - тонкая обёртка над crypto/rand с API в стиле math/rand.
|
||||
// Используется там, где gosec G404 бьёт по math/rand: антифингерпринт-jitter,
|
||||
// случайные задержки, выбор имён/паттернов. CSPRNG здесь не критичен по
|
||||
// производительности, зато снимает шум линтера и убирает PRNG-предсказуемость.
|
||||
@@ -22,7 +22,7 @@ func IntN(n int) int {
|
||||
return int(v.Int64())
|
||||
}
|
||||
|
||||
// Intn — алиас IntN для совместимости с math/rand.
|
||||
// Intn - алиас IntN для совместимости с math/rand.
|
||||
func Intn(n int) int { return IntN(n) }
|
||||
|
||||
// Float64 возвращает число из [0.0, 1.0) с 53 битами мантиссы.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package stats — счётчики пропускной способности и обёртка net.Conn
|
||||
// Package stats - счётчики пропускной способности и обёртка net.Conn
|
||||
// с подсчётом байт. Используется и клиентом, и сервером.
|
||||
package stats
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stats хранит счётчики tx/rx байт. При enabled=false Add* — no-op,
|
||||
// Stats хранит счётчики tx/rx байт. При enabled=false Add* - no-op,
|
||||
// LogEvery возвращается сразу (поведение под isDebug).
|
||||
type Stats struct {
|
||||
tx atomic.Uint64
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
//
|
||||
// DTLS в этом проекте используется прежде всего как слой *обфускации*, а не
|
||||
// как граница безопасности. VK TURN content-filter дропает payload, не
|
||||
// похожий на легитимный DTLS handshake с зашифрованными записями, — именно
|
||||
// handshake пропускает нас. Шифрование — бесплатный побочный эффект; мы уже
|
||||
// похожий на легитимный DTLS handshake с зашифрованными записями, - именно
|
||||
// handshake пропускает нас. Шифрование - бесплатный побочный эффект; мы уже
|
||||
// доверяем внутреннему туннелю (WireGuard в UDP-режиме или smux+KCP в tcpfwd)
|
||||
// для обеспечения конфиденциальности и целостности end-to-end.
|
||||
//
|
||||
// Именно поэтому старый флаг `-no-dtls` / `Direct` удалён в V2-0: без DTLS
|
||||
// VK дропает пакеты и прокси фактически не работает в production. Нет
|
||||
// сценария, в котором отключение DTLS против живого VK-фильтра — верное решение.
|
||||
// сценария, в котором отключение DTLS против живого VK-фильтра - верное решение.
|
||||
//
|
||||
// Для будущих изменений: не трактуй DTLS-слой как security-critical
|
||||
// (самоподписанные сертификаты, send-only CID, отсутствие проверки peer-cert —
|
||||
// (самоподписанные сертификаты, send-only CID, отсутствие проверки peer-cert -
|
||||
// это намеренно, не упущения).
|
||||
package dtlsdial
|
||||
|
||||
@@ -19,7 +19,7 @@ func GenerateSelfSignedCert() (tls.Certificate, error) {
|
||||
|
||||
// Dialer конфигурирует DTLS-handshake клиента.
|
||||
type Dialer struct {
|
||||
// HandshakeTimeout ограничивает контекст handshake. Ноль — без таймаута.
|
||||
// HandshakeTimeout ограничивает контекст handshake. Ноль - без таймаута.
|
||||
HandshakeTimeout time.Duration
|
||||
// HandshakeSem, если non-nil, ограничивает параллельные handshake
|
||||
// (Dial блокируется до появления слота или отмены ctx).
|
||||
@@ -31,7 +31,7 @@ type Dialer struct {
|
||||
// Self-signed сертификат генерируется заново на каждый handshake: каждая
|
||||
// TURN-сессия и каждый реконнект получают уникальный fingerprint, чтобы не
|
||||
// коррелировать N параллельных стримов с одного IP как ботный трафик
|
||||
// (DTLS здесь — для обфускации, не аутентификации; см. doc.go).
|
||||
// (DTLS здесь - для обфускации, не аутентификации; см. doc.go).
|
||||
func (d *Dialer) Dial(ctx context.Context, pc net.PacketConn, peer *net.UDPAddr) (*dtls.Conn, error) {
|
||||
certificate, err := GenerateSelfSignedCert()
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakePC blocks forever on ReadFrom and accepts writes — enough to keep the
|
||||
// fakePC blocks forever on ReadFrom and accepts writes - enough to keep the
|
||||
// DTLS handshake stuck so we can exercise the gate/cancel paths.
|
||||
type fakePC struct{}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/xtaci/smux"
|
||||
)
|
||||
|
||||
// Profile — настраиваемые KCP-параметры. Обе стороны туннеля должны совпадать.
|
||||
// Profile - настраиваемые KCP-параметры. Обе стороны туннеля должны совпадать.
|
||||
type Profile struct {
|
||||
NoDelay int
|
||||
Interval int
|
||||
@@ -26,7 +26,7 @@ type FEC struct {
|
||||
Parity int
|
||||
}
|
||||
|
||||
// DefaultProfile — исторический balanced-профиль, поставляемый с прокси.
|
||||
// DefaultProfile - исторический balanced-профиль, поставляемый с прокси.
|
||||
func DefaultProfile() Profile {
|
||||
return Profile{
|
||||
NoDelay: 1,
|
||||
@@ -80,7 +80,7 @@ func (d *DtlsPacketConn) SetWriteDeadline(t time.Time) error {
|
||||
}
|
||||
|
||||
// NewKCPOverDTLS создаёт KCP-сессию поверх DTLS-соединения.
|
||||
// isServer: true — серверная сторона (listener), false — клиентская (dialer).
|
||||
// isServer: true - серверная сторона (listener), false - клиентская (dialer).
|
||||
func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool, profile Profile, fec FEC) (*kcp.UDPSession, error) {
|
||||
pc := NewDtlsPacketConn(dtlsConn)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ type permWatchLogger struct {
|
||||
fired bool
|
||||
}
|
||||
|
||||
// Маркеры — литералы в format pion, матчим format до подстановки args.
|
||||
// Маркеры - литералы в format pion, матчим format до подстановки args.
|
||||
func (l *permWatchLogger) note(msg string) {
|
||||
switch {
|
||||
case strings.Contains(msg, permFailMarker):
|
||||
|
||||
@@ -25,14 +25,14 @@ const (
|
||||
LaneAttachTimeout = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
// Hello — per-lane handshake header, отправляемый сразу после открытия smux-потока.
|
||||
// Hello - per-lane handshake header, отправляемый сразу после открытия smux-потока.
|
||||
type Hello struct {
|
||||
ConnID uint64
|
||||
LaneIndex uint16
|
||||
LaneCount uint16
|
||||
}
|
||||
|
||||
// Frame — одна bonded data или FIN единица, идентифицированная Seq внутри ConnID.
|
||||
// Frame - одна bonded data или FIN единица, идентифицированная Seq внутри ConnID.
|
||||
type Frame struct {
|
||||
Type byte
|
||||
Seq uint64
|
||||
|
||||
@@ -35,7 +35,7 @@ type Codec interface {
|
||||
MaxWire(payloadLen int) int
|
||||
}
|
||||
|
||||
// NewClientCodec строит клиентский Codec для профиля. profile none/"" → (nil, nil)
|
||||
// NewClientCodec строит клиентский Codec для профиля. profile none/"" -> (nil, nil)
|
||||
// (обфускация выключена). Длину ключа проверяет конструктор профиля.
|
||||
func NewClientCodec(profile string, key []byte) (Codec, error) {
|
||||
switch profile {
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestNewClientCodec(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// none/"" → обфускация выключена (nil codec, без ошибки).
|
||||
// none/"" -> обфускация выключена (nil codec, без ошибки).
|
||||
for _, p := range []string{ProfileNone, ""} {
|
||||
c, err := NewClientCodec(p, nil)
|
||||
if err != nil || c != nil {
|
||||
@@ -20,7 +20,7 @@ func TestNewClientCodec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// rtpopus / rtpopus2 → валидный codec с осмысленными размерами.
|
||||
// rtpopus / rtpopus2 -> валидный codec с осмысленными размерами.
|
||||
for _, p := range []string{ProfileRTPOpus, ProfileRTPOpus2} {
|
||||
c, err := NewClientCodec(p, key)
|
||||
if err != nil || c == nil {
|
||||
@@ -34,7 +34,7 @@ func TestNewClientCodec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// rtpopus2 несёт RTP extension → заголовок больше, чем у v1.
|
||||
// rtpopus2 несёт RTP extension -> заголовок больше, чем у v1.
|
||||
v1, _ := NewClientCodec(ProfileRTPOpus, key)
|
||||
v2, _ := NewClientCodec(ProfileRTPOpus2, key)
|
||||
if v2.HeaderLen() <= v1.HeaderLen() {
|
||||
|
||||
@@ -58,7 +58,7 @@ func (l *packetListener) Accept() (net.PacketConn, net.Addr, error) {
|
||||
func (l *packetListener) Close() error { return l.inner.Close() }
|
||||
func (l *packetListener) Addr() net.Addr { return l.inner.Addr() }
|
||||
|
||||
// packetConn — per-peer net.PacketConn, AEAD-оборачивающий чтение/запись
|
||||
// packetConn - per-peer net.PacketConn, AEAD-оборачивающий чтение/запись
|
||||
// через bufPool для allocation-free горячего пути.
|
||||
type packetConn struct {
|
||||
inner net.PacketConn
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package rtpopus реализует AEAD-фрейминг с мимикрией под RTP/opus (один
|
||||
// из планируемых wire-профилей обфускации в internal/wire/). Цель — обход
|
||||
// из планируемых wire-профилей обфускации в internal/wire/). Цель - обход
|
||||
// VK TURN content-filter.
|
||||
//
|
||||
// Назначение: обфускация, а не безопасность. DTLS уже обеспечивает
|
||||
// конфиденциальность и целостность внутреннего канала. Этот слой существует,
|
||||
// чтобы трафик выглядел как SRTP — VK content-filter его не дропает;
|
||||
// чтобы трафик выглядел как SRTP - VK content-filter его не дропает;
|
||||
// сам по себе не является защитой от активного противника.
|
||||
//
|
||||
// Wire-формат:
|
||||
@@ -25,7 +25,7 @@
|
||||
// совпадает с MSB SSRC (direction bit). counter стартует с random uint64.
|
||||
// AAD = первые 24 байта (RTP header || nonce).
|
||||
//
|
||||
// Wire-формат заморожен — требуется побитовая совместимость с задеплоенными пирами.
|
||||
// Wire-формат заморожен - требуется побитовая совместимость с задеплоенными пирами.
|
||||
package rtpopus
|
||||
|
||||
import (
|
||||
@@ -46,7 +46,7 @@ const (
|
||||
nonceLen = 12
|
||||
tagLen = 16
|
||||
headerLen = rtpHdrLen + nonceLen // 24
|
||||
// HeaderLen — offset, с которого начинается plaintext в wire-буфере.
|
||||
// HeaderLen - offset, с которого начинается plaintext в wire-буфере.
|
||||
// Экспонирован для in-place API (WrapInPlace/UnwrapInPlace): вызывающий
|
||||
// читает payload сразу в buf[HeaderLen:], избегая копии.
|
||||
HeaderLen = headerLen
|
||||
@@ -126,7 +126,7 @@ func NewConnFromState(state *State, isServer bool) (*Conn, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// HeaderLen, Overhead, MaxWire — методы под интерфейс wire.Codec; значения
|
||||
// HeaderLen, Overhead, MaxWire - методы под интерфейс wire.Codec; значения
|
||||
// совпадают с пакетными HeaderLen/Overhead/MaxWire.
|
||||
func (*Conn) HeaderLen() int { return headerLen }
|
||||
func (*Conn) Overhead() int { return Overhead }
|
||||
@@ -144,7 +144,7 @@ func (c *Conn) WrapInto(dst, payload []byte) (int, error) {
|
||||
|
||||
// WrapInPlace кодирует plaintext, который вызывающий уже разместил в
|
||||
// buf[HeaderLen:HeaderLen+plainLen], дописывая RTP-заголовок+nonce перед ним
|
||||
// и AEAD-tag после — без копии payload. buf должен вмещать MaxWire(plainLen).
|
||||
// и AEAD-tag после - без копии payload. buf должен вмещать MaxWire(plainLen).
|
||||
// Возвращает число записанных wire-байт.
|
||||
func (c *Conn) WrapInPlace(buf []byte, plainLen int) (int, error) {
|
||||
wireLen := Overhead + plainLen
|
||||
@@ -189,7 +189,7 @@ func (c *Conn) Unwrap(wire, dst []byte) (int, error) {
|
||||
}
|
||||
|
||||
// UnwrapInPlace декодирует wire-пакет на месте и возвращает subslice plaintext
|
||||
// внутри wire (без копии в отдельный буфер). AEAD открывает in-place — wire
|
||||
// внутри wire (без копии в отдельный буфер). AEAD открывает in-place - wire
|
||||
// после вызова считается потреблённым, результат валиден до следующей записи в wire.
|
||||
func (c *Conn) UnwrapInPlace(wire []byte) ([]byte, error) {
|
||||
if len(wire) < Overhead {
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestConnRoundTrip(t *testing.T) {
|
||||
t.Fatalf("client Unwrap: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dst[:m2], payload) {
|
||||
t.Fatalf("server→client round trip mismatch")
|
||||
t.Fatalf("server->client round trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +97,10 @@ func TestInPlaceRoundTrip(t *testing.T) {
|
||||
t.Fatalf("Unwrap: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dst[:m], payload) {
|
||||
t.Fatalf("WrapInPlace→Unwrap mismatch: got %q want %q", dst[:m], payload)
|
||||
t.Fatalf("WrapInPlace->Unwrap mismatch: got %q want %q", dst[:m], payload)
|
||||
}
|
||||
|
||||
// Сервер: WrapInto → клиент UnwrapInPlace (subslice внутрь wire, без копии).
|
||||
// Сервер: WrapInto -> клиент UnwrapInPlace (subslice внутрь wire, без копии).
|
||||
wire := make([]byte, MaxWire(len(payload)))
|
||||
wn, err := server.WrapInto(wire, payload)
|
||||
if err != nil {
|
||||
@@ -140,7 +140,7 @@ func TestRTPHeaderProgression(t *testing.T) {
|
||||
seq1 := binary.BigEndian.Uint16(wire1[2:4])
|
||||
seq2 := binary.BigEndian.Uint16(wire2[2:4])
|
||||
if seq2 != seq1+1 {
|
||||
t.Fatalf("seq did not increment: %d → %d", seq1, seq2)
|
||||
t.Fatalf("seq did not increment: %d -> %d", seq1, seq2)
|
||||
}
|
||||
|
||||
ts1 := binary.BigEndian.Uint32(wire1[4:8])
|
||||
|
||||
Reference in New Issue
Block a user