Initial v1.1.8 Commits

This commit is contained in:
amurcanov
2026-05-23 22:18:08 +03:00
commit ac86caaf8b
76 changed files with 15693 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
package main
import (
"context"
"fmt"
"log"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"time"
)
var groupAuthMutex sync.Mutex
const (
workersPerGroup = 9
defaultCycleSecs = 36000
)
// WorkerGroup:
// Запускает 9 потоков с одними кредами. Ротации нет — работает до смерти воркеров.
func WorkerGroup(
ctx context.Context,
groupID int,
hashIndex int,
tp *TurnParams,
peer *net.UDPAddr,
d *Dispatcher,
localPort string,
getConfig bool,
configCh chan<- string,
workerIDs []int,
cycleDuration time.Duration,
pauseFlag *int32,
deviceID, password string,
stats *Stats,
waitReady <-chan struct{},
signalReady chan<- struct{},
) {
// Каскадный запуск: ждем свою очередь
if waitReady != nil {
log.Printf("[ГРУППА #%d] Ожидание сигнала от предыдущей группы...", groupID)
select {
case <-waitReady:
case <-ctx.Done():
return
}
}
var configSent int32
if !getConfig {
configSent = 1
}
// Doze-mode пауза
for atomic.LoadInt32(pauseFlag) != 0 {
if ctx.Err() != nil {
return
}
time.Sleep(1 * time.Second)
}
hash := tp.Hashes[hashIndex%len(tp.Hashes)]
shortHash := hash
if len(shortHash) > 8 {
shortHash = shortHash[:8]
}
log.Printf("[ГРУППА #%d] Запрос кредов (хеш: %s...)", groupID, shortHash)
credStreamID := groupID * 100
user, pass, turnURLs, err := GetCreds(ctx, hash, credStreamID)
var creds *Credentials
if err == nil {
creds = &Credentials{User: user, Pass: pass, TurnURLs: turnURLs, CacheStreamID: credStreamID}
} else {
log.Printf("[ГРУППА #%d] Ошибка кредов: %v", groupID, err)
return
}
log.Printf("[ГРУППА #%d] Креды OK, TURN: %v, %d воркеров", groupID, creds.TurnURLs, len(workerIDs))
var configRequestInFlight int32
var wg sync.WaitGroup
var credsMu sync.RWMutex
var refreshMu sync.Mutex
var lastCredRefresh atomic.Int64
refreshCreds := func(reason string) bool {
refreshMu.Lock()
defer refreshMu.Unlock()
now := time.Now().Unix()
last := lastCredRefresh.Load()
if last > 0 && now-last < 15 {
log.Printf("[TURN] Креды уже обновлялись %d сек назад, ждём следующий retry (%s)", now-last, reason)
return true
}
getStreamCache(credStreamID).invalidate(credStreamID)
u, p, urls, refreshErr := GetCreds(ctx, hash, credStreamID)
if refreshErr != nil {
log.Printf("[TURN] Не удалось обновить креды после %s: %v", reason, refreshErr)
return false
}
credsMu.Lock()
creds = &Credentials{User: u, Pass: p, TurnURLs: urls, CacheStreamID: credStreamID}
credsMu.Unlock()
lastCredRefresh.Store(time.Now().Unix())
log.Printf("[TURN] Креды обновлены после %s, TURN urls=%d", reason, len(urls))
return true
}
// Сигнализируем следующей группе, что мы успешно запустились (креды получены + 2 сек форы)
if signalReady != nil {
go func() {
time.Sleep(2000 * time.Millisecond)
close(signalReady)
log.Printf("[ГРУППА #%d] Успешный старт! Передача эстафеты следующей группе...", groupID)
}()
}
for i, wid := range workerIDs {
wg.Add(1)
// Stagger: 500мс между воркерами
workerDelay := time.Duration(i) * 500 * time.Millisecond
go func(wid int, delay time.Duration) {
defer wg.Done()
if delay > 0 {
select {
case <-time.After(delay):
case <-ctx.Done():
return
}
}
shouldGetConfig := getConfig
attempt := 0
for {
if ctx.Err() != nil {
return
}
getConf := false
if shouldGetConfig && atomic.LoadInt32(&configSent) == 0 {
getConf = atomic.CompareAndSwapInt32(&configRequestInFlight, 0, 1)
}
var cc chan<- string
if getConf {
cc = configCh
}
credsMu.RLock()
credsSnapshot := *creds
credsSnapshot.TurnURLs = cloneStringSlice(creds.TurnURLs)
credsMu.RUnlock()
configDelivered, sessErr := RunSession(ctx, tp, peer, d, localPort,
getConf, cc, wid, &credsSnapshot, deviceID, password, stats)
if getConf {
if configDelivered {
atomic.StoreInt32(&configSent, 1)
} else {
atomic.StoreInt32(&configRequestInFlight, 0)
}
}
if sessErr != nil {
if ctx.Err() != nil {
return
}
errStr := sessErr.Error()
errStrLower := strings.ToLower(errStr)
turnAllocAttrMissing := strings.Contains(errStrLower, "turn allocate") &&
strings.Contains(errStrLower, "attribute not found")
turnCredRefreshNeeded := turnAllocAttrMissing ||
strings.Contains(errStrLower, "turn allocate auth") ||
strings.Contains(errStrLower, "invalid credential") ||
strings.Contains(errStrLower, "stale nonce") ||
strings.Contains(errStrLower, "allocation mismatch") ||
strings.Contains(errStrLower, "error 508") ||
strings.Contains(errStrLower, "turn квота") ||
strings.Contains(errStrLower, "quota")
if strings.Contains(errStrLower, "rate limit") ||
strings.Contains(errStrLower, "flood control") ||
strings.Contains(errStrLower, "ip mismatch") ||
strings.Contains(errStrLower, "error 29") {
errStr += " (ошибка со стороны ВК)"
}
if strings.Contains(errStr, "хеш мёртв") ||
strings.Contains(errStr, "FATAL_AUTH") {
log.Printf("[ВОРКЕР #%d] Фатальная ошибка: %s", wid, errStr)
return
}
attempt++
if turnAllocAttrMissing {
log.Printf("[ВОРКЕР #%d] [TURN] Allocate вернул неполный ответ, обновляем TURN-креды и повторяем (попытка %d): %s", wid, attempt, errStr)
refreshCreds("TURN Allocate attribute-not-found")
} else if turnCredRefreshNeeded {
log.Printf("[ВОРКЕР #%d] [TURN] Ошибка allocation/кредов, обновляем TURN-креды и повторяем (попытка %d): %s", wid, attempt, errStr)
refreshCreds("TURN allocation error")
} else {
log.Printf("[ВОРКЕР #%d] Ошибка (попытка %d): %s", wid, attempt, errStr)
}
// Если ошибка STUN (credentials invalid), воркер не сможет переподключиться. Завершаем.
isStunDeath := strings.Contains(errStrLower, "error 29") ||
strings.Contains(errStrLower, "cannot create socket")
if isStunDeath {
log.Printf("[ВОРКЕР #%d] Невосстановимая TURN/STUN ошибка, завершение: %s", wid, errStr)
return
}
}
if ctx.Err() != nil {
return
}
retryDelay := time.Duration(5+rand.Intn(11)) * time.Second
select {
case <-time.After(retryDelay):
case <-ctx.Done():
return
}
}
}(wid, workerDelay)
}
wg.Wait()
log.Printf("[ГРУППА #%d] Все воркеры группы завершились.", groupID)
}
// ParseHashes — парсит строку хешей
func ParseHashes(raw string) []string {
var result []string
seen := make(map[string]struct{})
for _, h := range strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' || r == ' '
}) {
h = normalizeVKJoinHash(h)
if h != "" {
if _, exists := seen[h]; exists {
continue
}
seen[h] = struct{}{}
result = append(result, h)
}
}
return result
}
func normalizeVKJoinHash(input string) string {
s := strings.Trim(strings.TrimSpace(input), "<>\"'")
if s == "" {
return ""
}
lower := strings.ToLower(s)
if idx := strings.Index(lower, "/call/join/"); idx >= 0 {
s = s[idx+len("/call/join/"):]
} else if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
return ""
}
if idx := strings.IndexAny(s, "?#/"); idx != -1 {
s = s[:idx]
}
return strings.Trim(strings.TrimSpace(s), "/")
}
// TurnParams — конфигурация TURN
type TurnParams struct {
Host string
Port string
Hashes []string
WrapKey []byte // Password-derived WRAP key (32 bytes), nil = disabled
}
// Credentials — учетные данные TURN
type Credentials struct {
User string
Pass string
TurnURLs []string
CacheStreamID int
}
// Unused import suppressor
var _ = fmt.Sprintf