Mini Shell
import abc
import hashlib
import logging
from functools import wraps
from typing import List, Self
from defence360agent.utils import log_error_and_ignore, timeit
from im360.internals.core import ip_versions
from im360.internals.core.ipset import libipset
from defence360agent.utils.validate import IPVersion
logger = logging.getLogger(__name__)
def ignore_if_ipset_not_found(coro):
@wraps(coro)
async def wrapper(self, *args, **kwargs):
try:
return await coro(self, *args, **kwargs)
except libipset.IPSetNotFoundError:
logger.warning(
"%s not found. Skip '%s' command. Expect that actual ipset "
"content will be restored from persistent during the next "
"check of iptables rules/ipsets.",
self.__class__.__qualname__,
coro.__name__,
)
return wrapper
def raise_error_if_disabled(coro):
@wraps(coro)
async def wrapper(self, *args, **kwargs):
if not self.is_enabled():
raise RuntimeError(
f"Trying to use {coro.__name__} when "
f"{self.__class__.__qualname__} is disabled."
)
return await coro(self, *args, **kwargs)
return wrapper
class SingleIPSetInterface(abc.ABC):
"""
Interface for managing an IPSet. Implementing classes should represent
a single IPSet. AbstractIPSet should be used as a collection of
SingleIPSetInterface implementations.
Example:
- InputPortBlockingDenyModeIPSet: A collection of IP sets related to
port blocking in deny mode (derived from AbstractIPSet).
- i360.ipv6.input-ports-tcp, i360.ipv6.input-ports-udp: Instances of
PortBlockingDenyModeIPSetManager (an implementation of
SingleIPSetInterface).
"""
@abc.abstractmethod
def gen_ipset_create_ops(self, ip_version: IPVersion) -> List[str]:
pass
@abc.abstractmethod
def gen_ipset_destroy_ops(self, ip_version: IPVersion) -> List[str]:
pass
@abc.abstractmethod
def gen_ipset_flush_ops(self, ip_version: IPVersion) -> List[str]:
pass
@abc.abstractmethod
async def gen_ipset_restore_ops(self, ip_version: IPVersion) -> List[str]:
pass
@abc.abstractmethod
def gen_ipset_name_for_ip_version(self, ip_version: IPVersion) -> str:
pass
def is_enabled(self, ip_version: IPVersion = None):
return True
@log_error_and_ignore(
exception=libipset.IgnoredIPSetKernelError, log_handler=logger.warning
)
@ignore_if_ipset_not_found
async def restore_from_persistent(self, ip_version: IPVersion):
with timeit(
"Restoring records for %s" % self.__class__.__name__, logger
):
await libipset.restore(
await self.gen_ipset_restore_ops(ip_version),
name=self.gen_ipset_name_for_ip_version(ip_version),
)
class IPSetAtomicRestoreBase(SingleIPSetInterface, abc.ABC):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.custom_ipset_name = None
def clone_instance(self, new_ipset_name: str) -> Self:
"""
Create a copy of this instance and set a custom name
for the related ipset.
"""
inst = self.__class__(*self.args, **self.kwargs)
inst.custom_ipset_name = new_ipset_name
return inst
async def reset(self, ip_version: IPVersion = None):
if ip_version:
ip_versions_to_reset = [ip_version]
else:
ip_versions_to_reset = ip_versions.enabled()
for ip_version in ip_versions_to_reset:
try:
logger.info(
"Resetting %s ipset",
self.gen_ipset_name_for_ip_version(ip_version),
)
await self.restore_from_persistent_atomic(ip_version)
except ValueError as e:
logger.error(
"Failed to reset %s ipset: %s",
self.gen_ipset_name_for_ip_version(ip_version),
e,
)
async def exists(self, ip_version: IPVersion = None):
name = self.gen_ipset_name_for_ip_version(ip_version)
return name in await libipset.list_set()
@staticmethod
def get_tmp_ipset_name(original_ipset_name: str) -> str:
tmp_ipset_hash = hashlib.sha1(original_ipset_name.encode()).hexdigest()
return "i360." + tmp_ipset_hash[:20] + ".tmp"
async def restore_from_persistent_atomic(self, ip_version: IPVersion):
target_ipset_name = self.gen_ipset_name_for_ip_version(ip_version)
tmp_ipset_name = self.get_tmp_ipset_name(target_ipset_name)
tmp_ipset: IPSetAtomicRestoreBase = self.clone_instance(tmp_ipset_name)
create_ipset_cmd = tmp_ipset.gen_ipset_create_ops(ip_version)
if not create_ipset_cmd:
return
await libipset.restore(create_ipset_cmd)
try:
await libipset.flush_set(tmp_ipset_name)
await tmp_ipset.restore_from_persistent(ip_version)
await libipset.swap(tmp_ipset_name, target_ipset_name)
finally:
flush_ipset_cmd = tmp_ipset.gen_ipset_flush_ops(ip_version)
destroy_ipset_cmd = tmp_ipset.gen_ipset_destroy_ops(ip_version)
if flush_ipset_cmd:
await libipset.restore(flush_ipset_cmd)
if destroy_ipset_cmd:
await libipset.restore(destroy_ipset_cmd)