blob: 698d324d77bb521ea9dbe0de784d288397d604f4 [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
Lorenz Brun08fd1cb2025-02-10 19:11:17 +0100118 var hostnameBytes []byte
Serge Bazanskife7134b2022-04-01 15:46:29 +0200119 switch header.Atyp {
120 case 1:
121 addrBytes = make([]byte, 4)
Lorenz Brun08fd1cb2025-02-10 19:11:17 +0100122 case 3:
123 // Variable-length string to resolve
124 addrBytes = make([]byte, 1)
Serge Bazanskife7134b2022-04-01 15:46:29 +0200125 case 4:
Jan Schärd6d809a2024-04-10 13:14:45 +0200126 addrBytes = make([]byte, 16)
Serge Bazanskife7134b2022-04-01 15:46:29 +0200127 default:
128 return nil, errUnsupportedAddressType
129 }
130 if _, err := io.ReadFull(r, addrBytes); err != nil {
131 return nil, fmt.Errorf("when reading address: %w", err)
132 }
133
Lorenz Brun08fd1cb2025-02-10 19:11:17 +0100134 // Handle domain name addressing, required by for example Chrome
135 if header.Atyp == 3 {
136 hostnameBytes = make([]byte, addrBytes[0])
137 if _, err := io.ReadFull(r, hostnameBytes); err != nil {
138 return nil, fmt.Errorf("when reading address: %w", err)
139 }
140 }
141
Serge Bazanskife7134b2022-04-01 15:46:29 +0200142 var port uint16
143 if err := binary.Read(r, binary.BigEndian, &port); err != nil {
144 return nil, fmt.Errorf("when reading port: %w", err)
145 }
146
147 return &connectRequest{
Lorenz Brun08fd1cb2025-02-10 19:11:17 +0100148 address: addrBytes,
149 hostname: string(hostnameBytes),
150 port: port,
Serge Bazanskife7134b2022-04-01 15:46:29 +0200151 }, nil
152}
153
154type connectRequest struct {
Lorenz Brun08fd1cb2025-02-10 19:11:17 +0100155 address net.IP
156 hostname string
157 port uint16
Serge Bazanskife7134b2022-04-01 15:46:29 +0200158}
159
160// Reply is an RFC1928 6. “Replies” reply field value. It's returned to the
161// client by internal socksproxy code or a Handler to signal a success or error
162// condition within an RFC1928 reply.
163type Reply uint8
164
165const (
166 ReplySucceeded Reply = 0
167 ReplyGeneralFailure Reply = 1
168 ReplyConnectionNotAllowed Reply = 2
169 ReplyNetworkUnreachable Reply = 3
170 ReplyHostUnreachable Reply = 4
171 ReplyConnectionRefused Reply = 5
172 ReplyTTLExpired Reply = 6
173 ReplyCommandNotSupported Reply = 7
174 ReplyAddressTypeNotSupported Reply = 8
175)
176
177// writeReply implements RFC1928 6. “Replies” by sending a given Reply, bind
178// address and bind port to w. An error is returned if the given bind address is
179// invaild, or if a communication error occurred.
180func writeReply(w io.Writer, r Reply, bindAddr net.IP, bindPort uint16) error {
181 var atyp uint8
182 switch len(bindAddr) {
183 case 4:
184 atyp = 1
185 case 16:
186 atyp = 4
187 default:
188 return fmt.Errorf("unsupported bind address type")
189 }
190
191 header := struct {
192 Ver uint8
193 Reply uint8
194 Rsv uint8
195 Atyp uint8
196 }{
197 Ver: 5,
198 Reply: uint8(r),
199 Rsv: 0,
200 Atyp: atyp,
201 }
202 if err := binary.Write(w, binary.BigEndian, &header); err != nil {
203 return fmt.Errorf("when writing reply header: %w", err)
204 }
205 if _, err := w.Write(bindAddr); err != nil {
206 return fmt.Errorf("when writing reply bind address: %w", err)
207 }
208 if err := binary.Write(w, binary.BigEndian, bindPort); err != nil {
209 return fmt.Errorf("when writing reply bind port: %w", err)
210 }
211 return nil
212}