refactor and support user/password authentication

This commit is contained in:
Hǎi-Liàng Hal Wáng 2020-05-23 15:25:02 +01:00
parent 0ac3745d74
commit a6825e0818
11 changed files with 555 additions and 232 deletions

View File

@ -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

5
go.mod
View File

@ -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
)

4
go.sum Normal file
View File

@ -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=

72
net.go Normal file
View File

@ -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
}

61
parse.go Normal file
View File

@ -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
}

View File

@ -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",
},

229
socks.go
View File

@ -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

71
socks4.go Normal file
View File

@ -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
}

97
socks5.go Normal file
View File

@ -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
}

119
socks5_test.go Normal file
View File

@ -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()
}

115
spec/rfc1929.txt Normal file
View File

@ -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]