diff --git a/mtglib/init.go b/mtglib/init.go index a0e5f8f..cb2893d 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -9,13 +9,14 @@ import ( ) var ( - ErrSecretEmpty = errors.New("secret is empty") - ErrSecretInvalid = errors.New("secret is invalid") - ErrNetworkIsNotDefined = errors.New("network is not defined") - ErrAntiReplayCacheIsNotDefined = errors.New("anti-replay cache is not defined") - ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") - ErrEventStreamIsNotDefined = errors.New("event stream is not defined") - ErrLoggerIsNotDefined = errors.New("logger is not defined") + ErrSecretEmpty = errors.New("secret is empty") + ErrSecretInvalid = errors.New("secret is invalid") + ErrNetworkIsNotDefined = errors.New("network is not defined") + ErrAntiReplayCacheIsNotDefined = errors.New("anti-replay cache is not defined") + ErrTimeAttackDetectorIsNotDefined = errors.New("time attack detector is not defined") + ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") + ErrEventStreamIsNotDefined = errors.New("event stream is not defined") + ErrLoggerIsNotDefined = errors.New("logger is not defined") ) const ( diff --git a/mtglib/internal/faketls/clienthello/clienthello.go b/mtglib/internal/faketls/clienthello/clienthello.go new file mode 100644 index 0000000..4789029 --- /dev/null +++ b/mtglib/internal/faketls/clienthello/clienthello.go @@ -0,0 +1,67 @@ +package clienthello + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "fmt" + "time" + + "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" +) + +type ClientHello struct { + Time time.Time + Digest [RandomLen]byte + SessionID []byte +} + +func ParseHandshake(secret, handshake []byte) (ClientHello, error) { + hello := ClientHello{} + + if len(handshake) < MinLen { + return hello, fmt.Errorf("lengh of handshake is too small: %d", len(handshake)) + } + + if handshake[0] != HandshakeTypeClient { + return hello, fmt.Errorf("unknown handshake type %#x", handshake[0]) + } + + copy(hello.Digest[:], handshake[RandomOffset:]) + + for i := RandomOffset; i < RandomOffset+RandomLen; i++ { + handshake[i] = 0 + } + + rec := record.AcquireRecord() + defer record.ReleaseRecord(rec) + + rec.Type = record.TypeHandshake + rec.Version = record.Version10 + rec.Payload.Write(handshake) + + // mac is calculated for the whole record, not only + // for the payload part + mac := hmac.New(sha256.New, secret) + rec.Dump(mac) + + computedDigest := mac.Sum(nil) + + for i := 0; i < RandomLen; i++ { + computedDigest[i] ^= hello.Digest[i] + } + + for i := 0; i < RandomLen-4; i++ { + if computedDigest[i] != 0 { + return hello, ErrBadDigest + } + } + + timestamp := int64(binary.LittleEndian.Uint32(computedDigest[RandomLen-4:])) + hello.Time = time.Unix(timestamp, 0) + + hello.SessionID = make([]byte, handshake[SessionIDOffset]) + copy(hello.SessionID, handshake[SessionIDOffset+1:]) + + return hello, nil +} diff --git a/mtglib/internal/faketls/clienthello/init.go b/mtglib/internal/faketls/clienthello/init.go new file mode 100644 index 0000000..c597b84 --- /dev/null +++ b/mtglib/internal/faketls/clienthello/init.go @@ -0,0 +1,17 @@ +package clienthello + +import "errors" + +const ( + RandomLen = 32 + RandomOffset = 6 + SessionIDOffset = RandomOffset + RandomLen + MinLen = SessionIDOffset + 1 + + HandshakeTypeClient = 0x01 +) + +var ( + ErrBadDigest = errors.New("bad digest") + ErrAntiReplayAttack = errors.New("antireplay attack was detected") +) diff --git a/mtglib/internal/faketls/record/init.go b/mtglib/internal/faketls/record/init.go new file mode 100644 index 0000000..1e543aa --- /dev/null +++ b/mtglib/internal/faketls/record/init.go @@ -0,0 +1,66 @@ +package record + +import "fmt" + +type Type uint8 + +const ( + TypeChangeCipherSpec Type = 0x14 + TypeHandshake Type = 0x16 + TypeApplicationData Type = 0x17 +) + +func (t Type) String() string { + switch t { + case TypeChangeCipherSpec: + return "changeCipher(0x14)" + case TypeHandshake: + return "handshake(0x16)" + case TypeApplicationData: + return "applicationData(0x17)" + } + + return fmt.Sprintf("unknown(%#x)", byte(t)) +} + +func (t Type) Valid() error { + switch t { + case TypeChangeCipherSpec, TypeHandshake, TypeApplicationData: + return nil + } + + return fmt.Errorf("unknown type %#x", byte(t)) +} + +type Version uint16 + +const ( + Version10 Version = 769 // 0x03 0x01 + Version11 Version = 770 // 0x03 0x02 + Version12 Version = 771 // 0x03 0x03 + Version13 Version = 772 // 0x03 0x04 +) + +func (v Version) String() string { + switch v { + case Version10: + return "tls1.0" + case Version11: + return "tls1.1" + case Version12: + return "tls1.2" + case Version13: + return "tls1.3" + } + + return fmt.Sprintf("tls(%d)", uint16(v)) +} + +func (v Version) Valid() error { + switch v { + case Version10, Version11, Version12, Version13: + return nil + } + + return fmt.Errorf("unknown version %d", uint16(v)) +} diff --git a/mtglib/internal/faketls/record/pools.go b/mtglib/internal/faketls/record/pools.go new file mode 100644 index 0000000..62f03e9 --- /dev/null +++ b/mtglib/internal/faketls/record/pools.go @@ -0,0 +1,18 @@ +package record + +import "sync" + +var recordPool = sync.Pool{ + New: func() interface{} { + return &Record{} + }, +} + +func AcquireRecord() *Record { + return recordPool.Get().(*Record) +} + +func ReleaseRecord(r *Record) { + r.Reset() + recordPool.Put(r) +} diff --git a/mtglib/internal/faketls/record/record.go b/mtglib/internal/faketls/record/record.go new file mode 100644 index 0000000..31a1d9e --- /dev/null +++ b/mtglib/internal/faketls/record/record.go @@ -0,0 +1,87 @@ +package record + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "fmt" + "io" +) + +type Record struct { + Type Type + Version Version + Payload bytes.Buffer +} + +func (r *Record) String() string { + return fmt.Sprintf("", + r.Type, + r.Version, + base64.StdEncoding.EncodeToString(r.Payload.Bytes())) +} + +func (r *Record) Reset() { + r.Payload.Reset() +} + +func (r *Record) Read(reader io.Reader) error { + r.Reset() + + buf := [2]byte{} + + if _, err := io.ReadFull(reader, buf[:1]); err != nil { + return fmt.Errorf("cannot read type: %w", err) + } + + r.Type = Type(buf[0]) + if err := r.Type.Valid(); err != nil { + return fmt.Errorf("invalid type: %w", err) + } + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return fmt.Errorf("cannot read version: %w", err) + } + + r.Version = Version(binary.BigEndian.Uint16(buf[:])) + if err := r.Version.Valid(); err != nil { + return fmt.Errorf("invalid version: %w", err) + } + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return fmt.Errorf("cannot read payload length: %w", err) + } + + length := int64(binary.BigEndian.Uint16(buf[:])) + if _, err := io.CopyN(&r.Payload, reader, length); err != nil { + return fmt.Errorf("cannot read payload: %w", err) + } + + return nil +} + +func (r *Record) Dump(writer io.Writer) error { + buf := [2]byte{byte(r.Type), 0} + + if _, err := writer.Write(buf[:1]); err != nil { + return fmt.Errorf("cannot dump type: %w", err) + } + + binary.BigEndian.PutUint16(buf[:], uint16(r.Version)) + + if _, err := writer.Write(buf[:]); err != nil { + return fmt.Errorf("cannot dump version: %w", err) + } + + binary.BigEndian.PutUint16(buf[:], uint16(r.Payload.Len())) + + if _, err := writer.Write(buf[:]); err != nil { + return fmt.Errorf("cannot dump payload length: %w", err) + } + + if _, err := writer.Write(r.Payload.Bytes()); err != nil { + return fmt.Errorf("cannot dump payload: %w", err) + } + + return nil +} diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 43b1bbe..7273375 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -8,6 +8,8 @@ import ( "sync" "time" + "github.com/9seconds/mtg/v2/mtglib/internal/faketls/clienthello" + "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" "github.com/9seconds/mtg/v2/mtglib/internal/relay" "github.com/9seconds/mtg/v2/mtglib/internal/telegram" @@ -24,11 +26,12 @@ type Proxy struct { workerPool *ants.PoolWithFunc telegram *telegram.Telegram - secret Secret - antiReplayCache AntiReplayCache - ipBlocklist IPBlocklist - eventStream EventStream - logger Logger + secret Secret + antiReplayCache AntiReplayCache + timeAttackDetector TimeAttackDetector + ipBlocklist IPBlocklist + eventStream EventStream + logger Logger } func (p *Proxy) ServeConn(conn net.Conn) { @@ -55,6 +58,12 @@ func (p *Proxy) ServeConn(conn net.Conn) { ctx.logger.Info("Stream has been finished") }() + if err := p.doFakeTLSHandshake(ctx); err != nil { + p.logger.InfoError("faketls handshake is failed", err) + + return + } + if err := p.doObfuscated2Handshake(ctx); err != nil { p.logger.InfoError("obfuscated2 handshake is failed", err) @@ -112,6 +121,21 @@ func (p *Proxy) Shutdown() { p.workerPool.Release() } +func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) error { + clientHelloRecord := record.AcquireRecord() + defer record.ReleaseRecord(clientHelloRecord) + + if err := clientHelloRecord.Read(ctx.clientConn); err != nil { + return fmt.Errorf("cannot read client hello: %w", err) + } + + hello, _ := clienthello.ParseHandshake(p.secret.Key[:], + clientHelloRecord.Payload.Bytes()) + fmt.Println(hello) + + return fmt.Errorf("SUCCESS") +} + func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error { dc, encryptor, decryptor, err := obfuscated2.ClientHandshake(p.secret.Key[:], ctx.clientConn) if err != nil { @@ -173,6 +197,8 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop return nil, ErrIPBlocklistIsNotDefined case opts.EventStream == nil: return nil, ErrEventStreamIsNotDefined + case opts.TimeAttackDetector == nil: + return nil, ErrTimeAttackDetectorIsNotDefined case opts.Logger == nil: return nil, ErrLoggerIsNotDefined case !opts.Secret.Valid(): @@ -201,16 +227,17 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop ctx, cancel := context.WithCancel(context.Background()) proxy := &Proxy{ - ctx: ctx, - ctxCancel: cancel, - secret: opts.Secret, - antiReplayCache: opts.AntiReplayCache, - ipBlocklist: opts.IPBlocklist, - eventStream: opts.EventStream, - logger: opts.Logger.Named("proxy"), - idleTimeout: idleTimeout, - bufferSize: int(bufferSize), - telegram: tg, + ctx: ctx, + ctxCancel: cancel, + secret: opts.Secret, + antiReplayCache: opts.AntiReplayCache, + timeAttackDetector: opts.TimeAttackDetector, + ipBlocklist: opts.IPBlocklist, + eventStream: opts.EventStream, + logger: opts.Logger.Named("proxy"), + idleTimeout: idleTimeout, + bufferSize: int(bufferSize), + telegram: tg, } pool, err := ants.NewPoolWithFunc(int(concurrency), func(arg interface{}) {