Browse Source

Merge pull request #99 from alexmac6574/captcha

feat: Implement automatic Proof-of-Work (PoW) captcha solver
pull/108/head v1.3.0
cacggghp 2 months ago
committed by GitHub
parent
commit
2b1afe4496
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 590
      client/main.go
  2. 42
      client/namegen.go
  3. 55
      client/profiles.go

590
client/main.go

@ -6,37 +6,378 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"github.com/bschaatsbergen/dnsdialer"
"github.com/cbeuw/connutil"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/pion/dtls/v3"
"github.com/pion/dtls/v3/pkg/crypto/selfsign"
"github.com/pion/logging"
"github.com/pion/turn/v5"
"io"
"log"
"math/rand"
"net"
"net/http"
neturl "net/url"
"os"
"os/signal"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/bschaatsbergen/dnsdialer"
"github.com/cbeuw/connutil"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/pion/dtls/v3"
"github.com/pion/dtls/v3/pkg/crypto/selfsign"
"github.com/pion/logging"
"github.com/pion/transport/v4"
"github.com/pion/turn/v5"
)
type getCredsFunc func(string) (string, string, string, error)
type directNet struct{}
type directDialer struct {
*net.Dialer
}
type directListenConfig struct {
*net.ListenConfig
}
// globalClientWGAddr safely stores the UDP address of the local WireGuard client
var globalClientWGAddr atomic.Value
func newDirectNet() transport.Net {
return directNet{}
}
func (directNet) ListenPacket(network string, address string) (net.PacketConn, error) {
return net.ListenPacket(network, address) //nolint:noctx
}
func (directNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
return net.ListenUDP(network, locAddr)
}
func (directNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) {
listener, err := net.ListenTCP(network, laddr)
if err != nil {
return nil, err
}
return directTCPListener{listener}, nil
}
func (directNet) Dial(network, address string) (net.Conn, error) {
return net.Dial(network, address) //nolint:noctx
}
func (directNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
return net.DialUDP(network, laddr, raddr)
}
func (directNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) {
return net.DialTCP(network, laddr, raddr)
}
func (directNet) ResolveIPAddr(network, address string) (*net.IPAddr, error) {
return net.ResolveIPAddr(network, address)
}
func (directNet) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) {
return net.ResolveUDPAddr(network, address)
}
func (directNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
return net.ResolveTCPAddr(network, address)
}
func (directNet) Interfaces() ([]*transport.Interface, error) {
return nil, transport.ErrNotSupported
}
func (directNet) InterfaceByIndex(index int) (*transport.Interface, error) {
return nil, fmt.Errorf("%w: index=%d", transport.ErrInterfaceNotFound, index)
}
func (directNet) InterfaceByName(name string) (*transport.Interface, error) {
return nil, fmt.Errorf("%w: %s", transport.ErrInterfaceNotFound, name)
}
func (directNet) CreateDialer(dialer *net.Dialer) transport.Dialer {
return directDialer{Dialer: dialer}
}
func (directNet) CreateListenConfig(listenerConfig *net.ListenConfig) transport.ListenConfig {
return directListenConfig{ListenConfig: listenerConfig}
}
func (d directDialer) Dial(network, address string) (net.Conn, error) {
return d.Dialer.Dial(network, address)
}
func (d directListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) {
return d.ListenConfig.Listen(ctx, network, address)
}
func (d directListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
return d.ListenConfig.ListenPacket(ctx, network, address)
}
type directTCPListener struct {
*net.TCPListener
}
func (l directTCPListener) AcceptTCP() (transport.TCPConn, error) {
return l.TCPListener.AcceptTCP()
}
// region automatic captcha solver
type vkCaptchaError struct {
ErrorCode int
ErrorMsg string
CaptchaSid string
RedirectUri string
SessionToken string
CaptchaTs string
CaptchaAttempt string
}
func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError {
codeFloat, _ := errData["error_code"].(float64)
redirectUri, _ := errData["redirect_uri"].(string)
errorMsg, _ := errData["error_msg"].(string)
captchaSid, _ := errData["captcha_sid"].(string)
if captchaSid == "" {
if sidNum, ok := errData["captcha_sid"].(float64); ok {
captchaSid = fmt.Sprintf("%.0f", sidNum)
}
}
var sessionToken string
if redirectUri != "" {
if parsed, err := neturl.Parse(redirectUri); err == nil {
sessionToken = parsed.Query().Get("session_token")
}
}
var captchaTs string
if tsFloat, ok := errData["captcha_ts"].(float64); ok {
captchaTs = fmt.Sprintf("%.0f", tsFloat)
} else if tsStr, ok := errData["captcha_ts"].(string); ok {
captchaTs = tsStr
}
var captchaAttempt string
if attFloat, ok := errData["captcha_attempt"].(float64); ok {
captchaAttempt = fmt.Sprintf("%.0f", attFloat)
} else if attStr, ok := errData["captcha_attempt"].(string); ok {
captchaAttempt = attStr
}
return &vkCaptchaError{
ErrorCode: int(codeFloat),
ErrorMsg: errorMsg,
CaptchaSid: captchaSid,
RedirectUri: redirectUri,
SessionToken: sessionToken,
CaptchaTs: captchaTs,
CaptchaAttempt: captchaAttempt,
}
}
func solveVkCaptcha(ctx context.Context, captchaErr *vkCaptchaError, dialer *dnsdialer.Dialer) (string, error) {
log.Printf("Solving VK Smart Captcha automatically...")
if captchaErr.SessionToken == "" {
return "", fmt.Errorf("no session_token in redirect_uri")
}
powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer)
if err != nil {
return "", fmt.Errorf("failed to fetch PoW input: %w", err)
}
hash := solvePoW(powInput, difficulty)
successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, dialer)
if err != nil {
return "", fmt.Errorf("captchaNotRobot API failed: %w", err)
}
log.Printf("VK Smart Captcha Solved Successfully!")
return successToken, nil
}
func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer) (string, int, error) {
req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil)
if err != nil {
return "", 0, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
DialContext: dialer.DialContext,
},
}
resp, err := client.Do(req)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", 0, err
}
html := string(body)
powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
powInputMatch := powInputRe.FindStringSubmatch(html)
if len(powInputMatch) < 2 {
return "", 0, fmt.Errorf("powInput not found in captcha HTML")
}
powInput := powInputMatch[1]
diffRe := regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`)
diffMatch := diffRe.FindStringSubmatch(html)
difficulty := 2
if len(diffMatch) >= 2 {
if d, err := strconv.Atoi(diffMatch[1]); err == nil {
difficulty = d
}
}
return powInput, difficulty, nil
}
func solvePoW(powInput string, difficulty int) string {
target := strings.Repeat("0", difficulty)
for nonce := 1; nonce <= 10000000; nonce++ {
data := powInput + strconv.Itoa(nonce)
hash := sha256.Sum256([]byte(data))
hexHash := hex.EncodeToString(hash[:])
if strings.HasPrefix(hexHash, target) {
return hexHash
}
}
return ""
}
func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer *dnsdialer.Dialer) (string, error) {
vkReq := func(method string, postData string) (map[string]interface{}, error) {
reqURL := "https://api.vk.ru/method/" + method + "?v=5.131"
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData))
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Origin", "https://vk.ru")
req.Header.Set("Referer", "https://vk.ru/")
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
DialContext: dialer.DialContext,
},
}
httpResp, err := client.Do(req)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
var resp map[string]interface{}
if err := json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return resp, nil
}
baseParams := fmt.Sprintf("session_token=%s&domain=vk.com&adFp=&access_token=", neturl.QueryEscape(sessionToken))
// Step 1: settings
if _, err := vkReq("captchaNotRobot.settings", baseParams); err != nil {
return "", fmt.Errorf("settings failed: %w", err)
}
time.Sleep(200 * time.Millisecond)
// Step 2: componentDone
browserFp := fmt.Sprintf("%032x", rand.Int63())
deviceJSON := `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1920,"innerHeight":945,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":16,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"denied"}`
componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON))
if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil {
return "", fmt.Errorf("componentDone failed: %w", err)
}
time.Sleep(200 * time.Millisecond)
// Step 3: check
cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]`
answer := base64.StdEncoding.EncodeToString([]byte("{}"))
debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785"
checkData := baseParams + fmt.Sprintf(
"&accelerometer=%s&gyroscope=%s&motion=%s&cursor=%s&taps=%s&connectionRtt=%s&connectionDownlink=%s&browser_fp=%s&hash=%s&answer=%s&debug_info=%s",
neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"),
neturl.QueryEscape(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"),
neturl.QueryEscape("[9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5]"),
browserFp, hash, answer, debugInfo,
)
checkResp, err := vkReq("captchaNotRobot.check", checkData)
if err != nil {
return "", fmt.Errorf("check failed: %w", err)
}
respObj, ok := checkResp["response"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid check response: %v", checkResp)
}
status, _ := respObj["status"].(string)
if status != "OK" {
return "", fmt.Errorf("check status: %s", status)
}
successToken, ok := respObj["success_token"].(string)
if !ok || successToken == "" {
return "", fmt.Errorf("success_token not found")
}
time.Sleep(200 * time.Millisecond)
// Step 4: endSession
vkReq("captchaNotRobot.endSession", baseParams)
return successToken, nil
}
// endregion automatic captcha solver
func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, error) {
profile := getRandomProfile()
name := generateName()
escapedName := neturl.QueryEscape(name)
doRequest := func(data string, url string) (resp map[string]interface{}, err error) {
log.Printf("Connecting Identity - Name: %s | User-Agent: %s", name, profile.UserAgent)
doRequest := func(data string, url string) (resp map[string]interface{}, err error) {
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
@ -47,12 +388,13 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
},
}
defer client.CloseIdleConnections()
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0")
req.Header.Add("User-Agent", profile.UserAgent)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
httpResp, err := client.Do(req)
@ -93,17 +435,65 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
return "", "", "", fmt.Errorf("request error:%s", err)
}
token1 := resp["data"].(map[string]interface{})["access_token"].(string)
dataMap, ok := resp["data"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("unexpected anon token response: %v", resp)
}
token1, ok := dataMap["access_token"].(string)
if !ok {
return "", "", "", fmt.Errorf("missing access_token in response: %v", resp)
}
data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=123&access_token=%s", link, token1)
data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s", link, escapedName, token1)
url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487"
resp, err = doRequest(data, url)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
}
var token2 string
const maxCaptchaAttempts = 3
for attempt := 0; attempt <= maxCaptchaAttempts; attempt++ {
resp, err = doRequest(data, url)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
}
// Check for captcha error
if errObj, hasErr := resp["error"].(map[string]interface{}); hasErr {
errCode, _ := errObj["error_code"].(float64)
if errCode == 14 {
if attempt == maxCaptchaAttempts {
return "", "", "", fmt.Errorf("captcha failed after %d attempts", maxCaptchaAttempts)
}
captchaErr := parseVkCaptchaError(errObj)
if captchaErr.SessionToken != "" {
successToken, solveErr := solveVkCaptcha(context.Background(), captchaErr, dialer)
if solveErr != nil {
return "", "", "", fmt.Errorf("auto captcha solve error: %w", solveErr)
}
if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" {
captchaErr.CaptchaAttempt = "1"
}
token2 := resp["response"].(map[string]interface{})["token"].(string)
data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s&captcha_key=&captcha_sid=%s&is_sound_captcha=0&success_token=%s&captcha_ts=%s&captcha_attempt=%s",
link, escapedName, token1, captchaErr.CaptchaSid, neturl.QueryEscape(successToken), captchaErr.CaptchaTs, captchaErr.CaptchaAttempt)
continue
} else {
return "", "", "", fmt.Errorf("old image captcha detected - not supported in auto solver")
}
}
return "", "", "", fmt.Errorf("VK API error: %v", errObj)
}
respMap, ok := resp["response"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("unexpected getAnonymousToken response: %v", resp)
}
token2, ok = respMap["token"].(string)
if !ok {
return "", "", "", fmt.Errorf("missing token in response: %v", resp)
}
break
}
data = fmt.Sprintf("%s%s%s", "session_data=%7B%22version%22%3A2%2C%22device_id%22%3A%22", uuid.New(), "%22%2C%22client_version%22%3A1.1%2C%22client_type%22%3A%22SDK_JS%22%7D&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA")
url = "https://calls.okcdn.ru/fb.do"
@ -137,7 +527,9 @@ func getYandexCreds(link string) (string, string, string, error) {
const debug = false
const telemostConfHost = "cloud-api.yandex.ru"
telemostConfPath := fmt.Sprintf("%s%s%s", "/telemost_front/v2/telemost/conferences/https%3A%2F%2Ftelemost.yandex.ru%2Fj%2F", link, "/connection?next_gen_media_platform_allowed=false")
const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
profile := getRandomProfile()
userAgent := profile.UserAgent
name := generateName()
type ConferenceResponse struct {
URI string `json:"uri"`
@ -251,13 +643,14 @@ func getYandexCreds(link string) (string, string, string, error) {
}
endpoint := "https://" + telemostConfHost + telemostConfPath
tr := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
}
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
Timeout: 20 * time.Second,
Transport: tr,
}
defer client.CloseIdleConnections()
req, err := http.NewRequest("GET", endpoint, nil)
@ -316,14 +709,14 @@ func getYandexCreds(link string) (string, string, string, error) {
UID: uuid.New().String(),
Hello: HelloPayload{
ParticipantMeta: PartMeta{
Name: "Гость",
Name: name,
Role: "SPEAKER",
Description: "",
SendAudio: false,
SendVideo: false,
},
ParticipantAttributes: PartAttrs{
Name: "Гость",
Name: name,
Role: "SPEAKER",
Description: "",
},
@ -451,6 +844,7 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
}
func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c chan<- error) {
time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
var err error = nil
defer func() { c <- err }()
dtlsctx, dtlscancel := context.WithCancel(ctx)
@ -479,15 +873,16 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
log.Printf("Closed DTLS connection\n")
}()
log.Printf("Established DTLS connection!\n")
go func() {
for {
// Trigger the okchan safely to spawn the rest of the threads
if okchan != nil {
go func() {
select {
case <-dtlsctx.Done():
return
case okchan <- struct{}{}:
case <-dtlsctx.Done():
}
}
}()
}()
}
wg := sync.WaitGroup{}
wg.Add(2)
@ -499,7 +894,7 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
log.Printf("Failed to set DTLS deadline: %s", err)
}
})
var addr atomic.Value
// Start read-loop on listenConn
go func() {
defer wg.Done()
@ -517,7 +912,7 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
return
}
addr.Store(addr1) // store peer
globalClientWGAddr.Store(addr1) // store local WG peer address globally
_, err1 = dtlsConn.Write(buf[:n])
if err1 != nil {
@ -543,10 +938,11 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
log.Printf("Failed: %s", err1)
return
}
addr1, ok := addr.Load().(net.Addr)
addr1, ok := globalClientWGAddr.Load().(net.Addr)
if !ok {
log.Printf("Failed: no listener ip")
return
// Safely drop packet if wireguard hasn't sent an initial packet yet
continue
}
_, err1 = listenConn.WriteTo(buf[:n], addr1)
@ -583,6 +979,7 @@ type turnParams struct {
}
func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) {
time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
var err error = nil
defer func() { c <- err }()
user, pass, url, err1 := turnParams.getCreds(turnParams.link)
@ -655,6 +1052,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
STUNServerAddr: turnServerAddr,
TURNServerAddr: turnServerAddr,
Conn: turnConn,
Net: newDirectNet(),
Username: user,
Password: pass,
RequestedAddressFamily: addrFamily,
@ -704,7 +1102,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
log.Printf("Failed to set upstream deadline: %s", err)
}
})
var addr atomic.Value
var internalPipeAddr atomic.Value
// Start read-loop on conn2 (output of DTLS)
go func() {
defer wg.Done()
@ -722,7 +1120,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
return
}
addr.Store(addr1) // store peer
internalPipeAddr.Store(addr1) // store local async pipe peer
_, err1 = relayConn.WriteTo(buf[:n], peer)
if err1 != nil {
@ -748,7 +1146,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
log.Printf("Failed: %s", err1)
return
}
addr1, ok := addr.Load().(net.Addr)
addr1, ok := internalPipeAddr.Load().(net.Addr)
if !ok {
log.Printf("Failed: no listener ip")
return
@ -792,20 +1190,78 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne
case <-ctx.Done():
return
case conn2 := <-connchan:
// Ensure we block cleanly until the tick signals to proceed
select {
case <-t:
c := make(chan error)
go oneTurnConnection(ctx, turnParams, peer, conn2, c)
if err := <-c; err != nil {
log.Printf("%s", err)
case <-ctx.Done():
return
}
c := make(chan error)
go oneTurnConnection(ctx, turnParams, peer, conn2, c)
if err := <-c; err != nil {
log.Printf("%s", err)
}
}
}
}
type turnCred struct {
user, pass, addr string
}
// poolCreds allows retrieving unique TURN credentials for N distinct connections.
// Because it natively handles the automatic captcha bypass, every request gets a unique identity safely.
func poolCreds(f getCredsFunc, poolSize int) getCredsFunc {
var mu sync.Mutex
var pool []turnCred
var cTime time.Time
var idx int
return func(link string) (string, string, string, error) {
mu.Lock()
defer mu.Unlock()
// Refresh identities every 10 minutes
if !cTime.IsZero() && time.Since(cTime) > 10*time.Minute {
pool = nil
cTime = time.Time{}
}
if len(pool) < poolSize {
u, p, a, err := f(link)
if err == nil {
pool = append(pool, turnCred{u, p, a})
cTime = time.Now()
log.Printf("Successfully registered User Identity %d/%d", len(pool), poolSize)
// Space out requests by 1000ms to avoid API limits
if len(pool) < poolSize {
time.Sleep(1000 * time.Millisecond)
}
default:
c := pool[len(pool)-1]
idx++
return c.user, c.pass, c.addr, nil
}
log.Printf("Failed to get unique TURN identity: %v", err)
if len(pool) > 0 {
log.Printf("Falling back to reusing a previous identity...")
c := pool[idx%len(pool)]
idx++
return c.user, c.pass, c.addr, nil
}
return "", "", "", err
}
c := pool[idx%len(pool)]
idx++
return c.user, c.pass, c.addr, nil
}
}
func main() { //nolint:cyclop
rand.Seed(time.Now().UnixNano())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signalChan := make(chan os.Signal, 1)
@ -827,7 +1283,7 @@ func main() { //nolint:cyclop
vklink := flag.String("vk-link", "", "VK calls invite link \"https://vk.com/call/join/...\"")
yalink := flag.String("yandex-link", "", "Yandex telemost invite link \"https://telemost.yandex.ru/j/...\"")
peerAddr := flag.String("peer", "", "peer server address (host:port)")
n := flag.Int("n", 0, "connections to TURN (default 16 for VK, 1 for Yandex)")
n := flag.Int("n", 0, "connections to TURN (default 10 for VK, 1 for Yandex)")
udp := flag.Bool("udp", false, "connect to TURN with UDP")
direct := flag.Bool("no-dtls", false, "connect without obfuscation. DO NOT USE")
flag.Parse()
@ -858,7 +1314,7 @@ func main() { //nolint:cyclop
return getVkCreds(s, dialer)
}
if *n <= 0 {
*n = 16
*n = 10
}
} else {
parts := strings.Split(*yalink, "j/")
@ -872,11 +1328,11 @@ func main() { //nolint:cyclop
link = link[:idx]
}
params := &turnParams{
*host,
*port,
link,
*udp,
getCreds,
host: *host,
port: *port,
link: link,
udp: *udp,
getCreds: poolCreds(getCreds, *n),
}
listenConnChan := make(chan net.PacketConn)
@ -903,21 +1359,27 @@ func main() { //nolint:cyclop
t := time.Tick(200 * time.Millisecond)
if *direct {
for i := 0; i < *n; i++ {
wg1.Go(func() {
wg1.Add(1)
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t)
})
}()
}
} else {
okchan := make(chan struct{})
connchan := make(chan net.PacketConn)
wg1.Go(func() {
wg1.Add(1)
go func() {
defer wg1.Done()
oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, okchan)
})
}()
wg1.Go(func() {
wg1.Add(1)
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, connchan, t)
})
}()
select {
case <-okchan:
@ -925,12 +1387,16 @@ func main() { //nolint:cyclop
}
for i := 0; i < *n-1; i++ {
connchan := make(chan net.PacketConn)
wg1.Go(func() {
wg1.Add(1)
go func() {
defer wg1.Done()
oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil)
})
wg1.Go(func() {
}()
wg1.Add(1)
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, connchan, t)
})
}()
}
}

42
client/namegen.go

@ -0,0 +1,42 @@
package main
import (
"fmt"
"math/rand"
)
// firstNames contains Russian first names. Add or remove names as needed.
var firstNames = []string{
"Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья",
"Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван",
"Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк",
"Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга",
"Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета",
}
// lastNames contains Russian last names. Add or remove names as needed.
var lastNames = []string{
"Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов",
"Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров",
"Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Никитин",
"Захаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев",
}
// generateName generates a random Russian name.
// 30% chance to generate only first name, 70% chance first + last name.
// For female names (ending in 'а' or 'я'), adds 'а' to the last name.
func generateName() string {
if rand.Float32() < 0.3 {
return firstNames[rand.Intn(len(firstNames))]
}
fn := firstNames[rand.Intn(len(firstNames))]
ln := lastNames[rand.Intn(len(lastNames))]
// add 'a' to the last name for females
lastChar := fn[len(fn)-2:] // 2 bytes for cyrillic
if lastChar == "а" || lastChar == "я" {
return fmt.Sprintf("%s %sа", fn, ln)
}
return fmt.Sprintf("%s %s", fn, ln)
}

55
client/profiles.go

@ -0,0 +1,55 @@
package main
import (
"math/rand"
)
type Profile struct {
UserAgent string
}
// profiles contains realistic user-agent strings for different browsers and platforms.
// Add or remove profiles as needed.
var profiles = []Profile{
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.0.0 Yowser/2.5 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.2.0 Yowser/2.5 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/23.12.0.0 Yowser/2.5 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/112.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 OPR/111.0.0.0"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"},
}
// getRandomProfile returns a random user-agent profile.
func getRandomProfile() Profile {
return profiles[rand.Intn(len(profiles))]
}
Loading…
Cancel
Save