From 7d77f4a85e8a24d730b5bc55a4adea1675cdee43 Mon Sep 17 00:00:00 2001 From: wweir Date: Sun, 16 Feb 2020 16:33:24 +0800 Subject: [PATCH] Add Socks5 support --- conf/conf.go | 2 +- conf/conf_darwin.go | 4 +-- conf/dynamic_rule.go | 21 ++++++++---- conf/sower.toml | 2 +- go.mod | 2 +- go.sum | 3 +- internal/http/tgt_parser.go | 61 ++++++++++++++++------------------ internal/socks5/rfc_def.go | 44 +++++++++++++++++++++++++ internal/socks5/socks5.go | 66 +++++++++++-------------------------- proxy/dns.go | 2 +- proxy/http_proxy.go | 44 ++++++------------------- proxy/proxy.go | 36 ++++++++++++-------- proxy/util.go | 20 +++++++++++ util/util.go | 19 +++++++++++ 14 files changed, 187 insertions(+), 139 deletions(-) create mode 100644 internal/socks5/rfc_def.go create mode 100644 util/util.go diff --git a/conf/conf.go b/conf/conf.go index b6adf2f..fb5221b 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -68,7 +68,7 @@ func init() { flag.StringVar(&Server.Upstream, "s", "", "upstream http service, eg: 127.0.0.1:8080") flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, gen cert from letsencrypt if empty") flag.StringVar(&Server.KeyFile, "s_key", "", "tls key file, gen cert from letsencrypt if empty") - flag.StringVar(&Client.Address, "c", "", "remote server domain, eg: aa.bb.cc") // TODO: socks5://127.0.0.1:1080 + flag.StringVar(&Client.Address, "c", "", "remote server domain, eg: aa.bb.cc, socks5h://127.0.0.1:1080") flag.StringVar(&Client.HTTPProxy.Address, "http_proxy", ":8080", "http proxy, empty to disable") flag.StringVar(&Client.DNS.ServeIP, "dns_ip", "", "upstream dns, eg: 127.0.0.1, disable dns proxy if empty") flag.StringVar(&Client.DNS.Upstream, "dns_upstream", "", "dns relay server ip, dynamic detect if empty") diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go index 96513d3..2deeaa1 100644 --- a/conf/conf_darwin.go +++ b/conf/conf_darwin.go @@ -15,13 +15,13 @@ import ( "github.com/wweir/utils/log" ) -const svcPath = "/Library/LaunchDaemons/cc.wweir.sower.plist" +const svcPath = "/Library/LaunchDaemons/sower.plist" const svcFile = ` Label - cc.wweir.sower + sower ProgramArguments /bin/sh diff --git a/conf/dynamic_rule.go b/conf/dynamic_rule.go index b38abbc..b563d62 100644 --- a/conf/dynamic_rule.go +++ b/conf/dynamic_rule.go @@ -9,6 +9,7 @@ import ( "time" "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" "github.com/wweir/sower/util" "github.com/wweir/utils/log" "github.com/wweir/utils/mem" @@ -78,15 +79,21 @@ func (d *dynamic) Get(key interface{}) (err error) { defer wg.Done() var conn net.Conn - if conn, err = tls.Dial("tcp", net.JoinHostPort(Client.Address, "443"), &tls.Config{}); err != nil { - log.Errorw("tls dial", "addr", net.JoinHostPort(Client.Address, "443"), "err", err) - return - } + if addr, ok := socks5.IsSocks5Schema(Client.Address); ok { + conn, err = net.Dial("tcp", addr) + conn = socks5.ToSocks5(conn, domain, uint16(ping.port)) - if ping.port == http.HTTP { - conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTP, "", 80) } else { - conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTPS, "", 443) + conn, err = tls.Dial("tcp", net.JoinHostPort(Client.Address, "443"), &tls.Config{}) + if ping.port == http.HTTP { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTP, "", 80) + } else { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTPS, "", 443) + } + } + if err != nil { + log.Errorw("sower dial", "addr", Client.Address, "err", err) + return } if err := ping.port.PingWithConn(domain, conn, timeout); err != nil { diff --git a/conf/sower.toml b/conf/sower.toml index 8c6d0a6..a4ae296 100644 --- a/conf/sower.toml +++ b/conf/sower.toml @@ -1,5 +1,5 @@ [client] - address = "aa.bb.cc" + address = "" # aa.bb.cc, socks5h://127.0.0.1:1080 [client.dns] flush_cmd="" # macOS: pkill mDNSResponder || true, Windows: ipconfig /flushdnss diff --git a/go.mod b/go.mod index 7948dba..9525e21 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( github.com/pkg/errors v0.9.1 github.com/wweir/utils v0.0.0-20200214114658-f6f356a08736 golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 - golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe + golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 ) diff --git a/go.sum b/go.sum index bfbc677..c273591 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/internal/http/tgt_parser.go b/internal/http/tgt_parser.go index c2f3e46..9dafbce 100644 --- a/internal/http/tgt_parser.go +++ b/internal/http/tgt_parser.go @@ -72,16 +72,10 @@ func (c *conn) Write(b []byte) (n int, err error) { } // ParseAddr parse target addr from net.Conn -func ParseAddr(conn net.Conn, password []byte) (c net.Conn, domain string, port uint16, err error) { +func ParseAddr(conn net.Conn, password []byte) (_ net.Conn, domain string, port uint16, err error) { teeConn := &util.TeeConn{Conn: conn} teeConn.StartOrReset() - defer func() { - if err != nil { - teeConn.Close() - } else { - teeConn.Stop() - } - }() + defer teeConn.Stop() head := new(header) if err = binary.Read(conn, binary.BigEndian, head); err != nil { @@ -102,37 +96,40 @@ func ParseAddr(conn net.Conn, password []byte) (c net.Conn, domain string, port case TGT_HTTP: teeConn.DropAndRestart() - - var resp *http.Request - resp, err = http.ReadRequest(bufio.NewReader(teeConn)) - if err != nil { - return nil, "", 0, err - } - - idx := strings.LastIndex(resp.Host, ":") - if idx == -1 { - return teeConn, resp.Host, 80, nil - } - - var port uint64 - if port, err = strconv.ParseUint(resp.Host[idx+1:], 10, 16); err != nil { - return nil, "", 0, err - } - return teeConn, resp.Host[:idx], uint16(port), nil + return ParseHTTP(teeConn) case TGT_HTTPS: teeConn.DropAndRestart() - - domain, _, err := extractSNI(teeConn) - if err != nil { - return nil, "", 0, err - } - return teeConn, domain, head.Port, nil + conn, domain, err = ParseHTTPS(teeConn) + return conn, domain, head.Port, err default: - return nil, "", 0, errors.New("invalid request") + return teeConn, "", 0, errors.New("invalid request") } } +func ParseHTTP(teeConn net.Conn) (_ net.Conn, domain string, port uint16, err error) { + resp, err := http.ReadRequest(bufio.NewReader(teeConn)) + if err != nil { + return teeConn, "", 0, err + } + + idx := strings.LastIndex(resp.Host, ":") + if idx == -1 { + return teeConn, resp.Host, 80, nil + } + + p, err := strconv.ParseUint(resp.Host[idx+1:], 10, 16) + if err != nil { + return teeConn, "", 0, err + } + return teeConn, resp.Host[:idx], uint16(p), nil +} +func ParseHTTPS(teeConn net.Conn) (_ net.Conn, domain string, err error) { + if domain, _, err = extractSNI(teeConn); err != nil { + return nil, "", err + } + return teeConn, domain, nil +} var errChecksum = errors.New("invalid checksum") diff --git a/internal/socks5/rfc_def.go b/internal/socks5/rfc_def.go new file mode 100644 index 0000000..313bfee --- /dev/null +++ b/internal/socks5/rfc_def.go @@ -0,0 +1,44 @@ +package socks5 + +// https://tools.ietf.org/html/rfc1928 + +type authReq struct { + VER byte + NMETHODS byte + METHODS [1]byte // 1 to 255, fix to no authentication +} + +type authResp struct { + VER byte + METHOD byte +} + +type request struct { + req + DST_ADDR []byte // first byte is length + DST_PORT []byte // two bytes +} +type req struct { + VER byte + CMD byte + RSV byte + ATYP byte +} + +func (r *request) Bytes() []byte { + out := []byte{r.VER, r.CMD, r.RSV, r.ATYP} + out = append(out, r.DST_ADDR...) + return append(out, r.DST_PORT...) +} + +type response struct { + resp + DST_ADDR []byte // first byte is length + DST_PORT []byte // two bytes +} +type resp struct { + VER byte + REP byte + RSV byte + ATYP byte +} diff --git a/internal/socks5/socks5.go b/internal/socks5/socks5.go index b346fc1..a3a1aed 100644 --- a/internal/socks5/socks5.go +++ b/internal/socks5/socks5.go @@ -5,13 +5,28 @@ import ( "fmt" "io" "net" - "strconv" + "strings" ) -func ToSocks5(c net.Conn, domain, port string) net.Conn { - num, _ := strconv.Atoi(port) - bytes := []byte{byte(num >> 8), byte(num)} - return &conn{init: make(chan struct{}), Conn: c, domain: domain, port: bytes} +func IsSocks5Schema(addr string) (string, bool) { + if strings.HasPrefix(addr, "socks5://") { + return strings.TrimPrefix(addr, "socks5://"), true + } + + if strings.HasPrefix(addr, "socks5h://") { + return strings.TrimPrefix(addr, "socks5h://"), true + } + + return addr, false +} + +func ToSocks5(c net.Conn, domain string, port uint16) net.Conn { + return &conn{ + init: make(chan struct{}), + Conn: c, + domain: domain, + port: []byte{byte(port >> 8), byte(port)}, + } } type conn struct { @@ -106,44 +121,3 @@ func (c *conn) Write(b []byte) (n int, err error) { close(c.init) return c.Conn.Write(b) } - -type authReq struct { - VER byte - NMETHODS byte - METHODS [1]byte // 1 to 255, fix to no authentication -} - -type authResp struct { - VER byte - METHOD byte -} - -type request struct { - req - DST_ADDR []byte // first byte is length - DST_PORT []byte // two bytes -} -type req struct { - VER byte - CMD byte - RSV byte - ATYP byte -} - -func (r *request) Bytes() []byte { - out := []byte{r.VER, r.CMD, r.RSV, r.ATYP} - out = append(out, r.DST_ADDR...) - return append(out, r.DST_PORT...) -} - -type response struct { - resp - DST_ADDR []byte // first byte is length - DST_PORT []byte // two bytes -} -type resp struct { - VER byte - REP byte - RSV byte - ATYP byte -} diff --git a/proxy/dns.go b/proxy/dns.go index 4a65b0d..3572ca4 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -47,7 +47,7 @@ func ServeDNS(redirectIP, relayServer string) { server, err := pickRelayAddr(relayServer) if err != nil { log.Errorw("detect upstream dns", "err", err) - } else { + } else if relayServer != server { relayServer = server log.Infow("detect upstream dns", "addr", relayServer) } diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 670372f..67f552b 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -11,6 +11,7 @@ import ( "github.com/wweir/sower/conf" _http "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/util" "github.com/wweir/utils/log" ) @@ -33,20 +34,12 @@ func startHTTPProxy(httpProxyAddr, serverAddr string, password []byte) { } func httpProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - host = r.Host - } + host, port := util.ParseHostPort(r.Host, 80) roundTripper := &http.Transport{} if conf.ShouldProxy(host) { roundTripper.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) - if err != nil { - return nil, err - } - - return _http.NewTgtConn(conn, password, _http.TGT_HTTP, "", 80), nil + return dial(serverAddr, password, _http.TGT_HTTP, host, port) } } @@ -67,17 +60,7 @@ func httpProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passwo } func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { - var host string - var port = uint16(443) - if h, p, err := net.SplitHostPort(r.Host); err != nil { - host = r.Host - } else if pNum, err := strconv.ParseUint(p, 10, 16); err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - return - } else { - host = h - port = uint16(pNum) - } + host, port := util.ParseHostPort(r.Host, 443) conn, _, err := w.(http.Hijacker).Hijack() if err != nil { @@ -94,21 +77,14 @@ func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passw var rc net.Conn if conf.ShouldProxy(host) { - rc, err = tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) - if err != nil { - conn.Write([]byte("sower: tls dial: " + err.Error())) - conn.Close() - return - } - rc = _http.NewTgtConn(rc, password, _http.TGT_HTTPS, "", port) - + rc, err = dial(serverAddr, password, _http.TGT_HTTPS, host, port) } else { rc, err = net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(int(port)))) - if err != nil { - conn.Write([]byte("sower: tcp dial: " + err.Error())) - conn.Close() - return - } + } + if err != nil { + conn.Write([]byte("sower dial " + serverAddr + " fail: " + err.Error())) + conn.Close() + return } defer rc.Close() diff --git a/proxy/proxy.go b/proxy/proxy.go index 45c627d..2918023 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -7,6 +7,8 @@ import ( "strconv" _http "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" + "github.com/wweir/sower/util" "github.com/wweir/utils/log" "golang.org/x/crypto/acme/autocert" ) @@ -20,6 +22,8 @@ type head struct { func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards map[string]string) { passwordData := []byte(password) + _, isSocks5 := socks5.IsSocks5Schema(serverAddr) + if httpProxy != "" { go startHTTPProxy(httpProxy, serverAddr, passwordData) } @@ -39,15 +43,30 @@ func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards ma go func(conn net.Conn) { defer conn.Close() + if isSocks5 { + teeConn := &util.TeeConn{Conn: conn} + teeConn.StartOrReset() + switch tgtType { + case _http.TGT_HTTP: + conn, host, port, err = _http.ParseHTTP(teeConn) + case _http.TGT_HTTPS: + conn, host, err = _http.ParseHTTPS(teeConn) + } + if err != nil { + log.Errorw("parse socks5 target", "err", err) + return + } + teeConn.Stop() + } - rc, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) + rc, err := dial(serverAddr, passwordData, tgtType, host, port) if err != nil { - log.Errorw("tls dial", "addr", net.JoinHostPort(serverAddr, "443"), "err", err) + log.Errorw("dial", "addr", serverAddr, "err", err) return } defer rc.Close() - relay(conn, _http.NewTgtConn(rc, passwordData, tgtType, host, port)) + relay(conn, rc) }(conn) } } @@ -59,16 +78,7 @@ func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards ma for from, to := range forwards { go func(from, to string) { - host, portStr, err := net.SplitHostPort(to) - if err != nil { - log.Fatalw("parse port forward", "target", to, "err", err) - } - portNum, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - log.Fatalw("parse port forward", "target", to, "err", err) - } - port := uint16(portNum) - + host, port := util.ParseHostPort(to, 0) relayToRemote(_http.TGT_OTHER, from, host, port) }(from, to) } diff --git a/proxy/util.go b/proxy/util.go index efffeaa..570e7d8 100644 --- a/proxy/util.go +++ b/proxy/util.go @@ -1,13 +1,33 @@ package proxy import ( + "crypto/tls" "io" "net" "sync" "sync/atomic" "time" + + "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" ) +func dial(serverAddr string, password []byte, tgtType byte, domain string, port uint16) (net.Conn, error) { + if addr, ok := socks5.IsSocks5Schema(serverAddr); ok { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + return socks5.ToSocks5(conn, domain, port), nil + } + + conn, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) + if err != nil { + return nil, err + } + return http.NewTgtConn(conn, password, tgtType, domain, port), nil +} + func relay(conn1, conn2 net.Conn) { wg := &sync.WaitGroup{} exitFlag := new(int32) diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..5e6607a --- /dev/null +++ b/util/util.go @@ -0,0 +1,19 @@ +package util + +import ( + "net" + "strconv" +) + +func ParseHostPort(addr string, defaultPort uint16) (string, uint16) { + h, p, err := net.SplitHostPort(addr) + if err != nil { + if defaultPort == 0 { + panic("parse port fail with no default, addr: " + addr) + } + return addr, defaultPort + } + + pNum, _ := strconv.ParseUint(p, 10, 16) + return h, uint16(pNum) +}