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
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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Socks5Credentials {
33 username: String,
34 password: SecretSource,
35}
36
37#[doc(hidden)]
39#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ResolvedSocks5Credentials {
41 username: String,
42 password: Zeroizing<String>,
43}
44
45#[derive(Debug, Clone)]
47pub struct Socks4ProxyBuilder {
48 address: String,
49 user_id: Option<String>,
50}
51
52#[derive(Debug, Clone)]
54pub struct Socks5ProxyBuilder {
55 address: String,
56 credentials: Option<Socks5Credentials>,
57}
58
59#[cfg(feature = "engine")]
61pub(crate) struct Socks5UdpAssociation {
62 _control: TcpStream,
63 socket: tokio::net::UdpSocket,
64 dns_forwarder: Option<DnsForwarderHandle>,
65}
66
67#[cfg(feature = "engine")]
69struct Socks5Protocol;
70
71#[cfg(feature = "engine")]
73enum Socks5ReplyAddress {
74 Socket(SocketAddr),
75 Domain { name: String, port: u16 },
76}
77
78impl ResolvedOutboundProxy {
83 #[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 #[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 let _ = Socks5Protocol::command(&mut stream, 0x01, destination).await?;
166 Ok(stream)
167 }
168 }
169 }
170
171 #[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 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 #[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 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 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 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 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 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 pub fn new() -> Self {
392 Self
393 }
394
395 pub fn socks4(self, address: impl Into<String>) -> Socks4ProxyBuilder {
397 Socks4ProxyBuilder {
398 address: address.into(),
399 user_id: None,
400 }
401 }
402
403 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 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 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
431impl 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 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 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 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 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 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 fn address_len(address: SocketAddr) -> usize {
654 match address {
655 SocketAddr::V4(_) => 7,
656 SocketAddr::V6(_) => 19,
657 }
658 }
659
660 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 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 fn invalid_response(message: impl Into<String>) -> io::Error {
737 io::Error::new(io::ErrorKind::InvalidData, message.into())
738 }
739}
740
741#[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}