Commit a9efc009 authored by Vitaly Lipatov's avatar Vitaly Lipatov

gateway: restore active bans from Redis

parent 090ca570
[Settings]
# blocking requests queue
#redis_server = 10.20.30.101
# Redis stores the authoritative active-ban set. Configure the Redis server
# with durable persistence (for example: appendonly yes, appendfsync everysec).
# Redirect banned to
#ban_server = 192.168.0.0
......
......@@ -13,12 +13,13 @@ import re
from autoban_manager import AutoBanManager
path_to_config = '/etc/eterban/settings.ini'
path_to_eterban = '/usr/share/eterban/'
ipset_eterban_1 = 'eterban_1'
ipset_eterban_1_ipv6 = 'eterban_1_ipv6'
ipset_firehol = 'firehol_level1'
ipset_eterban_white = 'eterban_white'
ipset_eterban_white_ipv6 = 'eterban_white_ipv6'
redis_bans_key = 'eterban:active_bans'
redis_bans_initialized_key = 'eterban:active_bans:initialized'
try:
path_to_log = '/var/log/eterban/eterban.log'
......@@ -80,19 +81,12 @@ def parse_config (path_to_config, path_to_log):
maxelem = config.getint("Settings", "maxelem", fallback=2000000)
return (redis_server, ban_server, ban_server_ipv6, wan_ifaces, internal_interface, maxelem, whitelist_file)
def save_ipset_eterban_1():
global ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol, path_to_eterban
# whitelist is not saved: it is rebuilt from whitelist_file on startup
def restore_legacy_ipsets():
"""One-time migration fallback for snapshots made by older releases."""
global ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol
name_list = [ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol]
for name in name_list:
command = 'ipset save ' + name + ' --file ' + path_to_eterban + name
subprocess.call (command, shell = True)
def restore_ipset_eterban_1():
global ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol, path_to_eterban
name_list = [ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol]
for name in name_list:
command='ipset restore --file ' + path_to_eterban + name
command='ipset restore --file /usr/share/eterban/' + name
subprocess.call (command, shell = True)
......@@ -264,7 +258,6 @@ def destroy_ip6tables_rules ():
def exit_gracefully(signum, frame):
save_ipset_eterban_1()
destroy_iptables_rules()
destroy_ip6tables_rules()
......@@ -293,6 +286,93 @@ def log_redis_error(message):
log.flush()
def get_ipset_members(setname):
"""Return valid address members from an ipset without parsing human output."""
try:
result = subprocess.run(
['ipset', 'save', setname], text=True, capture_output=True,
check=True, timeout=10)
except (OSError, subprocess.SubprocessError) as error:
log_redis_error("Unable to read " + setname + " for Redis migration: " + str(error))
return []
members = []
for line in result.stdout.splitlines():
fields = line.split()
if len(fields) < 3 or fields[0] != 'add' or fields[1] != setname:
continue
try:
members.append(str(ipaddress.ip_address(fields[2])))
except ValueError:
log_redis_error("Ignoring invalid legacy ipset entry: " + fields[2])
return members
def initialize_ban_state():
"""Import the old ipset snapshot once, then make Redis authoritative."""
try:
if r.exists(redis_bans_initialized_key):
return
members = get_ipset_members(ipset_eterban_1)
members.extend(get_ipset_members(ipset_eterban_1_ipv6))
pipeline = r.pipeline()
if members:
pipeline.sadd(redis_bans_key, *members)
pipeline.set(redis_bans_initialized_key, '1')
pipeline.execute()
log_redis_error("Initialized Redis ban state with " + str(len(members)) + " migrated entries")
except redis.exceptions.RedisError as error:
log_redis_error("Unable to initialize Redis ban state: " + str(error))
def restore_bans_from_redis():
"""Rebuild the local ban ipsets from the durable Redis set."""
try:
members = r.smembers(redis_bans_key)
except redis.exceptions.RedisError as error:
log_redis_error("Unable to read Redis ban state: " + str(error))
return
grouped = {ipset_eterban_1: [], ipset_eterban_1_ipv6: []}
for member in members:
ip = member.decode('utf-8') if isinstance(member, bytes) else member
try:
address = ipaddress.ip_address(ip)
except ValueError:
log_redis_error("Ignoring invalid Redis ban state entry: " + str(ip))
continue
setname = ipset_eterban_1_ipv6 if isinstance(address, ipaddress.IPv6Address) else ipset_eterban_1
grouped[setname].append(str(address))
for setname, addresses in grouped.items():
try:
subprocess.run(['ipset', 'flush', setname], check=True, timeout=10)
for ip in addresses:
subprocess.run(['ipset', 'add', setname, ip, '-exist'], check=True, timeout=10)
except (OSError, subprocess.SubprocessError) as error:
log_redis_error("Unable to restore " + setname + " from Redis: " + str(error))
continue
log_redis_error("Restored " + str(sum(len(addresses) for addresses in grouped.values())) + " ban entries from Redis")
def persist_ban(ip):
try:
r.sadd(redis_bans_key, ip)
return True
except redis.exceptions.RedisError as error:
log_redis_error("Unable to persist ban " + ip + " in Redis: " + str(error))
return False
def remove_persisted_ban(ip):
try:
r.srem(redis_bans_key, ip)
return True
except redis.exceptions.RedisError as error:
log_redis_error("Unable to remove ban " + ip + " from Redis: " + str(error))
return False
def connect_redis():
"""Connect to Redis and subscribe, retrying after a connection failure."""
while True:
......@@ -349,9 +429,14 @@ if auto_mgr.enabled:
checker_thread = threading.Thread(target=auto_unban_checker, daemon=True)
checker_thread.start()
restore_ipset_eterban_1()
# Older releases also kept the FireHOL set in this snapshot. The manual ban
# sets are flushed below and rebuilt from Redis, so the snapshot is only a
# migration source for them.
restore_legacy_ipsets()
create_iptables_rules()
create_ip6tables_rules()
initialize_ban_state()
restore_bans_from_redis()
load_whitelist()
......@@ -366,6 +451,8 @@ def process_message(message):
log.write(info)
log.flush()
return
if not persist_ban(ip):
return
if isinstance(ipo, ipaddress.IPv6Address):
ban = 'ipset -A ' + ipset_eterban_1_ipv6 + ' ' + ip
else:
......@@ -391,7 +478,11 @@ def process_message(message):
elif isinstance(ipo, ipaddress.IPv4Network):
unban = 'ipset -D ' + ipset_eterban_1 + ' ' + ip
#add = 'ipset -A ' + ipset_eterban_white + ' ' + ip
subprocess.call (unban, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True)
if subprocess.call(unban + ' -exist', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True) != 0:
log_redis_error("Unable to remove ban from ipset: " + ip)
return
if not remove_persisted_ban(ip):
return
#subprocess.call (add, shell = True)
tcp_drop = 'conntrack -D -s ' + ip
subprocess.Popen(tcp_drop, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment