diff options
| author | Yawning Angel <yawning@torproject.org> | 2015-04-12 19:00:46 +0000 | 
|---|---|---|
| committer | Yawning Angel <yawning@torproject.org> | 2015-04-15 20:50:01 +0000 | 
| commit | a8d7134f1097bd50803da0e2a86c07524e433b51 (patch) | |
| tree | 346403d6ecfdf2a76d8e736862c1b42d165cc919 | |
| parent | 8996cb2646f2721b2d86f5f6b54b5c21d2acc71d (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.
| -rw-r--r-- | ChangeLog | 1 | ||||
| -rw-r--r-- | common/socks5/args.go | 96 | ||||
| -rw-r--r-- | common/socks5/args_test.go | 144 | ||||
| -rw-r--r-- | common/socks5/rfc1929.go | 105 | ||||
| -rw-r--r-- | common/socks5/socks5.go | 358 | ||||
| -rw-r--r-- | common/socks5/socks_test.go | 412 | ||||
| -rw-r--r-- | obfs4proxy/obfs4proxy.go | 51 | 
7 files changed, 1142 insertions, 25 deletions
| @@ -6,6 +6,7 @@ Changes in version 0.0.5 - UNRELEASED:   - Moved the leveled logging wrappers into common/log so they are usable     in transport implementations.   - Added a DEBUG log level. + - Use a bundled SOCKS 5 server instead of goptlib's SocksListener.  Changes in version 0.0.4 - 2015-02-17   - Improve the runtime performance of the obfs4 handshake tests. 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) diff --git a/obfs4proxy/obfs4proxy.go b/obfs4proxy/obfs4proxy.go index 608dd55..33fbce7 100644 --- a/obfs4proxy/obfs4proxy.go +++ b/obfs4proxy/obfs4proxy.go @@ -45,6 +45,7 @@ import (  	"git.torproject.org/pluggable-transports/goptlib.git"  	"git.torproject.org/pluggable-transports/obfs4.git/common/log" +	"git.torproject.org/pluggable-transports/obfs4.git/common/socks5"  	"git.torproject.org/pluggable-transports/obfs4.git/transports"  	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"  ) @@ -58,10 +59,6 @@ const (  var stateDir string  var termMon *termMonitor -// DialFn is a function pointer to a function that matches the net.Dialer.Dial -// interface. -type DialFn func(string, string) (net.Conn, error) -  func clientSetup() (launched bool, listeners []net.Listener) {  	ptClientInfo, err := pt.ClientSetup(transports.Transports())  	if err != nil { @@ -89,14 +86,14 @@ func clientSetup() (launched bool, listeners []net.Listener) {  			continue  		} -		ln, err := pt.ListenSocks("tcp", socksAddr) +		ln, err := net.Listen("tcp", socksAddr)  		if err != nil {  			pt.CmethodError(name, err.Error())  			continue  		}  		go clientAcceptLoop(f, ln, ptClientProxy) -		pt.Cmethod(name, ln.Version(), ln.Addr()) +		pt.Cmethod(name, socks5.Version(), ln.Addr())  		log.Infof("%s - registered listener: %s", name, ln.Addr()) @@ -108,10 +105,10 @@ func clientSetup() (launched bool, listeners []net.Listener) {  	return  } -func clientAcceptLoop(f base.ClientFactory, ln *pt.SocksListener, proxyURI *url.URL) error { +func clientAcceptLoop(f base.ClientFactory, ln net.Listener, proxyURI *url.URL) error {  	defer ln.Close()  	for { -		conn, err := ln.AcceptSocks() +		conn, err := ln.Accept()  		if err != nil {  			if e, ok := err.(net.Error); ok && !e.Temporary() {  				return err @@ -122,42 +119,46 @@ func clientAcceptLoop(f base.ClientFactory, ln *pt.SocksListener, proxyURI *url.  	}  } -func clientHandler(f base.ClientFactory, conn *pt.SocksConn, proxyURI *url.URL) { +func clientHandler(f base.ClientFactory, conn net.Conn, proxyURI *url.URL) {  	defer conn.Close()  	termMon.onHandlerStart()  	defer termMon.onHandlerFinish()  	name := f.Transport().Name() -	addrStr := log.ElideAddr(conn.Req.Target) -	log.Infof("%s(%s) - new connection", name, addrStr) + +	// Read the client's SOCKS handshake. +	socksReq, err := socks5.Handshake(conn) +	if err != nil { +		log.Errorf("%s - client failed socks handshake: %s", name, err) +		return +	} +	addrStr := log.ElideAddr(socksReq.Target)  	// Deal with arguments. -	args, err := f.ParseArgs(&conn.Req.Args) +	args, err := f.ParseArgs(&socksReq.Args)  	if err != nil {  		log.Errorf("%s(%s) - invalid arguments: %s", name, addrStr, err) -		conn.Reject() +		socksReq.Reply(socks5.ReplyGeneralFailure)  		return  	}  	// Obtain the proxy dialer if any, and create the outgoing TCP connection. -	var dialFn DialFn -	if proxyURI == nil { -		dialFn = proxy.Direct.Dial -	} else { -		// This is unlikely to happen as the proxy protocol is verified during -		// the configuration phase. +	dialFn := proxy.Direct.Dial +	if proxyURI != nil {  		dialer, err := proxy.FromURL(proxyURI, proxy.Direct)  		if err != nil { +			// This should basically never happen, since config protocol +			// verifies this.  			log.Errorf("%s(%s) - failed to obtain proxy dialer: %s", name, addrStr, log.ElideError(err)) -			conn.Reject() +			socksReq.Reply(socks5.ReplyGeneralFailure)  			return  		}  		dialFn = dialer.Dial  	} -	remoteConn, err := dialFn("tcp", conn.Req.Target) // XXX: Allow UDP? +	remoteConn, err := dialFn("tcp", socksReq.Target) // XXX: Allow UDP?  	if err != nil {  		log.Errorf("%s(%s) - outgoing connection failed: %s", name, addrStr, log.ElideError(err)) -		conn.Reject() +		socksReq.Reply(socks5.ErrorToReplyCode(err))  		return  	}  	defer remoteConn.Close() @@ -167,12 +168,12 @@ func clientHandler(f base.ClientFactory, conn *pt.SocksConn, proxyURI *url.URL)  	remote, err := f.WrapConn(remoteConn, args)  	if err != nil {  		log.Errorf("%s(%s) - handshake failed: %s", name, addrStr, log.ElideError(err)) -		conn.Reject() +		socksReq.Reply(socks5.ReplyGeneralFailure)  		return  	} -	err = conn.Grant(remoteConn.RemoteAddr().(*net.TCPAddr)) +	err = socksReq.Reply(socks5.ReplySucceeded)  	if err != nil { -		log.Errorf("%s(%s) - SOCKS grant failed: %s", name, addrStr, log.ElideError(err)) +		log.Errorf("%s(%s) - SOCKS reply failed: %s", name, addrStr, log.ElideError(err))  		return  	} | 
