Files
tg-ws-proxy/tg-ws-proxy.go
amurcanov 6953665a69 Update V1.0.6
не придирайтесь к ver name и другим символическим значениям. лень некоторые вещи менять.
2026-04-12 22:14:53 +03:00

2608 lines
60 KiB
Go
Raw 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
/*
#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)
}
}