Files
2026-05-26 22:48:52 +03:00

304 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"context"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"strings"
"sync"
"sync/atomic"
"syscall"
)
// CaptchaResultChan — канал для получения токена капчи из внешнего решателя (WebView)
var CaptchaResultChan = make(chan string, 1)
var captchaModeValue atomic.Value
func init() {
captchaModeValue.Store("auto")
}
func normalizeCaptchaMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "auto", "rjs", "wv":
return strings.ToLower(strings.TrimSpace(mode))
default:
return "auto"
}
}
func setCaptchaMode(mode string) string {
normalized := normalizeCaptchaMode(mode)
captchaModeValue.Store(normalized)
return normalized
}
func getCaptchaMode() string {
mode, _ := captchaModeValue.Load().(string)
if mode == "" {
return "auto"
}
return mode
}
// drainCaptchaResult удаляет устаревший результат капчи из канала
func drainCaptchaResult() {
select {
case <-CaptchaResultChan:
default:
}
}
func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
setupGlobalResolver()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Сигналы
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
go func() {
select {
case s := <-sig:
log.Printf("[КЛИЕНТ] Сигнал %v, завершаю...", s)
cancel()
case <-ctx.Done():
return
}
select {
case s := <-sig:
log.Printf("[КЛИЕНТ] Повторный %v, принудительный выход", s)
os.Exit(1)
case <-ctx.Done():
}
}()
var pauseFlag int32
// STDIN для PAUSE/RESUME/STOP и CAPTCHA_RESULT
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.Contains(line, "error:tunnel stopped") {
log.Printf("[STDIN] %s", line)
}
switch {
case line == "PAUSE":
atomic.StoreInt32(&pauseFlag, 1)
case line == "RESUME":
atomic.StoreInt32(&pauseFlag, 0)
case line == "STOP":
cancel()
return
case strings.HasPrefix(line, "CAPTCHA_RESULT|"):
result := strings.TrimPrefix(line, "CAPTCHA_RESULT|")
drainCaptchaResult()
CaptchaResultChan <- result
log.Printf("[КАПЧА] Результат от Kotlin записан в канал")
}
}
}()
host := flag.String("turn", "", "переопределить IP TURN")
port := flag.String("port", "", "переопределить порт TURN")
listen := flag.String("listen", "127.0.0.1:9000", "локальный адрес")
vkHash := flag.String("vk", "", "хеши VK-звонков (через запятую)")
peerAddr := flag.String("peer", "", "адрес:порт VPS сервера")
numW := flag.Int("n", 24, "количество воркеров (кратно 12)")
deviceID := flag.String("device-id", "unknown", "уникальный ID устройства")
connPassword := flag.String("password", "", "пароль подключения")
captchaMode := flag.String("captcha-mode", "auto", "режим обхода капчи (auto/wv/rjs)")
flag.Parse()
activeCaptchaMode := setCaptchaMode(*captchaMode)
if *peerAddr == "" || *vkHash == "" {
log.Fatal("[КЛИЕНТ] Нужны -peer и -vk")
}
peer, err := net.ResolveUDPAddr("udp", *peerAddr)
if err != nil {
log.Fatalf("[КЛИЕНТ] Ошибка разбора пира: %v", err)
}
hashes := ParseHashes(*vkHash)
if len(hashes) == 0 {
log.Fatal("[КЛИЕНТ] Нет хешей VK")
}
if *connPassword == "" {
log.Fatal("[КЛИЕНТ] Нужен -password: WRAP ключ теперь выводится из пароля подключения")
}
// WRAP key
wrapKey, err := deriveWrapKey(*connPassword)
if err != nil {
log.Fatalf("[КЛИЕНТ] WRAP key derive: %v", err)
}
// Лимит воркеров
maxWorkers := 108
if *numW > maxWorkers {
*numW = maxWorkers
}
if *numW < workersPerGroup {
*numW = workersPerGroup
}
*numW = (*numW / workersPerGroup) * workersPerGroup
tp := &TurnParams{
Host: *host,
Port: *port,
Hashes: hashes,
WrapKey: wrapKey,
}
// Слушаем локально
localConn, err := net.ListenPacket("udp", *listen)
if err != nil {
log.Fatalf("[КЛИЕНТ] Ошибка слушателя %s: %v", *listen, err)
}
if uc, ok := localConn.(*net.UDPConn); ok {
_ = uc.SetReadBuffer(socketBufSize)
_ = uc.SetWriteBuffer(socketBufSize)
}
stopLocalConn := context.AfterFunc(ctx, func() { _ = localConn.Close() })
defer stopLocalConn()
_, localPort, _ := net.SplitHostPort(*listen)
if localPort == "" {
localPort = "9000"
}
numGroups := *numW / workersPerGroup
wrapStatus := "OFF"
if len(wrapKey) == wrapKeyLen {
wrapStatus = "ON (password HKDF + RTP AEAD)"
}
captchaStatus := "AUTO: Go v2 x2 -> WBV Auto x2 -> Go v2 x1 -> Manual WBV"
switch activeCaptchaMode {
case "wv":
captchaStatus = "WBV selected in Android"
case "rjs":
captchaStatus = "RJS Go v2 with WBV Auto fallback"
}
log.Println("[КЛИЕНТ] ═══════════════════════════════════════")
log.Printf("[КЛИЕНТ] VK Creds: 2 stable app_id с циклическим fallback")
log.Printf("[КЛИЕНТ] TLS: Chrome 146 fingerprint")
log.Printf("[КЛИЕНТ] Воркеров: %d (групп: %d, по %d)", *numW, numGroups, workersPerGroup)
log.Printf("[КЛИЕНТ] Хешей: %d", len(hashes))
log.Printf("[КЛИЕНТ] Слушаю: %s | Пир: %s", *listen, *peerAddr)
log.Printf("[КЛИЕНТ] Протокол: UDP")
log.Printf("[КЛИЕНТ] WRAP: %s", wrapStatus)
log.Printf("[WRAP] Ключ выведен из пароля, режим RTP AEAD активен")
log.Printf("[КЛИЕНТ] Device ID: %s", *deviceID)
log.Printf("[КЛИЕНТ] Captcha: %s", captchaStatus)
log.Println("[КЛИЕНТ] ═══════════════════════════════════════")
stats := NewStats()
shutdownCh := make(chan struct{})
go func() {
<-ctx.Done()
close(shutdownCh)
}()
go stats.RunLoop(shutdownCh)
disp := NewDispatcher(ctx, localConn, stats)
defer disp.Shutdown()
configCh := make(chan string, 1)
configDone := make(chan struct{})
go func() {
defer close(configDone)
select {
case rawConf, ok := <-configCh:
if !ok || rawConf == "" {
return
}
finalConf := rawConf
if !strings.Contains(finalConf, "MTU =") {
lines := strings.Split(finalConf, "\n")
var newLines []string
for _, line := range lines {
newLines = append(newLines, line)
if strings.TrimSpace(line) == "[Interface]" {
newLines = append(newLines, "MTU = 1280")
}
}
finalConf = strings.Join(newLines, "\n")
}
fmt.Println()
fmt.Println("╔══════════════ WireGuard Конфиг ══════════════╗")
for _, line := range strings.Split(finalConf, "\n") {
fmt.Printf("║ %-44s ║\n", line)
}
fmt.Println("╚══════════════════════════════════════════════╝")
if err := os.WriteFile("wg-turn.conf", []byte(finalConf+"\n"), 0600); err != nil {
log.Printf("[КОНФИГ] Ошибка сохранения: %v", err)
} else {
log.Println("[КОНФИГ] Сохранён в wg-turn.conf")
}
case <-ctx.Done():
}
}()
var wg sync.WaitGroup
workerIDCounter := 1
var prevWaitReady <-chan struct{}
for g := 0; g < numGroups; g++ {
isFirst := (g == 0)
var myWaitReady <-chan struct{}
var mySignalReady chan<- struct{}
if g > 0 {
myWaitReady = prevWaitReady
}
if g < numGroups-1 {
ch := make(chan struct{})
mySignalReady = ch
prevWaitReady = ch
}
ids := make([]int, workersPerGroup)
for i := range ids {
ids[i] = workerIDCounter
workerIDCounter++
}
gID := g + 1
var cc chan<- string
if isFirst {
cc = configCh
}
wg.Add(1)
go func(groupID int, isFirstGroup bool, configChan chan<- string, workerIds []int, startHashIndex int, waitR <-chan struct{}, sigR chan<- struct{}) {
defer wg.Done()
WorkerGroup(ctx, groupID, startHashIndex, tp, peer, disp, localPort,
isFirstGroup, configChan, workerIds, &pauseFlag, *deviceID, *connPassword, stats, waitR, sigR)
}(gID, isFirst, cc, ids, g, myWaitReady, mySignalReady)
}
wg.Wait()
close(configCh)
<-configDone
log.Println("[КЛИЕНТ] Все воркеры завершены")
}