From ed6825976a3045bf427ea74532ad8604161a2700 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Tue, 19 May 2026 21:47:48 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B1=D0=B0=D0=B3=D0=B8,=20sentinel-?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8,=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Баги: - udprelay: errors.Is(DeadlineExceeded) вместо сравнения строк; короткий backoff в ветке lockout; обернуть DTLS-ошибку через %w - bondserver.copyBondToBackend: лимит pending-map в 1024 (паритет с bondclient) - srtpmimicry.RelayPacketConn: bufPool для горячего UDP-пути (паритет с listen.go) - vkauth.token.go: ручной солвер под ctx.WithTimeout(ctx, ...) вместо Background — нет утечки горутины при shutdown Гигиена: - sentinel-ошибки vkauth как var = errors.New(...); вызыватели используют errors.Is - убраны устаревшие комментарии этапов рефакторинга (Pion SPDX, "Package bond", internal/bond, удалённый файл заметок) - .gitignore: *.exe, *.so Тесты + lint проходят. --- .gitignore | 2 + cmd/vk-turn-client/main.go | 3 - internal/client/vkauth/client.go | 7 +- internal/client/vkauth/client_test.go | 7 +- internal/client/vkauth/token.go | 17 +- internal/client/vkauth/types.go | 17 +- internal/config/config.go | 6 +- internal/logx/logx.go | 9 +- internal/proxy/bondclient/bondclient.go | 2 +- internal/proxy/bondserver/bondserver.go | 11 +- internal/proxy/common/common.go | 8 +- internal/proxy/tcpfwd/pool.go | 4 +- internal/proxy/tcpfwd/tcpfwd.go | 3 +- internal/proxy/udprelay/udprelay.go | 31 +- internal/wire/bondframe/bondframe.go | 2 +- internal/wire/srtpmimicry/relay.go | 24 +- notes/AUDIT_2026-05-19_INDEPENDENT.md | 108 +++++++ notes/AUDIT_2026-05-19_V2.md | 390 ------------------------ 18 files changed, 203 insertions(+), 448 deletions(-) create mode 100644 notes/AUDIT_2026-05-19_INDEPENDENT.md delete mode 100644 notes/AUDIT_2026-05-19_V2.md diff --git a/.gitignore b/.gitignore index 7da2449..7dc305c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea .claude +*.exe +*.so diff --git a/cmd/vk-turn-client/main.go b/cmd/vk-turn-client/main.go index fd61f60..7acc296 100644 --- a/cmd/vk-turn-client/main.go +++ b/cmd/vk-turn-client/main.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2023 The Pion community -// SPDX-License-Identifier: MIT - package main import ( diff --git a/internal/client/vkauth/client.go b/internal/client/vkauth/client.go index f4fdebf..d21d1d6 100644 --- a/internal/client/vkauth/client.go +++ b/internal/client/vkauth/client.go @@ -2,6 +2,7 @@ package vkauth import ( "context" + "errors" "fmt" "log" "math/rand" @@ -211,7 +212,7 @@ func (c *Client) fetchSerialized(ctx context.Context, link string, streamID int) // fetch loops over c.credentials, returning on first success or terminal error. func (c *Client) fetch(ctx context.Context, link string, streamID int) (string, string, []string, error) { if time.Now().Unix() < c.lockout.Load() { - return "", "", nil, fmt.Errorf("CAPTCHA_WAIT_REQUIRED: global lockout active") + return "", "", nil, fmt.Errorf("%w: %w", ErrCaptchaWaitRequired, ErrLockoutActive) } var lastErr error @@ -227,10 +228,10 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string, lastErr = err log.Printf("[STREAM %d] [VK Auth] Failed with client_id=%s: %v", streamID, creds.ClientID, err) - es := err.Error() - if strings.Contains(es, ErrCaptchaWaitRequired) || strings.Contains(es, "FATAL_CAPTCHA") { + if errors.Is(err, ErrCaptchaWaitRequired) || errors.Is(err, ErrFatalCaptchaNoStreams) { return "", "", nil, err } + es := err.Error() if strings.Contains(es, "error_code:29") || strings.Contains(es, "error_code: 29") || strings.Contains(es, "Rate limit") { log.Printf("[STREAM %d] [VK Auth] Rate limit detected, trying next credentials...", streamID) } diff --git a/internal/client/vkauth/client_test.go b/internal/client/vkauth/client_test.go index 37d9345..29aadd6 100644 --- a/internal/client/vkauth/client_test.go +++ b/internal/client/vkauth/client_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "sync/atomic" "testing" "time" @@ -124,11 +123,11 @@ func TestFetchShortCircuitsOnCaptchaWait(t *testing.T) { var calls atomic.Int32 c := newTestClient(t, func(_ context.Context, _ string, _ int, _ VKCredentials, _ tlsclient.CookieJar) (string, string, []string, error) { calls.Add(1) - return "", "", nil, errors.New(ErrCaptchaWaitRequired) + return "", "", nil, ErrCaptchaWaitRequired }) _, _, _, err := c.GetCredentials(context.Background(), "L", 0) - if err == nil || !strings.Contains(err.Error(), ErrCaptchaWaitRequired) { + if err == nil || !errors.Is(err, ErrCaptchaWaitRequired) { t.Fatalf("expected CAPTCHA_WAIT_REQUIRED, got %v", err) } if calls.Load() != 1 { @@ -184,7 +183,7 @@ func TestLockoutBlocksFetch(t *testing.T) { } _, _, _, err := c.GetCredentials(context.Background(), "L", 0) - if err == nil || !strings.Contains(err.Error(), "global lockout active") { + if err == nil || !errors.Is(err, ErrLockoutActive) { t.Fatalf("expected lockout error, got %v", err) } } diff --git a/internal/client/vkauth/token.go b/internal/client/vkauth/token.go index f2a1372..fc99aae 100644 --- a/internal/client/vkauth/token.go +++ b/internal/client/vkauth/token.go @@ -135,9 +135,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c c.engageLockout(60 * time.Second) if c.streamsFn() == 0 { log.Printf("[STREAM %d] [FATAL] 0 connected streams and captcha solve modes exhausted.", streamID) - return "", "", nil, errors.New(ErrFatalCaptchaNoStreams) + return "", "", nil, ErrFatalCaptchaNoStreams } - return "", "", nil, errors.New(ErrCaptchaWaitRequired) + return "", "", nil, ErrCaptchaWaitRequired } var successToken string @@ -164,8 +164,11 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c break } log.Printf("[STREAM %d] [Captcha] Triggering manual captcha fallback...", streamID) - // Background ctx so a short parent deadline doesn't cut user solve time. - manualCtx, manualCancel := context.WithTimeout(context.Background(), 3*time.Minute) + // Manual solver gets its own 3-min budget so a tight parent + // deadline doesn't cut user solve time. We still propagate + // parent cancellation (app shutdown) so the in-flight solver + // goroutine doesn't outlive the process. + manualCtx, manualCancel := context.WithTimeout(ctx, 3*time.Minute) type manualRes struct { token string @@ -193,7 +196,7 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c log.Printf("[STREAM %d] [Captcha] manual solver returned error: %v", streamID, solveErr) } case <-manualCtx.Done(): - if manualCtx.Err() == context.DeadlineExceeded { + if errors.Is(manualCtx.Err(), context.DeadlineExceeded) { solveErr = fmt.Errorf("manual captcha timed out after 3m") } else { solveErr = fmt.Errorf("manual captcha interrupted: %w", manualCtx.Err()) @@ -212,9 +215,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c c.engageLockout(60 * time.Second) if c.streamsFn() == 0 { log.Printf("[STREAM %d] [FATAL] 0 connected streams and manual captcha failed/timed out.", streamID) - return "", "", nil, errors.New(ErrFatalCaptchaNoStreams) + return "", "", nil, ErrFatalCaptchaNoStreams } - return "", "", nil, errors.New(ErrCaptchaWaitRequired) + return "", "", nil, ErrCaptchaWaitRequired } if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" { diff --git a/internal/client/vkauth/types.go b/internal/client/vkauth/types.go index 4c31dfe..addc769 100644 --- a/internal/client/vkauth/types.go +++ b/internal/client/vkauth/types.go @@ -1,6 +1,9 @@ package vkauth -import "time" +import ( + "errors" + "time" +) // VKCredentials is one app_id/app_secret pair used to obtain anonymous tokens. type VKCredentials struct { @@ -35,10 +38,10 @@ const ( DefaultStreamsPerCache = 10 ) -// Sentinel error message fragments produced by the auth flow. Consumers match -// on substring to preserve wire compatibility with the existing caller logic. -const ( - ErrCaptchaWaitRequired = "CAPTCHA_WAIT_REQUIRED" - ErrFatalCaptchaNoStreams = "FATAL_CAPTCHA_FAILED_NO_STREAMS" - ErrLockoutActiveSuffix = "global lockout active" +// Sentinel errors produced by the auth flow. Callers use errors.Is. +// The string forms remain stable for log output. +var ( + ErrCaptchaWaitRequired = errors.New("CAPTCHA_WAIT_REQUIRED") + ErrFatalCaptchaNoStreams = errors.New("FATAL_CAPTCHA_FAILED_NO_STREAMS") + ErrLockoutActive = errors.New("global lockout active") ) diff --git a/internal/config/config.go b/internal/config/config.go index a125e3f..88dd6dd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,10 +4,8 @@ // wrap key, but do not touch the network, DNS, or process state. main() is // responsible for wiring those side effects after Parse* returns. // -// V2-7: options are grouped by domain (TURN, Obf, Proxy, VK, DNS, Log) so the -// struct shape mirrors the conceptual layers of the proxy instead of being a -// flat bag of bools. CLI flag names, defaults, and behavior are bit-exact -// compatible with the pre-V2-7 surface. +// Options are grouped by domain (TURN, Obf, Proxy, VK, DNS, Log) so the struct +// shape mirrors the conceptual layers of the proxy. package config import ( diff --git a/internal/logx/logx.go b/internal/logx/logx.go index 0f1088f..88894d7 100644 --- a/internal/logx/logx.go +++ b/internal/logx/logx.go @@ -1,7 +1,6 @@ -// Package logx is a minimal leveled logger over stdlib log. It replaces the -// per-package Deps{Debug bool, Debugf func} pair: callers receive a Logger and -// call Debugf/Infof/Warnf/Errorf directly. Output format is bit-compatible with -// the previous `log.Printf`-gated-by-isDebug pattern. +// Package logx is a minimal leveled logger over stdlib log. Callers receive a +// Logger and call Debugf/Infof/Warnf/Errorf directly; Debugf is gated by a +// debug flag, the other levels always print via stdlib log. package logx import "log" @@ -24,7 +23,7 @@ type stdLogger struct { } // New returns a Logger that prints via stdlib log. If debug is false, Debugf is -// a no-op (matches the legacy `if isDebug { log.Printf(...) }` shape). +// a no-op. func New(debug bool) Logger { return &stdLogger{debug: debug} } diff --git a/internal/proxy/bondclient/bondclient.go b/internal/proxy/bondclient/bondclient.go index c95f55b..5ed32c4 100644 --- a/internal/proxy/bondclient/bondclient.go +++ b/internal/proxy/bondclient/bondclient.go @@ -1,7 +1,7 @@ // Package bondclient implements the client side of the bonded VLESS lane: // a single accepted TCP connection striped (round-robin) across all currently // live smux sessions in a tcpfwd.SessionPool. Frame wire-format lives in -// internal/bond; this package wires the local TCP <-> lanes copy loops. +// internal/wire/bondframe; this package wires the local TCP <-> lanes copy loops. package bondclient import ( diff --git a/internal/proxy/bondserver/bondserver.go b/internal/proxy/bondserver/bondserver.go index b01ba91..f41edd0 100644 --- a/internal/proxy/bondserver/bondserver.go +++ b/internal/proxy/bondserver/bondserver.go @@ -1,7 +1,7 @@ // Package bondserver implements the server side of the bonded VLESS lane: // a single backend TCP connection multiplexed across N smux streams that all -// share a ConnID. Frame wire-format lives in internal/bond; this package wires -// the backend TCP <-> lanes copy loops and the per-ConnID registry. +// share a ConnID. Frame wire-format lives in internal/wire/bondframe; this package +// wires the backend TCP <-> lanes copy loops and the per-ConnID registry. package bondserver import ( @@ -283,6 +283,13 @@ func (c *serverConn) copyBondToBackend(backendConn net.Conn) { case f := <-c.recvCh: switch f.Type { case bondframe.FrameData: + // Bound the reorder buffer so a malicious or buggy peer + // that emits seq with permanent gaps cannot exhaust memory. + // Matches bondclient.copyBondToTCP. + if len(pending) >= 1024 { + log.Printf("[bond %d] pending map overflow (>1024), closing", c.id) + return + } pending[f.Seq] = f.Data case bondframe.FrameFIN: v := f.Seq diff --git a/internal/proxy/common/common.go b/internal/proxy/common/common.go index 89d0e11..21aa19a 100644 --- a/internal/proxy/common/common.go +++ b/internal/proxy/common/common.go @@ -1,7 +1,7 @@ -// Package common holds small helpers shared between udprelay and tcpfwd. The -// two proxy modes layer DTLS and srtpmimicry differently (see V2-6 decision -// in notes/AUDIT_2026-05-19_V2.md), so a full Engine/Handler abstraction was -// rejected. This package only collects the bits that are genuinely identical. +// Package common holds the few helpers shared by udprelay and tcpfwd +// (TURN dial + wrap codec construction). The two proxy modes layer DTLS and +// srtpmimicry differently, so a full Engine/Handler abstraction is intentionally +// avoided — this package only collects what is genuinely identical. package common import ( diff --git a/internal/proxy/tcpfwd/pool.go b/internal/proxy/tcpfwd/pool.go index 56291c3..bb81c1b 100644 --- a/internal/proxy/tcpfwd/pool.go +++ b/internal/proxy/tcpfwd/pool.go @@ -2,8 +2,8 @@ // smux sessions. Each accepted local TCP connection is opened as a smux stream // (round-robin across sessions) or, with bond, striped across all live sessions. // -// The package exposes SessionPool/PooledSession so that the bond client half -// (still living in main during refactor stage 4.2) can operate on them. +// SessionPool/PooledSession are exported so the bond client (internal/proxy/bondclient) +// can stripe a single TCP connection across multiple live sessions. package tcpfwd import ( diff --git a/internal/proxy/tcpfwd/tcpfwd.go b/internal/proxy/tcpfwd/tcpfwd.go index af874cd..ada0361 100644 --- a/internal/proxy/tcpfwd/tcpfwd.go +++ b/internal/proxy/tcpfwd/tcpfwd.go @@ -35,8 +35,7 @@ type Params struct { // BondHandler stripes one accepted TCP connection across all currently-live // pool sessions. Nil disables bond mode (callers will then use round-robin). -// Bond client lives in main during stage 4.2; it will move to internal/bond/client -// in stage 5.1. +// The bond client implementation lives in internal/proxy/bondclient. type BondHandler func(ctx context.Context, tcpConn net.Conn, connID uint64, lanes []*PooledSession) // Deps groups host-process dependencies needed by the VLESS loop. diff --git a/internal/proxy/udprelay/udprelay.go b/internal/proxy/udprelay/udprelay.go index ef97537..cb7ae07 100644 --- a/internal/proxy/udprelay/udprelay.go +++ b/internal/proxy/udprelay/udprelay.go @@ -7,15 +7,16 @@ package udprelay import ( "context" + "errors" "fmt" "log" "math/rand" "net" - "strings" "sync" "sync/atomic" "time" + "github.com/cacggghp/vk-turn-proxy/internal/client/vkauth" "github.com/cacggghp/vk-turn-proxy/internal/logx" "github.com/cacggghp/vk-turn-proxy/internal/proxy/common" "github.com/cacggghp/vk-turn-proxy/internal/stats" @@ -50,8 +51,7 @@ type AuthHandler interface { LockoutUntilUnix() int64 } -// Params is the per-stream TURN/wrap configuration shared by the DTLS and TURN -// loops. Equivalent of the old client/main.go turnParams. +// Params is the per-stream TURN/wrap configuration shared by the DTLS and TURN loops. type Params struct { Host string Port string @@ -92,10 +92,18 @@ func DTLSLoop(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net return default: err := oneDTLS(ctx, deps, peer, listenConn, inboundChan, connchan, okchan, streamID) - if err != nil { - if time.Now().Unix() < deps.Auth.LockoutUntilUnix() && strings.Contains(err.Error(), "context deadline exceeded") { - continue + // During captcha lockout the handshake deadline fires before + // auth retries can succeed; back off briefly to avoid a tight + // retry spin until the lockout clears. + if err != nil && time.Now().Unix() < deps.Auth.LockoutUntilUnix() && errors.Is(err, context.DeadlineExceeded) { + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(1+rand.Intn(2)) * time.Second): } + continue + } + if err != nil { select { case <-ctx.Done(): return @@ -124,15 +132,15 @@ func TURNLoop(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr go oneTURN(ctx, deps, params, peer, conn2, streamID, c) if err := <-c; err != nil { - if strings.Contains(err.Error(), "FATAL_CAPTCHA") { + if errors.Is(err, vkauth.ErrFatalCaptchaNoStreams) { log.Printf("[STREAM %d] Fatal manual captcha error. Shutting down application.", streamID) if deps.AppCancel != nil { deps.AppCancel() } return } - if strings.Contains(err.Error(), "CAPTCHA_WAIT_REQUIRED") { - if !strings.Contains(err.Error(), "global lockout active") { + if errors.Is(err, vkauth.ErrCaptchaWaitRequired) { + if !errors.Is(err, vkauth.ErrLockoutActive) { log.Printf("[STREAM %d] Backing off for 60 seconds to avoid IP ban...", streamID) select { case <-ctx.Done(): @@ -167,6 +175,9 @@ func oneDTLS(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net. defer dtlscancel() conn1, conn2 := connutil.AsyncPacketPipe() + // TURNLoop may restart oneTURN several times within a single DTLS lifetime, + // re-reading conn2 on each restart; keep publishing until the DTLS attempt + // itself ends. go func() { for { select { @@ -178,7 +189,7 @@ func oneDTLS(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net. }() dtlsRaw, err1 := deps.DTLSDialer.Dial(dtlsctx, conn1, peer) if err1 != nil { - return fmt.Errorf("failed to connect DTLS: %s", err1) + return fmt.Errorf("failed to connect DTLS: %w", err1) } var dtlsConn net.Conn = dtlsRaw defer func() { diff --git a/internal/wire/bondframe/bondframe.go b/internal/wire/bondframe/bondframe.go index ab4e4af..8dd7222 100644 --- a/internal/wire/bondframe/bondframe.go +++ b/internal/wire/bondframe/bondframe.go @@ -1,4 +1,4 @@ -// Package bond contains the wire format used by the VLESS bond multi-lane +// Package bondframe contains the wire format used by the VLESS bond multi-lane // transport (hello + framed data/FIN). Both the client (initiator) and the // server (acceptor) speak the same encoding. package bondframe diff --git a/internal/wire/srtpmimicry/relay.go b/internal/wire/srtpmimicry/relay.go index 6e0a56d..00dc453 100644 --- a/internal/wire/srtpmimicry/relay.go +++ b/internal/wire/srtpmimicry/relay.go @@ -19,8 +19,16 @@ func (r *RelayPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { if r.Conn == nil { return r.Relay.ReadFrom(b) } - buf := make([]byte, MaxWire(len(b))) - n, addr, err := r.Relay.ReadFrom(buf) + bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte + buf := *bp + need := MaxWire(len(b)) + if cap(buf) < need { + buf = make([]byte, need) + *bp = buf + } + defer bufPool.Put(bp) + + n, addr, err := r.Relay.ReadFrom(buf[:cap(buf)]) if err != nil { return 0, addr, err } @@ -35,7 +43,17 @@ func (r *RelayPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) { if r.Conn == nil { return r.Relay.WriteTo(b, r.Peer) } - out := make([]byte, MaxWire(len(b))) + wireLen := MaxWire(len(b)) + + bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte + out := *bp + if cap(out) < wireLen { + out = make([]byte, wireLen) + *bp = out + } + out = out[:wireLen] + defer bufPool.Put(bp) + n, err := r.Conn.WrapInto(out, b) if err != nil { return 0, err diff --git a/notes/AUDIT_2026-05-19_INDEPENDENT.md b/notes/AUDIT_2026-05-19_INDEPENDENT.md new file mode 100644 index 0000000..f578b40 --- /dev/null +++ b/notes/AUDIT_2026-05-19_INDEPENDENT.md @@ -0,0 +1,108 @@ +# Независимый аудит — 2026-05-19 + +Аудит проведён без опоры на предыдущие AUDIT_* и без учёта обратной совместимости. +Билд чистый, `go vet` / `golangci-lint run` без замечаний. Дефекты ниже не ловятся +текущими тулзами. + +## 1. Архитектура + +- **`cmd/vk-turn-server/main.go` 396 LoC**: `handleUDPConnection`, `handleVLESSConnection`, + `pipeConn`, `prefixedConn` — бизнес-логика. Должны переехать в `internal/proxy/...`. + Клиент симметрично уже сделан (`tcpfwd.Run`), сервер отстал. +- **Глобалы в server main** (`cmd/vk-turn-server/main.go:27-30`): `logger`, + `globalBondRegistry`. Mutable package-vars вместо передачи через стек. +- **Asymmetry клиента**: UDP-режим разворачивается inline в + `cmd/vk-turn-client/main.go:157-261`. Должен быть `udprelay.Run(...)` симметрично + `tcpfwd.Run(...)`. +- **Лишние `Deps`-структуры**: bondserver/bondclient/udprelay/tcpfwd. Большая часть + содержит только Logger + 1-2 поля. Три разных стиля инициализации. +- **Captcha-пакет монолит**: `manual.go` 892 / `slider.go` 627 / `captcha.go` 579 — + смешаны HTTP-flow, parsing, PoW-mining, fingerprinting. +- **`internal/proxy/common`** существует ради 2 функций (`DialTURN`, `NewClientWrap`). + Слить в `turndial`. + +## 2. Корректность / баги + +- **`internal/proxy/udprelay/udprelay.go:96`** — `strings.Contains(err.Error(), "context deadline exceeded")`. + Match по строке стандартной ошибки. Должно быть `errors.Is(err, context.DeadlineExceeded)`. + Tight-loop risk: ветка `continue` без backoff если oneDTLS быстро возвращает DeadlineExceeded. +- **`internal/proxy/udprelay/udprelay.go:170-178`** — бесконечная публикация одного + и того же `conn2` в `connchan`. TURNLoop читает один раз. Должно быть одноразово. +- **`internal/proxy/bondserver/bondserver.go:269`** — `pending` map без лимита, DoS-risk + при дырах в seq. У клиента лимит 1024 есть (`bondclient.go:240`). +- **`internal/client/vkauth/token.go:168-202`** — `context.WithTimeout(context.Background(), 3m)` + для manual captcha. При отмене parent ctx goroutine `c.manualSolve(...)` живёт 3 мин. + Goroutine-leak на shutdown. +- **`internal/client/captcha/captcha.go:466`** — `solveCaptchaPoW` ctx-check каждые 4096 + итераций. Cancel может опоздать на сотни мс. +- **`internal/wire/srtpmimicry/relay.go:22,38`** — `make([]byte, MaxWire(...))` на каждый + ReadFrom/WriteTo. На горячем пути UDP — GC давление. В `listen.go` есть `bufPool`. +- **`internal/client/captcha/captcha.go:47`** — `captchaDebugCache sync.Map` без TTL/eviction. +- **`internal/client/vkauth/token.go:36`** — `tlsclient.NewHttpClient` пересоздаётся + внутри `getTokenChain` на каждую попытку. +- **`cmd/vk-turn-server/main.go:209,219,240,250`** — magic `time.Minute*30` для + read/write deadline. Bidir UDP-relay с 30-минутным dead-timeout повисает при простое. +- **`internal/client/vkauth/client.go:213-238`** — sentinel-errors через + `strings.Contains(err.Error(), "...")`. Хрупко. + +## 3. Stale-комментарии (refactor-history протёк в godoc) + +- `internal/proxy/tcpfwd/tcpfwd.go:38-39` — "Bond client lives in main during stage 4.2; + it will move to internal/bond/client in stage 5.1". Уже мигрировано. +- `internal/proxy/tcpfwd/pool.go:6` — "bond client half (still living in main during + refactor stage 4.2)". Ложь. +- `internal/proxy/common/common.go:2-4` — ссылка на `notes/AUDIT_2026-05-19_V2.md`, + файл удалён. +- `internal/config/config.go:7-10` — "V2-7: options are grouped by domain...". + Стадия рефакторинга в публичном godoc. +- `internal/proxy/udprelay/udprelay.go:54-55` — "Equivalent of the old + client/main.go turnParams". Историческая ссылка. +- `cmd/vk-turn-client/main.go:1-2` — `SPDX-FileCopyrightText: 2023 The Pion community` + на основном proxy main. Ложный copyright. +- `internal/logx/logx.go:1-4` — описывает что заменило, а не что есть. +- `internal/proxy/bondclient/bondclient.go:3-4` — "Frame wire-format lives in internal/bond" + → должно быть `internal/wire/bondframe`. + +## 4. Нейминг + +- `internal/wire/bondframe/bondframe.go:1` — godoc `// Package bond ...`, пакет `bondframe`. +- `srtpmimicry.Conn` — AEAD-кодек, не `net.Conn`. Лучше `Cipher`/`Codec`. +- `kcptun.DtlsPacketConn` / `NewDtlsPacketConn` — экспортированы, используются только внутри. +- `udprelay.Pool` — глобальная `sync.Pool` экспортирована, никто извне не дёргает. +- `bondclient.lane.dead atomic.Bool` vs `bondserver` slice-search remove — разные стили. +- `turndial.Stream` vs `tcpfwd.PooledSession` — концептуальное пересечение. +- `connchan` / `okchan` / `cchan` в client/main.go — нечитаемо. +- Magic numbers: `2000` (inboundChan), `1024` (recvCh), 4 MiB vs 16 KiB чтение/запись фрейма. + +## 5. Прочее + +- 134 прямых `log.Printf` в 13 файлах при наличии `logx.Logger`. Mix — нельзя + глобально перенаправить вывод. +- `selfsign.GenerateSelfSigned()` на каждый `dtlsdial.Dial`. Кэшируй. +- Hardcoded VK secrets `internal/client/vkauth/types.go:21-27` (известно). +- Нет тестов: `bondclient`, `tcpfwd`, `udprelay`, `captcha/captcha.go` (auto-solver), + `captcha/slider.go`. Самые гоночные компоненты без тестов. +- Артефакты в репо: `client.exe`, `server.exe`, `libvkturn.so` — в `.gitignore` нет. + +## Приоритеты исправлений + +1. **Bugs (P0):** + 1.1. `udprelay`: `errors.Is(DeadlineExceeded)` вместо string-match + одноразовый conn2 + backoff. + 1.2. `bondserver`: cap `pending`. + 1.3. `srtpmimicry.RelayPacketConn`: bufPool как в listen.go. + 1.4. Goroutine-leak `manualSolve` (на shutdown). + +2. **Stale-комменты / нейминг (P1):** + 2.1. Убрать stage-4.2 / V2-* / notes/AUDIT_2026-05-19_V2.md / Pion-copyright / "Package bond". + 2.2. Stale ссылка `internal/bond` → `internal/wire/bondframe`. + +3. **Sentinel errors (P1):** `var ErrXxx = errors.New(...)` + `errors.Is`. + +4. **Server-main → internal (P2):** вынести handle*Connection / pipeConn / prefixedConn, + убрать глобалы. + +5. **`udprelay.Run(...)` симметрично tcpfwd.Run (P2).** + +6. **Logger unification (P2):** заменить `log.Printf` на `logx.Logger`. + +7. **`.gitignore`:** `*.exe`, `*.so`. diff --git a/notes/AUDIT_2026-05-19_V2.md b/notes/AUDIT_2026-05-19_V2.md deleted file mode 100644 index a69ea33..0000000 --- a/notes/AUDIT_2026-05-19_V2.md +++ /dev/null @@ -1,390 +0,0 @@ -# Архитектурный аудит V2 — vk-turn-proxy - -Дата: 2026-05-19. -Контекст: после закрытия этапов 1–6 первого `REFACTOR_PLAN.md` итог не устроил заказчика. Этот документ — независимый повторный аудит (структура + предметная область) и план финализации. - -Предыдущий план: `notes/REFACTOR_PLAN.md`. Не отменяется — закрытые этапы остаются как есть, V2 строится поверх. - ---- - -## 1. Снимок текущего состояния - -``` -client/ - main.go (272) # package main: wiring + isDebug + debugf - manual_captcha.go (875) # package main: HTTP-сервер, browser launch, gzip proxy - manual_captcha_test.go - internal/ - browserprofile/ captcha/ dnsdial/ namegen/ vkauth/ -server/ - main.go (402) # package main: wiring + globalBondRegistry + debugf -internal/ - bond/{frame.go,client/,server/} - config/ dtlsdial/ ish/ netadapt/ proxy/{udp,vless} - stats/ turnpipe/ wrap/ -tcputil/ - tcputil.go # KCP/smux профили, top-level -go.mod -notes/ README.md Dockerfile routes-*.{sh,ps1} -``` - -CLI флаги (текущие): -- client: `-turn -port -listen -vk-link -peer -n -udp -no-dtls -vless -vless-bond -wrap -wrap-key -gen-wrap-key -streams-per-cred -debug -manual-captcha -dns -dns-servers` -- server: `-listen -connect -vless -vless-bond -wrap -wrap-key -gen-wrap-key -debug` - ---- - -## 2. Структурные болячки - -### 2.1. Нет `cmd/` -Бинари в `client/` и `server/` рядом с бизнес-кодом. Канон Go: `cmd//main.go` = wiring only. Сейчас `client/` одновременно: -- путь к бинарю; -- родитель `client/internal/`; -- содержит 875-строчный `manual_captcha.go` в `package main`. - -### 2.2. `manual_captcha.go` в `package main` -875 строк (больше самого `main.go`): HTTP-сервер на 8765, запуск браузера, gzip-rewrite VK-страниц, регекс-патчинг, capability detection. Тесты в `package main` — изолированно не тестируется. Должен быть отдельный пакет. - -### 2.3. Двойная `internal/` иерархия -`client/internal/` vs `internal/`. Правило выбора **де-факто**: «если общее — наверх, если клиент-only — вниз». Сломалось на `internal/ish` — он клиент-only, но потребитель (vless) живёт в `internal/`, поэтому ish вытолкнут наверх. То есть правило перевёрнуто: «куда импортируется из общего, то наверх». Это симптом, не дизайн. - -Раздробление: -- `client/internal/vkauth, captcha, browserprofile, dnsdial, namegen` — клиент-only. -- `internal/proxy/{udp,vless}` — по смыслу клиент-only, но в общем. -- `internal/ish` — клиент-only, но в общем (ради vless). -- `internal/bond/{frame,client,server}` — frame общий, client клиент-only, server сервер-only — три разных аудита смешаны. - -### 2.4. `tcputil` top-level -Не `cmd/`, не `internal/`. Side-quest (переименовать в `kcptun`) не закрыт. Имя врёт: внутри KCP/smux профили, никакого TCP. - -### 2.5. Side-quests не закрыты (из старого плана) -- `tcputil` → `kcptun` — не сделан. -- `selectedKCPProfile()` `os.Getenv` в hot path → `sync.Once` — не сделан. -- `isDebug` глобал → logger interface — не сделан. -- `globalAppCancel` убран (4.1), но `globalBondRegistry` остался (5.2). - -### 2.6. Логирование — три способа -- `log.Printf` (stdlib). -- `debugf` (gated глобал в обоих main). -- `Deps{Debug bool, Debugf func}` callback в каждом пакете. - -Один `internal/logx.Logger` решает. - -### 2.7. `globalBondRegistry` инициализация на package init -`server/main.go:155`: -```go -var globalBondRegistry = bondserver.NewRegistry(bondserver.Deps{Debug: isDebug, Debugf: debugf}) -``` -Здесь `isDebug == false` (main ещё не выполнен). Работает только потому что bondserver **не читает** поле `Debug`, использует только `Debugf` callback. Bug-by-design, который сейчас не стреляет. Стрельнёт когда кто-то прочтёт `Debug`. - -### 2.8. `proxy/udp` и `proxy/vless` — 70% общего кода -Оба: -- открывают N TURN-стримов через `turnpipe`; -- крутят DTLS через `dtlsdial`; -- ходят за VK creds (`vkauth.Client`); -- держат `activeLocalPeer` / `connectedStreams`. - -Различие: что копировать поверх DTLS. Это **handler**, не отдельный mode. Нет общего `proxy.Engine` + handler-интерфейса. - ---- - -## 3. Путаница в предметной области - -### 3.1. Имена врут - -| Имя | Что говорит | Что делает | -|-----|-------------|------------| -| `-wrap` (CLI help) | «ChaCha20-XOR obfuscate» | ChaCha20-Poly1305 **AEAD** | -| `wrap` (пакет doc) | «SRTP-mimicry AEAD» | то же | -| `wrap` (Go-термин) | `errors.Wrap`, `net.Wrap*` | здесь — VK-bypass обфускация | -| `tcputil` | TCP utils | KCP/smux профили | -| `turnpipe` | data pipe | TURN dial+allocate setup | -| `pipeConn` (server) | то же слово | **bidi copy loop** — другое! | -| `netadapt` | adapter | conn wrappers | -| `-no-dtls` ↔ `Direct` | один флаг | два имени между CLI и Go | -| `-turn` → `Host` | «TURN server ip» | принимает host (не только IP) | -| `-connect` (server) | error: «server address» | реально backend address | - -### 3.2. Перегруженные термины - -**«Client» — 4 сущности:** -- `cmd/client` бинарь -- `vkauth.Client` HTTP-клиент к VK -- `bondclient.Handler` наша bond-сторона -- `turn.NewClient` TURN-protocol-клиент - -**«Bond» — feature или подсистема?** Layout `internal/bond/{frame,client,server}` делает его равным `wrap`. Реально bond — **только опция vless-режима**. Иерархия преувеличивает значимость. - -**«Stream» — 3 значения:** -- TURN stream (одна из N TURN-connections) -- smux stream (внутри одной DTLS-сессии) -- VK credential stream / cache (одни creds на группу TURN-streams) - -`-streams-per-cred` → `streamsPerCache` в коде. Грепаешь — путаешься. - -**«Mode» — 5 ортогональных переключателей**, нигде не сгруппированы: -- `-udp`: TURN transport (UDP|TCP к TURN). -- `-vless`: app proxy (UDP-relay|TCP-forward). -- `-vless-bond`: внутри TCP-forward. -- `-wrap`: obfuscation (off|SRTP-mimicry). -- `-no-dtls`: DTLS off (DO NOT USE). -- `-dns`: udp|doh|auto. - -Плоские bool в `config.Client` без группировки. - -### 3.3. Концептуальные дыры - -**a) DTLS = obfuscation, не security.** Доки, переменные, контекст говорят про DTLS как защиту. Реально DTLS нужен чтобы VK content-filter увидел legit-handshake — иначе drop. `-no-dtls` помечен «DO NOT USE» именно потому что не работает в проде. Это **нигде не зафиксировано**. - -**b) Captcha разбит на 3 места:** -- `client/internal/captcha/` — auto slider-solver. -- `client/manual_captcha.go` (875 строк, `package main`) — manual flow (browser+http+gzip). -- `vkauth.AutoSolveFunc`/`ManualSolveFunc` — DI-точки. - -Один домен «как пройти VK капчу», три места. - -**c) `ish` package** — iOS sandbox shim. Имя расшифровывается только если знаешь iSH (Linux-под-iOS на iPhone). Без header-doc — мистика. - -**d) Auto-detect bond на сервере** (`bond-autodetect=true` на старте) делает `server.-vless-bond` флаг **частично избыточным**. Документация не объясняет когда он нужен. - -### 3.4. TCP/UDP терминология (специально вынесено) - -**Два разных слоя:** -1. **TURN transport** — флаг `-udp`. Default `false` → TCP/TLS к TURN. True → UDP к TURN. -2. **App proxy mode** — флаг `-vless`. Default `false` → UDP packet relay. True → TCP forwarder (внутри VLESS-генерик). - -«TCP-режим» в проекте **есть** — это `vless`. Имя историческое (первый потребитель — Xray VLESS). Внутри ничего VLESS-специфичного: tcp-байты в smux-стрим, на сервере `net.Dial("tcp", backend)`. Честное имя пакета: `tcp` или `tcpfwd`. - ---- - -## 4. Корневая причина - -Проект рос **по фичам, не по доменам**. Каждая новая способность (wrap, bond, vless, scan) приклеивалась соседней сущностью вместо встраивания в иерархию. Итог: -- термины перегружены (client/bond/mode/stream); -- CLI ≠ Go-имена; -- DTLS-as-obfuscation нигде не объяснён; -- captcha и proxy раздроблены без причины; -- два режима прокси не имеют общего родителя. - -Первый рефакторинг (1–6) **вытаскивал** код из main, но не **перестраивал** домен. Сейчас main маленький, но домен такой же запутанный. - ---- - -## 5. Целевая архитектура - -``` -cmd/ - vk-turn-client/main.go # wiring only, ~150 строк - vk-turn-server/main.go # wiring only, ~100 строк - -internal/ - logx/ # slog-обёртка, заменяет isDebug+debugf - config/ # Parse*; группированные опции - ctxutil/ # closeWrite, pipe (data-copy) - - wire/ - srtpmimicry/ # бывш. internal/wrap - bondframe/ # бывш. internal/bond/frame.go - - transport/ - turndial/ # бывш. internal/turnpipe - dtlsdial/ # есть - kcptun/ # бывш. tcputil; sync.Once для профиля - - proxy/ - engine/ # общая часть (TURN×N + DTLS + creds + counters) - udprelay/ # бывш. proxy/udp; Handler - tcpfwd/ # бывш. proxy/vless; Handler - bondclient/ # бывш. internal/bond/client - bondserver/ # бывш. internal/bond/server - - netconn/ # бывш. netadapt - - client/ # КЛИЕНТ-ONLY (схлопывает client/internal) - vkauth/ # бывш. client/internal/vkauth - captcha/ - auto/ # бывш. client/internal/captcha - manual/ # бывш. client/manual_captcha.go (НОВЫЙ ПАКЕТ) - browserprofile/ # бывш. client/internal/browserprofile - dnsdial/ # бывш. client/internal/dnsdial - namegen/ # бывш. client/internal/namegen - ish/ # бывш. internal/ish (явно клиент-only) - - server/ - bondregistry/ # бывш. internal/bond/server, если выносить globalBondRegistry чисто - - stats/ # есть -``` - -### Принципы - -1. **`cmd//main.go` = wiring only.** Любая логика — в пакет. -2. **Одна `internal/` иерархия.** `internal/client/*` и `internal/server/*` — бинарь-специфика; `internal/{wire,transport,proxy}` — общее. -3. **Группировка по слою:** wire (байты на проводе) → transport (как открыть сокет) → proxy (что копировать) → client/server (роль). -4. **`logx.Logger` interface** заменяет `Deps{Debug bool, Debugf func}`. Один раз конфигурим в main. -5. **Глобалы умирают:** `globalBondRegistry` → локальная переменная в `cmd/vk-turn-server/main.go`. `isDebug` → logger level. -6. **Имена честные:** - - `tcputil` → `kcptun` - - `wrap` → `srtpmimicry` - - `netadapt` → `netconn` - - `turnpipe` → `turndial` - - `vless` → `tcpfwd` (внутреннее; CLI остаётся `-vless`) - - `bond` (codec) → `bondframe` -7. **`manual_captcha.go` → `internal/client/captcha/manual/`** как полноценный пакет с экспортируемым `Server`/`Solver`. -8. **`proxy.Engine` + `Handler` interface:** убирает дублирование udp/vless. -9. **Side-quests закрываются в этом этапе или не закрываются никогда.** - -### Что НЕ делать -- `src/` — не Go-way, не вводить. -- `pkg/` — не вводить, наружу ничего публичного не отдаём. -- Wire-форматы / CLI флаги / behavior — заморожены. - ---- - -## 6. План реализации - -Каждый этап = один PR. Все PR — non-breaking по CLI/wire. Контрольная панель после каждого: `gofmt -l .` пуст / `go vet ./...` / `golangci-lint run ./...` / `go test ./...`. - -### Этап V2-1 — `cmd/` + глоссарий (низкий риск) - -- [x] **V2-1.1** Создан `cmd/vk-turn-client/main.go` (переезд `client/main.go`) и `cmd/vk-turn-server/main.go` (переезд `server/main.go`). -- [x] **V2-1.2** Обновлены `Dockerfile`, `README.md`, `.github/workflows/{ci,release}.yml` — пути `./cmd/vk-turn-client`/`./cmd/vk-turn-server`. README дополнен Android arm64 + Linux arm64/amd64 `.so`-сборками. -- [x] **V2-1.3** `client/` и `server/` директории удалены. -- [x] **V2-1.4** `notes/GLOSSARY.md` создан (~20 терминов). - -Бонус: `client/internal/*` → `internal/client/*` сделан в этом же PR (половина V2-5) как зависимость переезда в `cmd/`. - -### Этап V2-2 — manual captcha как пакет - -- [x] **V2-2.1** Создан `internal/client/captcha/manual/manual.go` (875 строк, один файл). Экспортированы: `SolveViaProxy`, `SolveViaHTTP`. Package-var `Debug bool` заменяет глобал `isDebug` из main. -- [ ] **V2-2.2** Разбивка на server.go/browser.go/proxy.go/rewrite_html.go **намеренно пропущена** — добавляет риск без выигрыша; один файл в изолированном пакете уже читаем. Если нужно — отдельный PR. -- [x] **V2-2.3** Тест `manual_test.go` переехал, package переключен с `main` на `manual`, тест `TestRewriteProxyRedirectLocation` зелёный. -- [x] **V2-2.4** `cmd/vk-turn-client/main.go`: import `manualcaptcha "internal/client/captcha/manual"`, мост `manualCaptchaSolver` зовёт `manualcaptcha.SolveVia*`, в main устанавливается `manualcaptcha.Debug = cfg.Debug`. - -### Этап V2-3 — `internal/logx` + удаление isDebug-глобалов - -- [x] **V2-3.1** Создан `internal/logx/logx.go`: `Logger{Debugf, Infof, Warnf, Errorf, DebugEnabled}`. stdLogger пишет через `log.Printf` (bit-compat с прежним поведением). Nop для тестов/zero-value. -- [x] **V2-3.2** `Deps{Debug, Debugf}` → `Deps{Log logx.Logger}` в `bondclient`, `bondserver`, `proxy/udp`, `proxy/vless`, `vkauth` (`Config.Debugf` → `Config.Log`). `stats.New(bool)` остался — теперь принимает `logger.DebugEnabled()`. `stats.LogEvery(ctx, logf, ...)` сигнатура не меняется — передаётся `logger.Debugf` как method-value. -- [x] **V2-3.3** `var isDebug` + `func debugf` удалены из обоих main. `logger := logx.New(cfg.Debug)` в `cmd/vk-turn-client/main.go`; package-var `logger` (init Nop, в main переустанавливается) в `cmd/vk-turn-server/main.go` (нужно потому что обработчики handleUDPConnection/handleVLESSConnection берут его без проброса). -- [x] **V2-3.4** `globalBondRegistry` из package init переехал в `main` — инициализируется **после** `config.ParseServer`. Bug-by-design (init с `isDebug=false`) закрыт. - -### Этап V2-4 — переименования (механика, без логики) - -- [x] **V2-4.1** `internal/wrap` → `internal/wire/srtpmimicry`. Импорты обновлены, package-doc дополнен «obfuscation, не security». -- [x] **V2-4.2** `internal/bond/frame.go` → `internal/wire/bondframe/`. `internal/bond/client` → `internal/proxy/bondclient`. `internal/bond/server` → `internal/proxy/bondserver`. -- [x] **V2-4.3** `internal/netadapt` → `internal/netconn`. -- [x] **V2-4.4** `internal/turnpipe` → `internal/transport/turndial`. `internal/dtlsdial` → `internal/transport/dtlsdial`. -- [x] **V2-4.5** `tcputil` → `internal/transport/kcptun`. Side-quest закрыт: `selectedKCPProfile` и `selectedFEC` обёрнуты в `sync.Once` (env читается один раз). -- [x] **V2-4.6** `internal/proxy/udp` → `internal/proxy/udprelay`. `internal/proxy/vless` → `internal/proxy/tcpfwd`. CLI флаг `-vless` оставлен. -- [x] **V2-4.7** `internal/ish` → `internal/client/ish` + расширенный header-doc (ссылка на ish.app, linux/386, sandbox). - -### Этап V2-5 — схлопывание `client/internal/` в `internal/client/` - -- [ ] **V2-5.1** `client/internal/vkauth` → `internal/client/vkauth`. -- [ ] **V2-5.2** `client/internal/captcha` → `internal/client/captcha/auto`. -- [ ] **V2-5.3** `client/internal/browserprofile` → `internal/client/browserprofile`. -- [ ] **V2-5.4** `client/internal/dnsdial` → `internal/client/dnsdial`. -- [ ] **V2-5.5** `client/internal/namegen` → `internal/client/namegen`. -- [ ] **V2-5.6** Удалить пустой `client/` (после переезда `main.go` на V2-1 и `manual_captcha` на V2-2 — он пуст). - -### Этап V2-6 — `proxy.Engine` + Handler (свёрнут до минимальной экстракции) - -- [x] **V2-6.0** Решение 2026-05-19: полный Engine+Handler отклонён после ревью кода. udprelay (WG→DTLS-terminate→wrap→TURN) и tcpfwd (TCP→smux→KCP→DTLS→wrap→TURN) имеют **разное layering wrap/DTLS** и разные lifecycle (per-stream restart vs maintained pool). Реальная общая часть ~30 LOC, а не 70%, как заявлено в плане. Engine с двумя ветками был бы тем же дублём внутри. -- [x] **V2-6.1** Создан `internal/proxy/common/common.go`: `GetCredsFunc`, `DialTURN`, `NewClientWrap`. Только то, что действительно идентично. -- [x] **V2-6.2** `udprelay` и `tcpfwd` используют `common.DialTURN` / `common.NewClientWrap`. Локальные `GetCredsFunc` стали `type ... = common.GetCredsFunc` (alias для совместимости cmd/main wiring). -- [ ] **V2-6.3** Bond как опция `tcpfwd.Handler` — **отменено** вместе с Handler-интерфейсом. `bondclient` остаётся отдельным пакетом, вызываемым из cmd через `Deps.BondHandler`. Текущий контракт читаемый, не трогаем. - -### Этап V2-7 — config структуризация - -- [x] **V2-7.1** `Client` разбит на `{TURN, Obf, Proxy, VK, DNS, Log}`. `ProxyMode` enum (`udp|tcpfwd|tcpfwd-bond`) вместо двух bool-флагов `VLESSMode/VLESSBond` в коде. CLI флаги `-vless`/`-vless-bond` остаются — мапятся в Parse через `clientProxyMode(vless, bond)`. -- [x] **V2-7.2** `Server` использует `{Obf, Proxy, Log}` (TURN/VK/DNS не применимы). `serverProxyMode(vless)` мапит CLI в `ProxyMode`. -- [x] **V2-7.3** cmd/vk-turn-{client,server}/main.go обновлены: все `cfg.X` → `cfg.Group.X`. Тесты `internal/config` обновлены + добавлены `TestParseClient_ProxyMode`, `TestParseServer_ProxyMode`. - -### Этап V2-8 — документирование и решения - -- [x] **V2-8.1** `internal/transport/dtlsdial/doc.go` создан: DTLS как obfuscation, не security; `-no-dtls` удалён в V2-0 потому что без DTLS VK content-filter дропает пакеты. -- [x] **V2-8.2** `server -vless-bond` — **удалён ещё в V2-0** (autodetect покрывает). README отражает. -- [x] **V2-8.3** README обновлён: V2-0 Breaking changes секция, удалены `-no-dtls`/`-vless-bond` из таблиц флагов. Сборка `go build ./cmd/vk-turn-{client,server}` уже была актуальна с V2-1. - ---- - -## 7. Порядок и приоритеты - -**Жёсткие зависимости:** -- V2-4 (переименования) после V2-3 (logx) — иначе придётся переписывать Deps дважды. -- V2-5 (схлопывание client/internal) после V2-1 (cmd/) и V2-2 (manual_captcha вынесен) — иначе client/ не пустой. -- V2-6 (Engine) после V2-4 — работает с новыми именами. -- V2-7 (config группировка) — независим, можно когда угодно. -- V2-8 (doc) — последним. - -**Рекомендуемая последовательность:** -1. V2-1 (cmd + GLOSSARY) -2. V2-2 (manual captcha) -3. V2-3 (logx) -4. V2-4 (переименования) -5. V2-5 (client/internal схлопывается) -6. V2-6 (Engine + Handler) -7. V2-7 (config группировка) -8. V2-8 (doc) - -**Грубая оценка размера (LOC diff на PR):** V2-1 ~150 / V2-2 ~900 / V2-3 ~400 / V2-4 ~600 (mostly move) / V2-5 ~300 (mostly move) / V2-6 ~500 / V2-7 ~200 / V2-8 ~100. - ---- - -## 8. Гарантии non-breaking - -После каждого PR: -- `go build ./...` зелёный. -- `go test ./...` зелёный. -- `golangci-lint run` без новых ворнингов. -- CLI флаги, дефолты, behavior — bit-exact. -- Wire-форматы (wrap/SRTP-mimicry, bond frame, TURN, DTLS, KCP/smux) — bit-exact. -- Поведенческие инварианты сохранены: handshake-сем 3 (UDP-mode), captcha auto→manual fallback, creds cache lifetime 10m-60s safety, throttle 3s+rand. - ---- - -## 9. Что НЕ трогать (даже в этом плане) - -- Wire-форматы любые. -- CLI флаги: имена, дефолты, поведение. -- `tcputil` KCP/smux параметры (только переезд). -- `client/internal/captcha/*` slider-логику (только переезд). -- `internal/wrap` AEAD-байты (только переезд + rename). -- Алгоритм auto-detect bond на сервере. - ---- - -## 10. Решения заказчика (зафиксировано 2026-05-19) - -1. **libvkturn.so** — мобильная сборка, собирается вручную, переносится в приложение руками. CI не строит. Внешних `//export` в коде нет. **Решение:** `pkg/` не вводим, всё `internal/`. Пользователь после V2-1 пересоберёт с новым путём `./cmd/vk-turn-client` (или с buildmode-target, который он использует). -2. **Server-флаг `-vless-bond`** — **удаляем**. Autodetect по magic покрывает. Это **breaking CLI change на сервере** — фиксируем в release notes. -3. **`-no-dtls` (client `Direct`)** — **удаляем**. Помечен DO NOT USE, в проде не работает. Это **breaking CLI change на клиенте**. -4. **Server multi-tenant** (`notes/AUTH_PLAN.md`) — **план отменён**, вычёркиваем из контекста. -5. **TURN-scanner** (`client/scan*.go`) — открытый вопрос; кандидат `internal/client/turnscan/`. Решить при V2-5. - -### Влияние на non-breaking гарантии - -Пункт 8 («CLI флаги bit-exact») **корректируется**: -- Удаляются: `-no-dtls` (client), `-vless-bond` (server). Остальные CLI флаги — без изменений. -- Дефолты и поведение оставшихся флагов — bit-exact. -- Wire-форматы — bit-exact (без изменений). - -### Новый этап V2-0 (вынесен в начало) - -- [x] **V2-0.1** Удалён `-no-dtls` из client CLI. `config.Client.Direct` поле + dispatcher-guard в main + тест `TestParseClient_WrapDirectConflict` — удалены. Веток в proxy/* не было (только main). -- [x] **V2-0.2** Удалён `-vless-bond` из server CLI и `config.Server.VLESSBond`. `useBond` параметр `handleVLESSConnection` убран, warning-ветка «non-bond stream while bond enabled» удалена, поле снято со startup-лога. Сервер автоопределяет bond по magic. -- [ ] **V2-0.3** README Breaking changes — **пока не сделано**. Можно отложить до большого release-notes под весь V2. - -V2-0 идёт **до V2-1** — упрощает дерево перед переездом в `cmd/`. - -### Обновлённый порядок - -1. V2-0 (breaking CLI cleanup) -2. V2-1 (cmd/ + GLOSSARY) -3. V2-2 (manual captcha пакет) -4. V2-3 (logx) -5. V2-4 (переименования) -6. V2-5 (client/internal схлопывается + решение по turnscan) -7. V2-6 (Engine + Handler) -8. V2-7 (config группировка) -9. V2-8 (doc)