Skip to main content

microsandbox_network/proxy/
socks.rs

1//! SOCKS outbound proxy builders, credentials, and transport implementations.
2
3use std::fmt;
4#[cfg(feature = "engine")]
5use std::io;
6#[cfg(feature = "engine")]
7use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
8
9use microsandbox_types::SecretSource;
10use serde::{Deserialize, Serialize};
11#[cfg(feature = "engine")]
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13#[cfg(feature = "engine")]
14use tokio::net::TcpStream;
15#[cfg(feature = "engine")]
16use tokio_socks::tcp::Socks4Stream;
17use zeroize::Zeroizing;
18
19#[cfg(feature = "engine")]
20use super::http_connect::HttpConnectProtocol;
21use super::types::{
22    OutboundProxy, OutboundProxyBuildError, OutboundProxyBuilder, OutboundProxyConfig,
23    OutboundProxyProtocol, ResolvedOutboundProxy,
24};
25#[cfg(feature = "engine")]
26use crate::engine::dns::forwarder::{DnsForwarder, DnsForwarderHandle};
27
28//--------------------------------------------------------------------------------------------------
29// Types
30//--------------------------------------------------------------------------------------------------
31
32/// Environment-backed username/password credentials for a SOCKS5 proxy.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Socks5Credentials {
35    username: String,
36    password: SecretSource,
37}
38
39/// Resolved SOCKS5 credentials held only by the network runtime.
40#[doc(hidden)]
41#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ResolvedSocks5Credentials {
43    username: String,
44    password: Zeroizing<String>,
45}
46
47/// Builds a SOCKS4 outbound proxy.
48#[derive(Debug, Clone)]
49pub struct Socks4ProxyBuilder {
50    address: String,
51    user_id: Option<String>,
52}
53
54/// Builds a SOCKS5 outbound proxy.
55#[derive(Debug, Clone)]
56pub struct Socks5ProxyBuilder {
57    address: String,
58    credentials: Option<Socks5Credentials>,
59}
60
61/// Active SOCKS5 UDP association.
62#[cfg(feature = "engine")]
63pub(crate) struct Socks5UdpAssociation {
64    _control: TcpStream,
65    socket: tokio::net::UdpSocket,
66    dns_forwarder: Option<DnsForwarderHandle>,
67}
68
69/// SOCKS5 wire protocol operations shared by TCP and UDP proxying.
70#[cfg(feature = "engine")]
71struct Socks5Protocol;
72
73/// Address returned by a SOCKS5 command reply.
74#[cfg(feature = "engine")]
75enum Socks5ReplyAddress {
76    Socket(SocketAddr),
77    Domain { name: String, port: u16 },
78}
79
80//--------------------------------------------------------------------------------------------------
81// Methods
82//--------------------------------------------------------------------------------------------------
83
84impl ResolvedOutboundProxy {
85    /// Builds the complete proxy used by the network runtime.
86    #[doc(hidden)]
87    pub fn build(
88        configured: Option<&OutboundProxy>,
89        resolved: Option<ResolvedSocks5Credentials>,
90    ) -> Result<Option<Self>, OutboundProxyBuildError> {
91        let Some(configured) = configured else {
92            if resolved.is_some() {
93                return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
94                    reason: "launch credentials require a configured SOCKS5 proxy",
95                });
96            }
97            return Ok(None);
98        };
99
100        configured.validate()?;
101        match configured {
102            OutboundProxy::HttpConnect { address } => {
103                if resolved.is_some() {
104                    return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
105                        reason: "launch credentials require a configured SOCKS5 proxy",
106                    });
107                }
108                Ok(Some(Self::HttpConnect { address: *address }))
109            }
110            OutboundProxy::Socks4 { address, user_id } => {
111                if resolved.is_some() {
112                    return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
113                        reason: "launch credentials require a configured SOCKS5 proxy",
114                    });
115                }
116                Ok(Some(Self::Socks4 {
117                    address: *address,
118                    user_id: user_id.clone(),
119                }))
120            }
121            OutboundProxy::Socks5 {
122                address,
123                credentials,
124            } => {
125                let credentials = match (credentials, resolved) {
126                    (None, None) => None,
127                    (None, Some(_)) => {
128                        return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
129                            reason: "launch credentials require configured SOCKS5 credentials",
130                        });
131                    }
132                    (Some(_), None) => {
133                        return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
134                            reason: "configured SOCKS5 credentials were not resolved at launch",
135                        });
136                    }
137                    (Some(configured), Some(resolved)) => {
138                        if resolved.username != configured.username {
139                            return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
140                                reason: "launch username does not match the durable configuration",
141                            });
142                        }
143                        resolved.validate()?;
144                        Some(resolved)
145                    }
146                };
147                Ok(Some(Self::Socks5 {
148                    address: *address,
149                    credentials,
150                }))
151            }
152        }
153    }
154
155    /// Connects to `destination` through this outbound proxy.
156    #[cfg(feature = "engine")]
157    pub(crate) async fn connect(&self, destination: SocketAddr) -> io::Result<TcpStream> {
158        match self {
159            Self::HttpConnect { address } => {
160                HttpConnectProtocol::connect(*address, destination).await
161            }
162            Self::Socks4 { address, user_id } => match user_id {
163                Some(user_id) => {
164                    Socks4Stream::connect_with_userid(*address, destination, user_id).await
165                }
166                None => Socks4Stream::connect(*address, destination).await,
167            }
168            .map(|stream| stream.into_inner())
169            .map_err(io::Error::other),
170            Self::Socks5 {
171                address,
172                credentials,
173            } => {
174                let mut stream = TcpStream::connect(*address).await?;
175                Socks5Protocol::negotiate(&mut stream, credentials.as_ref()).await?;
176                // The bound address is informational for CONNECT. Parse it to
177                // validate the reply, but do not resolve proxy-supplied domains.
178                let _ = Socks5Protocol::command(&mut stream, 0x01, destination).await?;
179                Ok(stream)
180            }
181        }
182    }
183
184    /// Opens a SOCKS5 UDP association for relaying datagrams.
185    #[cfg(feature = "engine")]
186    pub(crate) async fn associate_udp(
187        &self,
188        dns_forwarder: Option<DnsForwarderHandle>,
189    ) -> io::Result<Socks5UdpAssociation> {
190        let Self::Socks5 {
191            address,
192            credentials,
193        } = self
194        else {
195            return Err(io::Error::new(
196                io::ErrorKind::Unsupported,
197                "SOCKS4 does not support UDP relay",
198            ));
199        };
200
201        let mut control = TcpStream::connect(*address).await?;
202        Socks5Protocol::negotiate(&mut control, credentials.as_ref()).await?;
203
204        let control_local = control.local_addr()?;
205        // The UDP endpoint is not known until the proxy returns its relay.
206        // RFC 1928 requires an all-zero endpoint in that case.
207        let request_address = match control_local.ip() {
208            IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
209            IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
210        };
211        let relays = match Socks5Protocol::command(&mut control, 0x03, request_address).await? {
212            Socks5ReplyAddress::Socket(relay) => vec![relay],
213            Socks5ReplyAddress::Domain { name, port } => {
214                Socks5Protocol::resolve_domain(dns_forwarder.as_ref(), &name, port).await?
215            }
216        };
217        Socks5UdpAssociation::connect(control, relays, dns_forwarder).await
218    }
219}
220
221impl OutboundProxy {
222    fn validate(&self) -> Result<(), OutboundProxyBuildError> {
223        match self {
224            Self::HttpConnect { .. } => Ok(()),
225            Self::Socks4 { user_id, .. } => Self::validate_socks4_user_id(user_id.as_deref()),
226            Self::Socks5 { credentials, .. } => credentials
227                .as_ref()
228                .map_or(Ok(()), Socks5Credentials::validate),
229        }
230    }
231
232    fn validate_socks4_user_id(user_id: Option<&str>) -> Result<(), OutboundProxyBuildError> {
233        let Some(user_id) = user_id else {
234            return Ok(());
235        };
236        let reason = if user_id.is_empty() {
237            "must not be empty"
238        } else if user_id.len() > 255 {
239            "must be at most 255 bytes"
240        } else if user_id.contains('\0') {
241            "must not contain a null byte"
242        } else {
243            return Ok(());
244        };
245
246        Err(OutboundProxyBuildError::InvalidSocks4UserId { reason })
247    }
248}
249
250impl ResolvedSocks5Credentials {
251    /// Creates credentials for the private launch contract.
252    #[doc(hidden)]
253    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
254        Self {
255            username: username.into(),
256            password: Zeroizing::new(password.into()),
257        }
258    }
259
260    /// Validates the RFC 1929 one-octet credential lengths.
261    fn validate(&self) -> Result<(), OutboundProxyBuildError> {
262        let reason = if self.username.is_empty() {
263            "username must not be empty"
264        } else if self.username.len() > u8::MAX as usize {
265            "username must be at most 255 bytes"
266        } else if self.password.is_empty() {
267            "password must not be empty"
268        } else if self.password.len() > u8::MAX as usize {
269            "password must be at most 255 bytes"
270        } else {
271            return Ok(());
272        };
273
274        Err(OutboundProxyBuildError::InvalidSocks5Credentials { reason })
275    }
276}
277
278impl Socks5Credentials {
279    pub(crate) fn username(&self) -> &str {
280        &self.username
281    }
282
283    pub(crate) fn password_source(&self) -> &SecretSource {
284        &self.password
285    }
286
287    /// Validates the durable SOCKS5 credential configuration.
288    fn validate(&self) -> Result<(), OutboundProxyBuildError> {
289        if self.username.is_empty() {
290            return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
291                reason: "username must not be empty",
292            });
293        }
294        if self.username.len() > u8::MAX as usize {
295            return Err(OutboundProxyBuildError::InvalidSocks5Credentials {
296                reason: "username must be at most 255 bytes",
297            });
298        }
299        match &self.password {
300            SecretSource::Env { var } if var.is_empty() => {
301                Err(OutboundProxyBuildError::InvalidSocks5Credentials {
302                    reason: "password environment variable must not be empty",
303                })
304            }
305            SecretSource::Env { .. } => Ok(()),
306            SecretSource::Store { .. } => Err(OutboundProxyBuildError::InvalidSocks5Credentials {
307                reason: "store-backed password sources are not supported yet",
308            }),
309        }
310    }
311}
312
313#[cfg(feature = "engine")]
314impl Socks5UdpAssociation {
315    /// Connects a UDP socket to the first usable relay address.
316    async fn connect(
317        control: TcpStream,
318        relays: Vec<SocketAddr>,
319        dns_forwarder: Option<DnsForwarderHandle>,
320    ) -> io::Result<Self> {
321        let peer_ip = control.peer_addr()?.ip();
322        let mut last_error = None;
323
324        for relay in relays {
325            let relay = if relay.ip().is_unspecified() {
326                SocketAddr::new(peer_ip, relay.port())
327            } else {
328                relay
329            };
330            let bind_address = match relay.ip() {
331                IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
332                IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
333            };
334            let socket = match tokio::net::UdpSocket::bind(bind_address).await {
335                Ok(socket) => socket,
336                Err(error) => {
337                    last_error = Some(error);
338                    continue;
339                }
340            };
341            match socket.connect(relay).await {
342                Ok(()) => {
343                    return Ok(Self {
344                        _control: control,
345                        socket,
346                        dns_forwarder,
347                    });
348                }
349                Err(error) => last_error = Some(error),
350            }
351        }
352
353        Err(last_error.unwrap_or_else(|| {
354            io::Error::new(
355                io::ErrorKind::AddrNotAvailable,
356                "SOCKS5 proxy returned no usable UDP relay address",
357            )
358        }))
359    }
360
361    /// Sends one payload to `destination` through the UDP association.
362    pub(crate) async fn send_to(
363        &self,
364        payload: &[u8],
365        destination: SocketAddr,
366    ) -> io::Result<usize> {
367        let mut datagram =
368            Vec::with_capacity(Socks5Protocol::address_len(destination) + 3 + payload.len());
369        datagram.extend_from_slice(&[0x00, 0x00, 0x00]);
370        Socks5Protocol::encode_address(&mut datagram, destination);
371        datagram.extend_from_slice(payload);
372        self.socket.send(&datagram).await.map(|_| payload.len())
373    }
374
375    /// Receives one payload and returns the remote endpoints encoded by the proxy.
376    pub(crate) async fn recv_from(
377        &self,
378        buffer: &mut [u8],
379    ) -> io::Result<(usize, Vec<SocketAddr>)> {
380        let received = self.socket.recv(buffer).await?;
381        let (header_len, source) = Socks5Protocol::decode_udp_header(&buffer[..received])?;
382        let sources = match source {
383            Socks5ReplyAddress::Socket(source) => vec![source],
384            Socks5ReplyAddress::Domain { name, port } => {
385                Socks5Protocol::resolve_domain(self.dns_forwarder.as_ref(), &name, port).await?
386            }
387        };
388        let payload_len = received - header_len;
389        buffer.copy_within(header_len..received, 0);
390        Ok((payload_len, sources))
391    }
392}
393
394impl fmt::Debug for ResolvedSocks5Credentials {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        f.debug_struct("ResolvedSocks5Credentials")
397            .field("username", &self.username)
398            .field("password", &"[REDACTED]")
399            .finish()
400    }
401}
402
403impl OutboundProxyBuilder {
404    /// Creates a protocol selector.
405    pub fn new() -> Self {
406        Self
407    }
408
409    /// Starts building an HTTP CONNECT outbound proxy.
410    pub fn http_connect(self, address: impl Into<String>) -> super::HttpConnectProxyBuilder {
411        super::HttpConnectProxyBuilder::new(address)
412    }
413
414    /// Starts building a SOCKS4 outbound proxy.
415    pub fn socks4(self, address: impl Into<String>) -> Socks4ProxyBuilder {
416        Socks4ProxyBuilder {
417            address: address.into(),
418            user_id: None,
419        }
420    }
421
422    /// Starts building a SOCKS5 outbound proxy.
423    pub fn socks5(self, address: impl Into<String>) -> Socks5ProxyBuilder {
424        Socks5ProxyBuilder {
425            address: address.into(),
426            credentials: None,
427        }
428    }
429}
430
431impl Socks4ProxyBuilder {
432    /// Sets the optional user ID sent during the SOCKS4 handshake.
433    pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
434        self.user_id = Some(user_id.into());
435        self
436    }
437}
438
439impl Socks5ProxyBuilder {
440    /// Sets username authentication and a host-side password source.
441    pub fn credentials(mut self, username: impl Into<String>, password: SecretSource) -> Self {
442        self.credentials = Some(Socks5Credentials {
443            username: username.into(),
444            password,
445        });
446        self
447    }
448}
449
450//--------------------------------------------------------------------------------------------------
451// Trait Implementations
452//--------------------------------------------------------------------------------------------------
453
454impl OutboundProxyConfig for Socks4ProxyBuilder {
455    fn build(self) -> Result<OutboundProxy, OutboundProxyBuildError> {
456        let address =
457            self.address
458                .parse()
459                .map_err(|source| OutboundProxyBuildError::InvalidAddress {
460                    protocol: OutboundProxyProtocol::Socks4,
461                    address: self.address,
462                    source,
463                })?;
464
465        OutboundProxy::validate_socks4_user_id(self.user_id.as_deref())?;
466        Ok(OutboundProxy::Socks4 {
467            address,
468            user_id: self.user_id,
469        })
470    }
471}
472
473impl OutboundProxyConfig for Socks5ProxyBuilder {
474    fn build(self) -> Result<OutboundProxy, OutboundProxyBuildError> {
475        let address =
476            self.address
477                .parse()
478                .map_err(|source| OutboundProxyBuildError::InvalidAddress {
479                    protocol: OutboundProxyProtocol::Socks5,
480                    address: self.address,
481                    source,
482                })?;
483        if let Some(credentials) = &self.credentials {
484            credentials.validate()?;
485        }
486        Ok(OutboundProxy::Socks5 {
487            address,
488            credentials: self.credentials,
489        })
490    }
491}
492
493impl OutboundProxyConfig for OutboundProxy {
494    fn build(self) -> Result<OutboundProxy, OutboundProxyBuildError> {
495        self.validate()?;
496        Ok(self)
497    }
498}
499
500#[cfg(feature = "engine")]
501impl Socks5Protocol {
502    /// Resolves a domain-form SOCKS5 endpoint through the internal DNS path.
503    async fn resolve_domain(
504        dns_forwarder: Option<&DnsForwarderHandle>,
505        name: &str,
506        port: u16,
507    ) -> io::Result<Vec<SocketAddr>> {
508        let dns_forwarder = dns_forwarder.ok_or_else(|| {
509            io::Error::other("DNS forwarder is unavailable for SOCKS5 UDP endpoint resolution")
510        })?;
511        let forwarder = DnsForwarder::wait(dns_forwarder.clone())
512            .await
513            .ok_or_else(|| {
514                io::Error::other("DNS forwarder is unavailable for SOCKS5 UDP endpoint resolution")
515            })?;
516        Ok(forwarder
517            .resolve_proxy_domain(name)
518            .await?
519            .into_iter()
520            .map(|address| SocketAddr::new(address, port))
521            .collect())
522    }
523
524    /// Negotiates a SOCKS5 authentication method and performs username/password
525    /// authentication when selected by the proxy.
526    async fn negotiate(
527        stream: &mut TcpStream,
528        credentials: Option<&ResolvedSocks5Credentials>,
529    ) -> io::Result<()> {
530        if let Some(credentials) = credentials {
531            credentials.validate().map_err(io::Error::other)?;
532        }
533
534        match credentials {
535            Some(_) => stream.write_all(&[0x05, 0x02, 0x00, 0x02]).await?,
536            None => stream.write_all(&[0x05, 0x01, 0x00]).await?,
537        }
538
539        let mut selection = [0u8; 2];
540        stream.read_exact(&mut selection).await?;
541        if selection[0] != 0x05 {
542            return Err(Self::invalid_response("invalid method-selection version"));
543        }
544
545        match selection[1] {
546            0x00 => Ok(()),
547            0x02 => {
548                let credentials = credentials.ok_or_else(|| {
549                    io::Error::new(
550                        io::ErrorKind::PermissionDenied,
551                        "SOCKS5 proxy requires username/password authentication",
552                    )
553                })?;
554                let username = credentials.username.as_bytes();
555                let password = credentials.password.as_bytes();
556                let mut request = Vec::with_capacity(3 + username.len() + password.len());
557                request.extend_from_slice(&[0x01, username.len() as u8]);
558                request.extend_from_slice(username);
559                request.push(password.len() as u8);
560                request.extend_from_slice(password);
561                stream.write_all(&request).await?;
562
563                let mut response = [0u8; 2];
564                stream.read_exact(&mut response).await?;
565                if response[0] != 0x01 {
566                    return Err(Self::invalid_response(
567                        "invalid username/password response version",
568                    ));
569                }
570                if response[1] != 0x00 {
571                    return Err(io::Error::new(
572                        io::ErrorKind::PermissionDenied,
573                        "SOCKS5 username/password authentication failed",
574                    ));
575                }
576                Ok(())
577            }
578            0xff => Err(io::Error::new(
579                io::ErrorKind::PermissionDenied,
580                "SOCKS5 proxy rejected all offered authentication methods",
581            )),
582            method => Err(Self::invalid_response(format!(
583                "SOCKS5 proxy selected unsupported authentication method {method:#04x}"
584            ))),
585        }
586    }
587
588    /// Sends a SOCKS5 command and returns the bound address from its reply.
589    async fn command(
590        stream: &mut TcpStream,
591        command: u8,
592        destination: SocketAddr,
593    ) -> io::Result<Socks5ReplyAddress> {
594        let mut request = Vec::with_capacity(3 + Self::address_len(destination));
595        request.extend_from_slice(&[0x05, command, 0x00]);
596        Self::encode_address(&mut request, destination);
597        stream.write_all(&request).await?;
598
599        let mut header = [0u8; 4];
600        stream.read_exact(&mut header).await?;
601        if header[0] != 0x05 || header[2] != 0x00 {
602            return Err(Self::invalid_response("invalid SOCKS5 command reply"));
603        }
604        if header[1] != 0x00 {
605            return Err(io::Error::other(format!(
606                "SOCKS5 proxy command failed: {}",
607                Self::reply_message(header[1])
608            )));
609        }
610
611        Self::read_address(stream, header[3]).await
612    }
613
614    /// Reads a SOCKS5 address whose address-type byte was already consumed.
615    async fn read_address(
616        stream: &mut TcpStream,
617        address_type: u8,
618    ) -> io::Result<Socks5ReplyAddress> {
619        let ip = match address_type {
620            0x01 => {
621                let mut octets = [0u8; 4];
622                stream.read_exact(&mut octets).await?;
623                IpAddr::V4(Ipv4Addr::from(octets))
624            }
625            0x04 => {
626                let mut octets = [0u8; 16];
627                stream.read_exact(&mut octets).await?;
628                IpAddr::V6(Ipv6Addr::from(octets))
629            }
630            0x03 => {
631                let length = stream.read_u8().await? as usize;
632                let mut domain = vec![0u8; length];
633                stream.read_exact(&mut domain).await?;
634                let domain = String::from_utf8(domain).map_err(|_| {
635                    Self::invalid_response("SOCKS5 reply contains a non-UTF-8 domain")
636                })?;
637                let mut port = [0u8; 2];
638                stream.read_exact(&mut port).await?;
639                return Ok(Socks5ReplyAddress::Domain {
640                    name: domain,
641                    port: u16::from_be_bytes(port),
642                });
643            }
644            _ => return Err(Self::invalid_response("unsupported SOCKS5 address type")),
645        };
646
647        let mut port = [0u8; 2];
648        stream.read_exact(&mut port).await?;
649        Ok(Socks5ReplyAddress::Socket(SocketAddr::new(
650            ip,
651            u16::from_be_bytes(port),
652        )))
653    }
654
655    /// Encodes a socket address in SOCKS5 address form.
656    fn encode_address(output: &mut Vec<u8>, address: SocketAddr) {
657        match address {
658            SocketAddr::V4(address) => {
659                output.push(0x01);
660                output.extend_from_slice(&address.ip().octets());
661                output.extend_from_slice(&address.port().to_be_bytes());
662            }
663            SocketAddr::V6(address) => {
664                output.push(0x04);
665                output.extend_from_slice(&address.ip().octets());
666                output.extend_from_slice(&address.port().to_be_bytes());
667            }
668        }
669    }
670
671    /// Returns the encoded length of a SOCKS5 socket address.
672    fn address_len(address: SocketAddr) -> usize {
673        match address {
674            SocketAddr::V4(_) => 7,
675            SocketAddr::V6(_) => 19,
676        }
677    }
678
679    /// Decodes a SOCKS5 UDP request header and returns its payload offset and endpoint.
680    fn decode_udp_header(datagram: &[u8]) -> io::Result<(usize, Socks5ReplyAddress)> {
681        if datagram.len() < 4 || datagram[..2] != [0x00, 0x00] {
682            return Err(Self::invalid_response("invalid SOCKS5 UDP header"));
683        }
684        if datagram[2] != 0x00 {
685            return Err(Self::invalid_response(
686                "fragmented SOCKS5 UDP datagrams are not supported",
687            ));
688        }
689
690        let (endpoint, port_offset) = match datagram[3] {
691            0x01 if datagram.len() >= 10 => (
692                Socks5ReplyAddress::Socket(SocketAddr::new(
693                    IpAddr::V4(Ipv4Addr::new(
694                        datagram[4],
695                        datagram[5],
696                        datagram[6],
697                        datagram[7],
698                    )),
699                    u16::from_be_bytes([datagram[8], datagram[9]]),
700                )),
701                8,
702            ),
703            0x04 if datagram.len() >= 22 => {
704                let mut octets = [0u8; 16];
705                octets.copy_from_slice(&datagram[4..20]);
706                (
707                    Socks5ReplyAddress::Socket(SocketAddr::new(
708                        IpAddr::V6(Ipv6Addr::from(octets)),
709                        u16::from_be_bytes([datagram[20], datagram[21]]),
710                    )),
711                    20,
712                )
713            }
714            0x03 if datagram.len() >= 7 => {
715                let length = datagram[4] as usize;
716                let port_offset = 5 + length;
717                if length == 0 || datagram.len() < port_offset + 2 {
718                    return Err(Self::invalid_response("invalid SOCKS5 UDP domain address"));
719                }
720                let name = std::str::from_utf8(&datagram[5..port_offset])
721                    .map_err(|_| Self::invalid_response("non-UTF-8 SOCKS5 UDP domain address"))?
722                    .to_owned();
723                (
724                    Socks5ReplyAddress::Domain {
725                        name,
726                        port: u16::from_be_bytes([
727                            datagram[port_offset],
728                            datagram[port_offset + 1],
729                        ]),
730                    },
731                    port_offset,
732                )
733            }
734            _ => return Err(Self::invalid_response("invalid SOCKS5 UDP address")),
735        };
736        Ok((port_offset + 2, endpoint))
737    }
738
739    /// Converts a SOCKS5 reply code into a stable diagnostic.
740    fn reply_message(reply: u8) -> &'static str {
741        match reply {
742            0x01 => "general server failure",
743            0x02 => "connection not allowed by ruleset",
744            0x03 => "network unreachable",
745            0x04 => "host unreachable",
746            0x05 => "connection refused",
747            0x06 => "TTL expired",
748            0x07 => "command not supported",
749            0x08 => "address type not supported",
750            _ => "unknown error",
751        }
752    }
753
754    /// Builds an invalid-data error for malformed SOCKS5 responses.
755    fn invalid_response(message: impl Into<String>) -> io::Error {
756        io::Error::new(io::ErrorKind::InvalidData, message.into())
757    }
758}
759
760//--------------------------------------------------------------------------------------------------
761// Tests
762//--------------------------------------------------------------------------------------------------
763
764#[cfg(all(test, feature = "engine"))]
765mod tests {
766    use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
767    use std::sync::Arc;
768
769    use hickory_net::proto::op::{Message, MessageType, OpCode};
770    use hickory_net::proto::rr::rdata::{A, AAAA};
771    use hickory_net::proto::rr::{RData, Record, RecordType};
772    use hickory_net::proto::serialize::binary::{BinDecodable, BinEncodable};
773    use microsandbox_types::SecretSource;
774    use tokio::io::{AsyncReadExt, AsyncWriteExt};
775    use tokio::net::{TcpListener, UdpSocket};
776    use tokio::sync::watch;
777
778    use super::{
779        OutboundProxy, OutboundProxyBuildError, OutboundProxyBuilder, OutboundProxyConfig,
780        OutboundProxyProtocol, ResolvedOutboundProxy, ResolvedSocks5Credentials,
781    };
782    use crate::engine::dns::forwarder::DnsForwarder;
783    use crate::netstack::poll::GatewayIps;
784    use crate::netstack::shared::SharedState;
785
786    async fn responding_dns(relay_ipv6: Ipv6Addr, source_ipv4: Ipv4Addr) -> SocketAddr {
787        let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
788        let address = socket.local_addr().unwrap();
789        tokio::spawn(async move {
790            let mut buffer = [0u8; 4096];
791            loop {
792                let Ok((length, source)) = socket.recv_from(&mut buffer).await else {
793                    continue;
794                };
795                let Ok(query) = Message::from_bytes(&buffer[..length]) else {
796                    continue;
797                };
798                let mut response =
799                    Message::new(query.metadata.id, MessageType::Response, OpCode::Query);
800                response.metadata.recursion_desired = query.metadata.recursion_desired;
801                response.metadata.recursion_available = true;
802                if let Some(question) = query.queries.first() {
803                    response.add_query(question.clone());
804                    let domain = question.name().to_string();
805                    let answer = match (domain.trim_end_matches('.'), question.query_type()) {
806                        ("relay.example.com", RecordType::AAAA) => {
807                            Some(RData::AAAA(AAAA::from(relay_ipv6)))
808                        }
809                        ("source.example.com", RecordType::A) => {
810                            Some(RData::A(A::from(source_ipv4)))
811                        }
812                        _ => None,
813                    };
814                    if let Some(answer) = answer {
815                        response.add_answer(Record::from_rdata(
816                            question.name().clone(),
817                            60,
818                            answer,
819                        ));
820                    }
821                }
822                if let Ok(bytes) = response.to_bytes() {
823                    let _ = socket.send_to(&bytes, source).await;
824                }
825            }
826        });
827        address
828    }
829
830    #[test]
831    fn builder_creates_socks4_proxy_with_optional_user_id() {
832        let address = "127.0.0.1:1080".parse().unwrap();
833        let without_user_id = OutboundProxyBuilder::new()
834            .socks4("127.0.0.1:1080")
835            .build()
836            .unwrap();
837        let with_user_id = OutboundProxyBuilder::new()
838            .socks4("127.0.0.1:1080")
839            .user_id("sandbox")
840            .build()
841            .unwrap();
842
843        assert_eq!(
844            without_user_id,
845            OutboundProxy::Socks4 {
846                address,
847                user_id: None,
848            }
849        );
850        assert_eq!(
851            with_user_id,
852            OutboundProxy::Socks4 {
853                address,
854                user_id: Some("sandbox".to_string()),
855            }
856        );
857    }
858
859    #[test]
860    fn builder_creates_socks5_proxy() {
861        let proxy = OutboundProxyBuilder::new()
862            .socks5("127.0.0.1:1080")
863            .build()
864            .unwrap();
865
866        assert_eq!(
867            proxy,
868            OutboundProxy::Socks5 {
869                address: "127.0.0.1:1080".parse().unwrap(),
870                credentials: None,
871            }
872        );
873    }
874
875    #[test]
876    fn builder_creates_socks5_proxy_with_password_source() {
877        let proxy = OutboundProxyBuilder::new()
878            .socks5("127.0.0.1:1080")
879            .credentials(
880                "sandbox",
881                SecretSource::Env {
882                    var: "SOCKS5_PASSWORD".into(),
883                },
884            )
885            .build()
886            .unwrap();
887
888        let debug = format!("{proxy:?}");
889        assert!(debug.contains("sandbox"));
890        assert!(debug.contains("SOCKS5_PASSWORD"));
891
892        let json = serde_json::to_value(&proxy).unwrap();
893        assert_eq!(json["credentials"]["username"], "sandbox");
894        assert_eq!(json["credentials"]["password"]["kind"], "env");
895        assert_eq!(json["credentials"]["password"]["var"], "SOCKS5_PASSWORD");
896        assert!(json["credentials"].get("value").is_none());
897    }
898
899    #[test]
900    fn builder_rejects_invalid_socks5_credentials() {
901        for (username, password_env) in [
902            (String::new(), "SOCKS5_PASSWORD".to_string()),
903            ("username".to_string(), String::new()),
904            ("u".repeat(256), "SOCKS5_PASSWORD".to_string()),
905        ] {
906            assert!(
907                OutboundProxyBuilder::new()
908                    .socks5("127.0.0.1:1080")
909                    .credentials(username, SecretSource::Env { var: password_env })
910                    .build()
911                    .is_err()
912            );
913        }
914
915        assert!(
916            OutboundProxyBuilder::new()
917                .socks5("127.0.0.1:1080")
918                .credentials(
919                    "username",
920                    SecretSource::Store {
921                        reference: "production/socks5-password".into(),
922                    },
923                )
924                .build()
925                .is_err()
926        );
927    }
928
929    #[test]
930    fn resolved_proxy_build_validates_resolved_socks5_password() {
931        let proxy = OutboundProxyBuilder::new()
932            .socks5("127.0.0.1:1080")
933            .credentials(
934                "sandbox",
935                SecretSource::Env {
936                    var: "SOCKS5_PASSWORD".into(),
937                },
938            )
939            .build()
940            .unwrap();
941        assert!(
942            ResolvedOutboundProxy::build(
943                Some(&proxy),
944                Some(ResolvedSocks5Credentials::new("sandbox", "")),
945            )
946            .is_err()
947        );
948        assert!(
949            ResolvedOutboundProxy::build(
950                Some(&proxy),
951                Some(ResolvedSocks5Credentials::new("sandbox", "p".repeat(256),)),
952            )
953            .is_err()
954        );
955        ResolvedOutboundProxy::build(
956            Some(&proxy),
957            Some(ResolvedSocks5Credentials::new("sandbox", "password")),
958        )
959        .unwrap();
960    }
961
962    #[test]
963    fn resolved_proxy_build_requires_matching_resolved_credentials() {
964        let authenticated = OutboundProxyBuilder::new()
965            .socks5("127.0.0.1:1080")
966            .credentials("sandbox", SecretSource::env("SOCKS5_PASSWORD"))
967            .build()
968            .unwrap();
969        let unauthenticated = OutboundProxyBuilder::new()
970            .socks5("127.0.0.1:1080")
971            .build()
972            .unwrap();
973        let resolved = || ResolvedSocks5Credentials::new("sandbox", "password");
974
975        assert!(ResolvedOutboundProxy::build(Some(&authenticated), None).is_err());
976        assert!(ResolvedOutboundProxy::build(Some(&unauthenticated), Some(resolved())).is_err());
977        assert!(ResolvedOutboundProxy::build(None, Some(resolved())).is_err());
978        assert!(
979            ResolvedOutboundProxy::build(
980                Some(&authenticated),
981                Some(ResolvedSocks5Credentials::new("different-user", "password")),
982            )
983            .is_err()
984        );
985    }
986
987    #[test]
988    fn uri_parses_and_formats_for_cli() {
989        let http: OutboundProxy = "http://127.0.0.1:1080".parse().unwrap();
990        let socks4: OutboundProxy = "socks4://127.0.0.1:1080".parse().unwrap();
991        let socks5: OutboundProxy = "socks5://127.0.0.1:1080".parse().unwrap();
992
993        assert_eq!(
994            http,
995            OutboundProxy::HttpConnect {
996                address: "127.0.0.1:1080".parse().unwrap(),
997            }
998        );
999        assert_eq!(http.to_string(), "http://127.0.0.1:1080");
1000        assert_eq!(
1001            socks4,
1002            OutboundProxy::Socks4 {
1003                address: "127.0.0.1:1080".parse().unwrap(),
1004                user_id: None,
1005            }
1006        );
1007        assert_eq!(socks4.to_string(), "socks4://127.0.0.1:1080");
1008        assert_eq!(
1009            socks5,
1010            OutboundProxy::Socks5 {
1011                address: "127.0.0.1:1080".parse().unwrap(),
1012                credentials: None,
1013            }
1014        );
1015        assert_eq!(socks5.to_string(), "socks5://127.0.0.1:1080");
1016    }
1017
1018    #[test]
1019    fn uri_rejects_unsupported_forms() {
1020        for raw in [
1021            "127.0.0.1:1080",
1022            "ftp://127.0.0.1:1080",
1023            "socks4://user@127.0.0.1:1080",
1024            "socks5://user@127.0.0.1:1080",
1025            "socks5://127.0.0.1:1080/path",
1026            "socks5://127.0.0.1:1080?option=value",
1027            "socks5://127.0.0.1:1080#fragment",
1028        ] {
1029            assert!(raw.parse::<OutboundProxy>().is_err(), "accepted {raw:?}");
1030        }
1031    }
1032
1033    #[test]
1034    fn builder_rejects_invalid_socks4_user_ids() {
1035        for user_id in [String::new(), "a\0b".to_string(), "a".repeat(256)] {
1036            assert!(
1037                OutboundProxyBuilder::new()
1038                    .socks4("127.0.0.1:1080")
1039                    .user_id(user_id)
1040                    .build()
1041                    .is_err()
1042            );
1043        }
1044    }
1045
1046    #[test]
1047    fn outbound_proxy_rejects_invalid_socks4_user_ids() {
1048        for user_id in [String::new(), "a\0b".to_string(), "a".repeat(256)] {
1049            let proxy = OutboundProxy::Socks4 {
1050                address: "127.0.0.1:1080".parse().unwrap(),
1051                user_id: Some(user_id),
1052            };
1053
1054            assert!(proxy.build().is_err());
1055        }
1056    }
1057
1058    #[test]
1059    fn invalid_address_error_uses_typed_protocol() {
1060        let error = OutboundProxyBuilder::new()
1061            .socks5("not-an-address")
1062            .build()
1063            .unwrap_err();
1064
1065        assert!(matches!(
1066            error,
1067            OutboundProxyBuildError::InvalidAddress {
1068                protocol: OutboundProxyProtocol::Socks5,
1069                ..
1070            }
1071        ));
1072    }
1073
1074    #[tokio::test]
1075    async fn connects_through_socks4_proxy_with_user_id() {
1076        let target: SocketAddr = "93.184.216.34:443".parse().unwrap();
1077        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1078        let proxy_addr = proxy_listener.local_addr().unwrap();
1079        let proxy_task = tokio::spawn(async move {
1080            let (mut client, _) = proxy_listener.accept().await.unwrap();
1081
1082            let mut request = [0u8; 16];
1083            client.read_exact(&mut request).await.unwrap();
1084            assert_eq!(request[0], 0x04, "SOCKS version");
1085            assert_eq!(request[1], 0x01, "CONNECT command");
1086            assert_eq!(u16::from_be_bytes([request[2], request[3]]), 443);
1087            assert_eq!(&request[4..8], &[93, 184, 216, 34]);
1088            assert_eq!(&request[8..], b"sandbox\0");
1089
1090            client
1091                .write_all(&[0x00, 0x5a, 0x01, 0xbb, 93, 184, 216, 34])
1092                .await
1093                .unwrap();
1094
1095            let mut buf = [0u8; 5];
1096            client.read_exact(&mut buf).await.unwrap();
1097            client.write_all(&buf).await.unwrap();
1098        });
1099
1100        let mut stream = ResolvedOutboundProxy::Socks4 {
1101            address: proxy_addr,
1102            user_id: Some("sandbox".to_string()),
1103        }
1104        .connect(target)
1105        .await
1106        .unwrap();
1107        stream.write_all(b"hello").await.unwrap();
1108        let mut echoed = [0u8; 5];
1109        stream.read_exact(&mut echoed).await.unwrap();
1110        assert_eq!(&echoed, b"hello");
1111
1112        proxy_task.await.unwrap();
1113    }
1114
1115    #[tokio::test]
1116    async fn connects_through_socks5_proxy() {
1117        let target: SocketAddr = "93.184.216.34:443".parse().unwrap();
1118        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1119        let proxy_addr = proxy_listener.local_addr().unwrap();
1120        let proxy_task = tokio::spawn(async move {
1121            let (mut client, _) = proxy_listener.accept().await.unwrap();
1122
1123            let mut greeting = [0u8; 3];
1124            client.read_exact(&mut greeting).await.unwrap();
1125            assert_eq!(greeting, [0x05, 0x01, 0x00]);
1126            client.write_all(&[0x05, 0x00]).await.unwrap();
1127
1128            let mut request = [0u8; 10];
1129            client.read_exact(&mut request).await.unwrap();
1130            assert_eq!(request[0], 0x05, "SOCKS version");
1131            assert_eq!(request[1], 0x01, "CONNECT command");
1132            assert_eq!(request[3], 0x01, "IPv4 address type");
1133            assert_eq!(&request[4..8], &[93, 184, 216, 34]);
1134            assert_eq!(u16::from_be_bytes([request[8], request[9]]), 443);
1135
1136            client
1137                .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1138                .await
1139                .unwrap();
1140
1141            let mut buf = [0u8; 5];
1142            client.read_exact(&mut buf).await.unwrap();
1143            client.write_all(&buf).await.unwrap();
1144        });
1145
1146        let mut stream = ResolvedOutboundProxy::Socks5 {
1147            address: proxy_addr,
1148            credentials: None,
1149        }
1150        .connect(target)
1151        .await
1152        .unwrap();
1153        stream.write_all(b"hello").await.unwrap();
1154        let mut echoed = [0u8; 5];
1155        stream.read_exact(&mut echoed).await.unwrap();
1156        assert_eq!(&echoed, b"hello");
1157
1158        proxy_task.await.unwrap();
1159    }
1160
1161    #[tokio::test]
1162    async fn connect_does_not_resolve_domain_from_socks5_reply() {
1163        let target: SocketAddr = "93.184.216.34:443".parse().unwrap();
1164        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1165        let proxy_addr = proxy_listener.local_addr().unwrap();
1166        let proxy_task = tokio::spawn(async move {
1167            let (mut client, _) = proxy_listener.accept().await.unwrap();
1168
1169            let mut greeting = [0u8; 3];
1170            client.read_exact(&mut greeting).await.unwrap();
1171            client.write_all(&[0x05, 0x00]).await.unwrap();
1172
1173            let mut request = [0u8; 10];
1174            client.read_exact(&mut request).await.unwrap();
1175            let domain = b"does-not-resolve.invalid";
1176            let mut response = vec![0x05, 0x00, 0x00, 0x03, domain.len() as u8];
1177            response.extend_from_slice(domain);
1178            response.extend_from_slice(&443u16.to_be_bytes());
1179            client.write_all(&response).await.unwrap();
1180
1181            let mut buf = [0u8; 5];
1182            client.read_exact(&mut buf).await.unwrap();
1183            client.write_all(&buf).await.unwrap();
1184        });
1185
1186        let mut stream = ResolvedOutboundProxy::Socks5 {
1187            address: proxy_addr,
1188            credentials: None,
1189        }
1190        .connect(target)
1191        .await
1192        .unwrap();
1193        stream.write_all(b"hello").await.unwrap();
1194        let mut echoed = [0u8; 5];
1195        stream.read_exact(&mut echoed).await.unwrap();
1196        assert_eq!(&echoed, b"hello");
1197
1198        proxy_task.await.unwrap();
1199    }
1200
1201    #[tokio::test]
1202    async fn connects_through_authenticated_socks5_proxy() {
1203        let target: SocketAddr = "93.184.216.34:443".parse().unwrap();
1204        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1205        let proxy_addr = proxy_listener.local_addr().unwrap();
1206        let proxy_task = tokio::spawn(async move {
1207            let (mut client, _) = proxy_listener.accept().await.unwrap();
1208
1209            let mut greeting = [0u8; 4];
1210            client.read_exact(&mut greeting).await.unwrap();
1211            assert_eq!(greeting, [0x05, 0x02, 0x00, 0x02]);
1212            client.write_all(&[0x05, 0x02]).await.unwrap();
1213
1214            let mut auth = [0u8; 18];
1215            client.read_exact(&mut auth).await.unwrap();
1216            assert_eq!(&auth, b"\x01\x07sandbox\x08password");
1217            client.write_all(&[0x01, 0x00]).await.unwrap();
1218
1219            let mut request = [0u8; 10];
1220            client.read_exact(&mut request).await.unwrap();
1221            assert_eq!(request[1], 0x01, "CONNECT command");
1222            client
1223                .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1224                .await
1225                .unwrap();
1226        });
1227
1228        let configured = OutboundProxyBuilder::new()
1229            .socks5(proxy_addr.to_string())
1230            .credentials(
1231                "sandbox",
1232                SecretSource::Env {
1233                    var: "SOCKS5_PASSWORD".into(),
1234                },
1235            )
1236            .build()
1237            .unwrap();
1238        let proxy = ResolvedOutboundProxy::build(
1239            Some(&configured),
1240            Some(ResolvedSocks5Credentials::new("sandbox", "password")),
1241        )
1242        .unwrap()
1243        .unwrap();
1244        proxy.connect(target).await.unwrap();
1245
1246        proxy_task.await.unwrap();
1247    }
1248
1249    #[tokio::test]
1250    async fn associates_and_relays_socks5_udp() {
1251        let target: SocketAddr = "93.184.216.34:5353".parse().unwrap();
1252        let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1253        let relay_addr = relay.local_addr().unwrap();
1254        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1255        let proxy_addr = proxy_listener.local_addr().unwrap();
1256
1257        let proxy_task = tokio::spawn(async move {
1258            let (mut control, _) = proxy_listener.accept().await.unwrap();
1259            let mut greeting = [0u8; 3];
1260            control.read_exact(&mut greeting).await.unwrap();
1261            assert_eq!(greeting, [0x05, 0x01, 0x00]);
1262            control.write_all(&[0x05, 0x00]).await.unwrap();
1263
1264            let mut request = [0u8; 10];
1265            control.read_exact(&mut request).await.unwrap();
1266            assert_eq!(request[1], 0x03, "UDP ASSOCIATE command");
1267
1268            let mut response = vec![0x05, 0x00, 0x00, 0x01];
1269            let SocketAddr::V4(relay_addr) = relay_addr else {
1270                unreachable!()
1271            };
1272            response.extend_from_slice(&relay_addr.ip().octets());
1273            response.extend_from_slice(&relay_addr.port().to_be_bytes());
1274            control.write_all(&response).await.unwrap();
1275
1276            let mut datagram = [0u8; 64];
1277            let (received, client) = relay.recv_from(&mut datagram).await.unwrap();
1278            assert_eq!(&datagram[..10], &[0, 0, 0, 1, 93, 184, 216, 34, 0x14, 0xe9]);
1279            assert_eq!(&datagram[10..received], b"hello");
1280            relay.send_to(&datagram[..received], client).await.unwrap();
1281        });
1282
1283        let configured = OutboundProxyBuilder::new()
1284            .socks5(proxy_addr.to_string())
1285            .build()
1286            .unwrap();
1287        let proxy = ResolvedOutboundProxy::build(Some(&configured), None)
1288            .unwrap()
1289            .unwrap();
1290        let association = proxy.associate_udp(None).await.unwrap();
1291        association.send_to(b"hello", target).await.unwrap();
1292        let mut response = [0u8; 64];
1293        let (received, sources) = association.recv_from(&mut response).await.unwrap();
1294        assert_eq!(sources, vec![target]);
1295        assert_eq!(&response[..received], b"hello");
1296
1297        proxy_task.await.unwrap();
1298    }
1299
1300    #[tokio::test]
1301    async fn udp_association_supports_domain_relay_in_another_address_family() {
1302        let target: SocketAddr = "93.184.216.34:5353".parse().unwrap();
1303        let relay = UdpSocket::bind("[::1]:0").await.unwrap();
1304        let relay_addr = relay.local_addr().unwrap();
1305        let dns_upstream =
1306            responding_dns(Ipv6Addr::LOCALHOST, Ipv4Addr::new(93, 184, 216, 34)).await;
1307        let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1308        let proxy_addr = proxy_listener.local_addr().unwrap();
1309        let proxy_task = tokio::spawn(async move {
1310            let (mut control, _) = proxy_listener.accept().await.unwrap();
1311
1312            let mut greeting = [0u8; 3];
1313            control.read_exact(&mut greeting).await.unwrap();
1314            control.write_all(&[0x05, 0x00]).await.unwrap();
1315
1316            let mut request = [0u8; 10];
1317            control.read_exact(&mut request).await.unwrap();
1318            assert_eq!(request[1], 0x03, "UDP ASSOCIATE command");
1319
1320            let domain = b"relay.example.com";
1321            let mut response = vec![0x05, 0x00, 0x00, 0x03, domain.len() as u8];
1322            response.extend_from_slice(domain);
1323            response.extend_from_slice(&relay_addr.port().to_be_bytes());
1324            control.write_all(&response).await.unwrap();
1325
1326            let mut datagram = [0u8; 64];
1327            let (_, client) = relay.recv_from(&mut datagram).await.unwrap();
1328            let source_domain = b"source.example.com";
1329            let mut response = vec![0x00, 0x00, 0x00, 0x03, source_domain.len() as u8];
1330            response.extend_from_slice(source_domain);
1331            response.extend_from_slice(&target.port().to_be_bytes());
1332            response.extend_from_slice(b"hello");
1333            relay.send_to(&response, client).await.unwrap();
1334        });
1335
1336        let proxy = ResolvedOutboundProxy::Socks5 {
1337            address: proxy_addr,
1338            credentials: None,
1339        };
1340        let forwarder = DnsForwarder::for_proxy_test(
1341            Arc::new(SharedState::new(4)),
1342            GatewayIps {
1343                ipv4: Some("127.0.0.1".parse().unwrap()),
1344                ipv6: None,
1345            },
1346            Some(dns_upstream),
1347        )
1348        .await;
1349        let (_dns_tx, dns_forwarder) = watch::channel(Some(forwarder));
1350        let association = proxy.associate_udp(Some(dns_forwarder)).await.unwrap();
1351        association.send_to(b"hello", target).await.unwrap();
1352        let mut response = [0u8; 64];
1353        let (received, sources) = association.recv_from(&mut response).await.unwrap();
1354        assert_eq!(sources, vec![target]);
1355        assert_eq!(&response[..received], b"hello");
1356
1357        proxy_task.await.unwrap();
1358    }
1359}