diff --git a/README.md b/README.md index 23cd78d..c6bef34 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,16 @@ SOCKS is a SOCKS4, SOCKS4A and SOCKS5 proxy package for Go. import "h12.io/socks" -### Create a SOCKS proxy dialing function +### Create a SOCKS proxy dialling function dialSocksProxy := socks.Dial("socks5://127.0.0.1:1080?timeout=5s") tr := &http.Transport{Dial: dialSocksProxy} httpClient := &http.Client{Transport: tr} +### User/password authentication + + dialSocksProxy := socks.Dial("socks5://user:password@127.0.0.1:1080?timeout=5s") + ## Example ```go diff --git a/go.mod b/go.mod index 6f30c63..b4d2150 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module h12.io/socks go 1.9 + +require ( + github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bff8b8d --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= diff --git a/net.go b/net.go new file mode 100644 index 0000000..5a5233f --- /dev/null +++ b/net.go @@ -0,0 +1,72 @@ +package socks + +import ( + "bytes" + "errors" + "fmt" + "net" + "strconv" + "time" +) + +type requestBuilder struct { + bytes.Buffer +} + +func (b *requestBuilder) add(data ...byte) { + _, _ = b.Write(data) +} + +func (c *config) sendReceive(conn net.Conn, req []byte) (resp []byte, err error) { + if c.Timeout > 0 { + if err := conn.SetWriteDeadline(time.Now().Add(c.Timeout)); err != nil { + return nil, err + } + } + _, err = conn.Write(req) + if err != nil { + return + } + resp, err = c.readAll(conn) + return +} + +func (c *config) readAll(conn net.Conn) (resp []byte, err error) { + resp = make([]byte, 1024) + if c.Timeout > 0 { + if err := conn.SetReadDeadline(time.Now().Add(c.Timeout)); err != nil { + return nil, err + } + } + n, err := conn.Read(resp) + resp = resp[:n] + return +} + +func lookupIP(host string) (net.IP, error) { + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + if len(ips) == 0 { + return nil, fmt.Errorf("cannot resolve host: %s", host) + } + ip := ips[0].To4() + if len(ip) != net.IPv4len { + return nil, errors.New("ipv6 is not supported by SOCKS4") + } + return ip, nil +} + +func splitHostPort(addr string) (host string, port uint16, err error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return "", 0, err + } + portInt, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return "", 0, err + } + port = uint16(portInt) + return +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..d6d3c3f --- /dev/null +++ b/parse.go @@ -0,0 +1,61 @@ +package socks + +import ( + "errors" + "fmt" + "net/url" + "time" +) + +type ( + config struct { + Proto int + Host string + Auth *auth + Timeout time.Duration + } + auth struct { + Username string + Password string + } +) + +func parse(proxyURI string) (*config, error) { + uri, err := url.Parse(proxyURI) + if err != nil { + return nil, err + } + cfg := &config{} + switch uri.Scheme { + case "socks4": + cfg.Proto = SOCKS4 + case "socks4a": + cfg.Proto = SOCKS4A + case "socks5": + cfg.Proto = SOCKS5 + default: + return nil, fmt.Errorf("unknown SOCKS protocol %s", uri.Scheme) + } + cfg.Host = uri.Host + user := uri.User.Username() + password, _ := uri.User.Password() + if user != "" || password != "" { + if user == "" || password == "" || len(user) > 255 || len(password) > 255 { + return nil, errors.New("invalid user name or password") + } + cfg.Auth = &auth{ + Username: user, + Password: password, + } + } + query := uri.Query() + timeout := query.Get("timeout") + if timeout != "" { + var err error + cfg.Timeout, err = time.ParseDuration(timeout) + if err != nil { + return nil, err + } + } + return cfg, nil +} diff --git a/socks_test.go b/parse_test.go similarity index 92% rename from socks_test.go rename to parse_test.go index a3cdd6d..35b8d3a 100644 --- a/socks_test.go +++ b/parse_test.go @@ -11,14 +11,14 @@ func TestParse(t *testing.T) { testcases := []struct { name string uri string - cfg Config + cfg config }{ { name: "full config", uri: "socks5://u1:p1@127.0.0.1:8080?timeout=2s", - cfg: Config{ + cfg: config{ Proto: SOCKS5, - Auth: Auth{ + Auth: &auth{ Username: "u1", Password: "p1", }, @@ -29,7 +29,7 @@ func TestParse(t *testing.T) { { name: "simple socks5", uri: "socks5://127.0.0.1:8080", - cfg: Config{ + cfg: config{ Proto: SOCKS5, Host: "127.0.0.1:8080", }, diff --git a/socks.go b/socks.go index 2a11309..d626901 100644 --- a/socks.go +++ b/socks.go @@ -41,12 +41,8 @@ A complete example using this package: package socks // import "h12.io/socks" import ( - "errors" "fmt" "net" - "net/url" - "strconv" - "time" ) // Constants to choose which version of SOCKS protocol to use. @@ -56,52 +52,6 @@ const ( SOCKS5 ) -type ( - Config struct { - Proto int - Host string - Auth Auth - Timeout time.Duration - } - Auth struct { - Username string - Password string - } -) - -func parse(proxyURI string) (*Config, error) { - uri, err := url.Parse(proxyURI) - if err != nil { - return nil, err - } - cfg := &Config{} - switch uri.Scheme { - case "socks4": - cfg.Proto = SOCKS4 - case "socks4a": - cfg.Proto = SOCKS4A - case "socks5": - cfg.Proto = SOCKS5 - default: - return nil, fmt.Errorf("unknown SOCKS protocol %s", uri.Scheme) - } - cfg.Host = uri.Host - if uri.User != nil { - cfg.Auth.Username = uri.User.Username() - cfg.Auth.Password, _ = uri.User.Password() - } - query := uri.Query() - timeout := query.Get("timeout") - if timeout != "" { - var err error - cfg.Timeout, err = time.ParseDuration(timeout) - if err != nil { - return nil, err - } - } - return cfg, nil -} - // Dial returns the dial function to be used in http.Transport object. // Argument proxyURI should be in the format: "socks5://user:password@127.0.0.1:1080?timeout=5s". // The protocol could be socks5, socks4 and socks4a. @@ -117,10 +67,10 @@ func Dial(proxyURI string) func(string, string) (net.Conn, error) { // Argument socksType should be one of SOCKS4, SOCKS4A and SOCKS5. // Argument proxy should be in this format "127.0.0.1:1080". func DialSocksProxy(socksType int, proxy string) func(string, string) (net.Conn, error) { - return (&Config{Proto: socksType, Host: proxy}).dialFunc() + return (&config{Proto: socksType, Host: proxy}).dialFunc() } -func (c *Config) dialFunc() func(string, string) (net.Conn, error) { +func (c *config) dialFunc() func(string, string) (net.Conn, error) { switch c.Proto { case SOCKS5: return func(_, targetAddr string) (conn net.Conn, err error) { @@ -134,181 +84,6 @@ func (c *Config) dialFunc() func(string, string) (net.Conn, error) { return dialError(fmt.Errorf("unknown SOCKS protocol %v", c.Proto)) } -func (cfg *Config) dialSocks5(targetAddr string) (conn net.Conn, err error) { - proxy := cfg.Host - - // dial TCP - conn, err = net.Dial("tcp", proxy) - if err != nil { - return - } - - // version identifier/method selection request - req := []byte{ - 5, // version number - 1, // number of methods - 0, // method 0: no authentication (only anonymous access supported for now) - } - resp, err := cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 2 { - err = errors.New("Server does not respond properly.") - return - } else if resp[0] != 5 { - err = errors.New("Server does not support Socks 5.") - return - } else if resp[1] != 0 { // no auth - err = errors.New("socks method negotiation failed.") - return - } - - // detail request - host, port, err := splitHostPort(targetAddr) - if err != nil { - return nil, err - } - req = []byte{ - 5, // version number - 1, // connect command - 0, // reserved, must be zero - 3, // address type, 3 means domain name - byte(len(host)), // address length - } - req = append(req, []byte(host)...) - req = append(req, []byte{ - byte(port >> 8), // higher byte of destination port - byte(port), // lower byte of destination port (big endian) - }...) - resp, err = cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 10 { - err = errors.New("Server does not respond properly.") - } else if resp[1] != 0 { - err = errors.New("Can't complete SOCKS5 connection.") - } - - return -} - -func (cfg *Config) dialSocks4(targetAddr string) (conn net.Conn, err error) { - socksType := cfg.Proto - proxy := cfg.Host - - // dial TCP - conn, err = net.Dial("tcp", proxy) - if err != nil { - return - } - - // connection request - host, port, err := splitHostPort(targetAddr) - if err != nil { - return - } - ip := net.IPv4(0, 0, 0, 1).To4() - if socksType == SOCKS4 { - ip, err = lookupIP(host) - if err != nil { - return - } - } - req := []byte{ - 4, // version number - 1, // command CONNECT - byte(port >> 8), // higher byte of destination port - byte(port), // lower byte of destination port (big endian) - ip[0], ip[1], ip[2], ip[3], // special invalid IP address to indicate the host name is provided - 0, // user id is empty, anonymous proxy only - } - if socksType == SOCKS4A { - req = append(req, []byte(host+"\x00")...) - } - - resp, err := cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 8 { - err = errors.New("Server does not respond properly.") - return - } - switch resp[1] { - case 90: - // request granted - case 91: - err = errors.New("Socks connection request rejected or failed.") - case 92: - err = errors.New("Socks connection request rejected becasue SOCKS server cannot connect to identd on the client.") - case 93: - err = errors.New("Socks connection request rejected because the client program and identd report different user-ids.") - default: - err = errors.New("Socks connection request failed, unknown error.") - } - // clear the deadline before returning - if err := conn.SetDeadline(time.Time{}); err != nil { - return nil, err - } - return -} - -func (cfg *Config) sendReceive(conn net.Conn, req []byte) (resp []byte, err error) { - if cfg.Timeout > 0 { - if err := conn.SetWriteDeadline(time.Now().Add(cfg.Timeout)); err != nil { - return nil, err - } - } - _, err = conn.Write(req) - if err != nil { - return - } - resp, err = cfg.readAll(conn) - return -} - -func (cfg *Config) readAll(conn net.Conn) (resp []byte, err error) { - resp = make([]byte, 1024) - if cfg.Timeout > 0 { - if err := conn.SetReadDeadline(time.Now().Add(cfg.Timeout)); err != nil { - return nil, err - } - } - n, err := conn.Read(resp) - resp = resp[:n] - return -} - -func lookupIP(host string) (ip net.IP, err error) { - ips, err := net.LookupIP(host) - if err != nil { - return - } - if len(ips) == 0 { - err = fmt.Errorf("Cannot resolve host: %s.", host) - return - } - ip = ips[0].To4() - if len(ip) != net.IPv4len { - fmt.Println(len(ip), ip) - err = errors.New("IPv6 is not supported by SOCKS4.") - return - } - return -} - -func splitHostPort(addr string) (host string, port uint16, err error) { - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - return "", 0, err - } - portInt, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return "", 0, err - } - port = uint16(portInt) - return -} - func dialError(err error) func(string, string) (net.Conn, error) { return func(_, _ string) (net.Conn, error) { return nil, err diff --git a/socks4.go b/socks4.go new file mode 100644 index 0000000..ec22509 --- /dev/null +++ b/socks4.go @@ -0,0 +1,71 @@ +package socks + +import ( + "errors" + "net" + "time" +) + +func (cfg *config) dialSocks4(targetAddr string) (_ net.Conn, err error) { + socksType := cfg.Proto + proxy := cfg.Host + + // dial TCP + conn, err := net.Dial("tcp", proxy) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + conn.Close() + } + }() + + // connection request + host, port, err := splitHostPort(targetAddr) + if err != nil { + return nil, err + } + ip := net.IPv4(0, 0, 0, 1).To4() + if socksType == SOCKS4 { + ip, err = lookupIP(host) + if err != nil { + return nil, err + } + } + req := []byte{ + 4, // version number + 1, // command CONNECT + byte(port >> 8), // higher byte of destination port + byte(port), // lower byte of destination port (big endian) + ip[0], ip[1], ip[2], ip[3], // special invalid IP address to indicate the host name is provided + 0, // user id is empty, anonymous proxy only + } + if socksType == SOCKS4A { + req = append(req, []byte(host+"\x00")...) + } + + resp, err := cfg.sendReceive(conn, req) + if err != nil { + return nil, err + } else if len(resp) != 8 { + return nil, errors.New("server does not respond properly") + } + switch resp[1] { + case 90: + // request granted + case 91: + return nil, errors.New("socks connection request rejected or failed") + case 92: + return nil, errors.New("socks connection request rejected because SOCKS server cannot connect to identd on the client") + case 93: + return nil, errors.New("socks connection request rejected because the client program and identd report different user-ids") + default: + return nil, errors.New("socks connection request failed, unknown error") + } + // clear the deadline before returning + if err := conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + return +} diff --git a/socks5.go b/socks5.go new file mode 100644 index 0000000..07467e4 --- /dev/null +++ b/socks5.go @@ -0,0 +1,97 @@ +package socks + +import ( + "errors" + "net" +) + +func (cfg *config) dialSocks5(targetAddr string) (_ net.Conn, err error) { + proxy := cfg.Host + + // dial TCP + conn, err := net.Dial("tcp", proxy) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + conn.Close() + } + }() + + var req requestBuilder + + version := byte(5) // socks version 5 + method := byte(0) // method 0: no authentication (only anonymous access supported for now) + if cfg.Auth != nil { + method = 2 // method 2: username/password + } + + // version identifier/method selection request + req.add( + version, // socks version + 1, // number of methods + method, + ) + + resp, err := cfg.sendReceive(conn, req.Bytes()) + if err != nil { + return nil, err + } else if len(resp) != 2 { + return nil, errors.New("server does not respond properly") + } else if resp[0] != 5 { + return nil, errors.New("server does not support Socks 5") + } else if resp[1] != method { + return nil, errors.New("socks method negotiation failed") + } + if cfg.Auth != nil { + version := byte(1) // user/password version 1 + req.Reset() + req.add( + version, // user/password version + byte(len(cfg.Auth.Username)), // length of username + ) + req.add([]byte(cfg.Auth.Username)...) + req.add(byte(len(cfg.Auth.Password))) + req.add([]byte(cfg.Auth.Password)...) + resp, err := cfg.sendReceive(conn, req.Bytes()) + if err != nil { + return nil, err + } else if len(resp) != 2 { + return nil, errors.New("server does not respond properly") + } else if resp[0] != version { + return nil, errors.New("server does not support user/password version 1") + } else if resp[1] != 0 { // not success + return nil, errors.New("user/password login failed") + } + } + + // detail request + host, port, err := splitHostPort(targetAddr) + if err != nil { + return nil, err + } + req.Reset() + req.add( + 5, // version number + 1, // connect command + 0, // reserved, must be zero + 3, // address type, 3 means domain name + byte(len(host)), // address length + ) + req.add([]byte(host)...) + req.add( + byte(port>>8), // higher byte of destination port + byte(port), // lower byte of destination port (big endian) + ) + resp, err = cfg.sendReceive(conn, req.Bytes()) + if err != nil { + return + } else if len(resp) != 10 { + return nil, errors.New("server does not respond properly") + } else if resp[1] != 0 { + return nil, errors.New("can't complete SOCKS5 connection") + } + + return conn, nil +} diff --git a/socks5_test.go b/socks5_test.go new file mode 100644 index 0000000..a874db6 --- /dev/null +++ b/socks5_test.go @@ -0,0 +1,119 @@ +package socks + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "runtime" + "strconv" + "testing" + "time" + + socks5 "github.com/h12w/go-socks5" + "github.com/phayes/freeport" +) + +var httpTestServer = func() *http.Server { + var err error + httpTestPort, err := freeport.GetFreePort() + if err != nil { + panic(err) + } + s := &http.Server{ + Addr: ":" + strconv.Itoa(httpTestPort), + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("hello")) + }), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + go s.ListenAndServe() + runtime.Gosched() + tcpReady(httpTestPort, 2*time.Second) + return s +}() + +func newTestSocksServer(withAuth bool) (port int) { + authenticator := socks5.Authenticator(socks5.NoAuthAuthenticator{}) + if withAuth { + authenticator = socks5.UserPassAuthenticator{ + Credentials: socks5.StaticCredentials{ + "test_user": "test_pass", + }, + } + } + conf := &socks5.Config{ + Logger: log.New(ioutil.Discard, "", log.LstdFlags), + AuthMethods: []socks5.Authenticator{ + authenticator, + }, + } + + srv, err := socks5.New(conf) + if err != nil { + panic(err) + } + + socksTestPort, err := freeport.GetFreePort() + if err != nil { + panic(err) + } + + go func() { + if err := srv.ListenAndServe("tcp", "0.0.0.0:"+strconv.Itoa(socksTestPort)); err != nil { + panic(err) + } + }() + runtime.Gosched() + tcpReady(socksTestPort, 2*time.Second) + return socksTestPort +} + +func TestSocks5Anonymous(t *testing.T) { + socksTestPort := newTestSocksServer(false) + dialSocksProxy := Dial(fmt.Sprintf("socks5://127.0.0.1:%d?timeout=5s", socksTestPort)) + tr := &http.Transport{Dial: dialSocksProxy} + httpClient := &http.Client{Transport: tr} + resp, err := httpClient.Get(fmt.Sprintf("http://localhost" + httpTestServer.Addr)) + if err != nil { + panic(err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + if string(respBody) != "hello" { + t.Fatalf("expect response hello but got %s", respBody) + } +} + +func TestSocks5Auth(t *testing.T) { + socksTestPort := newTestSocksServer(true) + dialSocksProxy := Dial(fmt.Sprintf("socks5://test_user:test_pass@127.0.0.1:%d?timeout=5s", socksTestPort)) + tr := &http.Transport{Dial: dialSocksProxy} + httpClient := &http.Client{Transport: tr} + resp, err := httpClient.Get(fmt.Sprintf("http://localhost" + httpTestServer.Addr)) + if err != nil { + panic(err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + if string(respBody) != "hello" { + t.Fatalf("expect response hello but got %s", respBody) + } +} + +func tcpReady(port int, timeout time.Duration) { + conn, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(port), timeout) + if err != nil { + panic(err) + } + conn.Close() +} diff --git a/spec/rfc1929.txt b/spec/rfc1929.txt new file mode 100644 index 0000000..64fa02c --- /dev/null +++ b/spec/rfc1929.txt @@ -0,0 +1,115 @@ + + + + + + +Network Working Group M. Leech +Request for Comments: 1929 Bell-Northern Research Ltd +Category: Standards Track March 1996 + + + Username/Password Authentication for SOCKS V5 + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +1. Introduction + + The protocol specification for SOCKS Version 5 specifies a + generalized framework for the use of arbitrary authentication + protocols in the initial socks connection setup. This document + describes one of those protocols, as it fits into the SOCKS Version 5 + authentication "subnegotiation". + +Note: + + Unless otherwise noted, the decimal numbers appearing in packet- + format diagrams represent the length of the corresponding field, in + octets. Where a given octet must take on a specific value, the + syntax X'hh' is used to denote the value of the single octet in that + field. When the word 'Variable' is used, it indicates that the + corresponding field has a variable length defined either by an + associated (one or two octet) length field, or by a data type field. + +2. Initial negotiation + + Once the SOCKS V5 server has started, and the client has selected the + Username/Password Authentication protocol, the Username/Password + subnegotiation begins. This begins with the client producing a + Username/Password request: + + +----+------+----------+------+----------+ + |VER | ULEN | UNAME | PLEN | PASSWD | + +----+------+----------+------+----------+ + | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + +----+------+----------+------+----------+ + + + + + + +Leech Standards Track [Page 1] + +RFC 1929 Username Authentication for SOCKS V5 March 1996 + + + The VER field contains the current version of the subnegotiation, + which is X'01'. The ULEN field contains the length of the UNAME field + that follows. The UNAME field contains the username as known to the + source operating system. The PLEN field contains the length of the + PASSWD field that follows. The PASSWD field contains the password + association with the given UNAME. + + The server verifies the supplied UNAME and PASSWD, and sends the + following response: + + +----+--------+ + |VER | STATUS | + +----+--------+ + | 1 | 1 | + +----+--------+ + + A STATUS field of X'00' indicates success. If the server returns a + `failure' (STATUS value other than X'00') status, it MUST close the + connection. + +3. Security Considerations + + This document describes a subnegotiation that provides authentication + services to the SOCKS protocol. Since the request carries the + password in cleartext, this subnegotiation is not recommended for + environments where "sniffing" is possible and practical. + +4. Author's Address + + Marcus Leech + Bell-Northern Research Ltd + P.O. Box 3511, Station C + Ottawa, ON + CANADA K1Y 4H7 + + Phone: +1 613 763 9145 + EMail: mleech@bnr.ca + + + + + + + + + + + + + + +Leech Standards Track [Page 2] +