From 4fa2892a524017ffb1f5d9dc6109f74c8949d9d4 Mon Sep 17 00:00:00 2001 From: "Yamamoto, Hirotaka" Date: Fri, 4 Mar 2016 10:54:59 +0900 Subject: [PATCH] Initial commit --- .gitignore | 30 +++++++++ .travis.yml | 8 +++ DESIGN.md | 115 ++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++ README.md | 119 +++++++++++++++++++++++++++++++++++ cmd/transocks/main.go | 79 +++++++++++++++++++++++ cmd/transocks/sample.toml | 9 +++ config.go | 58 +++++++++++++++++ http_tunnel.go | 122 ++++++++++++++++++++++++++++++++++++ http_tunnel_test.go | 28 +++++++++ original_dst_linux.go | 87 ++++++++++++++++++++++++++ original_dst_linux_test.go | 31 ++++++++++ original_dst_stub.go | 16 +++++ server.go | 124 +++++++++++++++++++++++++++++++++++++ 14 files changed, 847 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 DESIGN.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/transocks/main.go create mode 100644 cmd/transocks/sample.toml create mode 100644 config.go create mode 100644 http_tunnel.go create mode 100644 http_tunnel_test.go create mode 100644 original_dst_linux.go create mode 100644 original_dst_linux_test.go create mode 100644 original_dst_stub.go create mode 100644 server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25edb18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Editors +*~ +.*.swp +.#* +\#*# + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b47e70c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +sudo: false +language: go +go: + - 1.6 + - tip + +script: + - go test -v ./... diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a0c99fd --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,115 @@ +Design notes +============ + +transocks should work as a SOCKS5 client used as a transparent proxy +agent running on every hosts in trusted (i.e. data center) networks. + +Destination NAT (DNAT) +---------------------- + +On Linux, redirecting locally-generated packet to transocks can be done +by iptables with DNAT (or REDIRECT) target. + +Since DNAT/REDIRECT modifies packet's destination address, transocks +need to recover the destination address by using `getsockopt` with +`SO_ORIGINAL_DST` for IPv4 or with `IP6T_SO_ORIGINAL_DST` for IPv6. +This is, of course, Linux-specific, and Go does not provide standard +API for them. + +Policy-based routing +-------------------- + +Except for DNAT, some operating systems provide a way to route packets +to a specific program. In order to receive such packets, the program +need to set special options on the listening socket before `bind`. + +Difficult is that Go does not allow setting socket options before `bind`. + +### Linux TPROXY + +Linux iptables has [TPROXY][] target that can route packets to a +specific local port. The socket option is: + +* IPv4 + + ``` + setsockopt(IPPROTO_IP, IP_TRANSPARENT) + ``` + +* IPv6 + + ``` + setsockopt(IPPROTO_IPV6, IPV6_TRANSPARENT) + ``` + +To set this option, transocks must have `CAP_NET_ADMIN` capability. +Run transocks as root user, or grant `CAP_NET_ADMIN` for the file by: + +``` +sudo setcap 'cap_net_admin+ep' transocks +``` + +### FreeBSD, NetBSD, OpenBSD + +Use [PF with divert-to][pf] to route packets to a specific local port. + +The listening program needs to set a socket option before `bind`: + +* FreeBSD (IPv4) + + ``` + setsockopt(IPPROTO_IP, IP_BINDANY) + ``` + +* FreeBSD (IPv6) + + ``` + setsockopt(IPPROTO_IPV6, IPV6_BINDANY) + ``` + +* NetBSD, OpenBSD + + ``` + setsockopt(SOL_SOCKET, SO_BINDANY) + ``` + +For this to work, transocks must run as root. + +Implementation strategy +----------------------- + +We use Go for its efficiency and simpleness. + +For SOCKS5, [golang.org/x/net/proxy][x/net] already provides SOCKS5 client. + +For Linux NAT, we need to use [golang.org/x/sys/unix][x/sys] and +[unsafe.Pointer][] to use non-standard `getsockopt` options. + +To set socket options before `bind`, we need to create sockets manually +by using [golang.org/x/sys/unix] and then convert the native socket to +`*net.TCPListener` by [net.FileListener][]. + +CONNECT tunnel +-------------- + +As golang.org/x/net/proxy can add custom dialers, we can implement +a proxy using http CONNECT method for tunneling through HTTP proxies +such as [Squid][]. + +Note that the default Squid configuration file installed for +Ubuntu 14.04 prohibits CONNECT to ports other than 443. + +``` +# Deny CONNECT to other than secure SSL ports +http_access deny CONNECT !SSL_ports +``` + +Remove or comment out the line to allow CONNECT to ports other than 443. + +[TPROXY]: https://www.kernel.org/doc/Documentation/networking/tproxy.txt +[pf]: http://wiki.squid-cache.org/ConfigExamples/Intercept/OpenBsdPf +[x/net]: https://godoc.org/golang.org/x/net/proxy#SOCKS5 +[x/sys]: https://godoc.org/golang.org/x/sys/unix +[unsafe.Pointer]: https://golang.org/pkg/unsafe/#Pointer +[net.FileListener]: https://golang.org/pkg/net/#FileListener +[Squid]: http://www.squid-cache.org/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1fc63c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Cybozu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..226504b --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +[![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) + +transocks - a transparent SOCKS5/HTTP proxy +=========================================== + +**transocks** is a background service to redirect TCP connections +transparently to a SOCKS5 server or a HTTP proxy server like [Squid][]. + +Currently, transocks supports only Linux iptables with DNAT/REDIRECT target. + +Features +-------- + +* IPv4 and IPv6 + + Both IPv4 and IPv6 are supported. + Note that `nf_conntrack_ipv4` or `nf_conntrack_ipv6` kernel modules + must be loaded beforehand. + +* SOCKS5 and HTTP proxy (CONNECT) + + We recommend using SOCKS5 server if available. + + 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. + +* Library and executable + + transocks comes with a handy executable. + You may use the library to create your own. + +Usage +----- + +`transocks [-h] [-f CONFIG]` + +The default configuration file path is `/usr/local/etc/transocks.toml`. + +`transocks` does not have *daemon* mode. Use systemd or upstart to +run it on your background. + +Install +------- + +Use Go 1.5 or better. + +``` +go get github.com/cybozu-go/transocks +go install github.com/cybozu-go/transocks/cmd/transocks +``` + +Configuration file format +------------------------- + +`transocks.toml` is a [TOML][] file. + +`listen` and `proxy_url` are mandatory. +Other items are optional. + +``` +# listening address of transocks. +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" +``` + +Redirecting connections by iptables +----------------------------------- + +Use DNAT or REDIRECT target in OUTPUT chain of the `nat` table. + +Save the following example to a file, then execute: +`sudo iptables-restore < FILE` + +``` +*nat +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +:TRANSOCKS - [0:0] +-A OUTPUT -p tcp -j TRANSOCKS +-A TRANSOCKS -d 0.0.0.0/8 -j RETURN +-A TRANSOCKS -d 10.0.0.0/8 -j RETURN +-A TRANSOCKS -d 127.0.0.0/8 -j RETURN +-A TRANSOCKS -d 169.254.0.0/16 -j RETURN +-A TRANSOCKS -d 172.16.0.0/12 -j RETURN +-A TRANSOCKS -d 192.168.0.0/16 -j RETURN +-A TRANSOCKS -d 224.0.0.0/4 -j RETURN +-A TRANSOCKS -d 240.0.0.0/4 -j RETURN +-A TRANSOCKS -p tcp -j REDIRECT --to-ports 1081 +COMMIT +``` + +Library usage +------------- + +Read [the documentation][godoc]. + +License +------- + +[MIT](https://opensource.org/licenses/MIT) + +Author +------ + +[@ymmt2005][] + +[godoc]: https://godoc.org/github.com/cybozu-go/transocks +[Squid]: http://www.squid-cache.org/ +[TOML]: https://github.com/toml-lang/toml +[@ymmt2005]: https://github.com/ymmt2005 diff --git a/cmd/transocks/main.go b/cmd/transocks/main.go new file mode 100644 index 0000000..7bb89af --- /dev/null +++ b/cmd/transocks/main.go @@ -0,0 +1,79 @@ +// transocks server. +package main + +import ( + "flag" + "fmt" + "net/url" + "os" + + "github.com/BurntSushi/toml" + "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"` +} + +var ( + configFile = flag.String("f", "/usr/local/etc/transocks.toml", + "TOML configuration file path") +) + +func loadConfig() (*transocks.Config, string, error) { + tc := new(tomlConfig) + md, err := toml.DecodeFile(*configFile, tc) + if err != nil { + return nil, "", err + } + if len(md.Undecoded()) > 0 { + return nil, "", fmt.Errorf("undecoded key in TOML: %v", md.Undecoded()) + } + + c := transocks.NewConfig() + c.Listen = tc.Listen + + u, err := url.Parse(tc.ProxyURL) + if err != nil { + return nil, "", err + } + c.ProxyURL = u + + if err = log.DefaultLogger().SetThresholdByName(tc.LogLevel); err != nil { + return nil, "", err + } + + return c, tc.LogFile, nil +} + +func main() { + flag.Parse() + + c, logfile, 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) + } + + srv, err := transocks.NewServer(c) + if err != nil { + log.ErrorExit(err) + } + log.Info("server starts", nil) + + srv.Serve() + + log.Info("server ends", nil) +} diff --git a/cmd/transocks/sample.toml b/cmd/transocks/sample.toml new file mode 100644 index 0000000..8b32d0d --- /dev/null +++ b/cmd/transocks/sample.toml @@ -0,0 +1,9 @@ +# This is a sample TOML file for transocks. + +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 = "debug" +log_file = "/var/log/transocks.log" diff --git a/config.go b/config.go new file mode 100644 index 0000000..1b65d50 --- /dev/null +++ b/config.go @@ -0,0 +1,58 @@ +package transocks + +import ( + "errors" + "fmt" + "net" + "net/url" +) + +const ( + // NAT mode + ModeNAT = "nat" +) + +// Config keeps configurations for Server. +type Config struct { + // Listen is the listening address. + // e.g. "localhost:1081" + Listen string + + // ProxyURL is the URL for upstream proxy. + // + // For SOCKS5, URL looks like "socks5://USER:PASSWORD@HOST:PORT". + // + // For HTTP proxy, URL looks like "http://USER:PASSWORD@HOST:PORT". + // The HTTP proxy must support CONNECT method. + ProxyURL *url.URL + + // Mode determines how clients are routed to transocks. + // Default is "nat". No other options are available at this point. + Mode string + + // Dialer is the base dialer to connect to the proxy server. + // The server uses the default dialer if this is nil. + Dialer *net.Dialer +} + +// NewConfig creates and initializes a new Config. +func NewConfig() *Config { + c := new(Config) + c.Mode = ModeNAT + 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") + } + if c.Mode != ModeNAT { + return fmt.Errorf("Unknown mode: %s", c.Mode) + } + return nil +} diff --git a/http_tunnel.go b/http_tunnel.go new file mode 100644 index 0000000..aefb68f --- /dev/null +++ b/http_tunnel.go @@ -0,0 +1,122 @@ +// This file provides a dialer type of "http://" scheme for +// golang.org/x/net/proxy package. +// +// The dialer type will be automatically registered by init(). +// +// The dialer requests an upstream HTTP proxy to create a TCP tunnel +// by CONNECT method. + +package transocks + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "golang.org/x/net/proxy" +) + +func init() { + proxy.RegisterDialerType("http", httpDialType) +} + +type httpDialer struct { + addr string + header http.Header + forward proxy.Dialer +} + +func httpDialType(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { + authz := "" + if uu := u.User; uu != nil { + passwd, _ := uu.Password() + up := uu.Username() + ":" + passwd + authz = "Basic " + base64.StdEncoding.EncodeToString([]byte(up)) + } + header := map[string][]string{ + "Proxy-Authorization": []string{authz}, + } + return &httpDialer{ + addr: u.Host, + header: header, + forward: forward, + }, nil +} + +func (d *httpDialer) Dial(network, addr string) (c net.Conn, err error) { + req := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: d.header, + } + c, err = d.forward.Dial("tcp", d.addr) + if err != nil { + return + } + req.Write(c) + + // Read response until "\r\n\r\n". + // bufio cannot be used as the connected server may not be + // a HTTP(S) server. + c.SetReadDeadline(time.Now().Add(10 * time.Second)) + buf := make([]byte, 0, 4096) + b := make([]byte, 1) + state := 0 + for { + _, e := c.Read(b) + if e != nil { + c.Close() + return nil, errors.New("reset proxy connection") + } + buf = append(buf, b[0]) + switch state { + case 0: + if b[0] == byte('\r') { + state++ + } + continue + case 1: + if b[0] == byte('\n') { + state++ + } else { + state = 0 + } + continue + case 2: + if b[0] == byte('\r') { + state++ + } else { + state = 0 + } + continue + case 3: + if b[0] == byte('\n') { + goto PARSE + } else { + state = 0 + } + } + } + +PARSE: + var zero time.Time + c.SetReadDeadline(zero) + resp, e := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(buf)), req) + if e != nil { + c.Close() + return nil, e + } + if resp.StatusCode != 200 { + c.Close() + return nil, fmt.Errorf("proxy returns %s", resp.Status) + } + + return c, nil +} diff --git a/http_tunnel_test.go b/http_tunnel_test.go new file mode 100644 index 0000000..69d4131 --- /dev/null +++ b/http_tunnel_test.go @@ -0,0 +1,28 @@ +package transocks + +import ( + "io" + "net" + "os" + "testing" + "time" +) + +func TestHTTPDialer(t *testing.T) { + t.Skip() + + // This test only works if Squid allowing CONNECT to port 80 is + // running on the local machine on port 3128. + + d := &httpDialer{ + addr: "127.0.0.1:3128", + forward: &net.Dialer{Timeout: 5 * time.Second}, + } + + conn, err := d.Dial("tcp", "www.yahoo.com:80") + if err != nil { + t.Fatal(err) + } + conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.yahoo.com:80\r\nConnection: close\r\n\r\n")) + io.Copy(os.Stdout, conn) +} diff --git a/original_dst_linux.go b/original_dst_linux.go new file mode 100644 index 0000000..f098776 --- /dev/null +++ b/original_dst_linux.go @@ -0,0 +1,87 @@ +// +build linux + +package transocks + +import ( + "net" + "os" + "syscall" + "unsafe" +) + +const ( + SO_ORIGINAL_DST = 80 + IP6T_SO_ORIGINAL_DST = 80 +) + +func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) { + _, _, e := syscall.Syscall6( + syscall.SYS_GETSOCKOPT, uintptr(s), uintptr(level), uintptr(optname), + uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0) + if e != 0 { + return e + } + return +} + +// GetOriginalDST retrieves the original destination address from +// NATed connection. Currently, only Linux iptables using DNAT/REDIRECT +// is supported. For other operating systems, this will just return +// conn.LocalAddr(). +// +// Note that this function only works when nf_conntrack_ipv4 and/or +// nf_conntrack_ipv6 is loaded in the kernel. +func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) { + f, err := conn.File() + if err != nil { + return nil, err + } + defer f.Close() + + fd := int(f.Fd()) + // revert to non-blocking mode. + // see http://stackoverflow.com/a/28968431/1493661 + if err = syscall.SetNonblock(fd, true); err != nil { + return nil, os.NewSyscallError("setnonblock", err) + } + + v6 := conn.LocalAddr().(*net.TCPAddr).IP.To4() == nil + if v6 { + var addr syscall.RawSockaddrInet6 + var len uint32 + len = uint32(unsafe.Sizeof(addr)) + err = getsockopt(fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, + unsafe.Pointer(&addr), &len) + if err != nil { + return nil, os.NewSyscallError("getsockopt", err) + } + ip := make([]byte, 16) + for i, b := range addr.Addr { + ip[i] = b + } + pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) + return &net.TCPAddr{ + IP: ip, + Port: int(pb[0])*256 + int(pb[1]), + }, nil + } + + // IPv4 + var addr syscall.RawSockaddrInet4 + var len uint32 + len = uint32(unsafe.Sizeof(addr)) + err = getsockopt(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, + unsafe.Pointer(&addr), &len) + if err != nil { + return nil, os.NewSyscallError("getsockopt", err) + } + ip := make([]byte, 4) + for i, b := range addr.Addr { + ip[i] = b + } + pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) + return &net.TCPAddr{ + IP: ip, + Port: int(pb[0])*256 + int(pb[1]), + }, nil +} diff --git a/original_dst_linux_test.go b/original_dst_linux_test.go new file mode 100644 index 0000000..5afd2ec --- /dev/null +++ b/original_dst_linux_test.go @@ -0,0 +1,31 @@ +// +build linux + +package transocks + +import ( + "net" + "testing" +) + +func TestGetOriginalDST(t *testing.T) { + t.Skip() + + l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 1081}) + if err != nil { + t.Fatal(err) + } + defer l.Close() + + c, err := l.Accept() + if err != nil { + t.Fatal(err) + } + defer c.Close() + + orig_addr, err := GetOriginalDST(c.(*net.TCPConn)) + if err != nil { + t.Fatal(err) + } + + t.Log(orig_addr.String()) +} diff --git a/original_dst_stub.go b/original_dst_stub.go new file mode 100644 index 0000000..36e1eee --- /dev/null +++ b/original_dst_stub.go @@ -0,0 +1,16 @@ +// +build !linux + +package transocks + +import "net" + +// GetOriginalDST retrieves the original destination address from +// NATed connection. Currently, only Linux iptables using DNAT/REDIRECT +// is supported. For other operating systems, this will just return +// conn.LocalAddr(). +// +// Note that this function only works when nf_conntrack_ipv4 and/or +// nf_conntrack_ipv6 is loaded in the kernel. +func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) { + return conn.LocalAddr().(*net.TCPAddr), nil +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..0631b92 --- /dev/null +++ b/server.go @@ -0,0 +1,124 @@ +package transocks + +import ( + "io" + "net" + "time" + + "github.com/cybozu-go/log" + "golang.org/x/net/proxy" +) + +var ( + defaultDialer = &net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 60 * time.Second, + } +) + +// Server provides transparent proxy server functions. +type Server struct { + config *Config + dialer proxy.Dialer + listener net.Listener +} + +// NewServer creates Server. +// If c is not valid, this returns non-nil error. +func NewServer(c *Config) (*Server, error) { + if err := c.validate(); err != nil { + return nil, err + } + + dialer := defaultDialer + if c.Dialer != nil { + dialer = c.Dialer + } + proxy_dialer, 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 + } + + return &Server{c, proxy_dialer, l}, 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(), nil) + 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(err.Error(), nil) + break + } + } + + if log.Enabled(log.LvDebug) { + log.Debug("closing proxy connection", map[string]interface{}{ + "_dst": addr, + }) + } +} + +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() + } + if tsrc, ok := src.(*net.TCPConn); ok { + tsrc.CloseRead() + } + ch <- err +}