vrl/stdlib/
ip_cidr_contains.rs

1use crate::compiler::prelude::*;
2use cidr::IpCidr;
3use std::net::IpAddr;
4use std::str::FromStr;
5
6fn str_to_cidr(v: &str) -> Result<IpCidr, String> {
7    IpCidr::from_str(v).map_err(|err| format!("unable to parse CIDR: {err}"))
8}
9
10#[allow(clippy::result_large_err)]
11fn value_to_cidr(value: &Value) -> Result<IpCidr, function::Error> {
12    let str = &value.as_str().ok_or(function::Error::InvalidArgument {
13        keyword: "ip_cidr_contains",
14        value: value.clone(),
15        error: r#""cidr" must be string"#,
16    })?;
17
18    str_to_cidr(str).map_err(|_| function::Error::InvalidArgument {
19        keyword: "ip_cidr_contains",
20        value: value.clone(),
21        error: r#""cidr" must be valid cidr"#,
22    })
23}
24
25fn ip_cidr_contains(value: &Value, cidr: &Value) -> Resolved {
26    let bytes = value.try_bytes_utf8_lossy()?;
27    let ip_addr =
28        IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;
29
30    match cidr {
31        Value::Bytes(v) => {
32            let cidr = str_to_cidr(&String::from_utf8_lossy(v))?;
33            Ok(cidr.contains(&ip_addr).into())
34        }
35        Value::Array(vec) => {
36            for v in vec {
37                let cidr = str_to_cidr(&v.try_bytes_utf8_lossy()?)?;
38                if cidr.contains(&ip_addr) {
39                    return Ok(true.into());
40                }
41            }
42            Ok(false.into())
43        }
44        value => Err(ValueError::Expected {
45            got: value.kind(),
46            expected: Kind::bytes() | Kind::array(Collection::any()),
47        }
48        .into()),
49    }
50}
51
52fn ip_cidr_contains_constant(value: &Value, cidr_vec: &[IpCidr]) -> Resolved {
53    let bytes = value.try_bytes_utf8_lossy()?;
54    let ip_addr =
55        IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;
56
57    Ok(cidr_vec.iter().any(|cidr| cidr.contains(&ip_addr)).into())
58}
59
60#[derive(Clone, Copy, Debug)]
61pub struct IpCidrContains;
62
63impl Function for IpCidrContains {
64    fn identifier(&self) -> &'static str {
65        "ip_cidr_contains"
66    }
67
68    fn usage(&self) -> &'static str {
69        "Determines whether the `ip` is contained in the block referenced by the `cidr`."
70    }
71
72    fn category(&self) -> &'static str {
73        Category::Ip.as_ref()
74    }
75
76    fn internal_failure_reasons(&self) -> &'static [&'static str] {
77        &[
78            "`cidr` is not a valid CIDR.",
79            "`ip` is not a valid IP address.",
80        ]
81    }
82
83    fn return_kind(&self) -> u16 {
84        kind::BOOLEAN
85    }
86
87    fn parameters(&self) -> &'static [Parameter] {
88        const PARAMETERS: &[Parameter] = &[
89            Parameter::required(
90                "cidr",
91                kind::BYTES | kind::ARRAY,
92                "The CIDR mask (v4 or v6).",
93            ),
94            Parameter::required("value", kind::BYTES, "The IP address (v4 or v6)."),
95        ];
96        PARAMETERS
97    }
98
99    fn examples(&self) -> &'static [Example] {
100        &[
101            example! {
102                title: "IPv4 contains CIDR",
103                source: r#"ip_cidr_contains!("192.168.0.0/16", "192.168.10.32")"#,
104                result: Ok("true"),
105            },
106            example! {
107                title: "IPv4 is private",
108                source: r#"ip_cidr_contains!(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], "192.168.10.32")"#,
109                result: Ok("true"),
110            },
111            example! {
112                title: "IPv6 contains CIDR",
113                source: r#"ip_cidr_contains!("2001:4f8:4:ba::/64", "2001:4f8:4:ba:2e0:81ff:fe22:d1f1")"#,
114                result: Ok("true"),
115            },
116            example! {
117                title: "Not in range",
118                source: r#"ip_cidr_contains!("192.168.0.0/24", "192.168.10.32")"#,
119                result: Ok("false"),
120            },
121            example! {
122                title: "Invalid address",
123                source: r#"ip_cidr_contains!("192.168.0.0/24", "INVALID")"#,
124                result: Err(
125                    r#"function call error for "ip_cidr_contains" at (0:46): unable to parse IP address: invalid IP address syntax"#,
126                ),
127            },
128        ]
129    }
130
131    fn compile(
132        &self,
133        state: &state::TypeState,
134        _ctx: &mut FunctionCompileContext,
135        arguments: ArgumentList,
136    ) -> Compiled {
137        let cidr = arguments.required("cidr");
138
139        let cidr = match cidr.resolve_constant(state) {
140            None => IpCidrType::Expression(cidr),
141            Some(value) => IpCidrType::Constant(match value {
142                Value::Bytes(_) => vec![value_to_cidr(&value)?],
143                Value::Array(vec) => {
144                    let mut output = Vec::with_capacity(vec.len());
145                    for value in vec {
146                        output.push(value_to_cidr(&value)?);
147                    }
148                    output
149                }
150                _ => {
151                    return Err(function::Error::InvalidArgument {
152                        keyword: "ip_cidr_contains",
153                        value,
154                        error: r#""cidr" must be string or array of strings"#,
155                    }
156                    .into());
157                }
158            }),
159        };
160
161        let value = arguments.required("value");
162
163        Ok(IpCidrContainsFn { cidr, value }.as_expr())
164    }
165}
166
167#[derive(Debug, Clone)]
168enum IpCidrType {
169    Constant(Vec<IpCidr>),
170    Expression(Box<dyn Expression>),
171}
172
173#[derive(Debug, Clone)]
174struct IpCidrContainsFn {
175    cidr: IpCidrType,
176    value: Box<dyn Expression>,
177}
178
179impl FunctionExpression for IpCidrContainsFn {
180    fn resolve(&self, ctx: &mut Context) -> Resolved {
181        let value = self.value.resolve(ctx)?;
182
183        match &self.cidr {
184            IpCidrType::Constant(cidr_vec) => ip_cidr_contains_constant(&value, cidr_vec),
185            IpCidrType::Expression(exp) => {
186                let cidr = exp.resolve(ctx)?;
187                ip_cidr_contains(&value, &cidr)
188            }
189        }
190    }
191
192    fn type_def(&self, _: &state::TypeState) -> TypeDef {
193        TypeDef::boolean().fallible()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::value;
201
202    test_function! [
203        ip_cidr_contains => IpCidrContains;
204
205        ipv4_yes {
206            args: func_args![value: "192.168.10.32",
207                             cidr: "192.168.0.0/16",
208            ],
209            want: Ok(value!(true)),
210            tdef: TypeDef::boolean().fallible(),
211        }
212
213        ipv4_no {
214            args: func_args![value: "192.168.10.32",
215                             cidr: "192.168.0.0/24",
216            ],
217            want: Ok(value!(false)),
218            tdef: TypeDef::boolean().fallible(),
219        }
220
221        ipv4_yes_array {
222            args: func_args![value: "192.168.10.32",
223                             cidr: vec!["10.0.0.0/8", "192.168.0.0/16"],
224            ],
225            want: Ok(value!(true)),
226            tdef: TypeDef::boolean().fallible(),
227        }
228
229        ipv4_no_array {
230            args: func_args![value: "192.168.10.32",
231                             cidr: vec!["10.0.0.0/8", "192.168.0.0/24"],
232            ],
233            want: Ok(value!(false)),
234            tdef: TypeDef::boolean().fallible(),
235        }
236
237        ipv6_yes {
238            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
239                             cidr: "2001:4f8:3:ba::/64",
240            ],
241            want: Ok(value!(true)),
242            tdef: TypeDef::boolean().fallible(),
243        }
244
245        ipv6_no {
246            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
247                             cidr: "2001:4f8:4:ba::/64",
248            ],
249            want: Ok(value!(false)),
250            tdef: TypeDef::boolean().fallible(),
251        }
252
253        ipv6_yes_array {
254            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
255                             cidr: vec!["fc00::/7", "2001:4f8:3:ba::/64"],
256            ],
257            want: Ok(value!(true)),
258            tdef: TypeDef::boolean().fallible(),
259        }
260
261        ipv6_no_array {
262            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
263                             cidr: vec!["fc00::/7", "2001:4f8:4:ba::/64"],
264            ],
265            want: Ok(value!(false)),
266            tdef: TypeDef::boolean().fallible(),
267        }
268    ];
269}