diff --git a/.gitignore b/.gitignore index a1338d6..f32a420 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +# custom +glider +doc/ diff --git a/README.md b/README.md index d35e75f..d46cf24 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,113 @@ # glider glider is a forward proxy with several protocols support. + +## Install + + go get -u github.com/nadoo/glider + +## Build + + cd $GOPATH/src/github.com/nadoo/glider + go build + +## Usage +```bash +glider v0.1 usage: + -checksite string + proxy check address (default "www.apple.com:443") + -duration int + proxy check duration(seconds) (default 30) + -f value + forward url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT[,SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT] + -l value + listen url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT + -s string + forward strategy, default: rr (default "rr") + -v verbose mode + +Available Schemas: + mixed: serve as a http/socks5 proxy on the same port. (default) + ss: ss proxy + socks5: socks5 proxy + http: http proxy + redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules) + tcptun: a simple tcp tunnel + dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp) + +Available schemas for different modes: + listen: mixed ss socks5 http redir tcptun dnstun + forward: ss socks5 http + +Available methods for ss: + AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 + +Available forward strategies: + rr: Round Robin mode + ha: High Availability mode + +Examples: + glider -l :8443 -v + -listen on :8443, serve as http/socks5 proxy on the same port. + + glider -l ss://AEAD_CHACHA20_POLY1305:pass@:8443 + -listen on 0.0.0.0:8443 as a shadowsocks server. + + glider -l socks5://:1080 -v + -listen on :1080 as a socks5 proxy server, in verbose mode. + + glider -l http://:8080 -f socks5://127.0.0.1:1080 -v + -listen on :8080 as a http proxy server, forward all requests via socks5 server. + + glider -l redir://:1081 -f ss://method:pass@1.1.1.1:443 + -listen on :1081 as a transparent redirect server, forward all requests via remote ss server. + + glider -l tcptun://:80=2.2.2.2:80 -f ss://method:pass@1.1.1.1:443 + -listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server. + + glider -l socks5://:1080 -l http://:8080 -f ss://method:pass@1.1.1.1:443 + -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server. + + glider -l redir://:1081 -l dnstun://:53=8.8.8.8:53 -f ss://method:pass@server1:port1,ss://method:pass@server2:port2 + -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2. + + glider -l socks5://:1080 -f ss://method:pass@server1:port1 -f ss://method:pass@server2:port2 -s rr + -listen on :1080 as socks5 server, forward requests via server1 and server2 in roundrbin mode. + + glider -l mixed://:8443 -f ss://method:pass@server1:port1 + -listen on :8443, serve as http/socks5 proxy, forward requests via server1. + + glider -l mixed://:8443?http=1.1.1.1:80 -f ss://method:pass@server1:port1 + -listen on :8443, serve as socks5 proxy, and forward all HTTP requests to 1.1.1.1:80. +``` + +## Service +```bash +cd /etc/systemd/system/ +vim glider.service +``` + +```bash +[Unit] +Description=glider +After=network.target + +[Service] +Type=simple +ExecStartPre=/bin/mkdir -p /run/glider +ExecStartPre=/bin/chown nobody:nobody /run/glider +ExecStart=/opt/glider/glider -l redir://:7070 -l dnstun://:5353=8.8.8.8:53 -f ss://AEAD_CHACHA20_POLY1305:pass@yourhost:8443 +ExecReload=/bin/kill -HUP $MAINPID +ExecStop=/bin/kill -INT $MAINPID +Restart=always +User=nobody +Group=nobody +UMask=0027 + +[Install] +WantedBy=multi-user.target +``` + +```bash +systemctl enable glider.service +systemctl start glider.service +``` diff --git a/conn.go b/conn.go new file mode 100644 index 0000000..944e6ef --- /dev/null +++ b/conn.go @@ -0,0 +1,54 @@ +package main + +import ( + "bufio" + "io" + "net" + "time" +) + +type conn struct { + r *bufio.Reader + net.Conn +} + +func newConn(c net.Conn) conn { + return conn{bufio.NewReader(c), c} +} + +func newConnSize(c net.Conn, n int) conn { + return conn{bufio.NewReaderSize(c, n), c} +} + +func (c conn) Peek(n int) ([]byte, error) { + return c.r.Peek(n) +} + +func (c conn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + +func relay(left, right net.Conn) (int64, int64, error) { + type res struct { + N int64 + Err error + } + ch := make(chan res) + + go func() { + n, err := io.Copy(right, left) + right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right + left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left + ch <- res{n, err} + }() + + n, err := io.Copy(left, right) + right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right + left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left + rs := <-ch + + if err == nil { + err = rs.Err + } + return n, rs.N, err +} diff --git a/direct.go b/direct.go new file mode 100644 index 0000000..ceda39e --- /dev/null +++ b/direct.go @@ -0,0 +1,24 @@ +package main + +import "net" + +// direct proxy +type direct struct { + Proxy +} + +// Direct proxy +var Direct = &direct{Proxy: &proxy{addr: "127.0.0.1"}} + +// Direct proxy always enabled +func (d *direct) Enabled() bool { + return true +} + +func (d *direct) Dial(network, addr string) (net.Conn, error) { + c, err := net.Dial(network, addr) + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + return c, err +} diff --git a/dnstun.go b/dnstun.go new file mode 100644 index 0000000..f966878 --- /dev/null +++ b/dnstun.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/binary" + "io/ioutil" + "net" +) + +type dnstun struct { + Proxy + addr string + raddr string +} + +// DNSTunProxy returns a dns forwarder. client -> dns.udp -> glider -> forwarder -> remote dns addr +func DNSTunProxy(addr, raddr string, upProxy Proxy) (Proxy, error) { + s := &dnstun{ + Proxy: upProxy, + addr: addr, + raddr: raddr, + } + + return s, nil +} + +// ListenAndServe redirected requests as a server. +func (s *dnstun) ListenAndServe() { + l, err := net.ListenPacket("udp", s.addr) + if err != nil { + logf("failed to listen on %s: %v", s.addr, err) + return + } + + logf("listening UDP on %s", s.addr) + + for { + defer l.Close() + + data := make([]byte, 512) + n, clientAddr, err := l.ReadFrom(data) + if err != nil { + logf("DNS local read error: %v", err) + continue + } + + data = data[:n] + go func() { + rc, err := s.GetProxy().Dial("tcp", s.raddr) + if err != nil { + logf("failed to connect to server %v: %v", s.raddr, err) + return + } + defer rc.Close() + + logf("proxy-dnstun %s[dns.udp] <-> %s[dns.tcp]", clientAddr.String(), s.raddr) + + // 2 bytes length after tcp header, before dns message + length := make([]byte, 2) + binary.BigEndian.PutUint16(length, uint16(len(data))) + rc.Write(length) + rc.Write(data) + + buf, err := ioutil.ReadAll(rc) + if err != nil { + logf("error in ioutil.ReadAll: %s\n", err) + return + } + + // length is not needed in udp dns response. (2 bytes) + // SEE RFC1035, section 4.2.2 TCP: The message is prefixed with a two byte length field which gives the message length, excluding the two byte length field. + if len(buf) > 2 { + msg := buf[2:] + _, err = l.WriteTo(msg, clientAddr) + if err != nil { + logf("error in local write: %s\n", err) + } + } + + }() + } +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..d7134b0 --- /dev/null +++ b/http.go @@ -0,0 +1,250 @@ +// http proxy +// NOTE: never keep-alive so the implementation can be much easier. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/textproto" + "net/url" + "strings" + "time" +) + +// httpproxy +type httpproxy struct { + Proxy + addr string +} + +// HTTPProxy returns a http proxy. +func HTTPProxy(addr string, upProxy Proxy) (Proxy, error) { + s := &httpproxy{ + Proxy: upProxy, + addr: addr, + } + + return s, nil +} + +// ListenAndServe . +func (s *httpproxy) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + logf("failed to listen on %s: %v", s.addr, err) + return + } + + logf("listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve . +func (s *httpproxy) Serve(c net.Conn) { + + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + reqR := bufio.NewReader(c) + reqTP := textproto.NewReader(reqR) + method, requestURI, proto, ok := parseFirstLine(reqTP) + if !ok { + return + } + + if method == "CONNECT" { + s.servHTTPS(method, requestURI, proto, c) + return + } + + reqHeader, err := reqTP.ReadMIMEHeader() + if err != nil { + logf("read header error:%s", err) + return + } + cleanHeaders(reqHeader) + // tell the remote server not to keep alive + reqHeader.Set("Connection", "close") + + url, err := url.ParseRequestURI(requestURI) + if err != nil { + logf("parse request url error: %s", err) + return + } + + var tgt = url.Host + if !strings.Contains(url.Host, ":") { + tgt += ":80" + } + + rc, err := s.GetProxy().Dial("tcp", tgt) + if err != nil { + fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", proto) + logf("failed to dial: %v", err) + return + } + defer rc.Close() + + // GET http://example.com/a/index.htm HTTP/1.1 --> + // GET /a/index.htm HTTP/1.1 + url.Scheme = "" + url.Host = "" + uri := url.String() + + var reqBuf bytes.Buffer + writeFirstLine(method, uri, proto, &reqBuf) + writeHeaders(reqHeader, &reqBuf) + + // send request to remote server + rc.Write(reqBuf.Bytes()) + + // copy the left request bytes to remote server. eg. length specificed or chunked body. + go func() { + io.Copy(rc, reqR) + rc.SetDeadline(time.Now()) + c.SetDeadline(time.Now()) + }() + + respR := bufio.NewReader(rc) + respTP := textproto.NewReader(respR) + proto, code, status, ok := parseFirstLine(respTP) + if !ok { + return + } + + respHeader, err := respTP.ReadMIMEHeader() + if err != nil { + logf("read header error:%s", err) + return + } + + respHeader.Set("Proxy-Connection", "close") + respHeader.Set("Connection", "close") + + var respBuf bytes.Buffer + writeFirstLine(proto, code, status, &respBuf) + writeHeaders(respHeader, &respBuf) + + logf("proxy-http %s <-> %s", c.RemoteAddr(), tgt) + c.Write(respBuf.Bytes()) + + io.Copy(c, respR) + +} + +func (s *httpproxy) servHTTPS(method, requestURI, proto string, c net.Conn) { + rc, err := s.GetProxy().Dial("tcp", requestURI) + if err != nil { + c.Write([]byte(proto)) + c.Write([]byte(" 502 ERROR\r\n\r\n")) + logf("failed to dial: %v", err) + return + } + c.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) + + logf("proxy-https %s <-> %s", c.RemoteAddr(), requestURI) + + _, _, err = relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + logf("relay error: %v", err) + } +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *httpproxy) Dial(network, addr string) (net.Conn, error) { + c, err := s.GetProxy().Dial("tcp", s.addr) + if err != nil { + logf("dial to %s error: %s", s.addr, err) + return nil, err + } + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + c.Write([]byte("CONNECT " + addr + " HTTP/1.0\r\n")) + // c.Write([]byte("Proxy-Connection: Keep-Alive\r\n")) + + var b [1024]byte + n, err := c.Read(b[:]) + if bytes.Contains(b[:n], []byte("200")) { + return c, err + } + + return nil, err +} + +// parseFirstLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts. +func parseFirstLine(tp *textproto.Reader) (r1, r2, r3 string, ok bool) { + line, err := tp.ReadLine() + // logf("first line: %s", line) + if err != nil { + logf("read request line error:%s", err) + return + } + + s1 := strings.Index(line, " ") + s2 := strings.Index(line[s1+1:], " ") + if s1 < 0 || s2 < 0 { + return + } + s2 += s1 + 1 + return line[:s1], line[s1+1 : s2], line[s2+1:], true +} + +func cleanHeaders(header textproto.MIMEHeader) { + header.Del("Proxy-Connection") + header.Del("Connection") + header.Del("Keep-Alive") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") +} + +func writeFirstLine(s1, s2, s3 string, buf *bytes.Buffer) { + buf.Write([]byte(s1)) + buf.Write([]byte(" ")) + buf.Write([]byte(s2)) + buf.Write([]byte(" ")) + buf.Write([]byte(s3)) + buf.Write([]byte("\r\n")) +} + +func writeHeaders(header textproto.MIMEHeader, buf *bytes.Buffer) { + for key, values := range header { + buf.Write([]byte(key)) + buf.Write([]byte(": ")) + for k, v := range values { + buf.Write([]byte(v)) + if k > 0 { + buf.Write([]byte(" ")) + } + } + buf.Write([]byte("\r\n")) + } + + //header ended + buf.Write([]byte("\r\n")) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d9071d8 --- /dev/null +++ b/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" +) + +const version = "0.1" + +var config struct { + Verbose bool + Strategy string + CheckSite string + CheckDuration int +} + +func logf(f string, v ...interface{}) { + if config.Verbose { + log.Printf(f, v...) + } +} + +func usage() { + app := os.Args[0] + fmt.Fprintf(os.Stderr, "%s v%s usage:\n", app, version) + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available Schemas:\n") + fmt.Fprintf(os.Stderr, " mixed: serve as a http/socks5 proxy on the same port. (default)\n") + fmt.Fprintf(os.Stderr, " ss: ss proxy\n") + fmt.Fprintf(os.Stderr, " socks5: socks5 proxy\n") + fmt.Fprintf(os.Stderr, " http: http proxy\n") + fmt.Fprintf(os.Stderr, " redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)\n") + fmt.Fprintf(os.Stderr, " tcptun: a simple tcp tunnel\n") + fmt.Fprintf(os.Stderr, " dnstun: listen on udp port and forward all dns requests to remote dns server via forwarders(tcp)\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available schemas for different modes:\n") + fmt.Fprintf(os.Stderr, " listen: mixed ss socks5 http redir tcptun dnstun\n") + fmt.Fprintf(os.Stderr, " forward: ss socks5 http\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available methods for ss:\n") + fmt.Fprintf(os.Stderr, " "+ListCipher()) + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available forward strategies:\n") + fmt.Fprintf(os.Stderr, " rr: Round Robin mode\n") + fmt.Fprintf(os.Stderr, " ha: High Availability mode\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Examples:\n") + fmt.Fprintf(os.Stderr, " "+app+" -l :8443 -v\n") + fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy on the same port.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l ss://AEAD_CHACHA20_POLY1305:pass@:8443\n") + fmt.Fprintf(os.Stderr, " -listen on 0.0.0.0:8443 as a shadowsocks server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l socks5://:1080 -v\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as a socks5 proxy server, in verbose mode.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l http://:8080 -f socks5://127.0.0.1:1080 -v\n") + fmt.Fprintf(os.Stderr, " -listen on :8080 as a http proxy server, forward all requests via socks5 server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l redir://:1081 -f ss://method:pass@1.1.1.1:443\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l tcptun://:80=2.2.2.2:80 -f ss://method:pass@1.1.1.1:443\n") + fmt.Fprintf(os.Stderr, " -listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l socks5://:1080 -l http://:8080 -f ss://method:pass@1.1.1.1:443\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l redir://:1081 -l dnstun://:53=8.8.8.8:53 -f ss://method:pass@server1:port1,ss://method:pass@server2:port2\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l socks5://:1080 -f ss://method:pass@server1:port1 -f ss://method:pass@server2:port2 -s rr\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, forward requests via server1 and server2 in roundrbin mode.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l mixed://:8443 -f ss://method:pass@server1:port1\n") + fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy, forward requests via server1.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -l mixed://:8443?http=1.1.1.1:80 -f ss://method:pass@server1:port1\n") + fmt.Fprintf(os.Stderr, " -listen on :8443, serve as socks5 proxy, and forward all HTTP requests to 1.1.1.1:80.\n") + fmt.Fprintf(os.Stderr, "\n") +} + +type arrFlags []string + +// implement flag.Value interface +func (i *arrFlags) String() string { + return "" +} + +// implement flag.Value interface +func (i *arrFlags) Set(value string) error { + *i = append(*i, value) + return nil +} + +func main() { + + var flags struct { + Listen arrFlags + Forward arrFlags + } + + flag.BoolVar(&config.Verbose, "v", false, "verbose mode") + flag.StringVar(&config.Strategy, "s", "rr", "forward strategy, default: rr") + flag.StringVar(&config.CheckSite, "checksite", "www.apple.com:443", "proxy check address") + flag.IntVar(&config.CheckDuration, "duration", 30, "proxy check duration(seconds)") + + flag.Var(&flags.Listen, "l", "listen url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT") + flag.Var(&flags.Forward, "f", "forward url, format: SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT[,SCHEMA://[USER|METHOD:PASSWORD@][HOST]:PORT]") + + flag.Usage = usage + flag.Parse() + + if len(flags.Listen) == 0 { + flag.Usage() + fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n") + return + } + + var forwarders []Proxy + if len(flags.Forward) > 0 { + var err error + for _, chain := range flags.Forward { + var forward Proxy + for _, url := range strings.Split(chain, ",") { + forward, err = ProxyFromURL(url, forward) + if err != nil { + log.Fatal(err) + } + } + forwarders = append(forwarders, forward) + } + } + + for _, forward := range forwarders { + go check(forward, config.CheckSite, config.CheckDuration) + } + + if len(flags.Listen) > 0 { + for _, listen := range flags.Listen { + local, err := ProxyFromURL(listen, forwarders...) + if err != nil { + log.Fatal(err) + } + + go local.ListenAndServe() + } + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/mixed.go b/mixed.go new file mode 100644 index 0000000..a9a739c --- /dev/null +++ b/mixed.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "net" +) + +// https://www.ietf.org/rfc/rfc2616.txt, http methods must be uppercase. +var httpMethods = [][]byte{ + []byte("GET"), + []byte("POST"), + []byte("PUT"), + []byte("DELETE"), + []byte("CONNECT"), + []byte("HEAD"), + []byte("OPTIONS"), + []byte("TRACE"), +} + +// mixedproxy +type mixedproxy struct { + Proxy + http Proxy + socks5 Proxy + ss Proxy +} + +// MixedProxy returns a http mixed proxy. +func MixedProxy(network, addr, user, pass string, upProxy Proxy) (Proxy, error) { + p := &mixedproxy{ + Proxy: upProxy, + } + + p.http, _ = HTTPProxy(addr, upProxy) + p.socks5, _ = SOCKS5Proxy(network, addr, user, pass, upProxy) + + if user != "" && pass != "" { + p.ss, _ = SSProxy(user, pass, upProxy) + } + + return p, nil +} + +// mixedproxy . +func (p *mixedproxy) ListenAndServe() { + l, err := net.Listen("tcp", p.Addr()) + if err != nil { + logf("failed to listen on %s: %v", p.Addr(), err) + return + } + + logf("listening TCP on %s", p.Addr()) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + + go func() { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + c := newConn(c) + + if p.socks5 != nil { + head, err := c.Peek(1) + if err != nil { + logf("peek error: %s", err) + return + } + + // check socks5, client send socksversion: 5 as the first byte + if head[0] == socks5Version { + p.socks5.Serve(c) + return + } + } + + if p.http != nil { + head, err := c.Peek(8) + if err != nil { + logf("peek error: %s", err) + return + } + + for _, method := range httpMethods { + if bytes.HasPrefix(head, method) { + p.http.Serve(c) + return + } + } + } + + if p.ss != nil { + p.ss.Serve(c) + } + + }() + } +} diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..5c45b48 --- /dev/null +++ b/proxy.go @@ -0,0 +1,152 @@ +package main + +import ( + "errors" + "net" + "net/url" + "strings" + "time" +) + +// A Proxy means to establish a connection and relay it. +type Proxy interface { + // ListenAndServe as proxy server, use only in server mode. + ListenAndServe() + + // Serve as proxy server, use only in server mode. + Serve(c net.Conn) + + // Get address + Addr() string + + // Get current proxy + CurrentProxy() Proxy + + // Get a proxy according to the strategy + GetProxy() Proxy + + // Switch to the next proxy + NextProxy() Proxy + + // Get the status of proxy + Enabled() bool + + // Set the proxy status + SetEnable(enable bool) + + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// proxy +type proxy struct { + addr string + forward Proxy + enabled bool +} + +// newProxy . +func newProxy(addr string, forward Proxy) Proxy { + if forward == nil { + forward = Direct + } + + return &proxy{addr: addr, forward: forward, enabled: false} +} + +func (p *proxy) ListenAndServe() { logf("base proxy ListenAndServe") } +func (p *proxy) Serve(c net.Conn) { logf("base proxy Serve") } +func (p *proxy) CurrentProxy() Proxy { return p.forward } +func (p *proxy) GetProxy() Proxy { return p.forward } +func (p *proxy) NextProxy() Proxy { return p.forward } +func (p *proxy) Enabled() bool { return p.enabled } +func (p *proxy) SetEnable(enable bool) { p.enabled = enable } +func (p *proxy) Addr() string { return p.addr } + +func (p *proxy) Dial(network, addr string) (net.Conn, error) { + return p.forward.Dial(network, addr) +} + +// ProxyFromURL parses url and get a Proxy +// TODO: table +func ProxyFromURL(s string, forwarders ...Proxy) (Proxy, error) { + if !strings.Contains(s, "://") { + s = "mixed://" + s + } + + u, err := url.Parse(s) + if err != nil { + logf("parse err: %s", err) + return nil, err + } + + addr := u.Host + var user, pass string + if u.User != nil { + user = u.User.Username() + pass, _ = u.User.Password() + } + + var proxy Proxy + if forwarders == nil || len(forwarders) == 0 { + proxy = newProxy(addr, Direct) + } else if len(forwarders) == 1 { + proxy = newProxy(addr, forwarders[0]) + } else if len(forwarders) > 1 { + switch config.Strategy { + case "rr": + proxy = newRRProxy(addr, forwarders) + logf("forward to remote servers in round robin mode.") + case "ha": + proxy = newHAProxy(addr, forwarders) + logf("forward to remote servers in high availability mode.") + default: + logf("not supported forward mode '%s', just use the first forward server.", config.Strategy) + proxy = newProxy(addr, forwarders[0]) + } + } + + switch u.Scheme { + case "ss": + p, err := SSProxy(user, pass, proxy) + return p, err + case "socks5": + return SOCKS5Proxy("tcp", addr, user, pass, proxy) + case "redir": + return RedirProxy(addr, proxy) + case "tcptun": + d := strings.Split(addr, "=") + return TCPTunProxy(d[0], d[1], proxy) + case "dnstun": + d := strings.Split(addr, "=") + return DNSTunProxy(d[0], d[1], proxy) + case "http": + return HTTPProxy(addr, proxy) + case "mixed": + return MixedProxy("tcp", addr, user, pass, proxy) + } + + return nil, errors.New("unknown schema '" + u.Scheme + "'") +} + +// Check proxy +func check(p Proxy, target string, duration int) { + firstTime := true + for { + if !firstTime { + time.Sleep(time.Duration(duration) * time.Second) + } + firstTime = false + + c, err := p.Dial("tcp", target) + if err != nil { + logf("proxy %s check error: %s, set to disabled.", p.Addr(), err) + p.SetEnable(false) + continue + } + defer c.Close() + + p.SetEnable(true) + logf("proxy %s check ok.", p.Addr()) + } +} diff --git a/redir.go b/redir.go new file mode 100644 index 0000000..aad33ed --- /dev/null +++ b/redir.go @@ -0,0 +1,114 @@ +// +build !windows + +package main + +import ( + "errors" + "fmt" + "net" + "syscall" +) + +const SO_ORIGINAL_DST = 80 + +type redir struct { + Proxy + addr string +} + +// RedirProxy returns a redirect proxy. +func RedirProxy(addr string, upProxy Proxy) (Proxy, error) { + s := &redir{ + Proxy: upProxy, + addr: addr, + } + + return s, nil +} + +// ListenAndServe redirected requests as a server. +func (s *redir) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + logf("failed to listen on %s: %v", s.addr, err) + return + } + + logf("listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + + go func() { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + tgt, c, err := getOriginalDstAddr(c) + if err != nil { + logf("failed to get target address: %v", err) + return + } + + rc, err := s.GetProxy().Dial("tcp", tgt.String()) + if err != nil { + logf("failed to connect to target: %v", err) + return + } + defer rc.Close() + + logf("proxy-redir %s <-> %s", c.RemoteAddr(), tgt) + // go io.Copy(rc, c) + // io.Copy(c, rc) + + _, _, err = relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + logf("relay error: %v", err) + } + + }() + } +} + +func getOriginalDstAddr(conn net.Conn) (addr net.Addr, c *net.TCPConn, err error) { + defer conn.Close() + + fc, err := conn.(*net.TCPConn).File() + if err != nil { + return + } + defer fc.Close() + + mreq, err := syscall.GetsockoptIPv6Mreq(int(fc.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST) + if err != nil { + return + } + + // only ipv4 support + ip := net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7]) + port := uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3]) + addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", ip.String(), port)) + if err != nil { + return + } + + cc, err := net.FileConn(fc) + if err != nil { + return + } + + c, ok := cc.(*net.TCPConn) + if !ok { + err = errors.New("not a TCP connection") + } + return +} diff --git a/redir_win.go b/redir_win.go new file mode 100644 index 0000000..c6227b1 --- /dev/null +++ b/redir_win.go @@ -0,0 +1,17 @@ +// +build windows + +package main + +import "log" + +type redir struct{ Proxy } + +// RedirProxy returns a redirect proxy. +func RedirProxy(addr string, upProxy Proxy) (Proxy, error) { + return &redir{Proxy: upProxy}, nil +} + +// ListenAndServe redirected requests as a server. +func (s *redir) ListenAndServe() { + log.Fatal("redir not supported on windows") +} diff --git a/socks5.go b/socks5.go new file mode 100644 index 0000000..6043b43 --- /dev/null +++ b/socks5.go @@ -0,0 +1,436 @@ +// socks5 client: +// https://github.com/golang/net/tree/master/proxy +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// socks5 server: +// https://github.com/shadowsocks/go-shadowsocks2/tree/master/socks + +package main + +import ( + "errors" + "io" + "net" + "strconv" +) + +const socks5Version = 5 + +const ( + socks5AuthNone = 0 + socks5AuthPassword = 2 +) + +// SOCKS request commands as defined in RFC 1928 section 4. +const ( + socks5Connect = 1 + socks5Bind = 2 + socks5UDPAssociate = 3 +) + +// SOCKS address types as defined in RFC 1928 section 5. +const ( + socks5IP4 = 1 + socks5Domain = 3 + socks5IP6 = 4 +) + +// MaxAddrLen is the maximum size of SOCKS address in bytes. +const MaxAddrLen = 1 + 1 + 255 + 2 + +// Addr represents a SOCKS address as defined in RFC 1928 section 5. +type Addr []byte + +var socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +type socks5 struct { + Proxy + network, addr string + user, password string +} + +// SOCKS5Proxy returns a Proxy that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928. +func SOCKS5Proxy(network, addr, user, pass string, upProxy Proxy) (Proxy, error) { + s := &socks5{ + Proxy: upProxy, + addr: addr, + user: user, + password: pass, + } + + return s, nil +} + +// ListenAndServe connects to the address addr on the network net via the SOCKS5 proxy. +func (s *socks5) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + logf("failed to listen on %s: %v", s.addr, err) + return + } + + logf("listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +func (s *socks5) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + tgt, err := s.handshake(c) + if err != nil { + logf("failed to get target address: %v", err) + return + } + + rc, err := s.GetProxy().Dial("tcp", tgt.String()) + if err != nil { + logf("failed to connect to target: %v", err) + return + } + defer rc.Close() + + logf("proxy-socks5 %s <-> %s", c.RemoteAddr(), tgt) + + _, _, err = relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + logf("relay error: %v", err) + } +} + +// Dial connects to the address addr on the network net via the SOCKS5 proxy. +func (s *socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + c, err := s.GetProxy().Dial(s.network, s.addr) + if err != nil { + logf("dial to %s error: %s", s.addr, err) + return nil, err + } + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + if err := s.connect(c, addr); err != nil { + c.Close() + return nil, err + } + + return c, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, socks5AuthNone, socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + if buf[1] == socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, socks5Version, socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, socks5IP4) + ip = ip4 + } else { + buf = append(buf, socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination hostname too long: " + host) + } + buf = append(buf, socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(socks5Errors) { + failure = socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case socks5IP4: + bytesToDiscard = net.IPv4len + case socks5IP6: + bytesToDiscard = net.IPv6len + case socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} + +// Handshake fast-tracks SOCKS initialization to get target address to connect. +func (s *socks5) handshake(rw io.ReadWriter) (Addr, error) { + // Read RFC 1928 for request and reply structure and sizes. + buf := make([]byte, MaxAddrLen) + // read VER, NMETHODS, METHODS + if _, err := io.ReadFull(rw, buf[:2]); err != nil { + return nil, err + } + nmethods := buf[1] + if _, err := io.ReadFull(rw, buf[:nmethods]); err != nil { + return nil, err + } + // write VER METHOD + if _, err := rw.Write([]byte{5, 0}); err != nil { + return nil, err + } + // read VER CMD RSV ATYP DST.ADDR DST.PORT + if _, err := io.ReadFull(rw, buf[:3]); err != nil { + return nil, err + } + if buf[1] != socks5Connect { + return nil, errors.New(socks5Errors[7]) + } + addr, err := readAddr(rw, buf) + if err != nil { + return nil, err + } + // write VER REP RSV ATYP BND.ADDR BND.PORT + _, err = rw.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) + return addr, err +} + +// String serializes SOCKS address a to string form. +func (a Addr) String() string { + var host, port string + + switch a[0] { // address type + case socks5Domain: + host = string(a[2 : 2+int(a[1])]) + port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1])) + case socks5IP4: + host = net.IP(a[1 : 1+net.IPv4len]).String() + port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1])) + case socks5IP6: + host = net.IP(a[1 : 1+net.IPv6len]).String() + port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1])) + } + + return net.JoinHostPort(host, port) +} + +func readAddr(r io.Reader, b []byte) (Addr, error) { + if len(b) < MaxAddrLen { + return nil, io.ErrShortBuffer + } + _, err := io.ReadFull(r, b[:1]) // read 1st byte for address type + if err != nil { + return nil, err + } + + switch b[0] { + case socks5Domain: + _, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length + if err != nil { + return nil, err + } + _, err = io.ReadFull(r, b[2:2+int(b[1])+2]) + return b[:1+1+int(b[1])+2], err + case socks5IP4: + _, err = io.ReadFull(r, b[1:1+net.IPv4len+2]) + return b[:1+net.IPv4len+2], err + case socks5IP6: + _, err = io.ReadFull(r, b[1:1+net.IPv6len+2]) + return b[:1+net.IPv6len+2], err + } + + return nil, errors.New(socks5Errors[8]) +} + +// ReadAddr reads just enough bytes from r to get a valid Addr. +func ReadAddr(r io.Reader) (Addr, error) { + return readAddr(r, make([]byte, MaxAddrLen)) +} + +// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed. +func SplitAddr(b []byte) Addr { + addrLen := 1 + if len(b) < addrLen { + return nil + } + + switch b[0] { + case socks5Domain: + if len(b) < 2 { + return nil + } + addrLen = 1 + 1 + int(b[1]) + 2 + case socks5IP4: + addrLen = 1 + net.IPv4len + 2 + case socks5IP6: + addrLen = 1 + net.IPv6len + 2 + default: + return nil + + } + + if len(b) < addrLen { + return nil + } + + return b[:addrLen] +} + +// ParseAddr parses the address in string s. Returns nil if failed. +func ParseAddr(s string) Addr { + var addr Addr + host, port, err := net.SplitHostPort(s) + if err != nil { + return nil + } + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + addr = make([]byte, 1+net.IPv4len+2) + addr[0] = socks5IP4 + copy(addr[1:], ip4) + } else { + addr = make([]byte, 1+net.IPv6len+2) + addr[0] = socks5IP6 + copy(addr[1:], ip) + } + } else { + if len(host) > 255 { + return nil + } + addr = make([]byte, 1+1+len(host)+2) + addr[0] = socks5Domain + addr[1] = byte(len(host)) + copy(addr[2:], host) + } + + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil + } + + addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum) + + return addr +} diff --git a/ss.go b/ss.go new file mode 100644 index 0000000..6d24c3b --- /dev/null +++ b/ss.go @@ -0,0 +1,116 @@ +package main + +import ( + "errors" + "log" + "net" + "strings" + + "github.com/shadowsocks/go-shadowsocks2/core" +) + +// Shadowsocks +type shadowsocks struct { + Proxy + core.StreamConnCipher +} + +// SSProxy returns a shadowsocks proxy. +func SSProxy(method, pass string, upProxy Proxy) (Proxy, error) { + ciph, err := core.PickCipher(method, nil, pass) + if err != nil { + log.Fatal(err) + } + + s := &shadowsocks{ + Proxy: upProxy, + StreamConnCipher: ciph, + } + + return s, nil +} + +// ListenAndServe shadowsocks requests as a server. +func (s *shadowsocks) ListenAndServe() { + l, err := net.Listen("tcp", s.Addr()) + if err != nil { + logf("failed to listen on %s: %v", s.Addr(), err) + return + } + + logf("listening TCP on %s", s.Addr()) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + go s.Serve(c) + } +} + +func (s *shadowsocks) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + c = s.StreamConnCipher.StreamConn(c) + + tgt, err := ReadAddr(c) + if err != nil { + logf("failed to get target address: %v", err) + return + } + + rc, err := s.GetProxy().Dial("tcp", tgt.String()) + if err != nil { + logf("failed to connect to target: %v", err) + return + } + defer rc.Close() + + logf("proxy-ss %s <-> %s", c.RemoteAddr(), tgt) + + _, _, err = relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + logf("relay error: %v", err) + } + +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *shadowsocks) Dial(network, addr string) (net.Conn, error) { + target := ParseAddr(addr) + if target == nil { + return nil, errors.New("Unable to parse address: " + addr) + } + + c, err := s.GetProxy().Dial("tcp", s.Addr()) + if err != nil { + logf("dial to %s error: %s", s.Addr(), err) + return nil, err + } + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + c = s.StreamConn(c) + if _, err = c.Write(target); err != nil { + c.Close() + return nil, err + } + + return c, err +} + +// ListCipher . +func ListCipher() string { + return strings.Join(core.ListCipher(), " ") +} diff --git a/strategy.go b/strategy.go new file mode 100644 index 0000000..5dc1706 --- /dev/null +++ b/strategy.go @@ -0,0 +1,82 @@ +package main + +import ( + "net" + "time" +) + +// strategyProxy +type strategyProxy struct { + addr string + forwarders []Proxy + idx int +} + +// newStrategyProxy . +func newStrategyProxy(addr string, forwarders []Proxy) Proxy { + if len(forwarders) == 0 { + return Direct + } else if len(forwarders) == 1 { + return newProxy(addr, forwarders[0]) + } + + return &strategyProxy{addr: addr, forwarders: forwarders} +} + +func (p *strategyProxy) ListenAndServe() {} +func (p *strategyProxy) Serve(c net.Conn) {} +func (p *strategyProxy) CurrentProxy() Proxy { return p.forwarders[p.idx] } +func (p *strategyProxy) GetProxy() Proxy { return p.NextProxy() } + +func (p *strategyProxy) NextProxy() Proxy { + n := len(p.forwarders) + if n == 1 { + return p.forwarders[0] + } + + p.idx = (p.idx + 1) % n + if !p.forwarders[p.idx].Enabled() { + return p.NextProxy() + } + + return p.forwarders[p.idx] +} + +func (p *strategyProxy) Enabled() bool { return true } +func (p *strategyProxy) SetEnable(enable bool) {} + +func (p *strategyProxy) Check(proxy Proxy, target string, duration time.Duration) {} + +func (p *strategyProxy) Addr() string { return p.addr } + +func (p *strategyProxy) Dial(network, addr string) (net.Conn, error) { + return p.NextProxy().Dial(network, addr) +} + +// round robin proxy +type rrproxy struct { + Proxy +} + +// newRRProxy . +func newRRProxy(addr string, forwarders []Proxy) Proxy { + return newStrategyProxy(addr, forwarders) +} + +// high availability proxy +type haproxy struct { + Proxy +} + +// newHAProxy . +func newHAProxy(addr string, forwarders []Proxy) Proxy { + return newStrategyProxy(addr, forwarders) +} + +func (p *haproxy) GetProxy() Proxy { + proxy := p.CurrentProxy() + if proxy.Enabled() == false { + return p.NextProxy() + } + return proxy +} diff --git a/tcptun.go b/tcptun.go new file mode 100644 index 0000000..7b6abd4 --- /dev/null +++ b/tcptun.go @@ -0,0 +1,66 @@ +package main + +import "net" + +type tcptun struct { + Proxy + addr string + raddr string +} + +// TCPTunProxy returns a redirect proxy. +func TCPTunProxy(addr, raddr string, upProxy Proxy) (Proxy, error) { + s := &tcptun{ + Proxy: upProxy, + addr: addr, + raddr: raddr, + } + + return s, nil +} + +// ListenAndServe redirected requests as a server. +func (s *tcptun) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + logf("failed to listen on %s: %v", s.addr, err) + return + } + + logf("listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + logf("failed to accept: %v", err) + continue + } + + go func() { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + rc, err := s.GetProxy().Dial("tcp", s.raddr) + if err != nil { + + logf("failed to connect to target: %v", err) + return + } + defer rc.Close() + + logf("proxy-tcptun %s <-> %s", c.RemoteAddr(), s.raddr) + + _, _, err = relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + logf("relay error: %v", err) + } + + }() + } +}