Commit a9efc009 authored by Vitaly Lipatov's avatar Vitaly Lipatov

gateway: restore active bans from Redis

parent 090ca570
[Settings] [Settings]
# blocking requests queue # blocking requests queue
#redis_server = 10.20.30.101 #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 # Redirect banned to
#ban_server = 192.168.0.0 #ban_server = 192.168.0.0
......
...@@ -13,12 +13,13 @@ import re ...@@ -13,12 +13,13 @@ import re
from autoban_manager import AutoBanManager from autoban_manager import AutoBanManager
path_to_config = '/etc/eterban/settings.ini' path_to_config = '/etc/eterban/settings.ini'
path_to_eterban = '/usr/share/eterban/'
ipset_eterban_1 = 'eterban_1' ipset_eterban_1 = 'eterban_1'
ipset_eterban_1_ipv6 = 'eterban_1_ipv6' ipset_eterban_1_ipv6 = 'eterban_1_ipv6'
ipset_firehol = 'firehol_level1' ipset_firehol = 'firehol_level1'
ipset_eterban_white = 'eterban_white' ipset_eterban_white = 'eterban_white'
ipset_eterban_white_ipv6 = 'eterban_white_ipv6' ipset_eterban_white_ipv6 = 'eterban_white_ipv6'
redis_bans_key = 'eterban:active_bans'
redis_bans_initialized_key = 'eterban:active_bans:initialized'
try: try:
path_to_log = '/var/log/eterban/eterban.log' path_to_log = '/var/log/eterban/eterban.log'
...@@ -80,19 +81,12 @@ def parse_config (path_to_config, path_to_log): ...@@ -80,19 +81,12 @@ def parse_config (path_to_config, path_to_log):
maxelem = config.getint("Settings", "maxelem", fallback=2000000) maxelem = config.getint("Settings", "maxelem", fallback=2000000)
return (redis_server, ban_server, ban_server_ipv6, wan_ifaces, internal_interface, maxelem, whitelist_file) return (redis_server, ban_server, ban_server_ipv6, wan_ifaces, internal_interface, maxelem, whitelist_file)
def save_ipset_eterban_1(): def restore_legacy_ipsets():
global ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol, path_to_eterban """One-time migration fallback for snapshots made by older releases."""
# whitelist is not saved: it is rebuilt from whitelist_file on startup global ipset_eterban_1, ipset_eterban_1_ipv6, ipset_firehol
name_list = [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: for name in name_list:
command = 'ipset save ' + name + ' --file ' + path_to_eterban + name command='ipset restore --file /usr/share/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
subprocess.call (command, shell = True) subprocess.call (command, shell = True)
...@@ -264,7 +258,6 @@ def destroy_ip6tables_rules (): ...@@ -264,7 +258,6 @@ def destroy_ip6tables_rules ():
def exit_gracefully(signum, frame): def exit_gracefully(signum, frame):
save_ipset_eterban_1()
destroy_iptables_rules() destroy_iptables_rules()
destroy_ip6tables_rules() destroy_ip6tables_rules()
...@@ -293,6 +286,93 @@ def log_redis_error(message): ...@@ -293,6 +286,93 @@ def log_redis_error(message):
log.flush() 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(): def connect_redis():
"""Connect to Redis and subscribe, retrying after a connection failure.""" """Connect to Redis and subscribe, retrying after a connection failure."""
while True: while True:
...@@ -349,9 +429,14 @@ if auto_mgr.enabled: ...@@ -349,9 +429,14 @@ if auto_mgr.enabled:
checker_thread = threading.Thread(target=auto_unban_checker, daemon=True) checker_thread = threading.Thread(target=auto_unban_checker, daemon=True)
checker_thread.start() 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_iptables_rules()
create_ip6tables_rules() create_ip6tables_rules()
initialize_ban_state()
restore_bans_from_redis()
load_whitelist() load_whitelist()
...@@ -366,6 +451,8 @@ def process_message(message): ...@@ -366,6 +451,8 @@ def process_message(message):
log.write(info) log.write(info)
log.flush() log.flush()
return return
if not persist_ban(ip):
return
if isinstance(ipo, ipaddress.IPv6Address): if isinstance(ipo, ipaddress.IPv6Address):
ban = 'ipset -A ' + ipset_eterban_1_ipv6 + ' ' + ip ban = 'ipset -A ' + ipset_eterban_1_ipv6 + ' ' + ip
else: else:
...@@ -391,7 +478,11 @@ def process_message(message): ...@@ -391,7 +478,11 @@ def process_message(message):
elif isinstance(ipo, ipaddress.IPv4Network): elif isinstance(ipo, ipaddress.IPv4Network):
unban = 'ipset -D ' + ipset_eterban_1 + ' ' + ip unban = 'ipset -D ' + ipset_eterban_1 + ' ' + ip
#add = 'ipset -A ' + ipset_eterban_white + ' ' + 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) #subprocess.call (add, shell = True)
tcp_drop = 'conntrack -D -s ' + ip tcp_drop = 'conntrack -D -s ' + ip
subprocess.Popen(tcp_drop, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True) 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