mirror of https://github.com/fail2ban/fail2ban
331 lines
9.5 KiB
Python
331 lines
9.5 KiB
Python
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
|
|
|
# This file is part of Fail2Ban.
|
|
#
|
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fail2Ban 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 General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Fail2Ban; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
|
__author__ = "Steven Hiscocks"
|
|
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
|
__license__ = "GPL"
|
|
|
|
import datetime
|
|
import time
|
|
from distutils.version import LooseVersion
|
|
|
|
from systemd import journal
|
|
if LooseVersion(getattr(journal, '__version__', "0")) < '204':
|
|
raise ImportError("Fail2Ban requires systemd >= 204")
|
|
|
|
from .failmanager import FailManagerEmpty
|
|
from .filter import JournalFilter, Filter
|
|
from .mytime import MyTime
|
|
from .utils import Utils
|
|
from ..helpers import getLogger, logging, splitwords, uni_decode
|
|
|
|
# Gets the instance of the logger.
|
|
logSys = getLogger(__name__)
|
|
|
|
|
|
##
|
|
# Journal reader class.
|
|
#
|
|
# This class reads from systemd journal and detects login failures or anything
|
|
# else that matches a given regular expression. This class is instantiated by
|
|
# a Jail object.
|
|
|
|
class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|
##
|
|
# Constructor.
|
|
#
|
|
# Initialize the filter object with default values.
|
|
# @param jail the jail object
|
|
|
|
def __init__(self, jail, **kwargs):
|
|
jrnlargs = FilterSystemd._getJournalArgs(kwargs)
|
|
JournalFilter.__init__(self, jail, **kwargs)
|
|
self.__modified = 0
|
|
# Initialise systemd-journal connection
|
|
self.__journal = journal.Reader(**jrnlargs)
|
|
self.__matches = []
|
|
self.setDatePattern(None)
|
|
logSys.debug("Created FilterSystemd")
|
|
|
|
@staticmethod
|
|
def _getJournalArgs(kwargs):
|
|
args = {'converters':{'__CURSOR': lambda x: x}}
|
|
try:
|
|
args['path'] = kwargs.pop('journalpath')
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
args['files'] = kwargs.pop('journalfiles')
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
import glob
|
|
p = args['files']
|
|
if not isinstance(p, (list, set, tuple)):
|
|
p = splitwords(p)
|
|
files = []
|
|
for p in p:
|
|
files.extend(glob.glob(p))
|
|
args['files'] = list(set(files))
|
|
|
|
try:
|
|
args['flags'] = int(kwargs.pop('journalflags'))
|
|
except KeyError:
|
|
pass
|
|
|
|
return args
|
|
|
|
##
|
|
# Add a journal match filters from list structure
|
|
#
|
|
# @param matches list structure with journal matches
|
|
|
|
def _addJournalMatches(self, matches):
|
|
if self.__matches:
|
|
self.__journal.add_disjunction() # Add OR
|
|
newMatches = []
|
|
for match in matches:
|
|
newMatches.append([])
|
|
for match_element in match:
|
|
self.__journal.add_match(match_element)
|
|
newMatches[-1].append(match_element)
|
|
self.__journal.add_disjunction()
|
|
self.__matches.extend(newMatches)
|
|
|
|
##
|
|
# Add a journal match filter
|
|
#
|
|
# @param match journalctl syntax matches in list structure
|
|
|
|
def addJournalMatch(self, match):
|
|
newMatches = [[]]
|
|
for match_element in match:
|
|
if match_element == "+":
|
|
newMatches.append([])
|
|
else:
|
|
newMatches[-1].append(match_element)
|
|
try:
|
|
self._addJournalMatches(newMatches)
|
|
except ValueError:
|
|
logSys.error(
|
|
"Error adding journal match for: %r", " ".join(match))
|
|
self.resetJournalMatches()
|
|
raise
|
|
else:
|
|
logSys.info("[%s] Added journal match for: %r", self.jailName,
|
|
" ".join(match))
|
|
##
|
|
# Reset a journal match filter called on removal or failure
|
|
#
|
|
# @return None
|
|
|
|
def resetJournalMatches(self):
|
|
self.__journal.flush_matches()
|
|
logSys.debug("[%s] Flushed all journal matches", self.jailName)
|
|
match_copy = self.__matches[:]
|
|
self.__matches = []
|
|
try:
|
|
self._addJournalMatches(match_copy)
|
|
except ValueError:
|
|
logSys.error("Error restoring journal matches")
|
|
raise
|
|
else:
|
|
logSys.debug("Journal matches restored")
|
|
|
|
##
|
|
# Delete a journal match filter
|
|
#
|
|
# @param match journalctl syntax matches
|
|
|
|
def delJournalMatch(self, match=None):
|
|
# clear all:
|
|
if match is None:
|
|
if not self.__matches:
|
|
return
|
|
del self.__matches[:]
|
|
# delete by index:
|
|
elif match in self.__matches:
|
|
del self.__matches[self.__matches.index(match)]
|
|
else:
|
|
raise ValueError("Match %r not found" % match)
|
|
self.resetJournalMatches()
|
|
logSys.info("[%s] Removed journal match for: %r", self.jailName,
|
|
match if match else '*')
|
|
|
|
##
|
|
# Get current journal match filter
|
|
#
|
|
# @return journalctl syntax matches
|
|
|
|
def getJournalMatch(self):
|
|
return self.__matches
|
|
|
|
##
|
|
# Get journal reader
|
|
#
|
|
# @return journal reader
|
|
|
|
def getJournalReader(self):
|
|
return self.__journal
|
|
|
|
##
|
|
# Format journal log entry into syslog style
|
|
#
|
|
# @param entry systemd journal entry dict
|
|
# @return format log line
|
|
|
|
def formatJournalEntry(self, logentry):
|
|
# Be sure, all argument of line tuple should have the same type:
|
|
enc = self.getLogEncoding()
|
|
logelements = []
|
|
v = logentry.get('_HOSTNAME')
|
|
if v:
|
|
logelements.append(uni_decode(v, enc))
|
|
v = logentry.get('SYSLOG_IDENTIFIER')
|
|
if not v:
|
|
v = logentry.get('_COMM')
|
|
if v:
|
|
logelements.append(uni_decode(v, enc))
|
|
v = logentry.get('SYSLOG_PID')
|
|
if not v:
|
|
v = logentry.get('_PID')
|
|
if v:
|
|
logelements[-1] += ("[%i]" % v)
|
|
logelements[-1] += ":"
|
|
if logelements[-1] == "kernel:":
|
|
if '_SOURCE_MONOTONIC_TIMESTAMP' in logentry:
|
|
monotonic = logentry.get('_SOURCE_MONOTONIC_TIMESTAMP')
|
|
else:
|
|
monotonic = logentry.get('__MONOTONIC_TIMESTAMP')[0]
|
|
logelements.append("[%12.6f]" % monotonic.total_seconds())
|
|
msg = logentry.get('MESSAGE','')
|
|
if isinstance(msg, list):
|
|
logelements.append(" ".join(uni_decode(v, enc) for v in msg))
|
|
else:
|
|
logelements.append(uni_decode(msg, enc))
|
|
|
|
logline = " ".join(logelements)
|
|
|
|
date = logentry.get('_SOURCE_REALTIME_TIMESTAMP',
|
|
logentry.get('__REALTIME_TIMESTAMP'))
|
|
logSys.log(5, "[%s] Read systemd journal entry: %s %s", self.jailName,
|
|
date.isoformat(), logline)
|
|
## use the same type for 1st argument:
|
|
return ((logline[:0], date.isoformat(), logline),
|
|
time.mktime(date.timetuple()) + date.microsecond/1.0E6)
|
|
|
|
def seekToTime(self, date):
|
|
if not isinstance(date, datetime.datetime):
|
|
date = datetime.datetime.fromtimestamp(date)
|
|
self.__journal.seek_realtime(date)
|
|
|
|
##
|
|
# Main loop.
|
|
#
|
|
# Peridocily check for new journal entries matching the filter and
|
|
# handover to FailManager
|
|
|
|
def run(self):
|
|
|
|
if not self.getJournalMatch():
|
|
logSys.notice(
|
|
"Jail started without 'journalmatch' set. "
|
|
"Jail regexs will be checked against all journal entries, "
|
|
"which is not advised for performance reasons.")
|
|
|
|
# Seek to now - findtime in journal
|
|
start_time = datetime.datetime.now() - \
|
|
datetime.timedelta(seconds=int(self.getFindTime()))
|
|
self.seekToTime(start_time)
|
|
# Move back one entry to ensure do not end up in dead space
|
|
# if start time beyond end of journal
|
|
try:
|
|
self.__journal.get_previous()
|
|
except OSError:
|
|
pass # Reading failure, so safe to ignore
|
|
|
|
while self.active:
|
|
# wait for records (or for timeout in sleeptime seconds):
|
|
try:
|
|
## todo: find better method as wait_for to break (e.g. notify) journal.wait(self.sleeptime),
|
|
## don't use `journal.close()` for it, because in some python/systemd implementation it may
|
|
## cause abnormal program termination
|
|
#self.__journal.wait(self.sleeptime) != journal.NOP
|
|
##
|
|
## wait for entries without sleep in intervals, because "sleeping" in journal.wait:
|
|
Utils.wait_for(lambda: not self.active or \
|
|
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
|
|
self.sleeptime, 0.00001)
|
|
if self.idle:
|
|
# because journal.wait will returns immediatelly if we have records in journal,
|
|
# just wait a little bit here for not idle, to prevent hi-load:
|
|
if not Utils.wait_for(lambda: not self.active or not self.idle,
|
|
self.sleeptime * 10, self.sleeptime
|
|
):
|
|
self.ticks += 1
|
|
continue
|
|
self.__modified = 0
|
|
while self.active:
|
|
logentry = None
|
|
try:
|
|
logentry = self.__journal.get_next()
|
|
except OSError as e:
|
|
logSys.error("Error reading line from systemd journal: %s",
|
|
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
|
|
self.ticks += 1
|
|
if logentry:
|
|
self.processLineAndAdd(
|
|
*self.formatJournalEntry(logentry))
|
|
self.__modified += 1
|
|
if self.__modified >= 100: # todo: should be configurable
|
|
break
|
|
else:
|
|
break
|
|
if self.__modified:
|
|
self.performBan()
|
|
self.__modified = 0
|
|
except Exception as e: # pragma: no cover
|
|
if not self.active: # if not active - error by stop...
|
|
break
|
|
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
|
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
|
# incr common error counter:
|
|
self.commonError()
|
|
|
|
logSys.debug("[%s] filter terminated", self.jailName)
|
|
|
|
# close journal:
|
|
try:
|
|
if self.__journal:
|
|
self.__journal.close()
|
|
except Exception as e: # pragma: no cover
|
|
logSys.error("Close journal failed: %r", e,
|
|
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
|
|
|
logSys.debug("[%s] filter exited (systemd)", self.jailName)
|
|
return True
|
|
|
|
def status(self, flavor="basic"):
|
|
ret = super(FilterSystemd, self).status(flavor=flavor)
|
|
ret.append(("Journal matches",
|
|
[" + ".join(" ".join(match) for match in self.__matches)]))
|
|
return ret
|