diff --git a/antireplay/cache.go b/antireplay/cache.go index 0e634f2..a142bd9 100644 --- a/antireplay/cache.go +++ b/antireplay/cache.go @@ -3,14 +3,24 @@ package antireplay import "github.com/allegro/bigcache" type cache struct { - cache *bigcache.BigCache + obfuscated2 *bigcache.BigCache + tls *bigcache.BigCache } -func (c *cache) Add(data []byte) { - c.cache.Set(string(data), nil) // nolint: errcheck +func (c *cache) AddObfuscated2(data []byte) { + c.obfuscated2.Set(string(data), nil) // nolint: errcheck } -func (c *cache) Has(data []byte) bool { - _, err := c.cache.Get(string(data)) +func (c *cache) AddTLS(data []byte) { + c.tls.Set(string(data), nil) // nolint: errcheck +} + +func (c *cache) HasObfuscated2(data []byte) bool { + _, err := c.obfuscated2.Get(string(data)) + return err == nil +} + +func (c *cache) HasTLS(data []byte) bool { + _, err := c.tls.Get(string(data)) return err == nil } diff --git a/antireplay/init.go b/antireplay/init.go index 745e702..740fa6f 100644 --- a/antireplay/init.go +++ b/antireplay/init.go @@ -14,7 +14,17 @@ var ( func Init() { initOnce.Do(func() { - c, err := bigcache.NewBigCache(bigcache.Config{ + c1, err := bigcache.NewBigCache(bigcache.Config{ + Shards: 1024, + LifeWindow: config.C.AntiReplayEvictionTime, + Hasher: hasher{}, + HardMaxCacheSize: config.C.AntiReplayMaxSize, + }) + if err != nil { + panic(err) + } + + c2, err := bigcache.NewBigCache(bigcache.Config{ Shards: 1024, LifeWindow: config.C.AntiReplayEvictionTime, Hasher: hasher{}, @@ -25,7 +35,8 @@ func Init() { } Cache = &cache{ - cache: c, + obfuscated2: c1, + tls: c2, } }) } diff --git a/cli/proxy.go b/cli/proxy.go index ef94430..188dc68 100644 --- a/cli/proxy.go +++ b/cli/proxy.go @@ -10,6 +10,7 @@ import ( "github.com/9seconds/mtg/antireplay" "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/faketls" "github.com/9seconds/mtg/hub" "github.com/9seconds/mtg/ntp" "github.com/9seconds/mtg/obfuscated2" @@ -75,6 +76,7 @@ func Proxy() error { // nolint: funlen antireplay.Init() telegram.Init() hub.Init(ctx) + faketls.Init(ctx) proxyListener, err := net.Listen("tcp", config.C.Bind.String()) if err != nil { @@ -91,12 +93,9 @@ func Proxy() error { // nolint: funlen Context: ctx, ClientProtocolMaker: obfuscated2.MakeClientProtocol, } - // if len(config.C.AdTag) == 0 { - // app.TelegramProtocolMaker = obfuscated2.MakeTelegramProtocol - // } - // if config.C.SecretMode != config.SecretModeTLS { - // app.ClientProtocolMaker = obfuscated2.MakeClientProtocol - // } + if config.C.SecretMode == config.SecretModeTLS { + app.ClientProtocolMaker = faketls.MakeClientProtocol + } app.Serve(proxyListener) diff --git a/config/urls.go b/config/urls.go index 28a6b7b..1e90b1d 100644 --- a/config/urls.go +++ b/config/urls.go @@ -28,11 +28,13 @@ func GetURLs() (urls IPURLs) { secret = hex.EncodeToString(C.Secret) case SecretModeSecured: secret = "dd" + hex.EncodeToString(C.Secret) + case SecretModeTLS: + secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost)) } urls.IPv4 = makeURLs(C.PublicIPv4, secret) urls.IPv6 = makeURLs(C.PublicIPv6, secret) - urls.BotSecret = secret + urls.BotSecret = hex.EncodeToString(C.Secret) return urls } diff --git a/faketls/certificate_server.go b/faketls/certificate_server.go new file mode 100644 index 0000000..e11e105 --- /dev/null +++ b/faketls/certificate_server.go @@ -0,0 +1,91 @@ +package faketls + +import ( + "bytes" + "container/ring" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "strconv" + "time" + + "go.uber.org/zap" + + "github.com/9seconds/mtg/config" +) + +type connectionServer struct { + nextWriteItem *ring.Ring + nextReadItem *ring.Ring + + ctx context.Context + channelGet chan chan<- []byte +} + +func (c *connectionServer) get() ([]byte, error) { + resp := make(chan []byte) + select { + case <-c.ctx.Done(): + return nil, errors.New("context closed") + case c.channelGet <- resp: + return <-resp, nil + } +} + +func (c *connectionServer) fetch() ([]byte, error) { + addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort)) + conn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true}) // nolint: gosec + + if err != nil { + return nil, fmt.Errorf("cannot connect to the masked host: %w", err) + } + + defer conn.Close() + + if err = conn.Handshake(); err != nil { + return nil, fmt.Errorf("cannot perform tls handshake: %w", err) + } + + certificates := conn.ConnectionState().PeerCertificates + if len(certificates) == 0 { + return nil, errors.New("no certificates is found") + } + + var buf bytes.Buffer + + for _, v := range certificates { + buf.Write(v.Raw) + } + + return buf.Bytes(), nil +} + +func (c *connectionServer) run(tickEvery time.Duration) { + logger := zap.S().Named("tls-connection-server") + + ticker := time.NewTicker(tickEvery) + defer ticker.Stop() + + for { + select { + case <-c.ctx.Done(): + return + case resp := <-c.channelGet: + resp <- c.nextReadItem.Value.([]byte) + close(resp) + + c.nextReadItem = c.nextReadItem.Next() + case <-ticker.C: + cert, err := c.fetch() + switch err { + case nil: + c.nextWriteItem.Value = cert + c.nextWriteItem = c.nextWriteItem.Next() + default: + logger.Warnw("cannot fetch certificates", "error", err) + } + } + } +} diff --git a/faketls/client_protocol.go b/faketls/client_protocol.go index 620a89d..6102b88 100644 --- a/faketls/client_protocol.go +++ b/faketls/client_protocol.go @@ -2,9 +2,18 @@ package faketls import ( "bufio" + "encoding/binary" + "errors" + "fmt" + "io" + "time" + "github.com/9seconds/mtg/antireplay" "github.com/9seconds/mtg/conntypes" "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/protocol" + "github.com/9seconds/mtg/stats" + "github.com/9seconds/mtg/tlstypes" "github.com/9seconds/mtg/wrappers/stream" ) @@ -18,18 +27,77 @@ func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conn for _, expected := range faketlsStartBytes { if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected { - return nil, c.simulateWebsite(rewinded) + fmt.Println("!!!!!!!!!!!! ERROR !!!!!!!!!!!!", err) + return nil, errors.New("qqq") } } + rewinded.Rewind() + rewinded = stream.NewRewind(rewinded) + if err := c.tlsHandshake(rewinded); err != nil { - return nil, c.simulateWebsite(rewinded) + fmt.Println("!!!!!!!!!!!! ERROR !!!!!!!!!!!!", err) + return nil, errors.New("qqq") } - conn, err := c.ClientProtocol.Handshake(socket) + conn := stream.NewFakeTLS(socket) + conn, err := c.ClientProtocol.Handshake(conn) + if err != nil { return nil, err } return conn, err } + +func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error { + helloRecord, err := tlstypes.ReadRecord(conn) + if err != nil { + return fmt.Errorf("cannot read initial record: %w", err) + } + + clientHello, err := tlstypes.ParseClientHello(helloRecord.Data.Bytes()) + if err != nil { + return fmt.Errorf("cannot parse client hello: %w", err) + } + + digest := clientHello.Digest() + for i := 0; i < len(digest)-4; i++ { + if digest[i] != 0 { + return errBadDigest + } + } + + timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:])) + createdAt := time.Unix(timestamp, 0) + timeDiff := time.Since(createdAt) + + if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot { + return errBadTime + } + + if antireplay.Cache.HasTLS(clientHello.Random[:]) { + stats.Stats.AntiReplayDetected() + return errors.New("antireplay detected") + } + + antireplay.Cache.AddTLS(clientHello.Random[:]) + + hostCert, err := connectionServerInstance.get() + if err != nil { + return fmt.Errorf("cannot get host certificate: %w", err) + } + + serverHello := tlstypes.NewServerHello(clientHello) + serverHelloPacket := serverHello.WelcomePacket(hostCert) + + if _, err := conn.Write(serverHelloPacket); err != nil { + return fmt.Errorf("cannot send welcome packet: %w", err) + } + + return nil +} + +func MakeClientProtocol() protocol.ClientProtocol { + return &ClientProtocol{} +} diff --git a/faketls/consts.go b/faketls/consts.go index 66061d0..9bd2353 100644 --- a/faketls/consts.go +++ b/faketls/consts.go @@ -1,21 +1,30 @@ package faketls +import ( + "errors" + "time" +) + const ( - TLSHandshakeLength = 1 + 2 + 2 + 512 + TimeSkew = 5 * time.Second + TimeFromBoot = 24 * 60 * 60 ) var ( -faketlsStartBytes = [...]byte{ - 0x16, - 0x03, - 0x01, - 0x02, - 0x00, - 0x01, - 0x00, - 0x01, - 0xfc, - 0x03, - 0x03, -} + errBadDigest = errors.New("bad digest") + errBadTime = errors.New("bad time") + + faketlsStartBytes = [...]byte{ + 0x16, + 0x03, + 0x01, + 0x02, + 0x00, + 0x01, + 0x00, + 0x01, + 0xfc, + 0x03, + 0x03, + } ) diff --git a/faketls/init.go b/faketls/init.go new file mode 100644 index 0000000..b3b6500 --- /dev/null +++ b/faketls/init.go @@ -0,0 +1,50 @@ +package faketls + +import ( + "container/ring" + "context" + "sync" + "time" + + "github.com/9seconds/mtg/config" +) + +var ( + connectionServerInstance connectionServer + connectionServerInitOnce sync.Once +) + +const ( + connectionServerKeepCertificates = 5 + connectionServerUpdateEvery = 10 * time.Minute +) + +func Init(ctx context.Context) { + connectionServerInitOnce.Do(func() { + if config.C.CloakHost == "" { + return + } + + connectionServerInstance = connectionServer{ + channelGet: make(chan chan<- []byte), + ctx: ctx, + } + + cert, err := connectionServerInstance.fetch() + if err != nil { + panic(err) + } + + r := ring.New(connectionServerKeepCertificates) + + for i := 0; i < connectionServerKeepCertificates; i++ { + r.Value = cert + r = r.Next() + } + + connectionServerInstance.nextWriteItem = r + connectionServerInstance.nextReadItem = r + + go connectionServerInstance.run(connectionServerUpdateEvery) + }) +} diff --git a/faketls/telegram_protocol.go b/faketls/telegram_protocol.go deleted file mode 100644 index 9c1cd85..0000000 --- a/faketls/telegram_protocol.go +++ /dev/null @@ -1,10 +0,0 @@ -package faketls - -import ( - "github.com/9seconds/mtg/conntypes" - "github.com/9seconds/mtg/protocol" -) - -func TelegramProtocol(req *protocol.TelegramRequest) (conntypes.StreamReadWriteCloser, error) { - return nil, nil -} diff --git a/go.mod b/go.mod index f206fda..68e37aa 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.2.0 // indirect go.uber.org/zap v1.10.0 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20191009170851-d66e71096ffb // indirect golang.org/x/sys v0.0.0-20191010194322-b09406accb47 gopkg.in/alecthomas/kingpin.v2 v2.2.6 diff --git a/go.sum b/go.sum index f071844..5ac33a5 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,7 @@ go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/obfuscated2/client_protocol.go b/obfuscated2/client_protocol.go index 3ccc7c4..9f940af 100644 --- a/obfuscated2/client_protocol.go +++ b/obfuscated2/client_protocol.go @@ -13,6 +13,7 @@ import ( "github.com/9seconds/mtg/config" "github.com/9seconds/mtg/conntypes" "github.com/9seconds/mtg/protocol" + "github.com/9seconds/mtg/stats" "github.com/9seconds/mtg/utils" "github.com/9seconds/mtg/wrappers/stream" ) @@ -81,11 +82,12 @@ func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conn } antiReplayKey := decryptedFrame.Unique() - if antireplay.Cache.Has(antiReplayKey) { + if antireplay.Cache.HasObfuscated2(antiReplayKey) { + stats.Stats.AntiReplayDetected() return nil, errors.New("replay attack is detected") } - antireplay.Cache.Add(antiReplayKey) + antireplay.Cache.AddObfuscated2(antiReplayKey) return stream.NewObfuscated2(socket, encryptor, decryptor), nil } diff --git a/tlstypes/certificate_server.go b/tlstypes/certificate_server.go new file mode 100644 index 0000000..341d51d --- /dev/null +++ b/tlstypes/certificate_server.go @@ -0,0 +1,80 @@ +package tlstypes + +import ( + "container/ring" + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "strconv" + "time" + + "go.uber.org/zap" + + "github.com/9seconds/mtg/config" +) + +const ( + connectionServerKeepCertificates = 5 + connectionServerUpdateEvery = 10 * time.Minute +) + +type connectionServer struct { + nextWriteItem *ring.Ring + nextReadItem *ring.Ring + + ctx context.Context + channelGet chan chan<- *x509.Certificate +} + +func (c *connectionServer) fetch() (*x509.Certificate, error) { + addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort)) + conn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true}) // nolint: gosec + + if err != nil { + return nil, fmt.Errorf("cannot connect to the masked host: %w", err) + } + + defer conn.Close() + + if err = conn.Handshake(); err != nil { + return nil, fmt.Errorf("cannot perform tls handshake: %w", err) + } + + certificates := conn.ConnectionState().PeerCertificates + if len(certificates) == 0 { + return nil, errors.New("no certificates is found") + } + + return certificates[0], nil +} + +func (c *connectionServer) run() { + logger := zap.S().Named("tls-connection-server") + + ticker := time.NewTicker(connectionServerUpdateEvery) + defer ticker.Stop() + + for { + select { + case <-c.ctx.Done(): + return + case resp := <-c.channelGet: + resp <- c.nextReadItem.Value.(*x509.Certificate) + close(resp) + + c.nextReadItem = c.nextReadItem.Next() + case <-ticker.C: + cert, err := c.fetch() + switch err { + case nil: + c.nextWriteItem.Value = cert + c.nextWriteItem = c.nextWriteItem.Next() + default: + logger.Warnw("cannot fetch certificates", "error", err) + } + } + } +} diff --git a/tlstypes/client_hello.go b/tlstypes/client_hello.go new file mode 100644 index 0000000..dbdcde4 --- /dev/null +++ b/tlstypes/client_hello.go @@ -0,0 +1,86 @@ +package tlstypes + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "fmt" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/utils" +) + +type ClientHello struct { + Handshake +} + +func (c ClientHello) Digest() []byte { + dirtyDigest := c.Random + c.Random = [32]byte{} + + rec := Record{ + Type: RecordTypeHandshake, + Version: Version10, + Data: &c, + } + + mac := hmac.New(sha256.New, config.C.Secret) + mac.Write(rec.Bytes()) // nolint: errcheck + computedDigest := mac.Sum(nil) + + for i := range computedDigest { + computedDigest[i] ^= dirtyDigest[i] + } + + return computedDigest +} + +func ParseClientHello(raw []byte) (*ClientHello, error) { + rv := &ClientHello{} + + rv.Type = HandshakeType(raw[0]) + if rv.Type != HandshakeTypeClient { + return nil, fmt.Errorf("incorrect handshake type %v", rv.Type) + } + + raw = raw[1:] + sizeUint24 := utils.Uint24{} + copy(sizeUint24[:], utils.ReverseBytes(raw[:3])) + size := int(utils.FromUint24(sizeUint24)) + + raw = raw[3:] + if len(raw) != size { + return nil, fmt.Errorf("payload size mismatch (%d != %d)", len(raw), size) + } + + versionRaw := raw[:2] + + switch { + case bytes.Equal(versionRaw, Version13Bytes): + rv.Version = Version13 + case bytes.Equal(versionRaw, Version12Bytes): + rv.Version = Version12 + case bytes.Equal(versionRaw, Version11Bytes): + rv.Version = Version11 + case bytes.Equal(versionRaw, Version10Bytes): + rv.Version = Version10 + default: + return nil, fmt.Errorf("unknown protocol version %v", versionRaw) + } + + raw = raw[2:] + copy(rv.Random[:], raw[:32]) + raw = raw[32:] + + sessionIDLength := int(raw[0]) + raw = raw[1:] + rv.SessionID = make([]byte, sessionIDLength) + copy(rv.SessionID, raw) + raw = raw[sessionIDLength:] + + tail := make([]byte, len(raw)) + copy(tail, raw) + rv.Tail = RawBytes(tail) + + return rv, nil +} diff --git a/tlstypes/consts.go b/tlstypes/consts.go new file mode 100644 index 0000000..72e6935 --- /dev/null +++ b/tlstypes/consts.go @@ -0,0 +1,79 @@ +package tlstypes + +type RecordType uint8 + +const ( + RecordTypeHandshake RecordType = 0x16 + RecordTypeApplicationData RecordType = 0x17 + RecordTypeChangeCipherSpec RecordType = 0x14 +) + +type HandshakeType uint8 + +const ( + HandshakeTypeClient HandshakeType = 0x01 + HandshakeTypeServer HandshakeType = 0x02 +) + +type CipherSuiteType uint8 + +const ( + CipherSuiteType_TLS_AES_128_GCM_SHA256 CipherSuiteType = iota // nolint: stylecheck, golint + CipherSuiteType_TLS_AES_256_GCM_SHA384 // nolint: stylecheck, golint + CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256 // nolint: stylecheck, golint +) + +func (c CipherSuiteType) Bytes() []byte { + switch c { + case CipherSuiteType_TLS_AES_128_GCM_SHA256: + return CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes + case CipherSuiteType_TLS_AES_256_GCM_SHA384: + return CipherSuiteType_TLS_AES_256_GCM_SHA384_Bytes + } + + return CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256_Bytes +} + +type Version uint8 + +func (v Version) Bytes() []byte { + switch v { + case Version13: + return Version13Bytes + case Version12: + return Version12Bytes + case Version11: + return Version11Bytes + } + + return Version10Bytes +} + +const ( + VersionUnknown Version = iota + Version10 + Version11 + Version12 + Version13 +) + +var ( + Version10Bytes = []byte{0x03, 0x01} + Version11Bytes = []byte{0x03, 0x02} + Version12Bytes = []byte{0x03, 0x03} + Version13Bytes = []byte{0x03, 0x04} + + CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes = []byte{0x13, 0x01} // nolint: stylecheck, golint + CipherSuiteType_TLS_AES_256_GCM_SHA384_Bytes = []byte{0x13, 0x02} // nolint: stylecheck, golint + CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256_Bytes = []byte{0x13, 0x03} // nolint; stylecheck, golint +) + +type Byter interface { + Bytes() []byte +} + +type RawBytes []byte + +func (r RawBytes) Bytes() []byte { + return []byte(r) +} diff --git a/tlstypes/handshake.go b/tlstypes/handshake.go new file mode 100644 index 0000000..ec0accf --- /dev/null +++ b/tlstypes/handshake.go @@ -0,0 +1,37 @@ +package tlstypes + +import ( + "bytes" + + "github.com/9seconds/mtg/utils" +) + +type Handshake struct { + Type HandshakeType + Version Version + Random [32]byte + SessionID []byte + Tail Byter +} + +func (h *Handshake) Bytes() []byte { + buf := bytes.Buffer{} + packetBuf := bytes.Buffer{} + + buf.WriteByte(byte(h.Type)) + + packetBuf.Write(h.Version.Bytes()) + packetBuf.Write(h.Random[:]) + packetBuf.WriteByte(byte(len(h.SessionID))) + packetBuf.Write(h.SessionID) + packetBuf.Write(h.Tail.Bytes()) + + sizeUint24 := utils.ToUint24(uint32(packetBuf.Len())) + sizeUint24Bytes := sizeUint24[:] + sizeUint24Bytes[0], sizeUint24Bytes[2] = sizeUint24Bytes[2], sizeUint24Bytes[0] + + buf.Write(sizeUint24Bytes) + packetBuf.WriteTo(&buf) // nolint: errcheck + + return buf.Bytes() +} diff --git a/tlstypes/record.go b/tlstypes/record.go new file mode 100644 index 0000000..d6a71dd --- /dev/null +++ b/tlstypes/record.go @@ -0,0 +1,85 @@ +package tlstypes + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +const recordMaxChunkSize = 16384 + 24 + +type Record struct { + Type RecordType + Version Version + Data Byter +} + +func (r Record) Bytes() []byte { + buf := bytes.Buffer{} + data := r.Data.Bytes() + + buf.WriteByte(byte(r.Type)) + buf.Write(r.Version.Bytes()) + binary.Write(&buf, binary.BigEndian, uint16(len(data))) // nolint: errcheck + buf.Write(data) + + return buf.Bytes() +} + +func ReadRecord(reader io.Reader) (Record, error) { + buf := [2]byte{} + rec := Record{} + + if _, err := io.ReadFull(reader, buf[:1]); err != nil { + return rec, fmt.Errorf("cannot read record type: %w", err) + } + + rec.Type = RecordType(buf[0]) + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return rec, fmt.Errorf("cannot read version: %w", err) + } + + switch { + case bytes.Equal(buf[:], Version13Bytes): + rec.Version = Version13 + case bytes.Equal(buf[:], Version12Bytes): + rec.Version = Version12 + case bytes.Equal(buf[:], Version11Bytes): + rec.Version = Version11 + case bytes.Equal(buf[:], Version10Bytes): + rec.Version = Version10 + } + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return rec, fmt.Errorf("cannot read data length: %w", err) + } + + data := make([]byte, binary.BigEndian.Uint16(buf[:])) + if _, err := io.ReadFull(reader, data); err != nil { + return rec, fmt.Errorf("cannot read data: %w", err) + } + + rec.Data = RawBytes(data) + + return rec, nil +} + +func MakeRecords(raw []byte) (arr []Record) { + for len(raw) > 0 { + chunkSize := recordMaxChunkSize + if chunkSize > len(raw) { + chunkSize = len(raw) + } + + arr = append(arr, Record{ + Type: RecordTypeApplicationData, + Version: Version12, + Data: RawBytes(raw[:chunkSize]), + }) + raw = raw[chunkSize:] + } + + return +} diff --git a/tlstypes/server_hello.go b/tlstypes/server_hello.go new file mode 100644 index 0000000..b233d29 --- /dev/null +++ b/tlstypes/server_hello.go @@ -0,0 +1,92 @@ +package tlstypes + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "io" + + "golang.org/x/crypto/curve25519" + + "github.com/9seconds/mtg/config" +) + +type ServerHello struct { + Handshake + + clientHello *ClientHello +} + +func (s ServerHello) WelcomePacket(hostCert []byte) []byte { + s.Random = [32]byte{} + rec := Record{ + Type: RecordTypeHandshake, + Version: Version12, + Data: &s, + } + buf := bytes.NewBuffer(rec.Bytes()) + + recChangeCipher := Record{ + Type: RecordTypeChangeCipherSpec, + Version: Version12, + Data: RawBytes([]byte{0x01}), + } + buf.Write(recChangeCipher.Bytes()) + + recData := Record{ + Type: RecordTypeApplicationData, + Version: Version12, + Data: RawBytes(hostCert), + } + buf.Write(recData.Bytes()) + packet := buf.Bytes() + + mac := hmac.New(sha256.New, config.C.Secret) + mac.Write(s.clientHello.Random[:]) // nolint: errcheck + mac.Write(packet) // nolint: errcheck + copy(packet[11:], mac.Sum(nil)) + + return packet +} + +func NewServerHello(clientHello *ClientHello) *ServerHello { + rv := &ServerHello{ + clientHello: clientHello, + } + + rv.Type = HandshakeTypeServer + rv.Version = Version12 + rv.SessionID = make([]byte, len(clientHello.SessionID)) + copy(rv.SessionID, clientHello.SessionID) + + tail := bytes.NewBuffer(CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes) + tail.WriteByte(0x00) // no compression + makeTLSExtensions(tail) + rv.Tail = RawBytes(tail.Bytes()) + + return rv +} + +func makeTLSExtensions(buf io.Writer) { + buf.Write([]byte{ // nolint: errcheck + 0x00, 0x2e, // 46 bytes of data + 0x00, 0x33, // Extension - Key Share + 0x00, 0x24, // 36 bytes + 0x00, 0x1d, // x25519 curve + 0x00, 0x20, // 32 bytes of key + }) + + var dst, in, base [32]byte + + rand.Read(in[:]) // nolint: errcheck + rand.Read(base[:]) // nolint: errcheck + curve25519.ScalarMult(&dst, &in, &base) + buf.Write(dst[:]) // nolint: errcheck + + buf.Write([]byte{ // nolint: errcheck + 0x00, 0x2b, // Extension - Supported Versions + 0x00, 0x02, // 2 bytes are following + 0x03, 0x04, // TLS 1.3 + }) +} diff --git a/utils/uint24.go b/utils/uint24.go index 350f3d5..be4ac1e 100644 --- a/utils/uint24.go +++ b/utils/uint24.go @@ -1,5 +1,10 @@ package utils +import ( + "fmt" + "strings" +) + type Uint24 [3]byte func ToUint24(number uint32) Uint24 { @@ -9,3 +14,17 @@ func ToUint24(number uint32) Uint24 { func FromUint24(number Uint24) uint32 { return uint32(number[0]) + (uint32(number[1]) << 8) + (uint32(number[2]) << 16) } + +func Hexify(data []byte) string { + s := []string{} + + for _, v := range data { + if v < 0x10 { + s = append(s, fmt.Sprintf("0x0%x", v)) + } else { + s = append(s, fmt.Sprintf("0x%x", v)) + } + } + + return strings.Join(s, " ") +} diff --git a/wrappers/stream/faketls.go b/wrappers/stream/faketls.go index ee1e5f3..3db845b 100644 --- a/wrappers/stream/faketls.go +++ b/wrappers/stream/faketls.go @@ -1,28 +1,15 @@ package stream import ( - "bytes" - "encoding/binary" "errors" "fmt" - "io" "net" "time" "go.uber.org/zap" "github.com/9seconds/mtg/conntypes" -) - -var ( - errFakeTLSTimeout = errors.New("timeout") - fakeTLSWritePrefix = []byte{0x17, 0x03, 0x03} -) - -const ( - faketlsMaxChunkSize = 16384 + 24 - faketlsRecordTypeApplicationData = 0x17 - faketlsRecordTypeCCS = 0x14 + "github.com/9seconds/mtg/tlstypes" ) type wrapperFakeTLS struct { @@ -45,38 +32,20 @@ func (w *wrapperFakeTLS) WriteTimeout(p []byte, timeout time.Duration) (int, err if elapsed > timeout { return w.parent.WriteTimeout(b, timeout-elapsed) } - return 0, errFakeTLSTimeout + return 0, errors.New("timeout") }) } func (w *wrapperFakeTLS) write(p []byte, writeFunc func([]byte) (int, error)) (int, error) { sum := 0 - size := [2]byte{} - - for len(p) > 0 { - chunkSize := faketlsMaxChunkSize - if chunkSize > len(p) { - chunkSize = len(p) - } - - if _, err := writeFunc(fakeTLSWritePrefix); err != nil { - return sum, err - } - - binary.BigEndian.PutUint16(size[:], uint16(chunkSize)) - - if _, err := writeFunc(size[:]); err != nil { - return sum, err - } - - n, err := writeFunc(p[:chunkSize]) - sum += n + for _, v := range tlstypes.MakeRecords(p) { + _, err := writeFunc(v.Bytes()) if err != nil { return sum, err } - p = p[chunkSize:] + sum += len(v.Data.Bytes()) } return sum, nil @@ -108,41 +77,20 @@ func NewFakeTLS(socket conntypes.StreamReadWriteCloser) conntypes.StreamReadWrit } faketls.readFunc = func() ([]byte, error) { - data := &bytes.Buffer{} - buf := [2]byte{} - recordType := byte(faketlsRecordTypeCCS) - - for recordType == faketlsRecordTypeCCS { - if _, err := io.ReadFull(faketls.parent, buf[:1]); err != nil { - return nil, fmt.Errorf("cannot read record type: %w", err) + for { + rec, err := tlstypes.ReadRecord(faketls.parent) + if err != nil { + return nil, err } - switch buf[0] { - case faketlsRecordTypeCCS, faketlsRecordTypeApplicationData: - recordType = buf[0] + switch rec.Type { + case tlstypes.RecordTypeChangeCipherSpec: + case tlstypes.RecordTypeApplicationData: + return rec.Data.Bytes(), nil default: - return nil, fmt.Errorf("incorrect record type %v", buf[0]) - } - - if _, err := io.ReadFull(faketls.parent, buf[:]); err != nil { - return nil, fmt.Errorf("cannot read version: %w", err) - } - - if !bytes.Equal(buf[:], []byte{0x03, 0x03}) { - return nil, fmt.Errorf("unknown tls version %v", buf) - } - - if _, err := io.ReadFull(faketls.parent, buf[:]); err != nil { - return nil, fmt.Errorf("cannot read data length: %w", err) - } - - dataLength := binary.BigEndian.Uint16(buf[:]) - if _, err := io.CopyN(data, faketls.parent, int64(dataLength)); err != nil { - return nil, fmt.Errorf("cannot copy frame data: %w", err) + return nil, fmt.Errorf("unsupported record type %v", rec.Type) } } - - return data.Bytes(), nil } return faketls diff --git a/wrappers/stream/rewind.go b/wrappers/stream/rewind.go index b2f4d52..1170d9d 100644 --- a/wrappers/stream/rewind.go +++ b/wrappers/stream/rewind.go @@ -43,7 +43,10 @@ func (w *wrapperRewind) Read(p []byte) (int, error) { } n, err := w.parent.Read(p) - w.buf.Write(p[:n]) + + if !w.rewinded { + w.buf.Write(p[:n]) + } return n, err } @@ -59,7 +62,10 @@ func (w *wrapperRewind) ReadTimeout(p []byte, timeout time.Duration) (int, error } n, err := w.parent.ReadTimeout(p, timeout) - w.buf.Write(p[:n]) + + if !w.rewinded { + w.buf.Write(p[:n]) + } return n, err }