1mod arguments;
4mod blocks;
5mod detector;
6mod effects;
7mod report;
8mod rule;
9mod window;
10
11use std::net::IpAddr;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::mpsc::{self, SyncSender};
15use std::time::Duration;
16
17use http::StatusCode;
18use sofie_core::{Clock, Error, Result, Uuid};
19use sofie_db::Db;
20
21pub use arguments::{ActiveBlock, ActiveBlocks, ShieldBlocks, SignIns};
22pub(crate) use detector::Safety;
23pub use rule::Thresholds;
24
25use self::blocks::{Blocks, Grade, Key};
26use self::detector::{Detector, Message, Signal};
27use self::effects::Effects;
28use self::rule::Rule;
29use crate::answer::Response;
30use crate::pipeline::Asked;
31use crate::throttle::{Limiter, Rate, Requester};
32use crate::trace::RouteLabel;
33
34const QUEUE: usize = 16_384;
36const SLOW_RATE: Rate = Rate::per_second(1);
37const WELL_KNOWN: [&str; 6] = [
39 "/favicon.ico",
40 "/robots.txt",
41 "/sitemap.xml",
42 "/.well-known/security.txt",
43 "/.well-known/change-password",
44 "/.well-known/traffic-advice",
45];
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum ShieldMode {
50 Enforce,
51 DryRun,
52 Off,
54}
55
56impl ShieldMode {
57 pub fn parse(text: &str) -> Result<Self> {
58 match text.trim() {
59 "on" => Ok(Self::Enforce),
60 "dry-run" => Ok(Self::DryRun),
61 "off" => Ok(Self::Off),
62 other => Err(Error::internal_message(format!(
63 "shield mode `{other}` is not `on`, `dry-run` or `off`"
64 ))),
65 }
66 }
67
68 fn of_build() -> Self {
70 if cfg!(debug_assertions) {
71 Self::DryRun
72 } else {
73 Self::Enforce
74 }
75 }
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct Shield {
81 mode: ShieldMode,
82 thresholds: Thresholds,
83 shared: bool,
84}
85
86impl Default for Shield {
87 fn default() -> Self {
88 Self::new(ShieldMode::of_build())
89 }
90}
91
92impl Shield {
93 #[must_use]
94 pub fn new(mode: ShieldMode) -> Self {
95 Self {
96 mode,
97 thresholds: Thresholds::default(),
98 shared: false,
99 }
100 }
101
102 #[must_use]
103 pub fn thresholds(self, thresholds: Thresholds) -> Self {
104 Self { thresholds, ..self }
105 }
106
107 #[must_use]
108 pub fn shared(self) -> Self {
109 Self {
110 shared: true,
111 ..self
112 }
113 }
114}
115
116#[derive(Clone, Default)]
117pub(crate) struct Guard(Option<Arc<Sentinel>>);
118
119pub(crate) struct Sentinel {
120 blocks: Arc<Blocks>,
121 sender: SyncSender<Message>,
122 dropped: Arc<AtomicU64>,
123 clock: Clock,
124 slowed: Limiter,
125 shared: Option<Db>,
126}
127
128#[derive(Clone, Copy)]
130struct Shielded;
131
132#[derive(Clone, Copy)]
133struct SignedIn(Uuid);
134
135impl Guard {
136 pub(crate) fn start(
137 app: &mut crate::App,
138 background: &mut crate::background::Background,
139 ) -> Result<Self> {
140 let shield = std::mem::take(&mut app.serving.shield);
141 if shield.mode == ShieldMode::Off {
142 return Ok(Self(None));
143 }
144 let areas: Vec<&str> = app.routes.iter().map(|route| route.audience.name).collect();
145 shield.thresholds.check_areas(&areas)?;
146 let runtime = tokio::runtime::Handle::try_current()
147 .map_err(|error| Error::internal("starting the shield outside a runtime", error))?;
148 let db = app.db.clone();
149 let effects = Effects {
150 runtime: runtime.clone(),
151 db: db.clone(),
152 members: app.members.clone(),
153 shared: shield.shared,
154 };
155 let dry_run = shield.mode == ShieldMode::DryRun;
156 let safety = Safety {
157 #[cfg(feature = "trusted-proxies")]
158 proxies: app.serving.proxies.clone(),
159 };
160 let detector = Detector::new(shield.thresholds, safety, dry_run, Some(effects));
161 let (blocks, dropped) = (Arc::clone(&detector.blocks), Arc::clone(&detector.dropped));
162 if shield.shared {
163 background.spawn(effects::follow(
164 db.clone(),
165 db.clock(),
166 Arc::downgrade(&blocks),
167 ));
168 }
169 let (sender, receiver) = mpsc::sync_channel(QUEUE);
170 detector::start(receiver, detector, db.clock())?;
171 Ok(Self(Some(Arc::new(Sentinel {
172 blocks,
173 sender,
174 dropped,
175 clock: db.clock(),
176 slowed: Limiter::new(SLOW_RATE),
177 shared: shield.shared.then_some(db),
178 }))))
179 }
180
181 pub(crate) fn refusal(&self, asked: Asked<'_>, principal: Option<Uuid>) -> Option<Response> {
184 let sentinel = self.0.as_ref()?;
185 if sentinel.blocks.is_empty() {
186 return None;
187 }
188 let now = sentinel.clock.now().timestamp();
189 let requester = Requester::of(principal, asked.client);
190 let address = Requester::of(None, asked.client);
191 let (requester, block) = sentinel
192 .blocks
193 .find(Key::Caller(requester), now.as_microsecond())
194 .map(|block| (requester, block))
195 .or_else(|| {
196 let block = sentinel
197 .blocks
198 .find(Key::Caller(address), now.as_microsecond())?;
199 matches!(block.rule, Rule::SignIn | Rule::SignUp).then_some((address, block))
200 })?;
201 let wait = match block.grade {
202 Grade::Slow => sentinel.slowed.admit(requester, now).err()?,
203 Grade::Block => Duration::from_micros(
204 u64::try_from(block.until - now.as_microsecond()).unwrap_or_default(),
205 ),
206 Grade::Observe | Grade::Lock => return None,
207 };
208 let mut response = crate::answer::failure(Error::TooManyRequests(wait), asked.caller);
209 response.extensions_mut().insert(Shielded);
210 Some(response)
211 }
212
213 pub(crate) fn observe(
214 &self,
215 asked: Asked<'_>,
216 route: Option<RouteLabel<'_>>,
217 response: &Response,
218 ) {
219 let Some(sentinel) = &self.0 else {
220 return;
221 };
222 let area = route.and_then(|route| route.area);
223 let Some(rule) = rule_of(response.status(), area.is_some()) else {
224 return;
225 };
226 if rule == Rule::Scan && is_well_known(asked.request.uri.path()) {
227 return;
228 }
229 if response.extensions().get::<Shielded>().is_some() {
230 return;
231 }
232 let principal = response
233 .extensions()
234 .get::<SignedIn>()
235 .map(|signed| signed.0);
236 let key = Key::Caller(Requester::of(principal, asked.client));
237 sentinel.send(rule, key, asked.client, area);
238 }
239
240 pub(crate) fn connection_failed(&self, peer: IpAddr) {
241 if let Some(sentinel) = &self.0 {
242 let key = Key::Caller(Requester::of(None, peer));
243 sentinel.send(Rule::Protocol, key, peer, None);
244 }
245 }
246
247 pub(crate) fn for_request(
248 &self,
249 asked: Asked<'_>,
250 area: &'static str,
251 ) -> Option<RequestShield> {
252 Some(RequestShield {
253 sentinel: Arc::clone(self.0.as_ref()?),
254 address: asked.client,
255 area,
256 })
257 }
258}
259
260impl Sentinel {
261 fn send(&self, rule: Rule, key: Key, address: IpAddr, area: Option<&'static str>) {
262 let signal = Signal {
263 rule,
264 key,
265 address,
266 area,
267 at: self.now(),
268 };
269 self.post(Message::Signal(signal));
270 }
271
272 fn post(&self, message: Message) {
273 if self.sender.try_send(message).is_err() {
274 self.dropped.fetch_add(1, Ordering::Relaxed);
275 }
276 }
277
278 fn now(&self) -> i64 {
279 self.clock.now().timestamp().as_microsecond()
280 }
281}
282
283#[derive(Clone)]
284pub(crate) struct RequestShield {
285 sentinel: Arc<Sentinel>,
286 address: IpAddr,
287 area: &'static str,
288}
289
290pub(crate) fn tag(mut response: Response, principal: Option<Uuid>) -> Response {
292 if let Some(principal) = principal
293 && rule_of(response.status(), true).is_some()
294 {
295 response.extensions_mut().insert(SignedIn(principal));
296 }
297 response
298}
299
300fn is_well_known(path: &str) -> bool {
302 WELL_KNOWN.contains(&path)
303 || path
304 .strip_prefix("/apple-touch-icon")
305 .and_then(|rest| rest.strip_suffix(".png"))
306 .is_some_and(|rest| !rest.contains('/'))
307}
308
309fn rule_of(status: StatusCode, matched: bool) -> Option<Rule> {
311 match status {
312 StatusCode::NOT_FOUND if matched => Some(Rule::Probe),
313 StatusCode::NOT_FOUND | StatusCode::METHOD_NOT_ALLOWED => Some(Rule::Scan),
314 StatusCode::FORBIDDEN => Some(Rule::Refused),
315 StatusCode::BAD_REQUEST | StatusCode::PAYLOAD_TOO_LARGE => Some(Rule::Malformed),
316 StatusCode::TOO_MANY_REQUESTS => Some(Rule::Throttled),
317 _ => None,
318 }
319}
320
321impl crate::Serving {
322 #[must_use]
323 pub fn shield(mut self, shield: Shield) -> Self {
324 self.shield = shield;
325 self
326 }
327}
328
329#[cfg(test)]
330mod tests;