306 lines
8.6 KiB
Go
306 lines
8.6 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"os"
|
||
"os/signal"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"syscall"
|
||
"time"
|
||
)
|
||
|
||
// 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
|
||
cycle := time.Duration(defaultCycleSecs) * time.Second
|
||
var cc chan<- string
|
||
if isFirst {
|
||
cc = configCh
|
||
}
|
||
|
||
wg.Add(1)
|
||
go func(groupID int, cycleDir time.Duration, 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, cycleDir, &pauseFlag, *deviceID, *connPassword, stats, waitR, sigR)
|
||
}(gID, cycle, isFirst, cc, ids, g, myWaitReady, mySignalReady)
|
||
}
|
||
|
||
wg.Wait()
|
||
close(configCh)
|
||
<-configDone
|
||
log.Println("[КЛИЕНТ] Все воркеры завершены")
|
||
}
|