diff --git a/client/main.go b/client/main.go index 15c27fa..399de87 100644 --- a/client/main.go +++ b/client/main.go @@ -6,6 +6,7 @@ package main import ( "bytes" "context" + "crypto/md5" "crypto/sha256" "crypto/tls" "encoding/base64" @@ -18,7 +19,6 @@ import ( "math/rand" "net" "net/http" - "net/http/cookiejar" neturl "net/url" "os" "os/signal" @@ -30,6 +30,10 @@ import ( "syscall" "time" + fhttp "github.com/bogdanfinn/fhttp" + tlsclient "github.com/bogdanfinn/tls-client" + "github.com/bogdanfinn/tls-client/profiles" + "github.com/bschaatsbergen/dnsdialer" "github.com/cbeuw/connutil" "github.com/google/uuid" @@ -54,17 +58,20 @@ type directListenConfig struct { } // Global state trackers -var globalClientWGAddr atomic.Value -var globalCaptchaLockout atomic.Int64 -var connectedStreams atomic.Int32 -var globalAppCancel context.CancelFunc +var ( + globalClientWGAddr atomic.Value + globalCaptchaLockout atomic.Int64 + connectedStreams atomic.Int32 + globalAppCancel context.CancelFunc + handshakeSem = make(chan struct{}, 3) +) func newDirectNet() transport.Net { return directNet{} } func (directNet) ListenPacket(network string, address string) (net.PacketConn, error) { - return net.ListenPacket(network, address) //nolint:noctx + return net.ListenPacket(network, address) } func (directNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) { @@ -81,7 +88,7 @@ func (directNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPLis } func (directNet) Dial(network, address string) (net.Conn, error) { - return net.Dial(network, address) //nolint:noctx + return net.Dial(network, address) } func (directNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) { @@ -156,18 +163,58 @@ func applyBrowserProfile(req *http.Request, profile Profile) { req.Header.Set("DNT", "1") } +func applyBrowserProfileFhttp(req *fhttp.Request, profile Profile) { + req.Header.Set("User-Agent", profile.UserAgent) + req.Header.Set("sec-ch-ua", profile.SecChUa) + req.Header.Set("sec-ch-ua-mobile", profile.SecChUaMobile) + req.Header.Set("sec-ch-ua-platform", profile.SecChUaPlatform) + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("DNT", "1") +} + +func generateBrowserFp(profile Profile) string { + data := profile.UserAgent + profile.SecChUa + "1920x1080x24" + h := md5.Sum([]byte(data)) + return hex.EncodeToString(h[:]) +} + func generateFakeCursor() string { - startX := 800 + rand.Intn(200) - startY := 400 + rand.Intn(200) + startX := 600 + rand.Intn(400) + startY := 300 + rand.Intn(200) + startTime := time.Now().UnixMilli() - int64(rand.Intn(2000)+1000) 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)) + for i := 0; i < 15+rand.Intn(10); i++ { + startX += rand.Intn(15) - 5 + startY += rand.Intn(15) + 2 + startTime += int64(rand.Intn(40) + 10) + points = append(points, fmt.Sprintf(`{"x":%d,"y":%d,"t":%d}`, startX, startY, startTime)) } return "[" + strings.Join(points, ",") + "]" } +func getCustomNetDialer() net.Dialer { + return net.Dialer{ + Timeout: 20 * time.Second, + KeepAlive: 30 * time.Second, + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + var d net.Dialer + dnsServers := []string{"77.88.8.8:53", "77.88.8.1:53", "8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53", "1.0.0.1:53"} + var lastErr error + for _, dns := range dnsServers { + conn, err := d.DialContext(ctx, "udp", dns) + if err == nil { + return conn, nil + } + lastErr = err + } + return nil, lastErr + }, + }, + } +} + // endregion // region Automatic Captcha Solver & Authentication @@ -239,7 +286,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, jar *cookiejar.Jar, profile Profile) (string, error) { +func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) { log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID) if captchaErr.SessionToken == "" { @@ -249,7 +296,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, jar, profile) + powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, client, profile) if err != nil { return "", fmt.Errorf("failed to fetch PoW input: %w", err) } @@ -259,7 +306,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, jar, profile) + successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, client, profile) if err != nil { return "", fmt.Errorf("captchaNotRobot API failed: %w", err) } @@ -268,35 +315,25 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in return successToken, nil } -func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer, jar *cookiejar.Jar, profile Profile) (string, int, error) { +func fetchPowInput(ctx context.Context, redirectUri string, client tlsclient.HttpClient, profile Profile) (string, int, error) { parsedURL, err := neturl.Parse(redirectUri) if err != nil { return "", 0, err } domain := parsedURL.Hostname() - req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil) + req, err := fhttp.NewRequestWithContext(ctx, "GET", redirectUri, nil) if err != nil { return "", 0, err } req.Host = domain - applyBrowserProfile(req, profile) + applyBrowserProfileFhttp(req, profile) req.Header.Set("Sec-Fetch-Site", "none") req.Header.Set("Sec-Fetch-Mode", "navigate") req.Header.Set("Sec-Fetch-Dest", "document") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - client := &http.Client{ - Timeout: 20 * time.Second, - Jar: jar, - Transport: &http.Transport{ - DialContext: dialer.DialContext, - TLSClientConfig: &tls.Config{ - ServerName: domain, // Force SNI for DPI evasion - }, - }, - } resp, err := client.Do(req) if err != nil { return "", 0, err @@ -342,19 +379,19 @@ func solvePoW(powInput string, difficulty int) string { return "" } -func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, dialer *dnsdialer.Dialer, jar *cookiejar.Jar, profile Profile) (string, error) { +func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, 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) domain := parsedURL.Hostname() - req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData)) + req, err := fhttp.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData)) if err != nil { return nil, err } req.Host = domain - applyBrowserProfile(req, profile) + applyBrowserProfileFhttp(req, profile) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "*/*") req.Header.Set("Origin", "https://id.vk.ru") @@ -365,17 +402,6 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI req.Header.Set("Sec-GPC", "1") req.Header.Set("Priority", "u=1, i") - client := &http.Client{ - Timeout: 20 * time.Second, - Jar: jar, - Transport: &http.Transport{ - DialContext: dialer.DialContext, - TLSClientConfig: &tls.Config{ - ServerName: domain, // Enforce SNI for DPI evasion - }, - }, - } - httpResp, err := client.Do(req) if err != nil { return nil, err @@ -405,8 +431,8 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI time.Sleep(200 * time.Millisecond) log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID) - 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"}` + browserFp := generateBrowserFp(profile) + deviceJSON := fmt.Sprintf(`{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1040,"innerWidth":1920,"innerHeight":969,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"default","userAgent":"%s","platform":"Win32"}`, profile.UserAgent) componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON)) if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil { @@ -418,14 +444,18 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID) cursorJSON := generateFakeCursor() answer := base64.StdEncoding.EncodeToString([]byte("{}")) - debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785" + // Dynamically generate debug_info to avoid static fingerprint bans + debugInfoBytes := md5.Sum([]byte(profile.UserAgent + strconv.FormatInt(time.Now().UnixNano(), 10))) + debugInfo := hex.EncodeToString(debugInfoBytes[:]) + + connectionRtt := "[50,50,50,50,50,50,50,50,50,50]" connectionDownlink := "[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]" 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(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape(connectionRtt), neturl.QueryEscape(connectionDownlink), browserFp, hash, answer, debugInfo, ) @@ -503,7 +533,10 @@ func getCacheID(streamID int) int { return streamID / streamsPerCache } -var vkRequestMu sync.Mutex +var ( + vkRequestMu sync.Mutex + globalLastVkFetchTime time.Time +) func vkDelayRandom(minMs, maxMs int) { ms := minMs + rand.Intn(maxMs-minMs+1) @@ -638,6 +671,25 @@ func getVkCredsCached(ctx context.Context, link string, streamID int, dialer *dn func fetchVkCredsSerialized(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) { vkRequestMu.Lock() defer vkRequestMu.Unlock() + + // Ensure a minimum cooldown between credential requests to avoid VK rate limits + minInterval := 10*time.Second + time.Duration(rand.Intn(30000))*time.Millisecond + elapsed := time.Since(globalLastVkFetchTime) + + if !globalLastVkFetchTime.IsZero() && elapsed < minInterval { + wait := minInterval - elapsed + log.Printf("[STREAM %d] [VK Auth] Throttling: waiting %v to prevent rate limit...", streamID, wait.Truncate(time.Millisecond)) + select { + case <-ctx.Done(): + return "", "", "", ctx.Err() + case <-time.After(wait): + } + } + + defer func() { + globalLastVkFetchTime = time.Now() + }() + return fetchVkCreds(ctx, link, streamID, dialer) } @@ -648,7 +700,7 @@ func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdia } var lastErr error - jar, _ := cookiejar.New(nil) + jar := tlsclient.NewCookieJar() for _, creds := range vkCredentialsList { log.Printf("[STREAM %d] [VK Auth] Trying credentials: client_id=%s", streamID, creds.ClientID) @@ -676,8 +728,24 @@ 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, jar *cookiejar.Jar) (string, string, string, error) { - profile := getRandomProfile() +func getTokenChain(ctx context.Context, link string, streamID int, creds VKCredentials, dialer *dnsdialer.Dialer, jar tlsclient.CookieJar) (string, string, string, error) { + profile := Profile{ + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + SecChUa: `"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + } + + client, err := tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), + tlsclient.WithTimeoutSeconds(20), + tlsclient.WithClientProfile(profiles.Chrome_120), + tlsclient.WithCookieJar(jar), + tlsclient.WithDialer(getCustomNetDialer()), + ) + if err != nil { + return "", "", "", fmt.Errorf("failed to initialize tls_client: %w", err) + } + name := generateName() escapedName := neturl.QueryEscape(name) @@ -687,27 +755,13 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede parsedURL, _ := neturl.Parse(url) domain := parsedURL.Hostname() - client := &http.Client{ - Timeout: 20 * time.Second, - Jar: jar, - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - DialContext: dialer.DialContext, - TLSClientConfig: &tls.Config{ - ServerName: domain, // Force SNI for DPI evasion - }, - }, - } - - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data))) + req, err := fhttp.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data))) if err != nil { return nil, err } req.Host = domain - applyBrowserProfile(req, profile) + applyBrowserProfileFhttp(req, profile) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "*/*") req.Header.Set("Origin", "https://vk.ru") @@ -784,7 +838,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede if attempt < maxAutoAttempts { // Auto Solve Attempts if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { - successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, dialer, jar, profile) + successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile) if solveErr != nil { log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr) } @@ -1235,7 +1289,15 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net. CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, ConnectionIDGenerator: dtls.OnlySendCIDGenerator(), } - ctx1, cancel := context.WithTimeout(ctx, 30*time.Second) + + select { + case handshakeSem <- struct{}{}: + defer func() { <-handshakeSem }() + case <-ctx.Done(): + return nil, ctx.Err() + } + + ctx1, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() dtlsConn, err := dtls.Client(conn, peer, config) if err != nil { @@ -1414,7 +1476,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD ctx1, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if turnParams.udp { - conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr) // nolint: noctx + conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr) if err2 != nil { err = fmt.Errorf("failed to connect to TURN server: %s", err2) return @@ -1427,7 +1489,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD }() turnConn = &connectedUDPConn{conn} } else { - conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) // nolint: noctx + conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) if err2 != nil { err = fmt.Errorf("failed to connect to TURN server: %s", err2) return @@ -1584,7 +1646,13 @@ func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnCha if time.Now().Unix() < globalCaptchaLockout.Load() && strings.Contains(err.Error(), "context deadline exceeded") { continue } - log.Printf("%s", err) + log.Printf("[DTLS] Handshake failed, retrying in background: %v", err) + + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(10+rand.Intn(20)) * time.Second): + } } } } @@ -1722,7 +1790,7 @@ func main() { } listenConnChan := make(chan net.PacketConn) - listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx + listenConn, err := net.ListenPacket("udp", *listen) if err != nil { log.Panicf("Failed to listen: %s", err) } diff --git a/client/namegen.go b/client/namegen.go index e9e1804..0593c9d 100644 --- a/client/namegen.go +++ b/client/namegen.go @@ -3,40 +3,185 @@ package main import ( "fmt" "math/rand" + "strings" ) -// firstNames contains Russian first names. Add or remove names as needed. -var firstNames = []string{ - "Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья", - "Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", - "Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк", - "Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга", - "Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета", +var maleFirstNames = []string{ + "Александр", + "Алексей", + "Андрей", + "Антон", + "Арсений", + "Артур", + "Артём", + "Богдан", + "Валерий", + "Василий", + "Виктор", + "Владислав", + "Глеб", + "Григорий", + "Даниил", + "Денис", + "Дмитрий", + "Евгений", + "Егор", + "Иван", + "Игорь", + "Илья", + "Кирилл", + "Леонид", + "Максим", + "Марк", + "Матвей", + "Михаил", + "Никита", + "Николай", + "Олег", + "Павел", + "Пётр", + "Роман", + "Руслан", + "Сергей", + "Станислав", + "Тимофей", + "Фёдор", +} + +var femaleFirstNames = []string{ + "Алина", + "Алёна", + "Анастасия", + "Ангелина", + "Анна", + "Вера", + "Вероника", + "Виктория", + "Дарья", + "Ева", + "Екатерина", + "Елена", + "Елизавета", + "Ирина", + "Кира", + "Кристина", + "Ксения", + "Любовь", + "Маргарита", + "Марина", + "Мария", + "Милана", + "Надежда", + "Наталья", + "Ольга", + "Полина", + "Светлана", + "София", + "Татьяна", + "Юлия", + "Яна", } -// lastNames contains Russian last names. Add or remove names as needed. var lastNames = []string{ - "Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов", - "Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров", - "Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Никитин", - "Захаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев", + "Алексеев", + "Андреев", + "Антонов", + "Баранов", + "Белов", + "Белый", + "Бельский", + "Беляев", + "Борисов", + "Васильев", + "Великий", + "Волков", + "Воробьёв", + "Григорьев", + "Давыдов", + "Егоров", + "Жуков", + "Зайцев", + "Захаров", + "Иванов", + "Калинин", + "Ковалёв", + "Козлов", + "Комаров", + "Крамской", + "Кузнецов", + "Кузьмин", + "Лебедев", + "Макаров", + "Медведев", + "Михайлов", + "Морозов", + "Никитин", + "Николаев", + "Новиков", + "Орлов", + "Островский", + "Павлов", + "Петров", + "Покровский", + "Попов", + "Раевский", + "Романов", + "Семёнов", + "Сергеев", + "Смирнов", + "Соколов", + "Соловьёв", + "Степанов", + "Тарасов", + "Титов", + "Толстой", + "Трубецкой", + "Филиппов", + "Фролов", + "Фёдоров", + "Чайковский", + "Черный", + "Яковлев", +} + +// convertToFemaleSurname handles Russian suffix rules +func convertToFemaleSurname(surname string) string { + // Handle adjective-style surnames: + if strings.HasSuffix(surname, "ий") || strings.HasSuffix(surname, "ый") || strings.HasSuffix(surname, "ой") { + return surname[:len(surname)-4] + "ая" + } + + // Handle standard possessive surnames: + if strings.HasSuffix(surname, "ов") || strings.HasSuffix(surname, "ев") || + strings.HasSuffix(surname, "ин") || strings.HasSuffix(surname, "ын") || + strings.HasSuffix(surname, "ёв") { + return surname + "а" + } + + // Foreign or unchangeable + return surname } -// 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 { + // Decide gender first + isFemale := rand.Intn(2) == 0 + + var fn string + if isFemale { + fn = femaleFirstNames[rand.Intn(len(femaleFirstNames))] + } else { + fn = maleFirstNames[rand.Intn(len(maleFirstNames))] + } + + // 70% chance to have a last name if rand.Float32() < 0.3 { - return firstNames[rand.Intn(len(firstNames))] + return fn } - 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) + if isFemale { + ln = convertToFemaleSurname(ln) } + return fmt.Sprintf("%s %s", fn, ln) } diff --git a/client/profiles.go b/client/profiles.go index 44b4141..01d4f0c 100644 --- a/client/profiles.go +++ b/client/profiles.go @@ -12,7 +12,7 @@ type Profile struct { } // profiles contain paired User-Agent and Client Hints strings to harden bot detection. -var profiles = []Profile{ +var profile = []Profile{ // Windows Chrome { UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", @@ -78,5 +78,5 @@ var profiles = []Profile{ // getRandomProfile returns a paired User-Agent and Client Hints profile. func getRandomProfile() Profile { - return profiles[rand.Intn(len(profiles))] + return profile[rand.Intn(len(profile))] } diff --git a/go.mod b/go.mod index 8a7c99f..34d031c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/cacggghp/vk-turn-proxy go 1.25.5 require ( + github.com/bogdanfinn/fhttp v0.6.8 + github.com/bogdanfinn/tls-client v1.14.0 github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 github.com/cbeuw/connutil v1.0.1 github.com/google/uuid v1.6.0 @@ -14,15 +16,25 @@ require ( ) require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bdandy/go-errors v1.2.2 // indirect + github.com/bdandy/go-socks4 v1.2.3 // indirect + github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect + github.com/bogdanfinn/utls v1.7.7-barnius // indirect + github.com/bogdanfinn/websocket v1.5.5-barnius // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/miekg/dns v1.1.69 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/stun/v3 v3.1.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index bbaf68e..3303358 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,19 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= +github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= +github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= +github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI= +github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4= +github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg= +github.com/bogdanfinn/tls-client v1.14.0 h1:vyk7Cn4BIvLAGVuMfb0tP22OqogfO1lYamquQNEZU1A= +github.com/bogdanfinn/tls-client v1.14.0/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM= +github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU= +github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg= +github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI= +github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 h1:0b2i5TvZm8FVcuHP1288k+DEu1XM26DtRjcidOxpGXs= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU= github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= @@ -12,6 +28,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= @@ -28,26 +46,40 @@ github.com/pion/turn/v5 v5.0.2 h1:GHlDk+fiegz+yibb3ch+tK+iPFokoVWiM+aVJakySqA= github.com/pion/turn/v5 v5.0.2/go.mod h1:cumcsSEF2ytAtDhDwkYgYhv1uJ3AOP7a4pFt0NL/snY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=