python持续监控登录日志,封禁高频登录失败ip

使用ubuntu16.04以上,系统预装Fail2ban,可以通过配置文件来满足需求,其他linux系统也可以自行安装

功能比较简单,用python写下,自己写的代码方便以后增加新功能

#!/usr/bin/env python
#-*-coding:utf-8-*-
import time
import subprocess
import os,sys
import re
import threading
from configparser import ConfigParser

class SSHBlocker:
    def __init__(self, config_path):
        self.block_dic = {}  # 存储 IP 地址和封禁计数的字典
        self.count_dic = {'cron':time.time()} # 记录ip被封禁的次数
        self.lock = threading.Lock()  # 线程锁,用于保护共享数据
        self.lock2 = threading.Lock() 
        self.config = ConfigParser()
        self.config.read(config_path)
        self.log_path = self.config.get('Settings', 'log_path')  # 读取配置文件中的日志路径
        self.block_threshold = int(self.config.get('Settings', 'block_threshold'))  # 读取配置文件中的封禁阈值
        self.block_duration = int(self.config.get('Settings', 'block_duration'))  # 读取配置文件中的封禁持续时间
        self.unblock_duration = int(self.config.get('Settings', 'unblock_duration'))  # 读取配置文件中的解封持续时间
        self.blocked_port = int(self.config.get('Settings', 'blocked_port'))  # 读取配置文件中的封禁端口

    def block_ip(self, ip, blocked=False):
        """
        更新 IP 地址的封禁计数
        """
        dic,lock = (self.block_dic,self.lock) if not blocked else (self.count_dic,self.lock2)
        with lock:
            try:
                count, timestamp = dic[ip]
                dic[ip] = (count + 1, time.time())
            except KeyError:
                dic[ip] = (1, time.time())
    def add_rule(self):
        """
        根据封禁计数和时间,添加相应的 iptables 规则进行封禁
        """
        while True:
            with self.lock:
                if self.block_dic:
                    for ip, (count, timestamp) in self.block_dic.items():
                        cmd = f'iptables -L -n -w| grep {ip}'
                        output = subprocess.run(cmd, shell=True, capture_output=True, text=True)
                        if count > self.block_threshold and time.time() - timestamp < self.block_duration and not output.stdout:
                            subprocess.run(f'iptables -A INPUT -s {ip}/32 -p tcp --dport {self.blocked_port} -j DROP -w', shell=True)
                            self.block_ip(ip,True)
            time.sleep(1)

    def reduce_rule(self):
        """
        根据解封时间,移除相应的 iptables 规则进行解封
        """
        while True:
            with self.lock:
                cron = self.count_dic['cron']
                if time.time() - cron > 86400:
                    with self.lock2:
                        self.count_dic = {'cron':time.time()}
                remove_list = []
                if self.block_dic:
                    for ip, (count, timestamp) in self.block_dic.items():
                        unbantime = self.unblock_duration
                        with self.lock2:
                            if ip in self.count_dic:
                                blocked_count , blocked_time = self.count_dic[ip]
                                #解封时间,封禁一次增加10分钟
                                unbantime = blocked_count * self.unblock_duration
                        if time.time() - timestamp > unbantime:
                            blocked = True if count > self.block_threshold else False
                            remove_list.append((ip,blocked))
                    if remove_list:
                        for (ip,status) in remove_list:
                            del self.block_dic[ip]
                            reduce_cmd = f'iptables -D INPUT -s {ip}/32 -p tcp --dport {self.blocked_port} -j DROP -w'
                            if status: subprocess.run(reduce_cmd, shell=True)
            time.sleep(1)

    def monitor_log_file(self):
        """
        监控日志文件,根据失败的登录尝试更新封禁计数
        """
        while True:
            with open(self.log_path, 'r') as f:
                f.seek(0, 2)
                init_file_size = f.tell()
                inode = os.stat(self.log_path).st_ino
                while True:
                    """
                    比对日志文件大小和Inode,解决日志自动打包发生旋转问题
                    """                    
                    try:
                        file_stat = os.stat(self.log_path)
                        now_inode = file_stat.st_ino
                        now_file_size = file_stat.st_size
                        if now_inode != inode or now_file_size < init_file_size:
                            inode = now_inode
                            init_file_size = now_file_size
                            print(f"Log file rotated or reset. Inode: {inode}, Size: {init_file_size}")
                            break
                    except Exception as e:
                        print(f"Error occurred: {e}")
                        time.sleep(1)
                        continue
                    log_line = f.readline()
                    if 'Failed' in log_line:
                        ip = re.findall('\d+\.\d+\.\d+\.\d+', log_line)[0]
                        self.block_ip(ip)
                    time.sleep(1)

    def start(self):
        """
        启动监控线程
        """
        log_monitor_thread = threading.Thread(target=self.monitor_log_file)
        add_rule_thread = threading.Thread(target=self.add_rule)
        remove_rule_thread = threading.Thread(target=self.reduce_rule)
        log_monitor_thread.start()
        add_rule_thread.start()
        remove_rule_thread.start()

if __name__ == '__main__':
    script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
    config_path = os.path.join(script_dir, 'config.ini')
    monitor = SSHBlocker(config_path)
    monitor.start()

config.ini

[Settings]
log_path = /var/log/auth.log
block_threshold = 10
block_duration = 60
unblock_duration = 600
blocked_port = 22

模拟测试下

修改下配置,配置为60秒内失败2次,封禁60秒后解封

尝试登录失败

# ssh 111@10.0.3.109
111@10.0.3.109's password:
Permission denied, please try again.
111@10.0.3.109's password:

# ssh 111@10.0.3.109
111@10.0.3.109's password:
Permission denied, please try again.
111@10.0.3.109's password:

查看iptables

# iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:9123
REJECT     tcp  --  43.155.182.237       0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  207.154.245.181      0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  103.233.206.154      0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  8.222.230.151        0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  43.154.216.165       0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  45.237.45.144        0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  10.0.3.108           0.0.0.0/0            tcp dpt:ssh reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

等待60s

# iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:9123
REJECT     tcp  --  43.155.182.237       0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  207.154.245.181      0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  103.233.206.154      0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  8.222.230.151        0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  43.154.216.165       0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable
REJECT     tcp  --  45.237.45.144        0.0.0.0/0            tcp dpt:22 reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination  

Leave a Reply

Your email address will not be published. Required fields are marked *

X