diff --git a/main.go b/main.go index 17c5a7b..a29af64 100644 --- a/main.go +++ b/main.go @@ -95,7 +95,7 @@ func main() { usage("Cannot get local IP address.") } myIPBytes, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() + resp.Body.Close() // nolint: errcheck if err != nil { usage("Cannot get local IP address.") @@ -141,6 +141,6 @@ func printURLs(data interface{}) { } func usage(msg string) { - io.WriteString(os.Stderr, msg+"\n") + io.WriteString(os.Stderr, msg+"\n") // nolint: errcheck os.Exit(1) } diff --git a/obfuscated2/frame.go b/obfuscated2/frame.go index 8a3fec2..296e0e4 100644 --- a/obfuscated2/frame.go +++ b/obfuscated2/frame.go @@ -9,7 +9,6 @@ import ( "github.com/juju/errors" ) -// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ // [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd] const ( frameLenKey = 32 @@ -30,23 +29,34 @@ const ( var tgMagicBytes = []byte{tgMagicByte, tgMagicByte, tgMagicByte, tgMagicByte} +// Frame represents handshake frame. Telegram sends 64 bytes of obfuscated2 +// initialization data first. +// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ type Frame []byte +// Key returns AES encryption key. func (f Frame) Key() []byte { return f[frameOffsetFirst:frameOffsetKey] } +// IV returns AES encryption initialization vector func (f Frame) IV() []byte { return f[frameOffsetKey:frameOffsetIV] } +// Magic returns magic bytes from last 8 bytes of frame. Telegram checks +// for values there. If after decryption magic is not as expected, +// connection considered as failed. func (f Frame) Magic() []byte { return f[frameOffsetIV:frameOffsetMagic] } +// DC returns number of datacenter IP client wants to use. func (f Frame) DC() (n int16) { buf := bytes.NewReader(f[frameOffsetMagic:frameOffsetDC]) - binary.Read(buf, binary.LittleEndian, &n) + if err := binary.Read(buf, binary.LittleEndian, &n); err != nil { + n = 1 + } if n < 0 { n = -n @@ -57,10 +67,13 @@ func (f Frame) DC() (n int16) { return n - 1 } +// Valid checks that *decrypted* frame is valid. Only magic bytes are checked. func (f Frame) Valid() bool { return bytes.Equal(f.Magic(), tgMagicBytes) } +// Invert inverts frame for extracting encryption keys. Pkease check that link: +// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ func (f Frame) Invert() Frame { reversed := make(Frame, FrameLen) copy(reversed, f) @@ -72,6 +85,7 @@ func (f Frame) Invert() Frame { return reversed } +// ExtractFrame extracts exact obfuscated2 handshake frame from given reader. func ExtractFrame(conn io.Reader) (Frame, error) { buf := &bytes.Buffer{} if _, err := io.CopyN(buf, conn, FrameLen); err != nil { diff --git a/obfuscated2/obfuscated2.go b/obfuscated2/obfuscated2.go index e521d18..2ac62af 100644 --- a/obfuscated2/obfuscated2.go +++ b/obfuscated2/obfuscated2.go @@ -8,35 +8,43 @@ import ( "github.com/juju/errors" ) +// 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 } +// ParseObfuscated2ClientFrame parses client frame. Please check this link for +// 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) decHasher := sha256.New() - decHasher.Write(frame.Key()) - decHasher.Write(secret) + decHasher.Write(frame.Key()) // nolint: errcheck + decHasher.Write(secret) // nolint: errcheck decryptor := makeStreamCipher(decHasher.Sum(nil), frame.IV()) invertedFrame := frame.Invert() encHasher := sha256.New() - encHasher.Write(invertedFrame.Key()) - encHasher.Write(secret) + encHasher.Write(invertedFrame.Key()) // nolint: errcheck + encHasher.Write(secret) // nolint: errcheck encryptor := makeStreamCipher(encHasher.Sum(nil), invertedFrame.IV()) decryptedFrame := make(Frame, FrameLen) @@ -53,6 +61,9 @@ func ParseObfuscated2ClientFrame(secret, data []byte) (*Obfuscated2, int16, erro return obfs, decryptedFrame.DC(), nil } +// MakeTelegramObfuscated2Frame creates new handshake frame to send to +// Telegram. +// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ func MakeTelegramObfuscated2Frame() (*Obfuscated2, Frame) { frame := generateFrame() diff --git a/obfuscated2/obfuscated2_test.go b/obfuscated2/obfuscated2_test.go index 7a99b2c..9e660e0 100644 --- a/obfuscated2/obfuscated2_test.go +++ b/obfuscated2/obfuscated2_test.go @@ -2,7 +2,6 @@ package obfuscated2 import ( "crypto/sha256" - "fmt" "testing" "github.com/stretchr/testify/assert" diff --git a/proxy/cipherrwc.go b/proxy/cipherrwc.go index 549fd23..43a03cb 100644 --- a/proxy/cipherrwc.go +++ b/proxy/cipherrwc.go @@ -5,39 +5,44 @@ import ( "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 } -func (c *CipherReadWriteCloser) Write(p []byte) (n int, err error) { +// Write writes into connection. +func (c *CipherReadWriteCloser) Write(p []byte) (int, error) { encrypted := c.crypt.Encrypt(p) + allWritten := 0 - curN := 0 for len(encrypted) > 0 { - curN, err = c.conn.Write(encrypted) - n += curN + n, err := c.conn.Write(encrypted) + allWritten += n if err != nil { - return + return allWritten, err } encrypted = encrypted[n:] } - return + return allWritten, nil } +// Close closes underlying connection. func (c *CipherReadWriteCloser) Close() error { return c.conn.Close() } diff --git a/proxy/ctxrwc.go b/proxy/ctxrwc.go index ea9413e..e605ab3 100644 --- a/proxy/ctxrwc.go +++ b/proxy/ctxrwc.go @@ -7,12 +7,15 @@ import ( "github.com/juju/errors" ) +// CtxReadWriteCloser wraps underlying connection and does management of the +// context and its cancel function. type CtxReadWriteCloser struct { ctx context.Context conn io.ReadWriteCloser cancel context.CancelFunc } +// Read reads from connection func (c *CtxReadWriteCloser) Read(p []byte) (int, error) { select { case <-c.ctx.Done(): @@ -26,6 +29,7 @@ func (c *CtxReadWriteCloser) Read(p []byte) (int, error) { } } +// Write writes into connection. func (c *CtxReadWriteCloser) Write(p []byte) (int, error) { select { case <-c.ctx.Done(): @@ -39,11 +43,12 @@ func (c *CtxReadWriteCloser) Write(p []byte) (int, error) { } } +// Close closes underlying connection. func (c *CtxReadWriteCloser) Close() error { return c.conn.Close() } -func newCtxReadWriteCloser(conn io.ReadWriteCloser, ctx context.Context, cancel context.CancelFunc) io.ReadWriteCloser { +func newCtxReadWriteCloser(ctx context.Context, cancel context.CancelFunc, conn io.ReadWriteCloser) io.ReadWriteCloser { return &CtxReadWriteCloser{ conn: conn, ctx: ctx, diff --git a/proxy/logrwc.go b/proxy/logrwc.go index 8a0ad28..b783d3b 100644 --- a/proxy/logrwc.go +++ b/proxy/logrwc.go @@ -6,6 +6,8 @@ import ( "go.uber.org/zap" ) +// LogReadWriteCloser adds additional logging for reading/writing. All +// logging is performed for debug mode only. type LogReadWriteCloser struct { conn io.ReadWriteCloser logger *zap.SugaredLogger @@ -13,18 +15,21 @@ type LogReadWriteCloser struct { name string } +// Read reads from connection func (l *LogReadWriteCloser) Read(p []byte) (n int, err error) { n, err = l.conn.Read(p) l.logger.Debugw("Finish reading", "name", l.name, "socketid", l.sockid, "nbytes", n, "error", err) return } +// Write writes into connection. func (l *LogReadWriteCloser) Write(p []byte) (n int, err error) { n, err = l.conn.Write(p) l.logger.Debugw("Finish writing", "name", l.name, "socketid", l.sockid, "nbytes", n, "error", err) return } +// Close closes underlying connection. func (l *LogReadWriteCloser) Close() error { err := l.conn.Close() l.logger.Debugw("Finish closing socket", "name", l.name, "socketid", l.sockid, "error", err) diff --git a/proxy/server.go b/proxy/server.go index 3fb8d8e..bd3b457 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -14,14 +14,12 @@ import ( "go.uber.org/zap" ) -const bufferSize = 4096 - +// Server is an insgtance of MTPROTO proxy. type Server struct { ip net.IP port int secret []byte logger *zap.SugaredLogger - lsock net.Listener ctx context.Context readTimeout time.Duration writeTimeout time.Duration @@ -29,8 +27,10 @@ type Server struct { ipv6 bool } +// Serve does MTPROTO proxying. func (s *Server) Serve() error { - lsock, err := net.Listen("tcp", s.Addr()) + addr := net.JoinHostPort(s.ip.String(), strconv.Itoa(s.port)) + lsock, err := net.Listen("tcp", addr) if err != nil { return errors.Annotate(err, "Cannot create listen socket") } @@ -42,18 +42,12 @@ func (s *Server) Serve() error { go s.accept(conn) } } - - return nil -} - -func (s *Server) Addr() string { - return net.JoinHostPort(s.ip.String(), strconv.Itoa(s.port)) } func (s *Server) accept(conn net.Conn) { defer func() { s.stats.closeConnection() - conn.Close() + conn.Close() // nolint: errcheck if r := recover(); r != nil { s.logger.Errorw("Crash of accept handler", "error", r) @@ -70,7 +64,7 @@ func (s *Server) accept(conn net.Conn) { "socketid", socketID, ) - clientConn, dc, err := s.getClientStream(conn, ctx, cancel, socketID) + clientConn, dc, err := s.getClientStream(ctx, cancel, conn, socketID) if err != nil { s.logger.Warnw("Cannot initialize client connection", "secret", s.secret, @@ -80,9 +74,9 @@ func (s *Server) accept(conn net.Conn) { ) return } - defer clientConn.Close() + defer clientConn.Close() // nolint: errcheck - tgConn, err := s.getTelegramStream(dc, ctx, cancel, socketID) + tgConn, err := s.getTelegramStream(ctx, cancel, dc, socketID) if err != nil { s.logger.Warnw("Cannot initialize Telegram connection", "socketid", socketID, @@ -90,12 +84,18 @@ func (s *Server) accept(conn net.Conn) { ) return } - defer tgConn.Close() + defer tgConn.Close() // nolint: errcheck wait := &sync.WaitGroup{} wait.Add(2) - go s.pipe(wait, clientConn, tgConn) - go s.pipe(wait, tgConn, clientConn) + go func() { + defer wait.Done() + io.Copy(clientConn, tgConn) // nolint: errcheck + }() + go func() { + defer wait.Done() + io.Copy(tgConn, clientConn) // nolint: errcheck + }() <-ctx.Done() wait.Wait() @@ -110,7 +110,7 @@ func (s *Server) makeSocketID() string { return uuid.NewV4().String() } -func (s *Server) getClientStream(conn net.Conn, ctx context.Context, cancel context.CancelFunc, socketID string) (io.ReadWriteCloser, int16, error) { +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 = newTrafficReadWriteCloser(wConn, s.stats.addIncomingTraffic, s.stats.addOutgoingTraffic) frame, err := obfuscated2.ExtractFrame(wConn) @@ -125,12 +125,12 @@ func (s *Server) getClientStream(conn net.Conn, ctx context.Context, cancel cont wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "client") wConn = newCipherReadWriteCloser(wConn, obfs2) - wConn = newCtxReadWriteCloser(wConn, ctx, cancel) + wConn = newCtxReadWriteCloser(ctx, cancel, wConn) return wConn, dc, nil } -func (s *Server) getTelegramStream(dc int16, ctx context.Context, cancel context.CancelFunc, socketID string) (io.ReadWriteCloser, error) { +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) if err != nil { return nil, errors.Annotate(err, "Cannot dial") @@ -145,16 +145,12 @@ func (s *Server) getTelegramStream(dc int16, ctx context.Context, cancel context wConn = newLogReadWriteCloser(wConn, s.logger, socketID, "telegram") wConn = newCipherReadWriteCloser(wConn, obfs2) - wConn = newCtxReadWriteCloser(wConn, ctx, cancel) + wConn = newCtxReadWriteCloser(ctx, cancel, wConn) return wConn, nil } -func (s *Server) pipe(wait *sync.WaitGroup, reader io.Reader, writer io.Writer) { - defer wait.Done() - io.Copy(writer, reader) -} - +// 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 { return &Server{ diff --git a/proxy/stats.go b/proxy/stats.go index a7375e2..0c642bc 100644 --- a/proxy/stats.go +++ b/proxy/stats.go @@ -2,6 +2,7 @@ package proxy import ( "encoding/json" + "fmt" "net" "net/http" "net/url" @@ -17,6 +18,7 @@ func (s statsUptime) MarshalJSON() ([]byte, error) { return []byte(strconv.Itoa(uptime)), nil } +// Stats is a datastructure for statistics on work of this proxy. type Stats struct { AllConnections uint64 `json:"all_connections"` ActiveConnections uint32 `json:"active_connections"` @@ -50,20 +52,22 @@ func (s *Stats) addOutgoingTraffic(n int) { atomic.AddUint64(&s.Traffic.Outgoing, uint64(n)) } -func (s *Stats) Serve(host net.IP, port uint16) { +// Serve runs statistics HTTP server. +func (s *Stats) Serve(host fmt.Stringer, port uint16) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") - encoder.Encode(s) + encoder.Encode(s) // nolint: errcheck, gas }) addr := net.JoinHostPort(host.String(), strconv.Itoa(int(port))) - http.ListenAndServe(addr, nil) + http.ListenAndServe(addr, 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) diff --git a/proxy/telegram.go b/proxy/telegram.go index 1f5409d..ad3b1d6 100644 --- a/proxy/telegram.go +++ b/proxy/telegram.go @@ -7,19 +7,24 @@ import ( "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"}, diff --git a/proxy/timeoutrwc.go b/proxy/timeoutrwc.go index 4e0ca48..0067d60 100644 --- a/proxy/timeoutrwc.go +++ b/proxy/timeoutrwc.go @@ -6,22 +6,27 @@ import ( "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)) + 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)) + 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() } diff --git a/proxy/trafficrwc.go b/proxy/trafficrwc.go index 6afd300..a6be861 100644 --- a/proxy/trafficrwc.go +++ b/proxy/trafficrwc.go @@ -2,24 +2,29 @@ package proxy import "io" +// TrafficReadWriteCloser counts an amount of ingress/egress traffic by +// calling given callbacks. type TrafficReadWriteCloser struct { conn io.ReadWriteCloser readCallback func(int) writeCallback func(int) } +// Read reads from connection func (t *TrafficReadWriteCloser) Read(p []byte) (n int, err error) { n, err = t.conn.Read(p) t.readCallback(n) return } +// Write writes into connection. func (t *TrafficReadWriteCloser) Write(p []byte) (n int, err error) { n, err = t.conn.Write(p) t.writeCallback(n) return } +// Close closes underlying connection. func (t *TrafficReadWriteCloser) Close() error { return t.conn.Close() }