1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Socks5Credentials {
35 username: String,
36 password: SecretSource,
37}
38
39#[doc(hidden)]
41#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ResolvedSocks5Credentials {
43 username: String,
44 password: Zeroizing<String>,
45}
46
47#[derive(Debug, Clone)]
49pub struct Socks4ProxyBuilder {
50 address: String,
51 user_id: Option<String>,
52}
53
54#[derive(Debug, Clone)]
56pub struct Socks5ProxyBuilder {
57 address: String,
58 credentials: Option<Socks5Credentials>,
59}
60
61#[cfg(feature = "engine")]
63pub(crate) struct Socks5UdpAssociation {
64 _control: TcpStream,
65 socket: tokio::net::UdpSocket,
66 dns_forwarder: Option<DnsForwarderHandle>,
67}
68
69#[cfg(feature = "engine")]
71struct Socks5Protocol;
72
73#[cfg(feature = "engine")]
75enum Socks5ReplyAddress {
76 Socket(SocketAddr),
77 Domain { name: String, port: u16 },
78}
79
80impl ResolvedOutboundProxy {
85 #[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 #[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 let _ = Socks5Protocol::command(&mut stream, 0x01, destination).await?;
179 Ok(stream)
180 }
181 }
182 }
183
184 #[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 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 #[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 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 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 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 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 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 pub fn new() -> Self {
406 Self
407 }
408
409 pub fn http_connect(self, address: impl Into<String>) -> super::HttpConnectProxyBuilder {
411 super::HttpConnectProxyBuilder::new(address)
412 }
413
414 pub fn socks4(self, address: impl Into<String>) -> Socks4ProxyBuilder {
416 Socks4ProxyBuilder {
417 address: address.into(),
418 user_id: None,
419 }
420 }
421
422 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 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 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
450impl 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 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 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 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 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 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 fn address_len(address: SocketAddr) -> usize {
673 match address {
674 SocketAddr::V4(_) => 7,
675 SocketAddr::V6(_) => 19,
676 }
677 }
678
679 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 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 fn invalid_response(message: impl Into<String>) -> io::Error {
756 io::Error::new(io::ErrorKind::InvalidData, message.into())
757 }
758}
759
760#[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}