m/pkg/socksproxy: init

This implements a simple SOCKS5 proxy server, which will be used within
nanoswitch to expose multiple nodes to test code and metroctl.

Some existing alternatives were considered, but none were in a healthy
enough state to be usable within Metropolis. And, in the end, we only
need a small subset of an already simple standard, so implementing this
ourselves isn't a massive waste of time.

Change-Id: Ifa4d4edf837b55b93cae9981028efef336ff2a3d
Reviewed-on: https://review.monogon.dev/c/monogon/+/646
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/pkg/socksproxy/protocol.go b/metropolis/pkg/socksproxy/protocol.go
new file mode 100644
index 0000000..cb9ae0a
--- /dev/null
+++ b/metropolis/pkg/socksproxy/protocol.go
@@ -0,0 +1,195 @@
+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
+}