
18 changed files with 956 additions and 460 deletions
@ -0,0 +1,12 @@ |
|||
# Changelog |
|||
|
|||
This file is not intended as a dupplicate of Git logs. |
|||
Its purpose is to warn of important changes between version, that users should be aware of. |
|||
|
|||
## v2.0 |
|||
|
|||
After this version is installed, the following command should be run on the `action_nftBan.py.json` file: |
|||
|
|||
```bash |
|||
$ sudo sed -i s/nftSet/nfSet/g action_nftBan.py.json |
|||
``` |
File diff suppressed because it is too large
@ -0,0 +1,3 @@ |
|||
[Unit] |
|||
Requires=iptables.service |
|||
After=iptables.service |
@ -0,0 +1,37 @@ |
|||
# pyruse is intended as a replacement to both fail2ban and epylog |
|||
# Copyright © 2017–2018 Y. Gablin |
|||
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. |
|||
import os |
|||
import subprocess |
|||
from pyruse import ban, base, config |
|||
|
|||
class Action(base.Action, ban.NetfilterBan): |
|||
_storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \ |
|||
+ "/" + os.path.basename(__file__) + ".json" |
|||
_ipset = config.Config().asMap().get("ipsetBan", {}).get("ipset", ["/usr/bin/ipset", "-exist", "-quiet"]) |
|||
|
|||
def __init__(self, args): |
|||
base.Action.__init__(self) |
|||
ban.NetfilterBan.__init__(self, Action._storage) |
|||
if args is None: |
|||
return # on-boot configuration |
|||
ipv4Set = args["ipSetIPv4"] |
|||
ipv6Set = args["ipSetIPv6"] |
|||
field = args["IP"] |
|||
banSeconds = args.get("banSeconds", None) |
|||
self.initSelf(ipv4Set, ipv6Set, field, banSeconds) |
|||
|
|||
def act(self, entry): |
|||
ban.NetfilterBan.act(self, entry) |
|||
|
|||
def setBan(self, nfSet, ip, seconds): |
|||
cmd = list(Action._ipset) |
|||
cmd.extend(["add", nfSet, ip]) |
|||
if seconds > 0: |
|||
cmd.extend(["timeout", str(seconds)]) |
|||
subprocess.run(cmd) |
|||
|
|||
def cancelBan(self, nfSet, ip): |
|||
cmd = list(Action._ipset) |
|||
cmd.extend(["del", nfSet, ip]) |
|||
subprocess.run(cmd) |
@ -1,97 +1,39 @@ |
|||
# pyruse is intended as a replacement to both fail2ban and epylog |
|||
# Copyright © 2017–2018 Y. Gablin |
|||
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. |
|||
import datetime |
|||
import json |
|||
import os |
|||
import subprocess |
|||
from pyruse import base, config |
|||
from pyruse import ban, base, config |
|||
|
|||
class Action(base.Action): |
|||
class Action(base.Action, ban.NetfilterBan): |
|||
_storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \ |
|||
+ "/" + os.path.basename(__file__) + ".json" |
|||
_nft = config.Config().asMap().get("nftBan", {}).get("nft", ["/usr/bin/nft"]) |
|||
|
|||
def __init__(self, args): |
|||
super().__init__() |
|||
base.Action.__init__(self) |
|||
ban.NetfilterBan.__init__(self, Action._storage) |
|||
if args is None: |
|||
return # on-boot configuration |
|||
self.ipv4Set = args["nftSetIPv4"] |
|||
self.ipv6Set = args["nftSetIPv6"] |
|||
self.field = args["IP"] |
|||
self.banSeconds = args.get("banSeconds", None) |
|||
ipv4Set = args["nftSetIPv4"] |
|||
ipv6Set = args["nftSetIPv6"] |
|||
field = args["IP"] |
|||
banSeconds = args.get("banSeconds", None) |
|||
self.initSelf(ipv4Set, ipv6Set, field, banSeconds) |
|||
|
|||
def act(self, entry): |
|||
ip = entry[self.field] |
|||
nftSet = self.ipv6Set if ":" in ip else self.ipv4Set |
|||
newBan = {"IP": ip, "nftSet": nftSet} |
|||
ban.NetfilterBan.act(self, entry) |
|||
|
|||
now = datetime.datetime.utcnow() |
|||
bans = [] |
|||
previousTS = None |
|||
try: |
|||
with open(Action._storage) as dataFile: |
|||
for ban in json.load(dataFile): |
|||
if ban["timestamp"] > 0 and ban["timestamp"] <= now.timestamp(): |
|||
continue |
|||
elif {k: ban[k] for k in newBan.keys()} == newBan: |
|||
# should not happen, since the IP is banned… |
|||
previousTS = ban["timestamp"] |
|||
else: |
|||
bans.append(ban) |
|||
except IOError: |
|||
pass # new file |
|||
|
|||
if previousTS is not None: |
|||
try: |
|||
cmd = list(Action._nft) |
|||
cmd.append("delete element %s {%s}" % (nftSet, ip)) |
|||
subprocess.run(cmd) |
|||
except Exception: |
|||
pass # too late: not a problem |
|||
|
|||
if self.banSeconds: |
|||
until = now + datetime.timedelta(seconds = self.banSeconds) |
|||
newBan["timestamp"] = until.timestamp() |
|||
timeout = self.banSeconds |
|||
else: |
|||
newBan["timestamp"] = 0 |
|||
timeout = 0 |
|||
|
|||
self._doBan(timeout, ip, nftSet) |
|||
bans.append(newBan) |
|||
with open(Action._storage, "w") as dataFile: |
|||
json.dump(bans, dataFile) |
|||
|
|||
def boot(self): |
|||
now = datetime.datetime.utcnow() |
|||
bans = [] |
|||
try: |
|||
with open(Action._storage) as dataFile: |
|||
for ban in json.load(dataFile): |
|||
if ban["timestamp"] == 0: |
|||
self._doBan(0, ban["IP"], ban["nftSet"]) |
|||
bans.append(ban) |
|||
elif ban["timestamp"] <= now.timestamp(): |
|||
continue |
|||
else: |
|||
until = datetime.datetime.utcfromtimestamp(ban["timestamp"]) |
|||
timeout = (until - now).total_seconds() |
|||
self._doBan(int(timeout), ban["IP"], ban["nftSet"]) |
|||
bans.append(ban) |
|||
except IOError: |
|||
pass # no file |
|||
|
|||
with open(Action._storage, "w") as dataFile: |
|||
json.dump(bans, dataFile) |
|||
|
|||
def _doBan(self, seconds, ip, nftSet): |
|||
if seconds < 0: |
|||
return # can happen when the threshold is crossed while computing the duration |
|||
def setBan(self, nfSet, ip, seconds): |
|||
if seconds == 0: |
|||
timeout = "" |
|||
else: |
|||
timeout = " timeout %ss" % seconds |
|||
cmd = list(Action._nft) |
|||
cmd.append("add element %s {%s%s}" % (nftSet, ip, timeout)) |
|||
cmd.append("add element %s {%s%s}" % (nfSet, ip, timeout)) |
|||
subprocess.run(cmd) |
|||
|
|||
def cancelBan(self, nfSet, ip): |
|||
cmd = list(Action._nft) |
|||
cmd.append("delete element %s {%s}" % (nfSet, ip)) |
|||
subprocess.run(cmd) |
|||
|
@ -0,0 +1,85 @@ |
|||
# pyruse is intended as a replacement to both fail2ban and epylog |
|||
# Copyright © 2017–2018 Y. Gablin |
|||
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. |
|||
import abc |
|||
import datetime |
|||
import json |
|||
|
|||
class NetfilterBan(abc.ABC): |
|||
def __init__(self, storage): |
|||
self.storage = storage |
|||
|
|||
def initSelf(self, ipv4Set, ipv6Set, ipField, banSeconds): |
|||
self.ipv4Set = ipv4Set |
|||
self.ipv6Set = ipv6Set |
|||
self.field = ipField |
|||
self.banSeconds = banSeconds |
|||
|
|||
@abc.abstractmethod |
|||
def setBan(self, nfSet, ip, seconds): |
|||
pass |
|||
|
|||
@abc.abstractmethod |
|||
def cancelBan(self, nfSet, ip): |
|||
pass |
|||
|
|||
def act(self, entry): |
|||
ip = entry[self.field] |
|||
nfSet = self.ipv6Set if ":" in ip else self.ipv4Set |
|||
newBan = {"IP": ip, "nfSet": nfSet} |
|||
|
|||
now = datetime.datetime.utcnow() |
|||
bans = [] |
|||
previousTS = None |
|||
try: |
|||
with open(self.storage) as dataFile: |
|||
for ban in json.load(dataFile): |
|||
if ban["timestamp"] > 0 and ban["timestamp"] <= now.timestamp(): |
|||
continue |
|||
elif {k: ban[k] for k in newBan.keys()} == newBan: |
|||
# should not happen, since the IP is banned… |
|||
previousTS = ban["timestamp"] |
|||
else: |
|||
bans.append(ban) |
|||
except IOError: |
|||
pass # new file |
|||
|
|||
if previousTS is not None: |
|||
try: |
|||
self.cancelBan(nfSet, ip) |
|||
except Exception: |
|||
pass # too late: not a problem |
|||
|
|||
if self.banSeconds: |
|||
until = now + datetime.timedelta(seconds = self.banSeconds) |
|||
newBan["timestamp"] = until.timestamp() |
|||
timeout = self.banSeconds |
|||
else: |
|||
newBan["timestamp"] = 0 |
|||
timeout = 0 |
|||
|
|||
self.setBan(nfSet, ip, timeout) |
|||
bans.append(newBan) |
|||
with open(self.storage, "w") as dataFile: |
|||
json.dump(bans, dataFile) |
|||
|
|||
def boot(self): |
|||
now = int(datetime.datetime.utcnow().timestamp()) |
|||
bans = [] |
|||
try: |
|||
with open(self.storage) as dataFile: |
|||
for ban in json.load(dataFile): |
|||
if ban["timestamp"] == 0: |
|||
self.setBan(ban["nfSet"], ban["IP"], 0) |
|||
bans.append(ban) |
|||
elif int(ban["timestamp"]) <= now: |
|||
continue |
|||
else: |
|||
timeout = int(ban["timestamp"]) - now |
|||
self.setBan(ban["nfSet"], ban["IP"], timeout) |
|||
bans.append(ban) |
|||
except IOError: |
|||
pass # no file |
|||
|
|||
with open(self.storage, "w") as dataFile: |
|||
json.dump(bans, dataFile) |
@ -0,0 +1,147 @@ |
|||
# pyruse is intended as a replacement to both fail2ban and epylog |
|||
# Copyright © 2017–2018 Y. Gablin |
|||
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. |
|||
import json |
|||
import os |
|||
import time |
|||
from pyruse.actions.action_ipsetBan import Action |
|||
|
|||
ipBanCmd = "ipsetBan.cmd" |
|||
ipBanState = "action_ipsetBan.py.json" |
|||
|
|||
def _clean(): |
|||
if os.path.exists(ipBanCmd): |
|||
os.remove(ipBanCmd) |
|||
if os.path.exists(ipBanState): |
|||
os.remove(ipBanState) |
|||
|
|||
def whenBanIPv4ThenAddToIPv4Set(): |
|||
_clean() |
|||
Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "10.0.0.1"}) |
|||
assert os.path.exists(ipBanCmd) |
|||
assert os.path.exists(ipBanState) |
|||
nbLines = 0 |
|||
with open(ipBanCmd, "rt") as c: |
|||
for line in c: |
|||
assert line == "add I4ban 10.0.0.1\n", line |
|||
nbLines += 1 |
|||
assert nbLines == 1, nbLines |
|||
nbBans = 0 |
|||
with open(ipBanState) as s: |
|||
for ban in json.load(s): |
|||
assert ban["IP"] == "10.0.0.1" and ban["nfSet"] == "I4ban", str(ban) |
|||
nbBans += 1 |
|||
assert nbBans == 1, nbBans |
|||
_clean() |
|||
|
|||
def whenBanIPv6ThenAddToIPv6Set(): |
|||
_clean() |
|||
Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "::1"}) |
|||
assert os.path.exists(ipBanCmd) |
|||
assert os.path.exists(ipBanState) |
|||
nbLines = 0 |
|||
with open(ipBanCmd, "rt") as c: |
|||
for line in c: |
|||
assert line == "add I6ban ::1\n", line |
|||
nbLines += 1 |
|||
assert nbLines == 1, nbLines |
|||
nbBans = 0 |
|||
with open(ipBanState) as s: |
|||
for ban in json.load(s): |
|||
assert ban["IP"] == "::1" and ban["nfSet"] == "I6ban", str(ban) |
|||
nbBans += 1 |
|||
assert nbBans == 1, nbBans |
|||
_clean() |
|||
|
|||
def whenBanTwoIPThenTwoLinesInState(): |
|||
_clean() |
|||
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
action.act({"ip": "::1"}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
assert os.path.exists(ipBanState) |
|||
nbBans = 0 |
|||
with open(ipBanState) as s: |
|||
for ban in json.load(s): |
|||
if ban["IP"] == "10.0.0.1": |
|||
assert ban["nfSet"] == "I4ban", str(ban) |
|||
elif ban["IP"] == "::1": |
|||
assert ban["nfSet"] == "I6ban", str(ban) |
|||
else: |
|||
assert false, str(ban) |
|||
nbBans += 1 |
|||
assert nbBans == 2, nbBans |
|||
_clean() |
|||
|
|||
def whenBanAnewThenNoDuplicate(): |
|||
_clean() |
|||
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
assert os.path.exists(ipBanCmd) |
|||
assert os.path.exists(ipBanState) |
|||
lineCount = 0 |
|||
with open(ipBanCmd, "rt") as c: |
|||
for line in c: |
|||
lineCount += 1 |
|||
if lineCount == 1: |
|||
assert line == "add I4ban 10.0.0.1\n", line |
|||
elif lineCount == 2: |
|||
assert line == "del I4ban 10.0.0.1\n", line |
|||
elif lineCount == 3: |
|||
assert line == "add I4ban 10.0.0.1\n", line |
|||
assert lineCount == 3, lineCount |
|||
nbBans = 0 |
|||
with open(ipBanState) as s: |
|||
for ban in json.load(s): |
|||
if ban["IP"] == "10.0.0.1": |
|||
assert ban["nfSet"] == "I4ban", str(ban) |
|||
nbBans += 1 |
|||
assert nbBans == 1, nbBans |
|||
_clean() |
|||
|
|||
def whenFinishedBanThenAsIfNotThere(): |
|||
_clean() |
|||
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 1}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
time.sleep(1) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
assert os.path.exists(ipBanCmd) |
|||
lineCount = 0 |
|||
with open(ipBanCmd, "rt") as c: |
|||
for line in c: |
|||
lineCount += 1 |
|||
if lineCount == 1: |
|||
assert line == "add I4ban 10.0.0.1 timeout 1\n", line |
|||
elif lineCount == 2: |
|||
assert line == "add I4ban 10.0.0.1 timeout 1\n", line |
|||
assert lineCount == 2, lineCount |
|||
_clean() |
|||
|
|||
def whenUnfinishedBanThenTimeoutReset(): |
|||
_clean() |
|||
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 2}) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
time.sleep(1) |
|||
action.act({"ip": "10.0.0.1"}) |
|||
assert os.path.exists(ipBanCmd) |
|||
lineCount = 0 |
|||
with open(ipBanCmd, "rt") as c: |
|||
for line in c: |
|||
lineCount += 1 |
|||
if lineCount == 1: |
|||
assert line == "add I4ban 10.0.0.1 timeout 2\n", line |
|||
elif lineCount == 2: |
|||
assert line == "del I4ban 10.0.0.1\n", line |
|||
elif lineCount == 3: |
|||
assert line == "add I4ban 10.0.0.1 timeout 2\n", line |
|||
assert lineCount == 3, lineCount |
|||
_clean() |
|||
|
|||
def unitTests(): |
|||
whenBanIPv4ThenAddToIPv4Set() |
|||
whenBanIPv6ThenAddToIPv6Set() |
|||
whenBanTwoIPThenTwoLinesInState() |
|||
whenBanAnewThenNoDuplicate() |
|||
whenFinishedBanThenAsIfNotThere() |
|||
whenUnfinishedBanThenTimeoutReset() |
Loading…
Reference in new issue