From b44c8b4e6343ccac4c5265685d37ea15dd2d1b8c Mon Sep 17 00:00:00 2001 From: ymmt2005 Date: Wed, 31 Aug 2016 16:56:12 +0900 Subject: [PATCH] Reimplement transocks based on cybozu-go/cmd . --- .travis.yml | 8 +- CHANGELOG.md | 16 +++ README.md | 50 +++++----- cmd/transocks/main.go | 71 ++++++++------ config.go | 46 +++++++-- original_dst_linux.go | 5 +- original_dst_linux_test.go | 4 +- server.go | 194 +++++++++++++++++++++---------------- 8 files changed, 243 insertions(+), 151 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.travis.yml b/.travis.yml index b47e70c..a08054c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ sudo: false language: go go: - - 1.6 + - 1.7 - tip +before_install: + - go get github.com/golang/lint/golint + script: + - go install ./... - go test -v ./... + - go vet -x ./... + - $HOME/gopath/bin/golint -set_exit_status -min_confidence 0.81 ./... diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..42f17a8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [Unreleased] +### Added +- transocks now adopts [github.com/cybozu-go/cmd][cmd] framework. + As a result, it implements [the common spec][spec] including graceful restart. + +### Changed +- The default configuration file path is now `/etc/transocks.toml`. +- Configuration items for logging is changed. + +[cmd]: https://github.com/cybozu-go/cmd +[spec]: https://github.com/cybozu-go/cmd/blob/master/README.md#specifications +[Unreleased]: https://github.com/cybozu-go/transocks/compare/v0.1...HEAD diff --git a/README.md b/README.md index 96c63be..2a1fe5e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -[![GoDoc](https://godoc.org/github.com/cybozu-go/transocks?status.png)][godoc] -[![Build Status](https://travis-ci.org/cybozu-go/transocks.png)](https://travis-ci.org/cybozu-go/transocks) +[![GitHub release](https://img.shields.io/github/release/cybozu-go/transocks.svg?maxAge=60)][releases] +[![GoDoc](https://godoc.org/github.com/cybozu-go/transocks?status.svg)][godoc] +[![Build Status](https://travis-ci.org/cybozu-go/transocks.svg?branch=master)](https://travis-ci.org/cybozu-go/transocks) +[![Go Report Card](https://goreportcard.com/badge/github.com/cybozu-go/transocks)](https://goreportcard.com/report/github.com/cybozu-go/transocks) +[![License](https://img.shields.io/github/license/cybozu-go/transocks.svg?maxAge=2592000)](LICENSE) transocks - a transparent SOCKS5/HTTP proxy =========================================== @@ -21,35 +24,42 @@ Features * SOCKS5 and HTTP proxy (CONNECT) We recommend using SOCKS5 server if available. - Looking for a good SOCKS5 server? Take a look at our [usocksd][]! + Take a look at our SOCKS server [usocksd][] if you are looking for. HTTP proxies often prohibits CONNECT method to make connections to ports other than 443. Make sure your HTTP proxy allows CONNECT to the ports you want. +* Graceful stop & restart + + * On SIGINT/SIGTERM, transocks stops gracefully. + * On SIGHUP, transocks restarts gracefully. + * Library and executable transocks comes with a handy executable. You may use the library to create your own. +Install +------- + +Use Go 1.7 or better. + +``` +go get -u github.com/cybozu-go/transocks/... +``` + Usage ----- `transocks [-h] [-f CONFIG]` -The default configuration file path is `/usr/local/etc/transocks.toml`. +The default configuration file path is `/etc/transocks.toml`. -`transocks` does not have *daemon* mode. Use systemd or upstart to -run it on your background. +In addition, transocks implements [the common spec](https://github.com/cybozu-go/cmd#specifications) from [`cybozu-go/cmd`](https://github.com/cybozu-go/cmd). -Install -------- - -Use Go 1.5 or better. - -``` -go get github.com/cybozu-go/transocks/cmd/transocks -``` +transocks does not have *daemon* mode. Use systemd to run it +on your background. Configuration file format ------------------------- @@ -66,8 +76,10 @@ listen = "localhost:1081" proxy_url = "socks5://10.20.30.40:1080" # for SOCKS5 server #proxy_url = "http://10.20.30.40:3128" # for HTTP proxy server -log_level = "info" -log_file = "/var/log/transocks.log" +[log] +filename = "/path/to/file" # default to stderr +level = "info" # critical", error, warning, info, debug +format = "json" # plain, logfmt, json ``` Redirecting connections by iptables @@ -110,13 +122,7 @@ License [MIT](https://opensource.org/licenses/MIT) -Author ------- - -[@ymmt2005][] - [godoc]: https://godoc.org/github.com/cybozu-go/transocks [Squid]: http://www.squid-cache.org/ [usocksd]: https://github.com/cybozu-go/usocksd [TOML]: https://github.com/toml-lang/toml -[@ymmt2005]: https://github.com/ymmt2005 diff --git a/cmd/transocks/main.go b/cmd/transocks/main.go index 7bb89af..5370503 100644 --- a/cmd/transocks/main.go +++ b/cmd/transocks/main.go @@ -1,79 +1,90 @@ -// transocks server. package main import ( "flag" "fmt" + "net" "net/url" - "os" "github.com/BurntSushi/toml" + "github.com/cybozu-go/cmd" "github.com/cybozu-go/log" "github.com/cybozu-go/transocks" ) type tomlConfig struct { - Listen string - ProxyURL string `toml:"proxy_url"` - LogLevel string `toml:"log_level"` - LogFile string `toml:"log_file"` + Listen string `toml:"listen"` + ProxyURL string `toml:"proxy_url"` + Log cmd.LogConfig `toml:"log"` } var ( - configFile = flag.String("f", "/usr/local/etc/transocks.toml", + configFile = flag.String("f", "/etc/transocks.toml", "TOML configuration file path") ) -func loadConfig() (*transocks.Config, string, error) { +func loadConfig() (*transocks.Config, error) { tc := new(tomlConfig) md, err := toml.DecodeFile(*configFile, tc) if err != nil { - return nil, "", err + return nil, err } if len(md.Undecoded()) > 0 { - return nil, "", fmt.Errorf("undecoded key in TOML: %v", md.Undecoded()) + return nil, fmt.Errorf("undecoded key in TOML: %v", md.Undecoded()) } c := transocks.NewConfig() - c.Listen = tc.Listen + c.Addr = tc.Listen u, err := url.Parse(tc.ProxyURL) if err != nil { - return nil, "", err + return nil, err } c.ProxyURL = u - if err = log.DefaultLogger().SetThresholdByName(tc.LogLevel); err != nil { - return nil, "", err + err = tc.Log.Apply() + if err != nil { + return nil, err } - return c, tc.LogFile, nil + return c, nil +} + +func serve(lns []net.Listener, c *transocks.Config) { + s, err := transocks.NewServer(c) + if err != nil { + log.ErrorExit(err) + } + + for _, ln := range lns { + s.Serve(ln) + } + err = cmd.Wait() + if err != nil && !cmd.IsSignaled(err) { + log.ErrorExit(err) + } } func main() { flag.Parse() - c, logfile, err := loadConfig() + c, err := loadConfig() if err != nil { log.ErrorExit(err) } - if len(logfile) > 0 { - f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - log.ErrorExit(err) - } - defer f.Close() - log.DefaultLogger().SetOutput(f) + g := &cmd.Graceful{ + Listen: func() ([]net.Listener, error) { + return transocks.Listeners(c) + }, + Serve: func(lns []net.Listener) { + serve(lns, c) + }, } + g.Run() - srv, err := transocks.NewServer(c) - if err != nil { + err = cmd.Wait() + if err != nil && !cmd.IsSignaled(err) { log.ErrorExit(err) } - log.Info("server starts", nil) - - srv.Serve() - - log.Info("server ends", nil) } diff --git a/config.go b/config.go index 1b65d50..8ac1415 100644 --- a/config.go +++ b/config.go @@ -5,18 +5,32 @@ import ( "fmt" "net" "net/url" + "time" + + "github.com/cybozu-go/cmd" + "github.com/cybozu-go/log" ) const ( - // NAT mode - ModeNAT = "nat" + defaultShutdownTimeout = 1 * time.Minute +) + +// Mode is the type of transocks mode. +type Mode string + +func (m Mode) String() string { + return string(m) +} + +const ( + // ModeNAT is mode constant for NAT. + ModeNAT = Mode("nat") ) // Config keeps configurations for Server. type Config struct { - // Listen is the listening address. - // e.g. "localhost:1081" - Listen string + // Addr is the listening address. + Addr string // ProxyURL is the URL for upstream proxy. // @@ -27,27 +41,39 @@ type Config struct { ProxyURL *url.URL // Mode determines how clients are routed to transocks. - // Default is "nat". No other options are available at this point. - Mode string + // Default is ModeNAT. No other options are available at this point. + Mode Mode + + // ShutdownTimeout is the maximum duration the server waits for + // all connections to be closed before shutdown. + // + // Zero duration disables timeout. Default is 1 minute. + ShutdownTimeout time.Duration // Dialer is the base dialer to connect to the proxy server. // The server uses the default dialer if this is nil. Dialer *net.Dialer + + // Logger can be used to provide a custom logger. + // If nil, the default logger is used. + Logger *log.Logger + + // Env can be used to specify a cmd.Environment on which the server runs. + // If nil, the server will run on the global environment. + Env *cmd.Environment } // NewConfig creates and initializes a new Config. func NewConfig() *Config { c := new(Config) c.Mode = ModeNAT + c.ShutdownTimeout = defaultShutdownTimeout return c } // validate validates the configuration. // It returns non-nil error if the configuration is not valid. func (c *Config) validate() error { - if len(c.Listen) == 0 { - return errors.New("Listen is empty") - } if c.ProxyURL == nil { return errors.New("ProxyURL is nil") } diff --git a/original_dst_linux.go b/original_dst_linux.go index f098776..473dea4 100644 --- a/original_dst_linux.go +++ b/original_dst_linux.go @@ -10,7 +10,10 @@ import ( ) const ( - SO_ORIGINAL_DST = 80 + // SO_ORIGINAL_DST is a Linux getsockopt optname. + SO_ORIGINAL_DST = 80 + + // IP6T_SO_ORIGINAL_DST a Linux getsockopt optname. IP6T_SO_ORIGINAL_DST = 80 ) diff --git a/original_dst_linux_test.go b/original_dst_linux_test.go index 5afd2ec..0210d89 100644 --- a/original_dst_linux_test.go +++ b/original_dst_linux_test.go @@ -22,10 +22,10 @@ func TestGetOriginalDST(t *testing.T) { } defer c.Close() - orig_addr, err := GetOriginalDST(c.(*net.TCPConn)) + origAddr, err := GetOriginalDST(c.(*net.TCPConn)) if err != nil { t.Fatal(err) } - t.Log(orig_addr.String()) + t.Log(origAddr.String()) } diff --git a/server.go b/server.go index d91ff71..ae7e1b4 100644 --- a/server.go +++ b/server.go @@ -1,26 +1,39 @@ package transocks import ( + "context" "io" "net" + "sync" "time" + "github.com/cybozu-go/cmd" "github.com/cybozu-go/log" + "github.com/cybozu-go/netutil" "golang.org/x/net/proxy" ) -var ( - defaultDialer = &net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 60 * time.Second, - } +const ( + keepAliveTimeout = 3 * time.Minute + copyBufferSize = 64 << 10 ) +// Listeners returns a list of net.Listener. +func Listeners(c *Config) ([]net.Listener, error) { + ln, err := net.Listen("tcp", c.Addr) + if err != nil { + return nil, err + } + return []net.Listener{ln}, nil +} + // Server provides transparent proxy server functions. type Server struct { - config *Config - dialer proxy.Dialer - listener net.Listener + cmd.Server + mode Mode + logger *log.Logger + dialer proxy.Dialer + pool sync.Pool } // NewServer creates Server. @@ -30,99 +43,110 @@ func NewServer(c *Config) (*Server, error) { return nil, err } - dialer := defaultDialer - if c.Dialer != nil { - dialer = c.Dialer + dialer := c.Dialer + if dialer == nil { + dialer = &net.Dialer{ + KeepAlive: keepAliveTimeout, + DualStack: true, + } } - proxy_dialer, err := proxy.FromURL(c.ProxyURL, dialer) + pdialer, err := proxy.FromURL(c.ProxyURL, dialer) if err != nil { return nil, err } - - l, err := net.Listen("tcp", c.Listen) - if err != nil { - return nil, err + logger := c.Logger + if logger == nil { + logger = log.DefaultLogger() } - return &Server{c, proxy_dialer, l}, nil + s := &Server{ + Server: cmd.Server{ + ShutdownTimeout: c.ShutdownTimeout, + Env: c.Env, + }, + mode: c.Mode, + logger: logger, + dialer: pdialer, + pool: sync.Pool{ + New: func() interface{} { + return make([]byte, copyBufferSize) + }, + }, + } + s.Server.Handler = s.handleConnection + return s, nil } -// Serve accepts and handles new connections forever. -func (s *Server) Serve() error { - for { - conn, err := s.listener.Accept() - if err != nil { - log.Critical(err.Error(), nil) - return err - } - tcp_conn, ok := conn.(*net.TCPConn) - if !ok { - conn.Close() - panic("not a TCPConn!") - } - go s.handleConnection(tcp_conn) - } -} - -func (s *Server) handleConnection(c *net.TCPConn) { - defer c.Close() - - var addr string - - switch s.config.Mode { - case ModeNAT: - orig_addr, err := GetOriginalDST(c) - if err != nil { - log.Error(err.Error(), nil) - return - } - addr = orig_addr.String() - default: - addr = c.LocalAddr().String() - } - - if log.Enabled(log.LvDebug) { - log.Debug("making proxy connection", map[string]interface{}{ - "_dst": addr, - }) - } - - pconn, err := s.dialer.Dial("tcp", addr) - if err != nil { - log.Error(err.Error(), map[string]interface{}{ - "_dst": addr, +func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { + tc, ok := conn.(*net.TCPConn) + if !ok { + s.logger.Error("non-TCP connection", map[string]interface{}{ + "conn": conn, }) return } - defer pconn.Close() - ch := make(chan error, 2) - go copyData(c, pconn, ch) - go copyData(pconn, c, ch) - for i := 0; i < 2; i++ { - e := <-ch - if e != nil { - log.Error(e.Error(), map[string]interface{}{ - "_dst": addr, - }) - break + fields := cmd.FieldsFromContext(ctx) + fields[log.FnType] = "access" + fields["client_addr"] = conn.RemoteAddr().String() + + var addr string + switch s.mode { + case ModeNAT: + origAddr, err := GetOriginalDST(tc) + if err != nil { + fields[log.FnError] = err.Error() + s.logger.Error("GetOriginalDST failed", fields) + return } + addr = origAddr.String() + default: + addr = tc.LocalAddr().String() } + fields["dest_addr"] = addr - if log.Enabled(log.LvDebug) { - log.Debug("closing proxy connection", map[string]interface{}{ - "_dst": addr, - }) + destConn, err := s.dialer.Dial("tcp", addr) + if err != nil { + fields[log.FnError] = err.Error() + s.logger.Error("failed to connect to proxy server", fields) + return } -} + defer destConn.Close() -func copyData(dst io.Writer, src io.Reader, ch chan<- error) { - _, err := io.Copy(dst, src) - if tdst, ok := dst.(*net.TCPConn); ok { - tdst.CloseWrite() + s.logger.Info("proxy starts", fields) + + // do proxy + st := time.Now() + env := cmd.NewEnvironment(ctx) + env.Go(func(ctx context.Context) error { + buf := s.pool.Get().([]byte) + _, err := io.CopyBuffer(destConn, tc, buf) + s.pool.Put(buf) + if hc, ok := destConn.(netutil.HalfCloser); ok { + hc.CloseWrite() + } + tc.CloseRead() + return err + }) + env.Go(func(ctx context.Context) error { + buf := s.pool.Get().([]byte) + _, err := io.CopyBuffer(tc, destConn, buf) + s.pool.Put(buf) + tc.CloseWrite() + if hc, ok := destConn.(netutil.HalfCloser); ok { + hc.CloseRead() + } + return err + }) + env.Stop() + err = env.Wait() + + fields = cmd.FieldsFromContext(ctx) + fields["elapsed"] = time.Since(st).Seconds() + if err != nil { + fields[log.FnError] = err.Error() + s.logger.Error("proxy ends with an error", fields) + return } - if tsrc, ok := src.(*net.TCPConn); ok { - tsrc.CloseRead() - } - ch <- err + s.logger.Info("proxy ends", fields) }