Browse Source

Initial import.

No one should use this unless they're debugging something that interacts
with SOCKS5.  Most of the code is stolen from obfs4proxy.
Yawning Angel 1 year ago
commit
7b5ebb02a0
4 changed files with 965 additions and 0 deletions
  1. 129 0
      main.go
  2. 89 0
      socks5/rfc1929.go
  3. 348 0
      socks5/socks5.go
  4. 399 0
      socks5/socks_test.go

+ 129 - 0
main.go

@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2018, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package main
+
+import (
+	"flag"
+	"io"
+	"log"
+	"net"
+	"sync"
+
+	"git.schwanenlied.me/yawning/shittysocks.git/socks5"
+)
+
+func main() {
+	addr := flag.String("address", "127.0.0.1:1080", "Address to listen on.")
+
+	ln, err := net.Listen("tcp", *addr)
+	if err != nil {
+		log.Fatalf("Failed to listen: %v", err)
+	}
+	log.Printf("Listening on: %v", ln.Addr())
+	defer ln.Close()
+
+	id := 0
+	for {
+		conn, err := ln.Accept()
+		if err != nil {
+			if e, ok := err.(net.Error); ok && !e.Temporary() {
+				log.Printf("Critical accept failure: %v", err)
+				return
+			}
+			continue
+		}
+
+		rAddr := conn.RemoteAddr()
+		log.Printf("Accepted new connection: %v (%d)", rAddr, id)
+		go connWorker(conn, id)
+		id++
+	}
+}
+
+func connWorker(conn net.Conn, id int) {
+	defer conn.Close()
+
+	req, err := socks5.Handshake(conn)
+	if err != nil {
+		log.Printf("%d: Failed SOCKS5 handshake: %v", id, err)
+		return
+	}
+
+	log.Printf("%d: Target: %v (Auth: %v:%v)", id, req.Target, req.Uname, req.Passwd)
+	upConn, err := net.Dial("tcp", req.Target)
+	if err != nil {
+		req.Reply(socks5.ErrorToReplyCode(err))
+		return
+	}
+	defer upConn.Close()
+	if err = req.Reply(socks5.ReplySucceeded); err != nil {
+		return
+	}
+
+	log.Printf("%d: Connected to target.", id)
+
+	if err = copyLoop(conn, upConn); err != nil {
+		log.Printf("%d: Closed connection: %v", id, err)
+	} else {
+		log.Printf("%d: Closed connection", id)
+	}
+}
+
+func copyLoop(a net.Conn, b net.Conn) error {
+	// Note: b is always the pt connection.  a is the SOCKS/ORPort connection.
+	errChan := make(chan error, 2)
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		defer wg.Done()
+		defer b.Close()
+		defer a.Close()
+		_, err := io.Copy(b, a)
+		errChan <- err
+	}()
+	go func() {
+		defer wg.Done()
+		defer a.Close()
+		defer b.Close()
+		_, err := io.Copy(a, b)
+		errChan <- err
+	}()
+
+	// Wait for both upstream and downstream to close.  Since one side
+	// terminating closes the other, the second error in the channel will be
+	// something like EINVAL (though io.Copy() will swallow EOF), so only the
+	// first error is returned.
+	wg.Wait()
+	if len(errChan) > 0 {
+		return <-errChan
+	}
+
+	return nil
+}

+ 89 - 0
socks5/rfc1929.go

@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package socks5
+
+import "fmt"
+
+const (
+	authRFC1929Ver     = 0x01
+	authRFC1929Success = 0x00
+	authRFC1929Fail    = 0x01
+)
+
+func (req *Request) authRFC1929() (err error) {
+	sendErrResp := func() {
+		// Swallow write/flush errors, the auth failure is the relevant error.
+		resp := []byte{authRFC1929Ver, authRFC1929Fail}
+		req.rw.Write(resp[:])
+		req.flushBuffers()
+	}
+
+	// The client sends a Username/Password request.
+	//  uint8_t ver (0x01)
+	//  uint8_t ulen (>= 1)
+	//  uint8_t uname[ulen]
+	//  uint8_t plen (>= 1)
+	//  uint8_t passwd[plen]
+
+	if err = req.readByteVerify("auth version", authRFC1929Ver); err != nil {
+		sendErrResp()
+		return
+	}
+
+	// Read the username.
+	var ulen byte
+	if ulen, err = req.readByte(); err != nil {
+		sendErrResp()
+		return
+	} else if ulen < 1 {
+		sendErrResp()
+		return fmt.Errorf("username with 0 length")
+	}
+	if req.Uname, err = req.readBytes(int(ulen)); err != nil {
+		sendErrResp()
+		return
+	}
+
+	// Read the password.
+	var plen byte
+	if plen, err = req.readByte(); err != nil {
+		sendErrResp()
+		return
+	} else if plen < 1 {
+		sendErrResp()
+		return fmt.Errorf("password with 0 length")
+	}
+	if req.Passwd, err = req.readBytes(int(plen)); err != nil {
+		sendErrResp()
+		return
+	}
+
+	resp := []byte{authRFC1929Ver, authRFC1929Success}
+	_, err = req.rw.Write(resp[:])
+	return
+}

+ 348 - 0
socks5/socks5.go

@@ -0,0 +1,348 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package socks5 implements a SOCKS 5 server
+//
+// Notes:
+//  * GSSAPI authentication, is NOT supported.
+//  * Only the CONNECT command is supported.
+//  * The authentication provided by the client is always accepted as is.
+package socks5
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"net"
+	"syscall"
+	"time"
+)
+
+const (
+	version = 0x05
+	rsv     = 0x00
+
+	cmdConnect = 0x01
+
+	atypIPv4       = 0x01
+	atypDomainName = 0x03
+	atypIPv6       = 0x04
+
+	authNoneRequired        = 0x00
+	authUsernamePassword    = 0x02
+	authNoAcceptableMethods = 0xff
+
+	requestTimeout = 5 * time.Second
+)
+
+// ReplyCode is a SOCKS 5 reply code.
+type ReplyCode byte
+
+// The various SOCKS 5 reply codes from RFC 1928.
+const (
+	ReplySucceeded ReplyCode = iota
+	ReplyGeneralFailure
+	ReplyConnectionNotAllowed
+	ReplyNetworkUnreachable
+	ReplyHostUnreachable
+	ReplyConnectionRefused
+	ReplyTTLExpired
+	ReplyCommandNotSupported
+	ReplyAddressNotSupported
+)
+
+// ErrorToReplyCode converts an error to the "best" reply code.
+func ErrorToReplyCode(err error) ReplyCode {
+	opErr, ok := err.(*net.OpError)
+	if !ok {
+		return ReplyGeneralFailure
+	}
+
+	errno, ok := opErr.Err.(syscall.Errno)
+	if !ok {
+		return ReplyGeneralFailure
+	}
+	switch errno {
+	case syscall.EADDRNOTAVAIL:
+		return ReplyAddressNotSupported
+	case syscall.ETIMEDOUT:
+		return ReplyTTLExpired
+	case syscall.ENETUNREACH:
+		return ReplyNetworkUnreachable
+	case syscall.EHOSTUNREACH:
+		return ReplyHostUnreachable
+	case syscall.ECONNREFUSED, syscall.ECONNRESET:
+		return ReplyConnectionRefused
+	default:
+		return ReplyGeneralFailure
+	}
+}
+
+// Request describes a SOCKS 5 request.
+type Request struct {
+	Target string
+	Uname  []byte
+	Passwd []byte
+	rw     *bufio.ReadWriter
+}
+
+// Handshake attempts to handle a incoming client handshake over the provided
+// connection and receive the SOCKS5 request.  The routine handles sending
+// appropriate errors if applicable, but will not close the connection.
+func Handshake(conn net.Conn) (*Request, error) {
+	// Arm the handshake timeout.
+	var err error
+	if err = conn.SetDeadline(time.Now().Add(requestTimeout)); err != nil {
+		return nil, err
+	}
+	defer func() {
+		// Disarm the handshake timeout, only propagate the error if
+		// the handshake was successful.
+		nerr := conn.SetDeadline(time.Time{})
+		if err == nil {
+			err = nerr
+		}
+	}()
+
+	req := new(Request)
+	req.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
+
+	// Negotiate the protocol version and authentication method.
+	var method byte
+	if method, err = req.negotiateAuth(); err != nil {
+		return nil, err
+	}
+
+	// Authenticate if neccecary.
+	if err = req.authenticate(method); err != nil {
+		return nil, err
+	}
+
+	// Read the client command.
+	if err = req.readCommand(); err != nil {
+		return nil, err
+	}
+
+	return req, err
+}
+
+// Reply sends a SOCKS5 reply to the corresponding request.  The BND.ADDR and
+// BND.PORT fields are always set to an address/port corresponding to
+// "0.0.0.0:0".
+func (req *Request) Reply(code ReplyCode) error {
+	// The server sends a reply message.
+	//  uint8_t ver (0x05)
+	//  uint8_t rep
+	//  uint8_t rsv (0x00)
+	//  uint8_t atyp
+	//  uint8_t bnd_addr[]
+	//  uint16_t bnd_port
+
+	var resp [4 + 4 + 2]byte
+	resp[0] = version
+	resp[1] = byte(code)
+	resp[2] = rsv
+	resp[3] = atypIPv4
+
+	if _, err := req.rw.Write(resp[:]); err != nil {
+		return err
+	}
+
+	return req.flushBuffers()
+}
+
+func (req *Request) negotiateAuth() (byte, error) {
+	// The client sends a version identifier/selection message.
+	//	uint8_t ver (0x05)
+	//  uint8_t nmethods (>= 1).
+	//  uint8_t methods[nmethods]
+
+	var err error
+	if err = req.readByteVerify("version", version); err != nil {
+		return 0, err
+	}
+
+	// Read the number of methods, and the methods.
+	var nmethods byte
+	method := byte(authNoAcceptableMethods)
+	if nmethods, err = req.readByte(); err != nil {
+		return method, err
+	}
+	var methods []byte
+	if methods, err = req.readBytes(int(nmethods)); err != nil {
+		return 0, err
+	}
+
+	// Pick the best authentication method, prioritizing authenticating
+	// over not if both options are present.
+	if bytes.IndexByte(methods, authUsernamePassword) != -1 {
+		method = authUsernamePassword
+	} else if bytes.IndexByte(methods, authNoneRequired) != -1 {
+		method = authNoneRequired
+	}
+
+	// The server sends a method selection message.
+	//  uint8_t ver (0x05)
+	//  uint8_t method
+	msg := []byte{version, method}
+	if _, err = req.rw.Write(msg); err != nil {
+		return 0, err
+	}
+
+	return method, req.flushBuffers()
+}
+
+func (req *Request) authenticate(method byte) error {
+	switch method {
+	case authNoneRequired:
+		// No authentication required.
+	case authUsernamePassword:
+		if err := req.authRFC1929(); err != nil {
+			return err
+		}
+	case authNoAcceptableMethods:
+		return fmt.Errorf("no acceptable authentication methods")
+	default:
+		// This should never happen as only supported auth methods should be
+		// negotiated.
+		return fmt.Errorf("negotiated unsupported method 0x%02x", method)
+	}
+
+	return req.flushBuffers()
+}
+
+func (req *Request) readCommand() error {
+	// The client sends the request details.
+	//  uint8_t ver (0x05)
+	//  uint8_t cmd
+	//  uint8_t rsv (0x00)
+	//  uint8_t atyp
+	//  uint8_t dst_addr[]
+	//  uint16_t dst_port
+
+	var err error
+	if err = req.readByteVerify("version", version); err != nil {
+		req.Reply(ReplyGeneralFailure)
+		return err
+	}
+	if err = req.readByteVerify("command", cmdConnect); err != nil {
+		req.Reply(ReplyCommandNotSupported)
+		return err
+	}
+	if err = req.readByteVerify("reserved", rsv); err != nil {
+		req.Reply(ReplyGeneralFailure)
+		return err
+	}
+
+	// Read the destination address/port.
+	var atyp byte
+	var host string
+	if atyp, err = req.readByte(); err != nil {
+		req.Reply(ReplyGeneralFailure)
+		return err
+	}
+	switch atyp {
+	case atypIPv4:
+		var addr []byte
+		if addr, err = req.readBytes(net.IPv4len); err != nil {
+			req.Reply(ReplyGeneralFailure)
+			return err
+		}
+		host = net.IPv4(addr[0], addr[1], addr[2], addr[3]).String()
+	case atypDomainName:
+		var alen byte
+		if alen, err = req.readByte(); err != nil {
+			req.Reply(ReplyGeneralFailure)
+			return err
+		}
+		if alen == 0 {
+			req.Reply(ReplyGeneralFailure)
+			return fmt.Errorf("domain name with 0 length")
+		}
+		var addr []byte
+		if addr, err = req.readBytes(int(alen)); err != nil {
+			req.Reply(ReplyGeneralFailure)
+			return err
+		}
+		host = string(addr)
+	case atypIPv6:
+		var rawAddr []byte
+		if rawAddr, err = req.readBytes(net.IPv6len); err != nil {
+			req.Reply(ReplyGeneralFailure)
+			return err
+		}
+		addr := make(net.IP, net.IPv6len)
+		copy(addr[:], rawAddr[:])
+		host = fmt.Sprintf("[%s]", addr.String())
+	default:
+		req.Reply(ReplyAddressNotSupported)
+		return fmt.Errorf("unsupported address type 0x%02x", atyp)
+	}
+	var rawPort []byte
+	if rawPort, err = req.readBytes(2); err != nil {
+		req.Reply(ReplyGeneralFailure)
+		return err
+	}
+	port := int(rawPort[0])<<8 | int(rawPort[1])
+	req.Target = fmt.Sprintf("%s:%d", host, port)
+
+	return req.flushBuffers()
+}
+
+func (req *Request) flushBuffers() error {
+	if err := req.rw.Flush(); err != nil {
+		return err
+	}
+	if req.rw.Reader.Buffered() > 0 {
+		return fmt.Errorf("read buffer has %d bytes of trailing data", req.rw.Reader.Buffered())
+	}
+	return nil
+}
+
+func (req *Request) readByte() (byte, error) {
+	return req.rw.ReadByte()
+}
+
+func (req *Request) readByteVerify(descr string, expected byte) error {
+	val, err := req.rw.ReadByte()
+	if err != nil {
+		return err
+	}
+	if val != expected {
+		return fmt.Errorf("message field '%s' was 0x%02x (expected 0x%02x)", descr, val, expected)
+	}
+	return nil
+}
+
+func (req *Request) readBytes(n int) ([]byte, error) {
+	b := make([]byte, n)
+	if _, err := io.ReadFull(req.rw, b); err != nil {
+		return nil, err
+	}
+	return b, nil
+}

+ 399 - 0
socks5/socks_test.go

@@ -0,0 +1,399 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package socks5
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/hex"
+	"io"
+	"net"
+	"testing"
+)
+
+func tcpAddrsEqual(a, b *net.TCPAddr) bool {
+	return a.IP.Equal(b.IP) && a.Port == b.Port
+}
+
+// testReadWriter is a bytes.Buffer backed io.ReadWriter used for testing.  The
+// Read and Write routines are to be used by the component being tested.  Data
+// can be written to and read back via the writeHex and readHex routines.
+type testReadWriter struct {
+	readBuf  bytes.Buffer
+	writeBuf bytes.Buffer
+}
+
+func (c *testReadWriter) Read(buf []byte) (n int, err error) {
+	return c.readBuf.Read(buf)
+}
+
+func (c *testReadWriter) Write(buf []byte) (n int, err error) {
+	return c.writeBuf.Write(buf)
+}
+
+func (c *testReadWriter) writeHex(str string) (n int, err error) {
+	var buf []byte
+	if buf, err = hex.DecodeString(str); err != nil {
+		return
+	}
+	return c.readBuf.Write(buf)
+}
+
+func (c *testReadWriter) readHex() string {
+	return hex.EncodeToString(c.writeBuf.Bytes())
+}
+
+func (c *testReadWriter) toBufio() *bufio.ReadWriter {
+	return bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
+}
+
+func (c *testReadWriter) toRequest() *Request {
+	req := new(Request)
+	req.rw = c.toBufio()
+	return req
+}
+
+func (c *testReadWriter) reset(req *Request) {
+	c.readBuf.Reset()
+	c.writeBuf.Reset()
+	req.rw = c.toBufio()
+}
+
+// TestAuthInvalidVersion tests auth negotiation with an invalid version.
+func TestAuthInvalidVersion(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 03, NMETHODS = 01, METHODS = [00]
+	c.writeHex("030100")
+	if _, err := req.negotiateAuth(); err == nil {
+		t.Error("negotiateAuth(InvalidVersion) succeded")
+	}
+}
+
+// TestAuthInvalidNMethods tests auth negotiaton with no methods.
+func TestAuthInvalidNMethods(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 00
+	c.writeHex("0500")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(No Methods) failed:", err)
+	}
+	if method != authNoAcceptableMethods {
+		t.Error("negotiateAuth(No Methods) picked unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "05ff" {
+		t.Error("negotiateAuth(No Methods) invalid response:", msg)
+	}
+}
+
+// TestAuthNoneRequired tests auth negotiaton with NO AUTHENTICATION REQUIRED.
+func TestAuthNoneRequired(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 01, METHODS = [00]
+	c.writeHex("050100")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(None) failed:", err)
+	}
+	if method != authNoneRequired {
+		t.Error("negotiateAuth(None) unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "0500" {
+		t.Error("negotiateAuth(None) invalid response:", msg)
+	}
+}
+
+// TestAuthUsernamePassword tests auth negotiation with USERNAME/PASSWORD.
+func TestAuthUsernamePassword(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 01, METHODS = [02]
+	c.writeHex("050102")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(UsernamePassword) failed:", err)
+	}
+	if method != authUsernamePassword {
+		t.Error("negotiateAuth(UsernamePassword) unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "0502" {
+		t.Error("negotiateAuth(UsernamePassword) invalid response:", msg)
+	}
+}
+
+// TestAuthBoth tests auth negotiation containing both NO AUTHENTICATION
+// REQUIRED and USERNAME/PASSWORD.
+func TestAuthBoth(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 02, METHODS = [00, 02]
+	c.writeHex("05020002")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(Both) failed:", err)
+	}
+	if method != authUsernamePassword {
+		t.Error("negotiateAuth(Both) unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "0502" {
+		t.Error("negotiateAuth(Both) invalid response:", msg)
+	}
+}
+
+// TestAuthUnsupported tests auth negotiation with a unsupported method.
+func TestAuthUnsupported(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 01, METHODS = [01] (GSSAPI)
+	c.writeHex("050101")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(Unknown) failed:", err)
+	}
+	if method != authNoAcceptableMethods {
+		t.Error("negotiateAuth(Unknown) picked unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "05ff" {
+		t.Error("negotiateAuth(Unknown) invalid response:", msg)
+	}
+}
+
+// TestAuthUnsupported2 tests auth negotiation with supported and unsupported
+// methods.
+func TestAuthUnsupported2(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+	var err error
+	var method byte
+
+	// VER = 05, NMETHODS = 03, METHODS = [00,01,02]
+	c.writeHex("0503000102")
+	if method, err = req.negotiateAuth(); err != nil {
+		t.Error("negotiateAuth(Unknown2) failed:", err)
+	}
+	if method != authUsernamePassword {
+		t.Error("negotiateAuth(Unknown2) picked unexpected method:", method)
+	}
+	if msg := c.readHex(); msg != "0502" {
+		t.Error("negotiateAuth(Unknown2) invalid response:", msg)
+	}
+}
+
+// TestRFC1929InvalidVersion tests RFC1929 auth with an invalid version.
+func TestRFC1929InvalidVersion(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 03, ULEN = 5, UNAME = "ABCDE", PLEN = 5, PASSWD = "abcde"
+	c.writeHex("03054142434445056162636465")
+	if err := req.authenticate(authUsernamePassword); err == nil {
+		t.Error("authenticate(InvalidVersion) succeded")
+	}
+	if msg := c.readHex(); msg != "0101" {
+		t.Error("authenticate(InvalidVersion) invalid response:", msg)
+	}
+}
+
+// TestRFC1929InvalidUlen tests RFC1929 auth with an invalid ULEN.
+func TestRFC1929InvalidUlen(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 01, ULEN = 0, UNAME = "", PLEN = 5, PASSWD = "abcde"
+	c.writeHex("0100056162636465")
+	if err := req.authenticate(authUsernamePassword); err == nil {
+		t.Error("authenticate(InvalidUlen) succeded")
+	}
+	if msg := c.readHex(); msg != "0101" {
+		t.Error("authenticate(InvalidUlen) invalid response:", msg)
+	}
+}
+
+// TestRFC1929InvalidPlen tests RFC1929 auth with an invalid PLEN.
+func TestRFC1929InvalidPlen(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 01, ULEN = 5, UNAME = "ABCDE", PLEN = 0, PASSWD = ""
+	c.writeHex("0105414243444500")
+	if err := req.authenticate(authUsernamePassword); err == nil {
+		t.Error("authenticate(InvalidPlen) succeded")
+	}
+	if msg := c.readHex(); msg != "0101" {
+		t.Error("authenticate(InvalidPlen) invalid response:", msg)
+	}
+}
+
+// TestRFC1929Success tests RFC1929 auth with valid credentials.
+func TestRFC1929Success(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 01, ULEN = 5, UNAME = "ABCDE", PLEN = 5, PASSWD = "abcde"
+	c.writeHex("01054142434445056162636465")
+	if err := req.authenticate(authUsernamePassword); err != nil {
+		t.Error("authenticate(Success) failed:", err)
+	}
+	if msg := c.readHex(); msg != "0100" {
+		t.Error("authenticate(Success) invalid response:", msg)
+	}
+	if string(req.Uname) != "ABCDE" {
+		t.Error("RFC1929 uname mismatch:", req.Uname)
+	}
+	if string(req.Passwd) != "abcde" {
+		t.Error("RFC1929 passwd mismatch:", req.Passwd)
+	}
+}
+
+// TestRequestInvalidHdr tests SOCKS5 requests with invalid VER/CMD/RSV/ATYPE
+func TestRequestInvalidHdr(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 03, CMD = 01, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050
+	c.writeHex("030100017f000001235a")
+	if err := req.readCommand(); err == nil {
+		t.Error("readCommand(InvalidVer) succeded")
+	}
+	if msg := c.readHex(); msg != "05010001000000000000" {
+		t.Error("readCommand(InvalidVer) invalid response:", msg)
+	}
+	c.reset(req)
+
+	// VER = 05, CMD = 05, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050
+	c.writeHex("050500017f000001235a")
+	if err := req.readCommand(); err == nil {
+		t.Error("readCommand(InvalidCmd) succeded")
+	}
+	if msg := c.readHex(); msg != "05070001000000000000" {
+		t.Error("readCommand(InvalidCmd) invalid response:", msg)
+	}
+	c.reset(req)
+
+	// VER = 05, CMD = 01, RSV = 30, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050
+	c.writeHex("050130017f000001235a")
+	if err := req.readCommand(); err == nil {
+		t.Error("readCommand(InvalidRsv) succeded")
+	}
+	if msg := c.readHex(); msg != "05010001000000000000" {
+		t.Error("readCommand(InvalidRsv) invalid response:", msg)
+	}
+	c.reset(req)
+
+	// VER = 05, CMD = 01, RSV = 01, ATYPE = 05, DST.ADDR = 127.0.0.1, DST.PORT = 9050
+	c.writeHex("050100057f000001235a")
+	if err := req.readCommand(); err == nil {
+		t.Error("readCommand(InvalidAtype) succeded")
+	}
+	if msg := c.readHex(); msg != "05080001000000000000" {
+		t.Error("readCommand(InvalidAtype) invalid response:", msg)
+	}
+	c.reset(req)
+}
+
+// TestRequestIPv4 tests IPv4 SOCKS5 requests.
+func TestRequestIPv4(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 05, CMD = 01, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050
+	c.writeHex("050100017f000001235a")
+	if err := req.readCommand(); err != nil {
+		t.Error("readCommand(IPv4) failed:", err)
+	}
+	addr, err := net.ResolveTCPAddr("tcp", req.Target)
+	if err != nil {
+		t.Error("net.ResolveTCPAddr failed:", err)
+	}
+	if !tcpAddrsEqual(addr, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9050}) {
+		t.Error("Unexpected target:", addr)
+	}
+}
+
+// TestRequestIPv6 tests IPv4 SOCKS5 requests.
+func TestRequestIPv6(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 05, CMD = 01, RSV = 00, ATYPE = 04, DST.ADDR = 0102:0304:0506:0708:090a:0b0c:0d0e:0f10, DST.PORT = 9050
+	c.writeHex("050100040102030405060708090a0b0c0d0e0f10235a")
+	if err := req.readCommand(); err != nil {
+		t.Error("readCommand(IPv6) failed:", err)
+	}
+	addr, err := net.ResolveTCPAddr("tcp", req.Target)
+	if err != nil {
+		t.Error("net.ResolveTCPAddr failed:", err)
+	}
+	if !tcpAddrsEqual(addr, &net.TCPAddr{IP: net.ParseIP("0102:0304:0506:0708:090a:0b0c:0d0e:0f10"), Port: 9050}) {
+		t.Error("Unexpected target:", addr)
+	}
+}
+
+// TestRequestFQDN tests FQDN (DOMAINNAME) SOCKS5 requests.
+func TestRequestFQDN(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	// VER = 05, CMD = 01, RSV = 00, ATYPE = 04, DST.ADDR = example.com, DST.PORT = 9050
+	c.writeHex("050100030b6578616d706c652e636f6d235a")
+	if err := req.readCommand(); err != nil {
+		t.Error("readCommand(FQDN) failed:", err)
+	}
+	if req.Target != "example.com:9050" {
+		t.Error("Unexpected target:", req.Target)
+	}
+}
+
+// TestResponseNil tests nil address SOCKS5 responses.
+func TestResponseNil(t *testing.T) {
+	c := new(testReadWriter)
+	req := c.toRequest()
+
+	if err := req.Reply(ReplySucceeded); err != nil {
+		t.Error("Reply(ReplySucceeded) failed:", err)
+	}
+	if msg := c.readHex(); msg != "05000001000000000000" {
+		t.Error("Reply(ReplySucceeded) invalid response:", msg)
+	}
+}
+
+var _ io.ReadWriter = (*testReadWriter)(nil)