LCOV - code coverage report
Current view: top level - proxy/src/auth - credentials.rs (source / functions) Coverage Total Hit
Test: 8ff8efadb0253cf618c612650348666c0c564111.info Lines: 95.8 % 380 364
Test Date: 2024-11-20 17:53:50 Functions: 87.5 % 40 35

            Line data    Source code
       1              : //! User credentials used in authentication.
       2              : 
       3              : use std::collections::HashSet;
       4              : use std::net::IpAddr;
       5              : use std::str::FromStr;
       6              : 
       7              : use itertools::Itertools;
       8              : use pq_proto::StartupMessageParams;
       9              : use thiserror::Error;
      10              : use tracing::{debug, warn};
      11              : 
      12              : use crate::auth::password_hack::parse_endpoint_param;
      13              : use crate::context::RequestContext;
      14              : use crate::error::{ReportableError, UserFacingError};
      15              : use crate::metrics::{Metrics, SniKind};
      16              : use crate::proxy::NeonOptions;
      17              : use crate::serverless::SERVERLESS_DRIVER_SNI;
      18              : use crate::types::{EndpointId, RoleName};
      19              : 
      20            0 : #[derive(Debug, Error, PartialEq, Eq, Clone)]
      21              : pub(crate) enum ComputeUserInfoParseError {
      22              :     #[error("Parameter '{0}' is missing in startup packet.")]
      23              :     MissingKey(&'static str),
      24              : 
      25              :     #[error(
      26              :         "Inconsistent project name inferred from \
      27              :          SNI ('{}') and project option ('{}').",
      28              :         .domain, .option,
      29              :     )]
      30              :     InconsistentProjectNames {
      31              :         domain: EndpointId,
      32              :         option: EndpointId,
      33              :     },
      34              : 
      35              :     #[error(
      36              :         "Common name inferred from SNI ('{}') is not known",
      37              :         .cn,
      38              :     )]
      39              :     UnknownCommonName { cn: String },
      40              : 
      41              :     #[error("Project name ('{0}') must contain only alphanumeric characters and hyphen.")]
      42              :     MalformedProjectName(EndpointId),
      43              : }
      44              : 
      45              : impl UserFacingError for ComputeUserInfoParseError {}
      46              : 
      47              : impl ReportableError for ComputeUserInfoParseError {
      48            0 :     fn get_error_kind(&self) -> crate::error::ErrorKind {
      49            0 :         crate::error::ErrorKind::User
      50            0 :     }
      51              : }
      52              : 
      53              : /// Various client credentials which we use for authentication.
      54              : /// Note that we don't store any kind of client key or password here.
      55              : #[derive(Debug, Clone, PartialEq, Eq)]
      56              : pub(crate) struct ComputeUserInfoMaybeEndpoint {
      57              :     pub(crate) user: RoleName,
      58              :     pub(crate) endpoint_id: Option<EndpointId>,
      59              :     pub(crate) options: NeonOptions,
      60              : }
      61              : 
      62              : impl ComputeUserInfoMaybeEndpoint {
      63              :     #[inline]
      64            0 :     pub(crate) fn endpoint(&self) -> Option<&str> {
      65            0 :         self.endpoint_id.as_deref()
      66            0 :     }
      67              : }
      68              : 
      69           27 : pub(crate) fn endpoint_sni(
      70           27 :     sni: &str,
      71           27 :     common_names: &HashSet<String>,
      72           27 : ) -> Result<Option<EndpointId>, ComputeUserInfoParseError> {
      73           27 :     let Some((subdomain, common_name)) = sni.split_once('.') else {
      74            0 :         return Err(ComputeUserInfoParseError::UnknownCommonName { cn: sni.into() });
      75              :     };
      76           27 :     if !common_names.contains(common_name) {
      77            1 :         return Err(ComputeUserInfoParseError::UnknownCommonName {
      78            1 :             cn: common_name.into(),
      79            1 :         });
      80           26 :     }
      81           26 :     if subdomain == SERVERLESS_DRIVER_SNI {
      82            0 :         return Ok(None);
      83           26 :     }
      84           26 :     Ok(Some(EndpointId::from(subdomain)))
      85           27 : }
      86              : 
      87              : impl ComputeUserInfoMaybeEndpoint {
      88           13 :     pub(crate) fn parse(
      89           13 :         ctx: &RequestContext,
      90           13 :         params: &StartupMessageParams,
      91           13 :         sni: Option<&str>,
      92           13 :         common_names: Option<&HashSet<String>>,
      93           13 :     ) -> Result<Self, ComputeUserInfoParseError> {
      94           13 :         // Some parameters are stored in the startup message.
      95           13 :         let get_param = |key| {
      96           13 :             params
      97           13 :                 .get(key)
      98           13 :                 .ok_or(ComputeUserInfoParseError::MissingKey(key))
      99           13 :         };
     100           13 :         let user: RoleName = get_param("user")?.into();
     101           13 : 
     102           13 :         // Project name might be passed via PG's command-line options.
     103           13 :         let endpoint_option = params
     104           13 :             .options_raw()
     105           13 :             .and_then(|options| {
     106            7 :                 // We support both `project` (deprecated) and `endpoint` options for backward compatibility.
     107            7 :                 // However, if both are present, we don't exactly know which one to use.
     108            7 :                 // Therefore we require that only one of them is present.
     109            7 :                 options
     110            7 :                     .filter_map(parse_endpoint_param)
     111            7 :                     .at_most_one()
     112            7 :                     .ok()?
     113           13 :             })
     114           13 :             .map(|name| name.into());
     115              : 
     116           13 :         let endpoint_from_domain = if let Some(sni_str) = sni {
     117            7 :             if let Some(cn) = common_names {
     118            7 :                 endpoint_sni(sni_str, cn)?
     119              :             } else {
     120            0 :                 None
     121              :             }
     122              :         } else {
     123            6 :             None
     124              :         };
     125              : 
     126           12 :         let endpoint = match (endpoint_option, endpoint_from_domain) {
     127              :             // Invariant: if we have both project name variants, they should match.
     128            2 :             (Some(option), Some(domain)) if option != domain => {
     129            1 :                 Some(Err(ComputeUserInfoParseError::InconsistentProjectNames {
     130            1 :                     domain,
     131            1 :                     option,
     132            1 :                 }))
     133              :             }
     134              :             // Invariant: project name may not contain certain characters.
     135           11 :             (a, b) => a.or(b).map(|name| {
     136            7 :                 if project_name_valid(name.as_ref()) {
     137            7 :                     Ok(name)
     138              :                 } else {
     139            0 :                     Err(ComputeUserInfoParseError::MalformedProjectName(name))
     140              :                 }
     141           11 :             }),
     142              :         }
     143           12 :         .transpose()?;
     144              : 
     145           11 :         if let Some(ep) = &endpoint {
     146            7 :             ctx.set_endpoint_id(ep.clone());
     147            7 :         }
     148              : 
     149           11 :         let metrics = Metrics::get();
     150           11 :         debug!(%user, "credentials");
     151           11 :         if sni.is_some() {
     152            5 :             debug!("Connection with sni");
     153            5 :             metrics.proxy.accepted_connections_by_sni.inc(SniKind::Sni);
     154            6 :         } else if endpoint.is_some() {
     155            2 :             metrics
     156            2 :                 .proxy
     157            2 :                 .accepted_connections_by_sni
     158            2 :                 .inc(SniKind::NoSni);
     159            2 :             debug!("Connection without sni");
     160              :         } else {
     161            4 :             metrics
     162            4 :                 .proxy
     163            4 :                 .accepted_connections_by_sni
     164            4 :                 .inc(SniKind::PasswordHack);
     165            4 :             debug!("Connection with password hack");
     166              :         }
     167              : 
     168           11 :         let options = NeonOptions::parse_params(params);
     169           11 : 
     170           11 :         Ok(Self {
     171           11 :             user,
     172           11 :             endpoint_id: endpoint,
     173           11 :             options,
     174           11 :         })
     175           13 :     }
     176              : }
     177              : 
     178           10 : pub(crate) fn check_peer_addr_is_in_list(peer_addr: &IpAddr, ip_list: &[IpPattern]) -> bool {
     179           10 :     ip_list.is_empty() || ip_list.iter().any(|pattern| check_ip(peer_addr, pattern))
     180           10 : }
     181              : 
     182              : #[derive(Debug, Clone, Eq, PartialEq)]
     183              : pub(crate) enum IpPattern {
     184              :     Subnet(ipnet::IpNet),
     185              :     Range(IpAddr, IpAddr),
     186              :     Single(IpAddr),
     187              :     None,
     188              : }
     189              : 
     190              : impl<'de> serde::de::Deserialize<'de> for IpPattern {
     191            9 :     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     192            9 :     where
     193            9 :         D: serde::Deserializer<'de>,
     194            9 :     {
     195              :         struct StrVisitor;
     196              :         impl serde::de::Visitor<'_> for StrVisitor {
     197              :             type Value = IpPattern;
     198              : 
     199            0 :             fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     200            0 :                 write!(formatter, "comma separated list with ip address, ip address range, or ip address subnet mask")
     201            0 :             }
     202              : 
     203            9 :             fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
     204            9 :             where
     205            9 :                 E: serde::de::Error,
     206            9 :             {
     207            9 :                 Ok(parse_ip_pattern(v).unwrap_or_else(|e| {
     208            1 :                     warn!("Cannot parse ip pattern {v}: {e}");
     209            1 :                     IpPattern::None
     210            9 :                 }))
     211            9 :             }
     212              :         }
     213            9 :         deserializer.deserialize_str(StrVisitor)
     214            9 :     }
     215              : }
     216              : 
     217              : impl FromStr for IpPattern {
     218              :     type Err = anyhow::Error;
     219              : 
     220            6 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     221            6 :         parse_ip_pattern(s)
     222            6 :     }
     223              : }
     224              : 
     225           23 : fn parse_ip_pattern(pattern: &str) -> anyhow::Result<IpPattern> {
     226           23 :     if pattern.contains('/') {
     227            2 :         let subnet: ipnet::IpNet = pattern.parse()?;
     228            1 :         return Ok(IpPattern::Subnet(subnet));
     229           21 :     }
     230           21 :     if let Some((start, end)) = pattern.split_once('-') {
     231            3 :         let start: IpAddr = start.parse()?;
     232            2 :         let end: IpAddr = end.parse()?;
     233            1 :         return Ok(IpPattern::Range(start, end));
     234           18 :     }
     235           18 :     let addr: IpAddr = pattern.parse()?;
     236           15 :     Ok(IpPattern::Single(addr))
     237           23 : }
     238              : 
     239           16 : fn check_ip(ip: &IpAddr, pattern: &IpPattern) -> bool {
     240           16 :     match pattern {
     241            3 :         IpPattern::Subnet(subnet) => subnet.contains(ip),
     242            5 :         IpPattern::Range(start, end) => start <= ip && ip <= end,
     243            7 :         IpPattern::Single(addr) => addr == ip,
     244            1 :         IpPattern::None => false,
     245              :     }
     246           16 : }
     247              : 
     248            7 : fn project_name_valid(name: &str) -> bool {
     249           23 :     name.chars().all(|c| c.is_alphanumeric() || c == '-')
     250            7 : }
     251              : 
     252              : #[cfg(test)]
     253              : mod tests {
     254              :     use serde_json::json;
     255              :     use ComputeUserInfoParseError::*;
     256              : 
     257              :     use super::*;
     258              : 
     259              :     #[test]
     260            1 :     fn parse_bare_minimum() -> anyhow::Result<()> {
     261            1 :         // According to postgresql, only `user` should be required.
     262            1 :         let options = StartupMessageParams::new([("user", "john_doe")]);
     263            1 :         let ctx = RequestContext::test();
     264            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     265            1 :         assert_eq!(user_info.user, "john_doe");
     266            1 :         assert_eq!(user_info.endpoint_id, None);
     267              : 
     268            1 :         Ok(())
     269            1 :     }
     270              : 
     271              :     #[test]
     272            1 :     fn parse_excessive() -> anyhow::Result<()> {
     273            1 :         let options = StartupMessageParams::new([
     274            1 :             ("user", "john_doe"),
     275            1 :             ("database", "world"), // should be ignored
     276            1 :             ("foo", "bar"),        // should be ignored
     277            1 :         ]);
     278            1 :         let ctx = RequestContext::test();
     279            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     280            1 :         assert_eq!(user_info.user, "john_doe");
     281            1 :         assert_eq!(user_info.endpoint_id, None);
     282              : 
     283            1 :         Ok(())
     284            1 :     }
     285              : 
     286              :     #[test]
     287            1 :     fn parse_project_from_sni() -> anyhow::Result<()> {
     288            1 :         let options = StartupMessageParams::new([("user", "john_doe")]);
     289            1 : 
     290            1 :         let sni = Some("foo.localhost");
     291            1 :         let common_names = Some(["localhost".into()].into());
     292            1 : 
     293            1 :         let ctx = RequestContext::test();
     294            1 :         let user_info =
     295            1 :             ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())?;
     296            1 :         assert_eq!(user_info.user, "john_doe");
     297            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("foo"));
     298            1 :         assert_eq!(user_info.options.get_cache_key("foo"), "foo");
     299              : 
     300            1 :         Ok(())
     301            1 :     }
     302              : 
     303              :     #[test]
     304            1 :     fn parse_project_from_options() -> anyhow::Result<()> {
     305            1 :         let options = StartupMessageParams::new([
     306            1 :             ("user", "john_doe"),
     307            1 :             ("options", "-ckey=1 project=bar -c geqo=off"),
     308            1 :         ]);
     309            1 : 
     310            1 :         let ctx = RequestContext::test();
     311            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     312            1 :         assert_eq!(user_info.user, "john_doe");
     313            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("bar"));
     314              : 
     315            1 :         Ok(())
     316            1 :     }
     317              : 
     318              :     #[test]
     319            1 :     fn parse_endpoint_from_options() -> anyhow::Result<()> {
     320            1 :         let options = StartupMessageParams::new([
     321            1 :             ("user", "john_doe"),
     322            1 :             ("options", "-ckey=1 endpoint=bar -c geqo=off"),
     323            1 :         ]);
     324            1 : 
     325            1 :         let ctx = RequestContext::test();
     326            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     327            1 :         assert_eq!(user_info.user, "john_doe");
     328            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("bar"));
     329              : 
     330            1 :         Ok(())
     331            1 :     }
     332              : 
     333              :     #[test]
     334            1 :     fn parse_three_endpoints_from_options() -> anyhow::Result<()> {
     335            1 :         let options = StartupMessageParams::new([
     336            1 :             ("user", "john_doe"),
     337            1 :             (
     338            1 :                 "options",
     339            1 :                 "-ckey=1 endpoint=one endpoint=two endpoint=three -c geqo=off",
     340            1 :             ),
     341            1 :         ]);
     342            1 : 
     343            1 :         let ctx = RequestContext::test();
     344            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     345            1 :         assert_eq!(user_info.user, "john_doe");
     346            1 :         assert!(user_info.endpoint_id.is_none());
     347              : 
     348            1 :         Ok(())
     349            1 :     }
     350              : 
     351              :     #[test]
     352            1 :     fn parse_when_endpoint_and_project_are_in_options() -> anyhow::Result<()> {
     353            1 :         let options = StartupMessageParams::new([
     354            1 :             ("user", "john_doe"),
     355            1 :             ("options", "-ckey=1 endpoint=bar project=foo -c geqo=off"),
     356            1 :         ]);
     357            1 : 
     358            1 :         let ctx = RequestContext::test();
     359            1 :         let user_info = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, None, None)?;
     360            1 :         assert_eq!(user_info.user, "john_doe");
     361            1 :         assert!(user_info.endpoint_id.is_none());
     362              : 
     363            1 :         Ok(())
     364            1 :     }
     365              : 
     366              :     #[test]
     367            1 :     fn parse_projects_identical() -> anyhow::Result<()> {
     368            1 :         let options = StartupMessageParams::new([("user", "john_doe"), ("options", "project=baz")]);
     369            1 : 
     370            1 :         let sni = Some("baz.localhost");
     371            1 :         let common_names = Some(["localhost".into()].into());
     372            1 : 
     373            1 :         let ctx = RequestContext::test();
     374            1 :         let user_info =
     375            1 :             ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())?;
     376            1 :         assert_eq!(user_info.user, "john_doe");
     377            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("baz"));
     378              : 
     379            1 :         Ok(())
     380            1 :     }
     381              : 
     382              :     #[test]
     383            1 :     fn parse_multi_common_names() -> anyhow::Result<()> {
     384            1 :         let options = StartupMessageParams::new([("user", "john_doe")]);
     385            1 : 
     386            1 :         let common_names = Some(["a.com".into(), "b.com".into()].into());
     387            1 :         let sni = Some("p1.a.com");
     388            1 :         let ctx = RequestContext::test();
     389            1 :         let user_info =
     390            1 :             ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())?;
     391            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("p1"));
     392              : 
     393            1 :         let common_names = Some(["a.com".into(), "b.com".into()].into());
     394            1 :         let sni = Some("p1.b.com");
     395            1 :         let ctx = RequestContext::test();
     396            1 :         let user_info =
     397            1 :             ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())?;
     398            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("p1"));
     399              : 
     400            1 :         Ok(())
     401            1 :     }
     402              : 
     403              :     #[test]
     404            1 :     fn parse_projects_different() {
     405            1 :         let options =
     406            1 :             StartupMessageParams::new([("user", "john_doe"), ("options", "project=first")]);
     407            1 : 
     408            1 :         let sni = Some("second.localhost");
     409            1 :         let common_names = Some(["localhost".into()].into());
     410            1 : 
     411            1 :         let ctx = RequestContext::test();
     412            1 :         let err = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())
     413            1 :             .expect_err("should fail");
     414            1 :         match err {
     415            1 :             InconsistentProjectNames { domain, option } => {
     416            1 :                 assert_eq!(option, "first");
     417            1 :                 assert_eq!(domain, "second");
     418              :             }
     419            0 :             _ => panic!("bad error: {err:?}"),
     420              :         }
     421            1 :     }
     422              : 
     423              :     #[test]
     424            1 :     fn parse_inconsistent_sni() {
     425            1 :         let options = StartupMessageParams::new([("user", "john_doe")]);
     426            1 : 
     427            1 :         let sni = Some("project.localhost");
     428            1 :         let common_names = Some(["example.com".into()].into());
     429            1 : 
     430            1 :         let ctx = RequestContext::test();
     431            1 :         let err = ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())
     432            1 :             .expect_err("should fail");
     433            1 :         match err {
     434            1 :             UnknownCommonName { cn } => {
     435            1 :                 assert_eq!(cn, "localhost");
     436              :             }
     437            0 :             _ => panic!("bad error: {err:?}"),
     438              :         }
     439            1 :     }
     440              : 
     441              :     #[test]
     442            1 :     fn parse_neon_options() -> anyhow::Result<()> {
     443            1 :         let options = StartupMessageParams::new([
     444            1 :             ("user", "john_doe"),
     445            1 :             ("options", "neon_lsn:0/2 neon_endpoint_type:read_write"),
     446            1 :         ]);
     447            1 : 
     448            1 :         let sni = Some("project.localhost");
     449            1 :         let common_names = Some(["localhost".into()].into());
     450            1 :         let ctx = RequestContext::test();
     451            1 :         let user_info =
     452            1 :             ComputeUserInfoMaybeEndpoint::parse(&ctx, &options, sni, common_names.as_ref())?;
     453            1 :         assert_eq!(user_info.endpoint_id.as_deref(), Some("project"));
     454            1 :         assert_eq!(
     455            1 :             user_info.options.get_cache_key("project"),
     456            1 :             "project endpoint_type:read_write lsn:0/2"
     457            1 :         );
     458              : 
     459            1 :         Ok(())
     460            1 :     }
     461              : 
     462              :     #[test]
     463            1 :     fn test_check_peer_addr_is_in_list() {
     464            4 :         fn check(v: serde_json::Value) -> bool {
     465            4 :             let peer_addr = IpAddr::from([127, 0, 0, 1]);
     466            4 :             let ip_list: Vec<IpPattern> = serde_json::from_value(v).unwrap();
     467            4 :             check_peer_addr_is_in_list(&peer_addr, &ip_list)
     468            4 :         }
     469              : 
     470            1 :         assert!(check(json!([])));
     471            1 :         assert!(check(json!(["127.0.0.1"])));
     472            1 :         assert!(!check(json!(["8.8.8.8"])));
     473              :         // If there is an incorrect address, it will be skipped.
     474            1 :         assert!(check(json!(["88.8.8", "127.0.0.1"])));
     475            1 :     }
     476              :     #[test]
     477            1 :     fn test_parse_ip_v4() -> anyhow::Result<()> {
     478            1 :         let peer_addr = IpAddr::from([127, 0, 0, 1]);
     479              :         // Ok
     480            1 :         assert_eq!(parse_ip_pattern("127.0.0.1")?, IpPattern::Single(peer_addr));
     481            1 :         assert_eq!(
     482            1 :             parse_ip_pattern("127.0.0.1/31")?,
     483            1 :             IpPattern::Subnet(ipnet::IpNet::new(peer_addr, 31)?)
     484              :         );
     485            1 :         assert_eq!(
     486            1 :             parse_ip_pattern("0.0.0.0-200.0.1.2")?,
     487            1 :             IpPattern::Range(IpAddr::from([0, 0, 0, 0]), IpAddr::from([200, 0, 1, 2]))
     488              :         );
     489              : 
     490              :         // Error
     491            1 :         assert!(parse_ip_pattern("300.0.1.2").is_err());
     492            1 :         assert!(parse_ip_pattern("30.1.2").is_err());
     493            1 :         assert!(parse_ip_pattern("127.0.0.1/33").is_err());
     494            1 :         assert!(parse_ip_pattern("127.0.0.1-127.0.3").is_err());
     495            1 :         assert!(parse_ip_pattern("1234.0.0.1-127.0.3.0").is_err());
     496            1 :         Ok(())
     497            1 :     }
     498              : 
     499              :     #[test]
     500            1 :     fn test_check_ipv4() -> anyhow::Result<()> {
     501            1 :         let peer_addr = IpAddr::from([127, 0, 0, 1]);
     502            1 :         let peer_addr_next = IpAddr::from([127, 0, 0, 2]);
     503            1 :         let peer_addr_prev = IpAddr::from([127, 0, 0, 0]);
     504            1 :         // Success
     505            1 :         assert!(check_ip(&peer_addr, &IpPattern::Single(peer_addr)));
     506            1 :         assert!(check_ip(
     507            1 :             &peer_addr,
     508            1 :             &IpPattern::Subnet(ipnet::IpNet::new(peer_addr_prev, 31)?)
     509              :         ));
     510            1 :         assert!(check_ip(
     511            1 :             &peer_addr,
     512            1 :             &IpPattern::Subnet(ipnet::IpNet::new(peer_addr_next, 30)?)
     513              :         ));
     514            1 :         assert!(check_ip(
     515            1 :             &peer_addr,
     516            1 :             &IpPattern::Range(IpAddr::from([0, 0, 0, 0]), IpAddr::from([200, 0, 1, 2]))
     517            1 :         ));
     518            1 :         assert!(check_ip(
     519            1 :             &peer_addr,
     520            1 :             &IpPattern::Range(peer_addr, peer_addr)
     521            1 :         ));
     522              : 
     523              :         // Not success
     524            1 :         assert!(!check_ip(&peer_addr, &IpPattern::Single(peer_addr_prev)));
     525            1 :         assert!(!check_ip(
     526            1 :             &peer_addr,
     527            1 :             &IpPattern::Subnet(ipnet::IpNet::new(peer_addr_next, 31)?)
     528              :         ));
     529            1 :         assert!(!check_ip(
     530            1 :             &peer_addr,
     531            1 :             &IpPattern::Range(IpAddr::from([0, 0, 0, 0]), peer_addr_prev)
     532            1 :         ));
     533            1 :         assert!(!check_ip(
     534            1 :             &peer_addr,
     535            1 :             &IpPattern::Range(peer_addr_next, IpAddr::from([128, 0, 0, 0]))
     536            1 :         ));
     537              :         // There is no check that for range start <= end. But it's fine as long as for all this cases the result is false.
     538            1 :         assert!(!check_ip(
     539            1 :             &peer_addr,
     540            1 :             &IpPattern::Range(peer_addr, peer_addr_prev)
     541            1 :         ));
     542            1 :         Ok(())
     543            1 :     }
     544              : 
     545              :     #[test]
     546            1 :     fn test_connection_blocker() {
     547            3 :         fn check(v: serde_json::Value) -> bool {
     548            3 :             let peer_addr = IpAddr::from([127, 0, 0, 1]);
     549            3 :             let ip_list: Vec<IpPattern> = serde_json::from_value(v).unwrap();
     550            3 :             check_peer_addr_is_in_list(&peer_addr, &ip_list)
     551            3 :         }
     552              : 
     553            1 :         assert!(check(json!([])));
     554            1 :         assert!(check(json!(["127.0.0.1"])));
     555            1 :         assert!(!check(json!(["255.255.255.255"])));
     556            1 :     }
     557              : }
        

Generated by: LCOV version 2.1-beta