commit a8d7134f1097bd50803da0e2a86c07524e433b51
Author: Yawning Angel <yawn...@torproject.org>
Date:   Sun Apr 12 19:00:46 2015 +0000

    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.
---
 ChangeLog                   |    1 +
 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 +++++++++++++++++++++++++++++++++++++++++++
 obfs4proxy/obfs4proxy.go    |   51 +++---
 7 files changed, 1142 insertions(+), 25 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 50a2f49..6c7213c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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
        }
 

_______________________________________________
tor-commits mailing list
tor-commits@lists.torproject.org
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits

Reply via email to