Desausoi Laurent 2025-06-23 12:43:47 +00:00 committed by GitHub
commit 941eda4ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 107 additions and 58 deletions

View File

@ -252,7 +252,7 @@ class Beautifier:
sep = " " if len(inC) <= 3 else inC[3]
if sep == "--with-time":
sep = "\n"
msg = sep.join(response)
msg = sep.join([str(res) for res in response])
except Exception:
logSys.warning("Beautifier error. Please report the error")
logSys.error("Beautify %r with %r failed", response, self.__inputCmd,

View File

@ -28,6 +28,7 @@ import re
import sys
import traceback
from ast import literal_eval
from threading import Lock
from .server.mytime import MyTime
@ -317,6 +318,16 @@ def _merge_copy_dicts(x, y):
"""
return {**x, **y}
def parseExpressions(value):
def parseExpr(v):
try:
return literal_eval(v)
except SyntaxError:
return v
if value:
return list(map(parseExpr, value)) if isinstance(value, (list, tuple)) else parseExpr(value)
return None
#
# Following function used for parse options from parameter (e.g. `name[p1=0, p2="..."][p3='...']`).
#

View File

@ -54,9 +54,9 @@ protocol = [
["reload [--restart] [--unban] [--if-exists] <JAIL>", "reloads the jail <JAIL>, or restarts it (if option '--restart' specified)"],
["stop", "stops all jails and terminate the server"],
["unban --all", "unbans all IP addresses (in all jails and database)"],
["unban <IP> ... <IP>", "unbans <IP> (in all jails and database)"],
["unban [--expr] [--] <IP> ... <IP>", "unbans <IP> (in all jails and database)"],
["banned", "return jails with banned IPs as dictionary"],
["banned <IP> ... <IP>]", "return list(s) of jails where given IP(s) are banned"],
["banned [--expr] [--] <IP> ... <IP>]", "return list(s) of jails where given IP(s) are banned"],
["status", "gets the current status of the server"],
["status --all [FLAVOR]", "gets the current status of all jails, with optional output style [FLAVOR]. Flavors: 'basic' (default), 'cymru', 'short', 'stats'"],
["stat[istic]s", "gets the current statistics of all jails as table"],
@ -87,8 +87,8 @@ protocol = [
['', "JAIL CONFIGURATION", ""],
["set <JAIL> idle on|off", "sets the idle state of <JAIL>"],
["set <JAIL> ignoreself true|false", "allows the ignoring of own IP addresses"],
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"],
["set <JAIL> delignoreip <IP>", "removes <IP> from the ignore list of <JAIL>"],
["set <JAIL> addignoreip [--expr] [--] <IP> ... <IP>", "adds <IP> to the ignore list of <JAIL>"],
["set <JAIL> delignoreip [--expr] [--] <IP> ... <IP>", "removes <IP> from the ignore list of <JAIL>"],
["set <JAIL> ignorecommand <VALUE>", "sets ignorecommand of <JAIL>"],
["set <JAIL> ignorecache <VALUE>", "sets ignorecache of <JAIL>"],
["set <JAIL> addlogpath <FILE> ['tail']", "adds <FILE> to the monitoring list of <JAIL>, optionally starting at the 'tail' of the file (default 'head')."],
@ -104,9 +104,9 @@ protocol = [
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
["set <JAIL> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"],
["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
["set <JAIL> attempt <IP> [<failure1> ... <failureN>]", "manually notify about <IP> failure"],
["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip [--report-absent] <IP> ... <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> attempt [--expr] [--] <IP> [<failure1> ... <failureN>]", "manually notify about <IP> failure"],
["set <JAIL> banip [--expr] [--] <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip [--report-absent] [--expr] [--] <IP> ... <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxmatches <INT>", "sets the max number of matches stored in memory per ticket in <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
@ -124,7 +124,7 @@ protocol = [
["set <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]", "calls the <METHOD> with <JSONKWARGS> for the action <ACT> for <JAIL>"],
['', "JAIL INFORMATION", ""],
["get <JAIL> banned", "return banned IPs of <JAIL>"],
["get <JAIL> banned <IP> ... <IP>]", "return 1 if IP is banned in <JAIL> otherwise 0, or a list of 1/0 for multiple IPs"],
["get <JAIL> banned [--expr] [--] <IP> ... <IP>]", "return 1 if IP is banned in <JAIL> otherwise 0, or a list of 1/0 for multiple IPs"],
["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"],
["get <JAIL> logencoding", "gets the encoding of the log files for <JAIL>"],
["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"],

View File

@ -242,6 +242,33 @@ class Actions(JailThread, Mapping):
return self.__checkBan(tickets)
def removeMultiBannedIP(self, ips=None, db=True, ifexists=False):
"""Removes multiple banned IPs calling actions' unban method
Parameters
----------
ip : list, tuple or None
The IPs as list to unban or all IPs if None
Raises
------
ValueError
If at least one `ip` is not banned
"""
if ips is None:
return self.__flushBan(db)
missed = []
cnt = 0
for ip in ips:
try:
cnt += self.removeBannedIP(ip, db, ifexists)
except ValueError:
if not ifexists:
missed.append(ip)
if missed:
raise ValueError("not banned: %r" % missed)
return cnt
def removeBannedIP(self, ip=None, db=True, ifexists=False):
"""Removes banned IP calling actions' unban method
@ -250,8 +277,8 @@ class Actions(JailThread, Mapping):
Parameters
----------
ip : list, str, IPAddr or None
The IP address (or multiple IPs as list) to unban or all IPs if None
ip : str, IPAddr, TUPLE_ID representation (tuple/list) or None
The ID to unban or all IPs if None
Raises
------
@ -261,19 +288,6 @@ class Actions(JailThread, Mapping):
# Unban all?
if ip is None:
return self.__flushBan(db)
# Multiple IPs:
if isinstance(ip, (list, tuple)):
missed = []
cnt = 0
for i in ip:
try:
cnt += self.removeBannedIP(i, db, ifexists)
except ValueError:
if not ifexists:
missed.append(i)
if missed:
raise ValueError("not banned: %r" % missed)
return cnt
# Single IP:
# Always delete ip from database (also if currently not banned)
if db and self._jail.database is not None:
@ -285,14 +299,14 @@ class Actions(JailThread, Mapping):
self.__unBan(ticket)
else:
# Multiple IPs by subnet or dns:
if not isinstance(ip, IPAddr):
if not isinstance(ip, (IPAddr, tuple, list)):
ipa = IPAddr(ip)
if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname):
ips = list(filter(ipa.contains, self.banManager.getBanList()))
if ips:
return self.removeBannedIP(ips, db, ifexists)
return self.removeMultiBannedIP(ips, db, ifexists)
# not found:
msg = "%s is not banned" % ip
msg = "%s is not banned" % str(ip)
logSys.log(logging.MSG, msg)
if ifexists:
return 0

View File

@ -468,7 +468,7 @@ class Filter(JailThread):
def addAttempt(self, ip, *matches):
"""Generate a failed attempt for ip"""
if not isinstance(ip, IPAddr):
if not isinstance(ip, (IPAddr, list, tuple)):
ip = IPAddr(ip)
matches = list(matches) # tuple to list

View File

@ -24,13 +24,13 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import threading
from threading import Lock, RLock
import logging
import os
import signal
import stat
import sys
import threading
from threading import Lock, RLock
from .observer import Observers, ObserverThread
from .jails import Jails
@ -522,24 +522,24 @@ class Server:
def setBanTime(self, name, value):
self.__jails[name].actions.setBanTime(value)
def addAttemptIP(self, name, *args):
return self.__jails[name].filter.addAttempt(*args)
def addAttemptIP(self, name, ip, failures):
return self.__jails[name].filter.addAttempt(ip, *failures)
def setBanIP(self, name, value):
return self.__jails[name].actions.addBannedIP(value)
def setUnbanIP(self, name=None, value=None, ifexists=True):
def setUnbanIP(self, name=None, values=None, ifexists=True):
if name is not None:
# single jail:
jails = [self.__jails[name]]
else:
# in all jails:
jails = list(self.__jails.values())
# unban given or all (if value is None):
# unban given or all (if values is None):
cnt = 0
ifexists |= (name is None)
for jail in jails:
cnt += jail.actions.removeBannedIP(value, ifexists=ifexists)
cnt += jail.actions.removeMultiBannedIP(values, ifexists=ifexists)
return cnt
def banned(self, name=None, ids=None):
@ -563,7 +563,6 @@ class Server:
ret = jail.actions.getBanned(ids)
if name is not None:
return ret
res.append(ret)
else:
res.append({jail.name: ret})
return res

View File

@ -24,10 +24,11 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import getopt
import time
import json
from ..helpers import getLogger, logging
from ..helpers import getLogger, logging, parseExpressions
from .. import version
# Gets the instance of the logger.
@ -113,14 +114,19 @@ class Transmitter:
return 'OK'
elif name == "unban" and len(command) >= 2:
# unban in all jails:
value = command[1:]
opts, value = getopt.getopt(command[1:], "", ["expr", "all"])
opts = dict(opts)
# if all ips:
if len(value) == 1 and value[0] == "--all":
if "--all" in opts:
return self.__server.setUnbanIP()
value = parseExpressions(value) if "--expr" in opts else value
return self.__server.setUnbanIP(None, value)
elif name == "banned":
# check IP is banned in all jails:
return self.__server.banned(None, command[1:])
opts, value = getopt.getopt(command[1:], "", ["expr"])
opts = dict(opts)
value = parseExpressions(value) if "--expr" in opts else value
return self.__server.banned(None, value)
elif name == "echo":
return command[1:]
elif name == "server-status":
@ -228,13 +234,19 @@ class Transmitter:
if self.__quiet: return
return self.__server.getIgnoreSelf(name)
elif command[1] == "addignoreip":
for value in command[2:]:
self.__server.addIgnoreIP(name, value)
opts, value = getopt.getopt(command[2:], "", ["expr"])
opts = dict(opts)
isexpr = "--expr" in opts
for v in value:
self.__server.addIgnoreIP(name, parseExpressions(v) if isexpr else v)
if self.__quiet: return
return self.__server.getIgnoreIP(name)
elif command[1] == "delignoreip":
value = command[2]
self.__server.delIgnoreIP(name, value)
opts, value = getopt.getopt(command[2:], "", ["expr"])
opts = dict(opts)
isexpr = "--expr" in opts
for v in value:
self.__server.delIgnoreIP(name, parseExpressions(v) if isexpr else v)
if self.__quiet: return
return self.__server.getIgnoreIP(name)
elif command[1] == "ignorecommand":
@ -352,9 +364,11 @@ class Transmitter:
if self.__quiet: return
return self.__server.getBanTime(name)
elif command[1] == "attempt":
value = command[2:]
opts, value = getopt.getopt(command[2:], "", ["expr"])
opts = dict(opts)
if self.__quiet: return
return self.__server.addAttemptIP(name, *value)
ip = parseExpressions(value[0]) if "--expr" in opts else value[0]
return self.__server.addAttemptIP(name, ip, value[1:])
elif command[1].startswith("bantime."):
value = command[2]
opt = command[1][len("bantime."):]
@ -362,16 +376,15 @@ class Transmitter:
if self.__quiet: return
return self.__server.getBanTimeExtra(name, opt)
elif command[1] == "banip":
value = command[2:]
opts, value = getopt.getopt(command[2:], "", ["expr"])
opts = dict(opts)
value = parseExpressions(value) if "--expr" in opts else value
return self.__server.setBanIP(name,value)
elif command[1] == "unbanip":
ifexists = True
if command[2] != "--report-absent":
value = command[2:]
else:
ifexists = False
value = command[3:]
return self.__server.setUnbanIP(name, value, ifexists=ifexists)
opts, value = getopt.getopt(command[2:], "", ["expr", "report-absent"])
opts = dict(opts)
value = parseExpressions(value) if "--expr" in opts else value
return self.__server.setUnbanIP(name, value, ifexists=("--report-absent" not in opts))
elif command[1] == "addaction":
args = [command[2]]
if len(command) > 3:
@ -444,7 +457,10 @@ class Transmitter:
# Jail, Filter
elif command[1] == "banned":
# check IP is banned in all jails:
return self.__server.banned(name, command[2:])
opts, value = getopt.getopt(command[2:], "", ["expr"])
opts = dict(opts)
value = parseExpressions(value) if "--expr" in opts else value
return self.__server.banned(name, value)
elif command[1] == "logpath":
return self.__server.getLogPath(name)
elif command[1] == "logencoding":

View File

@ -1425,8 +1425,12 @@ class Fail2banServerTest(Fail2banClientServerBase):
all=True, wait=MID_WAITTIME
)
# unban 1, 2 and 5:
self.execCmd(SUCCESS, startparams, 'unban', '125-000-001', '125-000-002', '125-000-005')
# unban 1, 2 and 5 (as expr):
self.execCmd(SUCCESS, startparams, 'unban', '125-000-001', '125-000-002')
self.execCmd(SUCCESS, startparams, 'unban', '--expr', '"125-000-005"')
# check tuple IDs don't consider as a list of IDs to unban:
self.execCmd(SUCCESS, startparams, 'unban', '--expr', '("125-000-003","125-000-004")')
self.assertLogged('%r is not banned' % (('125-000-003', '125-000-004'),))
_out_file(mpfn)
# check really unbanned but other sessions are still present (blacklisted in map-file):
mp = _read_file(mpfn)

View File

@ -372,8 +372,13 @@ class Transmitter(TransmitterBase):
# ... (no error, IPs logged only):
self.assertEqual(
self.transm.proceed(
["set", self.jailName, "unbanip", "192.0.2.255", "192.0.2.254"]),(0, 0))
["set", self.jailName, "unbanip", "--", "192.0.2.255", "192.0.2.254"]),(0, 0))
self.assertLogged("192.0.2.255 is not banned", "192.0.2.254 is not banned", all=True, wait=True)
# tuple ID:
self.assertEqual(
self.transm.proceed(
["set", self.jailName, "unbanip", "--expr", "(1,2,3)"]),(0, 0))
self.assertLogged("%r is not banned" % ((1,2,3),), wait=True)
def testJailAttemptIP(self):
self.server.startJail(self.jailName) # Jail must be started