4768065e72
The Read path treated every TLS record's payload as application data and wrote it into readBuf — including the 1-byte payload (0x01) of ChangeCipherSpec records. mtg sends those records intermittently as a TLS-compat keep-alive; once one arrived inside the data stream it desynced the obfuscated2 CTR keystream by one byte. From that point on every MTProto message decrypted to garbage and the engine failed with "decrypt: msg_key is invalid", forcibly closed the connection, and looped. The Go switch cases for ChangeCipherSpec and Application were both empty (no fallthrough, no continue), so control reached the o.readBuf.Write(rec.Data) call below the switch for both — exactly the wrong behaviour for CCS. Reshape the loop so that: - ChangeCipherSpec records are silently dropped - Application records are written to readBuf and returned - Handshake / unsupported types still error out This matches tdlib's TlsTransport (CCS is skipped at the TLS framing layer and never reaches the MTProto decoder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
2.8 KiB
Go
120 lines
2.8 KiB
Go
package faketls
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/go-faster/errors"
|
|
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy"
|
|
)
|
|
|
|
// FakeTLS implements FakeTLS obfuscation protocol.
|
|
type FakeTLS struct {
|
|
rand io.Reader
|
|
clock clock.Clock
|
|
conn io.ReadWriter
|
|
|
|
version [2]byte
|
|
firstPacket bool
|
|
|
|
readBuf bytes.Buffer
|
|
readBufMux sync.Mutex
|
|
}
|
|
|
|
// NewFakeTLS creates new FakeTLS.
|
|
func NewFakeTLS(r io.Reader, conn io.ReadWriter) *FakeTLS {
|
|
return &FakeTLS{
|
|
rand: r,
|
|
clock: clock.System,
|
|
conn: conn,
|
|
version: Version12Bytes,
|
|
readBuf: bytes.Buffer{},
|
|
}
|
|
}
|
|
|
|
// Handshake performs FakeTLS handshake.
|
|
func (o *FakeTLS) Handshake(protocol [4]byte, dc int, s mtproxy.Secret) error {
|
|
o.readBufMux.Lock()
|
|
o.readBuf.Reset()
|
|
o.readBufMux.Unlock()
|
|
|
|
var sessionID [32]byte
|
|
if _, err := o.rand.Read(sessionID[:]); err != nil {
|
|
return errors.Wrap(err, "generate sessionID")
|
|
}
|
|
|
|
clientDigest, err := writeClientHello(o.conn, o.rand, o.clock, sessionID, s.CloakHost, s.Secret)
|
|
if err != nil {
|
|
return errors.Wrap(err, "send ClientHello")
|
|
}
|
|
|
|
if err := readServerHello(o.conn, clientDigest, s.Secret); err != nil {
|
|
return errors.Wrap(err, "receive ServerHello")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Write implements io.Writer.
|
|
func (o *FakeTLS) Write(b []byte) (n int, err error) {
|
|
// Some proxies require sending first packet with RecordTypeChangeCipherSpec.
|
|
//
|
|
// See https://github.com/tdlib/td/blob/master/td/mtproto/TcpTransport.cpp#L266.
|
|
if !o.firstPacket {
|
|
_, err = writeRecord(o.conn, record{
|
|
Type: RecordTypeChangeCipherSpec,
|
|
Version: o.version,
|
|
Data: []byte("\x01"),
|
|
})
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "write first TLS packet")
|
|
}
|
|
o.firstPacket = true
|
|
}
|
|
n, err = writeRecord(o.conn, record{
|
|
Type: RecordTypeApplication,
|
|
Version: o.version,
|
|
Data: b,
|
|
})
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "write TLS record")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Read implements io.Reader.
|
|
func (o *FakeTLS) Read(b []byte) (n int, err error) {
|
|
o.readBufMux.Lock()
|
|
defer o.readBufMux.Unlock()
|
|
|
|
if o.readBuf.Len() > 0 {
|
|
return o.readBuf.Read(b)
|
|
}
|
|
|
|
// Skip ChangeCipherSpec records — they are TLS-level keep-alive /
|
|
// compatibility markers (one-byte payload 0x01) and must NOT be
|
|
// passed up to the obfuscated2 layer, otherwise the CTR keystream
|
|
// position desyncs and subsequent MTProto messages decrypt to
|
|
// garbage (`msg_key is invalid`).
|
|
for {
|
|
rec, err := readRecord(o.conn)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "read TLS record")
|
|
}
|
|
|
|
switch rec.Type {
|
|
case RecordTypeChangeCipherSpec:
|
|
continue
|
|
case RecordTypeApplication:
|
|
o.readBuf.Write(rec.Data)
|
|
return o.readBuf.Read(b)
|
|
case RecordTypeHandshake:
|
|
return 0, errors.New("unexpected record type handshake")
|
|
default:
|
|
return 0, errors.Errorf("unsupported record type %v", rec.Type)
|
|
}
|
|
}
|
|
}
|