Merge pull request #7 from 9seconds/connection-types

Support different client connection types
This commit is contained in:
Sergey Arkhipov
2018-06-21 09:25:26 +03:00
committed by GitHub
11 changed files with 157 additions and 55 deletions
+2 -1
View File
@@ -5,7 +5,8 @@ import (
"net" "net"
"github.com/9seconds/mtg/config" "github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/mtproto"
) )
// Init has to initialize client connection based on given config. // Init has to initialize client connection based on given config.
type Init func(net.Conn, *config.Config) (int16, io.ReadWriteCloser, error) type Init func(net.Conn, *config.Config) (*mtproto.ConnectionOpts, io.ReadWriteCloser, error)
+6 -5
View File
@@ -7,25 +7,26 @@ import (
"github.com/juju/errors" "github.com/juju/errors"
"github.com/9seconds/mtg/config" "github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/mtproto"
"github.com/9seconds/mtg/obfuscated2" "github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/wrappers" "github.com/9seconds/mtg/wrappers"
) )
// DirectInit initializes client to access Telegram bypassing middleproxies. // DirectInit initializes client to access Telegram bypassing middleproxies.
func DirectInit(conn net.Conn, conf *config.Config) (int16, io.ReadWriteCloser, error) { func DirectInit(conn net.Conn, conf *config.Config) (*mtproto.ConnectionOpts, io.ReadWriteCloser, error) {
socket := wrappers.NewTimeoutRWC(conn, conf.TimeoutRead, conf.TimeoutWrite) socket := wrappers.NewTimeoutRWC(conn, conf.TimeoutRead, conf.TimeoutWrite)
frame, err := obfuscated2.ExtractFrame(socket) frame, err := obfuscated2.ExtractFrame(socket)
if err != nil { if err != nil {
return 0, nil, errors.Annotate(err, "Cannot extract frame") return nil, nil, errors.Annotate(err, "Cannot extract frame")
} }
defer obfuscated2.ReturnFrame(frame) defer obfuscated2.ReturnFrame(frame)
obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame) obfs2, connOpts, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame)
if err != nil { if err != nil {
return 0, nil, errors.Annotate(err, "Cannot parse obfuscated frame") return nil, nil, errors.Annotate(err, "Cannot parse obfuscated frame")
} }
socket = wrappers.NewStreamCipherRWC(socket, obfs2.Encryptor, obfs2.Decryptor) socket = wrappers.NewStreamCipherRWC(socket, obfs2.Encryptor, obfs2.Decryptor)
return dc, socket, nil return connOpts, socket, nil
} }
+55
View File
@@ -0,0 +1,55 @@
package mtproto
import (
"bytes"
"github.com/juju/errors"
)
// ConnectionType is a type of obfuscated2/mtproto connection requested
// by the user.
type ConnectionType uint8
// ConnectionOpts presents an options, metadata on connection requested
// by the user on handshake.
type ConnectionOpts struct {
DC int16
ConnectionType ConnectionType
}
// Different connection types which user requests from Telegram.
const (
ConnectionTypeUnknown ConnectionType = iota
ConnectionTypeAbridged
ConnectionTypeIntermediate
)
// Connection tags for mtproto handshakes.
var (
ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef}
ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee}
)
// Tag maps connection type to the corresponding handshake tag.
func (t ConnectionType) Tag() ([]byte, error) {
switch t {
case ConnectionTypeAbridged:
return ConnectionTagAbridged, nil
case ConnectionTypeIntermediate:
return ConnectionTagIntermediate, nil
default:
return nil, errors.Errorf("Unknown connection type %d", t)
}
}
// ConnectionTagFromHandshake maps magic bytes to the connection type.
func ConnectionTagFromHandshake(magic []byte) (ConnectionType, error) {
if bytes.Equal(magic, ConnectionTagIntermediate) {
return ConnectionTypeIntermediate, nil
}
if bytes.Equal(magic, ConnectionTagAbridged) {
return ConnectionTypeAbridged, nil
}
return ConnectionTypeUnknown, errors.New("Unknown handshake protocol")
}
+9 -9
View File
@@ -7,6 +7,8 @@ import (
"io" "io"
"github.com/juju/errors" "github.com/juju/errors"
"github.com/9seconds/mtg/mtproto"
) )
// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd] // [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd]
@@ -22,13 +24,9 @@ const (
frameOffsetMagic = frameOffsetIV + frameLenMagic frameOffsetMagic = frameOffsetIV + frameLenMagic
frameOffsetDC = frameOffsetMagic + frameLenDC frameOffsetDC = frameOffsetMagic + frameLenDC
tgMagicByte = byte(239)
FrameLen = 64 FrameLen = 64
) )
var tgMagicBytes = []byte{tgMagicByte, tgMagicByte, tgMagicByte, tgMagicByte}
// Frame represents handshake frame. Telegram sends 64 bytes of obfuscated2 // Frame represents handshake frame. Telegram sends 64 bytes of obfuscated2
// initialization data first. // initialization data first.
// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ // https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/
@@ -61,9 +59,9 @@ func (f Frame) DC() (n int16) {
return return
} }
// Valid checks that *decrypted* frame is valid. Only magic bytes are checked. // ConnectionType identifies connection type of the handshake frame.
func (f Frame) Valid() bool { func (f Frame) ConnectionType() (mtproto.ConnectionType, error) {
return bytes.Equal(f.Magic(), tgMagicBytes) return mtproto.ConnectionTagFromHandshake(f.Magic())
} }
// Invert inverts frame for extracting encryption keys. Pkease check that link: // Invert inverts frame for extracting encryption keys. Pkease check that link:
@@ -94,7 +92,7 @@ func ExtractFrame(conn io.Reader) (*Frame, error) {
return frame, nil return frame, nil
} }
func generateFrame() *Frame { func generateFrame(connectionType mtproto.ConnectionType) *Frame {
frame := MakeFrame() frame := MakeFrame()
data := *frame data := *frame
@@ -116,7 +114,9 @@ func generateFrame() *Frame {
continue continue
} }
copy(data.Magic(), tgMagicBytes) // error has to be checked before calling this function
tag, _ := connectionType.Tag() // nolint: errcheck
copy(data.Magic(), tag)
return frame return frame
} }
+22 -5
View File
@@ -2,9 +2,12 @@ package obfuscated2
import ( import (
"bytes" "bytes"
"strconv"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/9seconds/mtg/mtproto"
) )
func TestFrameKey(t *testing.T) { func TestFrameKey(t *testing.T) {
@@ -28,7 +31,7 @@ func TestFrameIV(t *testing.T) {
func TestFrameMagic(t *testing.T) { func TestFrameMagic(t *testing.T) {
toCompare := make([]byte, 4) toCompare := make([]byte, 4)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
toCompare[i] = tgMagicByte toCompare[i] = 0xee
} }
assert.Equal(t, toCompare, makeFrame().Magic()) assert.Equal(t, toCompare, makeFrame().Magic())
@@ -40,10 +43,13 @@ func TestFrameDC(t *testing.T) {
func TestFrameValid(t *testing.T) { func TestFrameValid(t *testing.T) {
frame := makeFrame() frame := makeFrame()
assert.True(t, frame.Valid()) connType, err := frame.ConnectionType()
assert.Nil(t, err)
assert.Equal(t, connType, mtproto.ConnectionTypeIntermediate)
frame[8+32+16+2] = byte(3) frame[8+32+16+2] = byte(3)
assert.False(t, frame.Valid()) _, err = frame.ConnectionType()
assert.NotNil(t, err)
} }
func TestFrameDoubleInvert(t *testing.T) { func TestFrameDoubleInvert(t *testing.T) {
@@ -66,7 +72,18 @@ func TestFrameInvert(t *testing.T) {
} }
func TestFrameGenerateValid(t *testing.T) { func TestFrameGenerateValid(t *testing.T) {
assert.True(t, generateFrame().Valid()) validTests := []mtproto.ConnectionType{
mtproto.ConnectionTypeIntermediate,
mtproto.ConnectionTypeAbridged,
}
for _, test := range validTests {
t.Run(strconv.Itoa(int(test)), func(tt *testing.T) {
frame := generateFrame(test)
conType, err := frame.ConnectionType()
assert.Nil(t, err)
assert.Equal(t, conType, test)
})
}
} }
func makeFrame() Frame { func makeFrame() Frame {
@@ -79,7 +96,7 @@ func makeFrame() Frame {
f[i] = byte(2) f[i] = byte(2)
} }
for i := (8 + 32 + 16); i < (8 + 32 + 16 + 4); i++ { for i := (8 + 32 + 16); i < (8 + 32 + 16 + 4); i++ {
f[i] = tgMagicByte f[i] = 0xee
} }
for i := (8 + 32 + 16 + 4); i < (8 + 32 + 16 + 4 + 2); i++ { for i := (8 + 32 + 16 + 4); i < (8 + 32 + 16 + 4 + 2); i++ {
f[i] = byte(3) f[i] = byte(3)
+13 -6
View File
@@ -6,6 +6,8 @@ import (
"crypto/sha256" "crypto/sha256"
"github.com/juju/errors" "github.com/juju/errors"
"github.com/9seconds/mtg/mtproto"
) )
// Obfuscated2 contains AES CTR encryption and decryption streams // Obfuscated2 contains AES CTR encryption and decryption streams
@@ -19,7 +21,7 @@ type Obfuscated2 struct {
// details: http://telegra.ph/telegram-blocks-wtf-05-26 // details: http://telegra.ph/telegram-blocks-wtf-05-26
// //
// Beware, link above is in russian. // Beware, link above is in russian.
func ParseObfuscated2ClientFrame(secret []byte, frame *Frame) (*Obfuscated2, int16, error) { func ParseObfuscated2ClientFrame(secret []byte, frame *Frame) (*Obfuscated2, *mtproto.ConnectionOpts, error) {
decHasher := sha256.New() decHasher := sha256.New()
decHasher.Write(frame.Key()) // nolint: errcheck decHasher.Write(frame.Key()) // nolint: errcheck
decHasher.Write(secret) // nolint: errcheck decHasher.Write(secret) // nolint: errcheck
@@ -34,23 +36,28 @@ func ParseObfuscated2ClientFrame(secret []byte, frame *Frame) (*Obfuscated2, int
decryptedFrame := MakeFrame() decryptedFrame := MakeFrame()
defer ReturnFrame(decryptedFrame) defer ReturnFrame(decryptedFrame)
decryptor.XORKeyStream(*decryptedFrame, *frame) decryptor.XORKeyStream(*decryptedFrame, *frame)
if !decryptedFrame.Valid() { connType, err := decryptedFrame.ConnectionType()
return nil, 0, errors.New("Unknown protocol") if err != nil {
return nil, nil, errors.Annotate(err, "Unknown protocol")
} }
obfs := &Obfuscated2{ obfs := &Obfuscated2{
Decryptor: decryptor, Decryptor: decryptor,
Encryptor: encryptor, Encryptor: encryptor,
} }
connOpts := &mtproto.ConnectionOpts{
DC: decryptedFrame.DC(),
ConnectionType: connType,
}
return obfs, decryptedFrame.DC(), nil return obfs, connOpts, nil
} }
// MakeTelegramObfuscated2Frame creates new handshake frame to send to // MakeTelegramObfuscated2Frame creates new handshake frame to send to
// Telegram. // Telegram.
// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ // https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/
func MakeTelegramObfuscated2Frame() (*Obfuscated2, *Frame) { func MakeTelegramObfuscated2Frame(opts *mtproto.ConnectionOpts) (*Obfuscated2, *Frame) {
frame := generateFrame() frame := generateFrame(opts.ConnectionType)
encryptor := makeStreamCipher(frame.Key(), frame.IV()) encryptor := makeStreamCipher(frame.Key(), frame.IV())
decryptorFrame := frame.Invert() decryptorFrame := frame.Invert()
+22 -6
View File
@@ -5,20 +5,31 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/9seconds/mtg/mtproto"
) )
func TestObfs2TelegramFrameDecrypt(t *testing.T) { func TestObfs2TelegramFrameDecrypt(t *testing.T) {
_, frame := MakeTelegramObfuscated2Frame() connOpts := &mtproto.ConnectionOpts{
DC: 1,
ConnectionType: mtproto.ConnectionTypeIntermediate,
}
_, frame := MakeTelegramObfuscated2Frame(connOpts)
decryptor := makeStreamCipher(frame.Key(), frame.IV()) decryptor := makeStreamCipher(frame.Key(), frame.IV())
decrypted := make(Frame, FrameLen) decrypted := make(Frame, FrameLen)
decryptor.XORKeyStream(decrypted, *frame) decryptor.XORKeyStream(decrypted, *frame)
assert.True(t, decrypted.Valid()) _, err := decrypted.ConnectionType()
assert.Nil(t, err)
} }
func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) { func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) {
obfs2, frame := MakeTelegramObfuscated2Frame() connOpts := &mtproto.ConnectionOpts{
DC: 1,
ConnectionType: mtproto.ConnectionTypeIntermediate,
}
obfs2, frame := MakeTelegramObfuscated2Frame(connOpts)
inverted := frame.Invert() inverted := frame.Invert()
encryptor := makeStreamCipher(inverted.Key(), inverted.IV()) encryptor := makeStreamCipher(inverted.Key(), inverted.IV())
@@ -34,7 +45,7 @@ func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) {
func TestObfs2Full(t *testing.T) { func TestObfs2Full(t *testing.T) {
secret := []byte{1, 2, 3, 4, 5} secret := []byte{1, 2, 3, 4, 5}
clientFrame := generateFrame() clientFrame := generateFrame(mtproto.ConnectionTypeIntermediate)
clientHasher := sha256.New() clientHasher := sha256.New()
clientHasher.Write(clientFrame.Key()) clientHasher.Write(clientFrame.Key())
clientHasher.Write(secret) clientHasher.Write(secret)
@@ -55,11 +66,16 @@ func TestObfs2Full(t *testing.T) {
clientObfs, _, err := ParseObfuscated2ClientFrame(secret, &encrypted) clientObfs, _, err := ParseObfuscated2ClientFrame(secret, &encrypted)
assert.Nil(t, err) assert.Nil(t, err)
tgObfs, tgFrame := MakeTelegramObfuscated2Frame() connOpts := &mtproto.ConnectionOpts{
DC: 1,
ConnectionType: mtproto.ConnectionTypeIntermediate,
}
tgObfs, tgFrame := MakeTelegramObfuscated2Frame(connOpts)
tgDecryptor := makeStreamCipher(tgFrame.Key(), tgFrame.IV()) tgDecryptor := makeStreamCipher(tgFrame.Key(), tgFrame.IV())
decrypted := make(Frame, FrameLen) decrypted := make(Frame, FrameLen)
tgDecryptor.XORKeyStream(decrypted, *tgFrame) tgDecryptor.XORKeyStream(decrypted, *tgFrame)
assert.True(t, decrypted.Valid()) _, err = decrypted.ConnectionType()
assert.Nil(t, err)
tgInvertedFrame := tgFrame.Invert() tgInvertedFrame := tgFrame.Invert()
tgEncryptor := makeStreamCipher(tgInvertedFrame.Key(), tgInvertedFrame.IV()) tgEncryptor := makeStreamCipher(tgInvertedFrame.Key(), tgInvertedFrame.IV())
+10 -9
View File
@@ -12,6 +12,7 @@ import (
"github.com/9seconds/mtg/client" "github.com/9seconds/mtg/client"
"github.com/9seconds/mtg/config" "github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/mtproto"
"github.com/9seconds/mtg/telegram" "github.com/9seconds/mtg/telegram"
"github.com/9seconds/mtg/wrappers" "github.com/9seconds/mtg/wrappers"
) )
@@ -60,7 +61,7 @@ func (s *Server) accept(conn net.Conn) {
"socketid", socketID, "socketid", socketID,
) )
dc, clientConn, err := s.getClientStream(ctx, cancel, conn, socketID) connOpts, clientConn, err := s.getClientStream(ctx, cancel, conn, socketID)
if err != nil { if err != nil {
s.logger.Warnw("Cannot initialize client connection", s.logger.Warnw("Cannot initialize client connection",
"addr", conn.RemoteAddr().String(), "addr", conn.RemoteAddr().String(),
@@ -71,7 +72,7 @@ func (s *Server) accept(conn net.Conn) {
} }
defer clientConn.Close() // nolint: errcheck defer clientConn.Close() // nolint: errcheck
tgConn, err := s.getTelegramStream(ctx, cancel, dc, socketID) tgConn, err := s.getTelegramStream(ctx, cancel, connOpts, socketID)
if err != nil { if err != nil {
s.logger.Warnw("Cannot initialize Telegram connection", s.logger.Warnw("Cannot initialize Telegram connection",
"socketid", socketID, "socketid", socketID,
@@ -96,27 +97,27 @@ func (s *Server) accept(conn net.Conn) {
) )
} }
func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (int16, io.ReadWriteCloser, error) { func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (*mtproto.ConnectionOpts, io.ReadWriteCloser, error) {
dc, socket, err := s.clientInit(conn, s.conf) connOpts, socket, err := s.clientInit(conn, s.conf)
if err != nil { if err != nil {
return 0, nil, errors.Annotate(err, "Cannot init client connection") return nil, nil, errors.Annotate(err, "Cannot init client connection")
} }
socket = wrappers.NewTrafficRWC(socket, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) socket = wrappers.NewTrafficRWC(socket, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
socket = wrappers.NewLogRWC(socket, s.logger, socketID, "client") socket = wrappers.NewLogRWC(socket, s.logger, socketID, "client")
socket = wrappers.NewCtxRWC(ctx, cancel, socket) socket = wrappers.NewCtxRWC(ctx, cancel, socket)
return dc, socket, nil return connOpts, socket, nil
} }
func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) { func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, connOpts *mtproto.ConnectionOpts, socketID string) (io.ReadWriteCloser, error) {
conn, err := s.tg.Dial(dc) conn, err := s.tg.Dial(connOpts)
if err != nil { if err != nil {
return nil, errors.Annotate(err, "Cannot connect to Telegram") return nil, errors.Annotate(err, "Cannot connect to Telegram")
} }
conn = wrappers.NewTrafficRWC(conn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) conn = wrappers.NewTrafficRWC(conn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic)
conn, err = s.tg.Init(conn) conn, err = s.tg.Init(connOpts, conn)
if err != nil { if err != nil {
return nil, errors.Annotate(err, "Cannot handshake Telegram") return nil, errors.Annotate(err, "Cannot handshake Telegram")
} }
+2 -2
View File
@@ -45,8 +45,8 @@ func (t *tgDialer) dialRWC(addr string) (io.ReadWriteCloser, error) {
return wrappers.NewTimeoutRWC(conn, t.conf.TimeoutRead, t.conf.TimeoutWrite), nil return wrappers.NewTimeoutRWC(conn, t.conf.TimeoutRead, t.conf.TimeoutWrite), nil
} }
func newDialer(conf *config.Config) *tgDialer { func newDialer(conf *config.Config) tgDialer {
return &tgDialer{ return tgDialer{
Dialer: net.Dialer{Timeout: conf.TimeoutRead}, Dialer: net.Dialer{Timeout: conf.TimeoutRead},
conf: conf, conf: conf,
} }
+10 -8
View File
@@ -6,6 +6,7 @@ import (
"github.com/juju/errors" "github.com/juju/errors"
"github.com/9seconds/mtg/config" "github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/mtproto"
"github.com/9seconds/mtg/obfuscated2" "github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/wrappers" "github.com/9seconds/mtg/wrappers"
) )
@@ -31,18 +32,19 @@ type directTelegram struct {
baseTelegram baseTelegram
} }
func (t *directTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) { func (t *directTelegram) Dial(connOpts *mtproto.ConnectionOpts) (io.ReadWriteCloser, error) {
if dcIdx < 0 { dc := connOpts.DC
dcIdx = -dcIdx if dc < 0 {
} else if dcIdx == 0 { dc = -dc
dcIdx = 1 } else if dc == 0 {
dc = 1
} }
return t.baseTelegram.Dial(dcIdx - 1) return t.baseTelegram.dial(dc - 1)
} }
func (t *directTelegram) Init(conn io.ReadWriteCloser) (io.ReadWriteCloser, error) { func (t *directTelegram) Init(connOpts *mtproto.ConnectionOpts, conn io.ReadWriteCloser) (io.ReadWriteCloser, error) {
obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame(connOpts)
defer obfuscated2.ReturnFrame(frame) defer obfuscated2.ReturnFrame(frame)
if n, err := conn.Write(*frame); err != nil || n != len(*frame) { if n, err := conn.Write(*frame); err != nil || n != len(*frame) {
+6 -4
View File
@@ -5,24 +5,26 @@ import (
"math/rand" "math/rand"
"github.com/juju/errors" "github.com/juju/errors"
"github.com/9seconds/mtg/mtproto"
) )
// Telegram defines an interface to connect to Telegram. This // Telegram defines an interface to connect to Telegram. This
// encapsulates logic of working with middleproxies or direct // encapsulates logic of working with middleproxies or direct
// connections. // connections.
type Telegram interface { type Telegram interface {
Dial(int16) (io.ReadWriteCloser, error) Dial(*mtproto.ConnectionOpts) (io.ReadWriteCloser, error)
Init(io.ReadWriteCloser) (io.ReadWriteCloser, error) Init(*mtproto.ConnectionOpts, io.ReadWriteCloser) (io.ReadWriteCloser, error)
} }
type baseTelegram struct { type baseTelegram struct {
dialer *tgDialer dialer tgDialer
v4Addresses map[int16][]string v4Addresses map[int16][]string
v6Addresses map[int16][]string v6Addresses map[int16][]string
} }
func (b *baseTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) { func (b *baseTelegram) dial(dcIdx int16) (io.ReadWriteCloser, error) {
addrs := make([]string, 2) addrs := make([]string, 2)
if addr, ok := b.v6Addresses[dcIdx]; ok && len(addr) > 0 { if addr, ok := b.v6Addresses[dcIdx]; ok && len(addr) > 0 {
addrs = append(addrs, addr[rand.Intn(len(addr))]) addrs = append(addrs, addr[rand.Intn(len(addr))])