1use derive_deftly::Deftly;
8use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
9use serde::Deserialize;
10use std::net::{IpAddr, SocketAddr};
11use tor_config::PaddingLevel;
12use tor_config::derive::prelude::*;
13use tor_socksproto::SocksAuth;
14use tor_socksproto::SocksVersion;
15use url::{Host, Url};
16
17#[derive(Debug, Clone, thiserror::Error)]
19#[non_exhaustive]
20pub enum ProxyProtocolParseError {
21 #[error("unsupported or missing proxy scheme: {0}")]
23 UnsupportedScheme(String),
24 #[error("password not supported for proxy scheme: {0}")]
26 UnsupportedPassword(String),
27 #[error("invalid proxy address: {0}")]
29 InvalidAddress(String),
30 #[error("missing or invalid port")]
32 InvalidPort,
33 #[error("invalid proxy URI format: {0}")]
35 InvalidFormat(String),
36}
37
38#[derive(Debug, Clone, Eq, PartialEq)]
44pub struct HttpConnectAuth {
45 pub username: String,
47 pub password: Option<String>,
49}
50
51#[derive(
80 Debug, Clone, Eq, PartialEq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
81)]
82#[non_exhaustive]
83pub enum ProxyProtocol {
84 Socks {
86 version: SocksVersion,
88 auth: SocksAuth,
90 addr: SocketAddr,
92 },
93 HttpConnect {
95 addr: SocketAddr,
97 credentials: Option<HttpConnectAuth>,
99 },
100}
101
102impl std::str::FromStr for ProxyProtocol {
103 type Err = ProxyProtocolParseError;
104
105 fn from_str(s: &str) -> Result<Self, Self::Err> {
106 let url = Url::parse(s).map_err(|e| match e {
107 url::ParseError::InvalidPort => ProxyProtocolParseError::InvalidPort,
108 url::ParseError::InvalidIpv4Address
109 | url::ParseError::InvalidIpv6Address
110 | url::ParseError::EmptyHost
111 | url::ParseError::InvalidDomainCharacter
112 | url::ParseError::IdnaError => ProxyProtocolParseError::InvalidAddress(s.to_string()),
113 _ => ProxyProtocolParseError::InvalidFormat(s.to_string()),
114 })?;
115
116 let scheme_lower = url.scheme().to_ascii_lowercase();
117
118 if url.query().is_some() || url.fragment().is_some() {
119 return Err(ProxyProtocolParseError::InvalidFormat(s.to_string()));
120 }
121
122 let path = url.path();
123 if !path.is_empty() && path != "/" {
124 return Err(ProxyProtocolParseError::InvalidFormat(s.to_string()));
125 }
126
127 let port = url.port().ok_or(ProxyProtocolParseError::InvalidPort)?;
128 let host = url
129 .host()
130 .ok_or_else(|| ProxyProtocolParseError::InvalidAddress(s.to_string()))?;
131 let ip = match host {
132 Host::Ipv4(ip) => IpAddr::V4(ip),
133 Host::Ipv6(ip) => IpAddr::V6(ip),
134 Host::Domain(domain) => domain
135 .parse::<IpAddr>()
136 .map_err(|_| ProxyProtocolParseError::InvalidAddress(domain.to_string()))?,
137 };
138 let addr = SocketAddr::new(ip, port);
139
140 match scheme_lower.as_str() {
141 "http" => {
142 let user = url.username();
144 let pass = url.password();
145 if user.is_empty() && pass.is_some() {
147 return Err(ProxyProtocolParseError::InvalidFormat(
148 "password without username not supported".to_string(),
149 ));
150 }
151 let credentials = if user.is_empty() {
152 None
153 } else {
154 let username = percent_decode_str(user)
155 .decode_utf8()
156 .map_err(|_| {
157 ProxyProtocolParseError::InvalidFormat(
158 "invalid UTF-8 in username".to_string(),
159 )
160 })?
161 .into_owned();
162 let password = pass
163 .map(|p| {
164 percent_decode_str(p).decode_utf8().map_err(|_| {
165 ProxyProtocolParseError::InvalidFormat(
166 "invalid UTF-8 in password".to_string(),
167 )
168 })
169 })
170 .transpose()?
171 .map(|s| s.into_owned());
172 Some(HttpConnectAuth { username, password })
173 };
174 Ok(ProxyProtocol::HttpConnect { addr, credentials })
175 }
176 "socks4" | "socks4a" | "socks5" | "socks5h" => {
177 let version = match scheme_lower.as_str() {
178 "socks4" | "socks4a" => SocksVersion::V4,
179 "socks5" | "socks5h" => SocksVersion::V5,
180 _ => unreachable!(),
181 };
182 let user = url.username();
184 let pass = url.password();
185 if version == SocksVersion::V4 && pass.is_some() {
186 return Err(ProxyProtocolParseError::UnsupportedPassword(
187 url.scheme().to_string(),
188 ));
189 }
190 let user_decoded = percent_decode_str(user).decode_utf8().map_err(|_| {
191 ProxyProtocolParseError::InvalidFormat("invalid UTF-8 in username".to_string())
192 })?;
193 let pass_decoded = pass
194 .map(|p| {
195 percent_decode_str(p).decode_utf8().map_err(|_| {
196 ProxyProtocolParseError::InvalidFormat(
197 "invalid UTF-8 in password".to_string(),
198 )
199 })
200 })
201 .transpose()?;
202 let auth = if user.is_empty() && pass.is_none() {
203 SocksAuth::NoAuth
204 } else {
205 match version {
206 SocksVersion::V4 => SocksAuth::Socks4(user_decoded.as_bytes().to_vec()),
207 SocksVersion::V5 => {
208 let pass = pass_decoded.as_deref().unwrap_or("");
209 SocksAuth::Username(
210 user_decoded.as_bytes().to_vec(),
211 pass.as_bytes().to_vec(),
212 )
213 }
214 _ => SocksAuth::NoAuth,
215 }
216 };
217 Ok(ProxyProtocol::Socks {
218 version,
219 auth,
220 addr,
221 })
222 }
223 _ => Err(ProxyProtocolParseError::UnsupportedScheme(
224 url.scheme().to_string(),
225 )),
226 }
227 }
228}
229
230impl std::fmt::Display for ProxyProtocol {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match self {
233 ProxyProtocol::Socks {
234 version,
235 auth,
236 addr,
237 } => {
238 match auth {
240 SocksAuth::NoAuth => write!(f, "{}://{}", version, addr),
241 SocksAuth::Socks4(user_id) => {
242 let user = String::from_utf8_lossy(user_id);
244 match encode_userinfo(*version, *addr, &user, None) {
245 Some((user_encoded, _)) => {
246 write!(f, "{}://{}@{}", version, user_encoded, addr)
247 }
248 None => write!(f, "{}://{}@{}", version, user, addr),
249 }
250 }
251 SocksAuth::Username(user, pass) => {
252 let user = String::from_utf8_lossy(user);
254 let pass = String::from_utf8_lossy(pass);
255 match encode_userinfo(*version, *addr, &user, Some(&pass)) {
256 Some((user_encoded, pass_encoded)) => {
257 let pass_encoded = pass_encoded.unwrap_or_default();
258 write!(
259 f,
260 "{}://{}:{}@{}",
261 version, user_encoded, pass_encoded, addr
262 )
263 }
264 None => write!(f, "{}://{}:{}@{}", version, user, pass, addr),
265 }
266 }
267 _ => write!(f, "{}://{}", version, addr),
269 }
270 }
271 ProxyProtocol::HttpConnect { addr, credentials } => {
272 if let Some(auth) = credentials {
273 let (user_encoded, pass_encoded) =
276 encode_userinfo_http(*addr, &auth.username, auth.password.as_deref())
277 .unwrap_or_else(|| {
278 debug_assert!(
280 false,
281 "encode_userinfo_http failed for addr={}, user={}",
282 addr, auth.username
283 );
284 let encoded_user = percent_encode_userinfo(&auth.username);
285 let encoded_pass =
286 auth.password.as_ref().map(|p| percent_encode_userinfo(p));
287 (encoded_user, encoded_pass)
288 });
289 if let Some(p) = pass_encoded {
290 write!(f, "http://{}:{}@{}", user_encoded, p, addr)
291 } else {
292 write!(f, "http://{}@{}", user_encoded, addr)
293 }
294 } else {
295 write!(f, "http://{}", addr)
296 }
297 }
298 }
299 }
300}
301
302impl ProxyProtocol {
303 pub fn is_loopback(&self) -> bool {
305 let addr = match self {
306 ProxyProtocol::Socks { addr, .. } => addr,
307 ProxyProtocol::HttpConnect { addr, .. } => addr,
308 };
309 addr.ip().is_loopback()
310 }
311}
312
313const USERINFO_ENCODE_SET: &AsciiSet = &CONTROLS
317 .add(b' ')
318 .add(b':')
319 .add(b'@')
320 .add(b'/')
321 .add(b'?')
322 .add(b'#')
323 .add(b'[')
324 .add(b']');
325
326fn percent_encode_userinfo(s: &str) -> String {
328 utf8_percent_encode(s, USERINFO_ENCODE_SET).to_string()
329}
330
331fn encode_userinfo_with_scheme(
336 scheme: &str,
337 addr: SocketAddr,
338 username: &str,
339 password: Option<&str>,
340) -> Option<(String, Option<String>)> {
341 let url_str = format!("{}://{}", scheme, addr);
342 let mut url = Url::parse(&url_str).ok()?;
343 if url.set_username(username).is_err() {
344 return None;
345 }
346 if url.set_password(password).is_err() {
347 return None;
348 }
349 let user_encoded = url.username().to_string();
350 let pass_encoded = url.password().map(str::to_string);
351 Some((user_encoded, pass_encoded))
352}
353
354fn encode_userinfo_http(
356 addr: SocketAddr,
357 username: &str,
358 password: Option<&str>,
359) -> Option<(String, Option<String>)> {
360 encode_userinfo_with_scheme("http", addr, username, password)
361}
362
363fn encode_userinfo(
368 version: SocksVersion,
369 addr: SocketAddr,
370 username: &str,
371 password: Option<&str>,
372) -> Option<(String, Option<String>)> {
373 encode_userinfo_with_scheme(&version.to_string(), addr, username, password)
374}
375
376impl ProxyProtocol {
377 pub fn socks_no_auth(version: SocksVersion, addr: SocketAddr) -> Self {
379 ProxyProtocol::Socks {
380 version,
381 auth: SocksAuth::NoAuth,
382 addr,
383 }
384 }
385}
386
387#[allow(clippy::option_option)]
389fn deserialize_outbound_proxy<'de, D>(
390 deserializer: D,
391) -> Result<Option<Option<ProxyProtocol>>, D::Error>
392where
393 D: serde::Deserializer<'de>,
394{
395 let value = Option::<String>::deserialize(deserializer)?;
396 match value {
397 None => Ok(None),
398 Some(s) => {
399 if s.trim().is_empty() {
400 return Ok(Some(None));
401 }
402 let parsed = s.parse().map_err(serde::de::Error::custom)?;
403 Ok(Some(Some(parsed)))
404 }
405 }
406}
407
408#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
413#[derive_deftly(TorConfig)]
414pub struct ChannelConfig {
415 #[deftly(tor_config(default))]
417 pub(crate) padding: PaddingLevel,
418
419 #[deftly(tor_config(
421 default,
422 serde = r#" deserialize_with = "deserialize_outbound_proxy" "#
423 ))]
424 pub(crate) outbound_proxy: Option<ProxyProtocol>,
425}
426
427impl ChannelConfig {
428 pub fn outbound_proxy(&self) -> Option<&ProxyProtocol> {
430 self.outbound_proxy.as_ref()
431 }
432}
433
434#[cfg(feature = "testing")]
435impl ChannelConfig {
436 pub fn padding(&self) -> PaddingLevel {
438 self.padding
439 }
440}
441
442#[cfg(test)]
443mod test {
444 #![allow(clippy::bool_assert_comparison)]
446 #![allow(clippy::clone_on_copy)]
447 #![allow(clippy::dbg_macro)]
448 #![allow(clippy::mixed_attributes_style)]
449 #![allow(clippy::print_stderr)]
450 #![allow(clippy::print_stdout)]
451 #![allow(clippy::single_char_pattern)]
452 #![allow(clippy::unwrap_used)]
453 #![allow(clippy::unchecked_time_subtraction)]
454 #![allow(clippy::useless_vec)]
455 #![allow(clippy::needless_pass_by_value)]
456 #![allow(clippy::string_slice)] use super::*;
459
460 #[test]
461 fn channel_config() {
462 let config = ChannelConfig::default();
463
464 assert_eq!(PaddingLevel::Normal, config.padding);
465 }
466
467 #[test]
468 fn proxy_protocol_parse_socks5_basic() {
469 let p: ProxyProtocol = "socks5://127.0.0.1:1080".parse().unwrap();
470 match p {
471 ProxyProtocol::Socks {
472 version,
473 auth,
474 addr,
475 } => {
476 assert_eq!(version, SocksVersion::V5);
477 assert_eq!(auth, SocksAuth::NoAuth);
478 assert_eq!(addr, "127.0.0.1:1080".parse().unwrap());
479 }
480 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
481 }
482 }
483
484 #[test]
485 fn proxy_protocol_parse_socks5_with_auth() {
486 let p: ProxyProtocol = "socks5://myuser:mypass@192.168.1.1:9050".parse().unwrap();
487 match p {
488 ProxyProtocol::Socks {
489 version,
490 auth,
491 addr,
492 } => {
493 assert_eq!(version, SocksVersion::V5);
494 assert_eq!(
495 auth,
496 SocksAuth::Username(b"myuser".to_vec(), b"mypass".to_vec())
497 );
498 assert_eq!(addr, "192.168.1.1:9050".parse().unwrap());
499 }
500 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
501 }
502 }
503
504 #[test]
505 fn proxy_protocol_parse_socks4() {
506 let p: ProxyProtocol = "socks4://10.0.0.1:1080".parse().unwrap();
507 match p {
508 ProxyProtocol::Socks {
509 version,
510 auth,
511 addr,
512 } => {
513 assert_eq!(version, SocksVersion::V4);
514 assert_eq!(auth, SocksAuth::NoAuth);
515 assert_eq!(addr, "10.0.0.1:1080".parse().unwrap());
516 }
517 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
518 }
519 }
520
521 #[test]
522 fn proxy_protocol_parse_socks4a() {
523 let p: ProxyProtocol = "socks4a://10.0.0.1:1080".parse().unwrap();
524 match p {
525 ProxyProtocol::Socks { version, auth, .. } => {
526 assert_eq!(version, SocksVersion::V4);
527 assert_eq!(auth, SocksAuth::NoAuth);
528 }
529 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
530 }
531 }
532
533 #[test]
534 fn proxy_protocol_parse_ipv6() {
535 let p: ProxyProtocol = "socks5://[::1]:1080".parse().unwrap();
536 match p {
537 ProxyProtocol::Socks { addr, .. } => {
538 assert_eq!(addr, "[::1]:1080".parse().unwrap());
539 }
540 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
541 }
542 }
543
544 #[test]
545 fn proxy_protocol_display_roundtrip() {
546 for uri in [
547 "socks5://127.0.0.1:1080",
548 "socks4://10.0.0.1:9050",
549 "socks5://user:pass@192.168.1.1:1080",
550 "socks5://[::1]:1080",
551 "http://127.0.0.1:8080",
552 "http://user:pass@192.168.1.1:3128",
553 ] {
554 let p: ProxyProtocol = uri.parse().unwrap();
555 let s = p.to_string();
556 let p2: ProxyProtocol = s.parse().unwrap();
557 assert_eq!(p, p2, "Round-trip failed for: {}", uri);
558 }
559 }
560
561 #[test]
562 fn proxy_protocol_parse_errors() {
563 assert!("127.0.0.1:1080".parse::<ProxyProtocol>().is_err());
565
566 assert!("invalid://127.0.0.1:1080".parse::<ProxyProtocol>().is_err());
568
569 assert!("socks5://127.0.0.1".parse::<ProxyProtocol>().is_err());
571
572 assert!("socks5://not-an-ip:1080".parse::<ProxyProtocol>().is_err());
574
575 assert!(
577 "socks4://user:pass@10.0.0.1:1080"
578 .parse::<ProxyProtocol>()
579 .is_err()
580 );
581 }
582
583 #[test]
584 fn proxy_protocol_case_insensitive() {
585 let p1: ProxyProtocol = "SOCKS5://127.0.0.1:1080".parse().unwrap();
587 let p2: ProxyProtocol = "socks5://127.0.0.1:1080".parse().unwrap();
588 let p3: ProxyProtocol = "SoCkS5://127.0.0.1:1080".parse().unwrap();
589
590 assert_eq!(p1, p2);
591 assert_eq!(p2, p3);
592 }
593
594 #[test]
595 fn proxy_protocol_parse_socks5h() {
596 let p: ProxyProtocol = "socks5h://127.0.0.1:1080".parse().unwrap();
598 match p {
599 ProxyProtocol::Socks { version, auth, .. } => {
600 assert_eq!(version, SocksVersion::V5);
601 assert_eq!(auth, SocksAuth::NoAuth);
602 }
603 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
604 }
605 }
606
607 #[test]
608 fn proxy_protocol_parse_socks4_user_only() {
609 let p: ProxyProtocol = "socks4://myuser@10.0.0.1:1080".parse().unwrap();
611 match p {
612 ProxyProtocol::Socks {
613 version,
614 auth,
615 addr,
616 } => {
617 assert_eq!(version, SocksVersion::V4);
618 assert_eq!(auth, SocksAuth::Socks4(b"myuser".to_vec()));
619 assert_eq!(addr, "10.0.0.1:1080".parse().unwrap());
620 }
621 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
622 }
623 }
624
625 #[test]
626 fn proxy_protocol_parse_socks5_user_only() {
627 let p: ProxyProtocol = "socks5://myuser@192.168.1.1:9050".parse().unwrap();
629 match p {
630 ProxyProtocol::Socks {
631 version,
632 auth,
633 addr,
634 } => {
635 assert_eq!(version, SocksVersion::V5);
636 assert_eq!(auth, SocksAuth::Username(b"myuser".to_vec(), b"".to_vec()));
637 assert_eq!(addr, "192.168.1.1:9050".parse().unwrap());
638 }
639 ProxyProtocol::HttpConnect { .. } => panic!("expected Socks"),
640 }
641 }
642
643 #[test]
644 fn proxy_protocol_percent_encoding_roundtrip() {
645 let p = ProxyProtocol::Socks {
648 version: SocksVersion::V5,
649 auth: SocksAuth::Username(b"user@domain".to_vec(), b"pass:word".to_vec()),
650 addr: "127.0.0.1:1080".parse().unwrap(),
651 };
652 let s = p.to_string();
653 assert!(s.contains("%40"), "@ should be encoded as %40");
655 assert!(
656 s.contains("%3A") || s.contains("%3a"),
657 ": in password should be encoded"
658 );
659
660 let p2: ProxyProtocol = s.parse().unwrap();
662 assert_eq!(p, p2, "Round-trip failed for percent-encoded URI");
663 }
664
665 #[test]
666 fn proxy_protocol_socks4_user_roundtrip() {
667 let uri = "socks4://testuser@10.0.0.1:1080";
669 let p: ProxyProtocol = uri.parse().unwrap();
670 let s = p.to_string();
671 let p2: ProxyProtocol = s.parse().unwrap();
672 assert_eq!(p, p2, "SOCKS4 user-only round-trip failed");
673 }
674
675 #[test]
676 fn proxy_protocol_parse_http_connect_basic() {
677 let p: ProxyProtocol = "http://127.0.0.1:8080".parse().unwrap();
678 match p {
679 ProxyProtocol::HttpConnect { addr, credentials } => {
680 assert_eq!(addr, "127.0.0.1:8080".parse().unwrap());
681 assert!(credentials.is_none());
682 }
683 _ => panic!("expected HttpConnect"),
684 }
685 }
686
687 #[test]
688 fn proxy_protocol_parse_http_connect_with_auth() {
689 let p: ProxyProtocol = "http://myuser:mypass@192.168.1.1:3128".parse().unwrap();
690 match p {
691 ProxyProtocol::HttpConnect { addr, credentials } => {
692 assert_eq!(addr, "192.168.1.1:3128".parse().unwrap());
693 let auth = credentials.expect("expected credentials");
694 assert_eq!(auth.username, "myuser");
695 assert_eq!(auth.password.as_deref(), Some("mypass"));
696 }
697 _ => panic!("expected HttpConnect"),
698 }
699 }
700
701 #[test]
702 fn proxy_protocol_parse_http_connect_ipv6() {
703 let p: ProxyProtocol = "http://[::1]:8080".parse().unwrap();
704 match p {
705 ProxyProtocol::HttpConnect { addr, .. } => {
706 assert_eq!(addr, "[::1]:8080".parse().unwrap());
707 }
708 _ => panic!("expected HttpConnect"),
709 }
710 }
711
712 #[test]
713 fn proxy_protocol_parse_http_connect_user_only() {
714 let p: ProxyProtocol = "http://myuser@127.0.0.1:8080".parse().unwrap();
716 match p {
717 ProxyProtocol::HttpConnect { credentials, .. } => {
718 let auth = credentials.expect("expected credentials");
719 assert_eq!(auth.username, "myuser");
720 assert!(auth.password.is_none());
721 }
722 _ => panic!("expected HttpConnect"),
723 }
724 }
725
726 #[test]
727 fn proxy_protocol_reject_password_only() {
728 let result: Result<ProxyProtocol, _> = "http://:secretpass@127.0.0.1:8080".parse();
730 assert!(result.is_err());
731 let err = result.unwrap_err();
732 assert!(
733 err.to_string().contains("password without username"),
734 "error should mention password without username: {}",
735 err
736 );
737 }
738
739 #[test]
740 fn proxy_protocol_is_loopback() {
741 let p: ProxyProtocol = "socks5://127.0.0.1:1080".parse().unwrap();
743 assert!(p.is_loopback());
744
745 let p: ProxyProtocol = "http://[::1]:8080".parse().unwrap();
747 assert!(p.is_loopback());
748
749 let p: ProxyProtocol = "socks5://10.0.0.1:1080".parse().unwrap();
751 assert!(!p.is_loopback());
752
753 let p: ProxyProtocol = "http://[2001:db8::1]:8080".parse().unwrap();
755 assert!(!p.is_loopback());
756 }
757
758 #[test]
759 fn proxy_protocol_http_connect_percent_encoding_roundtrip() {
760 let p = ProxyProtocol::HttpConnect {
763 addr: "127.0.0.1:8080".parse().unwrap(),
764 credentials: Some(HttpConnectAuth {
765 username: "user@domain".to_string(),
766 password: Some("pass:word".to_string()),
767 }),
768 };
769 let s = p.to_string();
770
771 assert!(s.contains("%40"), "@ should be encoded as %40: {}", s);
773 assert!(
774 s.contains("%3A") || s.contains("%3a"),
775 ": in password should be encoded: {}",
776 s
777 );
778
779 let p2: ProxyProtocol = s.parse().unwrap();
781 assert_eq!(
782 p, p2,
783 "Round-trip failed for percent-encoded HTTP CONNECT URI"
784 );
785 }
786
787 #[test]
788 fn proxy_protocol_http_connect_parse_percent_encoded() {
789 let p: ProxyProtocol = "http://user%40domain:pass%3Aword@127.0.0.1:8080"
791 .parse()
792 .unwrap();
793 match p {
794 ProxyProtocol::HttpConnect { addr, credentials } => {
795 assert_eq!(addr, "127.0.0.1:8080".parse().unwrap());
796 let auth = credentials.expect("expected credentials");
797 assert_eq!(
798 auth.username, "user@domain",
799 "username should decode %40 to @"
800 );
801 assert_eq!(
802 auth.password.as_deref(),
803 Some("pass:word"),
804 "password should decode %3A to :"
805 );
806 }
807 _ => panic!("expected HttpConnect"),
808 }
809 }
810}