init
Publish mtproxy_checker Docker image / test (push) Successful in 19s
Publish mtproxy_checker Docker image / build-and-push (push) Successful in 48s

This commit is contained in:
Denozordec
2026-04-11 00:38:22 +07:00
commit 4905c06b9b
26 changed files with 1887 additions and 0 deletions
+471
View File
@@ -0,0 +1,471 @@
// Генерация ClientHello по логике Telegram Desktop (mtproto_tls_socket.cpp, PrepareClientHelloRules).
// См. https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp
package faketls
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"fmt"
"math/big"
"time"
)
const (
kMaxGrease = 8
kClientHelloLimit = 2048
kHelloDigestLen = 32
kLengthSize = 2
kElementsM = 384
kAddedM = 32
)
// BuildTdesktopClientHello строит запись TLS 1.2 ClientHello как в tdesktop (Ed25519, permutation, ECH padding, HMAC).
func BuildTdesktopClientHello(secretKey []byte, domain []byte) (*ClientHello, error) {
if len(secretKey) != 16 {
return nil, fmt.Errorf("secret key must be 16 bytes")
}
greases := prepareGreases()
p := newTdesktopPart(domain, greases)
writeClientHelloRules(p)
if p.err {
return nil, fmt.Errorf("tdesktop client hello: build failed")
}
p.finalize(secretKey)
if p.err {
return nil, fmt.Errorf("tdesktop client hello: finalize failed")
}
digest := p.extractDigest()
out := p.take()
if len(out) == 0 {
return nil, fmt.Errorf("tdesktop client hello: empty buffer")
}
return &ClientHello{
Record: out,
RandomField: append([]byte(nil), digest...),
SessionID: p.sessionID(),
}, nil
}
func prepareGreases() []byte {
result := make([]byte, kMaxGrease)
if _, err := rand.Read(result); err != nil {
return bytesRepeat(0x0a, kMaxGrease)
}
for i := range result {
result[i] = (result[i] & 0xf0) + 0x0a
}
for i := 0; i < kMaxGrease; i += 2 {
if result[i] == result[i+1] {
result[i+1] ^= 0x10
}
}
return result
}
func bytesRepeat(b byte, n int) []byte {
out := make([]byte, n)
for i := range out {
out[i] = b
}
return out
}
type tdesktopPart struct {
domain []byte
greases []byte
buf []byte
digestPosition int
err bool
}
func newTdesktopPart(domain []byte, greases []byte) *tdesktopPart {
return &tdesktopPart{
domain: domain,
greases: greases,
digestPosition: -1,
}
}
func (p *tdesktopPart) grow(n int) []byte {
if p.err || n <= 0 || len(p.buf)+n > kClientHelloLimit {
p.err = true
return nil
}
off := len(p.buf)
p.buf = append(p.buf, make([]byte, n)...)
return p.buf[off : off+n]
}
func (p *tdesktopPart) writeString(b []byte) {
st := p.grow(len(b))
if st == nil {
return
}
copy(st, b)
}
func (p *tdesktopPart) writeZeros(n int) {
already := len(p.buf)
st := p.grow(n)
if st == nil {
return
}
if n == kHelloDigestLen && p.digestPosition < 0 {
p.digestPosition = already
}
clear(st)
}
func (p *tdesktopPart) writeGrease(seed int) {
if seed < 0 || seed >= len(p.greases) {
p.err = true
return
}
st := p.grow(2)
if st == nil {
return
}
// В tdesktop: bytes::set_with_const(storage, _greases[seed]) — оба байта равны значению seed.
st[0] = p.greases[seed]
st[1] = p.greases[seed]
}
func (p *tdesktopPart) writeRandom(n int) {
st := p.grow(n)
if st == nil {
return
}
if _, err := rand.Read(st); err != nil {
p.err = true
}
}
func (p *tdesktopPart) writeDomain() {
st := p.grow(len(p.domain))
if st == nil {
return
}
copy(st, p.domain)
}
func (p *tdesktopPart) writeEd25519PublicKey() {
pub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
p.err = true
return
}
st := p.grow(32)
if st == nil {
return
}
copy(st, pub)
}
func (p *tdesktopPart) writeScope(f func()) {
st := p.grow(kLengthSize)
if st == nil {
return
}
already := len(p.buf)
f()
length := len(p.buf) - already
if length > 65535 {
p.err = true
return
}
binary.BigEndian.PutUint16(p.buf[already-2:already], uint16(length))
}
func (p *tdesktopPart) newSubPart() *tdesktopPart {
return newTdesktopPart(p.domain, p.greases)
}
func (p *tdesktopPart) writePermutation(elements []func(*tdesktopPart)) {
var parts [][]byte
for _, el := range elements {
sub := p.newSubPart()
el(sub)
if sub.err {
p.err = true
return
}
parts = append(parts, sub.take())
}
shuffleByteSlices(parts)
for _, b := range parts {
if p.err {
return
}
st := p.grow(len(b))
if st == nil {
return
}
copy(st, b)
}
}
func shuffleByteSlices(s [][]byte) {
n := len(s)
for i := n - 1; i > 0; i-- {
jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
j := 0
if err != nil {
j = i
} else {
j = int(jBig.Int64())
}
s[i], s[j] = s[j], s[i]
}
}
func (p *tdesktopPart) writeM() {
kElements := kElementsM
kAdded := kAddedM
storage := p.grow(kElements*3 + kAdded)
if storage == nil {
return
}
random := make([]byte, kElements*8+kAdded)
if _, err := rand.Read(random); err != nil {
p.err = true
return
}
chars := storage
ints := make([]uint32, len(random)/4)
for i := 0; i < len(ints); i++ {
ints[i] = binary.LittleEndian.Uint32(random[i*4 : i*4+4])
}
ci := 0
for i := 0; i < kElements; i++ {
a := int(ints[i*2] % 3329)
b := int(ints[i*2+1] % 3329)
chars[ci] = byte(a & 255)
chars[ci+1] = byte((a >> 8) + ((b & 15) << 4))
chars[ci+2] = byte(b >> 4)
ci += 3
}
tail := storage[kElements*3:]
if _, err := rand.Read(tail); err != nil {
p.err = true
}
}
func (p *tdesktopPart) writeE() {
lengths := []int{144, 176, 208, 240}
var pick [1]byte
if _, err := rand.Read(pick[:]); err != nil {
p.err = true
return
}
length := lengths[int(pick[0])%len(lengths)]
p.writeRandom(length)
}
func (p *tdesktopPart) writePadding() {
if p.err {
return
}
cur := len(p.buf)
if cur >= 513 {
return
}
z := 513 - cur
p.writeString([]byte{0x00, 0x15})
p.writeScope(func() {
p.writeZeros(z)
})
}
func (p *tdesktopPart) finalize(key []byte) {
if p.err || p.digestPosition < 0 {
p.err = true
return
}
mac := hmac.New(sha256.New, key)
mac.Write(p.buf)
sum := mac.Sum(nil)
copy(p.buf[p.digestPosition:p.digestPosition+kHelloDigestLen], sum)
injectTimestamp(p.buf[p.digestPosition : p.digestPosition+kHelloDigestLen])
}
func injectTimestamp(digest32 []byte) {
if len(digest32) != 32 {
return
}
st := digest32[28:32]
u := binary.LittleEndian.Uint32(st)
ts := uint32(time.Now().Unix())
u ^= ts
binary.LittleEndian.PutUint32(st, u)
}
func (p *tdesktopPart) extractDigest() []byte {
if p.digestPosition < 0 {
return nil
}
return append([]byte(nil), p.buf[p.digestPosition:p.digestPosition+kHelloDigestLen]...)
}
func (p *tdesktopPart) take() []byte {
if p.err {
return nil
}
return append([]byte(nil), p.buf...)
}
// sessionID: первый 32-байтный Random сразу после префикса 0x20 (session id в ClientHello).
func (p *tdesktopPart) sessionID() []byte {
if p.err {
return nil
}
return firstR32Payload(p.buf)
}
func firstR32Payload(buf []byte) []byte {
pat := []byte{0x03, 0x03}
i := bytesIndex(buf, pat)
if i < 0 {
return nil
}
j := i + len(pat) + 32 // после Z(32) digest
if j+1+32 > len(buf) {
return nil
}
if buf[j] != 0x20 {
return nil
}
return append([]byte(nil), buf[j+1:j+1+32]...)
}
func bytesIndex(hay []byte, needle []byte) int {
outer:
for i := 0; i+len(needle) <= len(hay); i++ {
for k := range needle {
if hay[i+k] != needle[k] {
continue outer
}
}
return i
}
return -1
}
var cipherList32 = []byte{
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35,
0x01, 0x00,
}
func writeClientHelloRules(p *tdesktopPart) {
p.writeString([]byte{0x16, 0x03, 0x01})
p.writeScope(func() {
p.writeString([]byte{0x01, 0x00})
p.writeScope(func() {
p.writeString([]byte{0x03, 0x03})
p.writeZeros(32)
p.writeString([]byte{0x20})
p.writeRandom(32)
p.writeString([]byte{0x00, 0x20})
p.writeGrease(0)
p.writeString(cipherList32)
p.writeScope(func() {
p.writeGrease(2)
p.writeString([]byte{0x00, 0x00})
p.writePermutation(tdesktopPermutationElements())
p.writeGrease(3)
p.writeString([]byte{0x00, 0x01, 0x00})
p.writePadding()
})
})
})
}
func tdesktopPermutationElements() []func(*tdesktopPart) {
return []func(*tdesktopPart){
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x00})
p.writeScope(func() {
p.writeScope(func() {
p.writeString([]byte{0x00})
p.writeScope(func() {
p.writeDomain()
})
})
})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a})
p.writeGrease(4)
p.writeString([]byte{0x11, 0xec, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00})
},
func(p *tdesktopPart) {
p.writeString([]byte{
0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03,
0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01,
})
},
func(p *tdesktopPart) {
p.writeString([]byte{
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70,
0x2f, 0x31, 0x2e, 0x31,
})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x12, 0x00, 0x00})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x17, 0x00, 0x00})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x23, 0x00, 0x00})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x2b, 0x00, 0x07, 0x06})
p.writeGrease(6)
p.writeString([]byte{0x03, 0x04, 0x03, 0x03})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x2d, 0x00, 0x02, 0x01, 0x01})
},
func(p *tdesktopPart) {
p.writeString([]byte{0x00, 0x33, 0x04, 0xef, 0x04, 0xed})
p.writeGrease(4)
p.writeString([]byte{0x00, 0x01, 0x00, 0x11, 0xec, 0x04, 0xc0})
p.writeM()
p.writeEd25519PublicKey()
p.writeString([]byte{0x00, 0x1d, 0x00, 0x20})
p.writeEd25519PublicKey()
},
func(p *tdesktopPart) {
p.writeString([]byte{0x44, 0xcd, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32})
},
func(p *tdesktopPart) {
p.writeString([]byte{0xfe, 0x0d})
p.writeScope(func() {
p.writeString([]byte{0x00, 0x00, 0x01, 0x00, 0x01})
p.writeRandom(1)
p.writeString([]byte{0x00, 0x20})
p.writeRandom(32)
p.writeScope(func() {
p.writeE()
})
})
},
func(p *tdesktopPart) {
p.writeString([]byte{0xff, 0x01, 0x00, 0x01, 0x00})
},
}
}