From 96375f46459e2a3bdfc4f84701c8f02de213bc32 Mon Sep 17 00:00:00 2001 From: David Stainton Date: Thu, 26 May 2016 09:35:32 +0000 Subject: [PATCH] Add socks5 API from Yawning's or-ctl-filter repo borrow code from this git commit id 8897fe5b847b70fd54f19d1114d04e3bb67912d8 https://github.com/Yawning/or-ctl-filter --- socks5/client.go | 153 ++++++++++++++++++++++ socks5/common.go | 272 +++++++++++++++++++++++++++++++++++++++ socks5/server.go | 200 ++++++++++++++++++++++++++++ socks5/server_rfc1929.go | 84 ++++++++++++ 4 files changed, 709 insertions(+) create mode 100644 socks5/client.go create mode 100644 socks5/common.go create mode 100644 socks5/server.go create mode 100644 socks5/server_rfc1929.go diff --git a/socks5/client.go b/socks5/client.go new file mode 100644 index 0000000..474d2e1 --- /dev/null +++ b/socks5/client.go @@ -0,0 +1,153 @@ +/* + * client.go - SOCSK5 client implementation. + * + * To the extent possible under law, Yawning Angel has waived all copyright and + * related or neighboring rights to or-ctl-filter, using the creative commons + * "cc0" public domain dedication. See LICENSE or + * for full details. + */ + +package socks5 + +import ( + "fmt" + "io" + "net" + "time" +) + +// Redispatch dials the provided proxy and redispatches an existing request. +func Redispatch(proxyNet, proxyAddr string, req *Request) (conn net.Conn, bndAddr *Address, err error) { + defer func() { + if err != nil && conn != nil { + conn.Close() + } + }() + + conn, err = clientHandshake(proxyNet, proxyAddr, req) + if err != nil { + return nil, nil, err + } + bndAddr, err = clientCmd(conn, req) + return +} + +func clientHandshake(proxyNet, proxyAddr string, req *Request) (net.Conn, error) { + conn, err := net.Dial(proxyNet, proxyAddr) + if err != nil { + return nil, err + } + if err := conn.SetDeadline(time.Now().Add(requestTimeout)); err != nil { + return conn, err + } + authMethod, err := clientNegotiateAuth(conn, req) + if err != nil { + return conn, err + } + if err := clientAuthenticate(conn, req, authMethod); err != nil { + return conn, err + } + if err := conn.SetDeadline(time.Time{}); err != nil { + return conn, err + } + + return conn, nil +} + +func clientNegotiateAuth(conn net.Conn, req *Request) (byte, error) { + useRFC1929 := req.Auth.Uname != nil && req.Auth.Passwd != nil + // XXX: Validate uname/passwd lengths, though should always be valid. + + var buf [3]byte + buf[0] = version + buf[1] = 1 + if useRFC1929 { + buf[2] = authUsernamePassword + } else { + buf[2] = authNoneRequired + } + + if _, err := conn.Write(buf[:]); err != nil { + return authNoAcceptableMethods, err + } + + var resp [2]byte + if _, err := io.ReadFull(conn, resp[:]); err != nil { + return authNoAcceptableMethods, err + } + if err := validateByte("version", resp[0], version); err != nil { + return authNoAcceptableMethods, err + } + if err := validateByte("method", resp[1], buf[2]); err != nil { + return authNoAcceptableMethods, err + } + + return resp[1], nil +} + +func clientAuthenticate(conn net.Conn, req *Request, authMethod byte) error { + switch authMethod { + case authNoneRequired: + case authUsernamePassword: + var buf []byte + buf = append(buf, authRFC1929Ver) + buf = append(buf, byte(len(req.Auth.Uname))) + buf = append(buf, req.Auth.Uname...) + buf = append(buf, byte(len(req.Auth.Passwd))) + buf = append(buf, req.Auth.Passwd...) + if _, err := conn.Write(buf); err != nil { + return err + } + + var resp [2]byte + if _, err := io.ReadFull(conn, resp[:]); err != nil { + return err + } + if err := validateByte("version", resp[0], authRFC1929Ver); err != nil { + return err + } + if err := validateByte("status", resp[1], authRFC1929Success); err != nil { + return err + } + default: + panic(fmt.Sprintf("unknown authentication method: 0x%02x", authMethod)) + } + return nil +} + +func clientCmd(conn net.Conn, req *Request) (*Address, error) { + var buf []byte + buf = append(buf, version) + buf = append(buf, byte(req.Cmd)) + buf = append(buf, rsv) + buf = append(buf, req.Addr.raw...) + if _, err := conn.Write(buf); err != nil { + return nil, err + } + + var respHdr [3]byte + if _, err := io.ReadFull(conn, respHdr[:]); err != nil { + return nil, err + } + + if err := validateByte("version", respHdr[0], version); err != nil { + return nil, err + } + if err := validateByte("rep", respHdr[1], byte(ReplySucceeded)); err != nil { + return nil, clientError(respHdr[1]) + } + if err := validateByte("rsv", respHdr[2], rsv); err != nil { + return nil, err + } + + var bndAddr Address + if err := bndAddr.read(conn); err != nil { + return nil, err + } + + if err := conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return &bndAddr, nil +} diff --git a/socks5/common.go b/socks5/common.go new file mode 100644 index 0000000..aaa82a1 --- /dev/null +++ b/socks5/common.go @@ -0,0 +1,272 @@ +/* + * common.go - SOCSK5 common definitons/routines. + * + * To the extent possible under law, Yawning Angel has waived all copyright and + * related or neighboring rights to or-ctl-filter, using the creative commons + * "cc0" public domain dedication. See LICENSE or + * for full details. + */ + +// Package socks5 implements a SOCKS5 client/server. For more information see +// RFC 1928 and RFC 1929. +// +// Notes: +// * GSSAPI authentication, is NOT supported. +// * The authentication provided by the client is always accepted. +// * A lot of the code is shamelessly stolen from obfs4proxy. +package socks5 + +import ( + "errors" + "fmt" + "io" + "net" + "strconv" + "syscall" + "time" +) + +const ( + version = 0x05 + rsv = 0x00 + + atypIPv4 = 0x01 + atypDomainName = 0x03 + atypIPv6 = 0x04 + + authNoneRequired = 0x00 + authUsernamePassword = 0x02 + authNoAcceptableMethods = 0xff + + inboundTimeout = 5 * time.Second + requestTimeout = 30 * time.Second +) + +var errInvalidAtyp = errors.New("invalid address type") + +// 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 +) + +// Command is a SOCKS 5 command. +type Command byte + +// The various SOCKS 5 commands. +const ( + CommandConnect Command = 0x01 + CommandTorResolve Command = 0xf0 + CommandTorResolvePTR Command = 0xf1 +) + +// Address is a SOCKS 5 address + port. +type Address struct { + raw []byte + addrStr string + portStr string +} + +// FromString parses the provided "host:port" format address and populates the +// Address fields. +func (addr *Address) FromString(addrStr string) (err error) { + addr.addrStr, addr.portStr, err = net.SplitHostPort(addrStr) + if err != nil { + return + } + + var raw []byte + if ip := net.ParseIP(addr.addrStr); ip != nil { + if v4Addr := ip.To4(); v4Addr != nil { + raw = append(raw, atypIPv4) + raw = append(raw, v4Addr...) + } else if v6Addr := ip.To16(); v6Addr != nil { + raw = append(raw, atypIPv6) + raw = append(raw, v6Addr...) + } else { + return errors.New("unsupported IP address type") + } + } else { + // Must be a FQDN. + if len(addr.addrStr) > 255 { + return fmt.Errorf("invalid FQDN, len > 255 bytes (%d bytes)", len(addr.addrStr)) + } + raw = append(raw, atypDomainName) + raw = append(raw, addr.addrStr...) + } + + var port uint64 + if port, err = strconv.ParseUint(addr.portStr, 10, 16); err != nil { + return + } + raw = append(raw, byte(port>>8)) + raw = append(raw, byte(port&0xff)) + + addr.raw = raw + return +} + +// String returns the string representation of the address, in "host:port" +// format. +func (addr *Address) String() string { + return addr.addrStr + ":" + addr.portStr +} + +// HostPort returns the string representation of the addess, split into the +// host and port components. +func (addr *Address) HostPort() (string, string) { + return addr.addrStr, addr.portStr +} + +func (addr *Address) read(conn net.Conn) (err error) { + // The address looks like: + // uint8_t atyp + // uint8_t addr[] (Length depends on atyp) + // uint16_t port + + // Read the atype. + var atyp byte + if atyp, err = readByte(conn); err != nil { + return + } + addr.raw = append(addr.raw, atyp) + + // Read the address. + var rawAddr []byte + switch atyp { + case atypIPv4: + rawAddr = make([]byte, net.IPv4len) + if _, err = io.ReadFull(conn, rawAddr); err != nil { + return + } + v4Addr := net.IPv4(rawAddr[0], rawAddr[1], rawAddr[2], rawAddr[3]) + addr.addrStr = v4Addr.String() + case atypDomainName: + var alen byte + if alen, err = readByte(conn); err != nil { + return + } + if alen == 0 { + return fmt.Errorf("domain name with 0 length") + } + rawAddr = make([]byte, alen) + addr.raw = append(addr.raw, alen) + if _, err = io.ReadFull(conn, rawAddr); err != nil { + return + } + addr.addrStr = string(rawAddr) + case atypIPv6: + rawAddr = make([]byte, net.IPv6len) + if _, err = io.ReadFull(conn, rawAddr); err != nil { + return + } + v6Addr := make(net.IP, net.IPv6len) + copy(v6Addr[:], rawAddr) + addr.addrStr = fmt.Sprintf("[%s]", v6Addr.String()) + default: + return errInvalidAtyp + } + addr.raw = append(addr.raw, rawAddr...) + + // Read the port. + var rawPort [2]byte + if _, err = io.ReadFull(conn, rawPort[:]); err != nil { + return + } + port := int(rawPort[0])<<8 | int(rawPort[1]) + addr.portStr = fmt.Sprintf("%d", port) + addr.raw = append(addr.raw, rawPort[:]...) + + return +} + +// ErrorToReplyCode converts an error to the "best" reply code. +func ErrorToReplyCode(err error) ReplyCode { + if cErr, ok := err.(clientError); ok { + return ReplyCode(cErr) + } + 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 { + Auth AuthInfo + Cmd Command + Addr Address + + conn net.Conn +} + +type clientError ReplyCode + +func (e clientError) Error() string { + switch ReplyCode(e) { + case ReplySucceeded: + return "socks5: succeeded" + case ReplyGeneralFailure: + return "socks5: general failure" + case ReplyConnectionNotAllowed: + return "socks5: connection not allowed" + case ReplyNetworkUnreachable: + return "socks5: network unreachable" + case ReplyHostUnreachable: + return "socks5: host unreachable" + case ReplyConnectionRefused: + return "socks5: connection refused" + case ReplyTTLExpired: + return "socks5: ttl expired" + case ReplyCommandNotSupported: + return "socks5: command not supported" + case ReplyAddressNotSupported: + return "socks5: address not supported" + default: + return fmt.Sprintf("socks5: reply code: 0x%02x", e) + } +} + +func readByte(conn net.Conn) (byte, error) { + var tmp [1]byte + if _, err := conn.Read(tmp[:]); err != nil { + return 0, err + } + return tmp[0], nil +} + +func validateByte(descr string, val, expected byte) error { + if val != expected { + return fmt.Errorf("message field '%s' was 0x%02x (expected 0x%02x)", descr, val, expected) + } + return nil +} diff --git a/socks5/server.go b/socks5/server.go new file mode 100644 index 0000000..61c61e4 --- /dev/null +++ b/socks5/server.go @@ -0,0 +1,200 @@ +/* + * server.go - SOCSK5 server implementation. + * + * To the extent possible under law, Yawning Angel has waived all copyright and + * related or neighboring rights to or-ctl-filter, using the creative commons + * "cc0" public domain dedication. See LICENSE or + * for full details. + */ + +package socks5 + +import ( + "bytes" + "fmt" + "io" + "net" + "time" +) + +// 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(inboundTimeout)); 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.conn = 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 { + return req.ReplyAddr(code, nil) +} + +// ReplyAddr sends a SOCKS5 reply to the corresponding request. The BND.ADDR +// and BND.PORT fields are specified by addr, or "0.0.0.0:0" if not provided. +func (req *Request) ReplyAddr(code ReplyCode, addr *Address) 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 + + resp := []byte{version, byte(code), rsv} + if addr == nil { + var nilAddr [net.IPv4len + 2]byte + resp = append(resp, atypIPv4) + resp = append(resp, nilAddr[:]...) + } else { + resp = append(resp, addr.raw...) + } + + _, err := req.conn.Write(resp[:]) + return err + +} + +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 + } + methods := make([]byte, nmethods) + if _, err := io.ReadFull(req.conn, methods); 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.conn.Write(msg); err != nil { + return 0, err + } + + return method, nil +} + +func (req *Request) authenticate(method byte) error { + switch method { + case authNoneRequired: + return nil + case authUsernamePassword: + return req.authRFC1929() + 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) + } +} + +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 + var cmd byte + if err = req.readByteVerify("version", version); err != nil { + req.Reply(ReplyGeneralFailure) + return err + } + if cmd, err = req.readByte(); err != nil { + req.Reply(ReplyGeneralFailure) + return err + } + switch Command(cmd) { + case CommandConnect, CommandTorResolve, CommandTorResolvePTR: + req.Cmd = Command(cmd) + default: + req.Reply(ReplyCommandNotSupported) + return fmt.Errorf("unsupported SOCKS command: 0x%02x", cmd) + } + if err = req.readByteVerify("reserved", rsv); err != nil { + req.Reply(ReplyGeneralFailure) + return err + } + + // Read the destination address/port. + err = req.Addr.read(req.conn) + if err == errInvalidAtyp { + req.Reply(ReplyAddressNotSupported) + } else if err != nil { + req.Reply(ReplyGeneralFailure) + } + + return err +} + +func (req *Request) readByte() (byte, error) { + return readByte(req.conn) +} + +func (req *Request) readByteVerify(descr string, expected byte) error { + val, err := req.readByte() + if err != nil { + return err + } + return validateByte(descr, val, expected) +} diff --git a/socks5/server_rfc1929.go b/socks5/server_rfc1929.go new file mode 100644 index 0000000..7d0566a --- /dev/null +++ b/socks5/server_rfc1929.go @@ -0,0 +1,84 @@ +/* + * server_rfc1929.go - SOCSK 5 server authentication. + * + * To the extent possible under law, Yawning Angel has waived all copyright and + * related or neighboring rights to or-ctl-filter, using the creative commons + * "cc0" public domain dedication. See LICENSE or + * for full details. + */ + +package socks5 + +import ( + "fmt" + "io" +) + +const ( + authRFC1929Ver = 0x01 + authRFC1929Success = 0x00 + authRFC1929Fail = 0x01 +) + +// AuthInfo is the RFC 1929 Username/Password authentication data. +type AuthInfo struct { + Uname []byte + Passwd []byte +} + +func (req *Request) authRFC1929() (err error) { + sendErrResp := func() { + // Swallow write/flush errors, the auth failure is the relevant error. + resp := []byte{authRFC1929Ver, authRFC1929Fail} + req.conn.Write(resp[:]) + } + + // 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") + } + uname := make([]byte, ulen) + if _, err = io.ReadFull(req.conn, uname); 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") + } + passwd := make([]byte, plen) + if _, err = io.ReadFull(req.conn, passwd); err != nil { + sendErrResp() + return + } + + req.Auth.Uname = uname + req.Auth.Passwd = passwd + + resp := []byte{authRFC1929Ver, authRFC1929Success} + _, err = req.conn.Write(resp[:]) + return +}