Browse Source

feat: Another attempt to fix auto solving captcha

pull/105/head
alexmac6574 2 months ago
parent
commit
78386cc1d8
  1. 214
      client/main.go
  2. 189
      client/namegen.go
  3. 4
      client/profiles.go
  4. 18
      go.mod
  5. 44
      go.sum

214
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)
}

189
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)
}

4
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))]
}

18
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
)

44
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=

Loading…
Cancel
Save