summaryrefslogtreecommitdiff
path: root/common/socks5/socks5.go
diff options
context:
space:
mode:
Diffstat (limited to 'common/socks5/socks5.go')
-rw-r--r--common/socks5/socks5.go358
1 files changed, 358 insertions, 0 deletions
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
+}