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