Repository: cybozu-go/transocks Branch: master Commit: 2198aaeb4d1a Files: 20 Total size: 24.3 KB Directory structure: gitextract_fyfbg3on/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── DESIGN.md ├── LICENSE ├── README.md ├── RELEASE.md ├── cmd/ │ └── transocks/ │ ├── main.go │ └── sample.toml ├── config.go ├── defs_linux.go ├── go.mod ├── http_tunnel.go ├── http_tunnel_test.go ├── original_dst_linux.go ├── original_dst_linux_test.go ├── original_dst_stub.go └── server.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: build: docker: - image: quay.io/cybozu/golang:1.11-bionic working_directory: /work steps: - checkout - run: test -z "$(gofmt -s -l . | grep -v '^vendor' | tee /dev/stderr)" - run: golint -set_exit_status . - run: go build ./... - run: go test -race -v ./... - run: go vet ./... workflows: version: 2 main: jobs: - build ================================================ FILE: .gitignore ================================================ # 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 # Ignore go.sum /go.sum ================================================ FILE: .golangci.yml ================================================ linters: enable: - dupl - goconst - gofmt - golint - typecheck - unparam ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. ## [Unreleased] ## [1.1.1] - 2019-03-16 ### Changed - Replace `syscall` with `golang.org/x/sys/unix`, contriubted by @otariidae (#14). ## [1.1.0] - 2018-11-13 ### Changed - Update `github.com/cybozu-go/cmd` to `github.com/cybozu-go/well` (#7, #9). - Replace TravisCI with CircleCI. ## [1.0.0] - 2016-09-01 ### Added - transocks now adopts [github.com/cybozu-go/well][well] framework. As a result, it implements [the common spec][spec] including graceful restart. ### Changed - The default configuration file path is now `/etc/transocks.toml`. - "listen" config option becomes optional. Default is "localhost:1081". - Configuration items for logging is changed. [well]: https://github.com/cybozu-go/well [spec]: https://github.com/cybozu-go/well/blob/master/README.md#specifications [Unreleased]: https://github.com/cybozu-go/transocks/compare/v1.1.1...HEAD [1.1.1]: https://github.com/cybozu-go/transocks/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/cybozu-go/transocks/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/cybozu-go/transocks/compare/v0.1...v1.0.0 ================================================ FILE: CONTRIBUTORS.md ================================================ transocks contributors ====================== * [@ayatk](https://github.com/ayatk) * [@otariidae](https://github.com/otariidae) ================================================ FILE: DESIGN.md ================================================ 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/ ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ [![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] [![CircleCI](https://circleci.com/gh/cybozu-go/coil.svg?style=svg)](https://circleci.com/gh/cybozu-go/transocks) [![Go Report Card](https://goreportcard.com/badge/github.com/cybozu-go/transocks)](https://goreportcard.com/report/github.com/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. 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 `/etc/transocks.toml`. In addition, transocks implements [the common spec](https://github.com/cybozu-go/cmd#specifications) from [`cybozu-go/cmd`](https://github.com/cybozu-go/cmd). transocks does not have *daemon* mode. Use systemd to run it as a background service. Configuration file format ------------------------- `transocks.toml` is a [TOML][] file. `proxy_url` is mandatory. Other items are optional. ``` # listening address of transocks. listen = "localhost:1081" # default is "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] filename = "/path/to/file" # default to stderr level = "info" # critical", error, warning, info, debug format = "json" # plain, logfmt, json ``` 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 ``` Use *ip6tables* to redirect IPv6 connections. **NOTE:** If you are going to use transocks on Linux gateway to redirect transit traffic, you have to bind transocks on primary address of internal network interface because iptables REDIRECT action in PREROUTING chain changes packet destination IP to primary address of incoming interface. Library usage ------------- Read [the documentation][godoc]. License ------- [MIT](https://opensource.org/licenses/MIT) [releases]: https://github.com/cybozu-go/transocks/releases [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 ================================================ FILE: RELEASE.md ================================================ Release procedure ================= This document describes how to release a new version of coil. Versioning ---------- Follow [semantic versioning 2.0.0][semver] to choose the new version number. Prepare change log entries -------------------------- Add notable changes since the last release to [CHANGELOG.md](CHANGELOG.md). It should look like: ```markdown (snip) ## [Unreleased] ### Added - Implement ... (#35) ### Changed - Fix a bug in ... (#33) ### Removed - Deprecated `-option` is removed ... (#39) (snip) ``` Bump version ------------ 1. Determine a new version number. Let it write `$VERSION`. 1. Checkout `master` branch. 1. Edit `CHANGELOG.md` for the new version ([example][]). 1. Commit the change and add a git tag, then push them. ```console $ git commit -a -m "Bump version to $VERSION" $ git tag v$VERSION $ git push origin master --tags ``` Publish GitHub release page --------------------------- Go to https://github.com/cybozu-go/coil/releases and edit the tag. Finally, press `Publish release` button. [semver]: https://semver.org/spec/v2.0.0.html [example]: https://github.com/cybozu-go/etcdpasswd/commit/77d95384ac6c97e7f48281eaf23cb94f68867f79 ================================================ FILE: cmd/transocks/main.go ================================================ package main import ( "flag" "fmt" "net" "net/url" "github.com/BurntSushi/toml" "github.com/cybozu-go/log" "github.com/cybozu-go/transocks" "github.com/cybozu-go/well" ) type tomlConfig struct { Listen string `toml:"listen"` ProxyURL string `toml:"proxy_url"` Log well.LogConfig `toml:"log"` } const ( defaultAddr = "localhost:1081" ) var ( configFile = flag.String("f", "/etc/transocks.toml", "TOML configuration file path") ) func loadConfig() (*transocks.Config, error) { tc := &tomlConfig{ Listen: defaultAddr, } 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.Addr = tc.Listen u, err := url.Parse(tc.ProxyURL) if err != nil { return nil, err } c.ProxyURL = u err = tc.Log.Apply() if err != nil { return nil, err } 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 = well.Wait() if err != nil && !well.IsSignaled(err) { log.ErrorExit(err) } } func main() { flag.Parse() c, err := loadConfig() if err != nil { log.ErrorExit(err) } g := &well.Graceful{ Listen: func() ([]net.Listener, error) { return transocks.Listeners(c) }, Serve: func(lns []net.Listener) { serve(lns, c) }, } g.Run() err = well.Wait() if err != nil && !well.IsSignaled(err) { log.ErrorExit(err) } } ================================================ FILE: cmd/transocks/sample.toml ================================================ # 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" filename = "/var/log/transocks.log" ================================================ FILE: config.go ================================================ package transocks import ( "errors" "fmt" "net" "net/url" "time" "github.com/cybozu-go/log" "github.com/cybozu-go/well" ) const ( 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 { // Addr is the listening address. Addr 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 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 well.Environment on which the server runs. // If nil, the server will run on the global environment. Env *well.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 c.ProxyURL == nil { return errors.New("ProxyURL is nil") } if c.Mode != ModeNAT { return fmt.Errorf("Unknown mode: %s", c.Mode) } return nil } ================================================ FILE: defs_linux.go ================================================ // Code generated by hand. DO NOT EDIT. // +build linux package transocks const ( // 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 ) ================================================ FILE: go.mod ================================================ module github.com/cybozu-go/transocks require ( github.com/BurntSushi/toml v0.3.1 github.com/cybozu-go/log v1.5.0 github.com/cybozu-go/netutil v1.2.0 github.com/cybozu-go/well v1.8.1 golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 ) ================================================ FILE: http_tunnel.go ================================================ // 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) { var header http.Header 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": {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 } resp.Body.Close() if resp.StatusCode != 200 { c.Close() return nil, fmt.Errorf("proxy returns %s", resp.Status) } return c, nil } ================================================ FILE: http_tunnel_test.go ================================================ 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) } defer conn.Close() 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) } ================================================ FILE: original_dst_linux.go ================================================ // +build linux package transocks import ( syscall "golang.org/x/sys/unix" "net" "os" "unsafe" ) 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 } ================================================ FILE: original_dst_linux_test.go ================================================ // +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() origAddr, err := GetOriginalDST(c.(*net.TCPConn)) if err != nil { t.Fatal(err) } t.Log(origAddr.String()) } ================================================ FILE: original_dst_stub.go ================================================ // +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 } ================================================ FILE: server.go ================================================ package transocks import ( "context" "io" "net" "sync" "time" "github.com/cybozu-go/log" "github.com/cybozu-go/netutil" "github.com/cybozu-go/well" "golang.org/x/net/proxy" ) 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 { well.Server mode Mode logger *log.Logger dialer proxy.Dialer pool sync.Pool } // 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 := c.Dialer if dialer == nil { dialer = &net.Dialer{ KeepAlive: keepAliveTimeout, DualStack: true, } } pdialer, err := proxy.FromURL(c.ProxyURL, dialer) if err != nil { return nil, err } logger := c.Logger if logger == nil { logger = log.DefaultLogger() } s := &Server{ Server: well.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 } 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 } fields := well.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 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() s.logger.Info("proxy starts", fields) // do proxy st := time.Now() env := well.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 = well.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 } s.logger.Info("proxy ends", fields) }