socksy5

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2023 License: MIT Imports: 13 Imported by: 0

README

SOCKSY5

Socksy5 provides a Golang SOCKS5 middle layer that handle communication with clients for you, but let you to do decision on what to do with the handshakes and requests.

What the middle layer does:
  • Reading requests and sending replies
  • Wrap requests and provide methods for accepting and rejecting them
  • Attach connection from external code for CONNECT, BIND requests
  • Emitting different types of log entries
  • Support multi-homed BINDing and UDP ASSOCIATEing
What the middle layer requires you to do:
  • Decision on accepting / rejecting incoming client handshake and requests
  • Select which auth method and subnegotiation to use (supports custom)
  • Dial and pass outbound connections to the middle layer (CONNECT)
  • Listen for connections from application servers (BIND)
  • UDP relaying (UDP ASSOCIATE)
  • Writing logs to the console or files
If you want to make a SOCKS5 server fast, read on.
Some other implementation in this module:
  • Func Connect for CONNECT and outbound dialing.
  • Binder for BIND and listening for incoming connection.
  • Associator for UDP ASSOCIATE and UDP relaying.
  • NoAuthSubneg for NO AUTHENTICATION subnegotiation.
  • UsrPwdSubneg for USERNAME/PASSWORD subnegotiation.
  • NoCap for no encapsulation/decapsulation.

For details, see the Go reference.

Note

  • Issues, suggestions and PRs welcome
  • I will be busy for quite a long time, expect less maintanence.
  • It's still in unstable release, expect:
    • Weird error and log util design
      • If you have better ideas, submit issues!
    • Potential bugs
      • I use a SOCKS5 proxy which is based on socksy5 for daily use, so it's not...that buggy.

Documentation

Overview

Description

Package socksy5 provides a SOCKS5 middle layer and utils for simple request handling. MidLayer implements the middle layer, which accepts client connections in the form of net.Conn (see MidLayer.ServeClient), then wraps client handshakes and requests as structs, letting external code to decide whether accept or reject, which kind of subnegotiation to use e.t.c..

This provides advantages when you need multi-homed BND or UDP ASSOCIATION processing, custom subnegotiation and encryption, attaching special connection to CONNECT requests.

Besides that, socksy5 also provides Connect, Binder and Associator as simple handlers for CONNECT, BND and UDP ASSOC requests. Listen is also provided as a simple listening util which passes net.Conn to MidLayer automatically. They are for ease of use if you want to set up a SOCKS5 server fast, thus they only have basic features. You can handle handshakes and requests yourself if they don't meet your requirement.

How to use

First pass a net.Conn to a MidLayer instance, then MidLayer will begin communicating with the client. When client begins handshakes or sends requests, MidLayer will emit Handshake, ConnectRequest, BindRequest and AssocRequest via channels. Call methods of them to decide which kind of authentication to use, whether accept or reject and so on. Logs are emitted via channels too. See MidLayer.LogChan, MidLayer.HandshakeChan, MidLayer.RequestChan. User of this package should read Request, as it contains general info about different types of requests.

Note

socksy5 provides limited implementations of authenticate methods, for quite a long time. MidLayer does relay TCP traffic, but it doesn't dial outbound or relay UDP traffic.

Index

Constants

View Source
const (
	VerUsrPwd         byte = 0x01
	UsrPwdStatSuccess byte = 0x00
)

Constants used in Username/Password Authentication for SOCKS V5 (RFC 1929).

View Source
const (
	SeverityDebug   = "debug"
	SeverityInfo    = "info"
	SeverityWarning = "warning"
	SeverityError   = "error"
)

Severity levels used by LogEntry.

View Source
const (
	// Channel capacity of all channels returned by MidLayer's channel methods.
	ChanCap = 64
	// Time to close connection if auth failed, request denied, e.t.c..
	PeriodClose = time.Second * time.Duration(3)
	// Time to wait for external code to react to handshakes and requests.
	PeriodAutoDeny = time.Second * time.Duration(30)
)

Constants of MidLayer policy.

View Source
const (
	MethodNoAuth byte = 0x00
	MethodGSSAPI byte = 0x01
	MethodUsrPwd byte = 0x02
	MethodCHAP   byte = 0x03

	MethodCRAM byte = 0x05
	MethodSSL  byte = 0x06
	MethodNDS  byte = 0x07
	MethodMAF  byte = 0x08
	MethodJRB  byte = 0x09

	MethodNoAccepted byte = 0xFF
)

Authentication METHOD codes.

View Source
const (
	CmdCONNECT byte = 0x01
	CmdBIND    byte = 0x02
	CmdASSOC   byte = 0x03
)

CMD codes.

View Source
const (
	ATYPV4     byte = 0x01 // IPv4
	ATYPDOMAIN byte = 0x03 // Fully-qualified domain name
	ATYPV6     byte = 0x04 // IPv6
)

ATYP codes (address types)

View Source
const (
	RepSucceeded               byte = 0x00
	RepGeneralFailure          byte = 0x01
	RepConnNotAllowedByRuleset byte = 0x02
	RepNetworkUnreachable      byte = 0x03
	RepHostUnreachable         byte = 0x04
	RepConnRefused             byte = 0x05
	RepTtlExpired              byte = 0x06
	RepCmdNotSupported         byte = 0x07
	RepAddrTypeNotSupported    byte = 0x08
)

REP reply codes.

View Source
const RSV byte = 0x00

Value of reserved bytes

View Source
const VerSOCKS5 byte = 0x05

SOCKS5 VER byte

Variables

View Source
var ErrAcceptOrDenyFailed = errors.New("request already handled")

ErrAcceptOrDenyFailed is used by Connect, Binder and Associator. It indicates that the accept and deny methods of the request returned not ok.

View Source
var ErrAuthFailed = errors.New("auth failed")
View Source
var ErrDuplicatedRequest = errors.New("duplicated request with same parameters")

ErrDuplicatedRequest is returned by Associator.Handle and Binder.Handle indicating that another request with same parameters is being handled, e.g. the Binder is already listening the address stated by the BIND request.

View Source
var ErrMalformed = errors.New("malformed")

ErrMalformed represents protocol format violation. Usually a more specific error type is used: VerIncorrectError, RsvViolationError, CmdNotSupportedError, ATYPNotSupportedError and [UsrPwdVerIncorrectErr].

Functions

func Connect

func Connect(req *ConnectRequest) error

Connect handles the CONNECT request, accepting or denying it accordingly.

Currently if req is to be denied, only RepGeneralFailure will be replied.

func Listen

func Listen(addr string, ml *MidLayer) error

Listen listens addr and passes connections to ml. Listen also blocks until any error occurs.

addr can be a host name, in this case Listen will look it up and listen on resolved IP addresses.

Types

type ATYPNotSupportedError

type ATYPNotSupportedError byte

func (ATYPNotSupportedError) Error

func (e ATYPNotSupportedError) Error() string

func (ATYPNotSupportedError) Is

func (e ATYPNotSupportedError) Is(target error) bool

Is returns true if target is ErrMalformed.

func (ATYPNotSupportedError) Unwrap

func (e ATYPNotSupportedError) Unwrap() error

Unwrap returns errors.ErrUnsupported.

type AddrPort

type AddrPort struct {
	// ATYP byte value
	Type byte

	// If ATYP is ATYPDOMAIN,
	// the first byte that specifies the FQDN length is omitted,
	// length of Addr represents it implicitly.
	Addr []byte
	Port uint16

	// One of "tcp" and "udp", [AddrPort.Network] relies on this field.
	Protocol string
}

An AddrPort represents the address and the port sent in SOCKS5 requests and replies.

func ParseAddrPort

func ParseAddrPort(s string) (*AddrPort, error)

ParseAddrPort parses s to AddrPort. If s is not a valid IP address, ParseAddrPort will try parse it as <host name>:<port>, WITHOUT syntax checking on the host name.

In the returned AddrPort, [AddrPort.Protocol] will be empty.

func (*AddrPort) Equal

func (a *AddrPort) Equal(x *AddrPort) bool

Equal tests whether a and x are the same address, returns false if either a or x is nil. Both a.Protocol and x.Protocol are ignored.

func (*AddrPort) Host

func (a *AddrPort) Host() string

Host returns the string form of a, without the port part.

func (*AddrPort) MarshalBinary

func (a *AddrPort) MarshalBinary() (data []byte, err error)

MarshalBinary returns raw bytes used in SOCKS5 traffic. (ATYP+ADDR+PORT)

func (*AddrPort) Network

func (a *AddrPort) Network() string

Network returns the network of a. If a is nil, "<nil>" is returned. If a.Type is one of ATYPV4 or ATYPV6, Network will append "4" or "6" to a.Protocol accordingly. If a.Type is ATYPDOMAIN, Network will just return a.Protocol. Otherwise, Network returns the hex of a.Type.

func (*AddrPort) String

func (a *AddrPort) String() string

type AssocRequest

type AssocRequest struct {
	Request
	// contains filtered or unexported fields
}

func (*AssocRequest) Accept

func (r *AssocRequest) Accept(addr string, notify func(reason error)) (terminate func() error, ok bool)

Accept accepts the request.

terminate can be used to terminate the association by closing the control connection. Be aware it is nil if ok is false.

notify is called when the association terminates, e.g. TCP disconnection, IO error, call on terminate. If the client closed the control connection, reason will be io.EOF. If terminate is called, reason will be nil. Otherwise, reason will be the read error on the control connection. notify will only be called once, if it's not nil.

type Associator

type Associator struct {
	// Hostname of the server, not to be confused with listening address.
	// This is the address that will be sent in the first BND reply.
	// RFC 1928 states that addresses in replies to UDP ASSOCIATE requests
	// shall be IP addresses, but a host name is considered valid here.
	// Do not modify this field when Associator is running.
	Hostname string
	// contains filtered or unexported fields
}

Associator relays UDP packets for UDP ASSOCIATE requests.

All methods of Associator can be called simultaneously.

func (*Associator) Handle

func (a *Associator) Handle(req *AssocRequest, addr string) error

Handle handles the UDP ASSOCIATE request req.

addr is the address that a will listen for UDP packets from the client and send UDP packets to the client. It can be empty, in that case a will use all zero addresses with a system allocated port. If the host part in addr is a host name, Handle will look it up and listen on all of the resulting IP addresses. If the port in addr is 0, a system allocated port will be chosen. Note that if a host name is used in addr, Handle will duplicate host-to-client UDP packets and send them out using all of the IP addresses associated with the FQDN.

type BindRequest

type BindRequest struct {
	Request
	// contains filtered or unexported fields
}

func (*BindRequest) Accept

func (r *BindRequest) Accept(addr string) (ok bool)

Accept accepts the request, and tells the client which address the SOCKS server will listen on. This is the first reply from the server.

func (*BindRequest) Bind

func (r *BindRequest) Bind(conn net.Conn) (ok bool)

Bind binds the client. This is the second reply from the server.

No-op if the first reply is not decided, once it is, Bind can be called again.

Once r is accepted, r will wait for the decision on the second reply WITHOUT timeout, even if the connection to the client is closed.

func (*BindRequest) DenyBind

func (r *BindRequest) DenyBind(rep byte, addr string) (ok bool)

DenyBind denies the request. No-op if the first reply is not decided, once it is, DenyBind can be called again.

type Binder

type Binder struct {
	// Hostname of the server, not to be confused with listening address.
	// This is the address that will be sent in the first BND reply.
	// RFC 1928 states that the addresses in replies to BIND requests shall
	// be IP addresses, but a host name is considered valid here.
	// Do not modify this field when Binder is running.
	Hostname string
	// contains filtered or unexported fields
}

Binder handles the BND requests.

Binder can listen for inbounds for different requests on the same port. Thus it is totally ok if you want to handle for multiple requests on one fixed port. Although, please note that, if Binder is not listening for any inbound on a port, the listener on that port will be closed, and will only be created again when next time Binder is required to listen on that port.

Currently if req is to be denied, only RepGeneralFailure will be replied.

func (*Binder) Handle

func (b *Binder) Handle(req *BindRequest, addr string, timeout time.Duration) error

Handle handles the BND request, blocks until error or successful bind. It can be called simultainously.

addr represents the address to listen at, and it can be empty, in this case Handle will listen on 0.0.0.0 and :: with one single system allocated port. If the host part in addr is a host name, Handle will resolve it to IP addresses and listen on all of them. If the port in addr is 0, Handle will try to use one single system allocated port for all of them.

Handle doesn't wait for the data transmission after the 2nd reply to finish.

timeout is disabled if it's 0.

type Capsulator

type Capsulator interface {
	// Used for TCP connections.
	// MidLayer will invoke Write for encapsulation, and Read for decapsulation.
	// Connection is closed if non-nil error is returned.
	io.ReadWriter

	// Used for UDP packets. Packet is dropped if non-nil error is returned.
	// [MidLayer] doesn't actually invoke these methods,
	// Associator will invoke them though.
	EncapPacket(p []byte) ([]byte, error)
	DecapPacket(p []byte) ([]byte, error)
}

Capsulator does encapsulation and decapsulation as corresponding auth method requires.

type CmdNotSupportedError

type CmdNotSupportedError byte

func (CmdNotSupportedError) Error

func (e CmdNotSupportedError) Error() string

func (CmdNotSupportedError) Is

func (e CmdNotSupportedError) Is(target error) bool

Is returns true if target is ErrMalformed.

func (CmdNotSupportedError) Unwrap

func (e CmdNotSupportedError) Unwrap() error

Unwrap returns errors.ErrUnsupported.

type ConnectRequest

type ConnectRequest struct {
	Request
	// contains filtered or unexported fields
}

func (*ConnectRequest) Accept

func (r *ConnectRequest) Accept(conn net.Conn) (ok bool)

type Handshake

type Handshake struct {
	// contains filtered or unexported fields
}

A Handshake represents the version identifier/method selection message. The message is called handshake in this entire module because...its full name is just too long. Handshake will be denied automatically if it's not accepted or denied after PeriodAutoDeny.

All methods of Handshake can be called simultainously.

func (*Handshake) Accept

func (hs *Handshake) Accept(method byte, neg Subnegotiator) (ok bool)

Accept accepts the handshake, but also instead denies the request if params are invalid, e.g. when method is MethodNoAccepted.

Can be called only once, furthur calls are no-op.

func (*Handshake) Deny

func (hs *Handshake) Deny() (ok bool)

Deny denies the handshake by returning NO ACCEPTABLE METHODS.

Can be called only once, furthur calls are no-op.

func (*Handshake) LocalAddr

func (hs *Handshake) LocalAddr() net.Addr

func (*Handshake) Methods

func (hs *Handshake) Methods() []byte

Methods returns client's supported auth methods. Methods might return 0 method, or include method code 0xFF if the client did send so.

func (*Handshake) RemoteAddr

func (hs *Handshake) RemoteAddr() net.Addr

func (*Handshake) UUID

func (hs *Handshake) UUID() uuid.UUID

UUID returns the UUID of the session of hs. As soon as the MidLayer read the handshake message, the connection is considered as a valid session and is bound with a UUID. You can use the UUID to tell which handshake and which request belongs to which connection.

type LogEntry

type LogEntry struct {
	Time      time.Time // Timestamp
	Severity  string    // Severity of this error, one of severity constants
	Verbosity int       // Used by debug entries, the higher the more verbose, starts from 0
	Err       error     // Inner error
}

A LogEntry contains time stamp, severity and corresponding error message.

The returned string of its Error func doesn't include timestamp and severity.

func (*LogEntry) Error

func (e *LogEntry) Error() string

func (*LogEntry) String

func (e *LogEntry) String() string

String returns a string representation of LogEntry, in the format of [SEVERITY] HH:MM:SS ERROR

If e.Severity is SeverityDebug, SEVERITY is followed by a space and e.Verbosity.

func (*LogEntry) Unwrap

func (e *LogEntry) Unwrap() error

type MidLayer

type MidLayer struct {
	// contains filtered or unexported fields
}

A MidLayer is a SOCKS5 middle layer. See package description for detail.

All methods of MidLayer can be called simultaineously.

func (*MidLayer) Close

func (ml *MidLayer) Close() error

Close closes all established connections. It's useful if you want to kill all sessions.

If a connection has failed to close, ml won't try to close it next time. errs contain errors returned by net.Conn.Close.

If errors occur, Close joins them with errors.Join and return the result.

func (*MidLayer) HandshakeChan

func (ml *MidLayer) HandshakeChan() <-chan *Handshake

All channel methods create a corresponding channel if not ever created. If no channel is created or the channel is full, corresponding handshakes are rejected by closing connection, instead of sending a reply.

func (*MidLayer) LogChan

func (ml *MidLayer) LogChan() <-chan LogEntry

All channel methods create a corresponding channel if not ever created. If no channel is created or the channel is full, corresponding log entries are discarded.

func (*MidLayer) RequestChan

func (ml *MidLayer) RequestChan() <-chan any

RequestChan is guaranteed to return a channel that receives one of types *ConnectRequest, *BindRequest and *AssocRequest.

All channel methods create a corresponding channel if not ever created. If no channel is created or the channel is full, corresponding requests are rejected with RepGeneralFailure.

func (*MidLayer) ServeClient

func (ml *MidLayer) ServeClient(conn net.Conn) error

ServeClient starts serving the client and blocks til finish.

type NoAuthSubneg

type NoAuthSubneg struct{}

A NoAuthSubneg is a Subnegotiator that does no negotiation at all. It's typically used for NO AUTHENTICATION.

func (NoAuthSubneg) Negotiate

func (n NoAuthSubneg) Negotiate(rw io.ReadWriter) (Capsulator, error)

func (NoAuthSubneg) Type

func (n NoAuthSubneg) Type() string

Type returns "NO AUTHENTICATION".

type NoCap

type NoCap struct {
	// contains filtered or unexported fields
}

NoCap is a Capsulator that doesn't encapsulate/decapsulate at all. It's used for NO AUTHENTICATION and Username/Password Authentication.

func (NoCap) DecapPacket

func (c NoCap) DecapPacket(p []byte) ([]byte, error)

func (NoCap) EncapPacket

func (c NoCap) EncapPacket(p []byte) ([]byte, error)

func (NoCap) Read

func (c NoCap) Read(p []byte) (n int, err error)

func (NoCap) Write

func (c NoCap) Write(p []byte) (n int, err error)

type OpError

type OpError struct {
	Op         string // E.g. "read handshake", "serve", "reply".
	LocalAddr  net.Addr
	RemoteAddr net.Addr
	Err        error // Inner error
}

An OpError contains Op string describing in which operation has the error occured. Currently the error util of socksy5 is not well designed, so OpError is for now just for the convenience of converting errors to strings.

func (*OpError) Error

func (e *OpError) Error() string

func (*OpError) Unwrap

func (e *OpError) Unwrap() error

type RelayError

type RelayError struct {
	ClientRemoteAddr net.Addr
	ClientLocalAddr  net.Addr
	HostRemoteAddr   net.Addr
	HostLocalAddr    net.Addr
	Client2HostErr   error
	Host2ClientErr   error
}

A RelayError represents errors and address info of TCP traffic relaying for CONNECT and BIND requests.

func (*RelayError) Error

func (e *RelayError) Error() string

func (*RelayError) Unwrap

func (e *RelayError) Unwrap() (errs []error)

type Request

type Request struct {
	// contains filtered or unexported fields
}

A Request represents a client request. Use ConnectRequest, BindRequest and AssocRequest for handling.

Accept / Deny methods of different request types can be called only once. Furthur calls are no-op and return ok being false.

Requests are denied if params passed to Accept funcs are invalid, e.g. addr string doesn't contain a port number, net.Addr returned by conn params is invalid. However, port 0 is considered valid and will be sent as is.

A Request will be denied automatically if it's not accepted or denied after PeriodAutoDeny, with exception being BindRequest, see BindRequest.Bind.

All methods of all request types can be called simultainously.

func (*Request) Capsulation

func (r *Request) Capsulation() Capsulator

Capsulation returns the Capsulator in use.

func (*Request) Deny

func (r *Request) Deny(rep byte, addr string) (ok bool)

Deny denies the request with REP byte code. If rep is RepSucceeded, it's replaced by RepGeneralFailure.

addr is used for BND fields. If addr is invalid, BND.ADDR will be set to empty domain name, and BND.PORT will be set to 0.

func (*Request) Dst

func (r *Request) Dst() *AddrPort

Dst returns the DST fields in the request message.

func (*Request) LocalAddr

func (r *Request) LocalAddr() net.Addr

func (*Request) RemoteAddr

func (r *Request) RemoteAddr() net.Addr

func (*Request) UUID

func (r *Request) UUID() uuid.UUID

UUID returns the UUID of the session of r. As soon as the MidLayer read the handshake message, the connection is considered as a valid session and is bound with a UUID. You can use the UUID to tell which handshake and which request belongs to which connection.

type RequestNotHandledError

type RequestNotHandledError struct {
	Type    string // One of "handshake", "CONNECT", "BIND", "UDP ASSOCIATE"
	Timeout bool   // If the request is not handled in a duration of PeriodAutoDeny
}

A RequestNotHandledError can be received from the error channel when a handshake or request is not handled by external code.

func (*RequestNotHandledError) Error

func (e *RequestNotHandledError) Error() string

type RsvViolationError

type RsvViolationError byte

func (RsvViolationError) Error

func (e RsvViolationError) Error() string

func (RsvViolationError) Is

func (e RsvViolationError) Is(target error) bool

Is returns true if target is ErrMalformed.

type Subnegotiator

type Subnegotiator interface {
	Negotiate(io.ReadWriter) (Capsulator, error)
	Type() string
}

Subnegotiator does subnegotiation after an auth method has been chosen.

When subnegotiation begins, MidLayer will pass net.Conn to Negotiate. Implementation should retain the ReadWriter for capsulation use. If nil Capsulator is returned, NoCap is used instead. Connection is closed if non-nil error is returned.

type UsrPwdSubneg

type UsrPwdSubneg struct {
	// List of username password pair.
	// A list entry is to be ignored if its number of elements is not 2.
	List [][][]byte
}

A UsrPwdSubneg is a Subnegotiator for Username/Password Authentication. Implements RFC 1929.

func (UsrPwdSubneg) Negotiate

func (n UsrPwdSubneg) Negotiate(rw io.ReadWriter) (c Capsulator, err error)

func (UsrPwdSubneg) Type

func (n UsrPwdSubneg) Type() string

Type returns "USERNAME/PASSWORD".

type UsrPwdVerIncorrectError

type UsrPwdVerIncorrectError byte

func (UsrPwdVerIncorrectError) Error

func (e UsrPwdVerIncorrectError) Error() string

func (UsrPwdVerIncorrectError) Is

func (e UsrPwdVerIncorrectError) Is(target error) bool

Is returns true if target is ErrMalformed.

type VerIncorrectError

type VerIncorrectError byte

func (VerIncorrectError) Error

func (e VerIncorrectError) Error() string

func (VerIncorrectError) Is

func (e VerIncorrectError) Is(target error) bool

Is returns true if target is ErrMalformed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL