1use std::io::{Read, Write};
2use std::net::{IpAddr, SocketAddr};
3
4pub trait SocksConnector: Send + Sync {
5 fn connect<S: Read + Write>(&self, stream: S, target: &SocketAddr) -> Result<S, String>;
6}
7
8#[derive(Debug)]
10pub struct ProxyUrl {
11 pub protocol: ProxyProtocol,
12 pub host: String,
13 pub port: u16,
14 pub username: Option<String>,
15 pub password: Option<String>,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum ProxyProtocol {
20 Socks4,
21 Socks5,
22 Http,
23 Https,
24}
25
26impl ProxyUrl {
27 pub fn parse(url: &str) -> Result<Self, String> {
34 let url = url.trim();
35
36 let (protocol, rest) = if let Some(rest) = url.strip_prefix("socks5://") {
38 (ProxyProtocol::Socks5, rest)
39 } else if let Some(rest) = url.strip_prefix("socks4://") {
40 (ProxyProtocol::Socks4, rest)
41 } else if let Some(rest) = url.strip_prefix("http://") {
42 (ProxyProtocol::Http, rest)
43 } else if let Some(rest) = url.strip_prefix("https://") {
44 (ProxyProtocol::Https, rest)
45 } else {
46 return Err(format!("Unsupported proxy protocol in URL: {}", url));
47 };
48
49 let (auth_part, host_port) = match rest.find('@') {
51 Some(idx) => (&rest[..idx], &rest[idx + 1..]),
52 None => ("", rest),
53 };
54
55 let (username, password) = if !auth_part.is_empty() {
57 if let Some(colon_idx) = auth_part.find(':') {
58 (
59 Some(auth_part[..colon_idx].to_string()),
60 Some(auth_part[colon_idx + 1..].to_string()),
61 )
62 } else {
63 (Some(auth_part.to_string()), None)
65 }
66 } else {
67 (None, None)
68 };
69
70 let (host, port) = if let Some(colon_idx) = host_port.rfind(':') {
72 let h = &host_port[..colon_idx];
73 let p_str = &host_port[colon_idx + 1..];
74 let port: u16 = p_str
75 .parse()
76 .map_err(|_| format!("Invalid port number in proxy URL: {}", p_str))?;
77 (h.to_string(), port)
78 } else {
79 let default_port = match protocol {
81 ProxyProtocol::Socks4 | ProxyProtocol::Socks5 => 1080u16,
82 ProxyProtocol::Http => 8080u16,
83 ProxyProtocol::Https => 443u16,
84 };
85 (host_port.to_string(), default_port)
86 };
87
88 if host.is_empty() {
89 return Err("Host is empty in proxy URL".to_string());
90 }
91
92 Ok(Self {
93 protocol,
94 host,
95 port,
96 username,
97 password,
98 })
99 }
100
101 pub fn create_connector(&self) -> SocksConnectorEnum {
103 match self.protocol {
104 ProxyProtocol::Socks4 => SocksConnectorEnum::Socks4(Socks4Connector::new(
105 self.username.clone().unwrap_or_default(),
106 )),
107 ProxyProtocol::Socks5 => SocksConnectorEnum::Socks5(Socks5Connector::new(
108 self.username.clone(),
109 self.password.clone(),
110 )),
111 _ => panic!("HTTP/HTTPS connectors not yet implemented"),
112 }
113 }
114}
115
116pub enum SocksConnectorEnum {
118 Socks4(Socks4Connector),
119 Socks5(Socks5Connector),
120}
121
122impl SocksConnector for SocksConnectorEnum {
123 fn connect<S: Read + Write>(&self, stream: S, target: &SocketAddr) -> Result<S, String> {
124 match self {
125 Self::Socks4(c) => c.connect(stream, target),
126 Self::Socks5(c) => c.connect(stream, target),
127 }
128 }
129}
130
131pub struct NoProxyMatcher {
139 entries: Vec<NoProxyEntry>,
140 match_all: bool,
141}
142
143enum NoProxyEntry {
144 Domain(String), IpAddr(IpAddr), IpNetwork(IpAddr, u8), }
148
149impl NoProxyMatcher {
150 pub fn from_env_value(value: &str) -> Self {
153 let mut entries = Vec::new();
154 let mut match_all = false;
155
156 for pattern in value.split(',') {
157 let pattern = pattern.trim();
158 if pattern.is_empty() {
159 continue;
160 }
161
162 if pattern == "*" {
163 match_all = true;
164 continue;
165 }
166
167 if let Some(slash_pos) = pattern.rfind('/') {
169 let addr_str = &pattern[..slash_pos];
170 let prefix_str = &pattern[slash_pos + 1..];
171 if let Ok(addr) = addr_str.parse::<IpAddr>()
172 && let Ok(prefix) = prefix_str.parse::<u8>()
173 {
174 entries.push(NoProxyEntry::IpNetwork(addr, prefix));
175 continue;
176 }
177 }
178
179 if let Ok(addr) = pattern.parse::<IpAddr>() {
181 entries.push(NoProxyEntry::IpAddr(addr));
182 continue;
183 }
184
185 let normalized = if pattern.starts_with("*.") {
187 &pattern[1..]
188 } else {
189 pattern
190 };
191 entries.push(NoProxyEntry::Domain(normalized.to_lowercase()));
192 }
193
194 Self { entries, match_all }
195 }
196
197 pub fn should_bypass(&self, target: &SocketAddr) -> bool {
199 if self.match_all {
200 return true;
201 }
202
203 let ip = target.ip();
204
205 for entry in &self.entries {
206 match entry {
207 NoProxyEntry::IpAddr(entry_ip) if *entry_ip == ip => return true,
208 NoProxyEntry::IpNetwork(network_addr, prefix_len)
209 if Self::ip_in_network(ip, *network_addr, *prefix_len) =>
210 {
211 return true;
212 }
213 _ => {}
214 }
215 }
216
217 false
218 }
219
220 pub fn should_bypass_hostname(&self, hostname: &str) -> bool {
222 if self.match_all {
223 return true;
224 }
225
226 let hostname_lower = hostname.to_lowercase();
227
228 for entry in &self.entries {
229 if let NoProxyEntry::Domain(pattern) = entry {
230 if let Some(stripped) = pattern.strip_prefix('.') {
231 if hostname_lower.ends_with(pattern.as_str()) || hostname_lower == stripped {
233 return true;
234 }
235 } else {
236 if hostname_lower == *pattern {
238 return true;
239 }
240 }
241 }
242 }
243
244 false
245 }
246
247 fn ip_in_network(ip: IpAddr, network: IpAddr, prefix_len: u8) -> bool {
249 match (ip, network) {
250 (std::net::IpAddr::V4(ip_v4), std::net::IpAddr::V4(net_v4)) => {
251 let ip_u32 = u32::from_be_bytes(ip_v4.octets());
252 let net_u32 = u32::from_be_bytes(net_v4.octets());
253 let mask = if prefix_len >= 32 {
254 0xFFFFFFFFu32
255 } else {
256 !(0xFFFFFFFF >> prefix_len)
257 };
258 (ip_u32 & mask) == (net_u32 & mask)
259 }
260 (std::net::IpAddr::V6(ip_v6), std::net::IpAddr::V6(net_v6)) => {
261 let ip_octets = ip_v6.octets();
262 let net_octets = net_v6.octets();
263 let full_bytes = (prefix_len as usize) / 8;
264 let remaining_bits = (prefix_len as usize) % 8;
265
266 if ip_octets[..full_bytes] != net_octets[..full_bytes] {
268 return false;
269 }
270
271 if remaining_bits > 0 && full_bytes < 16 {
273 let mask = !(0xFFu8 >> remaining_bits);
274 if (ip_octets[full_bytes] & mask) != (net_octets[full_bytes] & mask) {
275 return false;
276 }
277 }
278
279 true
280 }
281 _ => false,
282 }
283 }
284}
285
286pub struct Socks4Connector {
287 pub user_id: String,
288}
289
290impl Socks4Connector {
291 pub fn new(user_id: impl Into<String>) -> Self {
292 Self {
293 user_id: user_id.into(),
294 }
295 }
296}
297
298impl SocksConnector for Socks4Connector {
299 fn connect<S: Read + Write>(&self, mut stream: S, target: &SocketAddr) -> Result<S, String> {
300 let ip = match target.ip() {
301 std::net::IpAddr::V4(v4) => v4,
302 std::net::IpAddr::V6(_) => {
303 return Err("SOCKS4 does not support IPv6 addresses".to_string());
304 }
305 };
306
307 let port = target.port();
308
309 let mut request = Vec::with_capacity(9 + self.user_id.len() + 1);
310 request.push(0x04);
311 request.push(0x01);
312 request.extend_from_slice(&port.to_be_bytes());
313 request.extend_from_slice(&ip.octets());
314 request.extend_from_slice(self.user_id.as_bytes());
315 request.push(0x00);
316
317 stream
318 .write_all(&request)
319 .map_err(|e| format!("SOCKS4 failed to send request: {}", e))?;
320
321 let mut response = [0u8; 8];
322 stream
323 .read_exact(&mut response)
324 .map_err(|e| format!("SOCKS4 failed to read response: {}", e))?;
325
326 if response[1] == 0x5A {
327 Ok(stream)
328 } else {
329 let msg = match response[1] {
330 0x91 => "request rejected or failed",
331 0x92 => "request rejected: SOCKS server cannot connect to identd on client",
332 0x93 => "request rejected: client program and identd report different user-ids",
333 code => return Err(format!("SOCKS4 unknown error code: 0x{:02X}", code)),
334 };
335 Err(msg.to_string())
336 }
337 }
338}
339
340pub struct Socks5Connector {
341 pub username: Option<String>,
342 pub password: Option<String>,
343}
344
345impl Socks5Connector {
346 pub fn new(username: Option<String>, password: Option<String>) -> Self {
347 Self { username, password }
348 }
349
350 pub fn no_auth() -> Self {
351 Self {
352 username: None,
353 password: None,
354 }
355 }
356
357 fn send_greeting<S: Read + Write>(&self, stream: &mut S) -> Result<u8, String> {
358 let has_credentials = self.username.is_some() && self.password.is_some();
359 let nmethods = if has_credentials { 2u8 } else { 1u8 };
360
361 let mut greeting = vec![0x05, nmethods];
362 greeting.push(0x00);
363 if has_credentials {
364 greeting.push(0x02);
365 }
366
367 stream
368 .write_all(&greeting)
369 .map_err(|e| format!("SOCKS5 failed to send greeting: {}", e))?;
370
371 let mut reply = [0u8; 2];
372 stream
373 .read_exact(&mut reply)
374 .map_err(|e| format!("SOCKS5 failed to read greeting response: {}", e))?;
375
376 if reply[0] != 0x05 {
377 return Err(format!(
378 "SOCKS5 invalid version in greeting response: 0x{:02X}",
379 reply[0]
380 ));
381 }
382
383 Ok(reply[1])
384 }
385
386 fn authenticate<S: Read + Write>(&self, stream: &mut S) -> Result<(), String> {
387 let username = self.username.as_deref().unwrap_or("");
388 let password = self.password.as_deref().unwrap_or("");
389
390 if username.len() > 255 || password.len() > 255 {
391 return Err(
392 "SOCKS5 username or password exceeds maximum length of 255 bytes".to_string(),
393 );
394 }
395
396 let mut auth_req = Vec::with_capacity(3 + username.len() + password.len());
397 auth_req.push(0x01);
398 auth_req.push(username.len() as u8);
399 auth_req.extend_from_slice(username.as_bytes());
400 auth_req.push(password.len() as u8);
401 auth_req.extend_from_slice(password.as_bytes());
402
403 stream
404 .write_all(&auth_req)
405 .map_err(|e| format!("SOCKS5 failed to send auth request: {}", e))?;
406
407 let mut auth_reply = [0u8; 2];
408 stream
409 .read_exact(&mut auth_reply)
410 .map_err(|e| format!("SOCKS5 failed to read auth response: {}", e))?;
411
412 if auth_reply[0] != 0x01 {
413 return Err(format!(
414 "SOCKS5 invalid auth response version: 0x{:02X}",
415 auth_reply[0]
416 ));
417 }
418
419 if auth_reply[1] != 0x00 {
420 return Err("SOCKS5 authentication failed".to_string());
421 }
422
423 Ok(())
424 }
425
426 fn send_connect_request<S: Read + Write>(
427 &self,
428 stream: &mut S,
429 target: &SocketAddr,
430 ) -> Result<(), String> {
431 let (atyp, addr_bytes) = match target.ip() {
432 std::net::IpAddr::V4(v4) => (0x01u8, v4.octets().to_vec()),
433 std::net::IpAddr::V6(v6) => (0x04u8, v6.octets().to_vec()),
434 };
435
436 let port = target.port();
437
438 let mut req = Vec::with_capacity(6 + addr_bytes.len());
439 req.push(0x05);
440 req.push(0x01);
441 req.push(0x00);
442 req.push(atyp);
443 req.extend_from_slice(&addr_bytes);
444 req.extend_from_slice(&port.to_be_bytes());
445
446 stream
447 .write_all(&req)
448 .map_err(|e| format!("SOCKS5 failed to send connect request: {}", e))?;
449
450 let ver =
451 read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reply version: {}", e))?;
452 if ver != 0x05 {
453 return Err(format!("SOCKS5 invalid reply version: 0x{:02X}", ver));
454 }
455
456 let rep =
457 read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reply code: {}", e))?;
458 if rep != 0x00 {
459 let msg = match rep {
460 0x01 => "general SOCKS server failure",
461 0x02 => "connection not allowed by ruleset",
462 0x03 => "network unreachable",
463 0x04 => "host unreachable",
464 0x05 => "connection refused",
465 0x06 => "TTL expired",
466 0x07 => "command not supported",
467 0x08 => "address type not supported",
468 code => return Err(format!("SOCKS5 unknown error code: 0x{:02X}", code)),
469 };
470 return Err(msg.to_string());
471 }
472
473 let _rsv =
474 read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reserved byte: {}", e))?;
475 let atyp_reply =
476 read_u8(stream).map_err(|e| format!("SOCKS5 failed to read address type: {}", e))?;
477
478 match atyp_reply {
479 0x01 => {
480 let mut _bnd_addr = [0u8; 4];
481 stream
482 .read_exact(&mut _bnd_addr)
483 .map_err(|e| format!("SOCKS5 failed to read bound IPv4 address: {}", e))?;
484 }
485 0x03 => {
486 let len = read_u8(stream)
487 .map_err(|e| format!("SOCKS5 failed to read domain length: {}", e))?;
488 let mut _bnd_domain = vec![0u8; len as usize];
489 stream
490 .read_exact(&mut _bnd_domain)
491 .map_err(|e| format!("SOCKS5 failed to read bound domain: {}", e))?;
492 }
493 0x04 => {
494 let mut _bnd_addr = [0u8; 16];
495 stream
496 .read_exact(&mut _bnd_addr)
497 .map_err(|e| format!("SOCKS5 failed to read bound IPv6 address: {}", e))?;
498 }
499 _ => {
500 return Err(format!(
501 "SOCKS5 unsupported address type in reply: 0x{:02X}",
502 atyp_reply
503 ));
504 }
505 }
506
507 let mut _bnd_port = [0u8; 2];
508 stream
509 .read_exact(&mut _bnd_port)
510 .map_err(|e| format!("SOCKS5 failed to read bound port: {}", e))?;
511
512 Ok(())
513 }
514}
515
516impl SocksConnector for Socks5Connector {
517 fn connect<S: Read + Write>(&self, mut stream: S, target: &SocketAddr) -> Result<S, String> {
518 let method = self.send_greeting(&mut stream)?;
519
520 match method {
521 0x00 => {}
522 0x02 => {
523 self.authenticate(&mut stream)?;
524 }
525 _ => {
526 return Err(format!(
527 "SOCKS5 server returned unacceptable authentication method: 0x{:02X}",
528 method
529 ));
530 }
531 }
532
533 self.send_connect_request(&mut stream, target)?;
534
535 Ok(stream)
536 }
537}
538
539fn read_u8<S: Read>(stream: &mut S) -> Result<u8, std::io::Error> {
540 let mut buf = [0u8; 1];
541 stream.read_exact(&mut buf)?;
542 Ok(buf[0])
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use std::io::Cursor;
549 use std::net::{Ipv4Addr, SocketAddrV4};
550
551 #[derive(Debug)]
552 struct MockTcpStream {
553 reader: Cursor<Vec<u8>>,
554 writer: Vec<u8>,
555 }
556
557 impl MockTcpStream {
558 fn new(read_data: Vec<u8>) -> Self {
559 Self {
560 reader: Cursor::new(read_data),
561 writer: Vec::new(),
562 }
563 }
564
565 fn into_write(self) -> Vec<u8> {
566 self.writer
567 }
568 }
569
570 impl Read for MockTcpStream {
571 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
572 self.reader.read(buf)
573 }
574 }
575
576 impl Write for MockTcpStream {
577 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
578 self.writer.extend_from_slice(buf);
579 Ok(buf.len())
580 }
581
582 fn flush(&mut self) -> std::io::Result<()> {
583 Ok(())
584 }
585 }
586
587 #[test]
588 fn test_socks4_happy_path() {
589 let connector = Socks4Connector::new("testuser");
590 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8080).into();
591
592 let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
593 let mock_stream = MockTcpStream::new(mock_response);
594
595 let result = connector.connect(mock_stream, &target);
596 assert!(result.is_ok(), "SOCKS4 happy path should succeed");
597
598 let written = result.unwrap().into_write();
599 assert_eq!(written[0], 0x04, "version byte should be 0x04");
600 assert_eq!(written[1], 0x01, "command byte should be 0x01 (connect)");
601 assert_eq!(&written[4..8], &[127, 0, 0, 1], "IP should be 127.0.0.1");
602 assert_eq!(
603 &written[8..17],
604 b"testuser\0",
605 "user ID should be null-terminated"
606 );
607 }
608
609 #[test]
610 fn test_socks4_rejected_error() {
611 let connector = Socks4Connector::new("user");
612 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
613
614 let mock_response: Vec<u8> = vec![0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
615 let mock_stream = MockTcpStream::new(mock_response);
616
617 let result = connector.connect(mock_stream, &target);
618 assert!(result.is_err(), "SOCKS4 rejection should return error");
619 let err_msg = result.err().unwrap();
620 assert!(
621 err_msg.contains("rejected"),
622 "error message should mention rejection: {}",
623 err_msg
624 );
625 }
626
627 #[test]
628 fn test_socks4_identd_error() {
629 let connector = Socks4Connector::new("user");
630 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
631
632 let mock_response: Vec<u8> = vec![0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
633 let mock_stream = MockTcpStream::new(mock_response);
634
635 let result = connector.connect(mock_stream, &target);
636 assert!(result.is_err());
637 let err_msg = result.err().unwrap();
638 assert!(
639 err_msg.contains("identd"),
640 "error message should mention identd: {}",
641 err_msg
642 );
643 }
644
645 #[test]
646 fn test_socks4_userid_mismatch_error() {
647 let connector = Socks4Connector::new("user");
648 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
649
650 let mock_response: Vec<u8> = vec![0x00, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
651 let mock_stream = MockTcpStream::new(mock_response);
652
653 let result = connector.connect(mock_stream, &target);
654 assert!(result.is_err());
655 let err_msg = result.err().unwrap();
656 assert!(
657 err_msg.contains("identd") && err_msg.contains("different"),
658 "error message should mention identd/user-id mismatch: {}",
659 err_msg
660 );
661 }
662
663 #[test]
664 fn test_socks4_empty_user_id() {
665 let connector = Socks4Connector::new("");
666 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 1), 443).into();
667
668 let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
669 let mock_stream = MockTcpStream::new(mock_response);
670
671 let result = connector.connect(mock_stream, &target);
672 assert!(result.is_ok(), "empty user ID should work");
673 let written = result.unwrap().into_write();
674 assert_eq!(
675 &written[4..9],
676 &[192, 168, 1, 1, 0x00],
677 "null terminator after IP when no user ID"
678 );
679 }
680
681 #[test]
682 fn test_socks5_no_auth_happy_path() {
683 let connector = Socks5Connector::no_auth();
684 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(93, 184, 216, 34), 443).into();
685
686 let mock_response: Vec<u8> = vec![
687 0x05, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x90,
688 ];
689 let mock_stream = MockTcpStream::new(mock_response);
690
691 let result = connector.connect(mock_stream, &target);
692 assert!(result.is_ok(), "SOCKS5 no-auth happy path should succeed");
693
694 let written = result.unwrap().into_write();
695 assert_eq!(written[0], 0x05, "greeting version should be 0x05");
696 assert_eq!(written[1], 0x01, "should offer exactly 1 method (no-auth)");
697 assert_eq!(written[2], 0x00, "method offered should be 0x00 (no-auth)");
698 let conn_offset = 3;
699 assert_eq!(written[conn_offset], 0x05, "connect request version");
700 assert_eq!(written[conn_offset + 1], 0x01, "connect command");
701 assert_eq!(written[conn_offset + 2], 0x00, "reserved");
702 assert_eq!(written[conn_offset + 3], 0x01, "ATYP IPv4");
703 assert_eq!(
704 &written[conn_offset + 4..conn_offset + 8],
705 &[93, 184, 216, 34],
706 "target IP"
707 );
708 assert_eq!(
709 &written[conn_offset + 8..conn_offset + 10],
710 &0x01BBu16.to_be_bytes(),
711 "target port 443"
712 );
713 }
714
715 #[test]
716 fn test_socks5_username_password_auth_happy_path() {
717 let connector =
718 Socks5Connector::new(Some("myuser".to_string()), Some("mypass".to_string()));
719 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 20, 30, 40), 8080).into();
720
721 let mock_response: Vec<u8> = vec![
722 0x05, 0x02, 0x01, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x90,
723 ];
724 let mock_stream = MockTcpStream::new(mock_response);
725
726 let result = connector.connect(mock_stream, &target);
727 assert!(
728 result.is_ok(),
729 "SOCKS5 username/password auth should succeed"
730 );
731
732 let written = result.unwrap().into_write();
733 assert_eq!(written[0], 0x05, "greeting version");
734 assert_eq!(written[1], 0x02, "should offer 2 methods");
735 assert_eq!(written[2], 0x00, "no-auth method");
736 assert_eq!(written[3], 0x02, "username/password method");
737 let auth_offset = 4;
738 assert_eq!(written[auth_offset], 0x01, "auth sub-negotiation version");
739 assert_eq!(written[auth_offset + 1], 6, "username length");
740 assert_eq!(
741 &written[auth_offset + 2..auth_offset + 8],
742 b"myuser",
743 "username"
744 );
745 assert_eq!(written[auth_offset + 8], 6, "password length");
746 assert_eq!(
747 &written[auth_offset + 9..auth_offset + 15],
748 b"mypass",
749 "password"
750 );
751 }
752
753 #[test]
754 fn test_socks5_auth_failure() {
755 let connector = Socks5Connector::new(Some("bad".to_string()), Some("cred".to_string()));
756 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 5678).into();
757
758 let mock_response: Vec<u8> = vec![0x05, 0x02, 0x01, 0x01];
759 let mock_stream = MockTcpStream::new(mock_response);
760
761 let result = connector.connect(mock_stream, &target);
762 assert!(result.is_err(), "auth failure should be an error");
763 let err_msg = result.err().unwrap();
764 assert!(
765 err_msg.contains("authentication failed") || err_msg.contains("failed"),
766 "error should mention auth failure: {}",
767 err_msg
768 );
769 }
770
771 #[test]
772 fn test_socks5_connection_refused() {
773 let connector = Socks5Connector::no_auth();
774 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(8, 8, 8, 8), 53).into();
775
776 let mock_response: Vec<u8> = vec![
777 0x05, 0x00, 0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
778 ];
779 let mock_stream = MockTcpStream::new(mock_response);
780
781 let result = connector.connect(mock_stream, &target);
782 assert!(result.is_err(), "connection refused should be an error");
783 let err_msg = result.err().unwrap();
784 assert!(
785 err_msg.contains("refused"),
786 "error should mention connection refused: {}",
787 err_msg
788 );
789 }
790
791 #[test]
792 fn test_socks5_unacceptable_method() {
793 let connector = Socks5Connector::no_auth();
794 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 1), 80).into();
795
796 let mock_response: Vec<u8> = vec![0x05, 0xFF];
797 let mock_stream = MockTcpStream::new(mock_response);
798
799 let result = connector.connect(mock_stream, &target);
800 assert!(result.is_err(), "unacceptable method should be an error");
801 let err_msg = result.err().unwrap();
802 assert!(
803 err_msg.contains("unacceptable") || err_msg.contains("0xFF"),
804 "error should mention unacceptable method: {}",
805 err_msg
806 );
807 }
808
809 #[test]
810 fn test_socks5_general_failure() {
811 let connector = Socks5Connector::no_auth();
812 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 1).into();
813
814 let mock_response: Vec<u8> = vec![
815 0x05, 0x00, 0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
816 ];
817 let mock_stream = MockTcpStream::new(mock_response);
818
819 let result = connector.connect(mock_stream, &target);
820 assert!(result.is_err(), "general failure should be an error");
821 let err_msg = result.err().unwrap();
822 assert!(
823 err_msg.contains("general") || err_msg.contains("failure"),
824 "error should mention general failure: {}",
825 err_msg
826 );
827 }
828
829 #[test]
833 fn e7_test_socks4_connect_success() {
834 let connector = Socks4Connector::new("proxyuser");
835 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(172, 16, 0, 1), 443).into();
836
837 let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
838 let mock_stream = MockTcpStream::new(mock_response);
839
840 let result = connector.connect(mock_stream, &target);
841 assert!(
842 result.is_ok(),
843 "SOCKS4 connect with valid response should succeed"
844 );
845
846 let written = result.unwrap().into_write();
847 assert_eq!(written[0], 0x04, "SOCKS version byte");
848 assert_eq!(written[1], 0x01, "CONNECT command");
849 }
850
851 #[test]
853 fn e7_test_socks4_connect_fail_rejected() {
854 let connector = Socks4Connector::new("test");
855 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 80).into();
856
857 let mock_response: Vec<u8> = vec![0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
858 let mock_stream = MockTcpStream::new(mock_response);
859
860 let result = connector.connect(mock_stream, &target);
861 assert!(result.is_err(), "SOCKS4 error 0x91 should return Err");
862 assert!(
863 result.unwrap_err().contains("rejected"),
864 "error message must contain 'rejected'"
865 );
866 }
867
868 #[test]
870 fn e7_test_socks5_no_auth_connect() {
871 let connector = Socks5Connector::no_auth();
872 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 8080).into();
873
874 let mock_response: Vec<u8> = vec![
875 0x05, 0x00, 0x05, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x01, 0x1f, 0x90,
876 ];
877 let mock_stream = MockTcpStream::new(mock_response);
878
879 let result = connector.connect(mock_stream, &target);
880 assert!(result.is_ok(), "SOCKS5 no-auth connect should succeed");
881
882 let written = result.unwrap().into_write();
883 assert_eq!(written[0], 0x05, "greeting version");
884 assert_eq!(written[1], 0x01, "one method offered");
885 assert_eq!(written[2], 0x00, "no-auth method");
886 }
887
888 #[test]
890 fn e7_test_socks5_password_auth_connect() {
891 let connector =
892 Socks5Connector::new(Some("admin".to_string()), Some("secret123".to_string()));
893 let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9090).into();
894
895 let mock_response: Vec<u8> = vec![
896 0x05, 0x02, 0x01, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x23, 0x82,
897 ];
898 let mock_stream = MockTcpStream::new(mock_response);
899
900 let result = connector.connect(mock_stream, &target);
901 assert!(
902 result.is_ok(),
903 "SOCKS5 password auth connect should succeed"
904 );
905
906 let written = result.unwrap().into_write();
907 assert_eq!(written[0], 0x05, "greeting version");
908 assert_eq!(written[1], 0x02, "two methods offered");
909 assert_eq!(written[3], 0x02, "username/password method offered");
910
911 let auth_offset = 4;
912 assert_eq!(written[auth_offset], 0x01, "auth sub-version");
913 assert_eq!(written[auth_offset + 1], 5, "username length 'admin'");
914 assert_eq!(
915 &written[auth_offset + 2..auth_offset + 7],
916 b"admin",
917 "username bytes"
918 );
919 assert_eq!(written[auth_offset + 7], 9, "password length 'secret123'");
920 assert_eq!(
921 &written[auth_offset + 8..auth_offset + 17],
922 b"secret123",
923 "password bytes"
924 );
925 }
926
927 #[test]
929 fn e7_test_no_proxy_bypass_matcher() {
930 let matcher = NoProxyMatcher::from_env_value("*.local,localhost,example.com,.internal");
931
932 assert!(
934 matcher.should_bypass_hostname("api.local"),
935 "*.local should match api.local"
936 );
937
938 assert!(
940 !matcher.should_bypass_hostname("example.org"),
941 "example.org should not bypass"
942 );
943
944 assert!(
946 matcher.should_bypass_hostname("localhost"),
947 "exact match localhost should bypass"
948 );
949
950 assert!(
952 matcher.should_bypass_hostname("example.com"),
953 "exact match example.com should bypass"
954 );
955
956 assert!(
958 matcher.should_bypass_hostname("sub.internal"),
959 ".internal should match sub.internal"
960 );
961
962 assert!(
964 matcher.should_bypass_hostname("internal"),
965 ".internal should also match bare domain"
966 );
967
968 assert!(
970 !matcher.should_bypass_hostname("google.com"),
971 "google.com should not bypass"
972 );
973 }
974
975 #[test]
977 fn e7_test_no_proxy_ip_matching() {
978 use std::net::{IpAddr, Ipv4Addr};
979
980 let matcher = NoProxyMatcher::from_env_value("192.168.1.1,10.0.0.0/8");
981
982 let addr_v4_192: SocketAddr =
983 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 80);
984 assert!(
985 matcher.should_bypass(&addr_v4_192),
986 "exact IP 192.168.1.1 should bypass"
987 );
988
989 let addr_v4_10: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 5, 3, 1)), 8080);
990 assert!(
991 matcher.should_bypass(&addr_v4_10),
992 "10.5.3.1 should be within 10.0.0.0/8"
993 );
994
995 let addr_v4_external: SocketAddr =
996 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 443);
997 assert!(
998 !matcher.should_bypass(&addr_v4_external),
999 "172.16.0.1 should NOT be within 10.0.0.0/8"
1000 );
1001 }
1002
1003 #[test]
1005 fn e7_test_no_proxy_wildcard_all() {
1006 let matcher = NoProxyMatcher::from_env_value("*");
1007
1008 assert!(
1009 matcher.should_bypass_hostname("anything"),
1010 "* should bypass any hostname"
1011 );
1012 assert!(
1013 matcher.should_bypass(&"127.0.0.1:80".parse::<SocketAddr>().unwrap()),
1014 "* should bypass any IP address"
1015 );
1016 }
1017
1018 #[test]
1020 fn e7_test_proxy_url_parsing_socks5_with_credentials() {
1021 let url = "socks5://user:pass@127.0.0.1:1080";
1022 let parsed = ProxyUrl::parse(url).expect("should parse socks5 URL");
1023
1024 assert_eq!(parsed.protocol, ProxyProtocol::Socks5);
1025 assert_eq!(parsed.host, "127.0.0.1");
1026 assert_eq!(parsed.port, 1080);
1027 assert_eq!(parsed.username, Some("user".to_string()));
1028 assert_eq!(parsed.password, Some("pass".to_string()));
1029 }
1030
1031 #[test]
1033 fn e7_test_proxy_url_parsing_socks4_no_credentials() {
1034 let parsed =
1035 ProxyUrl::parse("socks4://proxy.example.com:1080").expect("should parse socks4 URL");
1036
1037 assert_eq!(parsed.protocol, ProxyProtocol::Socks4);
1038 assert_eq!(parsed.host, "proxy.example.com");
1039 assert_eq!(parsed.port, 1080);
1040 assert!(parsed.username.is_none());
1041 assert!(parsed.password.is_none());
1042 }
1043
1044 #[test]
1046 fn e7_test_proxy_url_parsing_http() {
1047 let parsed = ProxyUrl::parse("http://admin:secret@proxy.corp.com:3128")
1048 .expect("should parse http proxy URL");
1049
1050 assert_eq!(parsed.protocol, ProxyProtocol::Http);
1051 assert_eq!(parsed.host, "proxy.corp.com");
1052 assert_eq!(parsed.port, 3128);
1053 assert_eq!(parsed.username, Some("admin".to_string()));
1054 assert_eq!(parsed.password, Some("secret".to_string()));
1055 }
1056
1057 #[test]
1059 fn e7_test_proxy_url_default_port() {
1060 let socks5_parsed = ProxyUrl::parse("socks5://proxy.local").expect("should parse");
1061 assert_eq!(socks5_parsed.port, 1080, "SOCKS default port is 1080");
1062
1063 let http_parsed = ProxyUrl::parse("http://webproxy.local").expect("should parse");
1064 assert_eq!(http_parsed.port, 8080, "HTTP default port is 8080");
1065
1066 let https_parsed = ProxyUrl::parse("https://secure.local").expect("should parse");
1067 assert_eq!(https_parsed.port, 443, "HTTPS default port is 443");
1068 }
1069
1070 #[test]
1072 fn e7_test_proxy_url_invalid_protocol() {
1073 let result = ProxyUrl::parse("ftp://host:21");
1074 assert!(result.is_err(), "unsupported protocol should return error");
1075 assert!(
1076 result.unwrap_err().contains("Unsupported"),
1077 "error should mention unsupported protocol"
1078 );
1079 }
1080
1081 #[test]
1083 fn e7_test_create_connector_from_url() {
1084 let url = ProxyUrl::parse("socks5://myuser:mypass@10.0.0.1:9050").expect("should parse");
1085 let _connector = url.create_connector();
1086 }
1087}