From a8d7134f1097bd50803da0e2a86c07524e433b51 Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Sun, 12 Apr 2015 19:00:46 +0000 Subject: 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. --- common/socks5/args.go | 96 +++++++++++ common/socks5/args_test.go | 144 ++++++++++++++++ common/socks5/rfc1929.go | 105 +++++++++++ common/socks5/socks5.go | 358 ++++++++++++++++++++++++++++++++++++++ common/socks5/socks_test.go | 412 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1115 insertions(+) create mode 100644 common/socks5/args.go create mode 100644 common/socks5/args_test.go create mode 100644 common/socks5/rfc1929.go create mode 100644 common/socks5/socks5.go create mode 100644 common/socks5/socks_test.go (limited to 'common') 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 + * 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 + * 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 + * 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 + * 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) -- cgit v1.2.3