| // package socksproxy implements a limited subset of the SOCKS 5 (RFC1928) | 
 | // protocol in the form of a pluggable Proxy object. However, this | 
 | // implementation is _not_ RFC1928 compliant, as it does not implement GSSAPI | 
 | // (which is mandated by the spec). It currently only implements CONNECT | 
 | // requests to IPv4/IPv6 addresses. It also doesn't implement any | 
 | // timeout/keepalive system for killing inactive connections. | 
 | // | 
 | // The intended use of the library is internally within Metropolis development | 
 | // environments for contacting test clusters. The code is simple and robust, but | 
 | // not really productionized (as noted above - no timeouts and no authentication | 
 | // make it a bad idea to ever expose this proxy server publicly). | 
 | // | 
 | // There are multiple other, existing Go SOCKS4/5 server implementations, but | 
 | // many of them are either not context aware, part of a larger project (and thus | 
 | // difficult to extract) or are brand new/untested/bleeding edge code. | 
 | package socksproxy | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"fmt" | 
 | 	"io" | 
 | 	"log" | 
 | 	"net" | 
 | 	"strconv" | 
 | ) | 
 |  | 
 | // Handler should be implemented by socksproxy users to allow SOCKS connections | 
 | // to be proxied in any other way than via the HostHandler. | 
 | type Handler interface { | 
 | 	// Connect is called by the server any time a SOCKS client sends a CONNECT | 
 | 	// request. The function should return a ConnectResponse describing some | 
 | 	// 'backend' connection, ie. the connection that will then be exposed to the | 
 | 	// SOCKS client. | 
 | 	// | 
 | 	// Connect should return with Error set to a non-default value to abort/deny the | 
 | 	// connection request. | 
 | 	// | 
 | 	// The underlying incoming socket is managed by the proxy server and is not | 
 | 	// visible to the client. However, any sockets/connections/files opened by the | 
 | 	// Handler should be cleaned up by tying them to the given context, which will | 
 | 	// be canceled whenever the connection is closed. | 
 | 	Connect(context.Context, *ConnectRequest) *ConnectResponse | 
 | } | 
 |  | 
 | // ConnectRequest represents a pending CONNECT request from a client. | 
 | type ConnectRequest struct { | 
 | 	// Address is an IPv4 or IPv6 address that the client requested to connect to. | 
 | 	// This address might be invalid/malformed/internal, and the Connect method | 
 | 	// should sanitize it before using it. | 
 | 	Address net.IP | 
 | 	// Port is the TCP port number that the client requested to connect to. | 
 | 	Port uint16 | 
 | } | 
 |  | 
 | // ConnectResponse indicates a 'backend' connection that the proxy should expose | 
 | // to the client, or an error if the connection cannot be made. | 
 | type ConnectResponse struct { | 
 | 	// Error will cause an error to be returned if it is anything else than the | 
 | 	// default value (ReplySucceeded). | 
 | 	Error Reply | 
 |  | 
 | 	// Backend is the ReadWriteCloser that will be bridged over to the connecting | 
 | 	// client if no Error is set. | 
 | 	Backend io.ReadWriteCloser | 
 | 	// LocalAddress is the IP address that is returned to the client as the local | 
 | 	// address of the newly established backend connection. | 
 | 	LocalAddress net.IP | 
 | 	// LocalPort is the local TCP port number that is returned to the client as the | 
 | 	// local port of the newly established backend connection. | 
 | 	LocalPort uint16 | 
 | } | 
 |  | 
 | // ConnectResponseFromConn builds a ConnectResponse from a net.Conn. This can be | 
 | // used by custom Handlers to easily return a ConnectResponse for a newly | 
 | // established net.Conn, eg. from a Dial call. | 
 | // | 
 | // An error is returned if the given net.Conn does not carry a properly formed | 
 | // LocalAddr. | 
 | func ConnectResponseFromConn(c net.Conn) (*ConnectResponse, error) { | 
 | 	laddr := c.LocalAddr().String() | 
 | 	host, port, err := net.SplitHostPort(laddr) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("could not parse LocalAddr %q: %w", laddr, err) | 
 | 	} | 
 | 	addr := net.ParseIP(host) | 
 | 	if addr == nil { | 
 | 		return nil, fmt.Errorf("could not parse LocalAddr host %q as IP", host) | 
 | 	} | 
 | 	portNum, err := strconv.ParseUint(port, 10, 16) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("could not parse LocalAddr port %q", port) | 
 | 	} | 
 | 	return &ConnectResponse{ | 
 | 		Backend:      c, | 
 | 		LocalAddress: addr, | 
 | 		LocalPort:    uint16(portNum), | 
 | 	}, nil | 
 | } | 
 |  | 
 | type hostHandler struct{} | 
 |  | 
 | func (h *hostHandler) Connect(ctx context.Context, req *ConnectRequest) *ConnectResponse { | 
 | 	port := fmt.Sprintf("%d", req.Port) | 
 | 	addr := net.JoinHostPort(req.Address.String(), port) | 
 | 	s, err := net.Dial("tcp", addr) | 
 | 	if err != nil { | 
 | 		log.Printf("HostHandler could not dial %q: %v", addr, err) | 
 | 		return &ConnectResponse{Error: ReplyConnectionRefused} | 
 | 	} | 
 | 	go func() { | 
 | 		<-ctx.Done() | 
 | 		s.Close() | 
 | 	}() | 
 | 	res, err := ConnectResponseFromConn(s) | 
 | 	if err != nil { | 
 | 		log.Printf("HostHandler could not build response: %v", err) | 
 | 		return &ConnectResponse{Error: ReplyGeneralFailure} | 
 | 	} | 
 | 	return res | 
 | } | 
 |  | 
 | var ( | 
 | 	// HostHandler is an unsafe SOCKS5 proxy Handler which passes all incoming | 
 | 	// connections into the local network stack. The incoming addresses/ports are | 
 | 	// not sanitized, and as the proxy does not perform authentication, this handler | 
 | 	// is an open proxy. This handler should never be used in cases where the proxy | 
 | 	// server is publicly available. | 
 | 	HostHandler = &hostHandler{} | 
 | ) | 
 |  | 
 | // Serve runs a SOCKS5 proxy server for a given Handler at a given listener. | 
 | // | 
 | // When the given context is canceled, the server will stop and the listener | 
 | // will be closed. All pending connections will also be canceled and their | 
 | // sockets closed. | 
 | func Serve(ctx context.Context, handler Handler, lis net.Listener) error { | 
 | 	go func() { | 
 | 		<-ctx.Done() | 
 | 		lis.Close() | 
 | 	}() | 
 |  | 
 | 	for { | 
 | 		con, err := lis.Accept() | 
 | 		if err != nil { | 
 | 			// Context cancellation will close listener socket with a generic 'use of closed | 
 | 			// network connection' error, translate that back to context error. | 
 | 			if ctx.Err() != nil { | 
 | 				return ctx.Err() | 
 | 			} | 
 | 			return err | 
 | 		} | 
 | 		go handle(ctx, handler, con) | 
 | 	} | 
 | } | 
 |  | 
 | // handle runs in a goroutine per incoming SOCKS connection. Its lifecycle | 
 | // corresponds to the lifecycle of a running proxy connection. | 
 | func handle(ctx context.Context, handler Handler, con net.Conn) { | 
 | 	// ctxR is a per-request context, and will be canceled whenever the handler | 
 | 	// exits or the server is stopped. | 
 | 	ctxR, ctxRC := context.WithCancel(ctx) | 
 | 	defer ctxRC() | 
 |  | 
 | 	go func() { | 
 | 		<-ctxR.Done() | 
 | 		con.Close() | 
 | 	}() | 
 |  | 
 | 	// Perform method negotiation with the client. | 
 | 	if err := negotiateMethod(con); err != nil { | 
 | 		return | 
 | 	} | 
 |  | 
 | 	// Read request from the client and translate problems into early error replies. | 
 | 	req, err := readRequest(con) | 
 | 	switch err { | 
 | 	case errNotConnect: | 
 | 		writeReply(con, ReplyCommandNotSupported, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	case errUnsupportedAddressType: | 
 | 		writeReply(con, ReplyAddressTypeNotSupported, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	case nil: | 
 | 	default: | 
 | 		writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	} | 
 |  | 
 | 	// Ask handler.Connect for a backend. | 
 | 	conRes := handler.Connect(ctxR, &ConnectRequest{ | 
 | 		Address: req.address, | 
 | 		Port:    req.port, | 
 | 	}) | 
 | 	// Handle programming error when returned value is nil. | 
 | 	if conRes == nil { | 
 | 		writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	} | 
 | 	// Handle returned errors. | 
 | 	if conRes.Error != ReplySucceeded { | 
 | 		writeReply(con, conRes.Error, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	} | 
 |  | 
 | 	// Ensure Bound.* fields are set. | 
 | 	if conRes.Backend == nil || conRes.LocalAddress == nil || conRes.LocalPort == 0 { | 
 | 		writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0) | 
 | 		return | 
 | 	} | 
 | 	// Send reply. | 
 | 	if err := writeReply(con, ReplySucceeded, conRes.LocalAddress, conRes.LocalPort); err != nil { | 
 | 		return | 
 | 	} | 
 |  | 
 | 	// Pipe returned backend into connection. | 
 | 	go func() { | 
 | 		io.Copy(conRes.Backend, con) | 
 | 		conRes.Backend.Close() | 
 | 	}() | 
 | 	io.Copy(con, conRes.Backend) | 
 | 	conRes.Backend.Close() | 
 | } |