blob: 0d5d133e0994c1168610b2734a629e5153213615 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskife7134b2022-04-01 15:46:29 +02004package socksproxy
5
6import (
7 "encoding/binary"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12)
13
14// readMethods implements RFC1928 3. “Procedure for TCP-based clients”,
15// paragraph 3. It receives a 'version identifier/method selection message' from
16// r and returns the methods supported by the client.
17func readMethods(r io.Reader) ([]method, error) {
18 var ver uint8
19 if err := binary.Read(r, binary.BigEndian, &ver); err != nil {
20 return nil, fmt.Errorf("when reading ver: %w", err)
21 }
22 if ver != 5 {
23 return nil, fmt.Errorf("unimplemented version %d", ver)
24 }
25 var nmethods uint8
26 if err := binary.Read(r, binary.BigEndian, &nmethods); err != nil {
27 return nil, fmt.Errorf("when reading nmethods: %w", err)
28 }
29 methodBytes := make([]byte, nmethods)
30 if _, err := io.ReadFull(r, methodBytes); err != nil {
31 return nil, fmt.Errorf("while reading methods: %w", err)
32 }
33 methods := make([]method, nmethods)
34 for i, m := range methodBytes {
35 methods[i] = method(m)
36 }
37 return methods, nil
38}
39
40// writeMethod implements RFC1928 3. “Procedure for TCP-based clients”,
41// paragraph 5. It sends a selected method to w.
42func writeMethod(w io.Writer, m method) error {
43 if err := binary.Write(w, binary.BigEndian, uint8(5)); err != nil {
44 return fmt.Errorf("while writing version: %w", err)
45 }
46 if err := binary.Write(w, binary.BigEndian, uint8(m)); err != nil {
47 return fmt.Errorf("while writing method: %w", err)
48 }
49 return nil
50}
51
52// method is an RFC1928 authentication method.
53type method uint8
54
55const (
56 methodNoAuthenticationRequired method = 0
57 methodNoAcceptableMethods method = 0xff
58)
59
60// negotiateMethod implements the entire flow RFC1928 3. “Procedure for
61// TCP-based clients” by negotiating for the 'NO AUTHENTICATION REQUIRED'
62// authentication method, and failing otherwise.
63func negotiateMethod(rw io.ReadWriter) error {
64 methods, err := readMethods(rw)
65 if err != nil {
66 return fmt.Errorf("could not read methods: %w", err)
67 }
68
69 found := false
70 for _, m := range methods {
71 if m == methodNoAuthenticationRequired {
72 found = true
73 break
74 }
75 }
76 if !found {
77 // Discard error, as this connection is failed anyway.
78 writeMethod(rw, methodNoAcceptableMethods)
79 return fmt.Errorf("no acceptable methods found")
80 }
81 if err := writeMethod(rw, methodNoAuthenticationRequired); err != nil {
82 return fmt.Errorf("could not respond with method: %w", err)
83 }
84 return nil
85}
86
87var (
88 // errNotConnect is returned by readRequest when the request contained some
89 // other request than CONNECT.
90 errNotConnect = errors.New("not CONNECT")
91 // errUnsupportedAddressType is returned by readRequest when the request
92 // contained some unsupported address type (not IPv4 or IPv6).
93 errUnsupportedAddressType = errors.New("unsupported address type")
94)
95
96// readRequest implements RFC1928 4. “Requests” by reading a SOCKS request from
97// r and ensuring it's an IPv4/IPv6 CONNECT request. The parsed address/port
98// pair is then returned.
99func readRequest(r io.Reader) (*connectRequest, error) {
100 header := struct {
101 Ver uint8
102 Cmd uint8
103 Rsv uint8
104 Atyp uint8
105 }{}
106 if err := binary.Read(r, binary.BigEndian, &header); err != nil {
107 return nil, fmt.Errorf("when reading request header: %w", err)
108 }
109
110 if header.Ver != 5 {
111 return nil, fmt.Errorf("invalid version %d", header.Ver)
112 }
113 if header.Cmd != 1 {
114 return nil, errNotConnect
115 }
116
117 var addrBytes []byte
118 switch header.Atyp {
119 case 1:
120 addrBytes = make([]byte, 4)
121 case 4:
Jan Schärd6d809a2024-04-10 13:14:45 +0200122 addrBytes = make([]byte, 16)
Serge Bazanskife7134b2022-04-01 15:46:29 +0200123 default:
124 return nil, errUnsupportedAddressType
125 }
126 if _, err := io.ReadFull(r, addrBytes); err != nil {
127 return nil, fmt.Errorf("when reading address: %w", err)
128 }
129
130 var port uint16
131 if err := binary.Read(r, binary.BigEndian, &port); err != nil {
132 return nil, fmt.Errorf("when reading port: %w", err)
133 }
134
135 return &connectRequest{
136 address: addrBytes,
137 port: port,
138 }, nil
139}
140
141type connectRequest struct {
142 address net.IP
143 port uint16
144}
145
146// Reply is an RFC1928 6. “Replies” reply field value. It's returned to the
147// client by internal socksproxy code or a Handler to signal a success or error
148// condition within an RFC1928 reply.
149type Reply uint8
150
151const (
152 ReplySucceeded Reply = 0
153 ReplyGeneralFailure Reply = 1
154 ReplyConnectionNotAllowed Reply = 2
155 ReplyNetworkUnreachable Reply = 3
156 ReplyHostUnreachable Reply = 4
157 ReplyConnectionRefused Reply = 5
158 ReplyTTLExpired Reply = 6
159 ReplyCommandNotSupported Reply = 7
160 ReplyAddressTypeNotSupported Reply = 8
161)
162
163// writeReply implements RFC1928 6. “Replies” by sending a given Reply, bind
164// address and bind port to w. An error is returned if the given bind address is
165// invaild, or if a communication error occurred.
166func writeReply(w io.Writer, r Reply, bindAddr net.IP, bindPort uint16) error {
167 var atyp uint8
168 switch len(bindAddr) {
169 case 4:
170 atyp = 1
171 case 16:
172 atyp = 4
173 default:
174 return fmt.Errorf("unsupported bind address type")
175 }
176
177 header := struct {
178 Ver uint8
179 Reply uint8
180 Rsv uint8
181 Atyp uint8
182 }{
183 Ver: 5,
184 Reply: uint8(r),
185 Rsv: 0,
186 Atyp: atyp,
187 }
188 if err := binary.Write(w, binary.BigEndian, &header); err != nil {
189 return fmt.Errorf("when writing reply header: %w", err)
190 }
191 if _, err := w.Write(bindAddr); err != nil {
192 return fmt.Errorf("when writing reply bind address: %w", err)
193 }
194 if err := binary.Write(w, binary.BigEndian, bindPort); err != nil {
195 return fmt.Errorf("when writing reply bind port: %w", err)
196 }
197 return nil
198}