Skip to main content

session_guard/
intel.rs

1//! Country, city, ASN, network name and anonymizer lookups for client IPs.
2
3use std::net::IpAddr;
4
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "maxmind")]
8#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
9mod maxmind;
10#[cfg(feature = "maxmind")]
11#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
12pub use maxmind::{MaxMindDatabase, MaxMindError, MaxMindIntel};
13
14/// What an [`IpIntel`] source knows about an address.
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct IpInfo {
18    /// ISO 3166-1 alpha-2 country code.
19    pub country: Option<String>,
20    /// Autonomous system number.
21    pub asn: Option<u32>,
22    /// Whether the address belongs to a VPN, proxy, hosting provider or Tor. `None` when the
23    /// source does not know.
24    pub anonymity: Option<Anonymity>,
25    /// The city's English name, e.g. from a `MaxMind` City database. For display; not scored.
26    pub city: Option<String>,
27    /// The name of the organization behind the autonomous system, e.g. `"Example Telecom"`.
28    /// For display; not scored.
29    pub network: Option<String>,
30}
31
32impl IpInfo {
33    /// Builds an `IpInfo` without anonymity, city or network information.
34    pub fn new(country: Option<String>, asn: Option<u32>) -> Self {
35        Self {
36            country,
37            asn,
38            ..Self::default()
39        }
40    }
41
42    /// Sets what the source knows about the network hiding the client.
43    #[must_use]
44    pub fn with_anonymity(mut self, anonymity: Anonymity) -> Self {
45        self.anonymity = Some(anonymity);
46        self
47    }
48
49    /// Sets the city's English name.
50    #[must_use]
51    pub fn with_city(mut self, city: impl Into<String>) -> Self {
52        self.city = Some(city.into());
53        self
54    }
55
56    /// Sets the name of the organization behind the autonomous system.
57    #[must_use]
58    pub fn with_network(mut self, network: impl Into<String>) -> Self {
59        self.network = Some(network.into());
60        self
61    }
62}
63
64/// The kind of network an address belongs to, as far as hiding the client goes.
65///
66/// Moving from [`Direct`](Self::Direct) onto an anonymizer fires
67/// [`Check::AnonymizerEntered`](crate::Check::AnonymizerEntered), moving onto Tor or an open
68/// proxy fires [`Check::TorOrProxyEntered`](crate::Check::TorOrProxyEntered).
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71#[non_exhaustive]
72pub enum Anonymity {
73    /// Looked up and not on a known anonymizer.
74    Direct,
75    /// A commercial VPN.
76    Vpn,
77    /// A hosting or cloud provider, where self-hosted VPNs and scripts run.
78    HostingProvider,
79    /// A residential proxy: traffic relayed through home connections.
80    ResidentialProxy,
81    /// An open proxy anyone can use.
82    PublicProxy,
83    /// A Tor exit node.
84    TorExitNode,
85}
86
87/// A geolocation / ASN source. Without one, IP changes are judged by network prefix only.
88///
89/// Lookups run on the request path and must be fast and non-blocking. With the `maxmind`
90/// feature, `MaxMindIntel` reads `GeoLite2` / `GeoIP2` databases held in memory.
91pub trait IpIntel: Send + Sync + 'static {
92    /// Looks up `ip`. Return empty fields for unknown addresses; never panic.
93    fn lookup(&self, ip: IpAddr) -> IpInfo;
94}
95
96/// The default source: knows nothing.
97#[derive(Clone, Copy, Debug, Default)]
98pub struct NoIpIntel;
99
100impl IpIntel for NoIpIntel {
101    fn lookup(&self, _ip: IpAddr) -> IpInfo {
102        IpInfo::default()
103    }
104}
105
106impl<F> IpIntel for F
107where
108    F: Fn(IpAddr) -> IpInfo + Send + Sync + 'static,
109{
110    fn lookup(&self, ip: IpAddr) -> IpInfo {
111        self(ip)
112    }
113}
114
115/// Lets the guard share a source the app keeps a handle to, for example to reload `MaxMind`
116/// databases.
117impl<T: IpIntel + ?Sized> IpIntel for std::sync::Arc<T> {
118    fn lookup(&self, ip: IpAddr) -> IpInfo {
119        (**self).lookup(ip)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn closures_are_sources() {
129        let intel = |_ip: IpAddr| IpInfo::new(Some("BE".into()), Some(5432));
130        assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).asn, Some(5432));
131        assert_eq!(NoIpIntel.lookup("1.2.3.4".parse().unwrap()), IpInfo::default());
132    }
133
134    #[test]
135    fn builders_set_city_and_network() {
136        let info = IpInfo::new(Some("BE".into()), Some(5432))
137            .with_city("Brussels")
138            .with_network(String::from("Example Telecom"));
139        assert_eq!(info.city.as_deref(), Some("Brussels"));
140        assert_eq!(info.network.as_deref(), Some("Example Telecom"));
141        assert_eq!(info.country.as_deref(), Some("BE"));
142        assert_eq!(info.asn, Some(5432));
143        assert_eq!(IpInfo::new(None, None).city, None);
144        assert_eq!(IpInfo::new(None, None).network, None);
145    }
146
147    #[test]
148    fn shared_sources_are_sources() {
149        fn source(intel: impl IpIntel) -> impl IpIntel {
150            intel
151        }
152        #[derive(Debug)]
153        struct Fixed;
154        impl IpIntel for Fixed {
155            fn lookup(&self, _ip: IpAddr) -> IpInfo {
156                IpInfo::new(Some("BE".into()), None)
157            }
158        }
159        let shared = std::sync::Arc::new(Fixed);
160        let intel = source(std::sync::Arc::clone(&shared));
161        assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).country.as_deref(), Some("BE"));
162    }
163}