Browse Source

feat: Another attempt to fix auto solving captcha

pull/105/head
alexmac6574 3 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 ( import (
"bytes" "bytes"
"context" "context"
"crypto/md5"
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
@ -18,7 +19,6 @@ import (
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar"
neturl "net/url" neturl "net/url"
"os" "os"
"os/signal" "os/signal"
@ -30,6 +30,10 @@ import (
"syscall" "syscall"
"time" "time"
fhttp "github.com/bogdanfinn/fhttp"
tlsclient "github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
"github.com/bschaatsbergen/dnsdialer" "github.com/bschaatsbergen/dnsdialer"
"github.com/cbeuw/connutil" "github.com/cbeuw/connutil"
"github.com/google/uuid" "github.com/google/uuid"
@ -54,17 +58,20 @@ type directListenConfig struct {
} }
// Global state trackers // Global state trackers
var globalClientWGAddr atomic.Value var (
var globalCaptchaLockout atomic.Int64 globalClientWGAddr atomic.Value
var connectedStreams atomic.Int32 globalCaptchaLockout atomic.Int64
var globalAppCancel context.CancelFunc connectedStreams atomic.Int32
globalAppCancel context.CancelFunc
handshakeSem = make(chan struct{}, 3)
)
func newDirectNet() transport.Net { func newDirectNet() transport.Net {
return directNet{} return directNet{}
} }
func (directNet) ListenPacket(network string, address string) (net.PacketConn, error) { 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) { 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) { 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) { 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") 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 { func generateFakeCursor() string {
startX := 800 + rand.Intn(200) startX := 600 + rand.Intn(400)
startY := 400 + rand.Intn(200) startY := 300 + rand.Intn(200)
startTime := time.Now().UnixMilli() - int64(rand.Intn(2000)+1000)
var points []string var points []string
for i := 0; i < 5+rand.Intn(5); i++ { for i := 0; i < 15+rand.Intn(10); i++ {
startX += rand.Intn(10) - 2 startX += rand.Intn(15) - 5
startY += rand.Intn(10) - 2 startY += rand.Intn(15) + 2
points = append(points, fmt.Sprintf(`{"x":%d,"y":%d}`, startX, startY)) startTime += int64(rand.Intn(40) + 10)
points = append(points, fmt.Sprintf(`{"x":%d,"y":%d,"t":%d}`, startX, startY, startTime))
} }
return "[" + strings.Join(points, ",") + "]" 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 // endregion
// region Automatic Captcha Solver & Authentication // region Automatic Captcha Solver & Authentication
@ -239,7 +286,7 @@ func (e *VkCaptchaError) IsCaptchaError() bool {
return e.ErrorCode == 14 && e.RedirectUri != "" && e.SessionToken != "" 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) log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID)
if captchaErr.SessionToken == "" { 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") 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 { if err != nil {
return "", fmt.Errorf("failed to fetch PoW input: %w", err) 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) hash := solvePoW(powInput, difficulty)
log.Printf("[STREAM %d] [Captcha] PoW solved: hash=%s", streamID, hash) 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 { if err != nil {
return "", fmt.Errorf("captchaNotRobot API failed: %w", err) return "", fmt.Errorf("captchaNotRobot API failed: %w", err)
} }
@ -268,35 +315,25 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in
return successToken, nil 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) parsedURL, err := neturl.Parse(redirectUri)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
domain := parsedURL.Hostname() domain := parsedURL.Hostname()
req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil) req, err := fhttp.NewRequestWithContext(ctx, "GET", redirectUri, nil)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
req.Host = domain req.Host = domain
applyBrowserProfile(req, profile) applyBrowserProfileFhttp(req, profile)
req.Header.Set("Sec-Fetch-Site", "none") req.Header.Set("Sec-Fetch-Site", "none")
req.Header.Set("Sec-Fetch-Mode", "navigate") req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Dest", "document") req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 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) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
@ -342,19 +379,19 @@ func solvePoW(powInput string, difficulty int) string {
return "" 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) { vkReq := func(method string, postData string) (map[string]interface{}, error) {
reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" reqURL := "https://api.vk.ru/method/" + method + "?v=5.131"
parsedURL, _ := neturl.Parse(reqURL) parsedURL, _ := neturl.Parse(reqURL)
domain := parsedURL.Hostname() 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 { if err != nil {
return nil, err return nil, err
} }
req.Host = domain req.Host = domain
applyBrowserProfile(req, profile) applyBrowserProfileFhttp(req, profile)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://id.vk.ru") 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("Sec-GPC", "1")
req.Header.Set("Priority", "u=1, i") 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) httpResp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -405,8 +431,8 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID) log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID)
browserFp := fmt.Sprintf("%032x", rand.Int63()) browserFp := generateBrowserFp(profile)
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"}` 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)) componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON))
if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil { 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) log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID)
cursorJSON := generateFakeCursor() cursorJSON := generateFakeCursor()
answer := base64.StdEncoding.EncodeToString([]byte("{}")) 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]" 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( 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", "&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("[]"), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"),
neturl.QueryEscape(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), neturl.QueryEscape(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape(connectionRtt),
neturl.QueryEscape(connectionDownlink), neturl.QueryEscape(connectionDownlink),
browserFp, hash, answer, debugInfo, browserFp, hash, answer, debugInfo,
) )
@ -503,7 +533,10 @@ func getCacheID(streamID int) int {
return streamID / streamsPerCache return streamID / streamsPerCache
} }
var vkRequestMu sync.Mutex var (
vkRequestMu sync.Mutex
globalLastVkFetchTime time.Time
)
func vkDelayRandom(minMs, maxMs int) { func vkDelayRandom(minMs, maxMs int) {
ms := minMs + rand.Intn(maxMs-minMs+1) 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) { func fetchVkCredsSerialized(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
vkRequestMu.Lock() vkRequestMu.Lock()
defer vkRequestMu.Unlock() 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) return fetchVkCreds(ctx, link, streamID, dialer)
} }
@ -648,7 +700,7 @@ func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdia
} }
var lastErr error var lastErr error
jar, _ := cookiejar.New(nil) jar := tlsclient.NewCookieJar()
for _, creds := range vkCredentialsList { for _, creds := range vkCredentialsList {
log.Printf("[STREAM %d] [VK Auth] Trying credentials: client_id=%s", streamID, creds.ClientID) 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) 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) { func getTokenChain(ctx context.Context, link string, streamID int, creds VKCredentials, dialer *dnsdialer.Dialer, jar tlsclient.CookieJar) (string, string, string, error) {
profile := getRandomProfile() 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() name := generateName()
escapedName := neturl.QueryEscape(name) escapedName := neturl.QueryEscape(name)
@ -687,27 +755,13 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
parsedURL, _ := neturl.Parse(url) parsedURL, _ := neturl.Parse(url)
domain := parsedURL.Hostname() domain := parsedURL.Hostname()
client := &http.Client{ req, err := fhttp.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data)))
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)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Host = domain req.Host = domain
applyBrowserProfile(req, profile) applyBrowserProfileFhttp(req, profile)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://vk.ru") 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 { if attempt < maxAutoAttempts {
// Auto Solve Attempts // Auto Solve Attempts
if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { 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 { if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr) 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}, CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
ConnectionIDGenerator: dtls.OnlySendCIDGenerator(), 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() defer cancel()
dtlsConn, err := dtls.Client(conn, peer, config) dtlsConn, err := dtls.Client(conn, peer, config)
if err != nil { 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) ctx1, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
if turnParams.udp { if turnParams.udp {
conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr) // nolint: noctx conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr)
if err2 != nil { if err2 != nil {
err = fmt.Errorf("failed to connect to TURN server: %s", err2) err = fmt.Errorf("failed to connect to TURN server: %s", err2)
return return
@ -1427,7 +1489,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}() }()
turnConn = &connectedUDPConn{conn} turnConn = &connectedUDPConn{conn}
} else { } else {
conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) // nolint: noctx conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr)
if err2 != nil { if err2 != nil {
err = fmt.Errorf("failed to connect to TURN server: %s", err2) err = fmt.Errorf("failed to connect to TURN server: %s", err2)
return 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") { if time.Now().Unix() < globalCaptchaLockout.Load() && strings.Contains(err.Error(), "context deadline exceeded") {
continue 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) listenConnChan := make(chan net.PacketConn)
listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx listenConn, err := net.ListenPacket("udp", *listen)
if err != nil { if err != nil {
log.Panicf("Failed to listen: %s", err) log.Panicf("Failed to listen: %s", err)
} }

189
client/namegen.go

@ -3,40 +3,185 @@ package main
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
) )
// firstNames contains Russian first names. Add or remove names as needed. var maleFirstNames = []string{
var firstNames = []string{ "Александр",
"Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья", "Алексей",
"Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", "Андрей",
"Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк", "Антон",
"Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга", "Арсений",
"Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета", "Артур",
"Артём",
"Богдан",
"Валерий",
"Василий",
"Виктор",
"Владислав",
"Глеб",
"Григорий",
"Даниил",
"Денис",
"Дмитрий",
"Евгений",
"Егор",
"Иван",
"Игорь",
"Илья",
"Кирилл",
"Леонид",
"Максим",
"Марк",
"Матвей",
"Михаил",
"Никита",
"Николай",
"Олег",
"Павел",
"Пётр",
"Роман",
"Руслан",
"Сергей",
"Станислав",
"Тимофей",
"Фёдор",
}
var femaleFirstNames = []string{
"Алина",
"Алёна",
"Анастасия",
"Ангелина",
"Анна",
"Вера",
"Вероника",
"Виктория",
"Дарья",
"Ева",
"Екатерина",
"Елена",
"Елизавета",
"Ирина",
"Кира",
"Кристина",
"Ксения",
"Любовь",
"Маргарита",
"Марина",
"Мария",
"Милана",
"Надежда",
"Наталья",
"Ольга",
"Полина",
"Светлана",
"София",
"Татьяна",
"Юлия",
"Яна",
} }
// lastNames contains Russian last names. Add or remove names as needed.
var lastNames = []string{ 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 { 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 { 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))] ln := lastNames[rand.Intn(len(lastNames))]
if isFemale {
// add 'a' to the last name for females ln = convertToFemaleSurname(ln)
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) 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. // profiles contain paired User-Agent and Client Hints strings to harden bot detection.
var profiles = []Profile{ var profile = []Profile{
// Windows Chrome // 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", 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. // getRandomProfile returns a paired User-Agent and Client Hints profile.
func getRandomProfile() 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 go 1.25.5
require ( 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/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45
github.com/cbeuw/connutil v1.0.1 github.com/cbeuw/connutil v1.0.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@ -14,15 +16,25 @@ require (
) )
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/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/miekg/dns v1.1.69 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.1.1 // 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 github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.47.0 // 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/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/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 h1:0b2i5TvZm8FVcuHP1288k+DEu1XM26DtRjcidOxpGXs=
github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU=
github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= 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/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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 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 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 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 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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 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/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= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=

Loading…
Cancel
Save