mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-05-23 07:51:43 +03:00
2608 lines
60 KiB
Go
2608 lines
60 KiB
Go
package main
|
||
|
||
/*
|
||
#cgo android LDFLAGS: -llog
|
||
#include <stdlib.h>
|
||
#include <signal.h>
|
||
#ifdef __ANDROID__
|
||
#include <android/log.h>
|
||
#endif
|
||
|
||
static void androidLogProxy(char *msg) {
|
||
#ifdef __ANDROID__
|
||
__android_log_print(ANDROID_LOG_INFO, "TgWsProxy", "%s", msg);
|
||
#endif
|
||
}
|
||
*/
|
||
import "C"
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"crypto/hmac"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"crypto/tls"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"math"
|
||
"net"
|
||
"os"
|
||
"os/signal"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"syscall"
|
||
"time"
|
||
"unsafe"
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Constants
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const (
|
||
defaultPort = 1443
|
||
tcpNodelay = true
|
||
defaultRecvBuf = 256 * 1024
|
||
defaultSendBuf = 256 * 1024
|
||
defaultPoolSz = 4
|
||
wsPoolMaxAge = 60.0
|
||
wsBridgeIdle = 120.0
|
||
|
||
dcFailCooldown = 10.0
|
||
wsFailTimeout = 2.0
|
||
poolMaintainInterval = 15
|
||
)
|
||
|
||
var (
|
||
recvBuf = defaultRecvBuf
|
||
sendBuf = defaultSendBuf
|
||
poolSize = defaultPoolSz
|
||
logVerbose = false
|
||
)
|
||
|
||
// Cloudflare proxy config
|
||
var (
|
||
cfproxyEnabled = true
|
||
cfproxyPriority = true
|
||
cfproxyUserDomain = ""
|
||
cfproxyDomains []string
|
||
activeCfDomain string
|
||
cfproxyMu sync.RWMutex
|
||
)
|
||
|
||
// MTProto proxy secret (hex, 32 chars = 16 bytes)
|
||
var (
|
||
proxySecret = "00000000000000000000000000000000"
|
||
proxySecretMu sync.RWMutex
|
||
)
|
||
|
||
var dcDefaultIPs = map[int]string{
|
||
1: "149.154.175.50",
|
||
2: "149.154.167.51",
|
||
3: "149.154.175.100",
|
||
4: "149.154.167.91",
|
||
5: "149.154.171.5",
|
||
203: "91.105.192.100",
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Logger
|
||
// ---------------------------------------------------------------------------
|
||
|
||
var (
|
||
logInfo *log.Logger
|
||
logWarn *log.Logger
|
||
logError *log.Logger
|
||
logDebug *log.Logger
|
||
)
|
||
|
||
type androidLogWriter struct{}
|
||
|
||
func (w androidLogWriter) Write(p []byte) (n int, err error) {
|
||
_, _ = os.Stderr.Write(p)
|
||
cs := C.CString(string(p))
|
||
C.androidLogProxy(cs)
|
||
C.free(unsafe.Pointer(cs))
|
||
return len(p), nil
|
||
}
|
||
|
||
func initLogging(verbose bool) {
|
||
flags := 0
|
||
out := androidLogWriter{}
|
||
logInfo = log.New(out, "", flags)
|
||
logWarn = log.New(out, "[WARN] ", flags)
|
||
logError = log.New(out, "[ERROR] ", flags)
|
||
if verbose {
|
||
logDebug = log.New(out, "[DEBUG] ", flags)
|
||
} else {
|
||
logDebug = log.New(io.Discard, "", 0)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Cloudflare proxy domain decoding
|
||
// ---------------------------------------------------------------------------
|
||
|
||
var cfproxyEnc = []string{"virkgj.com", "vmmzovy.com", "mkuosckvso.com", "zaewayzmplad.com", "twdmbzcm.com"}
|
||
|
||
func decodeCfDomain(s string) string {
|
||
if !strings.HasSuffix(s, ".com") {
|
||
return s
|
||
}
|
||
suffix := string([]byte{46, 99, 111, 46, 117, 107}) // decoded suffix
|
||
p := s[:len(s)-4]
|
||
n := 0
|
||
for _, c := range p {
|
||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
|
||
n++
|
||
}
|
||
}
|
||
var result []byte
|
||
for _, c := range []byte(p) {
|
||
if c >= 'a' && c <= 'z' {
|
||
result = append(result, byte((int(c-'a')-n%26+26)%26+'a'))
|
||
} else if c >= 'A' && c <= 'Z' {
|
||
result = append(result, byte((int(c-'A')-n%26+26)%26+'A'))
|
||
} else {
|
||
result = append(result, c)
|
||
}
|
||
}
|
||
return string(result) + suffix
|
||
}
|
||
|
||
func initCfproxyDomains() {
|
||
cfproxyMu.Lock()
|
||
defer cfproxyMu.Unlock()
|
||
if cfproxyUserDomain != "" {
|
||
cfproxyDomains = []string{cfproxyUserDomain}
|
||
activeCfDomain = cfproxyUserDomain
|
||
return
|
||
}
|
||
cfproxyDomains = make([]string, len(cfproxyEnc))
|
||
for i, enc := range cfproxyEnc {
|
||
cfproxyDomains[i] = decodeCfDomain(enc)
|
||
}
|
||
if len(cfproxyDomains) > 0 {
|
||
activeCfDomain = cfproxyDomains[0]
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Telegram IP ranges
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type ipRange struct {
|
||
lo, hi uint32
|
||
}
|
||
|
||
var tgRanges []ipRange
|
||
|
||
func init() {
|
||
ranges := [][2]string{
|
||
{"185.76.151.0", "185.76.151.255"},
|
||
{"149.154.160.0", "149.154.175.255"},
|
||
{"91.105.192.0", "91.105.193.255"},
|
||
{"91.108.0.0", "91.108.255.255"},
|
||
}
|
||
for _, r := range ranges {
|
||
lo := ipToUint32(net.ParseIP(r[0]))
|
||
hi := ipToUint32(net.ParseIP(r[1]))
|
||
tgRanges = append(tgRanges, ipRange{lo, hi})
|
||
}
|
||
}
|
||
|
||
func ipToUint32(ip net.IP) uint32 {
|
||
ip4 := ip.To4()
|
||
if ip4 == nil {
|
||
return 0
|
||
}
|
||
return binary.BigEndian.Uint32(ip4)
|
||
}
|
||
|
||
func isTelegramIP(ipStr string) bool {
|
||
ip := net.ParseIP(ipStr)
|
||
if ip == nil {
|
||
return false
|
||
}
|
||
n := ipToUint32(ip)
|
||
if n == 0 {
|
||
return false
|
||
}
|
||
for _, r := range tgRanges {
|
||
if n >= r.lo && n <= r.hi {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// IP -> DC mapping
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type dcInfo struct {
|
||
dc int
|
||
isMedia bool
|
||
}
|
||
|
||
var ipToDC = map[string]dcInfo{
|
||
// DC1
|
||
"149.154.175.50": {1, false}, "149.154.175.51": {1, false},
|
||
"149.154.175.53": {1, false}, "149.154.175.54": {1, false},
|
||
"149.154.175.52": {1, true},
|
||
// DC2
|
||
"149.154.167.41": {2, false}, "149.154.167.50": {2, false},
|
||
"149.154.167.51": {2, false}, "149.154.167.220": {2, false},
|
||
"149.154.167.35": {2, false}, "149.154.167.36": {2, false},
|
||
"95.161.76.100": {2, false},
|
||
"149.154.167.151": {2, true}, "149.154.167.222": {2, true},
|
||
"149.154.167.223": {2, true}, "149.154.162.123": {2, true},
|
||
// DC3
|
||
"149.154.175.100": {3, false}, "149.154.175.101": {3, false},
|
||
"149.154.175.102": {3, true},
|
||
// DC4
|
||
"149.154.167.91": {4, false}, "149.154.167.92": {4, false},
|
||
"149.154.164.250": {4, true}, "149.154.166.120": {4, true},
|
||
"149.154.166.121": {4, true}, "149.154.167.118": {4, true},
|
||
"149.154.165.111": {4, true},
|
||
// DC5
|
||
"91.108.56.100": {5, false}, "91.108.56.101": {5, false},
|
||
"91.108.56.116": {5, false}, "91.108.56.126": {5, false},
|
||
"149.154.171.5": {5, false},
|
||
"91.108.56.102": {5, true}, "91.108.56.128": {5, true},
|
||
"91.108.56.151": {5, true},
|
||
// DC203
|
||
"91.105.192.100": {203, false},
|
||
}
|
||
|
||
var dcOverrides = map[int]int{
|
||
203: 2,
|
||
}
|
||
|
||
var validProtos = map[uint32]bool{
|
||
0xEFEFEFEF: true,
|
||
0xEEEEEEEE: true,
|
||
0xDDDDDDDD: true,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Global state
|
||
// ---------------------------------------------------------------------------
|
||
|
||
var (
|
||
dcOpt map[int]string
|
||
dcOptMu sync.RWMutex
|
||
|
||
wsBlackMu sync.RWMutex
|
||
wsBlacklist = make(map[[2]int]bool)
|
||
|
||
dcFailMu sync.RWMutex
|
||
dcFailUntil = make(map[[2]int]float64)
|
||
|
||
zero64 = make([]byte, 64)
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Stats
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type Stats struct {
|
||
connectionsTotal atomic.Int64
|
||
connectionsActive atomic.Int64
|
||
connectionsWs atomic.Int64
|
||
connectionsTcpFallback atomic.Int64
|
||
connectionsCfproxy atomic.Int64
|
||
connectionsHttpReject atomic.Int64
|
||
connectionsPassthrough atomic.Int64
|
||
connectionsBad atomic.Int64
|
||
wsErrors atomic.Int64
|
||
bytesUp atomic.Int64
|
||
bytesDown atomic.Int64
|
||
poolHits atomic.Int64
|
||
poolMisses atomic.Int64
|
||
}
|
||
|
||
func (s *Stats) Summary() string {
|
||
ph := s.poolHits.Load()
|
||
pm := s.poolMisses.Load()
|
||
return fmt.Sprintf(
|
||
"total=%d active=%d ws=%d tcp_fb=%d cf=%d bad=%d err=%d pool=%d/%d up=%s down=%s",
|
||
s.connectionsTotal.Load(),
|
||
s.connectionsActive.Load(),
|
||
s.connectionsWs.Load(),
|
||
s.connectionsTcpFallback.Load(),
|
||
s.connectionsCfproxy.Load(),
|
||
s.connectionsBad.Load(),
|
||
s.wsErrors.Load(),
|
||
ph, ph+pm,
|
||
humanBytes(s.bytesUp.Load()),
|
||
humanBytes(s.bytesDown.Load()),
|
||
)
|
||
}
|
||
|
||
func (s *Stats) SummaryRu() string {
|
||
active := s.connectionsActive.Load()
|
||
ws := s.connectionsWs.Load()
|
||
cf := s.connectionsCfproxy.Load()
|
||
tcp := s.connectionsTcpFallback.Load()
|
||
errCount := s.wsErrors.Load()
|
||
up := humanBytes(s.bytesUp.Load())
|
||
down := humanBytes(s.bytesDown.Load())
|
||
|
||
parts := []string{fmt.Sprintf("акт:%d", active)}
|
||
if ws > 0 {
|
||
parts = append(parts, fmt.Sprintf("ws:%d", ws))
|
||
}
|
||
if cf > 0 {
|
||
parts = append(parts, fmt.Sprintf("cf:%d", cf))
|
||
}
|
||
if tcp > 0 {
|
||
parts = append(parts, fmt.Sprintf("tcp:%d", tcp))
|
||
}
|
||
if errCount > 0 {
|
||
parts = append(parts, fmt.Sprintf("ош:%d", errCount))
|
||
}
|
||
parts = append(parts, fmt.Sprintf("↑%s ↓%s", up, down))
|
||
return strings.Join(parts, " | ")
|
||
}
|
||
|
||
func (s *Stats) Reset() {
|
||
s.connectionsTotal.Store(0)
|
||
s.connectionsActive.Store(0)
|
||
s.connectionsWs.Store(0)
|
||
s.connectionsTcpFallback.Store(0)
|
||
s.connectionsCfproxy.Store(0)
|
||
s.connectionsHttpReject.Store(0)
|
||
s.connectionsPassthrough.Store(0)
|
||
s.connectionsBad.Store(0)
|
||
s.wsErrors.Store(0)
|
||
s.bytesUp.Store(0)
|
||
s.bytesDown.Store(0)
|
||
s.poolHits.Store(0)
|
||
s.poolMisses.Store(0)
|
||
}
|
||
|
||
var stats Stats
|
||
|
||
func humanBytes(n int64) string {
|
||
abs := n
|
||
if abs < 0 {
|
||
abs = -abs
|
||
}
|
||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||
f := float64(n)
|
||
for i, u := range units {
|
||
if math.Abs(f) < 1024 || i == len(units)-1 {
|
||
return fmt.Sprintf("%.1f%s", f, u)
|
||
}
|
||
f /= 1024
|
||
}
|
||
return fmt.Sprintf("%.1f%s", f, "TB")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Socket helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func setSockOpts(conn net.Conn) {
|
||
tc, ok := conn.(*net.TCPConn)
|
||
if !ok {
|
||
return
|
||
}
|
||
if tcpNodelay {
|
||
_ = tc.SetNoDelay(true)
|
||
}
|
||
raw, err := tc.SyscallConn()
|
||
if err != nil {
|
||
return
|
||
}
|
||
_ = raw.Control(func(fd uintptr) {
|
||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, recvBuf)
|
||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUF, sendBuf)
|
||
})
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// XOR mask — optimized 8-byte processing
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func xorMask(data, mask []byte) []byte {
|
||
n := len(data)
|
||
if n == 0 {
|
||
return data
|
||
}
|
||
|
||
result := make([]byte, n)
|
||
|
||
// Build 8-byte mask
|
||
mask8 := uint64(mask[0]) | uint64(mask[1])<<8 |
|
||
uint64(mask[2])<<16 | uint64(mask[3])<<24 |
|
||
uint64(mask[0])<<32 | uint64(mask[1])<<40 |
|
||
uint64(mask[2])<<48 | uint64(mask[3])<<56
|
||
|
||
i := 0
|
||
// Process 8 bytes at a time
|
||
for ; i+8 <= n; i += 8 {
|
||
v := binary.LittleEndian.Uint64(data[i:])
|
||
binary.LittleEndian.PutUint64(result[i:], v^mask8)
|
||
}
|
||
// Process remaining bytes
|
||
for ; i < n; i++ {
|
||
result[i] = data[i] ^ mask[i&3]
|
||
}
|
||
return result
|
||
}
|
||
|
||
// xorMaskInPlace modifies data in place
|
||
func xorMaskInPlace(data, mask []byte) {
|
||
n := len(data)
|
||
if n == 0 {
|
||
return
|
||
}
|
||
|
||
mask8 := uint64(mask[0]) | uint64(mask[1])<<8 |
|
||
uint64(mask[2])<<16 | uint64(mask[3])<<24 |
|
||
uint64(mask[0])<<32 | uint64(mask[1])<<40 |
|
||
uint64(mask[2])<<48 | uint64(mask[3])<<56
|
||
|
||
i := 0
|
||
for ; i+8 <= n; i += 8 {
|
||
v := binary.LittleEndian.Uint64(data[i:])
|
||
binary.LittleEndian.PutUint64(data[i:], v^mask8)
|
||
}
|
||
for ; i < n; i++ {
|
||
data[i] ^= mask[i&3]
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WsHandshakeError
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type WsHandshakeError struct {
|
||
StatusCode int
|
||
StatusLine string
|
||
Headers map[string]string
|
||
Location string
|
||
}
|
||
|
||
func (e *WsHandshakeError) Error() string {
|
||
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.StatusLine)
|
||
}
|
||
|
||
func (e *WsHandshakeError) IsRedirect() bool {
|
||
switch e.StatusCode {
|
||
case 301, 302, 303, 307, 308:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// RawWebSocket
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const (
|
||
opContinuation = 0x0
|
||
opText = 0x1
|
||
opBinary = 0x2
|
||
opClose = 0x8
|
||
opPing = 0x9
|
||
opPong = 0xA
|
||
)
|
||
|
||
type RawWebSocket struct {
|
||
conn net.Conn
|
||
bufReader *bufio.Reader
|
||
writeMu sync.Mutex
|
||
closed atomic.Bool
|
||
}
|
||
|
||
func wsConnect(ip, domain, path string, timeout float64) (*RawWebSocket, error) {
|
||
if path == "" {
|
||
path = "/apiws"
|
||
}
|
||
if timeout <= 0 {
|
||
timeout = 10.0
|
||
}
|
||
|
||
dialTimeout := timeout
|
||
if dialTimeout > 10.0 {
|
||
dialTimeout = 10.0
|
||
}
|
||
|
||
dialer := &net.Dialer{
|
||
Timeout: time.Duration(dialTimeout * float64(time.Second)),
|
||
}
|
||
|
||
tlsCfg := &tls.Config{
|
||
InsecureSkipVerify: true,
|
||
ServerName: domain,
|
||
}
|
||
|
||
rawConn, err := tls.DialWithDialer(dialer, "tcp", ip+":443", tlsCfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
setSockOpts(rawConn)
|
||
|
||
wsKeyBytes := make([]byte, 16)
|
||
_, _ = rand.Read(wsKeyBytes)
|
||
wsKey := base64.StdEncoding.EncodeToString(wsKeyBytes)
|
||
|
||
req := fmt.Sprintf(
|
||
"GET %s HTTP/1.1\r\n"+
|
||
"Host: %s\r\n"+
|
||
"Upgrade: websocket\r\n"+
|
||
"Connection: Upgrade\r\n"+
|
||
"Sec-WebSocket-Key: %s\r\n"+
|
||
"Sec-WebSocket-Version: 13\r\n"+
|
||
"Sec-WebSocket-Protocol: binary\r\n"+
|
||
"Origin: https://web.telegram.org\r\n"+
|
||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) "+
|
||
"Chrome/131.0.0.0 Safari/537.36\r\n"+
|
||
"\r\n",
|
||
path, domain, wsKey,
|
||
)
|
||
|
||
_ = rawConn.SetWriteDeadline(time.Now().Add(time.Duration(timeout * float64(time.Second))))
|
||
_, err = rawConn.Write([]byte(req))
|
||
if err != nil {
|
||
rawConn.Close()
|
||
return nil, err
|
||
}
|
||
_ = rawConn.SetWriteDeadline(time.Time{})
|
||
|
||
// Use buffered reader for efficient header parsing
|
||
bufReader := bufio.NewReaderSize(rawConn, 4096)
|
||
|
||
_ = rawConn.SetReadDeadline(time.Now().Add(time.Duration(timeout * float64(time.Second))))
|
||
|
||
var responseLines []string
|
||
for {
|
||
line, err := bufReader.ReadString('\n')
|
||
if err != nil {
|
||
rawConn.Close()
|
||
return nil, err
|
||
}
|
||
line = strings.TrimRight(line, "\r\n")
|
||
if line == "" {
|
||
break
|
||
}
|
||
responseLines = append(responseLines, line)
|
||
if len(responseLines) > 100 {
|
||
rawConn.Close()
|
||
return nil, fmt.Errorf("too many HTTP headers")
|
||
}
|
||
}
|
||
_ = rawConn.SetReadDeadline(time.Time{})
|
||
|
||
if len(responseLines) == 0 {
|
||
rawConn.Close()
|
||
return nil, &WsHandshakeError{StatusCode: 0, StatusLine: "empty response"}
|
||
}
|
||
|
||
firstLine := responseLines[0]
|
||
parts := strings.SplitN(firstLine, " ", 3)
|
||
statusCode := 0
|
||
if len(parts) >= 2 {
|
||
statusCode, _ = strconv.Atoi(parts[1])
|
||
}
|
||
|
||
if statusCode == 101 {
|
||
ws := &RawWebSocket{
|
||
conn: rawConn,
|
||
bufReader: bufReader,
|
||
}
|
||
return ws, nil
|
||
}
|
||
|
||
headers := make(map[string]string)
|
||
for _, hl := range responseLines[1:] {
|
||
idx := strings.IndexByte(hl, ':')
|
||
if idx >= 0 {
|
||
k := strings.TrimSpace(strings.ToLower(hl[:idx]))
|
||
v := strings.TrimSpace(hl[idx+1:])
|
||
headers[k] = v
|
||
}
|
||
}
|
||
rawConn.Close()
|
||
return nil, &WsHandshakeError{
|
||
StatusCode: statusCode,
|
||
StatusLine: firstLine,
|
||
Headers: headers,
|
||
Location: headers["location"],
|
||
}
|
||
}
|
||
|
||
func (ws *RawWebSocket) Send(data []byte) error {
|
||
if ws.closed.Load() {
|
||
return fmt.Errorf("WebSocket closed")
|
||
}
|
||
frame := ws.buildFrame(opBinary, data, true)
|
||
ws.writeMu.Lock()
|
||
_, err := ws.conn.Write(frame)
|
||
ws.writeMu.Unlock()
|
||
return err
|
||
}
|
||
|
||
func (ws *RawWebSocket) SendBatch(parts [][]byte) error {
|
||
if ws.closed.Load() {
|
||
return fmt.Errorf("WebSocket closed")
|
||
}
|
||
ws.writeMu.Lock()
|
||
defer ws.writeMu.Unlock()
|
||
for _, part := range parts {
|
||
frame := ws.buildFrame(opBinary, part, true)
|
||
if _, err := ws.conn.Write(frame); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (ws *RawWebSocket) SendPing() error {
|
||
if ws.closed.Load() {
|
||
return fmt.Errorf("WebSocket closed")
|
||
}
|
||
frame := ws.buildFrame(opPing, nil, true)
|
||
ws.writeMu.Lock()
|
||
_, err := ws.conn.Write(frame)
|
||
ws.writeMu.Unlock()
|
||
return err
|
||
}
|
||
|
||
func (ws *RawWebSocket) Recv() ([]byte, error) {
|
||
for !ws.closed.Load() {
|
||
opcode, payload, err := ws.readFrame()
|
||
if err != nil {
|
||
ws.closed.Store(true)
|
||
return nil, err
|
||
}
|
||
|
||
switch opcode {
|
||
case opClose:
|
||
ws.closed.Store(true)
|
||
closePayload := payload
|
||
if len(closePayload) > 2 {
|
||
closePayload = closePayload[:2]
|
||
}
|
||
reply := ws.buildFrame(opClose, closePayload, true)
|
||
ws.writeMu.Lock()
|
||
_, _ = ws.conn.Write(reply)
|
||
ws.writeMu.Unlock()
|
||
return nil, io.EOF
|
||
|
||
case opPing:
|
||
pong := ws.buildFrame(opPong, payload, true)
|
||
ws.writeMu.Lock()
|
||
_, _ = ws.conn.Write(pong)
|
||
ws.writeMu.Unlock()
|
||
continue
|
||
|
||
case opPong:
|
||
continue
|
||
|
||
case opText, opBinary:
|
||
return payload, nil
|
||
|
||
default:
|
||
continue
|
||
}
|
||
}
|
||
return nil, io.EOF
|
||
}
|
||
|
||
func (ws *RawWebSocket) Close() {
|
||
if ws.closed.Swap(true) {
|
||
return
|
||
}
|
||
frame := ws.buildFrame(opClose, nil, true)
|
||
ws.writeMu.Lock()
|
||
_, _ = ws.conn.Write(frame)
|
||
ws.writeMu.Unlock()
|
||
_ = ws.conn.Close()
|
||
}
|
||
|
||
// SetReadDeadline exposes deadline control for the bridge
|
||
func (ws *RawWebSocket) SetReadDeadline(t time.Time) error {
|
||
return ws.conn.SetReadDeadline(t)
|
||
}
|
||
|
||
// buildFrame creates a WebSocket frame with minimal allocations
|
||
func (ws *RawWebSocket) buildFrame(opcode int, data []byte, mask bool) []byte {
|
||
length := len(data)
|
||
fb := byte(0x80 | opcode)
|
||
|
||
// Calculate total size
|
||
headerSize := 2
|
||
if mask {
|
||
headerSize += 4
|
||
}
|
||
if length >= 126 && length < 65536 {
|
||
headerSize += 2
|
||
} else if length >= 65536 {
|
||
headerSize += 8
|
||
}
|
||
|
||
totalSize := headerSize + length
|
||
result := make([]byte, totalSize)
|
||
pos := 0
|
||
|
||
result[pos] = fb
|
||
pos++
|
||
|
||
var maskKey [4]byte
|
||
if mask {
|
||
_, _ = rand.Read(maskKey[:])
|
||
}
|
||
|
||
if length < 126 {
|
||
lb := byte(length)
|
||
if mask {
|
||
lb |= 0x80
|
||
}
|
||
result[pos] = lb
|
||
pos++
|
||
} else if length < 65536 {
|
||
lb := byte(126)
|
||
if mask {
|
||
lb |= 0x80
|
||
}
|
||
result[pos] = lb
|
||
pos++
|
||
binary.BigEndian.PutUint16(result[pos:], uint16(length))
|
||
pos += 2
|
||
} else {
|
||
lb := byte(127)
|
||
if mask {
|
||
lb |= 0x80
|
||
}
|
||
result[pos] = lb
|
||
pos++
|
||
binary.BigEndian.PutUint64(result[pos:], uint64(length))
|
||
pos += 8
|
||
}
|
||
|
||
if mask {
|
||
copy(result[pos:], maskKey[:])
|
||
pos += 4
|
||
// XOR directly into result buffer
|
||
payloadStart := pos
|
||
copy(result[payloadStart:], data)
|
||
xorMaskInPlace(result[payloadStart:payloadStart+length], maskKey[:])
|
||
} else {
|
||
copy(result[pos:], data)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
func (ws *RawWebSocket) readFrame() (int, []byte, error) {
|
||
hdr := make([]byte, 2)
|
||
if _, err := io.ReadFull(ws.bufReader, hdr); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
|
||
opcode := int(hdr[0] & 0x0F)
|
||
length := uint64(hdr[1] & 0x7F)
|
||
|
||
if length == 126 {
|
||
buf := make([]byte, 2)
|
||
if _, err := io.ReadFull(ws.bufReader, buf); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
length = uint64(binary.BigEndian.Uint16(buf))
|
||
} else if length == 127 {
|
||
buf := make([]byte, 8)
|
||
if _, err := io.ReadFull(ws.bufReader, buf); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
length = binary.BigEndian.Uint64(buf)
|
||
}
|
||
|
||
hasMask := (hdr[1] & 0x80) != 0
|
||
var maskKey []byte
|
||
if hasMask {
|
||
maskKey = make([]byte, 4)
|
||
if _, err := io.ReadFull(ws.bufReader, maskKey); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
}
|
||
|
||
payload := make([]byte, length)
|
||
if length > 0 {
|
||
if _, err := io.ReadFull(ws.bufReader, payload); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
}
|
||
|
||
if hasMask {
|
||
xorMaskInPlace(payload, maskKey)
|
||
}
|
||
|
||
return opcode, payload, nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Crypto helpers: DC extraction & patching
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func newAESCTR(key, iv []byte) (cipher.Stream, error) {
|
||
block, err := aes.NewCipher(key)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return cipher.NewCTR(block, iv), nil
|
||
}
|
||
|
||
func dcFromInit(data []byte) (dc int, isMedia bool, proto uint32, ok bool) {
|
||
if len(data) < 64 {
|
||
return 0, false, 0, false
|
||
}
|
||
|
||
stream, err := newAESCTR(data[8:40], data[40:56])
|
||
if err != nil {
|
||
logDebug.Printf("DC extraction failed: %v", err)
|
||
return 0, false, 0, false
|
||
}
|
||
|
||
keystream := make([]byte, 64)
|
||
stream.XORKeyStream(keystream, zero64)
|
||
|
||
plain := make([]byte, 8)
|
||
for i := 0; i < 8; i++ {
|
||
plain[i] = data[56+i] ^ keystream[56+i]
|
||
}
|
||
|
||
proto = binary.LittleEndian.Uint32(plain[0:4])
|
||
dcRaw := int16(binary.LittleEndian.Uint16(plain[4:6]))
|
||
|
||
logDebug.Printf("dc_from_init: proto=0x%08X dc_raw=%d plain=%x", proto, dcRaw, plain)
|
||
|
||
if !validProtos[proto] {
|
||
return 0, false, 0, false
|
||
}
|
||
|
||
dcAbs := int(dcRaw)
|
||
if dcAbs < 0 {
|
||
dcAbs = -dcAbs
|
||
}
|
||
media := dcRaw < 0
|
||
|
||
if (dcAbs >= 1 && dcAbs <= 5) || dcAbs == 203 {
|
||
return dcAbs, media, proto, true
|
||
}
|
||
|
||
return 0, false, 0, false
|
||
}
|
||
|
||
func patchInitDC(data []byte, dc int) []byte {
|
||
if len(data) < 64 {
|
||
return data
|
||
}
|
||
|
||
newDC := make([]byte, 2)
|
||
binary.LittleEndian.PutUint16(newDC, uint16(int16(dc)))
|
||
|
||
stream, err := newAESCTR(data[8:40], data[40:56])
|
||
if err != nil {
|
||
return data
|
||
}
|
||
|
||
ks := make([]byte, 64)
|
||
stream.XORKeyStream(ks, zero64)
|
||
|
||
patched := make([]byte, len(data))
|
||
copy(patched, data)
|
||
patched[60] = ks[60] ^ newDC[0]
|
||
patched[61] = ks[61] ^ newDC[1]
|
||
|
||
logDebug.Printf("init patched: dc_id -> %d", dc)
|
||
return patched
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// MsgSplitter
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const (
|
||
protoAbridged = 0 // 0xEFEFEFEF
|
||
protoIntermediate = 1 // 0xEEEEEEEE
|
||
protoPaddedIntermediate = 2 // 0xDDDDDDDD
|
||
)
|
||
|
||
type MsgSplitter struct {
|
||
stream cipher.Stream
|
||
protoType int
|
||
cipherBuf []byte // accumulates raw ciphertext across calls
|
||
plainBuf []byte // accumulates decrypted plaintext across calls
|
||
disabled bool
|
||
}
|
||
|
||
func protoTagToType(proto uint32) int {
|
||
switch proto {
|
||
case 0xEEEEEEEE:
|
||
return protoIntermediate
|
||
case 0xDDDDDDDD:
|
||
return protoPaddedIntermediate
|
||
default:
|
||
return protoAbridged
|
||
}
|
||
}
|
||
|
||
func newMsgSplitter(initData []byte, proto uint32) (*MsgSplitter, error) {
|
||
if len(initData) < 56 {
|
||
return nil, fmt.Errorf("init data too short")
|
||
}
|
||
stream, err := newAESCTR(initData[8:40], initData[40:56])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
skip := make([]byte, 64)
|
||
stream.XORKeyStream(skip, zero64)
|
||
|
||
return &MsgSplitter{
|
||
stream: stream,
|
||
protoType: protoTagToType(proto),
|
||
}, nil
|
||
}
|
||
|
||
func (s *MsgSplitter) Split(chunk []byte) [][]byte {
|
||
if len(chunk) == 0 {
|
||
return nil
|
||
}
|
||
if s.disabled {
|
||
return [][]byte{chunk}
|
||
}
|
||
|
||
// Accumulate ciphertext and decrypt the new chunk
|
||
s.cipherBuf = append(s.cipherBuf, chunk...)
|
||
decrypted := make([]byte, len(chunk))
|
||
s.stream.XORKeyStream(decrypted, chunk)
|
||
s.plainBuf = append(s.plainBuf, decrypted...)
|
||
|
||
var parts [][]byte
|
||
for len(s.cipherBuf) > 0 {
|
||
pktLen := s.nextPacketLen()
|
||
if pktLen < 0 {
|
||
// need more data
|
||
break
|
||
}
|
||
if pktLen == 0 {
|
||
// unknown protocol — pass through remainder and disable
|
||
parts = append(parts, append([]byte(nil), s.cipherBuf...))
|
||
s.cipherBuf = s.cipherBuf[:0]
|
||
s.plainBuf = s.plainBuf[:0]
|
||
s.disabled = true
|
||
break
|
||
}
|
||
if len(s.cipherBuf) < pktLen {
|
||
break // incomplete packet, wait for more data
|
||
}
|
||
parts = append(parts, append([]byte(nil), s.cipherBuf[:pktLen]...))
|
||
s.cipherBuf = s.cipherBuf[pktLen:]
|
||
s.plainBuf = s.plainBuf[pktLen:]
|
||
}
|
||
if len(parts) == 0 {
|
||
return nil // all buffered, nothing complete yet
|
||
}
|
||
return parts
|
||
}
|
||
|
||
func (s *MsgSplitter) Flush() [][]byte {
|
||
if len(s.cipherBuf) == 0 {
|
||
return nil
|
||
}
|
||
tail := append([]byte(nil), s.cipherBuf...)
|
||
s.cipherBuf = s.cipherBuf[:0]
|
||
s.plainBuf = s.plainBuf[:0]
|
||
return [][]byte{tail}
|
||
}
|
||
|
||
// nextPacketLen returns:
|
||
// >0 total bytes for the next complete packet (header + payload)
|
||
// 0 unknown protocol (disable splitter)
|
||
// -1 need more data
|
||
func (s *MsgSplitter) nextPacketLen() int {
|
||
if len(s.plainBuf) == 0 {
|
||
return -1
|
||
}
|
||
switch s.protoType {
|
||
case protoAbridged:
|
||
return s.nextAbridgedLen()
|
||
case protoIntermediate, protoPaddedIntermediate:
|
||
return s.nextIntermediateLen()
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
func (s *MsgSplitter) nextAbridgedLen() int {
|
||
first := s.plainBuf[0] & 0x7F
|
||
var headerLen, payloadLen int
|
||
if first == 0x7F {
|
||
// Long header: 1 byte (0x7F) + 3 bytes LE length in 4-byte words
|
||
if len(s.plainBuf) < 4 {
|
||
return -1
|
||
}
|
||
payloadLen = int(uint32(s.plainBuf[1]) | uint32(s.plainBuf[2])<<8 | uint32(s.plainBuf[3])<<16) * 4
|
||
headerLen = 4
|
||
} else {
|
||
payloadLen = int(first) * 4
|
||
headerLen = 1
|
||
}
|
||
if payloadLen <= 0 {
|
||
return 0
|
||
}
|
||
pktLen := headerLen + payloadLen
|
||
if len(s.plainBuf) < pktLen {
|
||
return -1
|
||
}
|
||
return pktLen
|
||
}
|
||
|
||
func (s *MsgSplitter) nextIntermediateLen() int {
|
||
if len(s.plainBuf) < 4 {
|
||
return -1
|
||
}
|
||
payloadLen := int(binary.LittleEndian.Uint32(s.plainBuf[:4]) & 0x7FFFFFFF)
|
||
if payloadLen <= 0 {
|
||
return 0
|
||
}
|
||
pktLen := 4 + payloadLen
|
||
if len(s.plainBuf) < pktLen {
|
||
return -1
|
||
}
|
||
return pktLen
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WS domains
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func wsDomains(dc int, isMedia *bool) []string {
|
||
effectiveDC := dc
|
||
if override, ok := dcOverrides[dc]; ok {
|
||
effectiveDC = override
|
||
}
|
||
|
||
if isMedia == nil || *isMedia {
|
||
return []string{
|
||
fmt.Sprintf("kws%d-1.web.telegram.org", effectiveDC),
|
||
fmt.Sprintf("kws%d.web.telegram.org", effectiveDC),
|
||
}
|
||
}
|
||
return []string{
|
||
fmt.Sprintf("kws%d.web.telegram.org", effectiveDC),
|
||
fmt.Sprintf("kws%d-1.web.telegram.org", effectiveDC),
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WsPool
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type poolEntry struct {
|
||
ws *RawWebSocket
|
||
created float64
|
||
}
|
||
|
||
type WsPool struct {
|
||
mu sync.Mutex
|
||
idle map[[2]int][]poolEntry
|
||
refilling map[[2]int]bool
|
||
}
|
||
|
||
func newWsPool() *WsPool {
|
||
return &WsPool{
|
||
idle: make(map[[2]int][]poolEntry),
|
||
refilling: make(map[[2]int]bool),
|
||
}
|
||
}
|
||
|
||
func isMediaInt(b bool) int {
|
||
if b {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func monoNow() float64 {
|
||
return float64(time.Now().UnixNano()) / 1e9
|
||
}
|
||
|
||
func (p *WsPool) Get(dc int, isMedia bool, targetIP string, domains []string) *RawWebSocket {
|
||
key := [2]int{dc, isMediaInt(isMedia)}
|
||
now := monoNow()
|
||
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
|
||
bucket := p.idle[key]
|
||
for len(bucket) > 0 {
|
||
entry := bucket[0]
|
||
bucket = bucket[1:]
|
||
p.idle[key] = bucket
|
||
|
||
age := now - entry.created
|
||
if age > wsPoolMaxAge || entry.ws.closed.Load() {
|
||
go entry.ws.Close()
|
||
continue
|
||
}
|
||
|
||
stats.poolHits.Add(1)
|
||
logDebug.Printf("⚡ Пул: DC%d%s взят (%.0fс, ост:%d)",
|
||
dc, mediaTag(isMedia), age, len(bucket))
|
||
p.scheduleRefillLocked(key, targetIP, domains)
|
||
return entry.ws
|
||
}
|
||
|
||
stats.poolMisses.Add(1)
|
||
p.scheduleRefillLocked(key, targetIP, domains)
|
||
return nil
|
||
}
|
||
|
||
// scheduleRefillLocked must be called with p.mu held
|
||
func (p *WsPool) scheduleRefillLocked(key [2]int, targetIP string, domains []string) {
|
||
if p.refilling[key] {
|
||
return
|
||
}
|
||
p.refilling[key] = true
|
||
go p.refill(key, targetIP, domains)
|
||
}
|
||
|
||
func (p *WsPool) refill(key [2]int, targetIP string, domains []string) {
|
||
dc := key[0]
|
||
isMedia := key[1] == 1
|
||
|
||
defer func() {
|
||
p.mu.Lock()
|
||
delete(p.refilling, key)
|
||
p.mu.Unlock()
|
||
}()
|
||
|
||
p.mu.Lock()
|
||
bucket := p.idle[key]
|
||
needed := poolSize - len(bucket)
|
||
p.mu.Unlock()
|
||
|
||
if needed <= 0 {
|
||
return
|
||
}
|
||
|
||
type result struct {
|
||
ws *RawWebSocket
|
||
}
|
||
|
||
ch := make(chan result, needed)
|
||
for i := 0; i < needed; i++ {
|
||
go func() {
|
||
ws := connectOneWS(targetIP, domains)
|
||
ch <- result{ws}
|
||
}()
|
||
}
|
||
|
||
for i := 0; i < needed; i++ {
|
||
r := <-ch
|
||
if r.ws != nil {
|
||
p.mu.Lock()
|
||
p.idle[key] = append(p.idle[key], poolEntry{r.ws, monoNow()})
|
||
p.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
p.mu.Lock()
|
||
logDebug.Printf("♻ Пул DC%d%s пополнен: %d готово",
|
||
dc, mediaTag(isMedia), len(p.idle[key]))
|
||
p.mu.Unlock()
|
||
}
|
||
|
||
func connectOneWS(targetIP string, domains []string) *RawWebSocket {
|
||
for _, domain := range domains {
|
||
ws, err := wsConnect(targetIP, domain, "/apiws", 5)
|
||
if err != nil {
|
||
if wsErr, ok := err.(*WsHandshakeError); ok && wsErr.IsRedirect() {
|
||
continue
|
||
}
|
||
return nil
|
||
}
|
||
return ws
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (p *WsPool) Warmup(dcOptMap map[int]string) {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
|
||
for dc, targetIP := range dcOptMap {
|
||
if targetIP == "" {
|
||
continue
|
||
}
|
||
for _, isMedia := range []bool{false, true} {
|
||
domains := wsDomains(dc, &isMedia)
|
||
key := [2]int{dc, isMediaInt(isMedia)}
|
||
p.scheduleRefillLocked(key, targetIP, domains)
|
||
}
|
||
}
|
||
logDebug.Printf("♻ Прогрев пула: %d DC", len(dcOptMap))
|
||
}
|
||
|
||
func (p *WsPool) Maintain(ctx context.Context, dcOptMap map[int]string) {
|
||
ticker := time.NewTicker(poolMaintainInterval * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
p.maintainOnce(dcOptMap)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (p *WsPool) maintainOnce(dcOptMap map[int]string) {
|
||
now := monoNow()
|
||
|
||
p.mu.Lock()
|
||
for key, bucket := range p.idle {
|
||
var fresh []poolEntry
|
||
for _, e := range bucket {
|
||
age := now - e.created
|
||
if age > wsPoolMaxAge || e.ws.closed.Load() {
|
||
go e.ws.Close()
|
||
} else {
|
||
// Send ping to keep connection alive
|
||
go func(ws *RawWebSocket) {
|
||
if err := ws.SendPing(); err != nil {
|
||
ws.Close()
|
||
}
|
||
}(e.ws)
|
||
fresh = append(fresh, e)
|
||
}
|
||
}
|
||
p.idle[key] = fresh
|
||
}
|
||
p.mu.Unlock()
|
||
|
||
// Refill all known DCs
|
||
p.mu.Lock()
|
||
for dc, targetIP := range dcOptMap {
|
||
if targetIP == "" {
|
||
continue
|
||
}
|
||
for _, isMedia := range []bool{false, true} {
|
||
domains := wsDomains(dc, &isMedia)
|
||
key := [2]int{dc, isMediaInt(isMedia)}
|
||
p.scheduleRefillLocked(key, targetIP, domains)
|
||
}
|
||
}
|
||
p.mu.Unlock()
|
||
}
|
||
|
||
func (p *WsPool) IdleCount() int {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
count := 0
|
||
for _, bucket := range p.idle {
|
||
count += len(bucket)
|
||
}
|
||
return count
|
||
}
|
||
|
||
func (p *WsPool) CloseAll() {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
for key, bucket := range p.idle {
|
||
for _, e := range bucket {
|
||
go e.ws.Close()
|
||
}
|
||
delete(p.idle, key)
|
||
}
|
||
}
|
||
|
||
var wsPool = newWsPool()
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helper tags
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func mediaTag(isMedia bool) string {
|
||
if isMedia {
|
||
return "ᵐ"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func mediaLabel(isMedia bool) string {
|
||
if isMedia {
|
||
return "медиа"
|
||
}
|
||
return "основной"
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// HTTP detection
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func isHTTPTransport(data []byte) bool {
|
||
if len(data) < 4 {
|
||
return false
|
||
}
|
||
return string(data[:4]) == "POST" ||
|
||
string(data[:3]) == "GET" ||
|
||
string(data[:4]) == "HEAD" ||
|
||
string(data[:7]) == "OPTIONS"
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// SOCKS5 reply
|
||
// ---------------------------------------------------------------------------
|
||
|
||
var socks5Replies = map[byte][]byte{
|
||
0x00: {0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0},
|
||
0x05: {0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0},
|
||
0x07: {0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0},
|
||
0x08: {0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0},
|
||
}
|
||
|
||
func socks5Reply(status byte) []byte {
|
||
if r, ok := socks5Replies[status]; ok {
|
||
return r
|
||
}
|
||
return []byte{0x05, status, 0x00, 0x01, 0, 0, 0, 0, 0, 0}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Bridging: TCP <-> WebSocket
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func bridgeWS(ctx context.Context, conn net.Conn, ws *RawWebSocket,
|
||
label string, dc int, dst string, port int, isMedia bool,
|
||
splitter *MsgSplitter, cltDec, cltEnc, tgEnc, tgDec cipher.Stream) {
|
||
|
||
dcTag := fmt.Sprintf("DC%d%s", dc, mediaTag(isMedia))
|
||
var upBytes, downBytes int64
|
||
startTime := time.Now()
|
||
|
||
ctx2, cancel := context.WithCancel(ctx)
|
||
|
||
// Critical: close connections when context is cancelled
|
||
// This unblocks the Read() calls in goroutines
|
||
go func() {
|
||
<-ctx2.Done()
|
||
_ = conn.Close()
|
||
ws.Close()
|
||
}()
|
||
|
||
var wg sync.WaitGroup
|
||
wg.Add(2)
|
||
|
||
// tcp -> ws
|
||
go func() {
|
||
defer wg.Done()
|
||
defer cancel()
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
n, err := conn.Read(buf)
|
||
if n > 0 {
|
||
chunk := buf[:n]
|
||
stats.bytesUp.Add(int64(n))
|
||
upBytes += int64(n)
|
||
|
||
cltDec.XORKeyStream(chunk, chunk)
|
||
tgEnc.XORKeyStream(chunk, chunk)
|
||
var sendErr error
|
||
if splitter != nil {
|
||
parts := splitter.Split(chunk)
|
||
if len(parts) > 1 {
|
||
sendErr = ws.SendBatch(parts)
|
||
} else if len(parts) == 1 {
|
||
sendErr = ws.Send(parts[0])
|
||
}
|
||
// len(parts) == 0 means data is buffered, waiting for more
|
||
} else {
|
||
sendErr = ws.Send(chunk)
|
||
}
|
||
if sendErr != nil {
|
||
return
|
||
}
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
|
||
// ws -> tcp
|
||
go func() {
|
||
defer wg.Done()
|
||
defer cancel()
|
||
for {
|
||
data, err := ws.Recv()
|
||
if err != nil || data == nil {
|
||
return
|
||
}
|
||
n := len(data)
|
||
stats.bytesDown.Add(int64(n))
|
||
downBytes += int64(n)
|
||
tgDec.XORKeyStream(data, data)
|
||
cltEnc.XORKeyStream(data, data)
|
||
if _, err := conn.Write(data); err != nil {
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
|
||
wg.Wait()
|
||
|
||
elapsed := time.Since(startTime).Seconds()
|
||
if upBytes > 0 || downBytes > 0 {
|
||
logInfo.Printf("✕ %s ↑%s ↓%s %.1fс",
|
||
dcTag, humanBytes(upBytes), humanBytes(downBytes), elapsed)
|
||
} else {
|
||
logDebug.Printf("✕ %s пустое (%.1fс)", dcTag, elapsed)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Bridging: TCP <-> TCP (fallback)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func bridgeTCP(ctx context.Context, client, remote net.Conn,
|
||
label string, dc int, dst string, port int, isMedia bool, cltDec, cltEnc, tgEnc, tgDec cipher.Stream) {
|
||
|
||
ctx2, cancel := context.WithCancel(ctx)
|
||
|
||
// Close connections when context cancelled
|
||
go func() {
|
||
<-ctx2.Done()
|
||
_ = client.Close()
|
||
_ = remote.Close()
|
||
}()
|
||
|
||
var wg sync.WaitGroup
|
||
wg.Add(2)
|
||
|
||
forward := func(src, dstW net.Conn, isUp bool) {
|
||
defer wg.Done()
|
||
defer cancel()
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
n, err := src.Read(buf)
|
||
if n > 0 {
|
||
if isUp {
|
||
stats.bytesUp.Add(int64(n))
|
||
cltDec.XORKeyStream(buf[:n], buf[:n])
|
||
tgEnc.XORKeyStream(buf[:n], buf[:n])
|
||
} else {
|
||
stats.bytesDown.Add(int64(n))
|
||
tgDec.XORKeyStream(buf[:n], buf[:n])
|
||
cltEnc.XORKeyStream(buf[:n], buf[:n])
|
||
}
|
||
if _, werr := dstW.Write(buf[:n]); werr != nil {
|
||
return
|
||
}
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
go forward(client, remote, true)
|
||
go forward(remote, client, false)
|
||
|
||
wg.Wait()
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TCP fallback
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func tcpFallback(ctx context.Context, client net.Conn, dst string, port int,
|
||
init []byte, label string, dc int, isMedia bool, cltDec, cltEnc, tgEnc, tgDec cipher.Stream) bool {
|
||
|
||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
||
remote, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", dst, port))
|
||
if err != nil {
|
||
logWarn.Printf("⚠ DC%d TCP→%s не удался", dc, dst)
|
||
logDebug.Printf("TCP fallback error [%s] %s:%d: %v", label, dst, port, err)
|
||
return false
|
||
}
|
||
|
||
stats.connectionsTcpFallback.Add(1)
|
||
logInfo.Printf("🔄 DC%d%s подключен по TCP", dc, mediaTag(isMedia))
|
||
_, _ = remote.Write(init)
|
||
bridgeTCP(ctx, client, remote, label, dc, dst, port, isMedia, cltDec, cltEnc, tgEnc, tgDec)
|
||
return true
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Cloudflare proxy fallback
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func cfproxyFallback(ctx context.Context, conn net.Conn, relayInit []byte, label string,
|
||
dc int, isMedia bool, splitter *MsgSplitter,
|
||
cltDec, cltEnc, tgEnc, tgDec cipher.Stream) bool {
|
||
|
||
cfproxyMu.RLock()
|
||
if !cfproxyEnabled || len(cfproxyDomains) == 0 {
|
||
cfproxyMu.RUnlock()
|
||
return false
|
||
}
|
||
active := activeCfDomain
|
||
domains := make([]string, len(cfproxyDomains))
|
||
copy(domains, cfproxyDomains)
|
||
cfproxyMu.RUnlock()
|
||
|
||
ordered := []string{active}
|
||
for _, d := range domains {
|
||
if d != active {
|
||
ordered = append(ordered, d)
|
||
}
|
||
}
|
||
|
||
mTag := mediaTag(isMedia)
|
||
logDebug.Printf("☁ DC%d%s → пробуем CF", dc, mTag)
|
||
|
||
type wsResult struct {
|
||
ws *RawWebSocket
|
||
domain string
|
||
}
|
||
ch := make(chan wsResult, len(ordered))
|
||
for _, baseDomain := range ordered {
|
||
go func(bd string) {
|
||
domain := fmt.Sprintf("kws%d.%s", dc, bd)
|
||
ws, err := wsConnect(domain, domain, "/apiws", 5)
|
||
if err != nil {
|
||
logDebug.Printf("☁ DC%d%s CF %s ✗: %v", dc, mTag, domain, err)
|
||
ch <- wsResult{nil, ""}
|
||
return
|
||
}
|
||
ch <- wsResult{ws, bd}
|
||
}(baseDomain)
|
||
}
|
||
|
||
var ws *RawWebSocket
|
||
var chosenDomain string
|
||
for i := 0; i < len(ordered); i++ {
|
||
r := <-ch
|
||
if r.ws != nil && ws == nil {
|
||
ws = r.ws
|
||
chosenDomain = r.domain
|
||
} else if r.ws != nil {
|
||
go r.ws.Close()
|
||
}
|
||
}
|
||
|
||
if ws == nil {
|
||
return false
|
||
}
|
||
|
||
if chosenDomain != "" && chosenDomain != active {
|
||
cfproxyMu.Lock()
|
||
activeCfDomain = chosenDomain
|
||
cfproxyMu.Unlock()
|
||
logInfo.Printf("☁ CF домен → %s", chosenDomain)
|
||
}
|
||
|
||
stats.connectionsCfproxy.Add(1)
|
||
logInfo.Printf("☁ DC%d%s подключен через CF", dc, mTag)
|
||
|
||
if err := ws.Send(relayInit); err != nil {
|
||
ws.Close()
|
||
return false
|
||
}
|
||
|
||
bridgeWS(ctx, conn, ws, label, dc, chosenDomain, 443, isMedia, splitter, cltDec, cltEnc, tgEnc, tgDec)
|
||
return true
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Unified fallback (CF + TCP)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func doFallback(ctx context.Context, conn net.Conn, relayInit []byte, label string,
|
||
dc int, isMedia bool, splitter *MsgSplitter,
|
||
cltDec, cltEnc, tgEnc, tgDec cipher.Stream) bool {
|
||
|
||
// Use configured DC IP if available, otherwise fall back to defaults
|
||
var fallbackDst string
|
||
dcOptMu.RLock()
|
||
if ip, ok := dcOpt[dc]; ok && ip != "" {
|
||
fallbackDst = ip
|
||
}
|
||
dcOptMu.RUnlock()
|
||
if fallbackDst == "" {
|
||
fallbackDst = dcDefaultIPs[dc]
|
||
}
|
||
|
||
cfproxyMu.RLock()
|
||
useCf := cfproxyEnabled
|
||
cfproxyMu.RUnlock()
|
||
|
||
mTag := mediaTag(isMedia)
|
||
|
||
type fbMethod string
|
||
var methods []fbMethod
|
||
if useCf {
|
||
methods = []fbMethod{"cf"}
|
||
} else {
|
||
methods = []fbMethod{"tcp"}
|
||
}
|
||
|
||
for _, m := range methods {
|
||
switch m {
|
||
case "cf":
|
||
if cfproxyFallback(ctx, conn, relayInit, label, dc, isMedia, splitter, cltDec, cltEnc, tgEnc, tgDec) {
|
||
return true
|
||
}
|
||
case "tcp":
|
||
if fallbackDst != "" {
|
||
logDebug.Printf("🔄 DC%d%s → TCP %s:443", dc, mTag, fallbackDst)
|
||
if tcpFallback(ctx, conn, fallbackDst, 443, relayInit, label, dc, isMedia, cltDec, cltEnc, tgEnc, tgDec) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
logWarn.Printf("⚠ DC%d%s нет доступных маршрутов", dc, mTag)
|
||
return false
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pipe (non-Telegram passthrough)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func pipe(ctx context.Context, src, dst net.Conn, done chan<- struct{}) {
|
||
defer func() { done <- struct{}{} }()
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
default:
|
||
}
|
||
n, err := src.Read(buf)
|
||
if n > 0 {
|
||
if _, werr := dst.Write(buf[:n]); werr != nil {
|
||
return
|
||
}
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Fake TLS support (ee-secret)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const (
|
||
tlsRecordHandshake = 0x16
|
||
tlsRecordCCS = 0x14
|
||
tlsRecordAppData = 0x17
|
||
tlsAppDataMax = 16384
|
||
clientRandomOffset = 11
|
||
clientRandomLen = 32
|
||
sessionIdOffset = 44
|
||
sessionIdLen = 32
|
||
timestampTolerance = 120
|
||
)
|
||
|
||
// verifyClientHello checks whether the incoming ClientHello has a valid
|
||
// HMAC-SHA256 computed over the hello with the random field zeroed,
|
||
// using `secret` as key. Returns (clientRandom, sessionId, ok).
|
||
func verifyClientHello(data, secret []byte) ([]byte, []byte, bool) {
|
||
n := len(data)
|
||
if n < 43 {
|
||
return nil, nil, false
|
||
}
|
||
if data[0] != tlsRecordHandshake {
|
||
return nil, nil, false
|
||
}
|
||
if data[5] != 0x01 {
|
||
return nil, nil, false
|
||
}
|
||
|
||
clientRandom := make([]byte, clientRandomLen)
|
||
copy(clientRandom, data[clientRandomOffset:clientRandomOffset+clientRandomLen])
|
||
|
||
zeroed := make([]byte, n)
|
||
copy(zeroed, data)
|
||
for i := 0; i < clientRandomLen; i++ {
|
||
zeroed[clientRandomOffset+i] = 0
|
||
}
|
||
|
||
mac := hmacSHA256(secret, zeroed)
|
||
|
||
for i := 0; i < 28; i++ {
|
||
if mac[i] != clientRandom[i] {
|
||
return nil, nil, false
|
||
}
|
||
}
|
||
|
||
// Check timestamp
|
||
tsXor := make([]byte, 4)
|
||
for i := 0; i < 4; i++ {
|
||
tsXor[i] = clientRandom[28+i] ^ mac[28+i]
|
||
}
|
||
timestamp := binary.LittleEndian.Uint32(tsXor)
|
||
now := uint32(time.Now().Unix())
|
||
diff := int64(now) - int64(timestamp)
|
||
if diff < 0 {
|
||
diff = -diff
|
||
}
|
||
if diff > timestampTolerance {
|
||
return nil, nil, false
|
||
}
|
||
|
||
sessionId := make([]byte, sessionIdLen)
|
||
if n >= sessionIdOffset+sessionIdLen && data[43] == 0x20 {
|
||
copy(sessionId, data[sessionIdOffset:sessionIdOffset+sessionIdLen])
|
||
}
|
||
|
||
return clientRandom, sessionId, true
|
||
}
|
||
|
||
func hmacSHA256(key, data []byte) []byte {
|
||
h := hmac.New(sha256.New, key)
|
||
h.Write(data)
|
||
return h.Sum(nil)
|
||
}
|
||
|
||
var serverHelloTemplate = []byte{
|
||
0x16, 0x03, 0x03, 0x00, 0x7a,
|
||
0x02, 0x00, 0x00, 0x76,
|
||
0x03, 0x03,
|
||
// 32 bytes server random (offset 11)
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0x20,
|
||
// 32 bytes session id (offset 44)
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0x13, 0x01, 0x00,
|
||
0x00, 0x2e,
|
||
0x00, 0x33, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20,
|
||
// 32 bytes public key (offset 89)
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0x00, 0x2b, 0x00, 0x02, 0x03, 0x04,
|
||
}
|
||
|
||
const (
|
||
shRandomOff = 11
|
||
shSessIdOff = 44
|
||
shPubKeyOff = 89
|
||
)
|
||
|
||
func buildServerHello(secret, clientRandom, sessionId []byte) []byte {
|
||
sh := make([]byte, len(serverHelloTemplate))
|
||
copy(sh, serverHelloTemplate)
|
||
|
||
copy(sh[shSessIdOff:shSessIdOff+32], sessionId)
|
||
|
||
pubKey := make([]byte, 32)
|
||
rand.Read(pubKey)
|
||
copy(sh[shPubKeyOff:shPubKeyOff+32], pubKey)
|
||
|
||
ccsFrame := []byte{0x14, 0x03, 0x03, 0x00, 0x01, 0x01}
|
||
|
||
encSize := 1900 + int(time.Now().UnixNano()%200)
|
||
encData := make([]byte, encSize)
|
||
rand.Read(encData)
|
||
appRecord := make([]byte, 5+encSize)
|
||
appRecord[0] = 0x17
|
||
appRecord[1] = 0x03
|
||
appRecord[2] = 0x03
|
||
binary.BigEndian.PutUint16(appRecord[3:5], uint16(encSize))
|
||
copy(appRecord[5:], encData)
|
||
|
||
response := make([]byte, 0, len(sh)+len(ccsFrame)+len(appRecord))
|
||
response = append(response, sh...)
|
||
response = append(response, ccsFrame...)
|
||
response = append(response, appRecord...)
|
||
|
||
hmacInput := make([]byte, 0, len(clientRandom)+len(response))
|
||
hmacInput = append(hmacInput, clientRandom...)
|
||
hmacInput = append(hmacInput, response...)
|
||
serverRandom := hmacSHA256(secret, hmacInput)
|
||
|
||
copy(response[shRandomOff:shRandomOff+32], serverRandom)
|
||
|
||
return response
|
||
}
|
||
|
||
func wrapTlsRecord(data []byte) []byte {
|
||
var parts []byte
|
||
offset := 0
|
||
for offset < len(data) {
|
||
end := offset + tlsAppDataMax
|
||
if end > len(data) {
|
||
end = len(data)
|
||
}
|
||
chunk := data[offset:end]
|
||
hdr := []byte{0x17, 0x03, 0x03, 0, 0}
|
||
binary.BigEndian.PutUint16(hdr[3:5], uint16(len(chunk)))
|
||
parts = append(parts, hdr...)
|
||
parts = append(parts, chunk...)
|
||
offset = end
|
||
}
|
||
return parts
|
||
}
|
||
|
||
// FakeTlsConn wraps a net.Conn, transparently unwrapping TLS AppData
|
||
// records on read and wrapping data in TLS AppData on write.
|
||
type FakeTlsConn struct {
|
||
conn net.Conn
|
||
readBuf []byte
|
||
readLeft int // remaining bytes in current TLS record
|
||
}
|
||
|
||
func newFakeTlsConn(conn net.Conn) *FakeTlsConn {
|
||
return &FakeTlsConn{conn: conn}
|
||
}
|
||
|
||
func (f *FakeTlsConn) Read(p []byte) (int, error) {
|
||
// If we have buffered data, return it first
|
||
if len(f.readBuf) > 0 {
|
||
n := copy(p, f.readBuf)
|
||
f.readBuf = f.readBuf[n:]
|
||
return n, nil
|
||
}
|
||
|
||
// If we're in the middle of a record, read remaining
|
||
if f.readLeft > 0 {
|
||
toRead := f.readLeft
|
||
if toRead > len(p) {
|
||
toRead = len(p)
|
||
}
|
||
n, err := f.conn.Read(p[:toRead])
|
||
f.readLeft -= n
|
||
return n, err
|
||
}
|
||
|
||
// Read next TLS record header
|
||
for {
|
||
hdr := make([]byte, 5)
|
||
if _, err := io.ReadFull(f.conn, hdr); err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
rtype := hdr[0]
|
||
recLen := int(binary.BigEndian.Uint16(hdr[3:5]))
|
||
|
||
if rtype == tlsRecordCCS {
|
||
// Skip CCS records
|
||
if recLen > 0 {
|
||
discard := make([]byte, recLen)
|
||
if _, err := io.ReadFull(f.conn, discard); err != nil {
|
||
return 0, err
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
if rtype != tlsRecordAppData {
|
||
return 0, fmt.Errorf("unexpected TLS record type 0x%02X", rtype)
|
||
}
|
||
|
||
// Read up to len(p) from this record
|
||
toRead := recLen
|
||
if toRead > len(p) {
|
||
toRead = len(p)
|
||
}
|
||
n, err := io.ReadAtLeast(f.conn, p[:toRead], 1)
|
||
f.readLeft = recLen - n
|
||
return n, err
|
||
}
|
||
}
|
||
|
||
func (f *FakeTlsConn) Write(p []byte) (int, error) {
|
||
wrapped := wrapTlsRecord(p)
|
||
_, err := f.conn.Write(wrapped)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return len(p), nil
|
||
}
|
||
|
||
func (f *FakeTlsConn) Close() error {
|
||
return f.conn.Close()
|
||
}
|
||
|
||
func (f *FakeTlsConn) LocalAddr() net.Addr {
|
||
return f.conn.LocalAddr()
|
||
}
|
||
|
||
func (f *FakeTlsConn) RemoteAddr() net.Addr {
|
||
return f.conn.RemoteAddr()
|
||
}
|
||
|
||
func (f *FakeTlsConn) SetDeadline(t time.Time) error {
|
||
return f.conn.SetDeadline(t)
|
||
}
|
||
|
||
func (f *FakeTlsConn) SetReadDeadline(t time.Time) error {
|
||
return f.conn.SetReadDeadline(t)
|
||
}
|
||
|
||
func (f *FakeTlsConn) SetWriteDeadline(t time.Time) error {
|
||
return f.conn.SetWriteDeadline(t)
|
||
}
|
||
|
||
func handleClient(ctx context.Context, conn net.Conn) {
|
||
stats.connectionsTotal.Add(1)
|
||
stats.connectionsActive.Add(1)
|
||
defer func() {
|
||
if stats.connectionsActive.Load() > 0 {
|
||
stats.connectionsActive.Add(-1)
|
||
}
|
||
}()
|
||
peer := conn.RemoteAddr().String()
|
||
label := peer
|
||
|
||
setSockOpts(conn)
|
||
defer conn.Close()
|
||
|
||
proxySecretMu.RLock()
|
||
currentSecret := proxySecret
|
||
proxySecretMu.RUnlock()
|
||
secretBytes, _ := hex.DecodeString(currentSecret)
|
||
|
||
// Read first byte to detect FakeTLS vs plain
|
||
firstByte := make([]byte, 1)
|
||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||
if _, err := io.ReadFull(conn, firstByte); err != nil {
|
||
logDebug.Printf("клиент отключился до рукопожатия")
|
||
return
|
||
}
|
||
_ = conn.SetReadDeadline(time.Time{})
|
||
|
||
var clientConn net.Conn = conn // the connection we read MTProto from
|
||
var handshake []byte
|
||
|
||
if firstByte[0] == tlsRecordHandshake {
|
||
// FakeTLS mode (ee-secret)
|
||
hdrRest := make([]byte, 4)
|
||
if _, err := io.ReadFull(conn, hdrRest); err != nil {
|
||
logDebug.Printf("неполный TLS-заголовок")
|
||
return
|
||
}
|
||
tlsHeader := append(firstByte, hdrRest...)
|
||
recordLen := int(binary.BigEndian.Uint16(tlsHeader[3:5]))
|
||
|
||
recordBody := make([]byte, recordLen)
|
||
if _, err := io.ReadFull(conn, recordBody); err != nil {
|
||
logDebug.Printf("неполное тело TLS-записи")
|
||
return
|
||
}
|
||
|
||
clientHello := append(tlsHeader, recordBody...)
|
||
|
||
clientRandom, sessionId, ok := verifyClientHello(clientHello, secretBytes)
|
||
if !ok {
|
||
stats.connectionsBad.Add(1)
|
||
logWarn.Printf("⚠ bad handshake")
|
||
return
|
||
}
|
||
|
||
logDebug.Printf("FakeTLS рукопожатие ОК")
|
||
|
||
serverHello := buildServerHello(secretBytes, clientRandom, sessionId)
|
||
if _, err := conn.Write(serverHello); err != nil {
|
||
return
|
||
}
|
||
|
||
tlsConn := newFakeTlsConn(conn)
|
||
clientConn = tlsConn
|
||
|
||
handshake = make([]byte, 64)
|
||
if _, err := io.ReadFull(tlsConn, handshake); err != nil {
|
||
logDebug.Printf("неполный обфускированный init внутри TLS")
|
||
return
|
||
}
|
||
} else {
|
||
// Plain obfuscated mode (dd-secret)
|
||
rest := make([]byte, 63)
|
||
if _, err := io.ReadFull(conn, rest); err != nil {
|
||
logDebug.Printf("клиент отключился до рукопожатия")
|
||
return
|
||
}
|
||
handshake = append(firstByte, rest...)
|
||
}
|
||
|
||
cltDecPrekey := handshake[8:40]
|
||
cltDecIv := handshake[40:56]
|
||
|
||
hashDec := sha256.New()
|
||
hashDec.Write(cltDecPrekey)
|
||
hashDec.Write(secretBytes)
|
||
cltDecKey := hashDec.Sum(nil)
|
||
|
||
cltDecryptor, err := newAESCTR(cltDecKey, cltDecIv)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
decrypted := make([]byte, 64)
|
||
cltDecryptor.XORKeyStream(decrypted, handshake)
|
||
|
||
protoTag := decrypted[56:60]
|
||
proto := binary.LittleEndian.Uint32(protoTag)
|
||
if !validProtos[proto] {
|
||
stats.connectionsBad.Add(1)
|
||
logWarn.Printf("⚠ bad handshake")
|
||
return
|
||
}
|
||
|
||
dcRaw := int16(binary.LittleEndian.Uint16(decrypted[60:62]))
|
||
dc := int(dcRaw)
|
||
if dc < 0 {
|
||
dc = -dc
|
||
}
|
||
isMedia := dcRaw < 0
|
||
|
||
logInfo.Printf("→ DC%d %s", dc, mediaLabel(isMedia))
|
||
|
||
// Encryption back to client
|
||
cltEncPrekeyAndIv := make([]byte, 48)
|
||
for i := 0; i < 48; i++ {
|
||
cltEncPrekeyAndIv[i] = handshake[8+47-i]
|
||
}
|
||
cltEncPrekey := cltEncPrekeyAndIv[:32]
|
||
cltEncIv := cltEncPrekeyAndIv[32:]
|
||
|
||
hashEnc := sha256.New()
|
||
hashEnc.Write(cltEncPrekey)
|
||
hashEnc.Write(secretBytes)
|
||
cltEncKey := hashEnc.Sum(nil)
|
||
cltEncryptor, _ := newAESCTR(cltEncKey, cltEncIv)
|
||
|
||
// cltDecryptor was already advanced by 64 bytes during handshake decryption.
|
||
// cltEncryptor does NOT need to be advanced according to Python logic.
|
||
|
||
// Generate relay Init
|
||
relayInit := make([]byte, 64)
|
||
for {
|
||
rand.Read(relayInit)
|
||
if relayInit[0] == 0xEF { continue }
|
||
s := string(relayInit[:4])
|
||
if s == "HEAD" || s == "POST" || s == "GET " || s == "\xee\xee\xee\xee" || s == "\xdd\xdd\xdd\xdd" {
|
||
continue
|
||
}
|
||
// TLS ClientHello start
|
||
if relayInit[0] == 0x16 && relayInit[1] == 0x03 && relayInit[2] == 0x01 && relayInit[3] == 0x02 {
|
||
continue
|
||
}
|
||
// Reserved continuation bytes
|
||
if relayInit[4] == 0 && relayInit[5] == 0 && relayInit[6] == 0 && relayInit[7] == 0 {
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
|
||
tgEncKey := relayInit[8:40]
|
||
tgEncIv := relayInit[40:56]
|
||
|
||
tgDecPrekeyAndIv := make([]byte, 48)
|
||
for i := 0; i < 48; i++ {
|
||
tgDecPrekeyAndIv[i] = relayInit[8+47-i]
|
||
}
|
||
tgDecKey := tgDecPrekeyAndIv[:32]
|
||
tgDecIv := tgDecPrekeyAndIv[32:]
|
||
|
||
tgEncryptor, _ := newAESCTR(tgEncKey, tgEncIv)
|
||
tgDecryptor, _ := newAESCTR(tgDecKey, tgDecIv)
|
||
|
||
dcBytes := make([]byte, 2)
|
||
dcIdx := dc
|
||
if isMedia {
|
||
dcIdx = -dc
|
||
}
|
||
binary.LittleEndian.PutUint16(dcBytes, uint16(dcIdx))
|
||
|
||
tailPlain := make([]byte, 8)
|
||
copy(tailPlain[0:4], protoTag)
|
||
copy(tailPlain[4:6], dcBytes)
|
||
rand.Read(tailPlain[6:8])
|
||
|
||
encryptedFull := make([]byte, 64)
|
||
tgEncryptor.XORKeyStream(encryptedFull, relayInit)
|
||
|
||
keystreamTail := make([]byte, 8)
|
||
for i := 0; i < 8; i++ {
|
||
keystreamTail[i] = encryptedFull[56+i] ^ relayInit[56+i]
|
||
relayInit[56+i] = tailPlain[i] ^ keystreamTail[i]
|
||
}
|
||
// tgEncryptor was already advanced by 64 bytes above.
|
||
// tgDecryptor does NOT need to be advanced according to Python logic.
|
||
|
||
mTag := mediaTag(isMedia)
|
||
dcKey := [2]int{dc, isMediaInt(isMedia)}
|
||
now := monoNow()
|
||
|
||
// Splitting MTProto if needed.
|
||
splitter, _ := newMsgSplitter(relayInit, proto)
|
||
|
||
dcOptMu.RLock()
|
||
target, dcConfigured := dcOpt[dc]
|
||
dcOptMu.RUnlock()
|
||
|
||
wsBlackMu.RLock()
|
||
blacklisted := wsBlacklist[dcKey]
|
||
wsBlackMu.RUnlock()
|
||
|
||
if !dcConfigured || blacklisted {
|
||
if !dcConfigured {
|
||
logDebug.Printf("DC%d не настроен → резерв", dc)
|
||
} else {
|
||
logDebug.Printf("DC%d%s WS заблокирован → резерв", dc, mTag)
|
||
}
|
||
ok := doFallback(ctx, clientConn, relayInit, label, dc, isMedia, splitter, cltDecryptor, cltEncryptor, tgEncryptor, tgDecryptor)
|
||
if ok {
|
||
logDebug.Printf("DC%d%s резерв закрыт", dc, mTag)
|
||
}
|
||
return
|
||
}
|
||
|
||
dcFailMu.RLock()
|
||
failUntil := dcFailUntil[dcKey]
|
||
dcFailMu.RUnlock()
|
||
|
||
wsTimeout := 10.0
|
||
if now < failUntil { wsTimeout = wsFailTimeout }
|
||
|
||
isMediaForDomains := isMedia
|
||
domains := wsDomains(dc, &isMediaForDomains)
|
||
|
||
var ws *RawWebSocket
|
||
wsFailedRedirect := false
|
||
allRedirects := true
|
||
|
||
ws = wsPool.Get(dc, isMedia, target, domains)
|
||
if ws != nil {
|
||
logInfo.Printf("⚡ DC%d%s подключен из пула", dc, mTag)
|
||
} else {
|
||
for _, domain := range domains {
|
||
logDebug.Printf("🔗 DC%d%s попытка WS %s", dc, mTag, domain)
|
||
|
||
var connErr error
|
||
ws, connErr = wsConnect(target, domain, "/apiws", wsTimeout)
|
||
if connErr == nil {
|
||
logInfo.Printf("🔗 DC%d%s подключен напрямую", dc, mTag)
|
||
allRedirects = false
|
||
break
|
||
}
|
||
|
||
stats.wsErrors.Add(1)
|
||
if wsErr, ok := connErr.(*WsHandshakeError); ok {
|
||
if wsErr.IsRedirect() {
|
||
wsFailedRedirect = true
|
||
continue
|
||
}
|
||
allRedirects = false
|
||
} else {
|
||
allRedirects = false
|
||
}
|
||
}
|
||
}
|
||
|
||
if ws == nil {
|
||
if wsFailedRedirect && allRedirects {
|
||
wsBlackMu.Lock()
|
||
wsBlacklist[dcKey] = true
|
||
wsBlackMu.Unlock()
|
||
logWarn.Printf("⚠ DC%d%s заблокирован (302)", dc, mTag)
|
||
} else if wsFailedRedirect {
|
||
dcFailMu.Lock()
|
||
dcFailUntil[dcKey] = now + dcFailCooldown
|
||
dcFailMu.Unlock()
|
||
} else {
|
||
dcFailMu.Lock()
|
||
dcFailUntil[dcKey] = now + dcFailCooldown
|
||
dcFailMu.Unlock()
|
||
logDebug.Printf("DC%d%s кулдаун %dс", dc, mTag, int(dcFailCooldown))
|
||
}
|
||
|
||
splitterFb, _ := newMsgSplitter(relayInit, proto)
|
||
ok := doFallback(ctx, clientConn, relayInit, label, dc, isMedia, splitterFb, cltDecryptor, cltEncryptor, tgEncryptor, tgDecryptor)
|
||
if ok {
|
||
logDebug.Printf("DC%d%s резерв закрыт", dc, mTag)
|
||
}
|
||
return
|
||
}
|
||
|
||
dcFailMu.Lock()
|
||
delete(dcFailUntil, dcKey)
|
||
dcFailMu.Unlock()
|
||
|
||
stats.connectionsWs.Add(1)
|
||
|
||
if err := ws.Send(relayInit); err != nil {
|
||
ws.Close()
|
||
tcpFallback(ctx, clientConn, target, 443, relayInit, label, dc, isMedia, cltDecryptor, cltEncryptor, tgEncryptor, tgDecryptor)
|
||
return
|
||
}
|
||
|
||
bridgeWS(ctx, clientConn, ws, label, dc, target, 443, isMedia, splitter, cltDecryptor, cltEncryptor, tgEncryptor, tgDecryptor)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Server
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func runProxy(ctx context.Context, host string, port int, dcOptMap map[int]string) error {
|
||
dcOptMu.Lock()
|
||
dcOpt = dcOptMap
|
||
dcOptMu.Unlock()
|
||
|
||
addr := fmt.Sprintf("%s:%d", host, port)
|
||
lc := net.ListenConfig{}
|
||
|
||
listener, err := lc.Listen(ctx, "tcp", addr)
|
||
if err != nil {
|
||
return fmt.Errorf("listen on %s: %w", addr, err)
|
||
}
|
||
|
||
if tcpL, ok := listener.(*net.TCPListener); ok {
|
||
raw, err := tcpL.SyscallConn()
|
||
if err == nil {
|
||
_ = raw.Control(func(fd uintptr) {
|
||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_NODELAY, 1)
|
||
})
|
||
}
|
||
}
|
||
|
||
srvCtx, srvCancel := context.WithCancel(ctx)
|
||
defer srvCancel()
|
||
|
||
logInfo.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||
logInfo.Println(" TG WS Proxy запущен")
|
||
logInfo.Printf(" Адрес: %s:%d", host, port)
|
||
for dc, ip := range dcOptMap {
|
||
logInfo.Printf(" DC%d → %s", dc, ip)
|
||
}
|
||
proxySecretMu.RLock()
|
||
currentSec := proxySecret
|
||
proxySecretMu.RUnlock()
|
||
logInfo.Printf(" Ключ: ee%s", currentSec)
|
||
logInfo.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||
|
||
// Stats logger
|
||
go func() {
|
||
ticker := time.NewTicker(60 * time.Second)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-srvCtx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
idleCount := wsPool.IdleCount()
|
||
logInfo.Printf("📊 %s | пул:%d", stats.SummaryRu(), idleCount)
|
||
}
|
||
}
|
||
}()
|
||
|
||
// Warmup WS pool
|
||
wsPool.Warmup(dcOptMap)
|
||
|
||
// Periodic pool maintenance
|
||
go wsPool.Maintain(srvCtx, dcOptMap)
|
||
|
||
// Track active connections for graceful shutdown
|
||
var activeConns sync.WaitGroup
|
||
|
||
// Accept loop
|
||
go func() {
|
||
for {
|
||
conn, err := listener.Accept()
|
||
if err != nil {
|
||
select {
|
||
case <-srvCtx.Done():
|
||
return
|
||
default:
|
||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||
continue
|
||
}
|
||
logError.Printf("ошибка accept: %v", err)
|
||
return
|
||
}
|
||
}
|
||
activeConns.Add(1)
|
||
go func() {
|
||
defer activeConns.Done()
|
||
handleClient(srvCtx, conn)
|
||
}()
|
||
}
|
||
}()
|
||
|
||
// Wait for context cancellation
|
||
<-srvCtx.Done()
|
||
logInfo.Println("⏹ Остановка прокси...")
|
||
_ = listener.Close()
|
||
|
||
// Wait for active connections with timeout
|
||
done := make(chan struct{})
|
||
go func() {
|
||
activeConns.Wait()
|
||
close(done)
|
||
}()
|
||
|
||
select {
|
||
case <-done:
|
||
logInfo.Println("✓ Все соединения закрыты")
|
||
case <-time.After(30 * time.Second):
|
||
logWarn.Println("⚠ Таймаут завершения (30с)")
|
||
}
|
||
|
||
// Close pool connections
|
||
wsPool.CloseAll()
|
||
|
||
logInfo.Printf("📊 Итого: %s", stats.SummaryRu())
|
||
return nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Parse DC:IP list / CIDR pool
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func parseCIDRPool(cidrsStr string) (map[int]string, error) {
|
||
result := make(map[int]string)
|
||
|
||
pairs := strings.Split(cidrsStr, ",")
|
||
for _, pair := range pairs {
|
||
parts := strings.Split(pair, ":")
|
||
if len(parts) == 2 {
|
||
dcRaw := strings.TrimSpace(parts[0])
|
||
ipRaw := strings.TrimSpace(parts[1])
|
||
|
||
dc, err := strconv.Atoi(dcRaw)
|
||
if err == nil && ipRaw != "" {
|
||
result[dc] = ipRaw
|
||
}
|
||
}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CGO exports for Android .so
|
||
// ---------------------------------------------------------------------------
|
||
|
||
var (
|
||
globalCtx context.Context
|
||
globalCancel context.CancelFunc
|
||
globalMu sync.Mutex
|
||
)
|
||
|
||
//export StartProxy
|
||
func StartProxy(cHost *C.char, port C.int, cDcIps *C.char, verbose C.int) C.int {
|
||
globalMu.Lock()
|
||
defer globalMu.Unlock()
|
||
|
||
if globalCancel != nil {
|
||
return -1 // Already running
|
||
}
|
||
|
||
host := C.GoString(cHost)
|
||
goPort := int(port)
|
||
dcIpsStr := C.GoString(cDcIps)
|
||
isVerbose := int(verbose) != 0
|
||
|
||
initLogging(isVerbose)
|
||
initCfproxyDomains()
|
||
|
||
dcOptMap, err := parseCIDRPool(dcIpsStr)
|
||
if err != nil {
|
||
logError.Printf("ошибка разбора DC: %v", err)
|
||
return -2
|
||
}
|
||
|
||
globalCtx, globalCancel = context.WithCancel(context.Background())
|
||
|
||
go func() {
|
||
if err := runProxy(globalCtx, host, goPort, dcOptMap); err != nil {
|
||
logError.Printf("✗ Ошибка прокси: %v", err)
|
||
}
|
||
}()
|
||
|
||
return 0
|
||
}
|
||
|
||
//export StopProxy
|
||
func StopProxy() C.int {
|
||
globalMu.Lock()
|
||
defer globalMu.Unlock()
|
||
|
||
if globalCancel == nil {
|
||
return -1
|
||
}
|
||
|
||
globalCancel()
|
||
globalCancel = nil
|
||
globalCtx = nil
|
||
|
||
// Reset state
|
||
stats.Reset()
|
||
|
||
wsBlackMu.Lock()
|
||
wsBlacklist = make(map[[2]int]bool)
|
||
wsBlackMu.Unlock()
|
||
|
||
dcFailMu.Lock()
|
||
dcFailUntil = make(map[[2]int]float64)
|
||
dcFailMu.Unlock()
|
||
|
||
return 0
|
||
}
|
||
|
||
//export SetPoolSize
|
||
func SetPoolSize(size C.int) {
|
||
n := int(size)
|
||
if n < 2 {
|
||
n = 2
|
||
}
|
||
if n > 16 {
|
||
n = 16
|
||
}
|
||
poolSize = n
|
||
if logInfo != nil {
|
||
logInfo.Printf("⚙ Пул: %d", n)
|
||
}
|
||
}
|
||
|
||
//export SetCfProxyConfig
|
||
func SetCfProxyConfig(enabled C.int, priority C.int, cUserDomain *C.char) {
|
||
cfproxyMu.Lock()
|
||
defer cfproxyMu.Unlock()
|
||
|
||
cfproxyEnabled = int(enabled) != 0
|
||
cfproxyPriority = int(priority) != 0
|
||
|
||
userDomain := C.GoString(cUserDomain)
|
||
cfproxyUserDomain = userDomain
|
||
|
||
if userDomain != "" {
|
||
cfproxyDomains = []string{userDomain}
|
||
activeCfDomain = userDomain
|
||
}
|
||
|
||
if logInfo != nil {
|
||
status := "выкл"
|
||
if cfproxyEnabled {
|
||
status = "вкл"
|
||
}
|
||
prio := "TCP→CF"
|
||
if cfproxyPriority {
|
||
prio = "CF→TCP"
|
||
}
|
||
dom := activeCfDomain
|
||
if dom == "" {
|
||
dom = "авто"
|
||
}
|
||
logInfo.Printf("☁ CF: %s (%s) %s", status, prio, dom)
|
||
}
|
||
}
|
||
|
||
//export SetSecret
|
||
func SetSecret(cSecret *C.char) {
|
||
s := C.GoString(cSecret)
|
||
if len(s) != 32 {
|
||
if logWarn != nil {
|
||
logWarn.Printf("⚠ Ключ: неверная длина %d (нужно 32)", len(s))
|
||
}
|
||
return
|
||
}
|
||
// Validate hex
|
||
if _, err := hex.DecodeString(s); err != nil {
|
||
if logWarn != nil {
|
||
logWarn.Printf("⚠ Ключ: невалидный hex")
|
||
}
|
||
return
|
||
}
|
||
proxySecretMu.Lock()
|
||
proxySecret = s
|
||
proxySecretMu.Unlock()
|
||
if logInfo != nil {
|
||
logInfo.Printf("🔑 Ключ обновлён: ee%s...", s[:8])
|
||
}
|
||
}
|
||
|
||
//export GetStats
|
||
func GetStats() *C.char {
|
||
s := stats.Summary()
|
||
return C.CString(s)
|
||
}
|
||
|
||
//export FreeString
|
||
func FreeString(p *C.char) {
|
||
C.free(unsafe.Pointer(p))
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Standalone main
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func main() {
|
||
runtime.LockOSThread()
|
||
|
||
initLogging(false)
|
||
initCfproxyDomains()
|
||
|
||
dcOptMap := map[int]string{
|
||
2: "149.154.167.220",
|
||
4: "149.154.167.220",
|
||
}
|
||
|
||
host := "127.0.0.1"
|
||
port := defaultPort
|
||
|
||
args := os.Args[1:]
|
||
for i := 0; i < len(args); i++ {
|
||
switch args[i] {
|
||
case "--port":
|
||
if i+1 < len(args) {
|
||
i++
|
||
p, err := strconv.Atoi(args[i])
|
||
if err == nil {
|
||
port = p
|
||
}
|
||
}
|
||
case "--host":
|
||
if i+1 < len(args) {
|
||
i++
|
||
host = args[i]
|
||
}
|
||
case "-v", "--verbose":
|
||
initLogging(true)
|
||
case "--dc-ip":
|
||
if i+1 < len(args) {
|
||
i++
|
||
entry := args[i]
|
||
parsed, err := parseCIDRPool(entry)
|
||
if err != nil {
|
||
logError.Printf("%v", err)
|
||
os.Exit(1)
|
||
}
|
||
for k, v := range parsed {
|
||
dcOptMap[k] = v
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
|
||
sigCh := make(chan os.Signal, 1)
|
||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||
go func() {
|
||
sig := <-sigCh
|
||
logInfo.Printf("Received signal %v, shutting down...", sig)
|
||
cancel()
|
||
}()
|
||
|
||
if err := runProxy(ctx, host, port, dcOptMap); err != nil {
|
||
logError.Printf("Fatal: %v", err)
|
||
os.Exit(1)
|
||
}
|
||
} |