Skip to main content

arcbox_proxy/
socks5.rs

1//! SOCKS5 (RFC 1928) shared primitives + the UDP-ASSOCIATE client.
2//!
3//! The greeting and the ATYP address codec are the single source of truth for
4//! the SOCKS5 address wire format, shared by the CONNECT tunnel client
5//! ([`connect_via_socks5`]), the UDP-ASSOCIATE client ([`Socks5UdpAssociation`]),
6//! and (cross-crate) the downstream aroxy Shadowsocks / Trojan target encoders —
7//! all of which use the same `ATYP | ADDR | PORT` layout. The latter lets arcbox
8//! (and aroxy) route guest UDP through an upstream SOCKS5 proxy.
9
10use std::io;
11use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
12use std::time::Duration;
13
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::net::{TcpStream, UdpSocket};
16use tokio_util::sync::CancellationToken;
17
18/// SOCKS protocol version (5).
19pub(crate) const VER: u8 = 0x05;
20const CMD_UDP_ASSOCIATE: u8 = 0x03;
21const ATYP_IPV4: u8 = 0x01;
22const ATYP_DOMAIN: u8 = 0x03;
23const ATYP_IPV6: u8 = 0x04;
24/// Upper bound on the UDP ASSOCIATE handshake (TCP connect + greeting + reply).
25const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
26
27/// Performs the no-auth SOCKS5 greeting on an established control stream: sends
28/// `[VER, 1, 0x00]` and requires the server to select method `0x00`.
29pub(crate) async fn greet(stream: &mut TcpStream) -> io::Result<()> {
30    stream.write_all(&[VER, 0x01, 0x00]).await?;
31    let mut resp = [0u8; 2];
32    stream.read_exact(&mut resp).await?;
33    if resp[0] != VER {
34        return Err(io::Error::other(format!(
35            "SOCKS5: unsupported version {}",
36            resp[0]
37        )));
38    }
39    if resp[1] != 0x00 {
40        return Err(io::Error::other(format!(
41            "SOCKS5: server chose unsupported auth method 0x{:02X} (expected 0x00 no-auth)",
42            resp[1]
43        )));
44    }
45    Ok(())
46}
47
48/// Appends a SOCKS5 address (`ATYP | ADDR | PORT`) to `out`. IP-literal hosts
49/// use ATYP v4/v6; everything else is sent as a domain name (ATYP 0x03) so the
50/// proxy resolves it — avoiding fake-IP leaks.
51pub fn encode_addr(out: &mut Vec<u8>, host: &str, port: u16) {
52    match host.parse::<IpAddr>() {
53        Ok(IpAddr::V4(ip)) => {
54            out.push(ATYP_IPV4);
55            out.extend_from_slice(&ip.octets());
56        }
57        Ok(IpAddr::V6(ip)) => {
58            out.push(ATYP_IPV6);
59            out.extend_from_slice(&ip.octets());
60        }
61        Err(_) => {
62            out.push(ATYP_DOMAIN);
63            // DNS names are ≤253 bytes, so the 1-byte length field always fits.
64            // A longer `host` is malformed and cannot occur for a real
65            // destination, so assert it loudly in dev/test rather than silently
66            // truncating (which would misroute to a different name); clamp
67            // defensively in release.
68            let bytes = host.as_bytes();
69            debug_assert!(
70                bytes.len() <= 255,
71                "SOCKS5 domain exceeds 255 bytes ({} bytes): {host}",
72                bytes.len()
73            );
74            let len = bytes.len().min(255);
75            out.push(len as u8);
76            out.extend_from_slice(&bytes[..len]);
77        }
78    }
79    out.extend_from_slice(&port.to_be_bytes());
80}
81
82/// Decodes a SOCKS5 address from the front of `buf`, returning
83/// `(host, port, bytes_consumed)`, or `None` if truncated / bad ATYP / non-UTF8
84/// domain.
85#[must_use]
86pub fn decode_addr(buf: &[u8]) -> Option<(String, u16, usize)> {
87    let (host, after) = match *buf.first()? {
88        ATYP_IPV4 => {
89            let o: [u8; 4] = buf.get(1..5)?.try_into().ok()?;
90            (Ipv4Addr::from(o).to_string(), 5)
91        }
92        ATYP_IPV6 => {
93            let o: [u8; 16] = buf.get(1..17)?.try_into().ok()?;
94            (Ipv6Addr::from(o).to_string(), 17)
95        }
96        ATYP_DOMAIN => {
97            let len = *buf.get(1)? as usize;
98            let name = std::str::from_utf8(buf.get(2..2 + len)?).ok()?.to_string();
99            (name, 2 + len)
100        }
101        _ => return None,
102    };
103    let port = u16::from_be_bytes([*buf.get(after)?, *buf.get(after + 1)?]);
104    Some((host, port, after + 2))
105}
106
107/// Reads a SOCKS5 address (`ATYP | ADDR | PORT`) from any async stream. Domains
108/// are decoded strictly (non-UTF8 is an error, matching [`decode_addr`]).
109pub async fn read_addr<R: AsyncReadExt + Unpin>(r: &mut R) -> io::Result<(String, u16)> {
110    let mut atyp = [0u8; 1];
111    r.read_exact(&mut atyp).await?;
112    let host = match atyp[0] {
113        ATYP_IPV4 => {
114            let mut o = [0u8; 4];
115            r.read_exact(&mut o).await?;
116            Ipv4Addr::from(o).to_string()
117        }
118        ATYP_IPV6 => {
119            let mut o = [0u8; 16];
120            r.read_exact(&mut o).await?;
121            Ipv6Addr::from(o).to_string()
122        }
123        ATYP_DOMAIN => {
124            let mut len = [0u8; 1];
125            r.read_exact(&mut len).await?;
126            let mut name = vec![0u8; len[0] as usize];
127            r.read_exact(&mut name).await?;
128            String::from_utf8(name).map_err(|_| {
129                io::Error::new(io::ErrorKind::InvalidData, "SOCKS5: non-UTF8 domain")
130            })?
131        }
132        other => {
133            return Err(io::Error::other(format!(
134                "SOCKS5: unsupported ATYP 0x{other:02X} in response"
135            )));
136        }
137    };
138    let mut port = [0u8; 2];
139    r.read_exact(&mut port).await?;
140    Ok((host, u16::from_be_bytes(port)))
141}
142
143/// Maps a SOCKS5 REP error code to an `io::Error` (RFC 1928 §6).
144pub(crate) fn rep_error(code: u8) -> io::Error {
145    let reason = match code {
146        0x01 => "general SOCKS server failure",
147        0x02 => "connection not allowed by ruleset",
148        0x03 => "network unreachable",
149        0x04 => "host unreachable",
150        0x05 => "connection refused",
151        0x06 => "TTL expired",
152        0x07 => "command not supported",
153        0x08 => "address type not supported",
154        _ => "unknown error",
155    };
156    io::Error::other(format!("SOCKS5: request failed: {reason} (code {code})"))
157}
158
159/// The wildcard bind address (`0.0.0.0:0` or `[::]:0`) in the same address
160/// family as `addr`. A local UDP socket must match the relay's family — an
161/// IPv4 socket cannot `connect` to an IPv6 relay (and vice versa).
162fn unspecified_in_family(addr: SocketAddr) -> SocketAddr {
163    if addr.is_ipv6() {
164        (Ipv6Addr::UNSPECIFIED, 0).into()
165    } else {
166        (Ipv4Addr::UNSPECIFIED, 0).into()
167    }
168}
169
170/// Establishes a TCP tunnel via a SOCKS5 proxy (RFC 1928 CONNECT, no-auth subset).
171///
172/// Hostnames are sent as ATYP=domain so the proxy resolves them (no fake-IP
173/// leak); IP literals use ATYP v4/v6. The returned stream is an end-to-end
174/// tunnel to `host:port`.
175pub async fn connect_via_socks5(proxy: &str, host: &str, port: u16) -> io::Result<TcpStream> {
176    let mut stream = TcpStream::connect(proxy).await?;
177
178    // Phase 1: no-auth greeting (shared with the UDP-ASSOCIATE client).
179    greet(&mut stream).await?;
180
181    // Phase 2: CONNECT request via the shared address codec.
182    let mut req = vec![VER, 0x01, 0x00]; // VER | CMD=CONNECT | RSV
183    encode_addr(&mut req, host, port);
184    stream.write_all(&req).await?;
185
186    // Phase 3: response — [VER, REP, RSV] then the bind address (discarded).
187    let mut hdr = [0u8; 3];
188    stream.read_exact(&mut hdr).await?;
189    if hdr[0] != VER {
190        return Err(io::Error::other(format!(
191            "SOCKS5: unexpected version in response: {}",
192            hdr[0]
193        )));
194    }
195    if hdr[1] != 0x00 {
196        return Err(rep_error(hdr[1]));
197    }
198    let _bind = read_addr(&mut stream).await?;
199
200    tracing::debug!(
201        proxy = %proxy,
202        target = %format!("{host}:{port}"),
203        "SOCKS5 tunnel established"
204    );
205    Ok(stream)
206}
207
208/// Aborts the control-connection watcher task when the association is dropped,
209/// which closes the control TCP stream the watcher owns — tearing down the
210/// association (and the proxy's relay binding).
211#[derive(Debug)]
212struct WatcherGuard(tokio::task::JoinHandle<()>);
213
214impl Drop for WatcherGuard {
215    fn drop(&mut self) {
216        self.0.abort();
217    }
218}
219
220/// A SOCKS5 UDP association (RFC 1928 §7).
221///
222/// The control TCP connection is owned by a watcher task that detects a
223/// proxy-initiated close (EOF) and cancels [`closed`](Self::closed), so a dead
224/// relay surfaces as an error instead of a silent black-hole. The local UDP
225/// socket is connected to the proxy's relay endpoint; datagrams to arbitrary
226/// targets carry their own address header, so one association serves a whole flow.
227#[derive(Debug)]
228pub struct Socks5UdpAssociation {
229    socket: UdpSocket,
230    /// Cancelled when the control connection closes; [`recv_from`](Self::recv_from)
231    /// selects on it so a half-closed proxy is detected promptly.
232    closed: CancellationToken,
233    /// Aborts the control watcher (and thus closes control) on drop.
234    _watcher: WatcherGuard,
235}
236
237impl Socks5UdpAssociation {
238    /// Opens a UDP association via the no-auth SOCKS5 `proxy` (`host:port`),
239    /// bounded by [`HANDSHAKE_TIMEOUT`].
240    pub async fn associate(proxy: &str) -> io::Result<Self> {
241        tokio::time::timeout(HANDSHAKE_TIMEOUT, Self::handshake(proxy))
242            .await
243            .map_err(|_| {
244                io::Error::new(io::ErrorKind::TimedOut, "SOCKS5 UDP associate timed out")
245            })?
246    }
247
248    async fn handshake(proxy: &str) -> io::Result<Self> {
249        let mut control = TcpStream::connect(proxy).await?;
250        greet(&mut control).await?;
251
252        // UDP ASSOCIATE: DST.ADDR/PORT advertise where we'll send from. We don't
253        // know our source port yet, so send the wildcard 0.0.0.0:0.
254        let mut req = vec![VER, CMD_UDP_ASSOCIATE, 0x00];
255        encode_addr(&mut req, "0.0.0.0", 0);
256        control.write_all(&req).await?;
257
258        // Reply: [VER, REP, RSV] then BND.ADDR / BND.PORT.
259        let mut hdr = [0u8; 3];
260        control.read_exact(&mut hdr).await?;
261        if hdr[0] != VER {
262            return Err(io::Error::other(format!(
263                "SOCKS5: unexpected version in response: {}",
264                hdr[0]
265            )));
266        }
267        if hdr[1] != 0x00 {
268            return Err(rep_error(hdr[1]));
269        }
270        let (bnd_host, bnd_port) = read_addr(&mut control).await?;
271
272        // An unspecified BND.ADDR means "same host as the control connection" —
273        // substitute the proxy's IP; otherwise honour the advertised relay.
274        let relay: SocketAddr = match bnd_host.parse::<IpAddr>() {
275            Ok(ip) if ip.is_unspecified() => {
276                let mut peer = control.peer_addr()?;
277                peer.set_port(bnd_port);
278                peer
279            }
280            Ok(ip) => SocketAddr::new(ip, bnd_port),
281            Err(_) => tokio::net::lookup_host((bnd_host.as_str(), bnd_port))
282                .await?
283                .next()
284                .ok_or_else(|| {
285                    io::Error::new(io::ErrorKind::InvalidData, "SOCKS5: BND address unresolved")
286                })?,
287        };
288
289        // Bind the local socket in the relay's address family — an IPv4 `0.0.0.0`
290        // socket cannot `connect` to an IPv6 relay (e.g. a proxy on `::1`).
291        let socket = UdpSocket::bind(unspecified_in_family(relay)).await?;
292        socket.connect(relay).await?;
293
294        // The control stream carries nothing after the handshake; a read that
295        // completes means the proxy closed (EOF) or errored → tear the
296        // association down. The watcher owns `control`, so dropping the
297        // association (→ WatcherGuard::drop → abort) closes it.
298        let closed = CancellationToken::new();
299        let watcher_token = closed.clone();
300        let watcher = tokio::spawn(async move {
301            let mut control = control;
302            let mut scratch = [0u8; 1];
303            let _ = control.read(&mut scratch).await;
304            watcher_token.cancel();
305        });
306
307        tracing::debug!(proxy = %proxy, relay = %relay, "SOCKS5 UDP association established");
308        Ok(Self {
309            socket,
310            closed,
311            _watcher: WatcherGuard(watcher),
312        })
313    }
314
315    /// Sends `payload` to `host:port` through the proxy's UDP relay. The target
316    /// is encoded by name when not an IP literal so the proxy resolves it.
317    pub async fn send_to(&self, payload: &[u8], host: &str, port: u16) -> io::Result<usize> {
318        let mut dgram = vec![0x00, 0x00, 0x00]; // RSV(2) | FRAG(0)
319        encode_addr(&mut dgram, host, port);
320        dgram.extend_from_slice(payload);
321        let sent = self.socket.send(&dgram).await?;
322        debug_assert_eq!(
323            sent,
324            dgram.len(),
325            "UDP short-send: {sent} of {} bytes",
326            dgram.len()
327        );
328        Ok(payload.len())
329    }
330
331    /// Receives one relayed datagram into `buf`, returning `Some((len, host,
332    /// port))` with the inner payload moved to the front of `buf`, or `None` if
333    /// the datagram is fragmented / malformed (skip it, the flow stays up). Errors
334    /// only on a real socket error or a proxy-initiated control close.
335    ///
336    /// `buf` must be large enough for a full UDP datagram (65 535 bytes); a
337    /// smaller buffer truncates at the OS recv as with any UDP socket.
338    pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<Option<(usize, String, u16)>> {
339        let n = tokio::select! {
340            () = self.closed.cancelled() => {
341                return Err(io::Error::new(
342                    io::ErrorKind::ConnectionAborted,
343                    "SOCKS5 UDP: control connection closed",
344                ));
345            }
346            r = self.socket.recv(buf) => r?,
347        };
348
349        // [RSV(2) | FRAG(1) | ATYP ADDR PORT | payload]. Drop fragments (FRAG!=0)
350        // and anything too short to hold a header.
351        if n < 4 || buf[2] != 0x00 {
352            return Ok(None);
353        }
354        let Some((host, port, consumed)) = decode_addr(&buf[3..]) else {
355            return Ok(None);
356        };
357        let start = 3 + consumed;
358        if start > n {
359            return Ok(None);
360        }
361        let len = n - start;
362        buf.copy_within(start..n, 0);
363        Ok(Some((len, host, port)))
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use tokio::net::TcpListener;
371
372    #[test]
373    fn addr_codec_round_trips() {
374        for (host, port) in [("example.com", 443u16), ("1.2.3.4", 53), ("::1", 80)] {
375            let mut buf = Vec::new();
376            encode_addr(&mut buf, host, port);
377            let (h, p, used) = decode_addr(&buf).expect("decodes");
378            assert_eq!((h.as_str(), p, used), (host, port, buf.len()));
379        }
380    }
381
382    #[test]
383    fn bind_family_matches_relay() {
384        // The local UDP socket must share the relay's family, else `connect` fails.
385        let v6: SocketAddr = "[::1]:9".parse().unwrap();
386        let v4: SocketAddr = "1.2.3.4:9".parse().unwrap();
387        assert!(
388            unspecified_in_family(v6).is_ipv6(),
389            "IPv6 relay → IPv6 bind"
390        );
391        assert!(
392            unspecified_in_family(v4).is_ipv4(),
393            "IPv4 relay → IPv4 bind"
394        );
395    }
396
397    /// Minimal no-auth SOCKS5 server: validates the client's greeting and
398    /// domain-ATYP CONNECT request, replies success, then echoes one message
399    /// back through the established tunnel.
400    async fn mock_socks5(listener: TcpListener, expect_host: &'static str, expect_port: u16) {
401        let (mut s, _) = listener.accept().await.unwrap();
402
403        // Greeting: VER=5, NMETHODS=1, METHODS=[0x00].
404        let mut greeting = [0u8; 3];
405        s.read_exact(&mut greeting).await.unwrap();
406        assert_eq!(greeting, [0x05, 0x01, 0x00], "client greeting");
407        s.write_all(&[0x05, 0x00]).await.unwrap(); // choose no-auth
408
409        // Connect request: VER, CMD=CONNECT, RSV, ATYP=domain.
410        let mut hdr = [0u8; 4];
411        s.read_exact(&mut hdr).await.unwrap();
412        assert_eq!(hdr, [0x05, 0x01, 0x00, 0x03], "connect request header");
413        let mut len = [0u8; 1];
414        s.read_exact(&mut len).await.unwrap();
415        let mut host = vec![0u8; len[0] as usize];
416        s.read_exact(&mut host).await.unwrap();
417        let mut port = [0u8; 2];
418        s.read_exact(&mut port).await.unwrap();
419        assert_eq!(host, expect_host.as_bytes(), "CONNECT host (by name)");
420        assert_eq!(u16::from_be_bytes(port), expect_port, "CONNECT port");
421
422        // Reply: success, ATYP=IPv4, BND.ADDR=0.0.0.0, BND.PORT=0.
423        s.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
424            .await
425            .unwrap();
426
427        // Tunnel is now end-to-end; echo one payload.
428        let mut buf = [0u8; 5];
429        s.read_exact(&mut buf).await.unwrap();
430        s.write_all(&buf).await.unwrap();
431    }
432
433    #[tokio::test]
434    async fn socks5_connects_by_domain_and_tunnels() {
435        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
436        let addr = listener.local_addr().unwrap();
437        let server = tokio::spawn(mock_socks5(listener, "example.com", 443));
438
439        let mut stream = connect_via_socks5(&addr.to_string(), "example.com", 443)
440            .await
441            .expect("SOCKS5 handshake should complete");
442
443        // The returned stream is an end-to-end tunnel through the proxy.
444        stream.write_all(b"hello").await.unwrap();
445        let mut got = [0u8; 5];
446        stream.read_exact(&mut got).await.unwrap();
447        assert_eq!(&got, b"hello", "payload round-trips through the tunnel");
448        server.await.unwrap();
449    }
450
451    #[tokio::test]
452    async fn socks5_propagates_server_failure() {
453        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
454        let addr = listener.local_addr().unwrap();
455        tokio::spawn(async move {
456            let (mut s, _) = listener.accept().await.unwrap();
457            let mut greeting = [0u8; 3];
458            s.read_exact(&mut greeting).await.unwrap();
459            s.write_all(&[0x05, 0x00]).await.unwrap();
460            // Drain the connect request, then reply REP=0x05 (connection refused).
461            let mut hdr = [0u8; 4];
462            s.read_exact(&mut hdr).await.unwrap();
463            let mut len = [0u8; 1];
464            s.read_exact(&mut len).await.unwrap();
465            let mut rest = vec![0u8; len[0] as usize + 2];
466            s.read_exact(&mut rest).await.unwrap();
467            s.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
468                .await
469                .unwrap();
470        });
471
472        let err = connect_via_socks5(&addr.to_string(), "blocked.example", 443)
473            .await
474            .expect_err("server failure must surface as an error");
475        assert!(err.to_string().contains("connection refused"), "{err}");
476    }
477
478    /// Mock SOCKS5 UDP relay: completes the control handshake, advertises a real
479    /// UDP relay socket as BND, then echoes one datagram back (re-headered with
480    /// the decoded target as the source).
481    async fn mock_socks5_udp(listener: TcpListener) {
482        let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
483        let relay_addr = relay.local_addr().unwrap();
484
485        let (mut ctrl, _) = listener.accept().await.unwrap();
486        let mut g = [0u8; 3];
487        ctrl.read_exact(&mut g).await.unwrap();
488        assert_eq!(g, [0x05, 0x01, 0x00], "greeting");
489        ctrl.write_all(&[0x05, 0x00]).await.unwrap();
490
491        // UDP ASSOCIATE request: [5, 3, 0, ATYP=1, 0.0.0.0, 0].
492        let mut req = [0u8; 4];
493        ctrl.read_exact(&mut req).await.unwrap();
494        assert_eq!(&req[..2], &[0x05, 0x03], "UDP ASSOCIATE cmd");
495        assert_eq!(req[3], 0x01, "wildcard DST is ATYP v4");
496        let mut rest = [0u8; 6];
497        ctrl.read_exact(&mut rest).await.unwrap();
498
499        // Reply success, BND = the relay socket.
500        let mut reply = vec![0x05, 0x00, 0x00];
501        encode_addr(&mut reply, &relay_addr.ip().to_string(), relay_addr.port());
502        ctrl.write_all(&reply).await.unwrap();
503
504        // Relay one datagram: decode target + payload, echo it back.
505        let mut buf = vec![0u8; 2048];
506        let (n, client) = relay.recv_from(&mut buf).await.unwrap();
507        let pkt = &buf[..n];
508        assert_eq!(pkt[2], 0x00, "FRAG 0");
509        let (host, port, consumed) = decode_addr(&pkt[3..]).unwrap();
510        let payload = &pkt[3 + consumed..];
511        let mut echo = vec![0x00, 0x00, 0x00];
512        encode_addr(&mut echo, &host, port);
513        echo.extend_from_slice(payload);
514        relay.send_to(&echo, client).await.unwrap();
515
516        // Hold the control conn open until the client has finished.
517        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
518        drop(ctrl);
519    }
520
521    #[tokio::test]
522    async fn udp_associate_round_trips_through_relay() {
523        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
524        let proxy = listener.local_addr().unwrap();
525        let server = tokio::spawn(mock_socks5_udp(listener));
526
527        let assoc = Socks5UdpAssociation::associate(&proxy.to_string())
528            .await
529            .expect("association established");
530        assoc.send_to(b"ping", "example.com", 53).await.unwrap();
531
532        let mut buf = [0u8; 64];
533        let (n, host, port) = assoc
534            .recv_from(&mut buf)
535            .await
536            .unwrap()
537            .expect("a deliverable datagram");
538        assert_eq!(&buf[..n], b"ping", "payload round-trips through the relay");
539        assert_eq!(
540            (host.as_str(), port),
541            ("example.com", 53),
542            "target echoed back"
543        );
544        server.await.unwrap();
545    }
546
547    #[tokio::test]
548    async fn udp_associate_surfaces_rep_error() {
549        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
550        let proxy = listener.local_addr().unwrap();
551        tokio::spawn(async move {
552            let (mut ctrl, _) = listener.accept().await.unwrap();
553            let mut g = [0u8; 3];
554            ctrl.read_exact(&mut g).await.unwrap();
555            ctrl.write_all(&[0x05, 0x00]).await.unwrap();
556            let mut req = [0u8; 4];
557            ctrl.read_exact(&mut req).await.unwrap();
558            let mut rest = [0u8; 6];
559            ctrl.read_exact(&mut rest).await.unwrap();
560            // REP = 0x07 (command not supported).
561            ctrl.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
562                .await
563                .unwrap();
564        });
565
566        let err = Socks5UdpAssociation::associate(&proxy.to_string())
567            .await
568            .expect_err("REP failure surfaces");
569        assert!(err.to_string().contains("command not supported"), "{err}");
570    }
571
572    /// A fragmented (FRAG!=0) relayed datagram is skipped (Ok(None)), not fatal.
573    #[tokio::test]
574    async fn fragmented_datagram_is_skipped_not_fatal() {
575        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
576        let proxy = listener.local_addr().unwrap();
577        tokio::spawn(async move {
578            let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
579            let relay_addr = relay.local_addr().unwrap();
580            let (mut ctrl, _) = listener.accept().await.unwrap();
581            let mut g = [0u8; 3];
582            ctrl.read_exact(&mut g).await.unwrap();
583            ctrl.write_all(&[0x05, 0x00]).await.unwrap();
584            let mut req = [0u8; 4];
585            ctrl.read_exact(&mut req).await.unwrap();
586            let mut rest = [0u8; 6];
587            ctrl.read_exact(&mut rest).await.unwrap();
588            let mut reply = vec![0x05, 0x00, 0x00];
589            encode_addr(&mut reply, &relay_addr.ip().to_string(), relay_addr.port());
590            ctrl.write_all(&reply).await.unwrap();
591            let (_n, client) = relay.recv_from(&mut [0u8; 64]).await.unwrap();
592            // FRAG = 1 → must be skipped.
593            let mut frag = vec![0x00, 0x00, 0x01];
594            encode_addr(&mut frag, "1.2.3.4", 9);
595            frag.extend_from_slice(b"x");
596            relay.send_to(&frag, client).await.unwrap();
597            tokio::time::sleep(std::time::Duration::from_millis(80)).await;
598            drop(ctrl);
599        });
600
601        let assoc = Socks5UdpAssociation::associate(&proxy.to_string())
602            .await
603            .unwrap();
604        assoc.send_to(b"hi", "1.2.3.4", 9).await.unwrap();
605        let mut buf = [0u8; 64];
606        let got = assoc.recv_from(&mut buf).await.unwrap();
607        assert!(
608            got.is_none(),
609            "fragmented datagram is skipped, flow survives"
610        );
611    }
612}