package main /* #cgo android LDFLAGS: -llog #include #include #ifdef __ANDROID__ #include #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) } }