mirror of https://github.com/fail2ban/fail2ban
NF: Allow setting of timeout for execution of action commands
This uses subprocess.Popen, polling until `timeout` seconds has passed or the command has exit. If the command has not exited, fail2ban then sends SIGTERM, and if this is unsuccessful, SIGKILL. The timeout can be changed for an entire action via action [Init] options, or via jail.conf override, or fail2ban-client. The default timeout period is 60 seconds.pull/190/merge^2
parent
bddbf1e398
commit
d07df66370
|
@ -61,8 +61,10 @@ class ActionReader(DefinitionInitConfigReader):
|
|||
stream.append(head + ["actionban", self._file, self._opts[opt]])
|
||||
elif opt == "actionunban":
|
||||
stream.append(head + ["actionunban", self._file, self._opts[opt]])
|
||||
# cInfo
|
||||
if self._initOpts:
|
||||
if "timeout" in self._initOpts:
|
||||
stream.append(head + ["timeout", self._file, self._opts["timeout"]])
|
||||
# cInfo
|
||||
for p in self._initOpts:
|
||||
stream.append(head + ["setcinfo", self._file, p, self._initOpts[p]])
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ protocol = [
|
|||
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"],
|
||||
["set <JAIL> setcinfo <ACT> <KEY> <VALUE>", "sets <VALUE> for <KEY> of the action <NAME> for <JAIL>"],
|
||||
["set <JAIL> delcinfo <ACT> <KEY>", "removes <KEY> for the action <NAME> for <JAIL>"],
|
||||
["set <JAIL> timeout <ACT> <TIMEOUT>", "sets <TIMEOUT> as the command timeout in seconds for the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actionstart <ACT> <CMD>", "sets the start command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actionstop <ACT> <CMD>", "sets the stop command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actioncheck <ACT> <CMD>", "sets the check command <CMD> of the action <ACT> for <JAIL>"],
|
||||
|
@ -96,6 +97,7 @@ protocol = [
|
|||
["get <JAIL> actionban <ACT>", "gets the ban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actionunban <ACT>", "gets the unban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> cinfo <ACT> <KEY>", "gets the value for <KEY> for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> timeout <ACT>", "gets the command timeout in seconds for the action <ACT> for <JAIL>"],
|
||||
]
|
||||
|
||||
##
|
||||
|
|
|
@ -27,7 +27,7 @@ __date__ = "$Date$"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging, os
|
||||
import logging, os, subprocess, time, signal
|
||||
import threading
|
||||
#from subprocess import call
|
||||
|
||||
|
@ -39,7 +39,7 @@ _cmd_lock = threading.Lock()
|
|||
|
||||
# Some hints on common abnormal exit codes
|
||||
_RETCODE_HINTS = {
|
||||
0x7f00: '"Command not found". Make sure that all commands in %(realCmd)r '
|
||||
127: '"Command not found". Make sure that all commands in %(realCmd)r '
|
||||
'are in the PATH of fail2ban-server process '
|
||||
'(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). '
|
||||
'You may want to start '
|
||||
|
@ -48,6 +48,10 @@ _RETCODE_HINTS = {
|
|||
'additional informative error messages appear in the terminals.'
|
||||
}
|
||||
|
||||
# Dictionary to lookup signal name from number
|
||||
signame = dict((num, name)
|
||||
for name, num in signal.__dict__.iteritems() if name.startswith("SIG"))
|
||||
|
||||
##
|
||||
# Execute commands.
|
||||
#
|
||||
|
@ -59,6 +63,7 @@ class Action:
|
|||
|
||||
def __init__(self, name):
|
||||
self.__name = name
|
||||
self.__timeout = 60
|
||||
self.__cInfo = dict()
|
||||
## Command executed in order to initialize the system.
|
||||
self.__actionStart = ''
|
||||
|
@ -88,6 +93,23 @@ class Action:
|
|||
def getName(self):
|
||||
return self.__name
|
||||
|
||||
##
|
||||
# Sets the timeout period for commands.
|
||||
#
|
||||
# @param timeout timeout period in seconds
|
||||
|
||||
def setTimeout(self, timeout):
|
||||
self.__timeout = int(timeout)
|
||||
logSys.debug("Set action %s timeout = %i" % (self.__name, timeout))
|
||||
|
||||
##
|
||||
# Returns the action timeout period for commands.
|
||||
#
|
||||
# @return the timeout period in seconds
|
||||
|
||||
def getTimeout(self):
|
||||
return self.__timeout
|
||||
|
||||
##
|
||||
# Sets a "CInfo".
|
||||
#
|
||||
|
@ -144,7 +166,7 @@ class Action:
|
|||
|
||||
def execActionStart(self):
|
||||
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo)
|
||||
return Action.executeCmd(startCmd)
|
||||
return Action.executeCmd(startCmd, self.__timeout)
|
||||
|
||||
##
|
||||
# Set the "ban" command.
|
||||
|
@ -240,7 +262,7 @@ class Action:
|
|||
|
||||
def execActionStop(self):
|
||||
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo)
|
||||
return Action.executeCmd(stopCmd)
|
||||
return Action.executeCmd(stopCmd, self.__timeout)
|
||||
|
||||
def escapeTag(tag):
|
||||
for c in '\\#&;`|*?~<>^()[]{}$\n\'"':
|
||||
|
@ -294,12 +316,12 @@ class Action:
|
|||
return True
|
||||
|
||||
checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo)
|
||||
if not Action.executeCmd(checkCmd):
|
||||
if not Action.executeCmd(checkCmd, self.__timeout):
|
||||
logSys.error("Invariant check failed. Trying to restore a sane" +
|
||||
" environment")
|
||||
self.execActionStop()
|
||||
self.execActionStart()
|
||||
if not Action.executeCmd(checkCmd):
|
||||
if not Action.executeCmd(checkCmd, self.__timeout):
|
||||
logSys.fatal("Unable to restore environment")
|
||||
return False
|
||||
|
||||
|
@ -312,7 +334,7 @@ class Action:
|
|||
# Replace static fields
|
||||
realCmd = Action.replaceTag(realCmd, self.__cInfo)
|
||||
|
||||
return Action.executeCmd(realCmd)
|
||||
return Action.executeCmd(realCmd, self.__timeout)
|
||||
|
||||
##
|
||||
# Executes a command.
|
||||
|
@ -327,27 +349,47 @@ class Action:
|
|||
# @return True if the command succeeded
|
||||
|
||||
#@staticmethod
|
||||
def executeCmd(realCmd):
|
||||
def executeCmd(realCmd, timeout=60):
|
||||
logSys.debug(realCmd)
|
||||
_cmd_lock.acquire()
|
||||
try: # Try wrapped within another try needed for python version < 2.5
|
||||
try:
|
||||
# The following line gives deadlock with multiple jails
|
||||
#retcode = call(realCmd, shell=True)
|
||||
retcode = os.system(realCmd)
|
||||
if retcode == 0:
|
||||
logSys.debug("%s returned successfully" % realCmd)
|
||||
return True
|
||||
else:
|
||||
msg = _RETCODE_HINTS.get(retcode, None)
|
||||
logSys.error("%s returned %x" % (realCmd, retcode))
|
||||
if msg:
|
||||
logSys.info("HINT on %x: %s"
|
||||
% (retcode, msg % locals()))
|
||||
popen = subprocess.Popen(realCmd, shell=True)
|
||||
stime = time.time()
|
||||
retcode = popen.poll()
|
||||
while time.time() - stime <= timeout and retcode is None:
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
if retcode is None:
|
||||
logSys.error("%s timed out after %i seconds." %
|
||||
(realCmd, timeout))
|
||||
os.kill(popen.pid, signal.SIGTERM) # Terminate the process
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
if retcode is None: # Still going...
|
||||
os.kill(popen.pid, signal.SIGKILL) # Kill the process
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
except OSError, e:
|
||||
logSys.error("%s failed with %s" % (realCmd, e))
|
||||
return False
|
||||
finally:
|
||||
_cmd_lock.release()
|
||||
|
||||
if retcode == 0:
|
||||
logSys.debug("%s returned successfully" % realCmd)
|
||||
return True
|
||||
elif retcode is None:
|
||||
logSys.error("Unable to kill PID %i: %s" % (popen.pid, realCmd))
|
||||
elif retcode < 0:
|
||||
logSys.error("%s killed with %s" %
|
||||
(realCmd, signame.get(-retcode, "signal %i" % -retcode)))
|
||||
else:
|
||||
msg = _RETCODE_HINTS.get(retcode, None)
|
||||
logSys.error("%s returned %i" % (realCmd, retcode))
|
||||
if msg:
|
||||
logSys.info("HINT on %i: %s"
|
||||
% (retcode, msg % locals()))
|
||||
return False
|
||||
executeCmd = staticmethod(executeCmd)
|
||||
|
||||
|
|
|
@ -289,6 +289,12 @@ class Server:
|
|||
|
||||
def getActionUnban(self, name, action):
|
||||
return self.__jails.getAction(name).getAction(action).getActionUnban()
|
||||
|
||||
def setActionTimeout(self, name, action, value):
|
||||
self.__jails.getAction(name).getAction(action).setTimeout(value)
|
||||
|
||||
def getActionTimeout(self, name, action):
|
||||
return self.__jails.getAction(name).getAction(action).getTimeout()
|
||||
|
||||
# Status
|
||||
def status(self):
|
||||
|
|
|
@ -234,6 +234,11 @@ class Transmitter:
|
|||
value = " ".join(command[3:])
|
||||
self.__server.setActionUnban(name, act, value)
|
||||
return self.__server.getActionUnban(name, act)
|
||||
elif command[1] == "timeout":
|
||||
act = command[2]
|
||||
value = int(command[3])
|
||||
self.__server.setActionTimeout(name, act, value)
|
||||
return self.__server.getActionTimeout(name, act)
|
||||
raise Exception("Invalid command (no set action or not yet implemented)")
|
||||
|
||||
def __commandGet(self, command):
|
||||
|
@ -286,6 +291,9 @@ class Transmitter:
|
|||
act = command[2]
|
||||
key = command[3]
|
||||
return self.__server.getCInfo(name, act, key)
|
||||
elif command[1] == "timeout":
|
||||
act = command[2]
|
||||
return self.__server.getActionTimeout(name, act)
|
||||
raise Exception("Invalid command (no get action or not yet implemented)")
|
||||
|
||||
def status(self, command):
|
||||
|
|
|
@ -95,4 +95,11 @@ class ExecuteAction(unittest.TestCase):
|
|||
|
||||
def testExecuteIncorrectCmd(self):
|
||||
Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
||||
self.assertTrue(self._is_logged('HINT on 7f00: "Command not found"'))
|
||||
self.assertTrue(self._is_logged('HINT on 127: "Command not found"'))
|
||||
|
||||
def testExecuteTimeout(self):
|
||||
stime = time.time()
|
||||
Action.executeCmd('sleep 60', timeout=2) # Should take a minute
|
||||
self.assertAlmostEqual(time.time() - stime, 2.1, places=1)
|
||||
self.assertTrue(self._is_logged('sleep 60 timed out after 2 seconds'))
|
||||
self.assertTrue(self._is_logged('sleep 60 killed with SIGTERM'))
|
||||
|
|
|
@ -467,6 +467,14 @@ class Transmitter(TransmitterBase):
|
|||
self.transm.proceed(
|
||||
["set", self.jailName, "delcinfo", action, "KEY"]),
|
||||
(0, None))
|
||||
self.assertEqual(
|
||||
self.transm.proceed(
|
||||
["set", self.jailName, "timeout", action, "10"]),
|
||||
(0, 10))
|
||||
self.assertEqual(
|
||||
self.transm.proceed(
|
||||
["get", self.jailName, "timeout", action]),
|
||||
(0, 10))
|
||||
self.assertEqual(
|
||||
self.transm.proceed(["set", self.jailName, "delaction", action]),
|
||||
(0, None))
|
||||
|
|
Loading…
Reference in New Issue