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]])
|
stream.append(head + ["actionban", self._file, self._opts[opt]])
|
||||||
elif opt == "actionunban":
|
elif opt == "actionunban":
|
||||||
stream.append(head + ["actionunban", self._file, self._opts[opt]])
|
stream.append(head + ["actionunban", self._file, self._opts[opt]])
|
||||||
# cInfo
|
|
||||||
if self._initOpts:
|
if self._initOpts:
|
||||||
|
if "timeout" in self._initOpts:
|
||||||
|
stream.append(head + ["timeout", self._file, self._opts["timeout"]])
|
||||||
|
# cInfo
|
||||||
for p in self._initOpts:
|
for p in self._initOpts:
|
||||||
stream.append(head + ["setcinfo", self._file, p, self._initOpts[p]])
|
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> 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> 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> 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> 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> 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>"],
|
["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> 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> 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> 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"
|
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
import logging, os
|
import logging, os, subprocess, time, signal
|
||||||
import threading
|
import threading
|
||||||
#from subprocess import call
|
#from subprocess import call
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ _cmd_lock = threading.Lock()
|
||||||
|
|
||||||
# Some hints on common abnormal exit codes
|
# Some hints on common abnormal exit codes
|
||||||
_RETCODE_HINTS = {
|
_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 '
|
'are in the PATH of fail2ban-server process '
|
||||||
'(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). '
|
'(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). '
|
||||||
'You may want to start '
|
'You may want to start '
|
||||||
|
@ -48,6 +48,10 @@ _RETCODE_HINTS = {
|
||||||
'additional informative error messages appear in the terminals.'
|
'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.
|
# Execute commands.
|
||||||
#
|
#
|
||||||
|
@ -59,6 +63,7 @@ class Action:
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.__name = name
|
self.__name = name
|
||||||
|
self.__timeout = 60
|
||||||
self.__cInfo = dict()
|
self.__cInfo = dict()
|
||||||
## Command executed in order to initialize the system.
|
## Command executed in order to initialize the system.
|
||||||
self.__actionStart = ''
|
self.__actionStart = ''
|
||||||
|
@ -88,6 +93,23 @@ class Action:
|
||||||
def getName(self):
|
def getName(self):
|
||||||
return self.__name
|
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".
|
# Sets a "CInfo".
|
||||||
#
|
#
|
||||||
|
@ -144,7 +166,7 @@ class Action:
|
||||||
|
|
||||||
def execActionStart(self):
|
def execActionStart(self):
|
||||||
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo)
|
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo)
|
||||||
return Action.executeCmd(startCmd)
|
return Action.executeCmd(startCmd, self.__timeout)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Set the "ban" command.
|
# Set the "ban" command.
|
||||||
|
@ -240,7 +262,7 @@ class Action:
|
||||||
|
|
||||||
def execActionStop(self):
|
def execActionStop(self):
|
||||||
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo)
|
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo)
|
||||||
return Action.executeCmd(stopCmd)
|
return Action.executeCmd(stopCmd, self.__timeout)
|
||||||
|
|
||||||
def escapeTag(tag):
|
def escapeTag(tag):
|
||||||
for c in '\\#&;`|*?~<>^()[]{}$\n\'"':
|
for c in '\\#&;`|*?~<>^()[]{}$\n\'"':
|
||||||
|
@ -294,12 +316,12 @@ class Action:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo)
|
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" +
|
logSys.error("Invariant check failed. Trying to restore a sane" +
|
||||||
" environment")
|
" environment")
|
||||||
self.execActionStop()
|
self.execActionStop()
|
||||||
self.execActionStart()
|
self.execActionStart()
|
||||||
if not Action.executeCmd(checkCmd):
|
if not Action.executeCmd(checkCmd, self.__timeout):
|
||||||
logSys.fatal("Unable to restore environment")
|
logSys.fatal("Unable to restore environment")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -312,7 +334,7 @@ class Action:
|
||||||
# Replace static fields
|
# Replace static fields
|
||||||
realCmd = Action.replaceTag(realCmd, self.__cInfo)
|
realCmd = Action.replaceTag(realCmd, self.__cInfo)
|
||||||
|
|
||||||
return Action.executeCmd(realCmd)
|
return Action.executeCmd(realCmd, self.__timeout)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Executes a command.
|
# Executes a command.
|
||||||
|
@ -327,27 +349,47 @@ class Action:
|
||||||
# @return True if the command succeeded
|
# @return True if the command succeeded
|
||||||
|
|
||||||
#@staticmethod
|
#@staticmethod
|
||||||
def executeCmd(realCmd):
|
def executeCmd(realCmd, timeout=60):
|
||||||
logSys.debug(realCmd)
|
logSys.debug(realCmd)
|
||||||
_cmd_lock.acquire()
|
_cmd_lock.acquire()
|
||||||
try: # Try wrapped within another try needed for python version < 2.5
|
try: # Try wrapped within another try needed for python version < 2.5
|
||||||
try:
|
try:
|
||||||
# The following line gives deadlock with multiple jails
|
popen = subprocess.Popen(realCmd, shell=True)
|
||||||
#retcode = call(realCmd, shell=True)
|
stime = time.time()
|
||||||
retcode = os.system(realCmd)
|
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:
|
if retcode == 0:
|
||||||
logSys.debug("%s returned successfully" % realCmd)
|
logSys.debug("%s returned successfully" % realCmd)
|
||||||
return True
|
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:
|
else:
|
||||||
msg = _RETCODE_HINTS.get(retcode, None)
|
msg = _RETCODE_HINTS.get(retcode, None)
|
||||||
logSys.error("%s returned %x" % (realCmd, retcode))
|
logSys.error("%s returned %i" % (realCmd, retcode))
|
||||||
if msg:
|
if msg:
|
||||||
logSys.info("HINT on %x: %s"
|
logSys.info("HINT on %i: %s"
|
||||||
% (retcode, msg % locals()))
|
% (retcode, msg % locals()))
|
||||||
except OSError, e:
|
|
||||||
logSys.error("%s failed with %s" % (realCmd, e))
|
|
||||||
finally:
|
|
||||||
_cmd_lock.release()
|
|
||||||
return False
|
return False
|
||||||
executeCmd = staticmethod(executeCmd)
|
executeCmd = staticmethod(executeCmd)
|
||||||
|
|
||||||
|
|
|
@ -290,6 +290,12 @@ class Server:
|
||||||
def getActionUnban(self, name, action):
|
def getActionUnban(self, name, action):
|
||||||
return self.__jails.getAction(name).getAction(action).getActionUnban()
|
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
|
# Status
|
||||||
def status(self):
|
def status(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -234,6 +234,11 @@ class Transmitter:
|
||||||
value = " ".join(command[3:])
|
value = " ".join(command[3:])
|
||||||
self.__server.setActionUnban(name, act, value)
|
self.__server.setActionUnban(name, act, value)
|
||||||
return self.__server.getActionUnban(name, act)
|
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)")
|
raise Exception("Invalid command (no set action or not yet implemented)")
|
||||||
|
|
||||||
def __commandGet(self, command):
|
def __commandGet(self, command):
|
||||||
|
@ -286,6 +291,9 @@ class Transmitter:
|
||||||
act = command[2]
|
act = command[2]
|
||||||
key = command[3]
|
key = command[3]
|
||||||
return self.__server.getCInfo(name, act, key)
|
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)")
|
raise Exception("Invalid command (no get action or not yet implemented)")
|
||||||
|
|
||||||
def status(self, command):
|
def status(self, command):
|
||||||
|
|
|
@ -95,4 +95,11 @@ class ExecuteAction(unittest.TestCase):
|
||||||
|
|
||||||
def testExecuteIncorrectCmd(self):
|
def testExecuteIncorrectCmd(self):
|
||||||
Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
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(
|
self.transm.proceed(
|
||||||
["set", self.jailName, "delcinfo", action, "KEY"]),
|
["set", self.jailName, "delcinfo", action, "KEY"]),
|
||||||
(0, None))
|
(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.assertEqual(
|
||||||
self.transm.proceed(["set", self.jailName, "delaction", action]),
|
self.transm.proceed(["set", self.jailName, "delaction", action]),
|
||||||
(0, None))
|
(0, None))
|
||||||
|
|
Loading…
Reference in New Issue