Mini Shell
import logging
from typing import Dict, FrozenSet, Iterable, List
from defence360agent.contracts.config import PORT_BLOCKING_MODE_ALLOW
from defence360agent.utils import retry_on, timeit
from im360.contracts.config import Firewall
from im360.internals.core import ip_versions
from im360.internals.core.firewall import FirewallRules, get_firewall
from im360.internals.core.firewall.base import FirewallBatchCommandError
from im360.internals.core.ipset import (
IP_SET_PREFIX,
AbstractIPSet,
IPSetAtomicRestoreBase,
IPSetCount,
get_ipset_family,
libipset,
)
from im360.internals.core.ipset.libipset import IPSetCmdBuilder
from im360.model.firewall import BlockedPort, IgnoredByPort
from defence360agent.utils.validate import IP, IPVersion, NumericIPVersion
from .redirect import IPSetNoRedirectPort
logger = logging.getLogger(__name__)
class SingleIPSetPort(IPSetAtomicRestoreBase):
TEMPLATE = "{prefix}.{ip_version}.ignored-by-{proto}-{port}"
def __init__(self, port, proto):
super().__init__(port, proto)
self.port = port
self.proto = proto
def gen_ipset_name_for_ip_version(self, ip_version: IPVersion) -> str:
return self.custom_ipset_name or self.TEMPLATE.format(
prefix=IP_SET_PREFIX,
ip_version=ip_version,
proto=self.proto,
port=self.port,
)
def gen_ipset_create_ops(self, ip_version: IPVersion) -> List[str]:
return [
IPSetCmdBuilder.get_create_cmd(
self.gen_ipset_name_for_ip_version(ip_version),
family=get_ipset_family(ip_version),
)
]
def gen_ipset_destroy_ops(self, ip_version: IPVersion) -> List[str]:
return [
IPSetCmdBuilder.get_destroy_cmd(
self.gen_ipset_name_for_ip_version(ip_version)
)
]
def gen_ipset_flush_ops(self, ip_version: IPVersion) -> List[str]:
return [
IPSetCmdBuilder.get_flush_cmd(
self.gen_ipset_name_for_ip_version(ip_version)
)
]
def _fetch(self, ip_version: IPVersion = None):
result = []
for row in IgnoredByPort.fetch(
NumericIPVersion.from_ip_version(ip_version)
).where(
BlockedPort.port == self.port, BlockedPort.proto == self.proto
):
result.append(row.ip)
return result
def db_count(self, ip_version: IPVersion = None):
return len(self._fetch(ip_version))
async def gen_ipset_restore_ops(self, ip_version: IPVersion) -> List[str]:
lines = []
name = self.gen_ipset_name_for_ip_version(ip_version)
for ip in self._fetch(ip_version):
lines.append(f"add {name} {ip} -exist")
return lines
class IPSetPort(AbstractIPSet):
TCP = "tcp"
UDP = "udp"
ALL = "all"
PROTOS = (TCP, UDP, ALL)
MIN_PORT, MAX_PORT = 0, 65535
def expand_proto(self, proto):
assert proto in self.PROTOS, 'protocol "{}" is not supported'.format(
proto
)
if proto == self.ALL:
return [self.TCP, self.UDP]
return [proto]
async def block(self, item, *args, **kwargs):
port, generic_proto = item
for ip_version in ip_versions.enabled():
async with await get_firewall(ip_version) as fw:
ipset = SingleIPSetPort(port=port, proto=generic_proto)
set_name = ipset.gen_ipset_name_for_ip_version(ip_version)
await libipset.create_hash_set(
set_name,
timeout=0,
set_type=libipset.HASH_NET,
family=get_ipset_family(ip_version),
)
actions = [
fw.append_rule(
FirewallRules.port_rule(
set_name,
port,
proto,
policy=FirewallRules.LOG_BLOCK_PORT_CHAIN,
),
chain=FirewallRules.BP_INPUT_CHAIN,
priority=FirewallRules.DEFAULT_PRIORITY,
)
for proto in self.expand_proto(generic_proto)
]
decorated = retry_on(FirewallBatchCommandError, max_tries=3)(
fw.commit
)
await decorated(actions)
await IPSetNoRedirectPort().add_item(port, ip_version)
async def unblock(self, item, *args, **kwargs):
port, generic_proto = item
for ip_version in ip_versions.enabled():
async with await get_firewall(ip_version) as fw:
ipset = SingleIPSetPort(port=port, proto=generic_proto)
set_name = ipset.gen_ipset_name_for_ip_version(ip_version)
actions = [
fw.delete_rule(
FirewallRules.port_rule(
set_name,
port,
proto,
policy=FirewallRules.LOG_BLOCK_PORT_CHAIN,
),
chain=FirewallRules.BP_INPUT_CHAIN,
priority=FirewallRules.DEFAULT_PRIORITY,
)
for proto in self.expand_proto(generic_proto)
]
decorated = retry_on(FirewallBatchCommandError, max_tries=3)(
fw.commit
)
await decorated(actions)
await IPSetNoRedirectPort().delete_item(port, ip_version)
ipset = SingleIPSetPort(port=port, proto=generic_proto)
set_name = ipset.gen_ipset_name_for_ip_version(ip_version)
await libipset.delete_set(set_name)
async def gen_ipset_restore_ops(self, ip_version: IPVersion):
return []
def get_all_ipset_instances(
self, ip_version: IPVersion
) -> List[SingleIPSetPort]:
if Firewall.port_blocking_mode != PORT_BLOCKING_MODE_ALLOW:
return []
result = []
for port, proto in self._fetch():
result.append(SingleIPSetPort(port=port, proto=proto))
return result
def gen_ipset_create_ops(self, ip_version: IPVersion) -> List[str]:
"""
Generate list of commands to create all ip sets
:return: list of ipset commands to use with ipset restore
"""
result: List[str] = []
for ip_set in self.get_all_ipset_instances(ip_version):
result.extend(ip_set.gen_ipset_create_ops(ip_version))
return result
def _fetch(self):
return [(row.port, row.proto) for row in BlockedPort.select()]
def get_all_ipsets(self, ip_version: IPVersion) -> FrozenSet[str]:
return frozenset(
ipset.gen_ipset_name_for_ip_version(ip_version)
for ipset in self.get_all_ipset_instances(ip_version)
)
def get_rules(self, ip_version: IPVersion, **kwargs) -> Iterable[dict]:
if Firewall.port_blocking_mode != PORT_BLOCKING_MODE_ALLOW:
return []
result = [
dict(
rule=FirewallRules.compose_action(
FirewallRules.BP_INPUT_CHAIN
),
chain=FirewallRules.IMUNIFY_INPUT_CHAIN,
table=FirewallRules.FILTER,
priority=FirewallRules.PORT_PROTO_PRIORITY,
)
]
for ipset in self.get_all_ipset_instances(ip_version):
result.extend(
[
dict(
rule=FirewallRules.port_rule(
ipset.gen_ipset_name_for_ip_version(ip_version),
ipset.port,
proto,
policy=FirewallRules.LOG_BLOCK_PORT_CHAIN,
),
chain=FirewallRules.BP_INPUT_CHAIN,
table=FirewallRules.FILTER,
priority=FirewallRules.DEFAULT_PRIORITY,
)
for proto in self.expand_proto(ipset.proto)
]
)
return result
async def restore(self, ip_version: IPVersion) -> None:
pass
async def get_ipsets_count(self, ip_version: IPVersion) -> list:
return []
class IPSetIgnoredByPort(AbstractIPSet):
async def block(self, ip, port, proto, *args, **kwargs):
ipset = SingleIPSetPort(port=port, proto=proto)
ip_version = IP.type_of(ip)
await libipset.add_item(
ipset.gen_ipset_name_for_ip_version(ip_version), ip, timeout=0
)
async def unblock(self, ip, port, proto, *args, **kwargs):
ipset = SingleIPSetPort(port=port, proto=proto)
ip_version = IP.type_of(ip)
await libipset.delete_item(
ipset.gen_ipset_name_for_ip_version(ip_version), ip
)
def get_all_ipset_instances(
self, ip_version: IPVersion
) -> List[SingleIPSetPort]:
if Firewall.port_blocking_mode != PORT_BLOCKING_MODE_ALLOW:
return []
result = []
for port, proto in self._fetch(ip_version):
result.append(SingleIPSetPort(port=port, proto=proto))
return result
async def gen_ipset_restore_ops(self, ip_version: IPVersion):
"""
Generate list of commands to fill all ip sets
:return: list of ipset commands to use with ipset restore
"""
lines = []
for ipset in self.get_all_ipset_instances(ip_version):
lines.extend(await ipset.gen_ipset_restore_ops(ip_version))
return lines
def _fetch(self, ip_version: IPVersion):
return [
(row.port_proto.port, row.port_proto.proto)
for row in IgnoredByPort.fetch(
NumericIPVersion.from_ip_version(ip_version)
)
]
def gen_ipset_create_ops(self, ip_version: IPVersion) -> List[str]:
# Just to follow an interface, there are no ipsets for this entity
return []
def get_all_ipsets(self, ip_version: IPVersion) -> FrozenSet[str]:
return frozenset()
def get_rules(self, ip_version: IPVersion, **kwargs) -> Iterable[dict]:
return []
def is_enabled(self):
return Firewall.port_blocking_mode == PORT_BLOCKING_MODE_ALLOW
async def restore(self, ip_version: IPVersion) -> None:
if not self.is_enabled():
return
with timeit("ipset_restore", logger):
await libipset.restore(
await self.gen_ipset_restore_ops(ip_version)
)
async def get_ipsets_count(self, ip_version: IPVersion) -> list:
if not self.is_enabled():
return []
ipsets_count: Dict[str, int] = {} # ipset name -> ips in db count
for ipset in self.get_all_ipset_instances(ip_version):
set_name = ipset.gen_ipset_name_for_ip_version(ip_version)
ipsets_count[set_name] = ipset.db_count(ip_version)
ipsets = []
for set_name, expected_count in ipsets_count.items():
ipset_count = await libipset.get_ipset_count(set_name)
ipsets.append(
IPSetCount(
name=set_name,
db_count=expected_count,
ipset_count=ipset_count,
)
)
return ipsets