blob: cb9ae0a97af5b2d776b198a29f1a81099cedab6d [file] [log] [blame]
package socksproxy
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
)
// readMethods implements RFC1928 3. “Procedure for TCP-based clients”,
// paragraph 3. It receives a 'version identifier/method selection message' from
// r and returns the methods supported by the client.
func readMethods(r io.Reader) ([]method, error) {
var ver uint8
if err := binary.Read(r, binary.BigEndian, &ver); err != nil {
return nil, fmt.Errorf("when reading ver: %w", err)
}
if ver != 5 {
return nil, fmt.Errorf("unimplemented version %d", ver)
}
var nmethods uint8
if err := binary.Read(r, binary.BigEndian, &nmethods); err != nil {
return nil, fmt.Errorf("when reading nmethods: %w", err)
}
methodBytes := make([]byte, nmethods)
if _, err := io.ReadFull(r, methodBytes); err != nil {
return nil, fmt.Errorf("while reading methods: %w", err)
}
methods := make([]method, nmethods)
for i, m := range methodBytes {
methods[i] = method(m)
}
return methods, nil
}
// writeMethod implements RFC1928 3. “Procedure for TCP-based clients”,
// paragraph 5. It sends a selected method to w.
func writeMethod(w io.Writer, m method) error {
if err := binary.Write(w, binary.BigEndian, uint8(5)); err != nil {
return fmt.Errorf("while writing version: %w", err)
}
if err := binary.Write(w, binary.BigEndian, uint8(m)); err != nil {
return fmt.Errorf("while writing method: %w", err)
}
return nil
}
// method is an RFC1928 authentication method.
type method uint8
const (
methodNoAuthenticationRequired method = 0
methodNoAcceptableMethods method = 0xff
)
// negotiateMethod implements the entire flow RFC1928 3. “Procedure for
// TCP-based clients” by negotiating for the 'NO AUTHENTICATION REQUIRED'
// authentication method, and failing otherwise.
func negotiateMethod(rw io.ReadWriter) error {
methods, err := readMethods(rw)
if err != nil {
return fmt.Errorf("could not read methods: %w", err)
}
found := false
for _, m := range methods {
if m == methodNoAuthenticationRequired {
found = true
break
}
}
if !found {
// Discard error, as this connection is failed anyway.
writeMethod(rw, methodNoAcceptableMethods)
return fmt.Errorf("no acceptable methods found")
}
if err := writeMethod(rw, methodNoAuthenticationRequired); err != nil {
return fmt.Errorf("could not respond with method: %w", err)
}
return nil
}
var (
// errNotConnect is returned by readRequest when the request contained some
// other request than CONNECT.
errNotConnect = errors.New("not CONNECT")
// errUnsupportedAddressType is returned by readRequest when the request
// contained some unsupported address type (not IPv4 or IPv6).
errUnsupportedAddressType = errors.New("unsupported address type")
)
// readRequest implements RFC1928 4. “Requests” by reading a SOCKS request from
// r and ensuring it's an IPv4/IPv6 CONNECT request. The parsed address/port
// pair is then returned.
func readRequest(r io.Reader) (*connectRequest, error) {
header := struct {
Ver uint8
Cmd uint8
Rsv uint8
Atyp uint8
}{}
if err := binary.Read(r, binary.BigEndian, &header); err != nil {
return nil, fmt.Errorf("when reading request header: %w", err)
}
if header.Ver != 5 {
return nil, fmt.Errorf("invalid version %d", header.Ver)
}
if header.Cmd != 1 {
return nil, errNotConnect
}
var addrBytes []byte
switch header.Atyp {
case 1:
addrBytes = make([]byte, 4)
case 4:
addrBytes = make([]byte, 4)
default:
return nil, errUnsupportedAddressType
}
if _, err := io.ReadFull(r, addrBytes); err != nil {
return nil, fmt.Errorf("when reading address: %w", err)
}
var port uint16
if err := binary.Read(r, binary.BigEndian, &port); err != nil {
return nil, fmt.Errorf("when reading port: %w", err)
}
return &connectRequest{
address: addrBytes,
port: port,
}, nil
}
type connectRequest struct {
address net.IP
port uint16
}
// Reply is an RFC1928 6. “Replies” reply field value. It's returned to the
// client by internal socksproxy code or a Handler to signal a success or error
// condition within an RFC1928 reply.
type Reply uint8
const (
ReplySucceeded Reply = 0
ReplyGeneralFailure Reply = 1
ReplyConnectionNotAllowed Reply = 2
ReplyNetworkUnreachable Reply = 3
ReplyHostUnreachable Reply = 4
ReplyConnectionRefused Reply = 5
ReplyTTLExpired Reply = 6
ReplyCommandNotSupported Reply = 7
ReplyAddressTypeNotSupported Reply = 8
)
// writeReply implements RFC1928 6. “Replies” by sending a given Reply, bind
// address and bind port to w. An error is returned if the given bind address is
// invaild, or if a communication error occurred.
func writeReply(w io.Writer, r Reply, bindAddr net.IP, bindPort uint16) error {
var atyp uint8
switch len(bindAddr) {
case 4:
atyp = 1
case 16:
atyp = 4
default:
return fmt.Errorf("unsupported bind address type")
}
header := struct {
Ver uint8
Reply uint8
Rsv uint8
Atyp uint8
}{
Ver: 5,
Reply: uint8(r),
Rsv: 0,
Atyp: atyp,
}
if err := binary.Write(w, binary.BigEndian, &header); err != nil {
return fmt.Errorf("when writing reply header: %w", err)
}
if _, err := w.Write(bindAddr); err != nil {
return fmt.Errorf("when writing reply bind address: %w", err)
}
if err := binary.Write(w, binary.BigEndian, bindPort); err != nil {
return fmt.Errorf("when writing reply bind port: %w", err)
}
return nil
}