1use alloc::vec::Vec;
13
14use log::{debug, trace};
15use thiserror::Error;
16
17use crate::{
18 coroutine::{ProxyCoroutine, ProxyCoroutineState, ProxyYield},
19 socks::v5::{
20 ATYP_DOMAIN, ATYP_IPV4, ATYP_IPV6, AUTH_VERSION, CMD_CONNECT, METHOD_NO_ACCEPTABLE,
21 METHOD_NO_AUTH, METHOD_USER_PASS, RSV, VERSION, address::Socks5Address,
22 auth::Socks5Credentials, message::Socks5Reply,
23 },
24};
25
26#[derive(Clone, Debug, Error, PartialEq, Eq)]
28pub enum Socks5ConnectError {
29 #[error("SOCKS5 connect failed: proxy returned version {0:#04x}, expected 0x05")]
31 UnexpectedVersion(u8),
32 #[error("SOCKS5 connect failed: proxy rejected all offered authentication methods")]
34 NoAcceptableAuthMethod,
35 #[error("SOCKS5 connect failed: proxy selected unsupported authentication method {0:#04x}")]
38 UnsupportedAuthMethod(u8),
39 #[error(
42 "SOCKS5 connect failed: proxy requires authentication but no credentials were provided"
43 )]
44 AuthRequired,
45 #[error("SOCKS5 connect failed: invalid auth sub-negotiation version {0:#04x}, expected 0x01")]
47 UnexpectedAuthVersion(u8),
48 #[error("SOCKS5 connect failed: proxy rejected the username/password credentials")]
50 AuthRejected,
51 #[error("SOCKS5 connect failed: {0}")]
53 Reply(Socks5Reply),
54 #[error("SOCKS5 connect failed: proxy returned unknown reply code {0:#04x}")]
56 UnknownReply(u8),
57 #[error("SOCKS5 connect failed: proxy returned unknown address type {0:#04x}")]
59 UnknownAddressType(u8),
60 #[error("SOCKS5 connect failed: proxy sent a malformed or truncated message")]
63 Malformed,
64}
65
66#[derive(Debug)]
69enum State {
70 Greet,
72 Method,
74 Auth,
76 AuthStatus,
78 Request,
80 ReplyHead,
82 ReplyDomainLen,
84 ReplyTail(usize),
86 Done,
88}
89
90#[derive(Debug)]
92pub struct Socks5Connect {
93 target: Socks5Address,
94 credentials: Option<Socks5Credentials>,
95 state: State,
96}
97
98impl Socks5Connect {
99 pub fn new(target: Socks5Address, credentials: Option<Socks5Credentials>) -> Self {
102 debug!("prepare socks5 connect handshake");
103 Self {
104 target,
105 credentials,
106 state: State::Greet,
107 }
108 }
109
110 fn greeting(&self) -> Vec<u8> {
113 if self.credentials.is_some() {
114 vec![VERSION, 2, METHOD_NO_AUTH, METHOD_USER_PASS]
115 } else {
116 vec![VERSION, 1, METHOD_NO_AUTH]
117 }
118 }
119
120 fn request(&self) -> Vec<u8> {
122 let mut out = vec![VERSION, CMD_CONNECT, RSV];
123 self.target.encode_into(&mut out);
124 out
125 }
126}
127
128impl ProxyCoroutine for Socks5Connect {
129 type Yield = ProxyYield;
130 type Return = Result<(), Socks5ConnectError>;
131
132 fn resume(&mut self, mut arg: Option<&[u8]>) -> ProxyCoroutineState<Self::Yield, Self::Return> {
133 use ProxyCoroutineState::{Complete, Yielded};
134
135 loop {
136 match self.state {
137 State::Greet => {
138 trace!("offering method negotiation");
139 self.state = State::Method;
140 return Yielded(ProxyYield::WantsWrite(self.greeting()));
141 }
142
143 State::Method => {
144 let Some(data) = arg.take() else {
145 return Yielded(ProxyYield::WantsRead(2));
146 };
147 let &[version, method] = data else {
148 return Complete(Err(Socks5ConnectError::Malformed));
149 };
150 if version != VERSION {
151 return Complete(Err(Socks5ConnectError::UnexpectedVersion(version)));
152 }
153 match method {
154 METHOD_NO_AUTH => {
155 trace!("proxy selected no-auth");
156 self.state = State::Request;
157 }
158 METHOD_USER_PASS => {
159 if self.credentials.is_none() {
160 return Complete(Err(Socks5ConnectError::AuthRequired));
161 }
162 trace!("proxy selected username/password auth");
163 self.state = State::Auth;
164 }
165 METHOD_NO_ACCEPTABLE => {
166 return Complete(Err(Socks5ConnectError::NoAcceptableAuthMethod));
167 }
168 other => {
169 return Complete(Err(Socks5ConnectError::UnsupportedAuthMethod(other)));
170 }
171 }
172 }
173
174 State::Auth => {
175 let bytes = self
177 .credentials
178 .as_ref()
179 .expect("credentials present in Auth state")
180 .encode();
181 self.state = State::AuthStatus;
182 return Yielded(ProxyYield::WantsWrite(bytes));
183 }
184
185 State::AuthStatus => {
186 let Some(data) = arg.take() else {
187 return Yielded(ProxyYield::WantsRead(2));
188 };
189 let &[version, status] = data else {
190 return Complete(Err(Socks5ConnectError::Malformed));
191 };
192 if version != AUTH_VERSION {
193 return Complete(Err(Socks5ConnectError::UnexpectedAuthVersion(version)));
194 }
195 if status != 0 {
196 return Complete(Err(Socks5ConnectError::AuthRejected));
197 }
198 trace!("username/password auth accepted");
199 self.state = State::Request;
200 }
201
202 State::Request => {
203 trace!("requesting connect to target");
204 self.state = State::ReplyHead;
205 return Yielded(ProxyYield::WantsWrite(self.request()));
206 }
207
208 State::ReplyHead => {
209 let Some(data) = arg.take() else {
210 return Yielded(ProxyYield::WantsRead(4));
211 };
212 let &[version, rep, _rsv, atyp] = data else {
213 return Complete(Err(Socks5ConnectError::Malformed));
214 };
215 if version != VERSION {
216 return Complete(Err(Socks5ConnectError::UnexpectedVersion(version)));
217 }
218 if rep != 0 {
219 let err = match Socks5Reply::from_u8(rep) {
220 Some(reply) => Socks5ConnectError::Reply(reply),
221 None => Socks5ConnectError::UnknownReply(rep),
222 };
223 return Complete(Err(err));
224 }
225 match atyp {
228 ATYP_IPV4 => self.state = State::ReplyTail(4 + 2),
229 ATYP_IPV6 => self.state = State::ReplyTail(16 + 2),
230 ATYP_DOMAIN => self.state = State::ReplyDomainLen,
231 other => {
232 return Complete(Err(Socks5ConnectError::UnknownAddressType(other)));
233 }
234 }
235 }
236
237 State::ReplyDomainLen => {
238 let Some(data) = arg.take() else {
239 return Yielded(ProxyYield::WantsRead(1));
240 };
241 let &[len] = data else {
242 return Complete(Err(Socks5ConnectError::Malformed));
243 };
244 self.state = State::ReplyTail(len as usize + 2);
245 }
246
247 State::ReplyTail(n) => {
248 if arg.take().is_none() {
249 return Yielded(ProxyYield::WantsRead(n));
250 }
251 debug!("socks5 tunnel established");
252 self.state = State::Done;
253 return Complete(Ok(()));
254 }
255
256 State::Done => panic!("Socks5Connect resumed after completion"),
257 }
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn no_auth() -> Socks5Connect {
267 Socks5Connect::new(Socks5Address::Domain("example.com".into(), 993), None)
268 }
269
270 fn with_auth() -> Socks5Connect {
271 let creds = Socks5Credentials::new("user", "pass").unwrap();
272 Socks5Connect::new(Socks5Address::Ipv4([1, 2, 3, 4].into(), 25), Some(creds))
273 }
274
275 fn wants_write(cor: &mut Socks5Connect, arg: Option<&[u8]>) -> Vec<u8> {
276 match cor.resume(arg) {
277 ProxyCoroutineState::Yielded(ProxyYield::WantsWrite(bytes)) => bytes,
278 state => panic!("expected WantsWrite, got {state:?}"),
279 }
280 }
281
282 fn wants_read(cor: &mut Socks5Connect, arg: Option<&[u8]>) -> usize {
283 match cor.resume(arg) {
284 ProxyCoroutineState::Yielded(ProxyYield::WantsRead(n)) => n,
285 state => panic!("expected WantsRead, got {state:?}"),
286 }
287 }
288
289 fn complete_ok(cor: &mut Socks5Connect, arg: Option<&[u8]>) {
290 match cor.resume(arg) {
291 ProxyCoroutineState::Complete(Ok(())) => {}
292 state => panic!("expected Complete(Ok), got {state:?}"),
293 }
294 }
295
296 fn complete_err(cor: &mut Socks5Connect, arg: Option<&[u8]>) -> Socks5ConnectError {
297 match cor.resume(arg) {
298 ProxyCoroutineState::Complete(Err(err)) => err,
299 state => panic!("expected Complete(Err), got {state:?}"),
300 }
301 }
302
303 #[test]
304 fn no_auth_domain_reply_happy_path() {
305 let mut cor = no_auth();
306
307 assert_eq!(wants_write(&mut cor, None), [0x05, 0x01, 0x00]);
309 assert_eq!(wants_read(&mut cor, None), 2);
311 let req = wants_write(&mut cor, Some(&[0x05, 0x00]));
313 let mut expected = vec![0x05, 0x01, 0x00, 0x03, 11];
314 expected.extend_from_slice(b"example.com");
315 expected.extend_from_slice(&993u16.to_be_bytes());
316 assert_eq!(req, expected);
317 assert_eq!(wants_read(&mut cor, None), 4);
319 assert_eq!(wants_read(&mut cor, Some(&[0x05, 0x00, 0x00, 0x03])), 1);
321 assert_eq!(wants_read(&mut cor, Some(&[0x03])), 5);
323 complete_ok(&mut cor, Some(&[b'a', b'b', b'c', 0x00, 0x50]));
324 }
325
326 #[test]
327 fn user_pass_ipv4_reply_happy_path() {
328 let mut cor = with_auth();
329
330 assert_eq!(wants_write(&mut cor, None), [0x05, 0x02, 0x00, 0x02]);
332 assert_eq!(wants_read(&mut cor, None), 2);
333 let auth = wants_write(&mut cor, Some(&[0x05, 0x02]));
335 assert_eq!(
336 auth,
337 [
338 0x01, 0x04, b'u', b's', b'e', b'r', 0x04, b'p', b'a', b's', b's'
339 ]
340 );
341 assert_eq!(wants_read(&mut cor, None), 2);
343 let _req = wants_write(&mut cor, Some(&[0x01, 0x00]));
345 assert_eq!(wants_read(&mut cor, None), 4);
346 assert_eq!(wants_read(&mut cor, Some(&[0x05, 0x00, 0x00, 0x01])), 6);
348 complete_ok(&mut cor, Some(&[0, 0, 0, 0, 0, 0]));
349 }
350
351 #[test]
352 fn server_requires_auth_without_credentials() {
353 let mut cor = no_auth();
354 wants_write(&mut cor, None);
355 wants_read(&mut cor, None);
356 let err = complete_err(&mut cor, Some(&[0x05, 0x02]));
357 assert_eq!(err, Socks5ConnectError::AuthRequired);
358 }
359
360 #[test]
361 fn no_acceptable_method() {
362 let mut cor = no_auth();
363 wants_write(&mut cor, None);
364 wants_read(&mut cor, None);
365 let err = complete_err(&mut cor, Some(&[0x05, 0xFF]));
366 assert_eq!(err, Socks5ConnectError::NoAcceptableAuthMethod);
367 }
368
369 #[test]
370 fn rejected_auth() {
371 let mut cor = with_auth();
372 wants_write(&mut cor, None);
373 wants_read(&mut cor, None);
374 wants_write(&mut cor, Some(&[0x05, 0x02]));
375 wants_read(&mut cor, None);
376 let err = complete_err(&mut cor, Some(&[0x01, 0x01]));
377 assert_eq!(err, Socks5ConnectError::AuthRejected);
378 }
379
380 #[test]
381 fn reply_failure_maps_to_reply_error() {
382 let mut cor = no_auth();
383 wants_write(&mut cor, None);
384 wants_read(&mut cor, None);
385 wants_write(&mut cor, Some(&[0x05, 0x00]));
386 wants_read(&mut cor, None);
387 let err = complete_err(&mut cor, Some(&[0x05, 0x04, 0x00, 0x01]));
389 assert_eq!(err, Socks5ConnectError::Reply(Socks5Reply::HostUnreachable));
390 }
391
392 #[test]
393 fn unexpected_version() {
394 let mut cor = no_auth();
395 wants_write(&mut cor, None);
396 wants_read(&mut cor, None);
397 let err = complete_err(&mut cor, Some(&[0x04, 0x00]));
398 assert_eq!(err, Socks5ConnectError::UnexpectedVersion(0x04));
399 }
400}