From 86e3be475a8e6d5395ae307903c533b6046e5c25 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Sun, 17 Jun 2018 12:25:51 +0300 Subject: [PATCH] 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())