summaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
authorYawning Angel <yawning@torproject.org>2015-04-12 19:00:46 +0000
committerYawning Angel <yawning@torproject.org>2015-04-15 20:50:01 +0000
commita8d7134f1097bd50803da0e2a86c07524e433b51 (patch)
tree346403d6ecfdf2a76d8e736862c1b42d165cc919 /common
parent8996cb2646f2721b2d86f5f6b54b5c21d2acc71d (diff)
Use a built in SOCKS 5 server instead of goptlibs.
Differences from my goptlib branch: * Instead of exposing a net.Listener, just expose a Handshake() routine that takes an existing net.Conn. (#14135 is irrelevant to this socks server. * There's an extra routine for sending back sensible errors on Dial failure instead of "General failure". * The code is slightly cleaner (IMO). Gotchas: * If the goptlib pt.Args datatype or external interface changes, args.go will need to be updated. Tested with obfs3 and obfs4, including IPv6.
Diffstat (limited to 'common')
-rw-r--r--common/socks5/args.go96
-rw-r--r--common/socks5/args_test.go144
-rw-r--r--common/socks5/rfc1929.go105
-rw-r--r--common/socks5/socks5.go358
-rw-r--r--common/socks5/socks_test.go412
5 files changed, 1115 insertions, 0 deletions
diff --git a/common/socks5/args.go b/common/socks5/args.go
new file mode 100644
index 0000000..d9ea099
--- /dev/null
+++ b/common/socks5/args.go
@@ -0,0 +1,96 @@
+/*
+ * 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"
+ "git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+// parseClientParameters takes a client parameter string formatted according to
+// "Passing PT-specific parameters to a client PT" in the pluggable transport
+// specification, and returns it as a goptlib Args structure.
+//
+// This is functionally identical to the equivalently named goptlib routine.
+func parseClientParameters(argStr string) (args pt.Args, err error) {
+ args = make(pt.Args)
+ if len(argStr) == 0 {
+ return
+ }
+
+ var key string
+ var acc []byte
+ prevIsEscape := false
+ for idx, ch := range []byte(argStr) {
+ switch ch {
+ case '\\':
+ prevIsEscape = !prevIsEscape
+ if prevIsEscape {
+ continue
+ }
+ case '=':
+ if !prevIsEscape {
+ if key != "" {
+ break
+ }
+ if len(acc) == 0 {
+ return nil, fmt.Errorf("unexpected '=' at %d", idx)
+ }
+ key = string(acc)
+ acc = nil
+ continue
+ }
+ case ';':
+ if !prevIsEscape {
+ if key == "" || idx == len(argStr)-1 {
+ return nil, fmt.Errorf("unexpected ';' at %d", idx)
+ }
+ args.Add(key, string(acc))
+ key = ""
+ acc = nil
+ continue
+ }
+ default:
+ if prevIsEscape {
+ return nil, fmt.Errorf("unexpected '\\' at %d", idx-1)
+ }
+ }
+ prevIsEscape = false
+ acc = append(acc, ch)
+ }
+ if prevIsEscape {
+ return nil, fmt.Errorf("underminated escape character")
+ }
+ // Handle the final k,v pair if any.
+ if key == "" {
+ return nil, fmt.Errorf("final key with no value")
+ }
+ args.Add(key, string(acc))
+
+ return args, nil
+}
diff --git a/common/socks5/args_test.go b/common/socks5/args_test.go
new file mode 100644
index 0000000..d9d3f22
--- /dev/null
+++ b/common/socks5/args_test.go
@@ -0,0 +1,144 @@
+// Shamelessly stolen from goptlib's args_test.go.
+
+package socks5
+
+import (
+ "testing"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+func stringSlicesEqual(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func argsEqual(a, b pt.Args) bool {
+ for k, av := range a {
+ bv := b[k]
+ if !stringSlicesEqual(av, bv) {
+ return false
+ }
+ }
+ for k, bv := range b {
+ av := a[k]
+ if !stringSlicesEqual(av, bv) {
+ return false
+ }
+ }
+ return true
+}
+
+func TestParseClientParameters(t *testing.T) {
+ badTests := [...]string{
+ "key",
+ "key\\",
+ "=value",
+ "==value",
+ "==key=value",
+ "key=value\\",
+ "a=b;key=value\\",
+ "a;b=c",
+ ";",
+ "key=value;",
+ ";key=value",
+ "key\\=value",
+ }
+ goodTests := [...]struct {
+ input string
+ expected pt.Args
+ }{
+ {
+ "",
+ pt.Args{},
+ },
+ {
+ "key=",
+ pt.Args{"key": []string{""}},
+ },
+ {
+ "key==",
+ pt.Args{"key": []string{"="}},
+ },
+ {
+ "key=value",
+ pt.Args{"key": []string{"value"}},
+ },
+ {
+ "a=b=c",
+ pt.Args{"a": []string{"b=c"}},
+ },
+ {
+ "key=a\nb",
+ pt.Args{"key": []string{"a\nb"}},
+ },
+ {
+ "key=value\\;",
+ pt.Args{"key": []string{"value;"}},
+ },
+ {
+ "key=\"value\"",
+ pt.Args{"key": []string{"\"value\""}},
+ },
+ {
+ "key=\"\"value\"\"",
+ pt.Args{"key": []string{"\"\"value\"\""}},
+ },
+ {
+ "\"key=value\"",
+ pt.Args{"\"key": []string{"value\""}},
+ },
+ {
+ "key=value;key=value",
+ pt.Args{"key": []string{"value", "value"}},
+ },
+ {
+ "key=value1;key=value2",
+ pt.Args{"key": []string{"value1", "value2"}},
+ },
+ {
+ "key1=value1;key2=value2;key1=value3",
+ pt.Args{"key1": []string{"value1", "value3"}, "key2": []string{"value2"}},
+ },
+ {
+ "\\;=\\;;\\\\=\\;",
+ pt.Args{";": []string{";"}, "\\": []string{";"}},
+ },
+ {
+ "a\\=b=c",
+ pt.Args{"a=b": []string{"c"}},
+ },
+ {
+ "shared-secret=rahasia;secrets-file=/tmp/blob",
+ pt.Args{"shared-secret": []string{"rahasia"}, "secrets-file": []string{"/tmp/blob"}},
+ },
+ {
+ "rocks=20;height=5.6",
+ pt.Args{"rocks": []string{"20"}, "height": []string{"5.6"}},
+ },
+ }
+
+ for _, input := range badTests {
+ _, err := parseClientParameters(input)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, test := range goodTests {
+ args, err := parseClientParameters(test.input)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if !argsEqual(args, test.expected) {
+ t.Errorf("%q → %q (expected %q)", test.input, args, test.expected)
+ }
+ }
+}
diff --git a/common/socks5/rfc1929.go b/common/socks5/rfc1929.go
new file mode 100644
index 0000000..f8176f1
--- /dev/null
+++ b/common/socks5/rfc1929.go
@@ -0,0 +1,105 @@
+/*
+ * 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")
+ }
+ var uname []byte
+ if 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")
+ }
+ var passwd []byte
+ if passwd, err = req.readBytes(int(plen)); err != nil {
+ sendErrResp()
+ return
+ }
+
+ // Pluggable transports use the username/password field to pass
+ // per-connection arguments. The fields contain ASCII strings that
+ // are combined and then parsed into key/value pairs.
+ argStr := string(uname)
+ if !(plen == 1 && passwd[0] == 0x00) {
+ // tor will set the password to 'NUL', if the field doesn't contain any
+ // actual argument data.
+ argStr += string(passwd)
+ }
+ if req.Args, err = parseClientParameters(argStr); err != nil {
+ sendErrResp()
+ return
+ }
+
+ resp := []byte{authRFC1929Ver, authRFC1929Success}
+ _, err = req.rw.Write(resp[:])
+ return
+}
diff --git a/common/socks5/socks5.go b/common/socks5/socks5.go
new file mode 100644
index 0000000..d15e542
--- /dev/null
+++ b/common/socks5/socks5.go
@@ -0,0 +1,358 @@
+/*
+ * 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 and the required pluggable
+// transport specific extensions. For more information see RFC 1928 and RFC
+// 1929.
+//
+// Notes:
+// * GSSAPI authentication, is NOT supported.
+// * Only the CONNECT command is supported.
+// * The authentication provided by the client is always accepted as it is
+// used as a channel to pass information rather than for authentication for
+// pluggable transports.
+package socks5
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "syscall"
+ "time"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+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
+)
+
+// Version returns a string suitable to be included in a call to Cmethod.
+func Version() string {
+ return "socks5"
+}
+
+// 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
+ Args pt.Args
+ 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
+}
diff --git a/common/socks5/socks_test.go b/common/socks5/socks_test.go
new file mode 100644
index 0000000..720476f
--- /dev/null
+++ b/common/socks5/socks_test.go
@@ -0,0 +1,412 @@
+/*
+ * 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)
+ }
+}
+
+// TestRFC1929InvalidArgs tests RFC1929 auth with invalid pt args.
+func TestRFC1929InvalidPTArgs(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(InvalidArgs) succeded")
+ }
+ if msg := c.readHex(); msg != "0101" {
+ t.Error("authenticate(InvalidArgs) invalid response:", msg)
+ }
+}
+
+// TestRFC1929Success tests RFC1929 auth with valid pt args.
+func TestRFC1929Success(t *testing.T) {
+ c := new(testReadWriter)
+ req := c.toRequest()
+
+ // VER = 01, ULEN = 9, UNAME = "key=value", PLEN = 1, PASSWD = "\0"
+ c.writeHex("01096b65793d76616c75650100")
+ 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)
+ }
+ v, ok := req.Args.Get("key")
+ if v != "value" || !ok {
+ t.Error("RFC1929 k,v parse failure:", v)
+ }
+}
+
+// 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)