1use std::{
2 borrow::Cow,
3 collections::HashSet,
4 net::{IpAddr, SocketAddr},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use crate::error::BootstrapError;
10use massa_logging::massa_trace;
11use parking_lot::RwLock;
12use tracing::{info, warn};
13
14use crate::tools::to_canonical;
15
16#[derive(Clone, Debug)]
19pub struct SharedWhiteBlackList<'a> {
20 inner: Arc<RwLock<WhiteBlackListInner>>,
21 white_path: Cow<'a, Path>,
22 black_path: Cow<'a, Path>,
23}
24
25impl SharedWhiteBlackList<'_> {
26 pub(crate) fn new(white_path: PathBuf, black_path: PathBuf) -> Result<Self, BootstrapError> {
27 let (white_list, black_list) = WhiteBlackListInner::init_list(&white_path, &black_path)?;
28 Ok(Self {
29 inner: Arc::new(RwLock::new(WhiteBlackListInner {
30 white_list,
31 black_list,
32 })),
33 white_path: Cow::from(white_path),
34 black_path: Cow::from(black_path),
35 })
36 }
37
38 pub fn get_white_list(&self) -> Option<HashSet<IpAddr>> {
40 self.inner.read().white_list.clone()
41 }
42
43 pub fn get_black_list(&self) -> Option<HashSet<IpAddr>> {
45 self.inner.read().black_list.clone()
46 }
47
48 pub fn add_ips_to_blacklist(&self, ips: Vec<IpAddr>) -> Result<(), BootstrapError> {
50 let ips = ips.into_iter().map(to_canonical).collect::<Vec<_>>();
55 let mut write_lock = self.inner.write();
56 if let Some(black_list) = &mut write_lock.black_list {
57 black_list.extend(ips);
58 } else {
59 write_lock.black_list = Some(HashSet::from_iter(ips));
60 };
61 self.write_to_file(&self.black_path, write_lock.black_list.as_ref().unwrap())?;
62 Ok(())
63 }
64
65 pub fn remove_ips_from_blacklist(&self, ips: Vec<IpAddr>) -> Result<(), BootstrapError> {
67 let ips = ips.into_iter().map(to_canonical).collect::<Vec<_>>();
68 let mut write_lock = self.inner.write();
69 if let Some(black_list) = &mut write_lock.black_list {
70 for ip in ips {
71 black_list.remove(&ip);
72 }
73 self.write_to_file(&self.black_path, black_list)?;
74 }
75 Ok(())
76 }
77
78 pub fn add_ips_to_whitelist(&self, ips: Vec<IpAddr>) -> Result<(), BootstrapError> {
80 let ips = ips.into_iter().map(to_canonical).collect::<Vec<_>>();
83 let mut write_lock = self.inner.write();
84 if let Some(white_list) = &mut write_lock.white_list {
85 white_list.extend(ips);
86 } else {
87 write_lock.white_list = Some(HashSet::from_iter(ips));
88 };
89 self.write_to_file(&self.white_path, write_lock.white_list.as_ref().unwrap())?;
90 Ok(())
91 }
92
93 pub fn remove_ips_from_whitelist(&self, ips: Vec<IpAddr>) -> Result<(), BootstrapError> {
95 let ips = ips.into_iter().map(to_canonical).collect::<Vec<_>>();
96 let mut write_lock = self.inner.write();
97 if let Some(white_list) = &mut write_lock.white_list {
98 for ip in ips {
99 white_list.remove(&ip);
100 }
101 self.write_to_file(&self.white_path, white_list)?;
102 }
103 Ok(())
104 }
105
106 fn write_to_file(
108 &self,
109 file_path: &Path,
110 data: &HashSet<IpAddr>,
111 ) -> Result<(), BootstrapError> {
112 let list = serde_json::to_string(data).map_err(|e| {
113 warn!(error = ?e, "failed to serialize list");
114 BootstrapError::SerializationError(e.to_string())
115 })?;
116 std::fs::write(file_path, list).map_err(|e| {
117 warn!(error = ?e, "failed to write list to file");
118 BootstrapError::IoError(e)
119 })?;
120 Ok(())
121 }
122
123 pub(crate) fn update(&mut self) -> Result<(), BootstrapError> {
126 let white_file = WhiteBlackListInner::read_list_file(&self.white_path);
129 let black_file = WhiteBlackListInner::read_list_file(&self.black_path);
130 let read_lock = self.inner.read();
131 let new_white_file =
132 WhiteBlackListInner::refresh_list(white_file, &read_lock.white_list, "whitelist")?;
133 let new_black_file =
134 WhiteBlackListInner::refresh_list(black_file, &read_lock.black_list, "blacklist")?;
135 let white_delta = new_white_file != read_lock.white_list;
136 let black_delta = new_black_file != read_lock.black_list;
137 if white_delta || black_delta {
138 let mut mut_inner = {
140 drop(read_lock);
141 self.inner.write()
142 };
143
144 if white_delta {
145 info!("whitelist has updated !");
146 mut_inner.white_list = new_white_file;
147 }
148 if black_delta {
149 info!("blacklist has updated !");
150 mut_inner.black_list = new_black_file;
151 }
152 }
153 Ok(())
154 }
155
156 pub(crate) fn is_ip_allowed(&self, remote_addr: &SocketAddr) -> Result<(), BootstrapError> {
157 let ip = to_canonical(remote_addr.ip());
158 let read = self.inner.read();
160 if let Some(ip_list) = &read.black_list {
161 if ip_list.contains(&ip) {
162 massa_trace!("bootstrap.lib.run.select.accept.refuse_blacklisted", {"remote_addr": remote_addr});
163 return Err(BootstrapError::BlackListed(ip.to_string()));
164 }
165 }
167 if let Some(ip_list) = &read.white_list {
168 if !ip_list.contains(&ip) {
169 massa_trace!("bootstrap.lib.run.select.accept.refuse_not_whitelisted", {"remote_addr": remote_addr});
170 return Err(BootstrapError::WhiteListed(ip.to_string()));
171 }
172 }
173 Ok(())
174 }
175}
176
177enum ListFileRead {
180 Missing,
182 Unreadable(std::io::Error),
184 Content(String),
186}
187
188impl WhiteBlackListInner {
189 #[allow(clippy::type_complexity)]
190 fn init_list(
191 whitelist_path: &Path,
192 blacklist_path: &Path,
193 ) -> Result<(Option<HashSet<IpAddr>>, Option<HashSet<IpAddr>>), BootstrapError> {
194 Ok((
195 Self::load_list(whitelist_path, "whitelist")?,
196 Self::load_list(blacklist_path, "blacklist")?,
197 ))
198 }
199
200 fn load_list(
203 list_path: &Path,
204 list_kind: &str,
205 ) -> Result<Option<HashSet<IpAddr>>, BootstrapError> {
206 match std::fs::read_to_string(list_path) {
207 Err(e) => {
208 warn!(
209 "error on load whitelist/blacklist file : {} | {}",
210 list_path.to_str().unwrap_or(" "),
211 e
212 );
213 Ok(None)
214 }
215 Ok(list) => Ok(Some(Self::parse_list(&list, list_kind)?)),
216 }
217 }
218
219 fn read_list_file(list_path: &Path) -> ListFileRead {
222 match std::fs::read_to_string(list_path) {
223 Ok(content) => ListFileRead::Content(content),
224 Err(e) if e.kind() == std::io::ErrorKind::NotFound => ListFileRead::Missing,
225 Err(e) => ListFileRead::Unreadable(e),
226 }
227 }
228
229 fn refresh_list(
236 file: ListFileRead,
237 current: &Option<HashSet<IpAddr>>,
238 list_kind: &str,
239 ) -> Result<Option<HashSet<IpAddr>>, BootstrapError> {
240 match file {
241 ListFileRead::Content(list) => Ok(Some(Self::parse_list(&list, list_kind)?)),
242 ListFileRead::Missing => {
243 if current.is_some() {
244 warn!(
245 "bootstrap {} file no longer exists: disabling the list",
246 list_kind
247 );
248 }
249 Ok(None)
250 }
251 ListFileRead::Unreadable(e) => {
252 warn!(
253 "failed to read bootstrap {} file: {} | keeping the previously loaded list",
254 list_kind, e
255 );
256 Ok(current.clone())
257 }
258 }
259 }
260
261 fn parse_list(list: &str, list_kind: &str) -> Result<HashSet<IpAddr>, BootstrapError> {
263 Ok(serde_json::from_str::<HashSet<IpAddr>>(list)
264 .map_err(|e| {
265 BootstrapError::InitListError(format!(
266 "Failed to parse bootstrap {} : {}",
267 list_kind, e
268 ))
269 })?
270 .into_iter()
271 .map(to_canonical)
272 .collect())
273 }
274}
275
276#[derive(Default, Debug)]
277pub(crate) struct WhiteBlackListInner {
278 white_list: Option<HashSet<IpAddr>>,
279 black_list: Option<HashSet<IpAddr>>,
280}
281
282#[cfg(test)]
283mod tests {
284 use super::SharedWhiteBlackList;
285 use crate::error::BootstrapError;
286 use std::net::{IpAddr, SocketAddr};
287 use tempfile::TempDir;
288
289 #[test]
290 fn blacklisting_mapped_ipv6_blocks_equivalent_ipv4_immediately() {
291 let dir = TempDir::new().unwrap();
292 let white = dir.path().join("whitelist.json");
293 let black = dir.path().join("blacklist.json");
294 let list = SharedWhiteBlackList::new(white, black).unwrap();
295
296 let mapped: IpAddr = "::ffff:127.0.0.2".parse().unwrap();
298 list.add_ips_to_blacklist(vec![mapped]).unwrap();
299
300 let peer = SocketAddr::new("127.0.0.2".parse().unwrap(), 12345);
303 assert!(matches!(
304 list.is_ip_allowed(&peer),
305 Err(BootstrapError::BlackListed(_))
306 ));
307 }
308
309 #[test]
310 fn whitelist_file_removal_disables_the_list() {
311 let dir = TempDir::new().unwrap();
312 let white = dir.path().join("whitelist.json");
313 let black = dir.path().join("blacklist.json");
314 std::fs::write(&white, r#"["127.0.0.1"]"#).unwrap();
315 let mut list = SharedWhiteBlackList::new(white.clone(), black).unwrap();
316
317 let refused = SocketAddr::new("127.0.0.2".parse().unwrap(), 12345);
318 assert!(matches!(
319 list.is_ip_allowed(&refused),
320 Err(BootstrapError::WhiteListed(_))
321 ));
322
323 std::fs::remove_file(&white).unwrap();
326 list.update().unwrap();
327
328 assert!(list.is_ip_allowed(&refused).is_ok());
329 }
330
331 #[test]
332 fn unreadable_whitelist_file_keeps_the_loaded_list() {
333 let dir = TempDir::new().unwrap();
334 let white = dir.path().join("whitelist.json");
335 let black = dir.path().join("blacklist.json");
336 std::fs::write(&white, r#"["127.0.0.1"]"#).unwrap();
337 let mut list = SharedWhiteBlackList::new(white.clone(), black).unwrap();
338
339 let allowed = SocketAddr::new("127.0.0.1".parse().unwrap(), 12345);
340 let refused = SocketAddr::new("127.0.0.2".parse().unwrap(), 12345);
341 assert!(list.is_ip_allowed(&allowed).is_ok());
342 assert!(matches!(
343 list.is_ip_allowed(&refused),
344 Err(BootstrapError::WhiteListed(_))
345 ));
346
347 std::fs::remove_file(&white).unwrap();
350 std::fs::create_dir(&white).unwrap();
351 list.update().unwrap();
352
353 assert!(list.is_ip_allowed(&allowed).is_ok());
354 assert!(matches!(
355 list.is_ip_allowed(&refused),
356 Err(BootstrapError::WhiteListed(_))
357 ));
358 }
359
360 #[test]
361 fn blacklist_file_removal_disables_the_list() {
362 let dir = TempDir::new().unwrap();
363 let white = dir.path().join("whitelist.json");
364 let black = dir.path().join("blacklist.json");
365 std::fs::write(&black, r#"["127.0.0.2"]"#).unwrap();
366 let mut list = SharedWhiteBlackList::new(white, black.clone()).unwrap();
367
368 let blocked = SocketAddr::new("127.0.0.2".parse().unwrap(), 12345);
369 assert!(matches!(
370 list.is_ip_allowed(&blocked),
371 Err(BootstrapError::BlackListed(_))
372 ));
373
374 std::fs::remove_file(&black).unwrap();
375 list.update().unwrap();
376
377 assert!(list.is_ip_allowed(&blocked).is_ok());
378 }
379
380 #[test]
381 fn refresh_without_any_configured_list_keeps_access_open() {
382 let dir = TempDir::new().unwrap();
383 let white = dir.path().join("whitelist.json");
384 let black = dir.path().join("blacklist.json");
385 let mut list = SharedWhiteBlackList::new(white, black).unwrap();
387
388 list.update().unwrap();
389
390 let peer = SocketAddr::new("127.0.0.1".parse().unwrap(), 12345);
391 assert!(list.is_ip_allowed(&peer).is_ok());
392 }
393
394 #[test]
395 fn recreated_whitelist_file_is_reloaded_on_refresh() {
396 let dir = TempDir::new().unwrap();
397 let white = dir.path().join("whitelist.json");
398 let black = dir.path().join("blacklist.json");
399 std::fs::write(&white, r#"["127.0.0.1"]"#).unwrap();
400 let mut list = SharedWhiteBlackList::new(white.clone(), black).unwrap();
401
402 std::fs::remove_file(&white).unwrap();
405 list.update().unwrap();
406 std::fs::write(&white, r#"["127.0.0.3"]"#).unwrap();
407 list.update().unwrap();
408
409 let newly_allowed = SocketAddr::new("127.0.0.3".parse().unwrap(), 12345);
410 let formerly_allowed = SocketAddr::new("127.0.0.1".parse().unwrap(), 12345);
411 assert!(list.is_ip_allowed(&newly_allowed).is_ok());
412 assert!(matches!(
413 list.is_ip_allowed(&formerly_allowed),
414 Err(BootstrapError::WhiteListed(_))
415 ));
416 }
417}