Skip to content
Snippets Groups Projects
Select Git revision
  • 729654deba041d1e852f9f2cca977c08d0fcf203
  • dev default protected
  • release/1.9.1 protected
  • pini-1.8-docker
  • pini-sync-onlypeers
  • duniter-v2s-issue-123-industrialize-releases
  • feature/build-aarch64-nodejs16
  • release/1.8 protected
  • pini-docker
  • ci_tags
  • fix/1448/1.8/txs_not_stored
  • feature/node-20
  • fix/1441/node_summary_with_storage
  • fix/1442/improve_bma_tx_history
  • feature/wotwizard-1.8
  • release/1.9 protected
  • 1.7 protected
  • feature/docker-set-latest protected
  • feature/fast-docker-build-1.8.4
  • fast-docker-build protected
  • feature/dump-distance
  • v1.8.7 protected
  • v1.8.7-rc4 protected
  • v1.8.7-rc3 protected
  • v1.8.7-rc2 protected
  • v1.8.7-rc1 protected
  • v1.8.6 protected
  • v1.7.23 protected
  • v1.8.5 protected
  • v1.8.4 protected
  • v1.8.3 protected
  • v1.8.2 protected
  • v1.8.1 protected
  • v1.8.0 protected
  • v1.8.0-rc1 protected
  • v1.8.0-beta5 protected
  • v1.8.0-beta4 protected
  • v1.8.0-beta3 protected
  • v1.8.0-beta2 protected
  • v1.8.0-beta protected
  • v1.7.21 protected
41 results

install.sh

Blame
  • anti_spam.rs 6.90 KiB
    //  Copyright (C) 2020 Éloïs SANCHEZ.
    //
    // This program is free software: you can redistribute it and/or modify
    // it under the terms of the GNU Affero General Public License as
    // published by the Free Software Foundation, either version 3 of the
    // License, or (at your option) any later version.
    //
    // This program is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    // GNU Affero General Public License for more details.
    //
    // You should have received a copy of the GNU Affero General Public License
    // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    
    use crate::*;
    use async_mutex::Mutex;
    use duniter_core::dbs::kv_typed::prelude::Arc;
    use std::{
        collections::{HashMap, HashSet},
        net::IpAddr,
        time::Duration,
        time::Instant,
    };
    
    pub(super) const MAX_BATCH_SIZE: usize = 5;
    
    const COUNT_INTERVAL: usize = 10;
    const MIN_DURATION_INTERVAL: Duration = Duration::from_secs(20);
    const LARGE_DURATION_INTERVAL: Duration = Duration::from_secs(180);
    const REDUCED_COUNT_INTERVAL: usize = COUNT_INTERVAL / 2;
    const MAX_BAN_COUNT: usize = 16;
    const BAN_FORGET_MIN_DURATION: Duration = Duration::from_secs(180);
    
    #[derive(Clone)]
    pub(crate) struct AntiSpam {
        state: Arc<Mutex<AntiSpamInner>>,
        whitelist: HashSet<IpAddr>,
    }
    
    #[derive(Clone)]
    pub(crate) struct AntiSpamResponse {
        pub is_whitelisted: bool,
        pub is_ok: bool,
    }
    
    impl AntiSpamResponse {
        fn ban() -> Self {
            AntiSpamResponse {
                is_whitelisted: false,
                is_ok: false,
            }
        }
        fn ok() -> Self {
            AntiSpamResponse {
                is_whitelisted: false,
                is_ok: true,
            }
        }
        fn whitelisted() -> Self {
            AntiSpamResponse {
                is_whitelisted: true,
                is_ok: true,
            }
        }
    }
    
    struct AntiSpamInner {
        ban: HashMap<IpAddr, (bool, usize, Instant)>,
        ips_time: HashMap<IpAddr, (usize, Instant)>,
    }
    
    impl From<&GvaConf> for AntiSpam {
        fn from(conf: &GvaConf) -> Self {
            AntiSpam {
                state: Arc::new(Mutex::new(AntiSpamInner {
                    ban: HashMap::with_capacity(10),
                    ips_time: HashMap::with_capacity(10),
                })),
                whitelist: conf.whitelist.iter().copied().collect(),
            }
        }
    }
    
    impl AntiSpam {
        pub(crate) async fn verify(
            &self,
            remote_addr_opt: Option<std::net::IpAddr>,
        ) -> AntiSpamResponse {
            if let Some(ip) = remote_addr_opt {
                log::trace!("GVA: receive request from {}", ip);
                if self.whitelist.contains(&ip) {
                    AntiSpamResponse::whitelisted()
                } else {
                    let mut guard = self.state.lock().await;
                    if let Some((is_banned, ban_count, instant)) = guard.ban.get(&ip).copied() {
                        let ban_duration =
                            Duration::from_secs(1 << std::cmp::min(ban_count, MAX_BAN_COUNT));
                        if is_banned {
                            if Instant::now().duration_since(instant) > ban_duration {
                                guard.ban.insert(ip, (false, ban_count + 1, Instant::now()));
                                guard.ips_time.insert(ip, (1, Instant::now()));
                                AntiSpamResponse::ok()
                            } else {
                                guard.ban.insert(ip, (true, ban_count + 1, Instant::now()));
                                AntiSpamResponse::ban()
                            }
                        } else if Instant::now().duration_since(instant)
                            > std::cmp::max(ban_duration, BAN_FORGET_MIN_DURATION)
                        {
                            guard.ban.remove(&ip);
                            guard.ips_time.insert(ip, (1, Instant::now()));
                            AntiSpamResponse::ok()
                        } else {
                            Self::verify_interval(ip, &mut guard, ban_count)
                        }
                    } else {
                        Self::verify_interval(ip, &mut guard, 0)
                    }
                }
            } else {
                AntiSpamResponse::ban()
            }
        }
        fn verify_interval(
            ip: IpAddr,
            state: &mut AntiSpamInner,
            ban_count: usize,
        ) -> AntiSpamResponse {
            if let Some((count, instant)) = state.ips_time.get(&ip).copied() {
                if count == COUNT_INTERVAL {
                    let duration = Instant::now().duration_since(instant);
                    if duration > MIN_DURATION_INTERVAL {
                        if duration > LARGE_DURATION_INTERVAL {
                            state.ips_time.insert(ip, (1, Instant::now()));
                            AntiSpamResponse::ok()
                        } else {
                            state
                                .ips_time
                                .insert(ip, (REDUCED_COUNT_INTERVAL, Instant::now()));
                            AntiSpamResponse::ok()
                        }
                    } else {
                        state.ban.insert(ip, (true, ban_count, Instant::now()));
                        AntiSpamResponse::ban()
                    }
                } else {
                    state.ips_time.insert(ip, (count + 1, instant));
                    AntiSpamResponse::ok()
                }
            } else {
                state.ips_time.insert(ip, (1, Instant::now()));
                AntiSpamResponse::ok()
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::net::{Ipv4Addr, Ipv6Addr};
    
        const LOCAL_IP4: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
        const LOCAL_IP6: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST);
    
        #[tokio::test]
        async fn test_anti_spam() {
            let anti_spam = AntiSpam::from(&GvaConf::default());
            assert!(!anti_spam.verify(None).await.is_ok);
    
            for _ in 0..(COUNT_INTERVAL * 2) {
                assert!(anti_spam.verify(Some(LOCAL_IP4)).await.is_ok);
                assert!(anti_spam.verify(Some(LOCAL_IP6)).await.is_ok);
            }
    
            let extern_ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED);
    
            // Consume max queries
            for _ in 0..COUNT_INTERVAL {
                assert!(anti_spam.verify(Some(extern_ip)).await.is_ok);
            }
            // Should be banned
            assert!(!anti_spam.verify(Some(extern_ip)).await.is_ok);
    
            // Should be un-banned after one second
            tokio::time::sleep(Duration::from_millis(1_100)).await;
            // Re-consume max queries
            for _ in 0..COUNT_INTERVAL {
                assert!(anti_spam.verify(Some(extern_ip)).await.is_ok);
            }
            // Should be banned for 2 seconds this time
            tokio::time::sleep(Duration::from_millis(1_100)).await;
            // Attempting a request when I'm banned must be twice my banning time
            assert!(!anti_spam.verify(Some(extern_ip)).await.is_ok);
            tokio::time::sleep(Duration::from_millis(4_100)).await;
            // Re-consume max queries
            for _ in 0..COUNT_INTERVAL {
                assert!(anti_spam.verify(Some(extern_ip)).await.is_ok);
            }
        }
    }