From 37656256ba805506a7b202f6700b10da57ece5de Mon Sep 17 00:00:00 2001 From: alexmac6574 <215134852+alexmac6574@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:38:37 +0300 Subject: [PATCH 1/2] fix: Implement automatic Proof-of-Work (PoW) captcha solver Co-Authored-By: kiper292 <152370987+kiper292@users.noreply.github.com> --- client/main.go | 555 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 505 insertions(+), 50 deletions(-) diff --git a/client/main.go b/client/main.go index 776b1a6..2b249f6 100644 --- a/client/main.go +++ b/client/main.go @@ -6,33 +6,370 @@ package main import ( "bytes" "context" + "crypto/sha256" "crypto/tls" + "encoding/base64" + "encoding/hex" "encoding/json" "flag" "fmt" - "github.com/bschaatsbergen/dnsdialer" - "github.com/cbeuw/connutil" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "github.com/pion/dtls/v3" - "github.com/pion/dtls/v3/pkg/crypto/selfsign" - "github.com/pion/logging" - "github.com/pion/turn/v5" "io" "log" + "math/rand" "net" "net/http" + neturl "net/url" "os" "os/signal" + "regexp" + "strconv" "strings" "sync" "sync/atomic" "syscall" "time" + + "github.com/bschaatsbergen/dnsdialer" + "github.com/cbeuw/connutil" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/pion/dtls/v3" + "github.com/pion/dtls/v3/pkg/crypto/selfsign" + "github.com/pion/logging" + "github.com/pion/transport/v4" + "github.com/pion/turn/v5" ) type getCredsFunc func(string) (string, string, string, error) +type directNet struct{} + +type directDialer struct { + *net.Dialer +} + +type directListenConfig struct { + *net.ListenConfig +} + +// globalClientWGAddr safely stores the UDP address of the local WireGuard client +var globalClientWGAddr atomic.Value + +func newDirectNet() transport.Net { + return directNet{} +} + +func (directNet) ListenPacket(network string, address string) (net.PacketConn, error) { + return net.ListenPacket(network, address) //nolint:noctx +} + +func (directNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) { + return net.ListenUDP(network, locAddr) +} + +func (directNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) { + listener, err := net.ListenTCP(network, laddr) + if err != nil { + return nil, err + } + + return directTCPListener{listener}, nil +} + +func (directNet) Dial(network, address string) (net.Conn, error) { + return net.Dial(network, address) //nolint:noctx +} + +func (directNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) { + return net.DialUDP(network, laddr, raddr) +} + +func (directNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) { + return net.DialTCP(network, laddr, raddr) +} + +func (directNet) ResolveIPAddr(network, address string) (*net.IPAddr, error) { + return net.ResolveIPAddr(network, address) +} + +func (directNet) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) { + return net.ResolveUDPAddr(network, address) +} + +func (directNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) { + return net.ResolveTCPAddr(network, address) +} + +func (directNet) Interfaces() ([]*transport.Interface, error) { + return nil, transport.ErrNotSupported +} + +func (directNet) InterfaceByIndex(index int) (*transport.Interface, error) { + return nil, fmt.Errorf("%w: index=%d", transport.ErrInterfaceNotFound, index) +} + +func (directNet) InterfaceByName(name string) (*transport.Interface, error) { + return nil, fmt.Errorf("%w: %s", transport.ErrInterfaceNotFound, name) +} + +func (directNet) CreateDialer(dialer *net.Dialer) transport.Dialer { + return directDialer{Dialer: dialer} +} + +func (directNet) CreateListenConfig(listenerConfig *net.ListenConfig) transport.ListenConfig { + return directListenConfig{ListenConfig: listenerConfig} +} + +func (d directDialer) Dial(network, address string) (net.Conn, error) { + return d.Dialer.Dial(network, address) +} + +func (d directListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) { + return d.ListenConfig.Listen(ctx, network, address) +} + +func (d directListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) { + return d.ListenConfig.ListenPacket(ctx, network, address) +} + +type directTCPListener struct { + *net.TCPListener +} + +func (l directTCPListener) AcceptTCP() (transport.TCPConn, error) { + return l.TCPListener.AcceptTCP() +} + +// region automatic captcha solver + +type vkCaptchaError struct { + ErrorCode int + ErrorMsg string + CaptchaSid string + RedirectUri string + SessionToken string + CaptchaTs string + CaptchaAttempt string +} + +func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError { + codeFloat, _ := errData["error_code"].(float64) + redirectUri, _ := errData["redirect_uri"].(string) + errorMsg, _ := errData["error_msg"].(string) + + captchaSid, _ := errData["captcha_sid"].(string) + if captchaSid == "" { + if sidNum, ok := errData["captcha_sid"].(float64); ok { + captchaSid = fmt.Sprintf("%.0f", sidNum) + } + } + + var sessionToken string + if redirectUri != "" { + if parsed, err := neturl.Parse(redirectUri); err == nil { + sessionToken = parsed.Query().Get("session_token") + } + } + + var captchaTs string + if tsFloat, ok := errData["captcha_ts"].(float64); ok { + captchaTs = fmt.Sprintf("%.0f", tsFloat) + } else if tsStr, ok := errData["captcha_ts"].(string); ok { + captchaTs = tsStr + } + + var captchaAttempt string + if attFloat, ok := errData["captcha_attempt"].(float64); ok { + captchaAttempt = fmt.Sprintf("%.0f", attFloat) + } else if attStr, ok := errData["captcha_attempt"].(string); ok { + captchaAttempt = attStr + } + + return &vkCaptchaError{ + ErrorCode: int(codeFloat), + ErrorMsg: errorMsg, + CaptchaSid: captchaSid, + RedirectUri: redirectUri, + SessionToken: sessionToken, + CaptchaTs: captchaTs, + CaptchaAttempt: captchaAttempt, + } +} + +func solveVkCaptcha(ctx context.Context, captchaErr *vkCaptchaError, dialer *dnsdialer.Dialer) (string, error) { + log.Printf("Solving VK Smart Captcha automatically...") + if captchaErr.SessionToken == "" { + return "", fmt.Errorf("no session_token in redirect_uri") + } + + powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer) + if err != nil { + return "", fmt.Errorf("failed to fetch PoW input: %w", err) + } + + hash := solvePoW(powInput, difficulty) + + successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, dialer) + if err != nil { + return "", fmt.Errorf("captchaNotRobot API failed: %w", err) + } + + log.Printf("VK Smart Captcha Solved Successfully!") + return successToken, nil +} + +func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer) (string, int, error) { + req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil) + if err != nil { + return "", 0, err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + + client := &http.Client{ + Timeout: 20 * time.Second, + Transport: &http.Transport{ + DialContext: dialer.DialContext, + }, + } + resp, err := client.Do(req) + if err != nil { + return "", 0, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", 0, err + } + html := string(body) + + powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`) + powInputMatch := powInputRe.FindStringSubmatch(html) + if len(powInputMatch) < 2 { + return "", 0, fmt.Errorf("powInput not found in captcha HTML") + } + powInput := powInputMatch[1] + + diffRe := regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`) + diffMatch := diffRe.FindStringSubmatch(html) + difficulty := 2 + if len(diffMatch) >= 2 { + if d, err := strconv.Atoi(diffMatch[1]); err == nil { + difficulty = d + } + } + return powInput, difficulty, nil +} + +func solvePoW(powInput string, difficulty int) string { + target := strings.Repeat("0", difficulty) + for nonce := 1; nonce <= 10000000; nonce++ { + data := powInput + strconv.Itoa(nonce) + hash := sha256.Sum256([]byte(data)) + hexHash := hex.EncodeToString(hash[:]) + if strings.HasPrefix(hexHash, target) { + return hexHash + } + } + return "" +} + +func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer *dnsdialer.Dialer) (string, error) { + vkReq := func(method string, postData string) (map[string]interface{}, error) { + reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" + req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData)) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", "https://vk.ru") + req.Header.Set("Referer", "https://vk.ru/") + + client := &http.Client{ + Timeout: 20 * time.Second, + Transport: &http.Transport{ + DialContext: dialer.DialContext, + }, + } + + httpResp, err := client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, err + } + var resp map[string]interface{} + if err := json.Unmarshal(body, &resp); err != nil { + return nil, err + } + return resp, nil + } + + baseParams := fmt.Sprintf("session_token=%s&domain=vk.com&adFp=&access_token=", neturl.QueryEscape(sessionToken)) + + // Step 1: settings + if _, err := vkReq("captchaNotRobot.settings", baseParams); err != nil { + return "", fmt.Errorf("settings failed: %w", err) + } + time.Sleep(200 * time.Millisecond) + + // Step 2: componentDone + browserFp := fmt.Sprintf("%032x", rand.Int63()) + deviceJSON := `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1920,"innerHeight":945,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":16,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"denied"}` + componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON)) + + if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil { + return "", fmt.Errorf("componentDone failed: %w", err) + } + time.Sleep(200 * time.Millisecond) + + // Step 3: check + cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]` + answer := base64.StdEncoding.EncodeToString([]byte("{}")) + debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785" + + checkData := baseParams + fmt.Sprintf( + "&accelerometer=%s&gyroscope=%s&motion=%s&cursor=%s&taps=%s&connectionRtt=%s&connectionDownlink=%s&browser_fp=%s&hash=%s&answer=%s&debug_info=%s", + neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), + neturl.QueryEscape(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), + neturl.QueryEscape("[9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5]"), + browserFp, hash, answer, debugInfo, + ) + + checkResp, err := vkReq("captchaNotRobot.check", checkData) + if err != nil { + return "", fmt.Errorf("check failed: %w", err) + } + + respObj, ok := checkResp["response"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("invalid check response: %v", checkResp) + } + status, _ := respObj["status"].(string) + if status != "OK" { + return "", fmt.Errorf("check status: %s", status) + } + successToken, ok := respObj["success_token"].(string) + if !ok || successToken == "" { + return "", fmt.Errorf("success_token not found") + } + + time.Sleep(200 * time.Millisecond) + + // Step 4: endSession + vkReq("captchaNotRobot.endSession", baseParams) + + return successToken, nil +} + +// endregion automatic captcha solver + func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, error) { doRequest := func(data string, url string) (resp map[string]interface{}, err error) { @@ -93,17 +430,65 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("request error:%s", err) } - token1 := resp["data"].(map[string]interface{})["access_token"].(string) + dataMap, ok := resp["data"].(map[string]interface{}) + if !ok { + return "", "", "", fmt.Errorf("unexpected anon token response: %v", resp) + } + token1, ok := dataMap["access_token"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing access_token in response: %v", resp) + } data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=123&access_token=%s", link, token1) url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487" - resp, err = doRequest(data, url) - if err != nil { - return "", "", "", fmt.Errorf("request error:%s", err) - } + var token2 string + const maxCaptchaAttempts = 3 + for attempt := 0; attempt <= maxCaptchaAttempts; attempt++ { + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + // Check for captcha error + if errObj, hasErr := resp["error"].(map[string]interface{}); hasErr { + errCode, _ := errObj["error_code"].(float64) + if errCode == 14 { + if attempt == maxCaptchaAttempts { + return "", "", "", fmt.Errorf("captcha failed after %d attempts", maxCaptchaAttempts) + } + + captchaErr := parseVkCaptchaError(errObj) + if captchaErr.SessionToken != "" { + successToken, solveErr := solveVkCaptcha(context.Background(), captchaErr, dialer) + if solveErr != nil { + return "", "", "", fmt.Errorf("auto captcha solve error: %w", solveErr) + } - token2 := resp["response"].(map[string]interface{})["token"].(string) + if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" { + captchaErr.CaptchaAttempt = "1" + } + + data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s&captcha_key=&captcha_sid=%s&is_sound_captcha=0&success_token=%s&captcha_ts=%s&captcha_attempt=%s", + link, escapedName, token1, captchaErr.CaptchaSid, neturl.QueryEscape(successToken), captchaErr.CaptchaTs, captchaErr.CaptchaAttempt) + continue + } else { + return "", "", "", fmt.Errorf("old image captcha detected - not supported in auto solver") + } + } + return "", "", "", fmt.Errorf("VK API error: %v", errObj) + } + + respMap, ok := resp["response"].(map[string]interface{}) + if !ok { + return "", "", "", fmt.Errorf("unexpected getAnonymousToken response: %v", resp) + } + token2, ok = respMap["token"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing token in response: %v", resp) + } + break + } data = fmt.Sprintf("%s%s%s", "session_data=%7B%22version%22%3A2%2C%22device_id%22%3A%22", uuid.New(), "%22%2C%22client_version%22%3A1.1%2C%22client_type%22%3A%22SDK_JS%22%7D&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA") url = "https://calls.okcdn.ru/fb.do" @@ -479,15 +864,16 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa log.Printf("Closed DTLS connection\n") }() log.Printf("Established DTLS connection!\n") - go func() { - for { + + // Trigger the okchan safely to spawn the rest of the threads + if okchan != nil { + go func() { select { - case <-dtlsctx.Done(): - return case okchan <- struct{}{}: + case <-dtlsctx.Done(): } - } - }() + }() + } wg := sync.WaitGroup{} wg.Add(2) @@ -499,7 +885,7 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa log.Printf("Failed to set DTLS deadline: %s", err) } }) - var addr atomic.Value + // Start read-loop on listenConn go func() { defer wg.Done() @@ -517,7 +903,7 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa return } - addr.Store(addr1) // store peer + globalClientWGAddr.Store(addr1) // store local WG peer address globally _, err1 = dtlsConn.Write(buf[:n]) if err1 != nil { @@ -543,10 +929,11 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa log.Printf("Failed: %s", err1) return } - addr1, ok := addr.Load().(net.Addr) + + addr1, ok := globalClientWGAddr.Load().(net.Addr) if !ok { - log.Printf("Failed: no listener ip") - return + // Safely drop packet if wireguard hasn't sent an initial packet yet + continue } _, err1 = listenConn.WriteTo(buf[:n], addr1) @@ -655,6 +1042,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD STUNServerAddr: turnServerAddr, TURNServerAddr: turnServerAddr, Conn: turnConn, + Net: newDirectNet(), Username: user, Password: pass, RequestedAddressFamily: addrFamily, @@ -704,7 +1092,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD log.Printf("Failed to set upstream deadline: %s", err) } }) - var addr atomic.Value + var internalPipeAddr atomic.Value // Start read-loop on conn2 (output of DTLS) go func() { defer wg.Done() @@ -722,7 +1110,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD return } - addr.Store(addr1) // store peer + internalPipeAddr.Store(addr1) // store local async pipe peer _, err1 = relayConn.WriteTo(buf[:n], peer) if err1 != nil { @@ -748,7 +1136,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD log.Printf("Failed: %s", err1) return } - addr1, ok := addr.Load().(net.Addr) + addr1, ok := internalPipeAddr.Load().(net.Addr) if !ok { log.Printf("Failed: no listener ip") return @@ -792,16 +1180,73 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne case <-ctx.Done(): return case conn2 := <-connchan: + // Ensure we block cleanly until the tick signals to proceed select { case <-t: - c := make(chan error) - go oneTurnConnection(ctx, turnParams, peer, conn2, c) - if err := <-c; err != nil { - log.Printf("%s", err) + case <-ctx.Done(): + return + } + c := make(chan error) + go oneTurnConnection(ctx, turnParams, peer, conn2, c) + if err := <-c; err != nil { + log.Printf("%s", err) + } + } + } +} + +type turnCred struct { + user, pass, addr string +} + +// poolCreds allows retrieving unique TURN credentials for N distinct connections. +// Because it natively handles the automatic captcha bypass, every request gets a unique identity safely. +func poolCreds(f getCredsFunc, poolSize int) getCredsFunc { + var mu sync.Mutex + var pool []turnCred + var cTime time.Time + var idx int + + return func(link string) (string, string, string, error) { + mu.Lock() + defer mu.Unlock() + + // Refresh identities every 10 minutes + if !cTime.IsZero() && time.Since(cTime) > 10*time.Minute { + pool = nil + cTime = time.Time{} + } + + if len(pool) < poolSize { + u, p, a, err := f(link) + if err == nil { + pool = append(pool, turnCred{u, p, a}) + cTime = time.Now() + log.Printf("Successfully registered User Identity %d/%d", len(pool), poolSize) + + // Space out requests by 1000ms to avoid API limits + if len(pool) < poolSize { + time.Sleep(1000 * time.Millisecond) } - default: + + c := pool[len(pool)-1] + idx++ + return c.user, c.pass, c.addr, nil + } + + log.Printf("Failed to get unique TURN identity: %v", err) + if len(pool) > 0 { + log.Printf("Falling back to reusing a previous identity...") + c := pool[idx%len(pool)] + idx++ + return c.user, c.pass, c.addr, nil } + return "", "", "", err } + + c := pool[idx%len(pool)] + idx++ + return c.user, c.pass, c.addr, nil } } @@ -827,7 +1272,7 @@ func main() { //nolint:cyclop vklink := flag.String("vk-link", "", "VK calls invite link \"https://vk.com/call/join/...\"") yalink := flag.String("yandex-link", "", "Yandex telemost invite link \"https://telemost.yandex.ru/j/...\"") peerAddr := flag.String("peer", "", "peer server address (host:port)") - n := flag.Int("n", 0, "connections to TURN (default 16 for VK, 1 for Yandex)") + n := flag.Int("n", 0, "connections to TURN (default 10 for VK, 1 for Yandex)") udp := flag.Bool("udp", false, "connect to TURN with UDP") direct := flag.Bool("no-dtls", false, "connect without obfuscation. DO NOT USE") flag.Parse() @@ -858,7 +1303,7 @@ func main() { //nolint:cyclop return getVkCreds(s, dialer) } if *n <= 0 { - *n = 16 + *n = 10 } } else { parts := strings.Split(*yalink, "j/") @@ -872,11 +1317,11 @@ func main() { //nolint:cyclop link = link[:idx] } params := &turnParams{ - *host, - *port, - link, - *udp, - getCreds, + host: *host, + port: *port, + link: link, + udp: *udp, + getCreds: poolCreds(getCreds, *n), } listenConnChan := make(chan net.PacketConn) @@ -903,21 +1348,27 @@ func main() { //nolint:cyclop t := time.Tick(200 * time.Millisecond) if *direct { for i := 0; i < *n; i++ { - wg1.Go(func() { + wg1.Add(1) + go func() { + defer wg1.Done() oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t) - }) + }() } } else { okchan := make(chan struct{}) connchan := make(chan net.PacketConn) - wg1.Go(func() { + wg1.Add(1) + go func() { + defer wg1.Done() oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, okchan) - }) + }() - wg1.Go(func() { + wg1.Add(1) + go func() { + defer wg1.Done() oneTurnConnectionLoop(ctx, params, peer, connchan, t) - }) + }() select { case <-okchan: @@ -925,12 +1376,16 @@ func main() { //nolint:cyclop } for i := 0; i < *n-1; i++ { connchan := make(chan net.PacketConn) - wg1.Go(func() { + wg1.Add(1) + go func() { + defer wg1.Done() oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil) - }) - wg1.Go(func() { + }() + wg1.Add(1) + go func() { + defer wg1.Done() oneTurnConnectionLoop(ctx, params, peer, connchan, t) - }) + }() } } From 6305087c7cd66d4eba1c81cd60ee37998200bd2a Mon Sep 17 00:00:00 2001 From: alexmac6574 <215134852+alexmac6574@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:43:17 +0300 Subject: [PATCH 2/2] feat: Add randomize for name and user-agent Co-Authored-By: zcb22 <263202030+zcb22@users.noreply.github.com> --- client/main.go | 35 +++++++++++++++++++---------- client/namegen.go | 42 +++++++++++++++++++++++++++++++++++ client/profiles.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 client/namegen.go create mode 100644 client/profiles.go diff --git a/client/main.go b/client/main.go index 2b249f6..07a2be4 100644 --- a/client/main.go +++ b/client/main.go @@ -371,9 +371,13 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer // endregion automatic captcha solver func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, error) { + profile := getRandomProfile() + name := generateName() + escapedName := neturl.QueryEscape(name) - doRequest := func(data string, url string) (resp map[string]interface{}, err error) { + log.Printf("Connecting Identity - Name: %s | User-Agent: %s", name, profile.UserAgent) + doRequest := func(data string, url string) (resp map[string]interface{}, err error) { client := &http.Client{ Timeout: 20 * time.Second, Transport: &http.Transport{ @@ -384,12 +388,13 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, }, } defer client.CloseIdleConnections() + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) if err != nil { return nil, err } - req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0") + req.Header.Add("User-Agent", profile.UserAgent) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") httpResp, err := client.Do(req) @@ -439,7 +444,7 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("missing access_token in response: %v", resp) } - data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=123&access_token=%s", link, token1) + data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s", link, escapedName, token1) url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487" var token2 string @@ -522,7 +527,9 @@ func getYandexCreds(link string) (string, string, string, error) { const debug = false const telemostConfHost = "cloud-api.yandex.ru" telemostConfPath := fmt.Sprintf("%s%s%s", "/telemost_front/v2/telemost/conferences/https%3A%2F%2Ftelemost.yandex.ru%2Fj%2F", link, "/connection?next_gen_media_platform_allowed=false") - const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0" + profile := getRandomProfile() + userAgent := profile.UserAgent + name := generateName() type ConferenceResponse struct { URI string `json:"uri"` @@ -636,13 +643,14 @@ func getYandexCreds(link string) (string, string, string, error) { } endpoint := "https://" + telemostConfHost + telemostConfPath + tr := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + } client := &http.Client{ - Timeout: 20 * time.Second, - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - }, + Timeout: 20 * time.Second, + Transport: tr, } defer client.CloseIdleConnections() req, err := http.NewRequest("GET", endpoint, nil) @@ -701,14 +709,14 @@ func getYandexCreds(link string) (string, string, string, error) { UID: uuid.New().String(), Hello: HelloPayload{ ParticipantMeta: PartMeta{ - Name: "Гость", + Name: name, Role: "SPEAKER", Description: "", SendAudio: false, SendVideo: false, }, ParticipantAttributes: PartAttrs{ - Name: "Гость", + Name: name, Role: "SPEAKER", Description: "", }, @@ -836,6 +844,7 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net. } func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c chan<- error) { + time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond) var err error = nil defer func() { c <- err }() dtlsctx, dtlscancel := context.WithCancel(ctx) @@ -970,6 +979,7 @@ type turnParams struct { } func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) { + time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond) var err error = nil defer func() { c <- err }() user, pass, url, err1 := turnParams.getCreds(turnParams.link) @@ -1251,6 +1261,7 @@ func poolCreds(f getCredsFunc, poolSize int) getCredsFunc { } func main() { //nolint:cyclop + rand.Seed(time.Now().UnixNano()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() signalChan := make(chan os.Signal, 1) diff --git a/client/namegen.go b/client/namegen.go new file mode 100644 index 0000000..e9e1804 --- /dev/null +++ b/client/namegen.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "math/rand" +) + +// firstNames contains Russian first names. Add or remove names as needed. +var firstNames = []string{ + "Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья", + "Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", + "Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк", + "Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга", + "Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета", +} + +// lastNames contains Russian last names. Add or remove names as needed. +var lastNames = []string{ + "Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов", + "Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров", + "Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Никитин", + "Захаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев", +} + +// generateName generates a random Russian name. +// 30% chance to generate only first name, 70% chance first + last name. +// For female names (ending in 'а' or 'я'), adds 'а' to the last name. +func generateName() string { + if rand.Float32() < 0.3 { + return firstNames[rand.Intn(len(firstNames))] + } + + fn := firstNames[rand.Intn(len(firstNames))] + ln := lastNames[rand.Intn(len(lastNames))] + + // add 'a' to the last name for females + lastChar := fn[len(fn)-2:] // 2 bytes for cyrillic + if lastChar == "а" || lastChar == "я" { + return fmt.Sprintf("%s %sа", fn, ln) + } + return fmt.Sprintf("%s %s", fn, ln) +} diff --git a/client/profiles.go b/client/profiles.go new file mode 100644 index 0000000..7df1dc7 --- /dev/null +++ b/client/profiles.go @@ -0,0 +1,55 @@ +package main + +import ( + "math/rand" +) + +type Profile struct { + UserAgent string +} + +// profiles contains realistic user-agent strings for different browsers and platforms. +// Add or remove profiles as needed. +var profiles = []Profile{ + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, + + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, + + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.0.0 Yowser/2.5 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.2.0 Yowser/2.5 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/23.12.0.0 Yowser/2.5 Safari/537.36"}, + + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/112.0.0.0"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 OPR/111.0.0.0"}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, + + {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, + + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"}, +} + +// getRandomProfile returns a random user-agent profile. +func getRandomProfile() Profile { + return profiles[rand.Intn(len(profiles))] +}