Skip to main content

acme_proxy/filter/
ip_allow.rs

1//! The `allowed_ip` check: which networks may talk to the server.
2//!
3//! The bluntest and most reliable control available: it needs no DNS, no
4//! external service, and cannot be influenced by the client. On a CA serving one
5//! LAN it is usually the only check needed — and the one worth having whether
6//! or not `challenge.bypass` is set.
7//!
8//! ## Allow and deny
9//!
10//! Two lists, with the same semantics the `reverse_dns` and `identifiers`
11//! checks use: `deny` is checked first and wins, and an empty `allow` imposes
12//! no constraint. That gives three usable shapes —
13//!
14//! - `allow` only: a strict allowlist.
15//! - `deny` only: a blocklist, everything else served.
16//! - both: an allowlist with holes punched in it, e.g.
17//!   `allow = ["10.0.0.0/8"]` and `deny = ["10.1.2.0/24"]`.
18//!
19//! Deny-wins is plain set membership, not longest-prefix-match: a `/32` in
20//! `allow` does not beat a `/8` in `deny`. That matches the other two checks,
21//! and the surprising direction is the safe one.
22//!
23//! ## Both stages
24//!
25//! This check reads nothing but the client address, which
26//! [`IdentifierContext`] carries as well as [`ConnectionContext`], so it decides
27//! at either stage. That is not a detail: it is what lets a rule say
28//! `mgmt-net or inventory` and have the address half remain answerable at the
29//! point where the inventory is consulted.
30
31use std::net::IpAddr;
32
33use async_trait::async_trait;
34use ipnet::IpNet;
35use tracing::info;
36
37use super::policy::{Check, StageSet, Verdict};
38use super::{ConnectionContext, IdentifierContext, ListVerdict, check_lists, parse_nets};
39
40/// Resolved `[filter.check.<name>]` settings for `type = "allowed_ip"`.
41#[derive(Debug, Clone, Default)]
42pub struct Settings {
43    pub allow: Vec<String>,
44    pub deny: Vec<String>,
45}
46
47/// Accepts or refuses a request by the network its client address falls in.
48#[derive(Debug)]
49pub struct AllowedFromIpAddress {
50    allow: Vec<IpNet>,
51    deny: Vec<IpNet>,
52}
53
54impl AllowedFromIpAddress {
55    /// Parses the configured networks, failing startup on a bad entry.
56    ///
57    /// Both lists empty is rejected rather than honoured: an empty `allow`
58    /// imposes no constraint, so the check would accept everything, and an
59    /// operator who configured it did not mean to turn on something inert.
60    pub fn from_settings(name: &str, settings: &Settings) -> anyhow::Result<Self> {
61        if settings.allow.is_empty() && settings.deny.is_empty() {
62            anyhow::bail!(
63                "filter.check.{name} has neither allow nor deny entries, so it would accept \
64                 every request; list the permitted or refused networks, or drop the check"
65            );
66        }
67
68        let allow = parse_nets(&settings.allow, &format!("filter.check.{name}.allow"))?;
69        let deny = parse_nets(&settings.deny, &format!("filter.check.{name}.deny"))?;
70        info!(event = "filter_allowed_ip_loaded", outcome = "success", check = name, allow = ?settings.allow, deny = ?settings.deny);
71        Ok(Self { allow, deny })
72    }
73
74    /// The whole decision, shared by both hooks because it reads only the
75    /// address.
76    fn decide(&self, client_ip: Option<IpAddr>) -> Verdict {
77        // Fails closed on a missing address, in blocklist mode as much as
78        // allowlist mode: a blocklist that cannot identify the caller protects
79        // nothing. See `ConnectionContext::require_client_ip`.
80        let client_ip = match super::require_client_ip(client_ip) {
81            Ok(client_ip) => client_ip,
82            Err(verdict) => return verdict,
83        };
84
85        // The two details differ so a refused client can tell which list bit.
86        match check_lists(&self.allow, &self.deny, |net| net.contains(&client_ip)) {
87            ListVerdict::Permitted => Verdict::Pass,
88            ListVerdict::Denied => Verdict::Fail(format!("address {client_ip} is denied")),
89            ListVerdict::NotAllowed => Verdict::Fail(format!("address {client_ip} is not allowed")),
90        }
91    }
92}
93
94#[async_trait]
95impl Check for AllowedFromIpAddress {
96    fn kind(&self) -> &'static str {
97        "allowed_ip"
98    }
99
100    fn stages(&self) -> StageSet {
101        StageSet::both()
102    }
103
104    async fn check_connection(&self, context: &ConnectionContext<'_>) -> Verdict {
105        self.decide(context.client_ip)
106    }
107
108    async fn check_identifiers(&self, context: &IdentifierContext<'_>) -> Verdict {
109        self.decide(context.client_ip)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::filter::IdentifierStage;
117    use axum::http::Method;
118
119    fn settings(allow: &[&str], deny: &[&str]) -> Settings {
120        Settings {
121            allow: allow.iter().map(std::string::ToString::to_string).collect(),
122            deny: deny.iter().map(std::string::ToString::to_string).collect(),
123        }
124    }
125
126    /// Allowlist-only check.
127    fn check(allow: &[&str]) -> AllowedFromIpAddress {
128        AllowedFromIpAddress::from_settings("net", &settings(allow, &[])).unwrap()
129    }
130
131    /// Check with both lists.
132    fn check_with(allow: &[&str], deny: &[&str]) -> AllowedFromIpAddress {
133        AllowedFromIpAddress::from_settings("net", &settings(allow, deny)).unwrap()
134    }
135
136    fn assert_failed(verdict: Verdict, needle: &str) {
137        match verdict {
138            Verdict::Fail(detail) => {
139                assert!(detail.contains(needle), "{detail:?} lacks {needle:?}");
140            }
141            other => panic!("expected Fail, got {other:?}"),
142        }
143    }
144
145    async fn connection(check: &AllowedFromIpAddress, ip: Option<&str>) -> Verdict {
146        let client_ip: Option<IpAddr> = ip.map(|value| value.parse().unwrap());
147        check
148            .check_connection(&ConnectionContext {
149                client_ip,
150                method: &Method::POST,
151                path: "/newOrder",
152            })
153            .await
154    }
155
156    #[tokio::test]
157    async fn allows_an_address_inside_an_ipv4_cidr() {
158        let check = check(&["192.168.1.0/24"]);
159        assert_eq!(connection(&check, Some("192.168.1.5")).await, Verdict::Pass);
160        assert_eq!(
161            connection(&check, Some("192.168.1.255")).await,
162            Verdict::Pass
163        );
164    }
165
166    #[tokio::test]
167    async fn denies_an_address_outside_every_network() {
168        let check = check(&["192.168.1.0/24", "10.0.0.0/8"]);
169        assert_failed(
170            connection(&check, Some("203.0.113.9")).await,
171            "203.0.113.9 is not allowed",
172        );
173    }
174
175    #[tokio::test]
176    async fn allows_an_address_inside_an_ipv6_cidr() {
177        let check = check(&["fd00::/8"]);
178        assert_eq!(connection(&check, Some("fd00::1")).await, Verdict::Pass);
179        assert_ne!(connection(&check, Some("2001:db8::1")).await, Verdict::Pass);
180    }
181
182    #[tokio::test]
183    async fn a_bare_address_entry_is_a_host_route() {
184        let check = check(&["203.0.113.7"]);
185        assert_eq!(connection(&check, Some("203.0.113.7")).await, Verdict::Pass);
186        assert_ne!(connection(&check, Some("203.0.113.8")).await, Verdict::Pass);
187    }
188
189    #[tokio::test]
190    async fn an_ipv4_mapped_client_matches_a_v4_rule() {
191        // The dual-stack `[::]:3000` default reports IPv4 clients this way.
192        let check = check(&["192.168.1.0/24"]);
193        assert_eq!(
194            connection(&check, Some("::ffff:192.168.1.5")).await,
195            Verdict::Pass
196        );
197    }
198
199    #[tokio::test]
200    async fn a_missing_client_address_is_denied() {
201        let check = check(&["192.168.1.0/24"]);
202        assert_failed(connection(&check, None).await, "unavailable");
203    }
204
205    /// Fail-closed applies in blocklist mode too: an address the server cannot
206    /// see is not "absent from the deny list, therefore fine".
207    #[tokio::test]
208    async fn a_missing_client_address_is_denied_in_blocklist_mode() {
209        let check = check_with(&[], &["203.0.113.9"]);
210        assert_failed(connection(&check, None).await, "unavailable");
211    }
212
213    /// A missing address is a *refusal*, not an unknown: the server saw a
214    /// request it cannot attribute, which is a decision about the client
215    /// rather than a failure to reach an authority. Pinned because
216    /// `Undecided` here would silently turn a 403 into a 500.
217    #[tokio::test]
218    async fn a_missing_client_address_is_a_refusal_not_an_unknown() {
219        let check = check(&["192.168.1.0/24"]);
220        assert!(matches!(connection(&check, None).await, Verdict::Fail(_)));
221    }
222
223    #[tokio::test]
224    async fn a_deny_only_config_is_a_blocklist() {
225        let check = check_with(&[], &["203.0.113.9", "10.0.0.0/8"]);
226
227        assert_failed(
228            connection(&check, Some("203.0.113.9")).await,
229            "203.0.113.9 is denied",
230        );
231        assert_failed(
232            connection(&check, Some("10.4.5.6")).await,
233            "10.4.5.6 is denied",
234        );
235        // Anything not listed is served.
236        assert_eq!(connection(&check, Some("192.168.1.5")).await, Verdict::Pass);
237        assert_eq!(connection(&check, Some("2001:db8::1")).await, Verdict::Pass);
238    }
239
240    /// Deny wins, so a subnet can be allowed with holes punched in it.
241    #[tokio::test]
242    async fn deny_wins_over_allow() {
243        let check = check_with(&["10.0.0.0/8"], &["10.1.2.0/24"]);
244
245        assert_eq!(connection(&check, Some("10.0.0.5")).await, Verdict::Pass);
246        assert_failed(
247            connection(&check, Some("10.1.2.5")).await,
248            "10.1.2.5 is denied",
249        );
250        // Still outside the allow list entirely.
251        assert_failed(
252            connection(&check, Some("192.168.1.5")).await,
253            "192.168.1.5 is not allowed",
254        );
255    }
256
257    /// Plain set membership, not longest-prefix-match: a host route in `allow`
258    /// does not beat a wide block in `deny`.
259    #[tokio::test]
260    async fn a_more_specific_allow_does_not_beat_a_broader_deny() {
261        let check = check_with(&["10.1.2.3"], &["10.0.0.0/8"]);
262        assert_failed(
263            connection(&check, Some("10.1.2.3")).await,
264            "10.1.2.3 is denied",
265        );
266    }
267
268    #[tokio::test]
269    async fn deny_accepts_bare_addresses_and_ipv6() {
270        let check = check_with(&[], &["203.0.113.7", "2001:db8::/32"]);
271
272        assert_failed(connection(&check, Some("203.0.113.7")).await, "is denied");
273        assert_eq!(connection(&check, Some("203.0.113.8")).await, Verdict::Pass);
274        assert_failed(connection(&check, Some("2001:db8::1")).await, "is denied");
275        assert_eq!(connection(&check, Some("2001:dba::1")).await, Verdict::Pass);
276    }
277
278    /// A denied IPv4 client arriving over the dual-stack socket as
279    /// `::ffff:…` must still match a plain v4 deny entry.
280    #[tokio::test]
281    async fn an_ipv4_mapped_client_matches_a_v4_deny_rule() {
282        let check = check_with(&[], &["192.168.1.0/24"]);
283        assert_failed(
284            connection(&check, Some("::ffff:192.168.1.5")).await,
285            "192.168.1.5 is denied",
286        );
287    }
288
289    /// The property that makes `mgmt-net or inventory` writable: the same
290    /// answer at the stage where the inventory is consulted.
291    #[tokio::test]
292    async fn it_decides_the_same_way_at_the_identifier_stage() {
293        let check = check(&["10.0.0.0/8"]);
294        let identifiers = vec![crate::sqlite::order::Identifier::dns("example.com")];
295
296        let verdict = check
297            .check_identifiers(&IdentifierContext {
298                client_ip: Some("10.0.0.5".parse().unwrap()),
299                account_id: "acct",
300                stage: IdentifierStage::NewOrder,
301                identifiers: &identifiers,
302
303                eab: None,
304            })
305            .await;
306        assert_eq!(verdict, Verdict::Pass);
307
308        let verdict = check
309            .check_identifiers(&IdentifierContext {
310                client_ip: Some("203.0.113.9".parse().unwrap()),
311                account_id: "acct",
312                stage: IdentifierStage::NewOrder,
313                identifiers: &identifiers,
314
315                eab: None,
316            })
317            .await;
318        assert_failed(verdict, "is not allowed");
319    }
320
321    #[test]
322    fn both_lists_empty_is_a_startup_error() {
323        let error = AllowedFromIpAddress::from_settings("net", &Settings::default())
324            .unwrap_err()
325            .to_string();
326        assert!(error.contains("would accept every request"), "{error}");
327        assert!(error.contains("filter.check.net"), "{error}");
328    }
329
330    #[test]
331    fn a_bad_allow_network_is_a_startup_error() {
332        let error = AllowedFromIpAddress::from_settings("net", &settings(&["192.168.1.0/99"], &[]))
333            .unwrap_err()
334            .to_string();
335        assert!(error.contains("filter.check.net.allow"), "{error}");
336    }
337
338    #[test]
339    fn a_bad_deny_network_is_a_startup_error() {
340        let error = AllowedFromIpAddress::from_settings("net", &settings(&[], &["garbage"]))
341            .unwrap_err()
342            .to_string();
343        assert!(error.contains("filter.check.net.deny"), "{error}");
344    }
345
346    #[test]
347    fn reports_its_type_and_stages() {
348        let check = check(&["10.0.0.0/8"]);
349        assert_eq!(check.kind(), "allowed_ip");
350        assert_eq!(check.stages(), StageSet::both());
351    }
352}