class FirewallWrapper: NETWORKBLOCK_IPSET4 = 'networkblock4' NETWORKBLOCK_IPSET6 = 'networkblock6' NETWORKBLOCK_IPSET_BASE_NAME = 'networkblock' def __init__(self): self.fw = FirewallClient() self.config = self.fw.config() if not self.config: log.warning('FirewallD is not running attempting to start...') import subprocess import time subprocess.check_output( ['systemctl', 'enable', '--now', 'firewalld']) # firewall-cmd synchronously waits for FirewallD startup subprocess.check_output(['firewall-cmd', '--state']) self.fw = FirewallClient() self.config = self.fw.config() def get_create_set(self, name, family='inet'): if name in self.config.getIPSetNames(): return self.config.getIPSetByName(name) settings = FirewallClientIPSetSettings() settings.setType('hash:net') settings.setOptions({ 'maxelem': '1000000', 'family': family, 'hashsize': '4096' }) return self.config.addIPSet(name, settings) def get_block_ipset4(self, name=None): if not name: name = FirewallWrapper.NETWORKBLOCK_IPSET_BASE_NAME name = name + '4' if name in self.config.getIPSetNames(): return self.config.getIPSetByName(name) settings = FirewallClientIPSetSettings() settings.setType('hash:net') settings.setOptions({ 'maxelem': '1000000', 'family': 'inet', 'hashsize': '4096' }) return self.config.addIPSet(name, settings) def get_block_ipset6(self, name=None): if not name: name = FirewallWrapper.NETWORKBLOCK_IPSET_BASE_NAME name = name + '6' if name in self.config.getIPSetNames(): return self.config.getIPSetByName(name) settings = FirewallClientIPSetSettings() settings.setType('hash:net') settings.setOptions({ 'maxelem': '1000000', 'family': 'inet6', 'hashsize': '4096' }) return self.config.addIPSet(name, settings) def get_block_ipset_for_ip(self, ip, name=None): if ip.version == 4: return self.get_block_ipset4(name) if ip.version == 6: return self.get_block_ipset6(name) return None @do_maybe_already_enabled def ensure_ipset_entries(self, ipset, entries): return ipset.setEntries(entries) @do_maybe_already_enabled def ensure_entry_in_ipset(self, ipset, entry): return ipset.addEntry(str(entry)) @do_maybe_already_enabled def ensure_entry_not_in_ipset(self, ipset, entry): return ipset.removeEntry(str(entry)) @do_maybe_already_enabled def ensure_block_ipset_in_drop_zone(self, ipset): # ensure that the block ipset is in drop zone: drop_zone = self.config.getZoneByName('drop') # self.config.getIPSetNames return drop_zone.addSource('ipset:{}'.format( ipset.get_property('name'))) @do_maybe_already_enabled def add_service(self, name, zone='public'): self.fw.addService(zone, name) self.fw.runtimeToPermanent() @do_maybe_already_enabled def block_ip(self, ip, ipset_name=None, reload=True): block_ipset = self.get_block_ipset_for_ip(ip, ipset_name) if not block_ipset: # TODO err: unsupported protocol raise Exception('Unsupported protocol') self.ensure_block_ipset_in_drop_zone(block_ipset) log.info('Adding IP address {} to block set {}'.format( ip, block_ipset.get_property('name'))) try: from aggregate6 import aggregate entries = [] for entry in block_ipset.getEntries(): entries.append(str(entry)) entries.append(str(ip)) block_ipset.setEntries(aggregate(entries)) except ImportError: block_ipset.addEntry(str(ip)) if reload: log.info('Reloading FirewallD to apply permanent configuration') self.fw.reload() log.info('Breaking connection with {}'.format(ip)) from subprocess import CalledProcessError, check_output, STDOUT try: check_output(["/sbin/conntrack", "-D", "-s", str(ip)], stderr=STDOUT) except CalledProcessError as e: pass def get_blocked_ips4(self, name=None): block_ipset4 = self.get_block_ipset4(name) return block_ipset4.getEntries() def get_blocked_ips6(self, name=None): block_ipset6 = self.get_block_ipset6(name) return block_ipset6.getEntries() @do_maybe_not_enabled def remove_ipset_from_zone(self, zone, ipset_name): # drop_zone.removeSource('ipset:') zone.removeSource('ipset:{}'.format(ipset_name)) @do_maybe_invalid_ipset def clear_ipset_by_name(self, ipset_name): try: # does not work: ipset.setEntries([]) self.fw.setEntries(ipset_name, []) except dbus.exceptions.DBusException: pass @do_maybe_invalid_ipset def destroy_ipset_by_name(self, name): log.info('Destroying IPSet {}'.format(name)) # firewalld up to this commit # https://github.com/firewalld/firewalld/commit/f5ed30ce71755155493e78c13fd9036be8f70fc4 # does not delete runtime ipsets, so we have to clear them :( # they are not removed from runtime as still reported by ipset -L # although they *are* removed from FirewallD if name not in self.fw.getIPSets(): return ipset = self.config.getIPSetByName(name) if ipset: self.clear_ipset_by_name(name) ipset.remove() def get_blocked_countries(self): blocked_countries = [] all_ipsets = self.fw.getIPSets() from .Countries import Countries countries = Countries() for ipset_name in all_ipsets: if ipset_name.startswith('fds-'): country_code = ipset_name.split('-')[1] if country_code in countries.names_by_code: blocked_countries.append( countries.names_by_code[country_code]) return blocked_countries def update_ipsets(self): need_reload = False all_ipsets = self.fw.getIPSets() from .Countries import Countries countries = Countries() is_tor_blocked = False for ipset_name in all_ipsets: if ipset_name.startswith('fds-tor-'): is_tor_blocked = True elif ipset_name.startswith('fds-'): country_code = ipset_name.split('-')[1] if country_code in countries.names_by_code: country_name = countries.names_by_code[country_code] country = countries.get_by_name(country_name) self.block_country(country, reload=False) need_reload = True if is_tor_blocked: self.block_tor(reload=False) need_reload = True if need_reload: self.fw.reload() return True def reset(self): drop_zone = self.config.getZoneByName('drop') self.remove_ipset_from_zone(drop_zone, self.NETWORKBLOCK_IPSET4) self.destroy_ipset_by_name(self.NETWORKBLOCK_IPSET4) self.remove_ipset_from_zone(drop_zone, self.NETWORKBLOCK_IPSET6) self.destroy_ipset_by_name(self.NETWORKBLOCK_IPSET6) all_ipsets = self.fw.getIPSets() # get any ipsets prefixed with "fds-" for ipset_name in all_ipsets: if ipset_name.startswith('fds-'): self.remove_ipset_from_zone(drop_zone, ipset_name) self.destroy_ipset_by_name(ipset_name) self.fw.reload() def block_tor(self, reload=True): log.info('Blocking Tor exit nodes') w = WebClient() tor4_exits = w.get_tor_exits(family=4) tor6_exits = w.get_tor_exits(family=6) tor4_ipset = self.get_create_set('fds-tor-4') self.ensure_ipset_entries(tor4_ipset, tor4_exits) tor6_ipset = self.get_create_set('fds-tor-6', family='inet6') self.ensure_ipset_entries(tor6_ipset, tor6_exits) self.ensure_block_ipset_in_drop_zone(tor4_ipset) self.ensure_block_ipset_in_drop_zone(tor6_ipset) if reload: log.info('Reloading FirewallD...') self.fw.reload() log.info('Done!') # while cron will do "sync" behavior" def block_country(self, country, reload=True): # print('address/netmask is invalid: %s' % sys.argv[1]) # parse out as a country log.info('Blocking {} {}'.format(country.name, country.getFlag())) # print("\N{grinning face}") # TODO get aggregated zone file, save as cache, # do diff to know which stuff was changed and add/remove blocks # https://docs.python.org/2/library/difflib.html # TODO persist info on which countries were blocked (in the config file) # then sync zones via "fds cron" # TODO conditional get test on getpagespeed.com w = WebClient() country_networks = w.get_country_networks(country=country) ipset = self.get_create_set(country.get_set_name()) self.ensure_ipset_entries(ipset, country_networks) # this is slow. setEntries is a lot faster # for network in tqdm(country_networks, unit='network', # desc='Adding {} networks to IPSet {}'.format(c.getNation(), c.get_set_name())): # log.debug(network) # fw.ensure_entry_in_ipset(ipset=ipset, entry=network) # TODO retry, timeout # this action re-adds all entries entirely # there should be "fds-<country.code>-<family>" ip set self.ensure_block_ipset_in_drop_zone(ipset) if reload: log.info('Reloading FirewallD...') self.fw.reload() log.info('Done!') # while cron will do "sync" behavior" def unblock_country(self, ip_or_country_name): # print('address/netmask is invalid: %s' % sys.argv[1]) # parse out as a country from .Countries import Countries countries = Countries() c = countries.get_by_name(ip_or_country_name) if not c: log.error( '{} does not look like a correct IP or a country name'.format( ip_or_country_name)) return False drop_zone = self.config.getZoneByName('drop') log.info('Unblocking {} {}'.format(c.name, c.getFlag())) self.remove_ipset_from_zone(drop_zone, c.get_set_name()) self.destroy_ipset_by_name(c.get_set_name()) log.info('Reloading FirewallD...') self.fw.reload() log.info('Done!') # while cron will do "sync" behavior" def unblock_ip(self, ip_or_country_name): block_ipset = self.get_block_ipset_for_ip(ip_or_country_name) if not block_ipset: # TODO err: unsupported protocol raise Exception('Unsupported protocol') log.info('Removing {} from block set {}'.format( ip_or_country_name, block_ipset.get_property('name'))) self.ensure_entry_not_in_ipset(block_ipset, ip_or_country_name) log.info('Reloading FirewallD to apply permanent configuration') self.fw.reload()
def main(): module_args = dict( name=dict(type="str", required=True), state=dict(choices=["present", "absent"], required=True), settype=dict( choices=[ "hash:ip", "hash:ip,port", "hash:ip,port,ip", "hash:ip,port,net", "hash:ip,mark", "hash:net", "hash:net,net", "hash:net,port", "hash:net,port,net", "hash:net,iface", "hash:mac", ], required=True, ), permanent=dict(type="bool", required=False, default=False), immediate=dict(type="bool", required=False, default=False), addresses=dict(type="list", required=False), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) FirewallTransaction.sanity_check(module) # setup the FirewallDClient client = FirewallClient() sets = client.getIPSets() settings = FirewallClientIPSetSettings() settings.setType(module.params["settype"]) config = client.config() # construct return data result = {"firewalld_ipset_name": module.params["name"]} # Modifying a preexisting ipset if module.params["name"] in sets and module.params["state"] == "present": client_ipset_config = config.getIPSetByName(module.params["name"]) original_entries = client_ipset_config.getEntries() # when we're in check mode we shouldn't commit anything to the # state of the target. Just output what would happen. if module.check_mode: new_entries = module.params["addresses"] else: client_ipset_config.setEntries(module.params["addresses"]) firewalld_state(client, module.params) new_entries = client_ipset_config.getEntries() result["changed"] = new_entries != original_entries result["firewalld_ipset_addresses"] = new_entries # Creating a new ipset because the one proposed in the module declaration # does not exist already. elif module.params["name"] not in sets and module.params["state"] == "present": if module.check_mode: result["firewalld_ipset_addresses"] = module.params["addresses"] else: client_ipset_config = config.addIPSet(module.params["name"], settings) client_ipset_config.setEntries(module.params["addresses"]) firewalld_state(client, module.params) new_entries = client_ipset_config.getEntries() result["firewalld_ipset_addresses"] = new_entries result["changed"] = True # Removing an ipset that exists right now. If an ipset is asked to be # removed when it doesn't exist, we should just return an "unchanged" # state. elif module.params["name"] in sets and module.params["state"] == "absent": client_ipset_config = config.getIPSetByName(module.params["name"]) # avoid stateful actions in check mode if not module.check_mode: original_entries = client_ipset_config.remove() firewalld_state(client, module.params) result["changed"] = True result["firewalld_ipset_addresses"] = [] module.exit_json(**result)