From 4486fbb8f469d5ae8fa7868b3919edc747a9525a Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 10:48:48 +0300 Subject: [PATCH 01/16] Refactor cipherrwc to wrappers --- obfuscated2/obfuscated2.go | 26 ++++------------- proxy/cipherrwc.go | 56 ------------------------------------- proxy/server.go | 5 ++-- wrappers/streamcipherrwc.go | 53 +++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 78 deletions(-) delete mode 100644 proxy/cipherrwc.go create mode 100644 wrappers/streamcipherrwc.go diff --git a/obfuscated2/obfuscated2.go b/obfuscated2/obfuscated2.go index 2ac62af..d75759a 100644 --- a/obfuscated2/obfuscated2.go +++ b/obfuscated2/obfuscated2.go @@ -11,22 +11,8 @@ import ( // Obfuscated2 contains AES CTR encryption and decryption streams // for telegram connection. type Obfuscated2 struct { - decryptor cipher.Stream - encryptor cipher.Stream -} - -// Encrypt encrypts given data. -func (o *Obfuscated2) Encrypt(data []byte) []byte { - buf := make([]byte, len(data)) - o.encryptor.XORKeyStream(buf, data) - return buf -} - -// Decrypt decrypts given data. -func (o *Obfuscated2) Decrypt(data []byte) []byte { - buf := make([]byte, len(data)) - o.decryptor.XORKeyStream(buf, data) - return buf + Decryptor cipher.Stream + Encryptor cipher.Stream } // ParseObfuscated2ClientFrame parses client frame. Please check this link for @@ -54,8 +40,8 @@ func ParseObfuscated2ClientFrame(secret, data []byte) (*Obfuscated2, int16, erro } obfs := &Obfuscated2{ - decryptor: decryptor, - encryptor: encryptor, + Decryptor: decryptor, + Encryptor: encryptor, } return obfs, decryptedFrame.DC(), nil @@ -77,8 +63,8 @@ func MakeTelegramObfuscated2Frame() (*Obfuscated2, Frame) { copy(frame, copyFrame) obfs := &Obfuscated2{ - decryptor: decryptor, - encryptor: encryptor, + Decryptor: decryptor, + Encryptor: encryptor, } return obfs, frame diff --git a/proxy/cipherrwc.go b/proxy/cipherrwc.go deleted file mode 100644 index 43a03cb..0000000 --- a/proxy/cipherrwc.go +++ /dev/null @@ -1,56 +0,0 @@ -package proxy - -import ( - "bytes" - "io" -) - -// Cipher is an interface to anything which can encrypt and decrypt -type Cipher interface { - Encrypt([]byte) []byte - Decrypt([]byte) []byte -} - -// CipherReadWriteCloser wraps connection for transparent encryption -type CipherReadWriteCloser struct { - crypt Cipher - conn io.ReadWriteCloser - rest *bytes.Buffer -} - -// Read reads from connection -func (c *CipherReadWriteCloser) Read(p []byte) (n int, err error) { - n, err = c.conn.Read(p) - copy(p, c.crypt.Decrypt(p[:n])) - return -} - -// Write writes into connection. -func (c *CipherReadWriteCloser) Write(p []byte) (int, error) { - encrypted := c.crypt.Encrypt(p) - allWritten := 0 - - for len(encrypted) > 0 { - n, err := c.conn.Write(encrypted) - allWritten += n - if err != nil { - return allWritten, err - } - encrypted = encrypted[n:] - } - - return allWritten, nil -} - -// Close closes underlying connection. -func (c *CipherReadWriteCloser) Close() error { - return c.conn.Close() -} - -func newCipherReadWriteCloser(conn io.ReadWriteCloser, crypt Cipher) *CipherReadWriteCloser { - return &CipherReadWriteCloser{ - conn: conn, - crypt: crypt, - rest: &bytes.Buffer{}, - } -} diff --git a/proxy/server.go b/proxy/server.go index bd3b457..8251c6c 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -9,6 +9,7 @@ import ( "time" "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/wrappers" "github.com/juju/errors" uuid "github.com/satori/go.uuid" "go.uber.org/zap" @@ -124,7 +125,7 @@ func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, } wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "client") - wConn = newCipherReadWriteCloser(wConn, obfs2) + wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) wConn = newCtxReadWriteCloser(ctx, cancel, wConn) return wConn, dc, nil @@ -144,7 +145,7 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun } wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "telegram") - wConn = newCipherReadWriteCloser(wConn, obfs2) + wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) wConn = newCtxReadWriteCloser(ctx, cancel, wConn) return wConn, nil diff --git a/wrappers/streamcipherrwc.go b/wrappers/streamcipherrwc.go new file mode 100644 index 0000000..c12b7dd --- /dev/null +++ b/wrappers/streamcipherrwc.go @@ -0,0 +1,53 @@ +package wrappers + +import ( + "bytes" + "crypto/cipher" + "io" +) + +type StreamCipherReadWriteCloser struct { + encryptor cipher.Stream + decryptor cipher.Stream + conn io.ReadWriteCloser + rest *bytes.Buffer +} + +// Read reads from connection +func (c *StreamCipherReadWriteCloser) Read(p []byte) (n int, err error) { + n, err = c.conn.Read(p) + c.decryptor.XORKeyStream(p, p[:n]) + return +} + +// Write writes into connection. +func (c *StreamCipherReadWriteCloser) Write(p []byte) (int, error) { + encrypted := make([]byte, len(p)) + c.encryptor.XORKeyStream(encrypted, p) + allWritten := 0 + + for len(encrypted) > 0 { + n, err := c.conn.Write(encrypted) + allWritten += n + if err != nil { + return allWritten, err + } + encrypted = encrypted[n:] + } + + return allWritten, nil +} + +// Close closes underlying connection. +func (c *StreamCipherReadWriteCloser) Close() error { + return c.conn.Close() +} + +func NewStreamCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.Stream) io.ReadWriteCloser { + return &StreamCipherReadWriteCloser{ + conn: conn, + encryptor: encryptor, + decryptor: decryptor, + rest: &bytes.Buffer{}, + } +} From 65f8e2ac30ae7dd1932ad0744d622b6f34f97835 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 10:50:57 +0300 Subject: [PATCH 02/16] Refactor ctxrw to wrappers --- proxy/server.go | 4 ++-- {proxy => wrappers}/ctxrwc.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename {proxy => wrappers}/ctxrwc.go (88%) diff --git a/proxy/server.go b/proxy/server.go index 8251c6c..14458a0 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -126,7 +126,7 @@ func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "client") wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) - wConn = newCtxReadWriteCloser(ctx, cancel, wConn) + wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) return wConn, dc, nil } @@ -146,7 +146,7 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "telegram") wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) - wConn = newCtxReadWriteCloser(ctx, cancel, wConn) + wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) return wConn, nil } diff --git a/proxy/ctxrwc.go b/wrappers/ctxrwc.go similarity index 88% rename from proxy/ctxrwc.go rename to wrappers/ctxrwc.go index e605ab3..452d2e3 100644 --- a/proxy/ctxrwc.go +++ b/wrappers/ctxrwc.go @@ -1,4 +1,4 @@ -package proxy +package wrappers import ( "context" @@ -48,7 +48,7 @@ func (c *CtxReadWriteCloser) Close() error { return c.conn.Close() } -func newCtxReadWriteCloser(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser { +func NewCtxRWC(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser { return &CtxReadWriteCloser{ conn: conn, ctx: ctx, From 26020fcd0cb42ed3b59f820ce180fe501331f9cb Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 10:52:50 +0300 Subject: [PATCH 03/16] Refactor logrwc to wrappers --- proxy/server.go | 4 ++-- {proxy => wrappers}/logrwc.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename {proxy => wrappers}/logrwc.go (88%) diff --git a/proxy/server.go b/proxy/server.go index 14458a0..67815e5 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -124,7 +124,7 @@ func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, return nil, 0, errors.Annotate(err, "Cannot create client stream") } - wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "client") + wConn = wrappers.NewLogRWC(wConn, s.logger, socketID, "client") wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) @@ -144,7 +144,7 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun return nil, errors.Annotate(err, "Cannot write hadnshake frame") } - wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "telegram") + wConn = wrappers.NewLogRWC(wConn, s.logger, socketID, "telegram") wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) diff --git a/proxy/logrwc.go b/wrappers/logrwc.go similarity index 88% rename from proxy/logrwc.go rename to wrappers/logrwc.go index b783d3b..00ef597 100644 --- a/proxy/logrwc.go +++ b/wrappers/logrwc.go @@ -1,4 +1,4 @@ -package proxy +package wrappers import ( "io" @@ -36,7 +36,7 @@ func (l *LogReadWriteCloser) Close() error { return err } -func newLogReadWriteCloser(conn io.ReadWriteCloser, logger *zap.SugaredLogger, sockid string, name string) io.ReadWriteCloser { +func NewLogRWC(conn io.ReadWriteCloser, logger *zap.SugaredLogger, sockid string, name string) io.ReadWriteCloser { return &LogReadWriteCloser{ conn: conn, logger: logger, From c92a4b8c681e33eb6930941178c058190d621e04 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 10:54:42 +0300 Subject: [PATCH 04/16] Refactor timeoutrwc to wrappers --- proxy/server.go | 4 ++-- proxy/timeoutrwc.go | 40 ---------------------------------------- 2 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 proxy/timeoutrwc.go diff --git a/proxy/server.go b/proxy/server.go index 67815e5..43a9be2 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -112,7 +112,7 @@ func (s *Server) makeSocketID() string { } func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (io.ReadWriteCloser, int16, error) { - wConn := newTimeoutReadWriteCloser(conn, s.readTimeout, s.writeTimeout) + wConn := wrappers.NewTimeoutRWC(conn, s.readTimeout, s.writeTimeout) wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) frame, err := obfuscated2.ExtractFrame(wConn) if err != nil { @@ -136,7 +136,7 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun if err != nil { return nil, errors.Annotate(err, "Cannot dial") } - wConn := newTimeoutReadWriteCloser(socket, s.readTimeout, s.writeTimeout) + wConn := wrappers.NewTimeoutRWC(socket, s.readTimeout, s.writeTimeout) wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() diff --git a/proxy/timeoutrwc.go b/proxy/timeoutrwc.go deleted file mode 100644 index 0067d60..0000000 --- a/proxy/timeoutrwc.go +++ /dev/null @@ -1,40 +0,0 @@ -package proxy - -import ( - "io" - "net" - "time" -) - -// TimeoutReadWriteCloser sets timeouts for read/write into underlying -// network connection. -type TimeoutReadWriteCloser struct { - conn net.Conn - readTimeout time.Duration - writeTimeout time.Duration -} - -// Read reads from connection -func (t *TimeoutReadWriteCloser) Read(p []byte) (int, error) { - t.conn.SetReadDeadline(time.Now().Add(t.readTimeout)) // nolint: errcheck, gas - return t.conn.Read(p) -} - -// Write writes into connection. -func (t *TimeoutReadWriteCloser) Write(p []byte) (int, error) { - t.conn.SetWriteDeadline(time.Now().Add(t.writeTimeout)) // nolint: errcheck, gas - return t.conn.Write(p) -} - -// Close closes underlying connection. -func (t *TimeoutReadWriteCloser) Close() error { - return t.conn.Close() -} - -func newTimeoutReadWriteCloser(conn net.Conn, readTimeout, writeTimeout time.Duration) io.ReadWriteCloser { - return &TimeoutReadWriteCloser{ - conn: conn, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - } -} From 7f01c03cb76fa90630d6d8156c578e311cda105b Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 10:56:14 +0300 Subject: [PATCH 05/16] Refactor trafficrwc to wrappers --- proxy/server.go | 4 ++-- wrappers/timeoutrwc.go | 40 +++++++++++++++++++++++++++++++ {proxy => wrappers}/trafficrwc.go | 4 ++-- 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 wrappers/timeoutrwc.go rename {proxy => wrappers}/trafficrwc.go (85%) diff --git a/proxy/server.go b/proxy/server.go index 43a9be2..26fcfec 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -113,7 +113,7 @@ func (s *Server) makeSocketID() string { func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (io.ReadWriteCloser, int16, error) { wConn := wrappers.NewTimeoutRWC(conn, s.readTimeout, s.writeTimeout) - wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) + wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) frame, err := obfuscated2.ExtractFrame(wConn) if err != nil { return nil, 0, errors.Annotate(err, "Cannot create client stream") @@ -137,7 +137,7 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun return nil, errors.Annotate(err, "Cannot dial") } wConn := wrappers.NewTimeoutRWC(socket, s.readTimeout, s.writeTimeout) - wConn = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) + wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() if n, err := socket.Write(frame); err != nil || n != len(frame) { diff --git a/wrappers/timeoutrwc.go b/wrappers/timeoutrwc.go new file mode 100644 index 0000000..b83236c --- /dev/null +++ b/wrappers/timeoutrwc.go @@ -0,0 +1,40 @@ +package wrappers + +import ( + "io" + "net" + "time" +) + +// TimeoutReadWriteCloser sets timeouts for read/write into underlying +// network connection. +type TimeoutReadWriteCloser struct { + conn net.Conn + readTimeout time.Duration + writeTimeout time.Duration +} + +// Read reads from connection +func (t *TimeoutReadWriteCloser) Read(p []byte) (int, error) { + t.conn.SetReadDeadline(time.Now().Add(t.readTimeout)) // nolint: errcheck, gas + return t.conn.Read(p) +} + +// Write writes into connection. +func (t *TimeoutReadWriteCloser) Write(p []byte) (int, error) { + t.conn.SetWriteDeadline(time.Now().Add(t.writeTimeout)) // nolint: errcheck, gas + return t.conn.Write(p) +} + +// Close closes underlying connection. +func (t *TimeoutReadWriteCloser) Close() error { + return t.conn.Close() +} + +func NewTimeoutRWC(conn net.Conn, readTimeout, writeTimeout time.Duration) io.ReadWriteCloser { + return &TimeoutReadWriteCloser{ + conn: conn, + readTimeout: readTimeout, + writeTimeout: writeTimeout, + } +} diff --git a/proxy/trafficrwc.go b/wrappers/trafficrwc.go similarity index 85% rename from proxy/trafficrwc.go rename to wrappers/trafficrwc.go index a6be861..207addd 100644 --- a/proxy/trafficrwc.go +++ b/wrappers/trafficrwc.go @@ -1,4 +1,4 @@ -package proxy +package wrappers import "io" @@ -29,7 +29,7 @@ func (t *TrafficReadWriteCloser) Close() error { return t.conn.Close() } -func newTrafficReadWriteCloser(conn io.ReadWriteCloser, readCallback, writeCallback func(int)) io.ReadWriteCloser { +func NewTrafficRWC(conn io.ReadWriteCloser, readCallback, writeCallback func(int)) io.ReadWriteCloser { return &TrafficReadWriteCloser{ conn: conn, readCallback: readCallback, From 336730b91974133c6959b2fc46f54d2e43ecab70 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 5 Jun 2018 11:20:09 +0300 Subject: [PATCH 06/16] Add rwc for block cipher mode --- wrappers/blockcipherrwc.go | 73 +++++++++++++++++++++++++++++++++++++ wrappers/streamcipherrwc.go | 3 -- 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 wrappers/blockcipherrwc.go diff --git a/wrappers/blockcipherrwc.go b/wrappers/blockcipherrwc.go new file mode 100644 index 0000000..fa72ecb --- /dev/null +++ b/wrappers/blockcipherrwc.go @@ -0,0 +1,73 @@ +package wrappers + +import ( + "bytes" + "crypto/cipher" + "io" + + "github.com/juju/errors" +) + +type BlockCipherReadWriteCloser struct { + encryptor cipher.BlockMode + decryptor cipher.BlockMode + conn io.ReadWriteCloser + buf *bytes.Buffer +} + +func (c *BlockCipherReadWriteCloser) Read(p []byte) (n int, err error) { + blockSize := c.decryptor.BlockSize() + if len(p) < blockSize { + return 0, errors.New("Cannot read less than blocksize") + } + + n, err = c.conn.Read(p) + c.buf.Write(p[:n]) + + wantToRead := c.getFullBlocks(len(p), blockSize) + haveBlocks := c.getFullBlocks(c.buf.Len(), blockSize) + if haveBlocks < wantToRead { + wantToRead = haveBlocks + } + wantToRead *= blockSize + + data := c.buf.Bytes() + c.decryptor.CryptBlocks(p, data[:wantToRead]) + c.buf = bytes.NewBuffer(data[wantToRead:]) + + return wantToRead, err +} + +func (c *BlockCipherReadWriteCloser) Write(p []byte) (n int, err error) { + blockSize := c.encryptor.BlockSize() + if len(p)%blockSize != 0 { + return 0, errors.New("Write size should be compatible with block size") + } + + buf := make([]byte, len(p)) + c.encryptor.CryptBlocks(buf, p) + + return c.conn.Write(buf) +} + +func (c *BlockCipherReadWriteCloser) Close() error { + return c.conn.Close() +} + +func (c *BlockCipherReadWriteCloser) getFullBlocks(number, blockSize int) int { + blocks := number / blockSize + + if blocks > 0 && number%blockSize != 0 { + blocks-- + } + + return blocks +} + +func NewBlockCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.BlockMode) io.ReadWriteCloser { + return &BlockCipherReadWriteCloser{ + conn: conn, + encryptor: encryptor, + decryptor: decryptor, + } +} diff --git a/wrappers/streamcipherrwc.go b/wrappers/streamcipherrwc.go index c12b7dd..5d7d018 100644 --- a/wrappers/streamcipherrwc.go +++ b/wrappers/streamcipherrwc.go @@ -1,7 +1,6 @@ package wrappers import ( - "bytes" "crypto/cipher" "io" ) @@ -10,7 +9,6 @@ type StreamCipherReadWriteCloser struct { encryptor cipher.Stream decryptor cipher.Stream conn io.ReadWriteCloser - rest *bytes.Buffer } // Read reads from connection @@ -48,6 +46,5 @@ func NewStreamCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.Str conn: conn, encryptor: encryptor, decryptor: decryptor, - rest: &bytes.Buffer{}, } } From 86e3be475a8e6d5395ae307903c533b6046e5c25 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Sun, 17 Jun 2018 12:25:51 +0300 Subject: [PATCH 07/16] Introduce explicit config --- config/config.go | 129 +++++++++++++++++++++++++++++++++++++++++++ config/global_ips.go | 38 +++++++++++++ config/urls.go | 59 ++++++++++++++++++++ main.go | 86 +++++++++++++---------------- proxy/server.go | 55 ++++++------------ proxy/stats.go | 81 +++++---------------------- proxy/telegram.go | 12 ++-- 7 files changed, 301 insertions(+), 159 deletions(-) create mode 100644 config/config.go create mode 100644 config/global_ips.go create mode 100644 config/urls.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..793d4d0 --- /dev/null +++ b/config/config.go @@ -0,0 +1,129 @@ +package config + +import ( + "encoding/hex" + "fmt" + "net" + "strconv" + "time" + + "github.com/juju/errors" +) + +type Config struct { + Debug bool + Verbose bool + BindIP net.IP + BindPort uint16 + + PublicIPv4 net.IP + PublicIPv4Port uint16 + PublicIPv6 net.IP + PublicIPv6Port uint16 + + StatsIP net.IP + StatsPort uint16 + + TimeoutRead time.Duration + TimeoutWrite time.Duration + + Secret []byte +} + +type URLs struct { + TG string `json:"tg_url"` + TMe string `json:"tme_url"` + TGQRCode string `json:"tg_qrcode"` + TMeQRCode string `json:"tme_qrcode"` +} + +type IPURLs struct { + IPv4 URLs `json:"ipv4"` + IPv6 URLs `json:"ipv6"` +} + +func (c *Config) BindAddr() string { + return getAddr(c.BindIP, c.BindPort) +} + +func (c *Config) IPv4Addr() string { + return getAddr(c.PublicIPv4, c.PublicIPv4Port) +} + +func (c *Config) IPv6Addr() string { + return getAddr(c.PublicIPv6, c.PublicIPv6Port) +} + +func (c *Config) StatAddr() string { + return getAddr(c.StatsIP, c.StatsPort) +} + +func (c *Config) GetURLs() IPURLs { + return IPURLs{ + IPv4: getURLs(c.PublicIPv4, c.PublicIPv4Port, c.Secret), + IPv6: getURLs(c.PublicIPv6, c.PublicIPv6Port, c.Secret), + } +} + +func getAddr(host fmt.Stringer, port uint16) string { + return net.JoinHostPort(host.String(), strconv.Itoa(int(port))) +} + +func NewConfig(debug, verbose bool, + bindIP net.IP, bindPort uint16, + publicIPv4 net.IP, PublicIPv4Port uint16, + publicIPv6 net.IP, publicIPv6Port uint16, + statsIP net.IP, statsPort uint16, + timeoutRead, timeoutWrite time.Duration, + secret string) (*Config, error) { + secretBytes, err := hex.DecodeString(secret) + if err != nil { + return nil, errors.Annotate(err, "Cannot create config") + } + + if publicIPv4 == nil { + publicIPv4, err = getGlobalIPv4() + if err != nil { + return nil, errors.Errorf("Cannot get public IP") + } + } + if publicIPv4.To4() == nil { + return nil, errors.Errorf("IP %s is not IPv4", publicIPv4.String()) + } + if PublicIPv4Port == 0 { + PublicIPv4Port = bindPort + } + + if publicIPv6 == nil { + publicIPv6, err = getGlobalIPv6() + if err != nil { + publicIPv6 = publicIPv4 + } + } + if publicIPv6.To16() == nil { + return nil, errors.Errorf("IP %s is not IPv6", publicIPv6.String()) + } + if publicIPv6Port == 0 { + publicIPv6Port = bindPort + } + + if statsIP == nil { + statsIP = publicIPv4 + } + + conf := &Config{ + Debug: debug, + Verbose: verbose, + BindIP: bindIP, + BindPort: bindPort, + PublicIPv4: publicIPv4, + PublicIPv4Port: PublicIPv4Port, + PublicIPv6: publicIPv6, + PublicIPv6Port: publicIPv6Port, + TimeoutRead: timeoutRead, + TimeoutWrite: timeoutWrite, + Secret: secretBytes, + } + + return conf, nil +} diff --git a/config/global_ips.go b/config/global_ips.go new file mode 100644 index 0000000..965a9cc --- /dev/null +++ b/config/global_ips.go @@ -0,0 +1,38 @@ +package config + +import ( + "io/ioutil" + "net" + "net/http" + "strings" + + "github.com/juju/errors" +) + +func getGlobalIPv4() (net.IP, error) { + return fetchIP("https://v4.ifconfig.co/ip") +} + +func getGlobalIPv6() (net.IP, error) { + return fetchIP("https://v6.ifconfig.co/ip") +} + +func fetchIP(url string) (net.IP, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respData, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + ip := net.ParseIP(strings.TrimSpace(string(respData))) + if ip == nil { + return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", resp) + } + + return ip, nil +} diff --git a/config/urls.go b/config/urls.go new file mode 100644 index 0000000..fa30ff3 --- /dev/null +++ b/config/urls.go @@ -0,0 +1,59 @@ +package config + +import ( + "encoding/hex" + "net" + "net/url" + "strconv" +) + +func getURLs(addr net.IP, port uint16, secret []byte) (urls URLs) { + values := url.Values{} + values.Set("server", addr.String()) + values.Set("port", strconv.Itoa(int(port))) + values.Set("secret", hex.EncodeToString(secret)) + + urls.TG = makeTGURL(values) + urls.TMe = makeTGURL(values) + urls.TGQRCode = makeQRCodeURL(urls.TG) + urls.TMeQRCode = makeQRCodeURL(urls.TG) + + return +} + +func makeTGURL(values url.Values) string { + tgURL := url.URL{ + Scheme: "tg", + Host: "proxy", + RawQuery: values.Encode(), + } + + return tgURL.String() +} + +func makeTMeURL(values url.Values) string { + tMeURL := url.URL{ + Scheme: "https", + Host: "t.me", + Path: "proxy", + RawQuery: values.Encode(), + } + + return tMeURL.String() +} + +func makeQRCodeURL(data string) string { + QRURL := url.URL{ + Scheme: "https", + Host: "api.qrserver.com", + Path: "v1/create-qr-code", + } + + values := url.Values{} + values.Set("qzone", "4") + values.Set("format", "svg") + values.Set("data", data) + QRURL.RawQuery = values.Encode() + + return QRURL.String() +} diff --git a/main.go b/main.go index c18b218..6ba0cb6 100644 --- a/main.go +++ b/main.go @@ -3,18 +3,16 @@ package main //go:generate scripts/generate_version.sh import ( - "encoding/hex" "encoding/json" "io" - "io/ioutil" - "net/http" "os" - "strings" - "github.com/9seconds/mtg/proxy" "go.uber.org/zap" "go.uber.org/zap/zapcore" kingpin "gopkg.in/alecthomas/kingpin.v2" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/proxy" ) var ( @@ -28,8 +26,9 @@ var ( Short('v'). Envar("MTG_VERBOSE"). Bool() + bindIP = app.Flag("bind-ip", "Which IP to bind to."). - Short('i'). + Short('b'). Envar("MTG_IP"). Default("127.0.0.1"). IP() @@ -38,11 +37,23 @@ var ( Envar("MTG_PORT"). Default("3128"). Uint16() - portToShow = app.Flag("show-bind-port", - "Which port to show in URL. Default is the value of bind-port"). - Short('a'). - Envar("MTG_SHOW_PORT"). - Uint16() + + publicIPv4 = app.Flag("public-ipv4", "Which IPv4 address is public."). + Short('4'). + Envar("MTG_IPV4"). + IP() + publicIPv4Port = app.Flag("public-ipv4-port", "Which IPv4 port is public. Default is 'bind-port' value."). + Envar("MTG_IPV4_PORT"). + Uint16() + + publicIPv6 = app.Flag("public-ipv6", "Which IPv6 address is public."). + Short('6'). + Envar("MTG_IPV6"). + IP() + publicIPv6Port = app.Flag("public-ipv6-port", "Which IPv6 port is public. Default is 'bind-port' value."). + Envar("MTG_IPV6_PORT"). + Uint16() + statsIP = app.Flag("stats-ip", "Which IP bind stats server to"). Short('t'). Envar("MTG_STATS_IP"). @@ -53,6 +64,7 @@ var ( Envar("MTG_STATS_PORT"). Default("3129"). Uint16() + readTimeout = app.Flag("read-timeout", "Socket read timeout."). Short('r'). Envar("MTG_READ_TIMEOUT"). @@ -63,15 +75,6 @@ var ( Envar("MTG_WRITE_TIMEOUT"). Default("30s"). Duration() - serverName = app.Flag("server-name", - "Which server name to use. Default is IP address resolved by ipify."). - Short('s'). - Envar("MTG_SERVER"). - String() - preferIPv6 = app.Flag("prefer-ipv6", "Use IPv6"). - Short('6'). - Envar("MTG_USE_IPV6"). - Bool() secret = app.Arg("secret", "Secret of this proxy.").Required().String() ) @@ -80,33 +83,22 @@ func main() { app.Version(version) kingpin.MustParse(app.Parse(os.Args[1:])) - secretBytes, err := hex.DecodeString(*secret) + conf, err := config.NewConfig(*debug, *verbose, + *bindIP, *bindPort, + *publicIPv4, *publicIPv4Port, + *publicIPv6, *publicIPv6Port, + *statsIP, *statsPort, + *readTimeout, *writeTimeout, + *secret, + ) if err != nil { - usage("Secret has to be hexadecimal string.") - } - - if *portToShow == 0 { - *portToShow = *bindPort - } - - if *serverName == "" { - resp, err := http.Get("https://api.ipify.org") - if err != nil || resp.StatusCode != http.StatusOK { - usage("Cannot get local IP address.") - } - myIPBytes, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() // nolint: errcheck - - if err != nil { - usage("Cannot get local IP address.") - } - *serverName = strings.TrimSpace(string(myIPBytes)) + usage(err.Error()) } atom := zap.NewAtomicLevel() - if *debug { + if conf.Debug { atom.SetLevel(zapcore.DebugLevel) - } else if *verbose { + } else if conf.Verbose { atom.SetLevel(zapcore.InfoLevel) } else { atom.SetLevel(zapcore.ErrorLevel) @@ -118,12 +110,12 @@ func main() { atom, )).Sugar() - stat := proxy.NewStats(*serverName, *portToShow, *secret) - go stat.Serve(*statsIP, *statsPort) - printURLs(stat.URLs) + stat := proxy.NewStats(conf) + go stat.Serve() + + srv := proxy.NewServer(conf, logger, stat) + printURLs(conf.GetURLs()) - srv := proxy.NewServer(*bindIP, int(*bindPort), secretBytes, logger, - *readTimeout, *writeTimeout, *preferIPv6, stat) if err := srv.Serve(); err != nil { logger.Fatal(err.Error()) } diff --git a/proxy/server.go b/proxy/server.go index 26fcfec..b806f90 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -4,34 +4,27 @@ import ( "context" "io" "net" - "strconv" "sync" - "time" - "github.com/9seconds/mtg/obfuscated2" - "github.com/9seconds/mtg/wrappers" "github.com/juju/errors" uuid "github.com/satori/go.uuid" "go.uber.org/zap" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/wrappers" ) // Server is an insgtance of MTPROTO proxy. type Server struct { - ip net.IP - port int - secret []byte - logger *zap.SugaredLogger - ctx context.Context - readTimeout time.Duration - writeTimeout time.Duration - stats *Stats - ipv6 bool + conf *config.Config + logger *zap.SugaredLogger + stats *Stats } // Serve does MTPROTO proxying. func (s *Server) Serve() error { - addr := net.JoinHostPort(s.ip.String(), strconv.Itoa(s.port)) - lsock, err := net.Listen("tcp", addr) + lsock, err := net.Listen("tcp", s.conf.BindAddr()) if err != nil { return errors.Annotate(err, "Cannot create listen socket") } @@ -57,10 +50,9 @@ func (s *Server) accept(conn net.Conn) { s.stats.newConnection() ctx, cancel := context.WithCancel(context.Background()) - socketID := s.makeSocketID() + socketID := uuid.NewV4().String() s.logger.Debugw("Client connected", - "secret", s.secret, "addr", conn.RemoteAddr().String(), "socketid", socketID, ) @@ -68,7 +60,6 @@ func (s *Server) accept(conn net.Conn) { clientConn, dc, err := s.getClientStream(ctx, cancel, conn, socketID) if err != nil { s.logger.Warnw("Cannot initialize client connection", - "secret", s.secret, "addr", conn.RemoteAddr().String(), "socketid", socketID, "error", err, @@ -101,25 +92,20 @@ func (s *Server) accept(conn net.Conn) { wait.Wait() s.logger.Debugw("Client disconnected", - "secret", s.secret, "addr", conn.RemoteAddr().String(), "socketid", socketID, ) } -func (s *Server) makeSocketID() string { - return uuid.NewV4().String() -} - func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (io.ReadWriteCloser, int16, error) { - wConn := wrappers.NewTimeoutRWC(conn, s.readTimeout, s.writeTimeout) + wConn := wrappers.NewTimeoutRWC(conn, s.conf.TimeoutRead, s.conf.TimeoutWrite) wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) frame, err := obfuscated2.ExtractFrame(wConn) if err != nil { return nil, 0, errors.Annotate(err, "Cannot create client stream") } - obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(s.secret, frame) + obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(s.conf.Secret, frame) if err != nil { return nil, 0, errors.Annotate(err, "Cannot create client stream") } @@ -132,11 +118,11 @@ func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, } func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) { - socket, err := dialToTelegram(s.ipv6, dc, s.readTimeout) + socket, err := dialToTelegram(dc, s.conf.TimeoutRead) if err != nil { return nil, errors.Annotate(err, "Cannot dial") } - wConn := wrappers.NewTimeoutRWC(socket, s.readTimeout, s.writeTimeout) + wConn := wrappers.NewTimeoutRWC(socket, s.conf.TimeoutRead, s.conf.TimeoutWrite) wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() @@ -152,17 +138,10 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun } // NewServer creates new instance of MTPROTO proxy. -func NewServer(ip net.IP, port int, secret []byte, logger *zap.SugaredLogger, - readTimeout, writeTimeout time.Duration, ipv6 bool, stat *Stats) *Server { +func NewServer(conf *config.Config, logger *zap.SugaredLogger, stat *Stats) *Server { return &Server{ - ip: ip, - port: port, - secret: secret, - ctx: context.Background(), - logger: logger, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - stats: stat, - ipv6: ipv6, + conf: conf, + logger: logger, + stats: stat, } } diff --git a/proxy/stats.go b/proxy/stats.go index 0c642bc..9469c2f 100644 --- a/proxy/stats.go +++ b/proxy/stats.go @@ -2,13 +2,12 @@ package proxy import ( "encoding/json" - "fmt" - "net" "net/http" - "net/url" "strconv" "sync/atomic" "time" + + "github.com/9seconds/mtg/config" ) type statsUptime time.Time @@ -26,13 +25,10 @@ type Stats struct { Incoming uint64 `json:"incoming"` Outgoing uint64 `json:"outgoing"` } `json:"traffic"` - URLs struct { - TG string `json:"tg_url"` - TMe string `json:"tme_url"` - TGQRCode string `json:"tg_qrcode"` - TMeQRCode string `json:"tme_qrcode"` - } `json:"urls"` - Uptime statsUptime `json:"uptime"` + URLs config.IPURLs `json:"urls"` + Uptime statsUptime `json:"uptime"` + + conf *config.Config } func (s *Stats) newConnection() { @@ -53,7 +49,7 @@ func (s *Stats) addOutgoingTraffic(n int) { } // Serve runs statistics HTTP server. -func (s *Stats) Serve(host fmt.Stringer, port uint16) { +func (s *Stats) Serve() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -63,65 +59,16 @@ func (s *Stats) Serve(host fmt.Stringer, port uint16) { encoder.Encode(s) // nolint: errcheck, gas }) - addr := net.JoinHostPort(host.String(), strconv.Itoa(int(port))) - http.ListenAndServe(addr, nil) // nolint: errcheck, gas + http.ListenAndServe(s.conf.StatAddr(), nil) // nolint: errcheck, gas } // NewStats returns new instance of statistics datastructure. -func NewStats(serverName string, port uint16, secret string) *Stats { - urlQuery := makeURLQuery(serverName, port, secret) - - stat := &Stats{Uptime: statsUptime(time.Now())} - stat.URLs.TG = makeTGURL(urlQuery) - stat.URLs.TMe = makeTMeURL(urlQuery) - stat.URLs.TGQRCode = makeQRCodeURL(stat.URLs.TG) - stat.URLs.TMeQRCode = makeQRCodeURL(stat.URLs.TMe) +func NewStats(conf *config.Config) *Stats { + stat := &Stats{ + Uptime: statsUptime(time.Now()), + conf: conf, + } + stat.URLs = conf.GetURLs() return stat } - -func makeURLQuery(serverName string, port uint16, secret string) url.Values { - values := url.Values{} - values.Set("server", serverName) - values.Set("port", strconv.Itoa(int(port))) - values.Set("secret", secret) - - return values -} - -func makeTGURL(values url.Values) string { - tgURL := url.URL{ - Scheme: "tg", - Host: "proxy", - RawQuery: values.Encode(), - } - - return tgURL.String() -} - -func makeTMeURL(values url.Values) string { - tMeURL := url.URL{ - Scheme: "https", - Host: "t.me", - Path: "proxy", - RawQuery: values.Encode(), - } - - return tMeURL.String() -} - -func makeQRCodeURL(data string) string { - QRURL := url.URL{ - Scheme: "https", - Host: "api.qrserver.com", - Path: "v1/create-qr-code", - } - - values := url.Values{} - values.Set("qzone", "4") - values.Set("format", "svg") - values.Set("data", data) - QRURL.RawQuery = values.Encode() - - return QRURL.String() -} diff --git a/proxy/telegram.go b/proxy/telegram.go index ad3b1d6..be514f4 100644 --- a/proxy/telegram.go +++ b/proxy/telegram.go @@ -37,12 +37,12 @@ const telegramPort = "443" const telegramKeepAlive = 30 * time.Second -func dialToTelegram(ipv6 bool, dcIdx int16, timeout time.Duration) (net.Conn, error) { +func dialToTelegram(dcIdx int16, timeout time.Duration) (net.Conn, error) { if dcIdx < 0 || dcIdx >= 5 { return nil, errors.New("Incorrect DC IDX") } - conn, err := doDial(ipv6, dcIdx, timeout) + conn, err := doDial(dcIdx, timeout) if err != nil { return nil, errors.Annotate(err, "Cannot dial") } @@ -57,14 +57,12 @@ func dialToTelegram(ipv6 bool, dcIdx int16, timeout time.Duration) (net.Conn, er return conn, nil } -func doDial(ipv6 bool, dcIdx int16, timeout time.Duration) (*net.TCPConn, error) { +func doDial(dcIdx int16, timeout time.Duration) (*net.TCPConn, error) { dialer := net.Dialer{Timeout: timeout} addr := TelegramAddresses[dcIdx] - if ipv6 { - if conn, err := dialer.Dial("tcp", addr.IPv6()); err == nil { - return conn.(*net.TCPConn), nil - } + if conn, err := dialer.Dial("tcp", addr.IPv6()); err == nil { + return conn.(*net.TCPConn), nil } conn, err := dialer.Dial("tcp", addr.IPv4()) From dd7a09594701bf67b83d047be50a90d86ce076a8 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 18 Jun 2018 08:39:38 +0300 Subject: [PATCH 08/16] Fix lint errors --- Makefile | 2 +- config/config.go | 33 +++++++++++++++++++++++---------- config/global_ips.go | 2 +- config/urls.go | 2 +- obfuscated2/obfuscated2_test.go | 9 ++++++--- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 871f9a0..ce6a1cc 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) IMAGE_NAME := mtg APP_NAME := $(IMAGE_NAME) -GOMETALINTER := gometalinter.v2 +GOMETALINTER := gometalinter VENDOR_FILES := $(shell find "$(ROOT_DIR)/vendor" 2>/dev/null || echo -n "vendor") CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,windows,darwin,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}") diff --git a/config/config.go b/config/config.go index 793d4d0..39f48c5 100644 --- a/config/config.go +++ b/config/config.go @@ -10,26 +10,28 @@ import ( "github.com/juju/errors" ) +// Config represents common configuration of mtg. type Config struct { - Debug bool - Verbose bool - BindIP net.IP - BindPort uint16 + Debug bool + Verbose bool - PublicIPv4 net.IP + BindPort uint16 PublicIPv4Port uint16 - PublicIPv6 net.IP PublicIPv6Port uint16 - - StatsIP net.IP - StatsPort uint16 + StatsPort uint16 TimeoutRead time.Duration TimeoutWrite time.Duration + BindIP net.IP + PublicIPv4 net.IP + PublicIPv6 net.IP + StatsIP net.IP + Secret []byte } +// URLs contains links to the proxy (tg://, t.me) and their QR codes. type URLs struct { TG string `json:"tg_url"` TMe string `json:"tme_url"` @@ -37,27 +39,33 @@ type URLs struct { TMeQRCode string `json:"tme_qrcode"` } +// IPURLs contains links to both ipv4 and ipv6 of the proxy. type IPURLs struct { IPv4 URLs `json:"ipv4"` IPv6 URLs `json:"ipv6"` } +// BindAddr returns connection for this server to bind to. func (c *Config) BindAddr() string { return getAddr(c.BindIP, c.BindPort) } +// IPv4Addr returns connection string to ipv6 for mtproto proxy. func (c *Config) IPv4Addr() string { return getAddr(c.PublicIPv4, c.PublicIPv4Port) } +// IPv6Addr returns connection string to ipv6 for mtproto proxy. func (c *Config) IPv6Addr() string { return getAddr(c.PublicIPv6, c.PublicIPv6Port) } +// StatAddr returns connection string to the stats API. func (c *Config) StatAddr() string { return getAddr(c.StatsIP, c.StatsPort) } +// GetURLs returns configured IPURLs instance with links to this server. func (c *Config) GetURLs() IPURLs { return IPURLs{ IPv4: getURLs(c.PublicIPv4, c.PublicIPv4Port, c.Secret), @@ -69,7 +77,10 @@ func getAddr(host fmt.Stringer, port uint16) string { return net.JoinHostPort(host.String(), strconv.Itoa(int(port))) } -func NewConfig(debug, verbose bool, +// NewConfig returns new configuration. If required, it manages and +// fetches data from external sources. Parameters passed to this +// function, should come from command line arguments. +func NewConfig(debug, verbose bool, // nolint: gocyclo bindIP net.IP, bindPort uint16, publicIPv4 net.IP, PublicIPv4Port uint16, publicIPv6 net.IP, publicIPv6Port uint16, @@ -120,6 +131,8 @@ func NewConfig(debug, verbose bool, PublicIPv4Port: PublicIPv4Port, PublicIPv6: publicIPv6, PublicIPv6Port: publicIPv6Port, + StatsIP: statsIP, + StatsPort: statsPort, TimeoutRead: timeoutRead, TimeoutWrite: timeoutWrite, Secret: secretBytes, diff --git a/config/global_ips.go b/config/global_ips.go index 965a9cc..5a73747 100644 --- a/config/global_ips.go +++ b/config/global_ips.go @@ -22,7 +22,7 @@ func fetchIP(url string) (net.IP, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer resp.Body.Close() // nolint: errcheck respData, err := ioutil.ReadAll(resp.Body) if err != nil { diff --git a/config/urls.go b/config/urls.go index fa30ff3..7aada07 100644 --- a/config/urls.go +++ b/config/urls.go @@ -14,7 +14,7 @@ func getURLs(addr net.IP, port uint16, secret []byte) (urls URLs) { values.Set("secret", hex.EncodeToString(secret)) urls.TG = makeTGURL(values) - urls.TMe = makeTGURL(values) + urls.TMe = makeTMeURL(values) urls.TGQRCode = makeQRCodeURL(urls.TG) urls.TMeQRCode = makeQRCodeURL(urls.TG) diff --git a/obfuscated2/obfuscated2_test.go b/obfuscated2/obfuscated2_test.go index 9e660e0..49255df 100644 --- a/obfuscated2/obfuscated2_test.go +++ b/obfuscated2/obfuscated2_test.go @@ -25,7 +25,8 @@ func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) { data := []byte{1, 2, 3} encrypted := make([]byte, 3) encryptor.XORKeyStream(encrypted, data) - decrypted := obfs2.Decrypt(encrypted) + decrypted := make([]byte, 3) + obfs2.Decryptor.XORKeyStream(decrypted, encrypted) assert.Equal(t, data, decrypted) } @@ -67,10 +68,12 @@ func TestObfs2Full(t *testing.T) { tgEncryptedMessage := make([]byte, len(message)) tgEncryptor.XORKeyStream(tgEncryptedMessage, message) - tgEncDecryptedMessage := tgObfs.Decrypt(tgEncryptedMessage) + tgEncDecryptedMessage := make([]byte, len(tgEncryptedMessage)) + tgObfs.Decryptor.XORKeyStream(tgEncDecryptedMessage, tgEncryptedMessage) assert.Equal(t, message, tgEncDecryptedMessage) - clientEncryptedMessage := clientObfs.Encrypt(tgEncDecryptedMessage) + clientEncryptedMessage := make([]byte, len(tgEncDecryptedMessage)) + clientObfs.Encryptor.XORKeyStream(clientEncryptedMessage, tgEncDecryptedMessage) finalMessage := make([]byte, len(clientEncryptedMessage)) clientDecryptor.XORKeyStream(finalMessage, clientEncryptedMessage) From 5a169b50269ca323157905e6ec91b3c05c2ad20e Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 18 Jun 2018 19:03:27 +0300 Subject: [PATCH 09/16] Move telegram to separate module --- obfuscated2/frame.go | 8 +---- proxy/server.go | 27 ++++++++-------- proxy/telegram.go | 73 -------------------------------------------- telegram/dialer.go | 53 ++++++++++++++++++++++++++++++++ telegram/direct.go | 59 +++++++++++++++++++++++++++++++++++ telegram/telegram.go | 38 +++++++++++++++++++++++ 6 files changed, 165 insertions(+), 93 deletions(-) delete mode 100644 proxy/telegram.go create mode 100644 telegram/dialer.go create mode 100644 telegram/direct.go create mode 100644 telegram/telegram.go diff --git a/obfuscated2/frame.go b/obfuscated2/frame.go index 296e0e4..9bf2bc1 100644 --- a/obfuscated2/frame.go +++ b/obfuscated2/frame.go @@ -58,13 +58,7 @@ func (f Frame) DC() (n int16) { n = 1 } - if n < 0 { - n = -n - } else if n == 0 { - n = 1 - } - - return n - 1 + return } // Valid checks that *decrypted* frame is valid. Only magic bytes are checked. diff --git a/proxy/server.go b/proxy/server.go index b806f90..e6e6a6e 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -12,6 +12,7 @@ import ( "github.com/9seconds/mtg/config" "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/telegram" "github.com/9seconds/mtg/wrappers" ) @@ -20,6 +21,7 @@ type Server struct { conf *config.Config logger *zap.SugaredLogger stats *Stats + tg telegram.Telegram } // Serve does MTPROTO proxying. @@ -118,23 +120,21 @@ func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, } func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) { - socket, err := dialToTelegram(dc, s.conf.TimeoutRead) + conn, err := s.tg.Dial(dc) if err != nil { - return nil, errors.Annotate(err, "Cannot dial") - } - wConn := wrappers.NewTimeoutRWC(socket, s.conf.TimeoutRead, s.conf.TimeoutWrite) - wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) - - obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() - if n, err := socket.Write(frame); err != nil || n != len(frame) { - return nil, errors.Annotate(err, "Cannot write hadnshake frame") + return nil, errors.Annotate(err, "Cannot connect to Telegram") } - wConn = wrappers.NewLogRWC(wConn, s.logger, socketID, "telegram") - wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) - wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) + conn = wrappers.NewTrafficRWC(conn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) + conn, err = s.tg.Init(conn) + if err != nil { + return nil, errors.Annotate(err, "Cannot handshake Telegram") + } - return wConn, nil + conn = wrappers.NewLogRWC(conn, s.logger, socketID, "telegram") + conn = wrappers.NewCtxRWC(ctx, cancel, conn) + + return conn, nil } // NewServer creates new instance of MTPROTO proxy. @@ -143,5 +143,6 @@ func NewServer(conf *config.Config, logger *zap.SugaredLogger, stat *Stats) *Ser conf: conf, logger: logger, stats: stat, + tg: telegram.NewDirectTelegram(conf), } } diff --git a/proxy/telegram.go b/proxy/telegram.go deleted file mode 100644 index be514f4..0000000 --- a/proxy/telegram.go +++ /dev/null @@ -1,73 +0,0 @@ -package proxy - -import ( - "net" - "time" - - "github.com/juju/errors" -) - -// TelegramAddress presents a pair of v4 and v6 addresses. This pairization -// is required because we want to use DC indexes. -type TelegramAddress struct { - v4 string - v6 string -} - -// IPv4 returns v4 address. -func (t *TelegramAddress) IPv4() string { - return net.JoinHostPort(t.v4, telegramPort) -} - -// IPv6 returns v4 address. -func (t *TelegramAddress) IPv6() string { - return net.JoinHostPort(t.v6, telegramPort) -} - -// TelegramAddresses is a list of all known Telegram addresses for DC indexes. -var TelegramAddresses = []TelegramAddress{ - TelegramAddress{v4: "149.154.175.50", v6: "2001:b28:f23d:f001::a"}, - TelegramAddress{v4: "149.154.167.51", v6: "2001:67c:04e8:f002::a"}, - TelegramAddress{v4: "149.154.175.100", v6: "2001:b28:f23d:f003::a"}, - TelegramAddress{v4: "149.154.167.91", v6: "2001:67c:04e8:f004::a"}, - TelegramAddress{v4: "149.154.171.5", v6: "2001:b28:f23f:f005::a"}, -} - -const telegramPort = "443" - -const telegramKeepAlive = 30 * time.Second - -func dialToTelegram(dcIdx int16, timeout time.Duration) (net.Conn, error) { - if dcIdx < 0 || dcIdx >= 5 { - return nil, errors.New("Incorrect DC IDX") - } - - conn, err := doDial(dcIdx, timeout) - if err != nil { - return nil, errors.Annotate(err, "Cannot dial") - } - - if err := conn.SetKeepAlive(true); err != nil { - return nil, errors.Annotate(err, "Cannot establish keepalive connection") - } - if err := conn.SetKeepAlivePeriod(telegramKeepAlive); err != nil { - return nil, errors.Annotate(err, "Cannot set keepalive timeout") - } - - return conn, nil -} - -func doDial(dcIdx int16, timeout time.Duration) (*net.TCPConn, error) { - dialer := net.Dialer{Timeout: timeout} - addr := TelegramAddresses[dcIdx] - - if conn, err := dialer.Dial("tcp", addr.IPv6()); err == nil { - return conn.(*net.TCPConn), nil - } - - conn, err := dialer.Dial("tcp", addr.IPv4()) - if err == nil { - return conn.(*net.TCPConn), nil - } - return nil, err -} diff --git a/telegram/dialer.go b/telegram/dialer.go new file mode 100644 index 0000000..267edc0 --- /dev/null +++ b/telegram/dialer.go @@ -0,0 +1,53 @@ +package telegram + +import ( + "io" + "net" + "time" + + "github.com/juju/errors" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/wrappers" +) + +const telegramKeepAlive = 30 * time.Second + +type tgDialer struct { + net.Dialer + + conf *config.Config +} + +func (t *tgDialer) dial(addr string) (net.Conn, error) { + connRaw, err := t.Dialer.Dial("tcp", addr) + if err != nil { + return nil, errors.Annotate(err, "Cannot connect to Telegram") + } + conn := connRaw.(*net.TCPConn) + + if err = conn.SetKeepAlive(true); err != nil { + return nil, errors.Annotate(err, "Cannot establish keepalive connection") + } + if err = conn.SetKeepAlivePeriod(telegramKeepAlive); err != nil { + return nil, errors.Annotate(err, "Cannot set keepalive timeout") + } + + return conn, nil +} + +func (t *tgDialer) dialRWC(addr string) (io.ReadWriteCloser, error) { + conn, err := t.dial(addr) + if err != nil { + return nil, err + } + + return wrappers.NewTimeoutRWC(conn, t.conf.TimeoutRead, t.conf.TimeoutWrite), nil +} + +func newDialer(conf *config.Config) *tgDialer { + return &tgDialer{ + Dialer: net.Dialer{Timeout: conf.TimeoutRead}, + conf: conf, + } +} diff --git a/telegram/direct.go b/telegram/direct.go new file mode 100644 index 0000000..6ac6c51 --- /dev/null +++ b/telegram/direct.go @@ -0,0 +1,59 @@ +package telegram + +import ( + "io" + + "github.com/juju/errors" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/wrappers" +) + +var ( + directV4Addresses = map[int16][]string{ + 0: []string{"149.154.175.50:443"}, + 1: []string{"149.154.167.51:443"}, + 2: []string{"149.154.175.100:443"}, + 3: []string{"149.154.167.91:443"}, + 4: []string{"149.154.171.5:443"}, + } + directV6Addresses = map[int16][]string{ + 0: []string{"[2001:b28:f23d:f001::a]:443"}, + 1: []string{"[2001:67c:04e8:f002::a]:443"}, + 2: []string{"[2001:b28:f23d:f003::a]:443"}, + 3: []string{"[2001:67c:04e8:f004::a]:443"}, + 4: []string{"[2001:b28:f23f:f005::a]:443"}, + } +) + +type directTelegram struct { + baseTelegram +} + +func (t *directTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) { + if dcIdx < 0 { + dcIdx = -dcIdx + } else if dcIdx == 0 { + dcIdx = 1 + } + + return t.baseTelegram.Dial(dcIdx - 1) +} + +func (t *directTelegram) Init(conn io.ReadWriteCloser) (io.ReadWriteCloser, error) { + obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame() + if n, err := conn.Write(frame); err != nil || n != len(frame) { + return nil, errors.Annotate(err, "Cannot write hadnshake frame") + } + + return wrappers.NewStreamCipherRWC(conn, obfs2.Encryptor, obfs2.Decryptor), nil +} + +func NewDirectTelegram(conf *config.Config) Telegram { + return &directTelegram{baseTelegram{ + dialer: newDialer(conf), + v4Addresses: directV4Addresses, + v6Addresses: directV6Addresses, + }} +} diff --git a/telegram/telegram.go b/telegram/telegram.go new file mode 100644 index 0000000..00a9268 --- /dev/null +++ b/telegram/telegram.go @@ -0,0 +1,38 @@ +package telegram + +import ( + "io" + "math/rand" + + "github.com/juju/errors" +) + +type Telegram interface { + Dial(int16) (io.ReadWriteCloser, error) + Init(io.ReadWriteCloser) (io.ReadWriteCloser, error) +} + +type baseTelegram struct { + dialer *tgDialer + + v4Addresses map[int16][]string + v6Addresses map[int16][]string +} + +func (b *baseTelegram) Dial(dcIdx int16) (io.ReadWriteCloser, error) { + addrs := make([]string, 2) + if addr, ok := b.v6Addresses[dcIdx]; ok && len(addr) > 0 { + addrs = append(addrs, addr[rand.Intn(len(addr))]) + } + if addr, ok := b.v4Addresses[dcIdx]; ok && len(addr) > 0 { + addrs = append(addrs, addr[rand.Intn(len(addr))]) + } + + for _, addr := range addrs { + if conn, err := b.dialer.dialRWC(addr); err == nil { + return conn, err + } + } + + return nil, errors.New("Cannot connect to Telegram") +} From daca606058644d97e3753200259139356c255aa6 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 09:21:48 +0300 Subject: [PATCH 10/16] Add client package --- client/client.go | 10 +++++++++ client/direct.go | 29 +++++++++++++++++++++++++ obfuscated2/frame_test.go | 2 +- obfuscated2/obfuscated2.go | 4 +--- proxy/server.go | 43 +++++++++++++++++--------------------- 5 files changed, 60 insertions(+), 28 deletions(-) create mode 100644 client/client.go create mode 100644 client/direct.go diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..3c6e29e --- /dev/null +++ b/client/client.go @@ -0,0 +1,10 @@ +package client + +import ( + "io" + "net" + + "github.com/9seconds/mtg/config" +) + +type Init func(net.Conn, *config.Config) (int16, io.ReadWriteCloser, error) diff --git a/client/direct.go b/client/direct.go new file mode 100644 index 0000000..ce56646 --- /dev/null +++ b/client/direct.go @@ -0,0 +1,29 @@ +package client + +import ( + "io" + "net" + + "github.com/juju/errors" + + "github.com/9seconds/mtg/config" + "github.com/9seconds/mtg/obfuscated2" + "github.com/9seconds/mtg/wrappers" +) + +func DirectInit(conn net.Conn, conf *config.Config) (int16, io.ReadWriteCloser, error) { + socket := wrappers.NewTimeoutRWC(conn, conf.TimeoutRead, conf.TimeoutWrite) + frame, err := obfuscated2.ExtractFrame(socket) + if err != nil { + return 0, nil, errors.Annotate(err, "Cannot extract frame") + } + + obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame) + if err != nil { + return 0, nil, errors.Annotate(err, "Cannot parse obfuscated frame") + } + + socket = wrappers.NewStreamCipherRWC(socket, obfs2.Encryptor, obfs2.Decryptor) + + return dc, socket, nil +} diff --git a/obfuscated2/frame_test.go b/obfuscated2/frame_test.go index 9b2e475..b772e3c 100644 --- a/obfuscated2/frame_test.go +++ b/obfuscated2/frame_test.go @@ -34,7 +34,7 @@ func TestFrameMagic(t *testing.T) { } func TestFrameDC(t *testing.T) { - assert.Equal(t, int16(770), makeFrame().DC()) + assert.Equal(t, int16(771), makeFrame().DC()) } func TestFrameValid(t *testing.T) { diff --git a/obfuscated2/obfuscated2.go b/obfuscated2/obfuscated2.go index d75759a..9c70c5d 100644 --- a/obfuscated2/obfuscated2.go +++ b/obfuscated2/obfuscated2.go @@ -19,9 +19,7 @@ type Obfuscated2 struct { // details: http://telegra.ph/telegram-blocks-wtf-05-26 // // Beware, link above is in russian. -func ParseObfuscated2ClientFrame(secret, data []byte) (*Obfuscated2, int16, error) { - frame := Frame(data) - +func ParseObfuscated2ClientFrame(secret []byte, frame Frame) (*Obfuscated2, int16, error) { decHasher := sha256.New() decHasher.Write(frame.Key()) // nolint: errcheck decHasher.Write(secret) // nolint: errcheck diff --git a/proxy/server.go b/proxy/server.go index e6e6a6e..bd5564d 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -10,18 +10,19 @@ import ( uuid "github.com/satori/go.uuid" "go.uber.org/zap" + "github.com/9seconds/mtg/client" "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/obfuscated2" "github.com/9seconds/mtg/telegram" "github.com/9seconds/mtg/wrappers" ) // Server is an insgtance of MTPROTO proxy. type Server struct { - conf *config.Config - logger *zap.SugaredLogger - stats *Stats - tg telegram.Telegram + conf *config.Config + logger *zap.SugaredLogger + stats *Stats + tg telegram.Telegram + clientInit client.Init } // Serve does MTPROTO proxying. @@ -59,7 +60,7 @@ func (s *Server) accept(conn net.Conn) { "socketid", socketID, ) - clientConn, dc, err := s.getClientStream(ctx, cancel, conn, socketID) + dc, clientConn, err := s.getClientStream(ctx, cancel, conn, socketID) if err != nil { s.logger.Warnw("Cannot initialize client connection", "addr", conn.RemoteAddr().String(), @@ -99,24 +100,17 @@ func (s *Server) accept(conn net.Conn) { ) } -func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (io.ReadWriteCloser, int16, error) { - wConn := wrappers.NewTimeoutRWC(conn, s.conf.TimeoutRead, s.conf.TimeoutWrite) - wConn = wrappers.NewTrafficRWC(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) - frame, err := obfuscated2.ExtractFrame(wConn) +func (s *Server) getClientStream(ctx context.Context, cancel context.CancelFunc, conn net.Conn, socketID string) (int16, io.ReadWriteCloser, error) { + dc, socket, err := s.clientInit(conn, s.conf) if err != nil { - return nil, 0, errors.Annotate(err, "Cannot create client stream") + return 0, nil, errors.Annotate(err, "Cannot init client connection") } - obfs2, dc, err := obfuscated2.ParseObfuscated2ClientFrame(s.conf.Secret, frame) - if err != nil { - return nil, 0, errors.Annotate(err, "Cannot create client stream") - } + socket = wrappers.NewTrafficRWC(socket, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) + socket = wrappers.NewLogRWC(socket, s.logger, socketID, "client") + socket = wrappers.NewCtxRWC(ctx, cancel, socket) - wConn = wrappers.NewLogRWC(wConn, s.logger, socketID, "client") - wConn = wrappers.NewStreamCipherRWC(wConn, obfs2.Encryptor, obfs2.Decryptor) - wConn = wrappers.NewCtxRWC(ctx, cancel, wConn) - - return wConn, dc, nil + return dc, socket, nil } func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFunc, dc int16, socketID string) (io.ReadWriteCloser, error) { @@ -140,9 +134,10 @@ func (s *Server) getTelegramStream(ctx context.Context, cancel context.CancelFun // NewServer creates new instance of MTPROTO proxy. func NewServer(conf *config.Config, logger *zap.SugaredLogger, stat *Stats) *Server { return &Server{ - conf: conf, - logger: logger, - stats: stat, - tg: telegram.NewDirectTelegram(conf), + conf: conf, + logger: logger, + stats: stat, + tg: telegram.NewDirectTelegram(conf), + clientInit: client.DirectInit, } } From b0292e4ed97161d88e12bedcb85054516d516f30 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 10:22:31 +0300 Subject: [PATCH 11/16] Fix linters --- client/client.go | 1 + client/direct.go | 1 + telegram/direct.go | 2 + telegram/telegram.go | 3 ++ wrappers/blockcipherrwc.go | 73 ------------------------------------- wrappers/ctxrwc.go | 2 + wrappers/logrwc.go | 1 + wrappers/streamcipherrwc.go | 4 ++ wrappers/timeoutrwc.go | 2 + wrappers/trafficrwc.go | 1 + 10 files changed, 17 insertions(+), 73 deletions(-) delete mode 100644 wrappers/blockcipherrwc.go diff --git a/client/client.go b/client/client.go index 3c6e29e..54e1310 100644 --- a/client/client.go +++ b/client/client.go @@ -7,4 +7,5 @@ import ( "github.com/9seconds/mtg/config" ) +// Init has to initialize client connection based on given config. type Init func(net.Conn, *config.Config) (int16, io.ReadWriteCloser, error) diff --git a/client/direct.go b/client/direct.go index ce56646..14776c4 100644 --- a/client/direct.go +++ b/client/direct.go @@ -11,6 +11,7 @@ import ( "github.com/9seconds/mtg/wrappers" ) +// DirectInit initializes client to access Telegram bypassing middleproxies. func DirectInit(conn net.Conn, conf *config.Config) (int16, io.ReadWriteCloser, error) { socket := wrappers.NewTimeoutRWC(conn, conf.TimeoutRead, conf.TimeoutWrite) frame, err := obfuscated2.ExtractFrame(socket) diff --git a/telegram/direct.go b/telegram/direct.go index 6ac6c51..f2437ea 100644 --- a/telegram/direct.go +++ b/telegram/direct.go @@ -50,6 +50,8 @@ func (t *directTelegram) Init(conn io.ReadWriteCloser) (io.ReadWriteCloser, erro return wrappers.NewStreamCipherRWC(conn, obfs2.Encryptor, obfs2.Decryptor), nil } +// NewDirectTelegram returns Telegram instance which connects directly +// to Telegram bypassing middleproxies. func NewDirectTelegram(conf *config.Config) Telegram { return &directTelegram{baseTelegram{ dialer: newDialer(conf), diff --git a/telegram/telegram.go b/telegram/telegram.go index 00a9268..c06f934 100644 --- a/telegram/telegram.go +++ b/telegram/telegram.go @@ -7,6 +7,9 @@ import ( "github.com/juju/errors" ) +// Telegram defines an interface to connect to Telegram. This +// encapsulates logic of working with middleproxies or direct +// connections. type Telegram interface { Dial(int16) (io.ReadWriteCloser, error) Init(io.ReadWriteCloser) (io.ReadWriteCloser, error) diff --git a/wrappers/blockcipherrwc.go b/wrappers/blockcipherrwc.go deleted file mode 100644 index fa72ecb..0000000 --- a/wrappers/blockcipherrwc.go +++ /dev/null @@ -1,73 +0,0 @@ -package wrappers - -import ( - "bytes" - "crypto/cipher" - "io" - - "github.com/juju/errors" -) - -type BlockCipherReadWriteCloser struct { - encryptor cipher.BlockMode - decryptor cipher.BlockMode - conn io.ReadWriteCloser - buf *bytes.Buffer -} - -func (c *BlockCipherReadWriteCloser) Read(p []byte) (n int, err error) { - blockSize := c.decryptor.BlockSize() - if len(p) < blockSize { - return 0, errors.New("Cannot read less than blocksize") - } - - n, err = c.conn.Read(p) - c.buf.Write(p[:n]) - - wantToRead := c.getFullBlocks(len(p), blockSize) - haveBlocks := c.getFullBlocks(c.buf.Len(), blockSize) - if haveBlocks < wantToRead { - wantToRead = haveBlocks - } - wantToRead *= blockSize - - data := c.buf.Bytes() - c.decryptor.CryptBlocks(p, data[:wantToRead]) - c.buf = bytes.NewBuffer(data[wantToRead:]) - - return wantToRead, err -} - -func (c *BlockCipherReadWriteCloser) Write(p []byte) (n int, err error) { - blockSize := c.encryptor.BlockSize() - if len(p)%blockSize != 0 { - return 0, errors.New("Write size should be compatible with block size") - } - - buf := make([]byte, len(p)) - c.encryptor.CryptBlocks(buf, p) - - return c.conn.Write(buf) -} - -func (c *BlockCipherReadWriteCloser) Close() error { - return c.conn.Close() -} - -func (c *BlockCipherReadWriteCloser) getFullBlocks(number, blockSize int) int { - blocks := number / blockSize - - if blocks > 0 && number%blockSize != 0 { - blocks-- - } - - return blocks -} - -func NewBlockCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.BlockMode) io.ReadWriteCloser { - return &BlockCipherReadWriteCloser{ - conn: conn, - encryptor: encryptor, - decryptor: decryptor, - } -} diff --git a/wrappers/ctxrwc.go b/wrappers/ctxrwc.go index 452d2e3..26f47be 100644 --- a/wrappers/ctxrwc.go +++ b/wrappers/ctxrwc.go @@ -48,6 +48,8 @@ func (c *CtxReadWriteCloser) Close() error { return c.conn.Close() } +// NewCtxRWC returns ReadWriteCloser which respects given context, +// cancellation etc. func NewCtxRWC(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser { return &CtxReadWriteCloser{ conn: conn, diff --git a/wrappers/logrwc.go b/wrappers/logrwc.go index 00ef597..355e239 100644 --- a/wrappers/logrwc.go +++ b/wrappers/logrwc.go @@ -36,6 +36,7 @@ func (l *LogReadWriteCloser) Close() error { return err } +// NewLogRWC wraps ReadWriteCloser with logger calls. func NewLogRWC(conn io.ReadWriteCloser, logger *zap.SugaredLogger, sockid string, name string) io.ReadWriteCloser { return &LogReadWriteCloser{ conn: conn, diff --git a/wrappers/streamcipherrwc.go b/wrappers/streamcipherrwc.go index 5d7d018..77243b7 100644 --- a/wrappers/streamcipherrwc.go +++ b/wrappers/streamcipherrwc.go @@ -5,6 +5,8 @@ import ( "io" ) +// StreamCipherReadWriteCloser is a ReadWriteCloser which ciphers +// incoming and outgoing data with givem cipher.Stream instances. type StreamCipherReadWriteCloser struct { encryptor cipher.Stream decryptor cipher.Stream @@ -41,6 +43,8 @@ func (c *StreamCipherReadWriteCloser) Close() error { return c.conn.Close() } +// NewStreamCipherRWC returns wrapper which transparently +// encrypts/decrypts traffic with obfuscated2 protocol. func NewStreamCipherRWC(conn io.ReadWriteCloser, encryptor, decryptor cipher.Stream) io.ReadWriteCloser { return &StreamCipherReadWriteCloser{ conn: conn, diff --git a/wrappers/timeoutrwc.go b/wrappers/timeoutrwc.go index b83236c..b5f637d 100644 --- a/wrappers/timeoutrwc.go +++ b/wrappers/timeoutrwc.go @@ -31,6 +31,8 @@ func (t *TimeoutReadWriteCloser) Close() error { return t.conn.Close() } +// NewTimeoutRWC returns wrapper over net.Conn which sets deadlines for +// every wrapped Read/Write. func NewTimeoutRWC(conn net.Conn, readTimeout, writeTimeout time.Duration) io.ReadWriteCloser { return &TimeoutReadWriteCloser{ conn: conn, diff --git a/wrappers/trafficrwc.go b/wrappers/trafficrwc.go index 207addd..485a54c 100644 --- a/wrappers/trafficrwc.go +++ b/wrappers/trafficrwc.go @@ -29,6 +29,7 @@ func (t *TrafficReadWriteCloser) Close() error { return t.conn.Close() } +// NewTrafficRWC wraps ReadWriteCloser to have read/write callbacks. func NewTrafficRWC(conn io.ReadWriteCloser, readCallback, writeCallback func(int)) io.ReadWriteCloser { return &TrafficReadWriteCloser{ conn: conn, From 8d6c038ccfebc825f12c1520d115879f6daf5ecb Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 10:23:51 +0300 Subject: [PATCH 12/16] Fix dockerfile --- Dockerfile | 3 +-- Makefile | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e5f0b58..02e69dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,8 +29,7 @@ ENTRYPOINT ["/usr/local/bin/mtg"] ENV MTG_IP=0.0.0.0 \ MTG_PORT=3128 \ MTG_STATS_IP=0.0.0.0 \ - MTG_STATS_PORT=3129 \ - MTG_USE_IPV6=true + MTG_STATS_PORT=3129 EXPOSE 3128 3129 COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt diff --git a/Makefile b/Makefile index ce6a1cc..fc5f9c9 100644 --- a/Makefile +++ b/Makefile @@ -76,5 +76,5 @@ install-dep: .PHONY: install-lint install-lint: - @go get gopkg.in/alecthomas/gometalinter.v2 && \ + @go get github.com/alecthomas/gometalinter && \ $(GOMETALINTER) --install >/dev/null From 0e7521b7e47234aa949d4001e96b15d806bdd089 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 10:39:20 +0300 Subject: [PATCH 13/16] Use upx for image build --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 02e69dc..9745b23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ RUN set -x \ curl \ git \ make \ + upx \ && update-ca-certificates ADD . /go/src/github.com/9seconds/mtg @@ -17,7 +18,8 @@ ADD . /go/src/github.com/9seconds/mtg RUN set -x \ && cd /go/src/github.com/9seconds/mtg \ && make clean \ - && make -j 4 static + && make -j 4 static \ + && upx --ultra-brute -qq ./mtg ############################################################################### From 1ab51397050bfefd995203c6a3acbfb712b6347f Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 10:55:44 +0300 Subject: [PATCH 14/16] Make README more friendly --- README.md | 124 +++++++++++------------------------------------------- 1 file changed, 25 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 6eb5360..9ac4875 100644 --- a/README.md +++ b/README.md @@ -8,109 +8,35 @@ Bullshit-free MTPROTO proxy for Telegram # Rationale -Telegram supports proxies and proxies act as a shield for censorship -and blocking actions of different goverments. At the moment of writing, -Telegram supports 2 types of proxies: +There are several available proxies for Telegram MTPROTO available. Here +are the most notable: -1. SOCKS5 -2. MTPROTO +* [Official](https://github.com/TelegramMessenger/MTProxy) +* [Python](https://github.com/alexbers/mtprotoproxy) +* [Erlang](https://github.com/seriyps/mtproto_proxy) +* [JS](https://github.com/FreedomPrevails/JSMTProxy) -SOCKS5 proxy is general SOCKS proxy as defined in -[RFC1928](https://www.ietf.org/rfc/rfc1928.txt). The problem is that -by default SOCKS5 proxy has an access to the whole internet so a lot -of people tend to hide them "just for a case". It is possible to setup -SOCKS5 proxy so it is able to access just some IPs/CIDRs but, you know, -yeah. +Almost all of them follow the way how official proxy was build. This +includes support of multiple secrets, support of promoted channels etc. -MTPROTO proxy is a native Telegram proxy. It has several advantages: +mtg is an implementation in golang which is intended to be: -1. Traffic is obfuscated by AES-CTR; -2. It allows connections only to Telegram services; -3. It gives proxy maintainer an ability to promote its channel. - -But in reality, MTPROTO have 2 advantages (from my biased view): - -1. Obfuscation -2. Simplify connection chain. - -Here is how it looks like to work with SOCKS5 proxy: - -``` -Client -> SOCKS -> MTPROTO -> Telegram -``` - -SOCKS5 connects to IPs of Telegram proxies. AFAIK this is because -Telegram wants us to avoid censorship and regulations. - -What MTPROTO proxies do: - -``` -Client -> MTPROTO -> Telegram -``` - -And promoted channels. I do not tend to use them because mtg was created -for slightly other way of using it but yeah. People want moneys. - -There are a number of unofficial proxies and one -[OFFICIAL](https://github.com/TelegramMessenger/MTProxy), so why bother? - - - -I'm a big fan of [ShadowSocks](http://www.shadowsocks.org/en/index.html) -project and I like how people use it. The majority of SS proxies are -disposable ones which are blocked/unblocked frequently. There are some -public lists of them in Internet so if one proxy has stopped to work, -you throw it out and use another one. - -Some SS proxies are long-living. This is because they are not public and -intended to be used only by limited number of people. And single secret -is fine there. - -What I do not get about official and some unofficial implementation is -why they decided to support multiple secrets? I mean, WTF with all of -you? - -1. MTPROTO obfuscation (called obfuscated2) does not allow to verify - client easily. You need to decrypt the frame for every secret. So, you - need a number of workers which will constantly try to crack initial - handshake frames with a list of secrets. That does not scale and will - never be. - -2. Why do you need a multiple secrets? Which task are you trying to - solve with them? Valid secret means only 1 thing: access to Telegram. A - binary thing. Absurd and rudimentarty access control. - -Okay, you want to revoke an access, thats fine. Will you ssh to the -machine and restart the container? Do you want to have API for that? Web -UI? Maybe store secrets in database and collect statisitcs per each? - -With all respect, this is idiotic thing. Guysngals, this is a proxy. -Gateway to Telegram. This is not a webservice, or SASS or name that -shit. This is disposable stuff. Blocked? Fine, go to the next one. Just -look at ShadowSocks. There is multiple user implementation available, -with control you want. Does anyone gives a flying fuck about it? - -> Those Who Do Not Learn History Are Doomed To Repeat It -- George Santayana - -What I want to have? - -1. Minimal tool for me and my friends (which are not all my FB friends but - a limited number of close friends). -2. Minimum viable configuration. -3. Single artifact runnable on every platform (not always Docker, some - environments may have no Docker) -4. Smallest Docker image -5. Lightweight -6. Have as less management as possible. - - - -So, please do not ask for: - -1. Multiple users/secrets -2. Web UI -3. Detailed statistics/histograms etc. +* **Lightweight** + It has to consume as less resources as possible but not by losing + maintainability. +* **Easily deployable** + I strongly believe that Telegram proxies should follow the way of + ShadowSocks: promoted channels is a strange way of doing business + I suppose. I think the only viable way is to have a proxy with + minimum configuration which should work everywhere. +* **Single secret** + I think that multiple secrets solves no problems and just complexify + software. I also believe that in case of throwout proxies, this feature + is useless luxury. +* **Minimum docker image size** + Official image is less than 2 megabytes. Literally. +* **No management WebUI** + This is an implementation of simple lightweight proxy. I won't do that. # How to build From 9be13fe093a6434e603a5241048452d41410db0e Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 11:15:16 +0300 Subject: [PATCH 15/16] Add run-mtg script --- run-mtg.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 run-mtg.sh diff --git a/run-mtg.sh b/run-mtg.sh new file mode 100755 index 0000000..8edd1cf --- /dev/null +++ b/run-mtg.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -eu -o pipefail + +IMAGE_NAME="nineseconds/mtg" +CONTAINER_NAME="mtg" +SECRET_PATH="$HOME/.mtg.secret" +PROXY_PORT=444 +STAT_PORT=3129 + +[[ -e "$SECRET_PATH" ]] || ( + openssl rand -hex 16 > "$SECRET_PATH" + chmod 0400 "$SECRET_PATH" +) + +# docker pull "$IMAGE_NAME" +docker ps --filter "Name=$CONTAINER_NAME" -aq | xargs -r docker rm -fv +docker run \ + --name "$CONTAINER_NAME" \ + --sysctl 'net.ipv4.ip_local_port_range=10000 65000' \ + --sysctl net.ipv4.tcp_congestion_control=bbr \ + --sysctl net.ipv4.tcp_fastopen=3 \ + --sysctl net.ipv4.tcp_fin_timeout=30 \ + --sysctl net.ipv4.tcp_keepalive_time=1200 \ + --sysctl net.ipv4.tcp_max_syn_backlog=4096 \ + --sysctl net.ipv4.tcp_max_tw_buckets=5000 \ + --sysctl net.ipv4.tcp_mtu_probing=1 \ + --sysctl 'net.ipv4.tcp_rmem=4096 87380 67108864' \ + --sysctl net.ipv4.tcp_syncookies=1 \ + --sysctl net.ipv4.tcp_tw_reuse=1 \ + --sysctl 'net.ipv4.tcp_wmem=4096 65536 67108864' \ + --ulimit nofile=51200:51200 \ + --restart=unless-stopped \ + -p $PROXY_PORT:3128 \ + -p $STAT_PORT:3129 \ + "$IMAGE_NAME" "$(cat "$SECRET_PATH")" From a2d0210780592eb8632ec443ec932070084a7dd2 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 19 Jun 2018 11:16:47 +0300 Subject: [PATCH 16/16] Mention run-mtg script --- README.md | 2 ++ config/global_ips.go | 7 ++++--- run-mtg.sh | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ac4875..f697f0d 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,5 @@ $ docker run --name mtg --restart=unless-stopped -p 444:3128 -p 3129:3129 -d nin You will have this tool up and running on port 444. Now curl `localhost:3129` to get `tg://` links or do `docker logs mtg`. Also, port 3129 will show you some statistics if you are interested in. + +Also, you can use [run-mtg.sh](https://github.com/9seconds/mtg/blob/master/run-mtg.sh) script diff --git a/config/global_ips.go b/config/global_ips.go index 5a73747..cf695c9 100644 --- a/config/global_ips.go +++ b/config/global_ips.go @@ -24,14 +24,15 @@ func fetchIP(url string) (net.IP, error) { } defer resp.Body.Close() // nolint: errcheck - respData, err := ioutil.ReadAll(resp.Body) + respDataBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } + respData := strings.TrimSpace(string(respDataBytes)) - ip := net.ParseIP(strings.TrimSpace(string(respData))) + ip := net.ParseIP(respData) if ip == nil { - return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", resp) + return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", respData) } return ip, nil diff --git a/run-mtg.sh b/run-mtg.sh index 8edd1cf..7cc965e 100755 --- a/run-mtg.sh +++ b/run-mtg.sh @@ -15,6 +15,7 @@ STAT_PORT=3129 # docker pull "$IMAGE_NAME" docker ps --filter "Name=$CONTAINER_NAME" -aq | xargs -r docker rm -fv docker run \ + -d \ --name "$CONTAINER_NAME" \ --sysctl 'net.ipv4.ip_local_port_range=10000 65000' \ --sysctl net.ipv4.tcp_congestion_control=bbr \