From dd59e08e318e05325192251047089e087da55f2a Mon Sep 17 00:00:00 2001 From: alexmac6574 <215134852+alexmac6574@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:02:46 +0300 Subject: [PATCH] feat: New attempt to fix auto solving captcha --- client/main.go | 199 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 161 insertions(+), 38 deletions(-) diff --git a/client/main.go b/client/main.go index a567390..15c27fa 100644 --- a/client/main.go +++ b/client/main.go @@ -18,6 +18,7 @@ import ( "math/rand" "net" "net/http" + "net/http/cookiejar" neturl "net/url" "os" "os/signal" @@ -52,8 +53,11 @@ type directListenConfig struct { *net.ListenConfig } -// globalClientWGAddr safely stores the UDP address of the local WireGuard client +// Global state trackers var globalClientWGAddr atomic.Value +var globalCaptchaLockout atomic.Int64 +var connectedStreams atomic.Int32 +var globalAppCancel context.CancelFunc func newDirectNet() transport.Net { return directNet{} @@ -152,6 +156,18 @@ func applyBrowserProfile(req *http.Request, profile Profile) { req.Header.Set("DNT", "1") } +func generateFakeCursor() string { + startX := 800 + rand.Intn(200) + startY := 400 + rand.Intn(200) + var points []string + for i := 0; i < 5+rand.Intn(5); i++ { + startX += rand.Intn(10) - 2 + startY += rand.Intn(10) - 2 + points = append(points, fmt.Sprintf(`{"x":%d,"y":%d}`, startX, startY)) + } + return "[" + strings.Join(points, ",") + "]" +} + // endregion // region Automatic Captcha Solver & Authentication @@ -223,7 +239,7 @@ func (e *VkCaptchaError) IsCaptchaError() bool { return e.ErrorCode == 14 && e.RedirectUri != "" && e.SessionToken != "" } -func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, dialer *dnsdialer.Dialer, profile Profile) (string, error) { +func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, dialer *dnsdialer.Dialer, jar *cookiejar.Jar, profile Profile) (string, error) { log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID) if captchaErr.SessionToken == "" { @@ -233,7 +249,7 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in return "", fmt.Errorf("no redirect_uri for auto-solve") } - powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer, profile) + powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer, jar, profile) if err != nil { return "", fmt.Errorf("failed to fetch PoW input: %w", err) } @@ -243,7 +259,7 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in hash := solvePoW(powInput, difficulty) log.Printf("[STREAM %d] [Captcha] PoW solved: hash=%s", streamID, hash) - successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, dialer, profile) + successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, dialer, jar, profile) if err != nil { return "", fmt.Errorf("captchaNotRobot API failed: %w", err) } @@ -252,7 +268,7 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in return successToken, nil } -func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer, profile Profile) (string, int, error) { +func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer, jar *cookiejar.Jar, profile Profile) (string, int, error) { parsedURL, err := neturl.Parse(redirectUri) if err != nil { return "", 0, err @@ -264,7 +280,7 @@ func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Di return "", 0, err } - req.Host = domain // Explicitly force Host header + req.Host = domain applyBrowserProfile(req, profile) req.Header.Set("Sec-Fetch-Site", "none") req.Header.Set("Sec-Fetch-Mode", "navigate") @@ -273,6 +289,7 @@ func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Di client := &http.Client{ Timeout: 20 * time.Second, + Jar: jar, Transport: &http.Transport{ DialContext: dialer.DialContext, TLSClientConfig: &tls.Config{ @@ -325,7 +342,7 @@ func solvePoW(powInput string, difficulty int) string { return "" } -func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, dialer *dnsdialer.Dialer, profile Profile) (string, error) { +func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, dialer *dnsdialer.Dialer, jar *cookiejar.Jar, profile Profile) (string, error) { vkReq := func(method string, postData string) (map[string]interface{}, error) { reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" parsedURL, _ := neturl.Parse(reqURL) @@ -350,6 +367,7 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI client := &http.Client{ Timeout: 20 * time.Second, + Jar: jar, Transport: &http.Transport{ DialContext: dialer.DialContext, TLSClientConfig: &tls.Config{ @@ -398,7 +416,7 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI time.Sleep(200 * time.Millisecond) log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID) - cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]` + cursorJSON := generateFakeCursor() answer := base64.StdEncoding.EncodeToString([]byte("{}")) debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785" @@ -624,12 +642,18 @@ func fetchVkCredsSerialized(ctx context.Context, link string, streamID int, dial } func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) { + // Check Global Lockout to prevent API bans + if time.Now().Unix() < globalCaptchaLockout.Load() { + return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED: global lockout active") + } + var lastErr error + jar, _ := cookiejar.New(nil) for _, creds := range vkCredentialsList { log.Printf("[STREAM %d] [VK Auth] Trying credentials: client_id=%s", streamID, creds.ClientID) - user, pass, addr, err := getTokenChain(ctx, link, streamID, creds, dialer) + user, pass, addr, err := getTokenChain(ctx, link, streamID, creds, dialer, jar) if err == nil { log.Printf("[STREAM %d] [VK Auth] Success with client_id=%s", streamID, creds.ClientID) @@ -639,6 +663,11 @@ func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdia lastErr = err log.Printf("[STREAM %d] [VK Auth] Failed with client_id=%s: %v", streamID, creds.ClientID, err) + // Hard abort on captcha/fatal conditions instead of trying next creds + if strings.Contains(err.Error(), "CAPTCHA_WAIT_REQUIRED") || strings.Contains(err.Error(), "FATAL_CAPTCHA") { + return "", "", "", err + } + if strings.Contains(err.Error(), "error_code:29") || strings.Contains(err.Error(), "error_code: 29") || strings.Contains(err.Error(), "Rate limit") { log.Printf("[STREAM %d] [VK Auth] Rate limit detected, trying next credentials...", streamID) } @@ -647,7 +676,7 @@ func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdia return "", "", "", fmt.Errorf("all VK credentials failed: %w", lastErr) } -func getTokenChain(ctx context.Context, link string, streamID int, creds VKCredentials, dialer *dnsdialer.Dialer) (string, string, string, error) { +func getTokenChain(ctx context.Context, link string, streamID int, creds VKCredentials, dialer *dnsdialer.Dialer, jar *cookiejar.Jar) (string, string, string, error) { profile := getRandomProfile() name := generateName() escapedName := neturl.QueryEscape(name) @@ -660,6 +689,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede client := &http.Client{ Timeout: 20 * time.Second, + Jar: jar, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, @@ -670,7 +700,6 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede }, }, } - defer client.CloseIdleConnections() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data))) if err != nil { @@ -731,15 +760,15 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&fields=photo_200&access_token=%s", link, token1) _, _ = doRequest(data, "https://api.vk.ru/method/calls.getCallPreview?v=5.275&client_id="+creds.ClientID) - vkDelayRandom(500, 1000) // HAR: Delay updated + vkDelayRandom(500, 1000) - // Token 2 + // Token 2 (with 2 auto attempts + 1 manual fallback) data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s", link, escapedName, token1) urlAddr := fmt.Sprintf("https://api.vk.ru/method/calls.getAnonymousToken?v=5.275&client_id=%s", creds.ClientID) var token2 string - const maxCaptchaAttempts = 3 - for attempt := 0; attempt <= maxCaptchaAttempts; attempt++ { + const maxAutoAttempts = 2 + for attempt := 0; attempt <= maxAutoAttempts+1; attempt++ { resp, err = doRequest(data, urlAddr) if err != nil { return "", "", "", err @@ -752,35 +781,87 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede var captchaKey string var solveErr error - // Try automatic if possible and attempts remain - if attempt < maxCaptchaAttempts && captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { - successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, dialer, profile) - if solveErr != nil { - log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v. Falling back to manual...", streamID, solveErr) + if attempt < maxAutoAttempts { + // Auto Solve Attempts + if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { + successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, dialer, jar, profile) + if solveErr != nil { + log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr) + } + } else { + solveErr = fmt.Errorf("missing fields for auto solve") + } + } else if attempt == maxAutoAttempts { + // Manual Solve Fallback with 60s Timeout + log.Printf("[STREAM %d] [Captcha] Auto failed %d times. Triggering MANUAL fallback...", streamID, maxAutoAttempts) + + manualCtx, manualCancel := context.WithTimeout(ctx, 60*time.Second) + + type manualRes struct { + token string + key string + err error } - } else if attempt >= maxCaptchaAttempts { - log.Printf("[STREAM %d] [Captcha] Max auto attempts reached. Falling back to manual...", streamID) - solveErr = fmt.Errorf("max attempts reached") + resCh := make(chan manualRes, 1) + + go func() { + var t, k string + var e error + if captchaErr.RedirectUri != "" { + t, e = solveCaptchaViaProxy(captchaErr.RedirectUri, dialer) + } else if captchaErr.CaptchaImg != "" { + k, e = solveCaptchaViaHTTP(captchaErr.CaptchaImg) + } else { + e = fmt.Errorf("no redirect_uri or captcha_img") + } + resCh <- manualRes{t, k, e} + }() + + select { + case res := <-resCh: + successToken = res.token + captchaKey = res.key + solveErr = res.err + case <-manualCtx.Done(): + solveErr = fmt.Errorf("manual captcha timed out after 60s") + } + manualCancel() } else { - log.Printf("[STREAM %d] [Captcha] Missing fields for auto solve. Falling back to manual...", streamID) - solveErr = fmt.Errorf("missing fields") + solveErr = fmt.Errorf("max attempts reached") } - // If auto failed, or we skipped it, drop to manual fallback + // If solving failed (auto or manual) or timed out if solveErr != nil { - if captchaErr.RedirectUri != "" { - successToken, solveErr = solveCaptchaViaProxy(captchaErr.RedirectUri, dialer) - if solveErr != nil { - return "", "", "", fmt.Errorf("manual proxy captcha solve error: %w", solveErr) + log.Printf("[STREAM %d] [Captcha] Failed to solve (attempt %d): %v", streamID, attempt+1, solveErr) + + if attempt < maxAutoAttempts-1 { + log.Printf("[STREAM %d] [Captcha] Backing off for 10 seconds before next auto attempt...", streamID) + select { + case <-ctx.Done(): + return "", "", "", ctx.Err() + case <-time.After(10 * time.Second): } - } else if captchaErr.CaptchaImg != "" { - captchaKey, solveErr = solveCaptchaViaHTTP(captchaErr.CaptchaImg) - if solveErr != nil { - return "", "", "", fmt.Errorf("manual HTTP captcha solve error: %w", solveErr) + continue + } else if attempt == maxAutoAttempts-1 { + log.Printf("[STREAM %d] [Captcha] Backing off for 30 seconds before manual fallback...", streamID) + select { + case <-ctx.Done(): + return "", "", "", ctx.Err() + case <-time.After(30 * time.Second): } - } else { - return "", "", "", fmt.Errorf("cannot solve captcha manually: no redirect_uri or captcha_img") + continue + } + + // Engage global lockout to protect API + globalCaptchaLockout.Store(time.Now().Add(60 * time.Second).Unix()) + + // If we have 0 streams alive, this is fatal + if connectedStreams.Load() == 0 { + log.Printf("[STREAM %d] [FATAL] 0 connected streams and manual captcha failed/timed out.", streamID) + return "", "", "", fmt.Errorf("FATAL_CAPTCHA_FAILED_NO_STREAMS") } + + return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED") } if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" { @@ -839,7 +920,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede clean := strings.Split(urlStr, "?")[0] address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:") - vkDelayRandom(4000, 5000) // HAR: Final matching delay + vkDelayRandom(4000, 5000) return user, pass, address, nil } @@ -1398,7 +1479,11 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD err = fmt.Errorf("failed to allocate: %s", err1) return } + + // Safely track active streams globally + connectedStreams.Add(1) defer func() { + connectedStreams.Add(-1) if err1 := relayConn.Close(); err1 != nil { err = fmt.Errorf("failed to close TURN allocated connection: %s", err1) } @@ -1495,6 +1580,10 @@ func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnCha c := make(chan error) go oneDtlsConnection(ctx, peer, listenConn, connchan, okchan, c) if err := <-c; err != nil { + // Suppress DTLS handshake timeout logs while a captcha lockout is active + if time.Now().Unix() < globalCaptchaLockout.Load() && strings.Contains(err.Error(), "context deadline exceeded") { + continue + } log.Printf("%s", err) } } @@ -1514,8 +1603,41 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne } c := make(chan error) go oneTurnConnection(ctx, turnParams, peer, conn2, streamID, c) + if err := <-c; err != nil { - log.Printf("[STREAM %d] %s", streamID, err) + if strings.Contains(err.Error(), "FATAL_CAPTCHA") { + log.Printf("[STREAM %d] Fatal manual captcha error. Shutting down application.", streamID) + if globalAppCancel != nil { + globalAppCancel() + } + return + } + if strings.Contains(err.Error(), "CAPTCHA_WAIT_REQUIRED") { + // Only log the backoff message once (the stream that triggered it) + // For subsequently awoken streams: calculate exact remaining sleep duration and sleep silently + if !strings.Contains(err.Error(), "global lockout active") { + log.Printf("[STREAM %d] !!! VK DEMANDS SLIDER CAPTCHA. Backing off for 60 seconds to avoid IP ban...", streamID) + select { + case <-ctx.Done(): + return + case <-time.After(60 * time.Second): + } + } else { + lockoutEnd := globalCaptchaLockout.Load() + sleepDuration := time.Until(time.Unix(lockoutEnd, 0)) + if sleepDuration < 0 { + sleepDuration = 5 * time.Second + } + select { + case <-ctx.Done(): + return + case <-time.After(sleepDuration): + } + } + } else { + log.Printf("[STREAM %d] %s", streamID, err) + time.Sleep(2 * time.Second) + } } } } @@ -1523,6 +1645,7 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne func main() { ctx, cancel := context.WithCancel(context.Background()) + globalAppCancel = cancel defer cancel() signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)