Skip to main content

proxychains_masq/proxy/
socks5.rs

1use std::net::IpAddr;
2
3use anyhow::{bail, Context, Result};
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6use super::{BoxStream, Target};
7
8const METHOD_NO_AUTH: u8 = 0x00;
9const METHOD_USER_PASS: u8 = 0x02;
10const METHOD_NO_ACCEPTABLE: u8 = 0xFF;
11
12/// Perform a SOCKS5 CONNECT handshake on an already-connected `stream`.
13///
14/// Supports:
15/// - No-auth (method 0x00)
16/// - Username/password auth (method 0x02, RFC 1929)
17/// - IPv4, IPv6, and hostname targets
18///
19/// # Errors
20///
21/// Returns an error if the handshake fails, auth is rejected, or the proxy
22/// returns a non-success reply.
23pub async fn connect(
24    mut stream: BoxStream,
25    target: &Target,
26    username: Option<&str>,
27    password: Option<&str>,
28) -> Result<BoxStream> {
29    // ── Method negotiation ────────────────────────────────────────────────────
30    let methods: &[u8] = if username.is_some() {
31        &[METHOD_NO_AUTH, METHOD_USER_PASS]
32    } else {
33        &[METHOD_NO_AUTH]
34    };
35
36    let mut greeting = vec![5u8, methods.len() as u8];
37    greeting.extend_from_slice(methods);
38    stream
39        .write_all(&greeting)
40        .await
41        .context("socks5: write greeting")?;
42
43    let mut sel = [0u8; 2];
44    stream
45        .read_exact(&mut sel)
46        .await
47        .context("socks5: read method selection")?;
48    if sel[0] != 5 {
49        bail!("socks5: unexpected version in method selection: {}", sel[0]);
50    }
51    if sel[1] == METHOD_NO_ACCEPTABLE {
52        bail!("socks5: no acceptable authentication method");
53    }
54
55    // ── Auth sub-negotiation (if required) ────────────────────────────────────
56    if sel[1] == METHOD_USER_PASS {
57        let user = username.unwrap_or("");
58        let pass = password.unwrap_or("");
59
60        let mut auth: Vec<u8> = Vec::with_capacity(3 + user.len() + pass.len());
61        auth.push(1); // sub-negotiation version
62        auth.push(user.len() as u8);
63        auth.extend_from_slice(user.as_bytes());
64        auth.push(pass.len() as u8);
65        auth.extend_from_slice(pass.as_bytes());
66        stream
67            .write_all(&auth)
68            .await
69            .context("socks5: write auth")?;
70
71        let mut ar = [0u8; 2];
72        stream
73            .read_exact(&mut ar)
74            .await
75            .context("socks5: read auth response")?;
76        // Version field can be 1 or 5 (some non-conforming proxies send 5)
77        if ar[1] != 0 {
78            bail!("socks5: authentication failed (status {})", ar[1]);
79        }
80    } else if sel[1] != METHOD_NO_AUTH {
81        bail!("socks5: server chose unexpected method: {}", sel[1]);
82    }
83
84    // ── CONNECT request ───────────────────────────────────────────────────────
85    let mut req: Vec<u8> = Vec::with_capacity(32);
86    req.extend_from_slice(&[5, 1, 0]); // VER CMD RSV
87
88    match target {
89        Target::Ip(IpAddr::V4(v4), _) => {
90            req.push(1); // atype IPv4
91            req.extend_from_slice(&v4.octets());
92        }
93        Target::Ip(IpAddr::V6(v6), _) => {
94            req.push(4); // atype IPv6
95            req.extend_from_slice(&v6.octets());
96        }
97        Target::Host(hostname, _) => {
98            req.push(3); // atype domain name
99            req.push(hostname.len() as u8);
100            req.extend_from_slice(hostname.as_bytes());
101        }
102    }
103    req.extend_from_slice(&target.port().to_be_bytes());
104
105    stream
106        .write_all(&req)
107        .await
108        .context("socks5: write CONNECT")?;
109
110    // ── Read reply ────────────────────────────────────────────────────────────
111    let mut rep = [0u8; 4];
112    stream
113        .read_exact(&mut rep)
114        .await
115        .context("socks5: read reply header")?;
116    if rep[0] != 5 {
117        bail!("socks5: unexpected version in reply: {}", rep[0]);
118    }
119    if rep[1] != 0 {
120        bail!("socks5: CONNECT failed (reply code {})", rep[1]);
121    }
122
123    // Drain the bound address from the reply
124    let atype = rep[3];
125    let addr_len: usize = match atype {
126        1 => 4,
127        4 => 16,
128        3 => {
129            let len = stream.read_u8().await.context("socks5: read domain len")? as usize;
130            len
131        }
132        _ => bail!("socks5: unknown address type in reply: {atype}"),
133    };
134    let mut addr_buf = vec![0u8; addr_len + 2]; // addr + 2-byte port
135    stream
136        .read_exact(&mut addr_buf)
137        .await
138        .context("socks5: drain bound addr")?;
139
140    Ok(stream)
141}