Skip to main content

ghast_auth/
connect.rs

1//! Opening the game connection: target addresses, proxy routing policy and the [`Connector`].
2//!
3//! A [`Connector`] turns a [`TargetAddr`] into a connected [`TcpStream`] (with `TCP_NODELAY` set),
4//! either directly or through a [`Socks5Proxy`]. Which of the two is allowed for a given target is
5//! decided by a [`ProxyPolicy`], so an application can enforce rules such as "never connect to a
6//! public server without a proxy" in one place.
7//!
8//! # DNS
9//!
10//! When a target is reached through the proxy, its domain name is sent to the proxy and resolved
11//! there (SOCKS5 address type `DOMAINNAME`). The connector never resolves a proxied target
12//! locally, so no DNS query for it leaves the machine. Only two things are ever resolved locally,
13//! through the configured [`Resolve`] implementation: the proxy's own host name (if it is not an
14//! IP literal) and targets of *direct* connections.
15//!
16//! Minecraft `_minecraft._tcp` SRV records are not looked up here: SRV lookups cannot be done
17//! through SOCKS5, and doing them locally would leak the target name.
18
19use std::fmt;
20use std::future::Future;
21use std::io;
22use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
23use std::pin::Pin;
24use std::str::FromStr;
25use std::sync::Arc;
26use std::time::Duration;
27
28use thiserror::Error;
29use tokio::net::TcpStream;
30
31use crate::socks5::{self, Socks5Error, Socks5Proxy};
32
33/// Maximum length of a domain name in bytes (the SOCKS5 length prefix is one byte).
34pub const MAX_DOMAIN_LEN: usize = 255;
35
36/// The default Minecraft server port.
37pub const DEFAULT_MINECRAFT_PORT: u16 = 25565;
38
39/// Where to connect: an IP socket address or a domain name plus port.
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub enum TargetAddr {
42    /// An IP address and port.
43    Ip(SocketAddr),
44    /// A domain name and port. Through a proxy the name is resolved by the proxy.
45    ///
46    /// Values built with [`TargetAddr::new`] or [`str::parse`] are validated (non-empty, at most
47    /// [`MAX_DOMAIN_LEN`] bytes, only `A-Z a-z 0-9 - . _`). A hand-built variant is validated
48    /// again before it is sent to a proxy.
49    Domain(String, u16),
50}
51
52/// Why a target address could not be parsed.
53#[derive(Debug, Clone, PartialEq, Eq, Error)]
54pub enum AddrError {
55    /// The host part is empty.
56    #[error("host is empty")]
57    EmptyHost,
58    /// No port was given and no default applies.
59    #[error("port is missing")]
60    MissingPort,
61    /// The port is not a number in `0..=65535`.
62    #[error("invalid port {0:?}")]
63    InvalidPort(String),
64    /// A bracketed IPv6 literal is malformed.
65    #[error("invalid IPv6 address {0:?}")]
66    InvalidIpv6(String),
67    /// The domain name is longer than [`MAX_DOMAIN_LEN`] bytes.
68    #[error("domain name is {0} bytes long, the maximum is {MAX_DOMAIN_LEN}")]
69    DomainTooLong(usize),
70    /// The domain name contains a character outside `A-Z a-z 0-9 - . _`.
71    ///
72    /// Internationalized names must be given in their ASCII (punycode) form.
73    #[error("domain name contains the invalid character {0:?}")]
74    InvalidDomainChar(char),
75}
76
77impl TargetAddr {
78    /// Builds a target from a host (IP literal or domain name) and a port.
79    ///
80    /// IP literals (including IPv6 without brackets) become [`TargetAddr::Ip`]; everything else is
81    /// validated as a domain name.
82    pub fn new(host: &str, port: u16) -> Result<Self, AddrError> {
83        let unbracketed = host
84            .strip_prefix('[')
85            .and_then(|h| h.strip_suffix(']'))
86            .unwrap_or(host);
87        if let Ok(ip) = unbracketed.parse::<IpAddr>() {
88            return Ok(Self::Ip(SocketAddr::new(ip, port)));
89        }
90        if unbracketed.len() != host.len() {
91            return Err(AddrError::InvalidIpv6(host.to_owned()));
92        }
93        validate_domain(host)?;
94        Ok(Self::Domain(host.to_owned(), port))
95    }
96
97    /// Parses `host`, `host:port`, `[v6]`, `[v6]:port` or a bare IPv6 literal, using
98    /// `default_port` when the string has no port.
99    ///
100    /// ```
101    /// use ghast_auth::connect::{DEFAULT_MINECRAFT_PORT, TargetAddr};
102    ///
103    /// let t = TargetAddr::parse_with_default_port("mc.example.net", DEFAULT_MINECRAFT_PORT).unwrap();
104    /// assert_eq!(t, TargetAddr::Domain("mc.example.net".into(), 25565));
105    /// ```
106    pub fn parse_with_default_port(s: &str, default_port: u16) -> Result<Self, AddrError> {
107        parse_target(s, Some(default_port))
108    }
109
110    /// The port.
111    pub fn port(&self) -> u16 {
112        match self {
113            Self::Ip(addr) => addr.port(),
114            Self::Domain(_, port) => *port,
115        }
116    }
117
118    /// The host part as a string (IPv6 without brackets).
119    pub fn host(&self) -> String {
120        match self {
121            Self::Ip(addr) => addr.ip().to_string(),
122            Self::Domain(host, _) => host.clone(),
123        }
124    }
125
126    /// Returns `true` if the target is on this machine or on the local network: loopback,
127    /// private IPv4 ranges (RFC 1918), link-local, IPv6 unique-local, or the `localhost` name
128    /// (RFC 6761).
129    ///
130    /// Other domain names are **not** considered local, because deciding that would require a
131    /// local DNS lookup. CGNAT (`100.64.0.0/10`) is not considered local either.
132    pub fn is_local(&self) -> bool {
133        match self {
134            Self::Ip(addr) => is_local_ip(addr.ip()),
135            Self::Domain(host, _) => {
136                let host = host.strip_suffix('.').unwrap_or(host).to_ascii_lowercase();
137                host == "localhost" || host.ends_with(".localhost")
138            }
139        }
140    }
141}
142
143impl From<SocketAddr> for TargetAddr {
144    fn from(addr: SocketAddr) -> Self {
145        Self::Ip(addr)
146    }
147}
148
149impl FromStr for TargetAddr {
150    type Err = AddrError;
151
152    /// Parses `host:port` or `[v6]:port`. The port is required; see
153    /// [`TargetAddr::parse_with_default_port`] for a default.
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        parse_target(s, None)
156    }
157}
158
159impl fmt::Display for TargetAddr {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            Self::Ip(addr) => write!(f, "{addr}"),
163            Self::Domain(host, port) => write!(f, "{host}:{port}"),
164        }
165    }
166}
167
168fn parse_target(s: &str, default_port: Option<u16>) -> Result<TargetAddr, AddrError> {
169    let s = s.trim();
170    let parse_port = |p: &str| {
171        p.parse::<u16>()
172            .map_err(|_| AddrError::InvalidPort(p.to_owned()))
173    };
174    let need_default = || default_port.ok_or(AddrError::MissingPort);
175
176    if let Some(rest) = s.strip_prefix('[') {
177        let (inside, after) = rest
178            .split_once(']')
179            .ok_or_else(|| AddrError::InvalidIpv6(s.to_owned()))?;
180        let ip: Ipv6Addr = inside
181            .parse()
182            .map_err(|_| AddrError::InvalidIpv6(inside.to_owned()))?;
183        let port = match after {
184            "" => need_default()?,
185            _ => match after.strip_prefix(':') {
186                Some(p) => parse_port(p)?,
187                None => return Err(AddrError::InvalidIpv6(s.to_owned())),
188            },
189        };
190        return Ok(TargetAddr::Ip(SocketAddr::new(IpAddr::V6(ip), port)));
191    }
192    // A bare IPv6 literal has several colons and no port.
193    if let Ok(ip) = s.parse::<Ipv6Addr>() {
194        return Ok(TargetAddr::Ip(SocketAddr::new(
195            IpAddr::V6(ip),
196            need_default()?,
197        )));
198    }
199    let (host, port) = match s.rsplit_once(':') {
200        Some((host, port)) => (host, parse_port(port)?),
201        None => (s, need_default()?),
202    };
203    if host.is_empty() {
204        return Err(AddrError::EmptyHost);
205    }
206    if let Ok(ip) = host.parse::<Ipv4Addr>() {
207        return Ok(TargetAddr::Ip(SocketAddr::new(IpAddr::V4(ip), port)));
208    }
209    validate_domain(host)?;
210    Ok(TargetAddr::Domain(host.to_owned(), port))
211}
212
213pub(crate) fn validate_domain(host: &str) -> Result<(), AddrError> {
214    if host.is_empty() {
215        return Err(AddrError::EmptyHost);
216    }
217    if host.len() > MAX_DOMAIN_LEN {
218        return Err(AddrError::DomainTooLong(host.len()));
219    }
220    match host
221        .chars()
222        .find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_')))
223    {
224        Some(c) => Err(AddrError::InvalidDomainChar(c)),
225        None => Ok(()),
226    }
227}
228
229fn is_local_ip(ip: IpAddr) -> bool {
230    match ip {
231        IpAddr::V4(v4) => is_local_v4(v4),
232        IpAddr::V6(v6) => {
233            if let Some(v4) = v6.to_ipv4_mapped() {
234                return is_local_v4(v4);
235            }
236            v6.is_loopback() || v6.is_unique_local() || v6.is_unicast_link_local()
237        }
238    }
239}
240
241fn is_local_v4(v4: Ipv4Addr) -> bool {
242    v4.is_loopback() || v4.is_private() || v4.is_link_local()
243}
244
245/// How a single connection should be routed. Returned by a [`ProxyPolicy`].
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum Route {
248    /// Connect through the configured proxy; fail with [`ConnectError::ProxyRequired`] if none
249    /// is configured.
250    Proxy,
251    /// Connect through the proxy if one is configured, otherwise directly.
252    PreferProxy,
253    /// Connect directly, even if a proxy is configured.
254    Direct,
255}
256
257/// A user-supplied routing rule, see [`ProxyPolicy::Custom`].
258pub type RouteFn = dyn Fn(&TargetAddr) -> Route + Send + Sync;
259
260/// Decides, per target, whether a connection must, may or must not use the proxy.
261#[derive(Clone, Default)]
262pub enum ProxyPolicy {
263    /// Use the proxy when one is configured, otherwise connect directly. ([`Route::PreferProxy`]
264    /// for every target.)
265    #[default]
266    PreferProxy,
267    /// Every connection must go through the proxy. ([`Route::Proxy`] for every target.)
268    RequireProxy,
269    /// Targets for which [`TargetAddr::is_local`] holds are connected directly (a remote proxy
270    /// usually cannot reach this machine's LAN); every other target must use the proxy.
271    RequireProxyExceptLocal,
272    /// A custom rule.
273    Custom(Arc<RouteFn>),
274}
275
276impl ProxyPolicy {
277    /// Builds a [`ProxyPolicy::Custom`] from a closure.
278    pub fn custom(f: impl Fn(&TargetAddr) -> Route + Send + Sync + 'static) -> Self {
279        Self::Custom(Arc::new(f))
280    }
281
282    /// The route this policy selects for `target`.
283    pub fn route(&self, target: &TargetAddr) -> Route {
284        match self {
285            Self::PreferProxy => Route::PreferProxy,
286            Self::RequireProxy => Route::Proxy,
287            Self::RequireProxyExceptLocal if target.is_local() => Route::Direct,
288            Self::RequireProxyExceptLocal => Route::Proxy,
289            Self::Custom(f) => f(target),
290        }
291    }
292}
293
294impl fmt::Debug for ProxyPolicy {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        match self {
297            Self::PreferProxy => f.write_str("PreferProxy"),
298            Self::RequireProxy => f.write_str("RequireProxy"),
299            Self::RequireProxyExceptLocal => f.write_str("RequireProxyExceptLocal"),
300            Self::Custom(_) => f.write_str("Custom(..)"),
301        }
302    }
303}
304
305/// The future returned by [`Resolve::resolve`].
306pub type ResolveFuture<'a> = Pin<Box<dyn Future<Output = io::Result<Vec<SocketAddr>>> + Send + 'a>>;
307
308/// Local name resolution, used only for direct connections and for the proxy's own host name.
309///
310/// The default is [`SystemResolver`]. Applications can plug in their own (for example DNS over
311/// HTTPS, a static table, or a counter in tests).
312pub trait Resolve: Send + Sync {
313    /// Resolves `host` to socket addresses with the given port.
314    fn resolve<'a>(&'a self, host: &'a str, port: u16) -> ResolveFuture<'a>;
315}
316
317/// The operating system resolver (`getaddrinfo` through [`tokio::net::lookup_host`]).
318#[derive(Debug, Clone, Copy, Default)]
319pub struct SystemResolver;
320
321impl Resolve for SystemResolver {
322    fn resolve<'a>(&'a self, host: &'a str, port: u16) -> ResolveFuture<'a> {
323        Box::pin(async move { Ok(tokio::net::lookup_host((host, port)).await?.collect()) })
324    }
325}
326
327/// Timeouts used by the [`Connector`].
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub struct Timeouts {
330    /// Local DNS resolution (proxy host name or direct target).
331    pub resolve: Duration,
332    /// Each TCP connection attempt (to the proxy or the target).
333    pub connect: Duration,
334    /// The whole SOCKS5 negotiation, from the greeting to the CONNECT reply.
335    pub handshake: Duration,
336}
337
338impl Default for Timeouts {
339    fn default() -> Self {
340        Self {
341            resolve: Duration::from_secs(10),
342            connect: Duration::from_secs(10),
343            handshake: Duration::from_secs(15),
344        }
345    }
346}
347
348/// Which step of [`Connector::connect`] timed out.
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum TimeoutStage {
351    /// Local DNS resolution.
352    Resolve,
353    /// The TCP connection attempt.
354    Connect,
355    /// The SOCKS5 negotiation.
356    Handshake,
357}
358
359impl fmt::Display for TimeoutStage {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        f.write_str(match self {
362            Self::Resolve => "DNS resolution",
363            Self::Connect => "TCP connect",
364            Self::Handshake => "SOCKS5 handshake",
365        })
366    }
367}
368
369/// Errors from [`Connector::connect`].
370#[derive(Debug, Error)]
371pub enum ConnectError {
372    /// The policy requires a proxy for this target but none is configured. Nothing was sent.
373    #[error("the proxy policy requires a proxy to connect to {0}, but none is configured")]
374    ProxyRequired(TargetAddr),
375    /// Local DNS resolution failed.
376    #[error("could not resolve {host}: {source}")]
377    Resolve {
378        /// The name being resolved.
379        host: String,
380        /// The resolver error.
381        #[source]
382        source: io::Error,
383    },
384    /// Local DNS resolution returned no addresses.
385    #[error("{0} resolved to no addresses")]
386    NoAddresses(String),
387    /// A step took longer than its [`Timeouts`] entry.
388    #[error("{0} timed out")]
389    Timeout(TimeoutStage),
390    /// The TCP connection to the target (direct) or the proxy failed.
391    #[error("could not connect to {addr}: {source}")]
392    Connect {
393        /// The last address that was tried.
394        addr: SocketAddr,
395        /// The socket error.
396        #[source]
397        source: io::Error,
398    },
399    /// The SOCKS5 negotiation failed (including the proxy refusing the CONNECT).
400    #[error("SOCKS5 proxy {proxy}: {source}")]
401    Socks5 {
402        /// The proxy address (credentials are never included).
403        proxy: TargetAddr,
404        /// What went wrong.
405        #[source]
406        source: Socks5Error,
407    },
408    /// Another socket error (for example setting `TCP_NODELAY`).
409    #[error(transparent)]
410    Io(#[from] io::Error),
411}
412
413/// Opens game connections, directly or through a SOCKS5 proxy.
414///
415/// Cheap to clone; one connector can serve many bots.
416#[derive(Clone)]
417pub struct Connector {
418    proxy: Option<Socks5Proxy>,
419    policy: ProxyPolicy,
420    timeouts: Timeouts,
421    resolver: Arc<dyn Resolve>,
422}
423
424impl Default for Connector {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430impl fmt::Debug for Connector {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        f.debug_struct("Connector")
433            .field("proxy", &self.proxy)
434            .field("policy", &self.policy)
435            .field("timeouts", &self.timeouts)
436            .finish_non_exhaustive()
437    }
438}
439
440impl Connector {
441    /// A connector without a proxy, with [`ProxyPolicy::PreferProxy`], default [`Timeouts`] and
442    /// the [`SystemResolver`].
443    pub fn new() -> Self {
444        Self {
445            proxy: None,
446            policy: ProxyPolicy::default(),
447            timeouts: Timeouts::default(),
448            resolver: Arc::new(SystemResolver),
449        }
450    }
451
452    /// Sets the SOCKS5 proxy.
453    pub fn with_proxy(mut self, proxy: Socks5Proxy) -> Self {
454        self.proxy = Some(proxy);
455        self
456    }
457
458    /// Sets or clears the SOCKS5 proxy.
459    pub fn with_optional_proxy(mut self, proxy: Option<Socks5Proxy>) -> Self {
460        self.proxy = proxy;
461        self
462    }
463
464    /// Sets the routing policy.
465    pub fn with_policy(mut self, policy: ProxyPolicy) -> Self {
466        self.policy = policy;
467        self
468    }
469
470    /// Sets the timeouts.
471    pub fn with_timeouts(mut self, timeouts: Timeouts) -> Self {
472        self.timeouts = timeouts;
473        self
474    }
475
476    /// Replaces the local resolver.
477    pub fn with_resolver(mut self, resolver: Arc<dyn Resolve>) -> Self {
478        self.resolver = resolver;
479        self
480    }
481
482    /// The configured proxy, if any.
483    pub fn proxy(&self) -> Option<&Socks5Proxy> {
484        self.proxy.as_ref()
485    }
486
487    /// The routing policy.
488    pub fn policy(&self) -> &ProxyPolicy {
489        &self.policy
490    }
491
492    /// The route [`Connector::connect`] would take for `target`: `Some(true)` through the
493    /// proxy, `Some(false)` directly, `None` if the policy forbids connecting at all.
494    pub fn plan(&self, target: &TargetAddr) -> Option<bool> {
495        match (self.policy.route(target), self.proxy.is_some()) {
496            (Route::Proxy, true) | (Route::PreferProxy, true) => Some(true),
497            (Route::Proxy, false) => None,
498            (Route::PreferProxy, false) | (Route::Direct, _) => Some(false),
499        }
500    }
501
502    /// Connects to `target` according to the policy and returns a stream with `TCP_NODELAY` set.
503    ///
504    /// Through a proxy the stream is positioned right after the SOCKS5 CONNECT reply, so the
505    /// first bytes written are the game's handshake packet.
506    pub async fn connect(&self, target: &TargetAddr) -> Result<TcpStream, ConnectError> {
507        let stream = match (self.plan(target), &self.proxy) {
508            (None, _) => return Err(ConnectError::ProxyRequired(target.clone())),
509            (Some(true), Some(proxy)) => self.connect_via_proxy(proxy, target).await?,
510            (Some(_), _) => self.connect_addr(target).await?,
511        };
512        stream.set_nodelay(true)?;
513        Ok(stream)
514    }
515
516    async fn connect_via_proxy(
517        &self,
518        proxy: &Socks5Proxy,
519        target: &TargetAddr,
520    ) -> Result<TcpStream, ConnectError> {
521        let mut stream = self.connect_addr(proxy.addr()).await?;
522        // Disable Nagle before the negotiation so its small writes are not delayed.
523        stream.set_nodelay(true)?;
524        let negotiation = socks5::handshake(&mut stream, target, proxy.auth());
525        match tokio::time::timeout(self.timeouts.handshake, negotiation).await {
526            Err(_) => Err(ConnectError::Timeout(TimeoutStage::Handshake)),
527            Ok(Err(source)) => Err(ConnectError::Socks5 {
528                proxy: proxy.addr().clone(),
529                source,
530            }),
531            Ok(Ok(_bound)) => Ok(stream),
532        }
533    }
534
535    /// Opens a plain TCP connection, resolving `addr` locally if it is a domain name.
536    async fn connect_addr(&self, addr: &TargetAddr) -> Result<TcpStream, ConnectError> {
537        let candidates = match addr {
538            TargetAddr::Ip(ip) => vec![*ip],
539            TargetAddr::Domain(host, port) => {
540                let lookup = self.resolver.resolve(host, *port);
541                let addrs = match tokio::time::timeout(self.timeouts.resolve, lookup).await {
542                    Err(_) => return Err(ConnectError::Timeout(TimeoutStage::Resolve)),
543                    Ok(Err(source)) => {
544                        return Err(ConnectError::Resolve {
545                            host: host.clone(),
546                            source,
547                        });
548                    }
549                    Ok(Ok(addrs)) => addrs,
550                };
551                if addrs.is_empty() {
552                    return Err(ConnectError::NoAddresses(host.clone()));
553                }
554                addrs
555            }
556        };
557        let mut last_err = None;
558        for candidate in candidates {
559            match tokio::time::timeout(self.timeouts.connect, TcpStream::connect(candidate)).await {
560                Ok(Ok(stream)) => return Ok(stream),
561                Ok(Err(source)) => {
562                    last_err = Some(ConnectError::Connect {
563                        addr: candidate,
564                        source,
565                    })
566                }
567                Err(_) => last_err = Some(ConnectError::Timeout(TimeoutStage::Connect)),
568            }
569        }
570        // `candidates` is never empty here, so an error was recorded.
571        Err(last_err.unwrap_or(ConnectError::Timeout(TimeoutStage::Connect)))
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    fn ip(s: &str) -> TargetAddr {
580        TargetAddr::Ip(s.parse().unwrap())
581    }
582
583    #[test]
584    fn parse_targets() {
585        assert_eq!("1.2.3.4:25565".parse(), Ok(ip("1.2.3.4:25565")));
586        assert_eq!("[::1]:80".parse(), Ok(ip("[::1]:80")));
587        assert_eq!(
588            "mc.example.net:25570".parse(),
589            Ok(TargetAddr::Domain("mc.example.net".into(), 25570))
590        );
591        assert_eq!(
592            "mc.example.net".parse::<TargetAddr>(),
593            Err(AddrError::MissingPort)
594        );
595        assert_eq!(
596            TargetAddr::parse_with_default_port("mc.example.net", 25565),
597            Ok(TargetAddr::Domain("mc.example.net".into(), 25565))
598        );
599        assert_eq!(
600            TargetAddr::parse_with_default_port("::1", 25565),
601            Ok(ip("[::1]:25565"))
602        );
603        assert_eq!(
604            TargetAddr::parse_with_default_port("[fe80::1]", 1),
605            Ok(ip("[fe80::1]:1"))
606        );
607        assert_eq!(
608            "host:99999".parse::<TargetAddr>(),
609            Err(AddrError::InvalidPort("99999".into()))
610        );
611        assert_eq!(":25565".parse::<TargetAddr>(), Err(AddrError::EmptyHost));
612        assert!(matches!(
613            "[::1".parse::<TargetAddr>(),
614            Err(AddrError::InvalidIpv6(_))
615        ));
616        assert!(matches!(
617            "[nope]:1".parse::<TargetAddr>(),
618            Err(AddrError::InvalidIpv6(_))
619        ));
620        assert_eq!(
621            "bad host:1".parse::<TargetAddr>(),
622            Err(AddrError::InvalidDomainChar(' '))
623        );
624        assert_eq!(
625            "b\u{fc}cher.de:1".parse::<TargetAddr>(),
626            Err(AddrError::InvalidDomainChar('\u{fc}'))
627        );
628        let long = format!("{}:1", "a".repeat(256));
629        assert_eq!(
630            long.parse::<TargetAddr>(),
631            Err(AddrError::DomainTooLong(256))
632        );
633    }
634
635    #[test]
636    fn new_and_display() {
637        assert_eq!(TargetAddr::new("::1", 5), Ok(ip("[::1]:5")));
638        assert_eq!(TargetAddr::new("[::1]", 5), Ok(ip("[::1]:5")));
639        assert_eq!(TargetAddr::new("10.0.0.1", 5), Ok(ip("10.0.0.1:5")));
640        assert!(TargetAddr::new("[x]", 5).is_err());
641        assert_eq!(
642            TargetAddr::new("a.b", 7).unwrap().to_string(),
643            "a.b:7".to_owned()
644        );
645        assert_eq!(ip("[::1]:5").to_string(), "[::1]:5");
646        assert_eq!(ip("[::1]:5").host(), "::1");
647    }
648
649    #[test]
650    fn locality() {
651        for local in [
652            "127.0.0.1:1",
653            "10.1.2.3:1",
654            "172.16.0.1:1",
655            "10.0.0.10:25570",
656            "169.254.1.1:1",
657            "[::1]:1",
658            "[fd00::1]:1",
659            "[fe80::1]:1",
660            "[::ffff:10.0.0.1]:1",
661        ] {
662            assert!(ip(local).is_local(), "{local}");
663        }
664        for public in [
665            "8.8.8.8:1",
666            "100.64.0.1:1",
667            "172.32.0.1:1",
668            "[2001:db8::1]:1",
669            "[::ffff:8.8.8.8]:1",
670        ] {
671            assert!(!ip(public).is_local(), "{public}");
672        }
673        assert!(TargetAddr::new("localhost", 1).unwrap().is_local());
674        assert!(TargetAddr::new("LocalHost.", 1).unwrap().is_local());
675        assert!(TargetAddr::new("a.localhost", 1).unwrap().is_local());
676        assert!(!TargetAddr::new("lan-server", 1).unwrap().is_local());
677        assert!(!TargetAddr::new("localhost.example", 1).unwrap().is_local());
678    }
679
680    #[test]
681    fn policies() {
682        let lan = ip("10.0.0.10:25570");
683        let public = TargetAddr::new("mc.example.net", 25565).unwrap();
684        assert_eq!(ProxyPolicy::PreferProxy.route(&public), Route::PreferProxy);
685        assert_eq!(ProxyPolicy::RequireProxy.route(&lan), Route::Proxy);
686        assert_eq!(
687            ProxyPolicy::RequireProxyExceptLocal.route(&lan),
688            Route::Direct
689        );
690        assert_eq!(
691            ProxyPolicy::RequireProxyExceptLocal.route(&public),
692            Route::Proxy
693        );
694        let custom = ProxyPolicy::custom(|t| {
695            if t.port() == 25570 {
696                Route::Direct
697            } else {
698                Route::Proxy
699            }
700        });
701        assert_eq!(custom.route(&lan), Route::Direct);
702        assert_eq!(custom.route(&public), Route::Proxy);
703        assert_eq!(format!("{custom:?}"), "Custom(..)");
704    }
705
706    #[test]
707    fn plan() {
708        let lan = ip("10.0.0.10:25570");
709        let public = ip("8.8.8.8:25565");
710        let proxy: Socks5Proxy = "socks5h://127.0.0.1:1080".parse().unwrap();
711
712        let none = Connector::new().with_policy(ProxyPolicy::RequireProxyExceptLocal);
713        assert_eq!(none.plan(&lan), Some(false));
714        assert_eq!(none.plan(&public), None);
715
716        let some = none.clone().with_proxy(proxy.clone());
717        assert_eq!(some.plan(&lan), Some(false));
718        assert_eq!(some.plan(&public), Some(true));
719
720        let prefer = Connector::new();
721        assert_eq!(prefer.plan(&public), Some(false));
722        assert_eq!(prefer.clone().with_proxy(proxy).plan(&public), Some(true));
723    }
724}