Commit 40cb2e01 authored by prx's avatar prx

lasts modifs from vincent

parent b14b1aec
......@@ -16,15 +16,3 @@ To see banned IP :
pfctl -t vilain_bruteforce -T show
To start vilain at boot, add this in ``/etc/rc.local``
```
/usr/bin/tmux new -s vilain -d /usr/local/bin/vilain
```
Then, to attach to the tmux session, run :
```
tmux a -t vilain
```
- Improve logs. Show who comes from where if possible. (work on regex needed)
- Add more regex
- Add custom command instead of pf call. Or not, another script watching logs seems better.
#!/bin/sh
# script to launch vilain with the latest python3 version avaiable
PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1)
PYTHON="/usr/local/bin/python$PYTHONVERSION"
if [ -x $PYTHON ]; then
$PYTHON /usr/local/sbin/vilain.py >> /var/log/daemon 2>&1 &
$PYTHON /usr/local/sbin/vilain.py >/dev/null 2>&1 &
else
echo "Error : no python3 executable found"
fi
......
......@@ -19,10 +19,6 @@ In pf.conf, add according to your configuration :
table <vilain_bruteforce> persist
block quick from <vilain_bruteforce>
You might want to add a cron task to remove old banned IP. As example, to ban for one day max :
pfctl -t vilain_bruteforce -T expire 86400
To see banned IP :
pfctl -t vilain_bruteforce -T show
......
......@@ -14,10 +14,14 @@ vilain_table = vilain_bruteforce
ip1 = 92.150.160.157
ip2 = 92.150.160.156
# duration before each checks on the different log files
sleeptime = 0.5
### Guardians
#[name of the guardian]
#logfile = /file/to/watch
#regex = regex that return the bad guy IP
#maxtries = 2 #facultative
[ssh]
logfile = /var/log/authlog
......
......@@ -9,14 +9,12 @@ Require : python >= 3.5
Description : Mimic fail2ban with pf for OpenBSD.
Inspired from http://www.vincentdelft.be/post/post_20161106
with improvements of vincendelft
In pf.conf, add :
table <vilain_bruteforce> persist
block quick from <vilain_bruteforce>
You might want to add a cron task to remove old banned IP. As example, to ban for one day max :
pfctl -t vilain_bruteforce -T expire 86400
To see banned IP :
pfctl -t vilain_bruteforce -T show
"""
......@@ -31,10 +29,9 @@ import asyncio
import time
configfile = "/etc/vilain.conf"
version = "0.3"
version = "0.4"
vilain_table = "vilain_bruteforce"
logfile = "/var/log/daemon"
sleeptime = 0.5
if os.geteuid() != 0:
print("Only root can use this tool")
......@@ -45,7 +42,7 @@ logger = logging.getLogger(__name__)
logging.basicConfig(filename=logfile,
format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
datefmt='%H:%M:%S')
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)
ch = logging.StreamHandler(sys.stdout)
logger.addHandler(ch)
......@@ -63,14 +60,14 @@ def load_config():
c = readconfig()
d = c.defaults()
watch_while = int(d['watch_while'])
maxtries = int(d['maxtries'])
default_maxtries = int(d['maxtries'])
vilain_table = d['vilain_table']
sleeptime = float(d['sleeptime'])
ignore_ips = []
if c.has_section('ignoreip'):
ignoreip = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()]
else:
ignoreip = []
return(watch_while, maxtries, vilain_table, ignoreip)
ignoreips = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()]
return(watch_while, default_maxtries, vilain_table, ignoreips, sleeptime)
def load_sections():
c = readconfig()
......@@ -78,22 +75,41 @@ def load_sections():
if c.has_option(s,'logfile'):
logfile = c.get(s,'logfile')
regex = c.get(s,'regex')
d = {'name' : s, 'logfile':logfile, 'regex':regex}
#we take the default value of maxtries
maxtries = c.defaults()['maxtries']
if c.has_option(s,'maxtries'):
#if we have a maxtries defined in the section, we overwrite the default
maxtries = int(c.get(s,'maxtries'))
d = {'name' : s, 'logfile':logfile, 'regex':regex, 'maxtries': maxtries}
yield d
class Vilain():
def __init__(self):
self.loop = asyncio.get_event_loop()
self.watch_while, self.maxtries, self.vilain_table, self.ignore_ip = load_config()
#self.bad_ip_queue = []
self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config()
self.ip_seen_at = {}
self.load_bad_ips()
self.bad_ip_queue = asyncio.Queue(loop=self.loop)
for entry in load_sections():
logger.info("Start vilain for {}".format(entry['name']))
asyncio.ensure_future(self.check_logs(entry['logfile'], entry['regex'], entry['name']))
asyncio.ensure_future(self.check_logs(entry['logfile'], entry['maxtries'], entry['regex'], entry['name']))
asyncio.ensure_future(self.ban_ips())
def load_bad_ips(self):
try:
ret = subprocess.check_output(["pfctl", "-t", self.vilain_table, "-T", "show"])
except:
ret = ""
for res in ret.split():
ip = res.strip().decode('utf-8')
logger.debug('Add existing banned IPs in your pf table: {}'.format(ip))
#we assign the counter to 1, but for sure we don't know the real value
self.ip_seen_at[ip]={'time':time.time(),'count':1}
def start(self):
try:
self.loop.run_forever()
......@@ -102,7 +118,7 @@ class Vilain():
finally:
self.loop.close()
async def check_logs(self, logfile, regex, reason):
async def check_logs(self, logfile, maxtries, regex, reason):
"""
worker who put in bad_ip_queue bruteforce IP
"""
......@@ -115,9 +131,10 @@ class Vilain():
mtime = stat.st_mtime
RE = re.compile(regex)
while True:
await asyncio.sleep(sleeptime)
await asyncio.sleep(self.sleeptime)
stat = os.stat(logfile)
if mtime < stat.st_mtime:
logger.debug("{} has been modified".format(logfile))
mtime = stat.st_mtime
with open(logfile, "rb") as f:
f.seek(size)
......@@ -126,53 +143,53 @@ class Vilain():
line = "".join(ul).strip()
ret = RE.match(line)
logger.debug('line:{}'.format(line))
if ret:
bad_ip = ret.groups()[0]
if bad_ip not in self.ignore_ip :
#self.bad_ip_queue.append({'ip' : bad_ip, 'reason' : reason})
if bad_ip not in self.ignore_ips :
logger.info('line match {} because of rule : {}'.format(bad_ip, reason))
await self.bad_ip_queue.put({'ip' : bad_ip, 'reason' : reason})
logger.debug('queue size: {}'.format(self.bad_ip_queue.qsize()))
else:
logger.info('line match {}. But IP in ignore list'.format(bad_ip))
size = stat.st_size
async def ban_ips(self):
"""
worker who ban IP on bad_ip_queue
add IP in bad_ips_list
record time when this IP has been seen in ip_seen_at = { ip:time }
check number of occurence of the same ip in bad_ips_list
if more than 3 : ban and clean of list
record time when this IP has been seen in ip_seen_at = { ip:{'time':<time>,'count':<counter} }
check old ip in ip_seen_at : remove older than watch_while
"""
bad_ips_list = []
ip_seen_at = {}
logger.info('ban_ips sarted with sleeptime={}'.format(self.sleeptime))
while True:
await asyncio.sleep(sleeptime)
#if not len(s#elf.bad_ip_queue) > 0:
# continue
await asyncio.sleep(self.sleeptime)
ip_item = await self.bad_ip_queue.get()
#ip_item = self.bad_ip_queue.pop()
logger.debug('ban_ips awake')
ip = ip_item['ip']
reason = ip_item['reason']
logger.info("{} detected, reason {}".format(ip, reason))
bad_ips_list.append(ip)
ip_seen_at[ip] = time.time()
n_ip = bad_ips_list.count(ip)
if n_ip >= self.maxtries:
logger.info("Blacklisting {}".format(ip))
subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip])
ip_seen_at.pop(ip)
while ip in bad_ips_list:
bad_ips_list.remove(ip)
maxtries = ip_item['maxtries']
self.ip_seen_at.setdefault(ip,{'time':time.time(),'count':0})
self.ip_seen_at[ip]['count'] += 1
n_ip = self.ip_seen_at[ip]['count']
logger.info("{} detected, reason {}, count: {}, maxtries: {}".format(ip, reason, n_ip, maxtries))
if n_ip >= maxtries:
ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip])
logger.info("Blacklisting {}, return code:{}".format(ip, ret))
self.ip_seen_at.pop(ip)
to_remove = []
for recorded_ip, last_seen in ip_seen_at.items():
if time.time() - last_seen >= self.watch_while:
logger.info("{} not seen since a long time, forgetting...".format(recorded_ip))
for recorded_ip, data in self.ip_seen_at.items():
if time.time() - data['time'] >= self.watch_while:
ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "delete", recorded_ip])
logger.info("{} not blocked any more, return code:{}".format(recorded_ip, ret))
to_remove.append(recorded_ip)
for i in to_remove:
ip_seen_at.pop(i)
for ip in to_remove:
self.ip_seen_at.pop(ip)
#for debugging, this line allow us to see if the script run until here
logger.debug('ban_ips end:{}'.format(self.ip_seen_at))
......
......@@ -6,8 +6,9 @@ daemon="/usr/local/bin/vilain"
. /etc/rc.d/rc.subr
pexp="python3.*vilain\.py"
pexp=".*vilain\.py"
rc_reload=NO
rc_cmd $1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment