Files
Denozordec 4905c06b9b
Publish mtproxy_checker Docker image / test (push) Successful in 19s
Publish mtproxy_checker Docker image / build-and-push (push) Successful in 48s
init
2026-04-11 00:38:22 +07:00

472 lines
10 KiB
Go
Raw Permalink 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.
// Генерация 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})
},
}
}