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

View File

@ -28,6 +28,7 @@ import re
import sys import sys
import traceback import traceback
from ast import literal_eval
from threading import Lock from threading import Lock
from .server.mytime import MyTime from .server.mytime import MyTime
@ -317,6 +318,16 @@ def _merge_copy_dicts(x, y):
""" """
return {**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='...']`). # 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)"], ["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"], ["stop", "stops all jails and terminate the server"],
["unban --all", "unbans all IP addresses (in all jails and database)"], ["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", "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", "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'"], ["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"], ["stat[istic]s", "gets the current statistics of all jails as table"],
@ -87,8 +87,8 @@ protocol = [
['', "JAIL CONFIGURATION", ""], ['', "JAIL CONFIGURATION", ""],
["set <JAIL> idle on|off", "sets the idle state of <JAIL>"], ["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> ignoreself true|false", "allows the ignoring of own IP addresses"],
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"], ["set <JAIL> addignoreip [--expr] [--] <IP> ... <IP>", "adds <IP> to the ignore list of <JAIL>"],
["set <JAIL> delignoreip <IP>", "removes <IP> from 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> ignorecommand <VALUE>", "sets ignorecommand of <JAIL>"],
["set <JAIL> ignorecache <VALUE>", "sets ignorecache 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')."], ["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> 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> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"],
["set <JAIL> usedns <VALUE>", "sets the usedns mode 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> attempt [--expr] [--] <IP> [<failure1> ... <failureN>]", "manually notify about <IP> failure"],
["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <JAIL>"], ["set <JAIL> banip [--expr] [--] <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip [--report-absent] <IP> ... <IP>", "manually Unban <IP> in <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> 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> 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>"], ["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>"], ["set <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]", "calls the <METHOD> with <JSONKWARGS> for the action <ACT> for <JAIL>"],
['', "JAIL INFORMATION", ""], ['', "JAIL INFORMATION", ""],
["get <JAIL> banned", "return banned IPs of <JAIL>"], ["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> logpath", "gets the list of the monitored files for <JAIL>"],
["get <JAIL> logencoding", "gets the encoding of the log 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>"], ["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"],

View File

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

View File

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

View File

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

View File

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

View File

@ -1425,8 +1425,12 @@ class Fail2banServerTest(Fail2banClientServerBase):
all=True, wait=MID_WAITTIME all=True, wait=MID_WAITTIME
) )
# unban 1, 2 and 5: # unban 1, 2 and 5 (as expr):
self.execCmd(SUCCESS, startparams, 'unban', '125-000-001', '125-000-002', '125-000-005') 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) _out_file(mpfn)
# check really unbanned but other sessions are still present (blacklisted in map-file): # check really unbanned but other sessions are still present (blacklisted in map-file):
mp = _read_file(mpfn) mp = _read_file(mpfn)

View File

@ -372,8 +372,13 @@ class Transmitter(TransmitterBase):
# ... (no error, IPs logged only): # ... (no error, IPs logged only):
self.assertEqual( self.assertEqual(
self.transm.proceed( 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) 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): def testJailAttemptIP(self):
self.server.startJail(self.jailName) # Jail must be started self.server.startJail(self.jailName) # Jail must be started