Skip to main content

irontide_engine/
proxy.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    reason = "M175: SOCKS4/5/HTTP CONNECT proxy — wire-format byte/u16 fields fixed by spec"
6)]
7
8//! Proxy support: SOCKS4, SOCKS5, HTTP CONNECT, and SOCKS5 UDP ASSOCIATE.
9//!
10//! Hand-rolled implementations — no external dependency beyond `tokio`.
11//! Provides [`connect_through_proxy`] for TCP tunneling and
12//! [`socks5_udp_associate`] + [`ProxiedUdpSocket`] for UDP relay.
13
14use std::io;
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
16
17use tokio::io::{AsyncReadExt, AsyncWriteExt};
18use tokio::net::{TcpStream, UdpSocket};
19
20// ── Proxy config types ────────────────────────────────────────────────
21// Lifted to irontide-core (M242); re-exported to keep `crate::proxy::{ProxyType, ProxyConfig}` stable.
22pub use irontide_core::{ProxyConfig, ProxyType};
23
24// ── connect_through_proxy ────────────────────────────────────────────
25
26/// Connect to `target` through the configured proxy.
27///
28/// Returns a `TcpStream` with the proxy handshake already completed.
29/// The stream is ready for `BitTorrent` protocol I/O.
30pub(crate) async fn connect_through_proxy(
31    proxy: &ProxyConfig,
32    target: SocketAddr,
33) -> io::Result<TcpStream> {
34    let proxy_addr = format!("{}:{}", proxy.hostname, proxy.port);
35    let mut stream = TcpStream::connect(&proxy_addr).await?;
36
37    match proxy.proxy_type {
38        ProxyType::Socks4 => socks4_connect(&mut stream, target).await?,
39        ProxyType::Socks5 => socks5_connect(&mut stream, target, None).await?,
40        ProxyType::Socks5Password => {
41            let auth = match (&proxy.username, &proxy.password) {
42                (Some(u), Some(p)) => Some((u.as_str(), p.as_str())),
43                _ => None,
44            };
45            socks5_connect(&mut stream, target, auth).await?;
46        }
47        ProxyType::Http => http_connect(&mut stream, target, None).await?,
48        ProxyType::HttpPassword => {
49            let auth = match (&proxy.username, &proxy.password) {
50                (Some(u), Some(p)) => Some((u.as_str(), p.as_str())),
51                _ => None,
52            };
53            http_connect(&mut stream, target, auth).await?;
54        }
55        ProxyType::None => {
56            return Err(io::Error::new(
57                io::ErrorKind::InvalidInput,
58                "no proxy configured",
59            ));
60        }
61    }
62
63    Ok(stream)
64}
65
66// ── SOCKS4 ───────────────────────────────────────────────────────────
67
68/// SOCKS4 CONNECT handshake (RFC 1928 predecessor).
69///
70/// Request: VN(1)=4, CD(1)=1, DSTPORT(2), DSTIP(4), USERID(var), NULL(1)
71/// Response: VN(1)=0, CD(1), DSTPORT(2), DSTIP(4) — CD=90 means granted
72async fn socks4_connect(stream: &mut TcpStream, target: SocketAddr) -> io::Result<()> {
73    let ip = match target.ip() {
74        IpAddr::V4(v4) => v4,
75        IpAddr::V6(_) => {
76            return Err(io::Error::new(
77                io::ErrorKind::InvalidInput,
78                "SOCKS4 does not support IPv6",
79            ));
80        }
81    };
82
83    // Build request
84    let mut req = Vec::with_capacity(9);
85    req.push(4); // VN
86    req.push(1); // CD = CONNECT
87    req.extend_from_slice(&target.port().to_be_bytes());
88    req.extend_from_slice(&ip.octets());
89    req.push(0); // NULL-terminated empty userid
90    stream.write_all(&req).await?;
91
92    // Read 8-byte response
93    let mut resp = [0u8; 8];
94    stream.read_exact(&mut resp).await?;
95
96    if resp[1] != 90 {
97        return Err(io::Error::new(
98            io::ErrorKind::ConnectionRefused,
99            format!("SOCKS4 request rejected (CD={})", resp[1]),
100        ));
101    }
102
103    Ok(())
104}
105
106// ── SOCKS5 ───────────────────────────────────────────────────────────
107
108/// SOCKS5 CONNECT handshake (RFC 1928 + optional RFC 1929 auth).
109async fn socks5_connect(
110    stream: &mut TcpStream,
111    target: SocketAddr,
112    auth: Option<(&str, &str)>,
113) -> io::Result<()> {
114    // Step 1: Method negotiation
115    socks5_negotiate_method(stream, auth.is_some()).await?;
116
117    // Step 2: Username/password auth if needed
118    if let Some((user, pass)) = auth {
119        socks5_auth(stream, user, pass).await?;
120    }
121
122    // Step 3: Connect request
123    socks5_send_connect(stream, target).await
124}
125
126/// Negotiate authentication method with SOCKS5 proxy.
127async fn socks5_negotiate_method(stream: &mut TcpStream, want_auth: bool) -> io::Result<()> {
128    let methods: &[u8] = if want_auth {
129        &[5, 2, 0, 2] // VER=5, NMETHODS=2, NO_AUTH + USERNAME/PASSWORD
130    } else {
131        &[5, 1, 0] // VER=5, NMETHODS=1, NO_AUTH
132    };
133    stream.write_all(methods).await?;
134
135    let mut resp = [0u8; 2];
136    stream.read_exact(&mut resp).await?;
137
138    if resp[0] != 5 {
139        return Err(io::Error::new(
140            io::ErrorKind::InvalidData,
141            format!("SOCKS5: unexpected version {}", resp[0]),
142        ));
143    }
144
145    match resp[1] {
146        0 => Ok(()),              // NO_AUTH accepted
147        2 if want_auth => Ok(()), // USERNAME/PASSWORD accepted
148        0xFF => Err(io::Error::new(
149            io::ErrorKind::PermissionDenied,
150            "SOCKS5: no acceptable methods",
151        )),
152        m => Err(io::Error::new(
153            io::ErrorKind::InvalidData,
154            format!("SOCKS5: unexpected method {m}"),
155        )),
156    }
157}
158
159/// RFC 1929 username/password subnegotiation.
160///
161/// Request: VER(1)=1, ULEN(1), UNAME(ULEN), PLEN(1), PASSWD(PLEN)
162/// Response: VER(1)=1, STATUS(1) — 0=success
163async fn socks5_auth(stream: &mut TcpStream, user: &str, pass: &str) -> io::Result<()> {
164    if user.len() > 255 || pass.len() > 255 {
165        return Err(io::Error::new(
166            io::ErrorKind::InvalidInput,
167            "SOCKS5: username or password too long (max 255 bytes)",
168        ));
169    }
170
171    let mut req = Vec::with_capacity(3 + user.len() + pass.len());
172    req.push(1); // subneg version
173    req.push(user.len() as u8);
174    req.extend_from_slice(user.as_bytes());
175    req.push(pass.len() as u8);
176    req.extend_from_slice(pass.as_bytes());
177    stream.write_all(&req).await?;
178
179    let mut resp = [0u8; 2];
180    stream.read_exact(&mut resp).await?;
181
182    if resp[1] != 0 {
183        return Err(io::Error::new(
184            io::ErrorKind::PermissionDenied,
185            "SOCKS5: authentication failed",
186        ));
187    }
188
189    Ok(())
190}
191
192/// Send SOCKS5 CONNECT request and read response.
193///
194/// Request: VER(1)=5, CMD(1)=1, RSV(1)=0, ATYP(1), DST.ADDR(var), DST.PORT(2)
195/// Response: VER(1)=5, REP(1), RSV(1)=0, ATYP(1), BND.ADDR(var), BND.PORT(2)
196async fn socks5_send_connect(stream: &mut TcpStream, target: SocketAddr) -> io::Result<()> {
197    let mut req = Vec::with_capacity(22);
198    req.push(5); // VER
199    req.push(1); // CMD = CONNECT
200    req.push(0); // RSV
201    encode_socks5_addr(&mut req, target);
202    stream.write_all(&req).await?;
203
204    read_socks5_response(stream).await
205}
206
207/// Encode a socket address in SOCKS5 format (ATYP + ADDR + PORT).
208fn encode_socks5_addr(buf: &mut Vec<u8>, addr: SocketAddr) {
209    match addr {
210        SocketAddr::V4(v4) => {
211            buf.push(1); // ATYP = IPv4
212            buf.extend_from_slice(&v4.ip().octets());
213            buf.extend_from_slice(&v4.port().to_be_bytes());
214        }
215        SocketAddr::V6(v6) => {
216            buf.push(4); // ATYP = IPv6
217            buf.extend_from_slice(&v6.ip().octets());
218            buf.extend_from_slice(&v6.port().to_be_bytes());
219        }
220    }
221}
222
223/// Read and validate a SOCKS5 response (CONNECT or UDP ASSOCIATE).
224async fn read_socks5_response(stream: &mut TcpStream) -> io::Result<()> {
225    let mut header = [0u8; 4];
226    stream.read_exact(&mut header).await?;
227
228    if header[0] != 5 {
229        return Err(io::Error::new(
230            io::ErrorKind::InvalidData,
231            format!("SOCKS5: unexpected version {}", header[0]),
232        ));
233    }
234
235    if header[1] != 0 {
236        let msg = socks5_error_message(header[1]);
237        return Err(io::Error::new(io::ErrorKind::ConnectionRefused, msg));
238    }
239
240    // Skip bound address
241    match header[3] {
242        1 => {
243            // IPv4: 4 bytes + 2 port
244            let mut skip = [0u8; 6];
245            stream.read_exact(&mut skip).await?;
246        }
247        4 => {
248            // IPv6: 16 bytes + 2 port
249            let mut skip = [0u8; 18];
250            stream.read_exact(&mut skip).await?;
251        }
252        3 => {
253            // Domain: 1 len + domain + 2 port
254            let mut len = [0u8; 1];
255            stream.read_exact(&mut len).await?;
256            let mut skip = vec![0u8; len[0] as usize + 2];
257            stream.read_exact(&mut skip).await?;
258        }
259        atyp => {
260            return Err(io::Error::new(
261                io::ErrorKind::InvalidData,
262                format!("SOCKS5: unknown ATYP {atyp}"),
263            ));
264        }
265    }
266
267    Ok(())
268}
269
270/// Read a SOCKS5 response and return the bound address.
271async fn read_socks5_response_addr(stream: &mut TcpStream) -> io::Result<SocketAddr> {
272    let mut header = [0u8; 4];
273    stream.read_exact(&mut header).await?;
274
275    if header[0] != 5 {
276        return Err(io::Error::new(
277            io::ErrorKind::InvalidData,
278            format!("SOCKS5: unexpected version {}", header[0]),
279        ));
280    }
281
282    if header[1] != 0 {
283        let msg = socks5_error_message(header[1]);
284        return Err(io::Error::new(io::ErrorKind::ConnectionRefused, msg));
285    }
286
287    match header[3] {
288        1 => {
289            let mut buf = [0u8; 6];
290            stream.read_exact(&mut buf).await?;
291            let ip = Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]);
292            let port = u16::from_be_bytes([buf[4], buf[5]]);
293            Ok(SocketAddr::V4(SocketAddrV4::new(ip, port)))
294        }
295        4 => {
296            let mut buf = [0u8; 18];
297            stream.read_exact(&mut buf).await?;
298            let ip = Ipv6Addr::from(<[u8; 16]>::try_from(&buf[..16]).unwrap());
299            let port = u16::from_be_bytes([buf[16], buf[17]]);
300            Ok(SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)))
301        }
302        atyp => Err(io::Error::new(
303            io::ErrorKind::InvalidData,
304            format!("SOCKS5: unsupported BND.ATYP {atyp} in UDP ASSOCIATE"),
305        )),
306    }
307}
308
309fn socks5_error_message(code: u8) -> String {
310    match code {
311        1 => "SOCKS5: general failure".into(),
312        2 => "SOCKS5: connection not allowed by ruleset".into(),
313        3 => "SOCKS5: network unreachable".into(),
314        4 => "SOCKS5: host unreachable".into(),
315        5 => "SOCKS5: connection refused".into(),
316        6 => "SOCKS5: TTL expired".into(),
317        7 => "SOCKS5: command not supported".into(),
318        8 => "SOCKS5: address type not supported".into(),
319        _ => format!("SOCKS5: unknown error ({code})"),
320    }
321}
322
323// ── HTTP CONNECT ─────────────────────────────────────────────────────
324
325/// HTTP CONNECT tunnel handshake.
326async fn http_connect(
327    stream: &mut TcpStream,
328    target: SocketAddr,
329    auth: Option<(&str, &str)>,
330) -> io::Result<()> {
331    let host_port = format!("{}:{}", target.ip(), target.port());
332
333    let mut request = format!("CONNECT {host_port} HTTP/1.1\r\nHost: {host_port}\r\n");
334
335    if let Some((user, pass)) = auth {
336        use std::fmt::Write;
337        let credentials = format!("{user}:{pass}");
338        let encoded = base64_encode(credentials.as_bytes());
339        let _ = write!(request, "Proxy-Authorization: Basic {encoded}\r\n");
340    }
341
342    request.push_str("\r\n");
343    stream.write_all(request.as_bytes()).await?;
344
345    // Read until we see \r\n\r\n (end of HTTP headers)
346    let mut response_buf = Vec::with_capacity(256);
347    loop {
348        let mut byte = [0u8; 1];
349        stream.read_exact(&mut byte).await?;
350        response_buf.push(byte[0]);
351
352        if response_buf.len() >= 4 {
353            let len = response_buf.len();
354            if response_buf[len - 4..] == [b'\r', b'\n', b'\r', b'\n'] {
355                break;
356            }
357        }
358
359        if response_buf.len() > 8192 {
360            return Err(io::Error::new(
361                io::ErrorKind::InvalidData,
362                "HTTP CONNECT: response too large",
363            ));
364        }
365    }
366
367    let response_str = String::from_utf8_lossy(&response_buf);
368    let status_line = response_str.lines().next().unwrap_or("");
369
370    // Parse status code from "HTTP/1.x NNN ..."
371    let parts: Vec<&str> = status_line.splitn(3, ' ').collect();
372    if parts.len() < 2 {
373        return Err(io::Error::new(
374            io::ErrorKind::InvalidData,
375            format!("HTTP CONNECT: malformed response: {status_line}"),
376        ));
377    }
378
379    let status_code: u16 = parts[1].parse().map_err(|_| {
380        io::Error::new(
381            io::ErrorKind::InvalidData,
382            format!("HTTP CONNECT: invalid status code: {}", parts[1]),
383        )
384    })?;
385
386    if status_code != 200 {
387        return Err(io::Error::new(
388            io::ErrorKind::ConnectionRefused,
389            format!("HTTP CONNECT: proxy returned status {status_code}"),
390        ));
391    }
392
393    Ok(())
394}
395
396/// Minimal base64 encoder (avoids external dep).
397fn base64_encode(data: &[u8]) -> String {
398    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
399    let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
400
401    for chunk in data.chunks(3) {
402        let b0 = u32::from(chunk[0]);
403        let b1 = if chunk.len() > 1 {
404            u32::from(chunk[1])
405        } else {
406            0
407        };
408        let b2 = if chunk.len() > 2 {
409            u32::from(chunk[2])
410        } else {
411            0
412        };
413        let triple = (b0 << 16) | (b1 << 8) | b2;
414
415        result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
416        result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
417
418        if chunk.len() > 1 {
419            result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
420        } else {
421            result.push('=');
422        }
423
424        if chunk.len() > 2 {
425            result.push(CHARS[(triple & 0x3F) as usize] as char);
426        } else {
427            result.push('=');
428        }
429    }
430
431    result
432}
433
434// ── SOCKS5 UDP ASSOCIATE ─────────────────────────────────────────────
435
436/// Establish a SOCKS5 UDP relay.
437///
438/// Returns `(relay_addr, control_stream)`. The control stream must be
439/// kept alive for the duration of the relay — dropping it terminates
440/// the association.
441pub(crate) async fn socks5_udp_associate(
442    proxy: &ProxyConfig,
443    local_addr: SocketAddr,
444) -> io::Result<(SocketAddr, TcpStream)> {
445    let proxy_addr = format!("{}:{}", proxy.hostname, proxy.port);
446    let mut stream = TcpStream::connect(&proxy_addr).await?;
447
448    let want_auth = proxy.proxy_type == ProxyType::Socks5Password;
449    socks5_negotiate_method(&mut stream, want_auth).await?;
450
451    if want_auth {
452        let user = proxy.username.as_deref().unwrap_or("");
453        let pass = proxy.password.as_deref().unwrap_or("");
454        socks5_auth(&mut stream, user, pass).await?;
455    }
456
457    // UDP ASSOCIATE request (CMD=3)
458    let mut req = Vec::with_capacity(22);
459    req.push(5); // VER
460    req.push(3); // CMD = UDP ASSOCIATE
461    req.push(0); // RSV
462
463    if proxy.socks5_udp_send_local_ep {
464        encode_socks5_addr(&mut req, local_addr);
465    } else {
466        // Send zeros — let proxy figure it out
467        req.push(1); // ATYP = IPv4
468        req.extend_from_slice(&[0, 0, 0, 0]); // 0.0.0.0
469        req.extend_from_slice(&[0, 0]); // port 0
470    }
471
472    stream.write_all(&req).await?;
473
474    let relay_addr = read_socks5_response_addr(&mut stream).await?;
475
476    Ok((relay_addr, stream))
477}
478
479// ── SOCKS5 UDP header ────────────────────────────────────────────────
480
481/// Encode a SOCKS5 UDP relay header.
482///
483/// Format: RSV(2)=0, FRAG(1)=0, ATYP(1), DST.ADDR(var), DST.PORT(2)
484pub(crate) fn encode_socks5_udp_header(target: SocketAddr) -> Vec<u8> {
485    let mut hdr = Vec::with_capacity(10);
486    hdr.extend_from_slice(&[0, 0]); // RSV
487    hdr.push(0); // FRAG
488    encode_socks5_addr(&mut hdr, target);
489    hdr
490}
491
492/// Decode a SOCKS5 UDP relay header, returning (`source_addr`, `data_offset`).
493pub(crate) fn decode_socks5_udp_header(buf: &[u8]) -> io::Result<(SocketAddr, usize)> {
494    if buf.len() < 7 {
495        return Err(io::Error::new(
496            io::ErrorKind::InvalidData,
497            "SOCKS5 UDP header too short",
498        ));
499    }
500
501    // RSV(2) + FRAG(1) = first 3 bytes
502    let atyp = buf[3];
503
504    match atyp {
505        1 => {
506            // IPv4: 4 + 2 = 6 bytes after ATYP
507            if buf.len() < 10 {
508                return Err(io::Error::new(
509                    io::ErrorKind::InvalidData,
510                    "SOCKS5 UDP: IPv4 header truncated",
511                ));
512            }
513            let ip = Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]);
514            let port = u16::from_be_bytes([buf[8], buf[9]]);
515            Ok((SocketAddr::V4(SocketAddrV4::new(ip, port)), 10))
516        }
517        4 => {
518            // IPv6: 16 + 2 = 18 bytes after ATYP
519            if buf.len() < 22 {
520                return Err(io::Error::new(
521                    io::ErrorKind::InvalidData,
522                    "SOCKS5 UDP: IPv6 header truncated",
523                ));
524            }
525            let ip = Ipv6Addr::from(<[u8; 16]>::try_from(&buf[4..20]).unwrap());
526            let port = u16::from_be_bytes([buf[20], buf[21]]);
527            Ok((SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)), 22))
528        }
529        _ => Err(io::Error::new(
530            io::ErrorKind::InvalidData,
531            format!("SOCKS5 UDP: unsupported ATYP {atyp}"),
532        )),
533    }
534}
535
536// ── ProxiedUdpSocket ─────────────────────────────────────────────────
537
538/// A UDP socket that routes through a SOCKS5 UDP relay.
539///
540/// Wraps each datagram with the SOCKS5 UDP header and strips it on receive.
541/// The `_control` stream must remain alive for the relay to function.
542pub(crate) struct ProxiedUdpSocket {
543    socket: UdpSocket,
544    relay_addr: SocketAddr,
545    _control: TcpStream,
546}
547
548impl ProxiedUdpSocket {
549    /// Create a new proxied UDP socket.
550    ///
551    /// `socket` is the local UDP socket, `relay_addr` is the proxy's UDP relay
552    /// endpoint, and `control` is the TCP stream that must stay alive.
553    pub fn new(socket: UdpSocket, relay_addr: SocketAddr, control: TcpStream) -> Self {
554        Self {
555            socket,
556            relay_addr,
557            _control: control,
558        }
559    }
560
561    /// Send data to `target` through the SOCKS5 UDP relay.
562    pub async fn send_to(&self, data: &[u8], target: SocketAddr) -> io::Result<usize> {
563        let header = encode_socks5_udp_header(target);
564        let mut packet = Vec::with_capacity(header.len() + data.len());
565        packet.extend_from_slice(&header);
566        packet.extend_from_slice(data);
567        self.socket.send_to(&packet, self.relay_addr).await
568    }
569
570    /// Receive data from the SOCKS5 UDP relay, stripping the relay header.
571    ///
572    /// Returns `(bytes_read, source_addr)` where `source_addr` is the original
573    /// sender (extracted from the SOCKS5 header, not the relay).
574    pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
575        let mut raw = vec![0u8; buf.len() + 22]; // max header overhead
576        let (n, _relay) = self.socket.recv_from(&mut raw).await?;
577
578        let (source, offset) = decode_socks5_udp_header(&raw[..n])?;
579        let data_len = n - offset;
580
581        if data_len > buf.len() {
582            return Err(io::Error::new(
583                io::ErrorKind::InvalidData,
584                "SOCKS5 UDP: data exceeds buffer",
585            ));
586        }
587
588        buf[..data_len].copy_from_slice(&raw[offset..n]);
589        Ok((data_len, source))
590    }
591}
592
593// ── Tests ────────────────────────────────────────────────────────────
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    // ── Test 11: SOCKS5 method negotiation + connect request/response ──
600
601    #[test]
602    fn socks5_encode_method_negotiation() {
603        // No auth
604        let no_auth = [5u8, 1, 0];
605        assert_eq!(no_auth[0], 5); // VER
606        assert_eq!(no_auth[1], 1); // NMETHODS
607        assert_eq!(no_auth[2], 0); // NO_AUTH
608
609        // With auth
610        let with_auth = [5u8, 2, 0, 2];
611        assert_eq!(with_auth[0], 5);
612        assert_eq!(with_auth[1], 2);
613        assert_eq!(with_auth[2], 0); // NO_AUTH
614        assert_eq!(with_auth[3], 2); // USERNAME/PASSWORD
615    }
616
617    #[test]
618    fn socks5_encode_connect_request_ipv4() {
619        let target: SocketAddr = "192.168.1.1:8080".parse().unwrap();
620        let mut buf = Vec::new();
621        buf.push(5); // VER
622        buf.push(1); // CMD = CONNECT
623        buf.push(0); // RSV
624        encode_socks5_addr(&mut buf, target);
625
626        assert_eq!(buf[0], 5); // VER
627        assert_eq!(buf[1], 1); // CMD
628        assert_eq!(buf[2], 0); // RSV
629        assert_eq!(buf[3], 1); // ATYP = IPv4
630        assert_eq!(&buf[4..8], &[192, 168, 1, 1]);
631        assert_eq!(&buf[8..10], &8080u16.to_be_bytes());
632    }
633
634    #[test]
635    fn socks5_encode_connect_request_ipv6() {
636        let target: SocketAddr = "[::1]:9999".parse().unwrap();
637        let mut buf = Vec::new();
638        buf.push(5);
639        buf.push(1);
640        buf.push(0);
641        encode_socks5_addr(&mut buf, target);
642
643        assert_eq!(buf[3], 4); // ATYP = IPv6
644        assert_eq!(buf.len(), 3 + 1 + 16 + 2); // header + atyp + ipv6 + port
645        // Last two bytes = port
646        assert_eq!(&buf[20..22], &9999u16.to_be_bytes());
647    }
648
649    #[test]
650    fn socks5_error_messages() {
651        assert!(socks5_error_message(1).contains("general failure"));
652        assert!(socks5_error_message(5).contains("connection refused"));
653        assert!(socks5_error_message(99).contains("unknown"));
654    }
655
656    // ── Test 12: SOCKS5 password auth subnegotiation ──
657
658    #[test]
659    fn socks5_auth_encode() {
660        // Verify RFC 1929 encoding: VER(1)=1, ULEN(1), UNAME, PLEN(1), PASSWD
661        let user = "alice";
662        let pass = "secret";
663
664        let mut req = Vec::new();
665        req.push(1); // subneg version
666        req.push(user.len() as u8);
667        req.extend_from_slice(user.as_bytes());
668        req.push(pass.len() as u8);
669        req.extend_from_slice(pass.as_bytes());
670
671        assert_eq!(req[0], 1); // VER
672        assert_eq!(req[1], 5); // ULEN
673        assert_eq!(&req[2..7], b"alice");
674        assert_eq!(req[7], 6); // PLEN
675        assert_eq!(&req[8..14], b"secret");
676    }
677
678    // ── Test 13: SOCKS4 handshake encode/decode ──
679
680    #[test]
681    fn socks4_encode_connect_request() {
682        let target: SocketAddr = "1.2.3.4:80".parse().unwrap();
683        let mut req = Vec::new();
684        req.push(4); // VN
685        req.push(1); // CD = CONNECT
686        req.extend_from_slice(&target.port().to_be_bytes());
687        req.extend_from_slice(&[1, 2, 3, 4]);
688        req.push(0); // NULL userid
689
690        assert_eq!(req[0], 4);
691        assert_eq!(req[1], 1);
692        assert_eq!(&req[2..4], &80u16.to_be_bytes());
693        assert_eq!(&req[4..8], &[1, 2, 3, 4]);
694        assert_eq!(req[8], 0);
695    }
696
697    #[test]
698    fn socks4_response_granted() {
699        let resp = [0u8, 90, 0, 0, 0, 0, 0, 0];
700        assert_eq!(resp[1], 90); // granted
701    }
702
703    #[test]
704    fn socks4_response_rejected() {
705        let resp = [0u8, 91, 0, 0, 0, 0, 0, 0];
706        assert_ne!(resp[1], 90); // rejected
707    }
708
709    // ── Test 14: HTTP CONNECT format/parse ──
710
711    #[test]
712    fn http_connect_request_no_auth() {
713        let target: SocketAddr = "93.184.216.34:443".parse().unwrap();
714        let host_port = format!("{}:{}", target.ip(), target.port());
715        let request = format!("CONNECT {host_port} HTTP/1.1\r\nHost: {host_port}\r\n\r\n");
716
717        assert!(request.starts_with("CONNECT 93.184.216.34:443 HTTP/1.1\r\n"));
718        assert!(request.contains("Host: 93.184.216.34:443\r\n"));
719        assert!(request.ends_with("\r\n\r\n"));
720    }
721
722    #[test]
723    fn http_connect_request_with_auth() {
724        let encoded = base64_encode(b"user:pass");
725        assert_eq!(encoded, "dXNlcjpwYXNz");
726
727        let header = format!("Proxy-Authorization: Basic {encoded}\r\n");
728        assert!(header.contains("dXNlcjpwYXNz"));
729    }
730
731    #[test]
732    fn http_connect_parse_200_response() {
733        let response = b"HTTP/1.1 200 Connection Established\r\n\r\n";
734        let response_str = String::from_utf8_lossy(response);
735        let status_line = response_str.lines().next().unwrap();
736        let parts: Vec<&str> = status_line.splitn(3, ' ').collect();
737        let status_code: u16 = parts[1].parse().unwrap();
738        assert_eq!(status_code, 200);
739    }
740
741    #[test]
742    fn http_connect_parse_407_response() {
743        let response = b"HTTP/1.1 407 Proxy Authentication Required\r\n\r\n";
744        let response_str = String::from_utf8_lossy(response);
745        let status_line = response_str.lines().next().unwrap();
746        let parts: Vec<&str> = status_line.splitn(3, ' ').collect();
747        let status_code: u16 = parts[1].parse().unwrap();
748        assert_eq!(status_code, 407);
749    }
750
751    // ── Test 15: SOCKS5 UDP header encode/decode ──
752
753    #[test]
754    fn socks5_udp_header_ipv4_roundtrip() {
755        let target: SocketAddr = "10.0.0.1:6881".parse().unwrap();
756        let header = encode_socks5_udp_header(target);
757
758        assert_eq!(header[0], 0); // RSV
759        assert_eq!(header[1], 0); // RSV
760        assert_eq!(header[2], 0); // FRAG
761        assert_eq!(header[3], 1); // ATYP = IPv4
762        assert_eq!(&header[4..8], &[10, 0, 0, 1]);
763        assert_eq!(&header[8..10], &6881u16.to_be_bytes());
764
765        let (decoded_addr, offset) = decode_socks5_udp_header(&header).unwrap();
766        assert_eq!(decoded_addr, target);
767        assert_eq!(offset, 10);
768    }
769
770    #[test]
771    fn socks5_udp_header_ipv6_roundtrip() {
772        let target: SocketAddr = "[2001:db8::1]:51413".parse().unwrap();
773        let header = encode_socks5_udp_header(target);
774
775        assert_eq!(header[3], 4); // ATYP = IPv6
776        assert_eq!(header.len(), 22); // 3 + 1 + 16 + 2
777
778        let (decoded_addr, offset) = decode_socks5_udp_header(&header).unwrap();
779        assert_eq!(decoded_addr, target);
780        assert_eq!(offset, 22);
781    }
782
783    #[test]
784    fn socks5_udp_header_too_short() {
785        let short = [0u8; 5];
786        assert!(decode_socks5_udp_header(&short).is_err());
787    }
788
789    // ── Test 16: ProxyConfig::to_url() ──
790
791    #[test]
792    fn proxy_config_to_url_none() {
793        let cfg = ProxyConfig::default();
794        assert_eq!(cfg.to_url(), "");
795    }
796
797    #[test]
798    fn proxy_config_to_url_socks5() {
799        let cfg = ProxyConfig {
800            proxy_type: ProxyType::Socks5,
801            hostname: "proxy.example.com".into(),
802            port: 1080,
803            ..Default::default()
804        };
805        assert_eq!(cfg.to_url(), "socks5://proxy.example.com:1080");
806    }
807
808    #[test]
809    fn proxy_config_to_url_socks5_password() {
810        let cfg = ProxyConfig {
811            proxy_type: ProxyType::Socks5Password,
812            hostname: "proxy.example.com".into(),
813            port: 1080,
814            username: Some("user".into()),
815            password: Some("pass".into()),
816            ..Default::default()
817        };
818        assert_eq!(cfg.to_url(), "socks5://user:pass@proxy.example.com:1080");
819    }
820
821    #[test]
822    fn proxy_config_to_url_http() {
823        let cfg = ProxyConfig {
824            proxy_type: ProxyType::Http,
825            hostname: "httpproxy.local".into(),
826            port: 8080,
827            ..Default::default()
828        };
829        assert_eq!(cfg.to_url(), "http://httpproxy.local:8080");
830    }
831
832    #[test]
833    fn proxy_config_to_url_http_password() {
834        let cfg = ProxyConfig {
835            proxy_type: ProxyType::HttpPassword,
836            hostname: "httpproxy.local".into(),
837            port: 3128,
838            username: Some("admin".into()),
839            password: Some("secret".into()),
840            ..Default::default()
841        };
842        assert_eq!(cfg.to_url(), "http://admin:secret@httpproxy.local:3128");
843    }
844
845    #[test]
846    fn proxy_config_to_url_socks4() {
847        let cfg = ProxyConfig {
848            proxy_type: ProxyType::Socks4,
849            hostname: "s4proxy".into(),
850            port: 1080,
851            ..Default::default()
852        };
853        assert_eq!(cfg.to_url(), "socks4://s4proxy:1080");
854    }
855
856    #[test]
857    fn base64_encode_basic() {
858        assert_eq!(base64_encode(b""), "");
859        assert_eq!(base64_encode(b"f"), "Zg==");
860        assert_eq!(base64_encode(b"fo"), "Zm8=");
861        assert_eq!(base64_encode(b"foo"), "Zm9v");
862        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
863        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
864        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
865    }
866
867    #[test]
868    fn proxy_config_default_flags() {
869        let cfg = ProxyConfig::default();
870        assert_eq!(cfg.proxy_type, ProxyType::None);
871        assert!(cfg.proxy_peer_connections);
872        assert!(cfg.proxy_tracker_connections);
873        assert!(cfg.proxy_hostnames);
874        assert!(!cfg.socks5_udp_send_local_ep);
875    }
876}