feat: Implement net.cidr_contains builtin (#471)

Major changes:
- Implement the `net.cidr_contains` builtin
- Enable the v0 and v1 test for `net.cidr_contains`
- Add the `netip` crate to standardize CIDR searching and other
  operations

Key Concept:
- Allow users to leverage the `net.cidr_contains` builtin to check
  whether an IPv4 or IPv6 CIDR contains a specified IP address or
  subnet.

Testing:
- All tests passing.

Signed-off-by: tjons <tylerschade99@gmail.com>
This commit is contained in:
Tyler Schade
2025-09-05 16:21:41 -04:00
committed by GitHub
parent 85753aaf37
commit 1b0c2d4072
10 changed files with 149 additions and 5 deletions

View File

@@ -1,4 +1,6 @@
use core::net::IpAddr;
use ipnet::IpNet;
use std::format;
use std::sync::Arc;
use crate::ast::{Expr, Ref};
@@ -7,12 +9,13 @@ use crate::builtins::utils::ensure_args_count;
use crate::lexer::Span;
use crate::value::Value;
use anyhow::Result;
use anyhow::{anyhow, bail, Result};
use super::utils::ensure_string;
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("net.cidr_is_valid", (cidr_is_valid, 1));
m.insert("net.cidr_contains", (cidr_contains, 2));
}
/// Checks if a CIDR string is valid or invalid. Uses the
@@ -59,6 +62,50 @@ fn is_valid_cidr(cidr: Arc<str>) -> bool {
}
}
pub fn cidr_contains(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
ensure_args_count(span, "cidr_contains", params, args, 2)?;
let cidr = ensure_string("cidr_contains", &params[0], &args[0])?;
let cidr_or_ip = ensure_string("cidr_contains", &params[1], &args[1])?;
let contains = _cidr_contains(cidr, cidr_or_ip);
match contains {
Ok(r) => Ok(Value::from(r)),
// The rego implementation will retur an error in strict mode, see
// https://github.com/open-policy-agent/opa/blob/main/v1/test/cases/testdata/v1/netcidrcontains/test-netcidrcontains-0100.yaml
// as an example, so we will propagate the error if the builtin is
// run in strict mode.
Err(e) if strict => bail!(span.error(&format!("{e}"))),
// If not in strict mode, an error will result in Undefined.
_ => Ok(Value::Undefined),
}
}
fn _cidr_contains(cidr: Arc<str>, cidr_or_ip: Arc<str>) -> Result<bool> {
let net = cidr
.parse::<IpNet>()
.map_err(|e| anyhow!("Error parsing {cidr}: {e}"))?;
if cidr_or_ip.contains("/") {
let subnet = cidr_or_ip
.parse::<IpNet>()
.map_err(|e| anyhow!("Error parsing {cidr_or_ip} as CIDR: {e}"))?;
return Ok(net.contains(&subnet));
}
// if the caller did not provide a CIDR string, try to parse
// the input as an IP address.
let subnet = cidr_or_ip
.parse::<IpAddr>()
.map_err(|e| anyhow!("Error parsing {cidr_or_ip} as IP address: {e}"))?;
Ok(net.contains(&subnet))
}
#[cfg(test)]
mod net_tests {
use super::*;
@@ -83,4 +130,58 @@ mod net_tests {
);
}
}
#[test]
fn test_cidr_contains() {
let test_cases: std::vec::IntoIter<(Arc<str>, Arc<str>, bool, bool)> = Vec::from([
// Each case is a tuple of (cidr, cidr_or_ip, expected Ok(result), and expected error)
(
Arc::from("127.0.0.1/32"),
Arc::from("127.0.0.1"),
true,
false,
),
(
Arc::from("10.0.0.0/8"),
Arc::from("10.10.10.10"),
true,
false,
),
(
Arc::from("10.0.0.0/8"),
Arc::from("10.10.10.0/24"),
true,
false,
),
(Arc::from("fd00::/16"), Arc::from("fd00::/17"), true, false),
(
Arc::from("127.0.0.1/32"),
Arc::from("127.0.0.2"),
false,
false,
),
(Arc::from("10.0.0.0/8"), Arc::from("11.0.0.1"), false, false),
(Arc::from("fd00::/16"), Arc::from("fd00::/15"), false, false),
(
Arc::from("127.0.0.0/8"),
Arc::from("not a cidr"),
false,
true,
),
])
.into_iter();
for (cidr, sub, result, should_err) in test_cases {
let got = _cidr_contains(cidr.clone(), sub.clone());
match got {
Err(_) if should_err => continue,
Ok(res) if res == result => continue,
_ => {
panic!(
"Expected `cidr_contains` for cidr {cidr} and subnet {sub} to be {result}"
)
}
}
}
}
}