| 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 |
| } |