class Firewall_test(object): def __init__(self): self._firewalld_conf = firewalld_conf(FIREWALLD_CONF) self.ip4tables_enabled = False self.ip6tables_enabled = False self.ebtables_enabled = False self.ipset_enabled = False self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] self._min_mark = FALLBACK_MINIMAL_MARK # will be overloaded by firewalld.conf self.cleanup_on_exit = True self.ipv6_rpfilter_enabled = True def start(self): # initialize firewall default_zone = FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.error("Failed to open firewalld config file '%s': %s", FIREWALLD_CONF, msg) else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): mark = self._firewalld_conf.get("MinimalMark") if mark is not None: try: self._min_mark = int(mark) except Exception as msg: log.error("MinimalMark %s is not valid, using default " "value %d", mark, self._min_mark) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(FIREWALLD_IPSETS, "ipset") self._loader(ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(FIREWALLD_ICMPTYPES, "icmptype") self._loader(ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load service files self._loader(FIREWALLD_SERVICES, "service") self._loader(ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(FIREWALLD_ZONES, "zone") self._loader(ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # load direct rules obj = Direct(FIREWALLD_DIRECT) if os.path.exists(FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", FIREWALLD_DIRECT, msg) self.config.set_direct(copy.deepcopy(obj)) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) self._default_zone = self.check_zone(default_zone) self._state = "RUNNING" def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) self.ipset.add_ipset(obj) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def cleanup(self): self.icmptype.cleanup() self.service.cleanup() # no self.ipset.cleanup(), this would remove active ipsets self.zone.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): self.cleanup() # check functions def check_panic(self): return def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug2("'%s': port > 65535" % port) elif range == -1: log.debug2("'%s': port is invalid" % port) elif range is None: log.debug2("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug2("'%s': range start >= end" % port) raise FirewallError(INVALID_PORT, port) def check_protocol(self, protocol): if not protocol: raise FirewallError(MISSING_PROTOCOL) if protocol not in [ "tcp", "udp" ]: raise FirewallError(INVALID_PROTOCOL, protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(INVALID_ADDR, source) else: raise FirewallError(INVALID_IPV) def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) # RELOAD def reload(self, stop=False): return # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): return def disable_panic_mode(self): return def query_panic_mode(self): return self._panic # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() else: raise FirewallError(ZONE_ALREADY_SET, _zone) # lockdown def enable_lockdown(self): self._firewalld_conf.set("Lockdown", "yes") self._firewalld_conf.write() def disable_lockdown(self): self._firewalld_conf.set("Lockdown", "no") self._firewalld_conf.write()
class Firewall(object): def __init__(self): self._firewalld_conf = firewalld_conf(config.FIREWALLD_CONF) self.ip4tables_backend = ipXtables.ip4tables(self) self.ip4tables_enabled = True self.ip4tables_supported_icmp_types = [] self.ip6tables_backend = ipXtables.ip6tables(self) self.ip6tables_enabled = True self.ip6tables_supported_icmp_types = [] self.ebtables_backend = ebtables.ebtables() self.ebtables_enabled = True self.ipset_backend = ipset.ipset() self.ipset_enabled = True self.ipset_supported_types = [] self.nftables_backend = nftables.nftables(self) self.nftables_enabled = True self.modules_backend = modules.modules() self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.helper = FirewallHelper(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled, self._individual_calls, self._log_denied, self._automatic_helpers) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = {} self._marks = [] # fallback settings will be overloaded by firewalld.conf self._min_mark = config.FALLBACK_MINIMAL_MARK self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS self._log_denied = config.FALLBACK_LOG_DENIED self._automatic_helpers = config.FALLBACK_AUTOMATIC_HELPERS self._firewall_backend = config.FALLBACK_FIREWALL_BACKEND self._flush_all_on_reload = config.FALLBACK_FLUSH_ALL_ON_RELOAD self.nf_conntrack_helper_setting = 0 self.nf_conntrack_helpers = {} self.nf_nat_helpers = {} def individual_calls(self): return self._individual_calls def _check_tables(self): # check if iptables, ip6tables and ebtables are usable, else disable if self.ip4tables_enabled and \ "filter" not in self.get_backend_by_ipv("ipv4").get_available_tables(): log.warning("iptables not usable, disabling IPv4 firewall.") self.ip4tables_enabled = False if self.ip6tables_enabled and \ "filter" not in self.get_backend_by_ipv("ipv6").get_available_tables(): log.warning("ip6tables not usable, disabling IPv6 firewall.") self.ip6tables_enabled = False if self.ebtables_enabled and \ "filter" not in self.get_backend_by_ipv("eb").get_available_tables(): log.warning( "ebtables not usable, disabling ethernet bridge firewall.") self.ebtables_enabled = False # is there at least support for ipv4 or ipv6 if not self.ip4tables_enabled and not self.ip6tables_enabled \ and not self.nftables_enabled: log.fatal("No IPv4 and IPv6 firewall.") sys.exit(1) def _start_check(self): try: self.ipset_backend.set_list() except ValueError: log.warning("ipset not usable, disabling ipset usage in firewall.") # ipset is not usable, no supported types self.ipset_enabled = False self.ipset_supported_types = [] else: # ipset is usable, get all supported types self.ipset_supported_types = self.ipset_backend.set_supported_types( ) self.ip4tables_backend.fill_exists() if not self.ip4tables_backend.restore_command_exists: if self.ip4tables_backend.command_exists: log.warning("iptables-restore is missing, using " "individual calls for IPv4 firewall.") else: log.warning("iptables-restore and iptables are missing, " "disabling IPv4 firewall.") self.ip4tables_enabled = False if self.ip4tables_enabled: self.ip4tables_supported_icmp_types = \ self.ip4tables_backend.supported_icmp_types() else: self.ip4tables_supported_icmp_types = [] self.ip6tables_backend.fill_exists() if not self.ip6tables_backend.restore_command_exists: if self.ip6tables_backend.command_exists: log.warning("ip6tables-restore is missing, using " "individual calls for IPv6 firewall.") else: log.warning("ip6tables-restore and ip6tables are missing, " "disabling IPv6 firewall.") self.ip6tables_enabled = False if self.ip6tables_enabled: self.ip6tables_supported_icmp_types = \ self.ip6tables_backend.supported_icmp_types() else: self.ip6tables_supported_icmp_types = [] self.ebtables_backend.fill_exists() if not self.ebtables_backend.restore_command_exists: if self.ebtables_backend.command_exists: log.warning("ebtables-restore is missing, using " "individual calls for bridge firewall.") else: log.warning("ebtables-restore and ebtables are missing, " "disabling bridge firewall.") self.ebtables_enabled = False if self.ebtables_enabled and not self._individual_calls and \ not self.ebtables_backend.restore_noflush_option: log.debug1("ebtables-restore is not supporting the --noflush " "option, will therefore not be used") if os.path.exists(config.COMMANDS["modinfo"]): self.nf_conntrack_helpers = functions.get_nf_conntrack_helpers() if len(self.nf_conntrack_helpers) > 0: log.debug1("Conntrack helpers supported by the kernel:") for key, values in self.nf_conntrack_helpers.items(): log.debug1(" %s: %s", key, ", ".join(values)) else: log.debug1("No conntrack helpers supported by the kernel.") self.nf_nat_helpers = functions.get_nf_nat_helpers() if len(self.nf_nat_helpers) > 0: log.debug1("NAT helpers supported by the kernel:") for key, values in self.nf_nat_helpers.items(): log.debug1(" %s: %s", key, ", ".join(values)) else: log.debug1("No NAT helpers supported by the kernel.") else: self.nf_conntrack_helpers = {} self.nf_nat_helpers = {} log.warning( "modinfo command is missing, not able to detect conntrack helpers." ) def _start(self, reload=False, complete_reload=False): # initialize firewall default_zone = config.FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning(msg) log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in ["no", "false"]: self.cleanup_on_exit = False log.debug1("CleanupOnExit is set to '%s'", self.cleanup_on_exit) if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in ["yes", "true"]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in ["no", "false"]: self.ipv6_rpfilter_enabled = False if value.lower() in ["yes", "true"]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") if self._firewalld_conf.get("IndividualCalls"): value = self._firewalld_conf.get("IndividualCalls") if value is not None and value.lower() in ["yes", "true"]: log.debug1("IndividualCalls is enabled") self._individual_calls = True if self._firewalld_conf.get("LogDenied"): value = self._firewalld_conf.get("LogDenied") if value is None or value.lower() == "no": self._log_denied = "off" else: self._log_denied = value.lower() log.debug1("LogDenied is set to '%s'", self._log_denied) if self._firewalld_conf.get("AutomaticHelpers"): value = self._firewalld_conf.get("AutomaticHelpers") if value is not None: if value.lower() in ["no", "false"]: self._automatic_helpers = "no" elif value.lower() in ["yes", "true"]: self._automatic_helpers = "yes" else: self._automatic_helpers = value.lower() log.debug1("AutomaticHelpers is set to '%s'", self._automatic_helpers) if self._firewalld_conf.get("FirewallBackend"): self._firewall_backend = self._firewalld_conf.get( "FirewallBackend") log.debug1("FirewallBackend is set to '%s'", self._firewall_backend) if self._firewalld_conf.get("FlushAllOnReload"): value = self._firewalld_conf.get("FlushAllOnReload") if value.lower() in ["no", "false"]: self._flush_all_on_reload = False else: self._flush_all_on_reload = True log.debug1("FlushAllOnReload is set to '%s'", self._flush_all_on_reload) self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) self._select_firewall_backend(self._firewall_backend) self._start_check() # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(config.FIREWALLD_IPSETS, "ipset") self._loader(config.ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(config.FIREWALLD_ICMPTYPES, "icmptype") self._loader(config.ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load helper files self._loader(config.FIREWALLD_HELPERS, "helper") self._loader(config.ETC_FIREWALLD_HELPERS, "helper") # load service files self._loader(config.FIREWALLD_SERVICES, "service") self._loader(config.ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(config.FIREWALLD_ZONES, "zone") self._loader(config.ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in ["block", "drop", "trusted"]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) # load direct rules obj = Direct(config.FIREWALLD_DIRECT) if os.path.exists(config.FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % \ config.FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.error("Failed to load direct rules file '%s': %s", config.FIREWALLD_DIRECT, msg) self.direct.set_permanent_config(obj) self.config.set_direct(copy.deepcopy(obj)) # automatic helpers # # NOTE: must force loading of nf_conntrack to make sure the values are # available in /proc module_return = self.handle_modules(["nf_conntrack"], True) if module_return: log.error("Failed to load nf_conntrack module: %s" % module_return[1]) sys.exit(1) if self._automatic_helpers != "system": functions.set_nf_conntrack_helper_setting( self._automatic_helpers == "yes") self.nf_conntrack_helper_setting = \ functions.get_nf_conntrack_helper_setting() # check if needed tables are there self._check_tables() if log.getDebugLogLevel() > 0: # get time before flushing and applying tm1 = time.time() # Start transaction transaction = FirewallTransaction(self) # flush rules self.flush(use_transaction=transaction) # If modules need to be unloaded in complete reload or if there are # ipsets to get applied, limit the transaction to flush. # # Future optimization for the ipset case in reload: The transaction # only needs to be split here if there are conflicting ipset types in # exsting ipsets and the configuration in firewalld. if (reload and complete_reload) or \ (self.ipset_enabled and self.ipset.has_ipsets()): transaction.execute(True) transaction.clear() # complete reload: unload modules also if reload and complete_reload: log.debug1("Unloading firewall modules") self.modules_backend.unload_firewall_modules() self.apply_default_tables(use_transaction=transaction) transaction.execute(True) transaction.clear() # apply settings for loaded ipsets while reloading here if self.ipset_enabled and self.ipset.has_ipsets(): log.debug1("Applying ipsets") self.ipset.apply_ipsets() # Start or continue with transaction # apply default rules log.debug1("Applying default rule set") self.apply_default_rules(use_transaction=transaction) # apply settings for loaded zones log.debug1("Applying used zones") self.zone.apply_zones(use_transaction=transaction) self._default_zone = self.check_zone(default_zone) self.zone.change_default_zone(None, self._default_zone, use_transaction=transaction) # Execute transaction transaction.execute(True) # Start new transaction for direct rules transaction.clear() # apply direct chains, rules and passthrough rules if self.direct.has_configuration(): log.debug1("Applying direct chains rules and passthrough rules") self.direct.apply_direct(transaction) # since direct rules are easy to make syntax errors lets highlight # the cause if the transaction fails. try: transaction.execute(True) transaction.clear() except FirewallError as e: raise FirewallError(e.code, "Direct: %s" % (e.msg if e.msg else "")) except Exception: raise del transaction if log.getDebugLogLevel() > 1: # get time after flushing and applying tm2 = time.time() log.debug2("Flushing and applying took %f seconds" % (tm2 - tm1)) def start(self): try: self._start() except Exception: self._state = "FAILED" self.set_policy("ACCEPT") raise else: self._state = "RUNNING" self.set_policy("ACCEPT") def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(config.ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(config.ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True try: self.icmptype.add_icmptype(obj) except FirewallError as error: log.info1("%s: %s, ignoring for run-time." % \ (obj.name, str(error))) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path, no_check_name=combine) if combine: # Change name for permanent configuration obj.name = "%s/%s" % (os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True config_obj.default = True self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True try: self.ipset.add_ipset(obj) except FirewallError as error: log.warning("%s: %s, ignoring for run-time." % \ (obj.name, str(error))) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) elif reader_type == "helper": obj = helper_reader(filename, path) if obj.name in self.helper.get_helpers(): orig_obj = self.helper.get_helper(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.helper.remove_helper(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.helper.add_helper(obj) # add a deep copy to the configuration interface self.config.add_helper(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except Exception: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.ipset.cleanup() self.helper.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): if self.cleanup_on_exit: self.flush() self.set_policy("ACCEPT") self.modules_backend.unload_firewall_modules() self.cleanup() # marks def new_mark(self): # return first unused mark i = self._min_mark while i in self._marks: i += 1 self._marks.append(i) return i def del_mark(self, mark): self._marks.remove(mark) # handle modules def handle_modules(self, _modules, enable): for i, module in enumerate(_modules): if enable: (status, msg) = self.modules_backend.load_module(module) else: if self._module_refcount[module] > 1: status = 0 # module referenced more then one, do not unload else: (status, msg) = self.modules_backend.unload_module(module) if status != 0: if enable: return (_modules[:i], msg) # cleanup modules and error msg # else: ignore cleanup if enable: self._module_refcount.setdefault(module, 0) self._module_refcount[module] += 1 else: if module in self._module_refcount: self._module_refcount[module] -= 1 if self._module_refcount[module] == 0: del self._module_refcount[module] return None def _select_firewall_backend(self, backend): if backend != "nftables": self.nftables_enabled = False # even if using nftables, the other backends are enabled for use with # the direct interface. nftables is used for the firewalld primitives. def get_backend_by_name(self, name): for backend in self.all_backends(): if backend.name == name: return backend raise FirewallError(errors.UNKNOWN_ERROR, "'%s' backend does not exist" % name) def get_backend_by_ipv(self, ipv): if self.nftables_enabled: return self.nftables_backend if ipv == "ipv4": return self.ip4tables_backend elif ipv == "ipv6": return self.ip6tables_backend elif ipv == "eb": return self.ebtables_backend raise FirewallError(errors.INVALID_IPV, "'%s' is not a valid backend" % ipv) def get_direct_backend_by_ipv(self, ipv): if ipv == "ipv4": return self.ip4tables_backend elif ipv == "ipv6": return self.ip6tables_backend elif ipv == "eb": return self.ebtables_backend raise FirewallError(errors.INVALID_IPV, "'%s' is not a valid backend" % ipv) def is_backend_enabled(self, name): if name == "ip4tables": return self.ip4tables_enabled elif name == "ip6tables": return self.ip6tables_enabled elif name == "ebtables": return self.ebtables_enabled elif name == "nftables": return self.nftables_enabled return False def is_ipv_enabled(self, ipv): if self.nftables_enabled: return True if ipv == "ipv4": return self.ip4tables_enabled elif ipv == "ipv6": return self.ip6tables_enabled elif ipv == "eb": return self.ebtables_enabled return False def enabled_backends(self): backends = [] if self.nftables_enabled: backends.append(self.nftables_backend) else: if self.ip4tables_enabled: backends.append(self.ip4tables_backend) if self.ip6tables_enabled: backends.append(self.ip6tables_backend) if self.ebtables_enabled: backends.append(self.ebtables_backend) return backends def all_backends(self): backends = [] if self.ip4tables_enabled: backends.append(self.ip4tables_backend) if self.ip6tables_enabled: backends.append(self.ip6tables_backend) if self.ebtables_enabled: backends.append(self.ebtables_backend) if self.nftables_enabled: backends.append(self.nftables_backend) return backends def apply_default_tables(self, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction for backend in self.enabled_backends(): transaction.add_rules(backend, backend.build_default_tables()) if use_transaction is None: transaction.execute(True) def apply_default_rules(self, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction for backend in self.enabled_backends(): rules = backend.build_default_rules(self._log_denied) transaction.add_rules(backend, rules) ipv6_backend = self.get_backend_by_ipv("ipv6") if self.ipv6_rpfilter_enabled and \ "raw" in ipv6_backend.get_available_tables(): # Execute existing transaction transaction.execute(True) # Start new transaction transaction.clear() rules = ipv6_backend.build_rpfilter_rules(self._log_denied) transaction.add_rules(ipv6_backend, rules) # Execute ipv6_rpfilter transaction, it might fail try: transaction.execute(True) except FirewallError as msg: log.warning("Applying rules for ipv6_rpfilter failed: %s", msg) # Start new transaction transaction.clear() else: if use_transaction is None: transaction.execute(True) # flush and policy def flush(self, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction log.debug1("Flushing rule set") for backend in self.all_backends(): rules = backend.build_flush_rules() transaction.add_rules(backend, rules) if use_transaction is None: transaction.execute(True) def set_policy(self, policy, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction log.debug1("Setting policy to '%s'", policy) for backend in self.enabled_backends(): rules = backend.build_set_policy_rules(policy) transaction.add_rules(backend, rules) if use_transaction is None: transaction.execute(True) # rule function used in handle_ functions def rule(self, backend_name, rule): if not rule: return "" backend = self.get_backend_by_name(backend_name) if not backend: raise FirewallError(errors.INVALID_IPV, "'%s' is not a valid backend" % backend_name) if not self.is_backend_enabled(backend_name): return "" return backend.set_rule(rule, self._log_denied) def rules(self, backend_name, rules): _rules = list(filter(None, rules)) backend = self.get_backend_by_name(backend_name) if not backend: raise FirewallError(errors.INVALID_IPV, "'%s' is not a valid backend" % backend_name) if not self.is_backend_enabled(backend_name): return "" if self._individual_calls or \ not backend.restore_command_exists or \ (backend_name == "ebtables" and not self.ebtables_backend.restore_noflush_option): for i, rule in enumerate(_rules): try: backend.set_rule(rule, self._log_denied) except Exception as msg: log.debug1(traceback.format_exc()) log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) for rule in reversed(_rules[:i]): try: backend.set_rule(backend.reverse_rule(rule), self._log_denied) except Exception: # ignore errors here pass return False return True else: return backend.set_rules(_rules, self._log_denied) # check functions def check_panic(self): if self._panic: raise FirewallError(errors.PANIC_MODE) def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(errors.INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(errors.INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): if not functions.check_port(port): raise FirewallError(errors.INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(errors.MISSING_PROTOCOL) if protocol not in ["tcp", "udp", "sctp", "dccp"]: raise FirewallError(errors.INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'|'sctp'|'dccp'}" % \ protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(errors.INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(errors.INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(errors.INVALID_ADDR, source) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) def check_timeout(self, timeout): if not isinstance(timeout, int): raise TypeError("%s is %s, expected int" % (timeout, type(timeout))) if int(timeout) < 0: raise FirewallError( errors.INVALID_VALUE, "timeout '%d' is not positive number" % timeout) # RELOAD def reload(self, stop=False): _panic = self._panic # must stash this. The value may change after _start() flush_all = self._flush_all_on_reload if not flush_all: # save zone interfaces _zone_interfaces = {} for zone in self.zone.get_zones(): _zone_interfaces[zone] = self.zone.get_settings( zone)["interfaces"] # save direct config _direct_config = self.direct.get_runtime_config() _old_dz = self.get_default_zone() # stop self.cleanup() self.set_policy("DROP") start_exception = None try: self._start(reload=True, complete_reload=stop) except Exception as e: # save the exception for later, but continue restoring interfaces, # etc. We'll re-raise it at the end. start_exception = e if not flush_all: # handle interfaces in the default zone and move them to the new # default zone if it changed _new_dz = self.get_default_zone() if _new_dz != _old_dz: # if_new_dz has been introduced with the reload, we need to add it # https://github.com/firewalld/firewalld/issues/53 if _new_dz not in _zone_interfaces: _zone_interfaces[_new_dz] = {} # default zone changed. Move interfaces from old default zone to # the new one. for iface, settings in list(_zone_interfaces[_old_dz].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as # default) _zone_interfaces[_new_dz][iface] = \ _zone_interfaces[_old_dz][iface] del _zone_interfaces[_old_dz][iface] # add interfaces to zones again for zone in self.zone.get_zones(): if zone in _zone_interfaces: self.zone.set_settings( zone, {"interfaces": _zone_interfaces[zone]}) del _zone_interfaces[zone] else: log.info1("New zone '%s'.", zone) if len(_zone_interfaces) > 0: for zone in list(_zone_interfaces.keys()): log.info1("Lost zone '%s', zone interfaces dropped.", zone) del _zone_interfaces[zone] del _zone_interfaces # restore direct config self.direct.set_config(_direct_config) # Restore permanent interfaces from NetworkManager nm_bus_name = nm_get_bus_name() if nm_bus_name: for zone in self.zone.get_zones() + [""]: for interface in nm_get_interfaces_in_zone(zone): self.zone.add_interface(zone, interface, sender=nm_bus_name) # enable panic mode again if it has been enabled before or set policy # to ACCEPT if _panic: self.enable_panic_mode() else: self.set_policy("ACCEPT") if start_exception: self._state = "FAILED" raise start_exception else: self._state = "RUNNING" # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): if self._panic: raise FirewallError(errors.ALREADY_ENABLED, "panic mode already enabled") # TODO: use rule in raw table not default chain policy try: self.set_policy("DROP") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = True def disable_panic_mode(self): if not self._panic: raise FirewallError(errors.NOT_ENABLED, "panic mode is not enabled") # TODO: use rule in raw table not default chain policy try: self.set_policy("ACCEPT") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = False def query_panic_mode(self): return self._panic # LOG DENIED def get_log_denied(self): return self._log_denied def set_log_denied(self, value): if value not in config.LOG_DENIED_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.LOG_DENIED_VALUES))) if value != self.get_log_denied(): self._log_denied = value self._firewalld_conf.set("LogDenied", value) self._firewalld_conf.write() else: raise FirewallError(errors.ALREADY_SET, value) # AUTOMATIC HELPERS def get_automatic_helpers(self): return self._automatic_helpers def set_automatic_helpers(self, value): if value not in config.AUTOMATIC_HELPERS_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.AUTOMATIC_HELPERS_VALUES))) if value != self.get_automatic_helpers(): self._automatic_helpers = value self._firewalld_conf.set("AutomaticHelpers", value) self._firewalld_conf.write() else: raise FirewallError(errors.ALREADY_SET, value) # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() # remove old default zone from ZONES and add new default zone self.zone.change_default_zone(_old_dz, _zone) # Move interfaces from old default zone to the new one. _old_dz_settings = self.zone.get_settings(_old_dz) for iface, settings in list( _old_dz_settings["interfaces"].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as default) self.zone.change_zone_of_interface("", iface) else: raise FirewallError(errors.ZONE_ALREADY_SET, _zone)
class Firewall_test(object): def __init__(self): self._firewalld_conf = firewalld_conf(config.FIREWALLD_CONF) self.ip4tables_enabled = False self.ip6tables_enabled = False self.ebtables_enabled = False self.ipset_enabled = False self.ipset_supported_types = IPSET_TYPES self.available_tables = { } self.available_tables["ipv4"] = [ ] self.available_tables["ipv6"] = [ ] self.available_tables["eb"] = [ ] self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.helper = FirewallHelper(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled, self._individual_calls, self._log_denied, self._automatic_helpers) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] # fallback settings will be overloaded by firewalld.conf self._min_mark = config.FALLBACK_MINIMAL_MARK self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS self._log_denied = config.FALLBACK_LOG_DENIED self._automatic_helpers = config.FALLBACK_AUTOMATIC_HELPERS def individual_calls(self): return self._individual_calls def _start(self, reload=False, complete_reload=False): # initialize firewall default_zone = config.FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") if self._firewalld_conf.get("IndividualCalls"): value = self._firewalld_conf.get("IndividualCalls") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("IndividualCalls is enabled") self._individual_calls = True if self._firewalld_conf.get("LogDenied"): value = self._firewalld_conf.get("LogDenied") if value is None or value.lower() == "no": self._log_denied = "off" else: self._log_denied = value.lower() log.debug1("LogDenied is set to '%s'", self._log_denied) if self._firewalld_conf.get("AutomaticHelpers"): value = self._firewalld_conf.get("AutomaticHelpers") if value is None: if value.lower() in [ "no", "false" ]: self._automatic_helpers = "no" if value.lower() in [ "yes", "true" ]: self._automatic_helpers = "yes" else: self._automatic_helpers = value.lower() log.debug1("AutomaticHelpers is set to '%s'", self._automatic_helpers) self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(config.FIREWALLD_IPSETS, "ipset") self._loader(config.ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(config.FIREWALLD_ICMPTYPES, "icmptype") self._loader(config.ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load helper files self._loader(config.FIREWALLD_HELPERS, "helper") self._loader(config.ETC_FIREWALLD_HELPERS, "helper") # load service files self._loader(config.FIREWALLD_SERVICES, "service") self._loader(config.ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(config.FIREWALLD_ZONES, "zone") self._loader(config.ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) # load direct rules obj = Direct(config.FIREWALLD_DIRECT) if os.path.exists(config.FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % \ config.FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", config.FIREWALLD_DIRECT, msg) self.config.set_direct(copy.deepcopy(obj)) self._default_zone = self.check_zone(default_zone) self._state = "RUNNING" def start(self): self._start() def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(config.ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(config.ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True config_obj.default = True self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.ipset.add_ipset(obj) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) elif reader_type == "helper": obj = helper_reader(filename, path) if obj.name in self.helper.get_helpers(): orig_obj = self.helper.get_helper(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.helper.remove_helper(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.helper.add_helper(obj) # add a deep copy to the configuration interface self.config.add_helper(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def get_available_tables(self, ipv): if ipv in [ "ipv4", "ipv6", "eb" ]: return self.available_tables[ipv] return [ ] def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.ipset.cleanup() self.helper.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): self.cleanup() # check functions def check_panic(self): return def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(errors.INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(errors.INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug1("'%s': port > 65535" % port) elif range == -1: log.debug1("'%s': port is invalid" % port) elif range is None: log.debug1("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug1("'%s': range start >= end" % port) raise FirewallError(errors.INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(errors.MISSING_PROTOCOL) if not protocol in [ "tcp", "udp" ]: raise FirewallError(errors.INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(errors.INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(errors.INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(errors.INVALID_ADDR, source) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) # RELOAD def reload(self, stop=False): return # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): return def disable_panic_mode(self): return def query_panic_mode(self): return self._panic # LOG DENIED def get_log_denied(self): return self._log_denied def set_log_denied(self, value): if value not in config.LOG_DENIED_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.LOG_DENIED_VALUES))) if value != self.get_log_denied(): self._log_denied = value self._firewalld_conf.set("LogDenied", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # AUTOMATIC HELPERS def get_automatic_helpers(self): return self._automatic_helpers def set_automatic_helpers(self, value): if value not in config.AUTOMATIC_HELPERS_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.AUTOMATIC_HELPERS_VALUES))) if value != self.get_automatic_helpers(): self._automatic_helpers = value self._firewalld_conf.set("AutomaticHelpers", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() else: raise FirewallError(errors.ZONE_ALREADY_SET, _zone) # lockdown def enable_lockdown(self): self._firewalld_conf.set("Lockdown", "yes") self._firewalld_conf.write() def disable_lockdown(self): self._firewalld_conf.set("Lockdown", "no") self._firewalld_conf.write()
class Firewall(object): def __init__(self): self._firewalld_conf = firewalld_conf(FIREWALLD_CONF) self._ip4tables = ipXtables.ip4tables() self.ip4tables_enabled = True self._ip6tables = ipXtables.ip6tables() self.ip6tables_enabled = True self._ebtables = ebtables.ebtables() self.ebtables_enabled = True self._modules = modules.modules() self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] self._min_mark = FALLBACK_MINIMAL_MARK # will be overloaded by firewalld.conf self.cleanup_on_exit = FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = FALLBACK_IPV6_RPFILTER def _check_tables(self): # check if iptables, ip6tables and ebtables are usable, else disable if "filter" not in ipXtables.ip4tables_available_tables: log.warning("iptables not usable, disabling IPv4 firewall.") self.ip4tables_enabled = False if "filter" not in ipXtables.ip6tables_available_tables: log.warning("ip6tables not usable, disabling IPv6 firewall.") self.ip6tables_enabled = False if "filter" not in ebtables.ebtables_available_tables: log.error("ebtables not usable, disabling ethernet bridge firewall.") self.ebtables_enabled = False if not self.ip4tables_enabled and not self.ip6tables_enabled: log.fatal("No IPv4 and IPv6 firewall.") sys.exit(1) def _start(self): # initialize firewall default_zone = FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # apply default rules self._apply_default_rules() # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load icmptype files self._loader(FIREWALLD_ICMPTYPES, "icmptype") self._loader(ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load service files self._loader(FIREWALLD_SERVICES, "service") self._loader(ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(FIREWALLD_ZONES, "zone") self._loader(ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # apply settings for loaded zones self.zone.apply_zones() # load direct rules obj = Direct(FIREWALLD_DIRECT) if os.path.exists(FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", FIREWALLD_DIRECT, msg) else: self.direct.set_permanent_config(obj) self.config.set_direct(copy.deepcopy(obj)) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) self._default_zone = self.check_zone(default_zone) self.zone.change_default_zone(None, self._default_zone) self._state = "RUNNING" def start(self): self._check_tables() self._flush() self._set_policy("ACCEPT") self._start() def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): if self.cleanup_on_exit: self._flush() self._set_policy("ACCEPT") self._modules.unload_firewall_modules() self.cleanup() # marks def new_mark(self): # return first unused mark i = self._min_mark while i in self._marks: i += 1 self._marks.append(i) return i def del_mark(self, mark): self._marks.remove(mark) # handle rules, chains and modules def handle_rules(self, rules, enable, insert=False): if insert: append_delete = { True: "-I", False: "-D", } else: append_delete = { True: "-A", False: "-D", } # appends rules # returns None if all worked, else (cleanup rules, error message) for i,value in enumerate(rules): if len(value) == 3: (ipv, rule, insert) = value else: (ipv, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) table = None for t in ipXtables.BUILT_IN_CHAINS.keys(): if t in rule: table = t if table and not self.is_table_available(ipv, table): if ((ipv == "ipv4" and self.ip4tables_enabled) or (ipv == "ipv6" and self.ip6tables_enabled)): log.error("Unable to add %s into %s %s" % (rule, ipv, table)) continue # run try: self.rule(ipv, [ append_delete[enable], ] + rule) except Exception as msg: log.error("Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables.") log.error(msg) return (rules[:i], msg) # cleanup rules and error message return None def handle_rules2(self, rules, enable, insert=False): if insert: append_delete = { True: "-I", False: "-D", } else: append_delete = { True: "-A", False: "-D", } # appends rules # returns None if all worked, else (cleanup rules, error message) for i,value in enumerate(rules): if len(value) == 5: (ipv, table, chain, rule, insert) = value else: (ipv, table, chain, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) if not self.is_table_available(ipv, table): if ((ipv == "ipv4" and self.ip4tables_enabled) or (ipv == "ipv6" and self.ip6tables_enabled)): log.error("Unable to add %s into %s %s" % (rule, ipv, table)) continue # run try: self.rule(ipv, [ "-t", table, append_delete[enable], chain, ] + rule) except Exception as msg: log.error("Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables.") log.error(msg) return (rules[:i], msg) # cleanup rules and error message return None def handle_chains(self, rules, enable): new_delete = { True: "-N", False: "-X" } # appends chains # returns None if all worked, else (cleanup chains, error message) for i,(ipv, rule) in enumerate(rules): try: self.rule(ipv, [ new_delete[enable], ] + rule) except Exception as msg: log.error(msg) return (rules[:i], msg) # cleanup chains and error message return None def handle_modules(self, modules, enable): for i,module in enumerate(modules): if enable: (status, msg) = self._modules.load_module(module) else: if self._module_refcount[module] > 1: status = 0 # module referenced more then one, do not unload else: (status, msg) = self._modules.unload_module(module) if status != 0: if enable: return (modules[:i], msg) # cleanup modules and error msg # else: ignore cleanup if enable: self._module_refcount.setdefault(module, 0) self._module_refcount[module] += 1 else: if module in self._module_refcount: self._module_refcount[module] -= 1 if self._module_refcount[module] == 0: del self._module_refcount[module] return None def is_table_available(self, ipv, table): return ((ipv == "ipv4" and table in ipXtables.ip4tables_available_tables) or (ipv == "ipv6" and table in ipXtables.ip6tables_available_tables) or (ipv == "eb" and table in ebtables.ebtables_available_tables)) # apply default rules def __apply_default_rules(self, ipv): if ipv in [ "ipv4", "ipv6" ]: default_rules = ipXtables.DEFAULT_RULES else: default_rules = ebtables.DEFAULT_RULES for table in default_rules: if not self.is_table_available(ipv, table): continue prefix = [ "-t", table ] for rule in default_rules[table]: _rule = prefix + rule.split() self.rule(ipv, _rule) # try: # except Exception as msg: # TODO: better handling of init error # if "Chain already exists." in msg: # continue # raise FirewallError, <code> def _apply_default_rules(self): for ipv in [ "ipv4", "ipv6", "eb" ]: self.__apply_default_rules(ipv) if self.ipv6_rpfilter_enabled and \ self.is_table_available("ipv6", "raw"): rule = [ "-t", "raw", "-I", "PREROUTING", "1", "-p", "icmpv6", "--icmpv6-type=router-advertisement", "-j", "ACCEPT" ] # RHBZ#1058505 self.rule("ipv6", rule) rule = [ "-t", "raw", "-I", "PREROUTING", "2", "-m", "rpfilter", "--invert", "-j", "DROP" ] try: self.rule("ipv6", rule) except ValueError: # some problem with ip6t_rpfilter module ? rule = [ "-t", "raw", "-D", "PREROUTING", "1"] self.rule("ipv6", rule) # flush and policy def _flush(self): if self.ip4tables_enabled: self._ip4tables.flush() if self.ip6tables_enabled: self._ip6tables.flush() if self.ebtables_enabled: self._ebtables.flush() def _set_policy(self, policy, which="used"): if self.ip4tables_enabled: self._ip4tables.set_policy(policy, which) if self.ip6tables_enabled: self._ip6tables.set_policy(policy, which) if self.ebtables_enabled: self._ebtables.set_policy(policy, which) # rule function used in handle_ functions def rule(self, ipv, rule): # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i:i+1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if ipv == "ipv4": # do not call if disabled if self.ip4tables_enabled: return self._ip4tables.set_rule(rule) elif ipv == "ipv6": # do not call if disabled if self.ip6tables_enabled: return self._ip6tables.set_rule(rule) elif ipv == "eb": # do not call if disabled if self.ebtables_enabled: return self._ebtables.set_rule(rule) else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) return "" # check functions def check_panic(self): if self._panic: raise FirewallError(PANIC_MODE) def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug1("'%s': port > 65535" % port) elif range == -1: log.debug1("'%s': port is invalid" % port) elif range is None: log.debug1("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug1("'%s': range start >= end" % port) raise FirewallError(INVALID_PORT, port) def check_protocol(self, protocol): if not protocol: raise FirewallError(MISSING_PROTOCOL) if not protocol in [ "tcp", "udp" ]: raise FirewallError(INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(INVALID_ADDR, source) else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) def check_timeout(self, timeout): if not isinstance(timeout, int): raise TypeError("%s is %s, expected int" % (timeout, type(timeout))) if int(timeout) < 0: raise FirewallError(INVALID_VALUE, "timeout '%d' is not positive number" % timeout) # RELOAD def reload(self, stop=False): _panic = self._panic # save zone interfaces _zone_interfaces = { } for zone in self.zone.get_zones(): _zone_interfaces[zone] = self.zone.get_settings(zone)["interfaces"] # save direct config _direct_config = self.direct.get_runtime_config() _old_dz = self.get_default_zone() # stop self._set_policy("DROP") self._flush() if stop: self._modules.unload_firewall_modules() self.cleanup() # start self._start() # handle interfaces in the default zone and move them to the new # default zone if it changed _new_dz = self.get_default_zone() if _new_dz != _old_dz: # default zone changed. Move interfaces from old default zone to # the new one. for iface, settings in list(_zone_interfaces[_old_dz].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as # default) _zone_interfaces[_new_dz][iface] = \ _zone_interfaces[_old_dz][iface] del _zone_interfaces[_old_dz][iface] # add interfaces to zones again for zone in self.zone.get_zones(): if zone in _zone_interfaces: self.zone.set_settings(zone, { "interfaces": _zone_interfaces[zone] }) del _zone_interfaces[zone] else: log.info1("New zone '%s'.", zone) if len(_zone_interfaces) > 0: for zone in _zone_interfaces.keys(): log.info1("Lost zone '%s', zone interfaces dropped.", zone) del _zone_interfaces[zone] del _zone_interfaces # restore direct config self.direct.set_config(_direct_config) # enable panic mode again if it has been enabled before or set policy # to ACCEPT if _panic: self.enable_panic_mode() else: self._set_policy("ACCEPT") # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): if self._panic: raise FirewallError(ALREADY_ENABLED, "panic mode already enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("DROP", "all") except Exception as msg: raise FirewallError(COMMAND_FAILED, msg) self._panic = True def disable_panic_mode(self): if not self._panic: raise FirewallError(NOT_ENABLED, "panic mode is not enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("ACCEPT", "all") except Exception as msg: raise FirewallError(COMMAND_FAILED, msg) self._panic = False def query_panic_mode(self): return self._panic # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() # remove old default zone from ZONES and add new default zone self.zone.change_default_zone(_old_dz, _zone) # Move interfaces from old default zone to the new one. _old_dz_settings = self.zone.get_settings(_old_dz) for iface, settings in list(_old_dz_settings["interfaces"].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as default) self.zone.change_zone_of_interface("", iface) else: raise FirewallError(ZONE_ALREADY_SET, _zone)
class Firewall(object): def __init__(self): self._firewalld_conf = firewalld_conf(config.FIREWALLD_CONF) self.ip4tables_backend = ipXtables.ip4tables() self.ip4tables_enabled = True self.ip6tables_backend = ipXtables.ip6tables() self.ip6tables_enabled = True self.ebtables_backend = ebtables.ebtables() self.ebtables_enabled = True self.ipset_backend = ipset.ipset() self.ipset_enabled = True self.ipset_supported_types = [] self.available_tables = {} self.available_tables[ "ipv4"] = self.ip4tables_backend.available_tables() self.available_tables[ "ipv6"] = self.ip6tables_backend.available_tables() self.available_tables["eb"] = self.ebtables_backend.available_tables() self.modules_backend = modules.modules() self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled, self._individual_calls, self._log_denied) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = {} self._marks = [] # fallback settings will be overloaded by firewalld.conf self._min_mark = config.FALLBACK_MINIMAL_MARK self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS self._log_denied = config.FALLBACK_LOG_DENIED def individual_calls(self): return self._individual_calls def _check_tables(self): # check if iptables, ip6tables and ebtables are usable, else disable if self.ip4tables_enabled and \ "filter" not in self.get_available_tables("ipv4"): log.warning("iptables not usable, disabling IPv4 firewall.") self.ip4tables_enabled = False if self.ip6tables_enabled and \ "filter" not in self.get_available_tables("ipv6"): log.warning("ip6tables not usable, disabling IPv6 firewall.") self.ip6tables_enabled = False if self.ebtables_enabled and \ "filter" not in self.get_available_tables("eb"): log.error( "ebtables not usable, disabling ethernet bridge firewall.") self.ebtables_enabled = False # is there at least support for ipv4 or ipv6 if not self.ip4tables_enabled and not self.ip6tables_enabled: log.fatal("No IPv4 and IPv6 firewall.") sys.exit(1) def _start_check(self): try: x = self.ipset_backend.list() except: log.error("ipset not usable, disabling ipset usage in firewall.") # ipset is not usable, no supported types self.ipset_enabled = False self.ipset_supported_types = [] else: # ipset is usable, get all supported types self.ipset_supported_types = self.ipset_backend.supported_types() def _start(self, reload=False, complete_reload=False): # initialize firewall default_zone = config.FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in ["no", "false"]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in ["yes", "true"]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in ["no", "false"]: self.ipv6_rpfilter_enabled = False if value.lower() in ["yes", "true"]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") if self._firewalld_conf.get("IndividualCalls"): value = self._firewalld_conf.get("IndividualCalls") if value is not None and value.lower() in ["yes", "true"]: log.debug1("IndividualCalls is enabled") self._individual_calls = True if self._firewalld_conf.get("LogDenied"): value = self._firewalld_conf.get("LogDenied") if value is None or value.lower() == "no": self._log_denied = "off" else: self._log_denied = value.lower() log.debug1("LogDenied is set to '%s'", self._log_denied) if self.ebtables_enabled and not self._individual_calls and \ not self.ebtables_backend.restore_noflush_option: log.debug1( "ebtables-restore is not supporting the --noflush option, will therefore not be used" ) self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) self._start_check() # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(config.FIREWALLD_IPSETS, "ipset") self._loader(config.ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(config.FIREWALLD_ICMPTYPES, "icmptype") self._loader(config.ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load service files self._loader(config.FIREWALLD_SERVICES, "service") self._loader(config.ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(config.FIREWALLD_ZONES, "zone") self._loader(config.ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in ["block", "drop", "trusted"]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) # load direct rules obj = Direct(config.FIREWALLD_DIRECT) if os.path.exists(config.FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % \ config.FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", config.FIREWALLD_DIRECT, msg) self.direct.set_permanent_config(obj) self.config.set_direct(copy.deepcopy(obj)) if reload: self._set_policy("DROP") # check if needed tables are there self._check_tables() # flush rules self._flush() # complete reload: unload modules also if reload and complete_reload: log.debug1("Unloading firewall modules") self.modules_backend.unload_firewall_modules() # apply default rules log.debug1("Applying default rule set") self._apply_default_rules() # apply settings for loaded ipsets if self.ipset_enabled: log.debug1("Applying ipsets") self.ipset.apply_ipsets() # apply settings for loaded zones log.debug1("Applying used zones") self.zone.apply_zones() self._default_zone = self.check_zone(default_zone) self.zone.change_default_zone(None, self._default_zone) # apply direct chains, rules and passthrough rules log.debug1("Applying direct chains rules and and passthrough rules") self.direct.apply_direct() self._state = "RUNNING" def start(self): self._start() self._set_policy("ACCEPT") def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(config.ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(config.ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % (os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True config_obj.default = True self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.ipset.add_ipset(obj) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def get_available_tables(self, ipv): if ipv in ["ipv4", "ipv6", "eb"]: return self.available_tables[ipv] return [] def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.ipset.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): if self.cleanup_on_exit: self._flush() self._set_policy("ACCEPT") self.modules_backend.unload_firewall_modules() self.cleanup() # marks def new_mark(self): # return first unused mark i = self._min_mark while i in self._marks: i += 1 self._marks.append(i) return i def del_mark(self, mark): self._marks.remove(mark) # handle rules, chains and modules def handle_rules(self, rules, enable, insert=False): if insert: append_delete = { True: "-I", False: "-D", } else: append_delete = { True: "-A", False: "-D", } _rules = {} # appends rules # returns None if all worked, else (cleanup rules, error message) for i, value in enumerate(rules): table = chain = None if len(value) == 5: (ipv, table, chain, rule, insert) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) elif len(value) == 4: (ipv, table, chain, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) elif len(value) == 3: (ipv, rule, insert) = value else: (ipv, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) if table and table not in self.get_available_tables(ipv): if ((ipv == "ipv4" and self.ip4tables_enabled) or (ipv == "ipv6" and self.ip6tables_enabled)): log.error("Unable to add %s into %s %s" % (rule, ipv, table)) continue if table is not None: _rule = [ "-t", table, append_delete[enable], ] else: _rule = [ append_delete[enable], ] if chain is not None: _rule.append(chain) _rule += ["%s" % item for item in rule] if self._individual_calls or \ (ipv == "eb" and not self.ebtables_backend.restore_noflush_option): ## run try: self.rule(ipv, _rule) except Exception as msg: log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) return (rules[:i], msg) # cleanup rules and error message else: _rules.setdefault(ipv, []).append(_rule) try: for ipv in _rules: self.rules(ipv, _rules[ipv]) except Exception as msg: log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) return ([], msg) # no cleanup rules and error message return None def handle_chains(self, rules, enable): new_delete = {True: "-N", False: "-X"} _rules = {} # appends chains # returns None if all worked, else (cleanup chains, error message) for i, (ipv, rule) in enumerate(rules): _rule = [ new_delete[enable], ] + rule if self._individual_calls or \ (ipv == "eb" and not self.ebtables_backend.restore_noflush_option): try: self.rule(ipv, _rule) except Exception as msg: log.error(msg) return (rules[:i], msg) # cleanup chains and error message else: _rules.setdefault(ipv, []).append(_rule) try: for ipv in _rules: self.rules(ipv, _rules[ipv]) except Exception as msg: log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) return ([], msg) # no cleanup rules and error message return None def handle_modules(self, modules, enable): for i, module in enumerate(modules): if enable: (status, msg) = self.modules_backend.load_module(module) else: if self._module_refcount[module] > 1: status = 0 # module referenced more then one, do not unload else: (status, msg) = self.modules_backend.unload_module(module) if status != 0: if enable: return (modules[:i], msg) # cleanup modules and error msg # else: ignore cleanup if enable: self._module_refcount.setdefault(module, 0) self._module_refcount[module] += 1 else: if module in self._module_refcount: self._module_refcount[module] -= 1 if self._module_refcount[module] == 0: del self._module_refcount[module] return None # apply default rules def __apply_default_rules(self, ipv): default_rules = {} if ipv in ["ipv4", "ipv6"]: x = ipXtables else: x = ebtables for table in x.DEFAULT_RULES: default_rules[table] = x.DEFAULT_RULES[table][:] if self._log_denied != "off": for table in x.LOG_RULES: default_rules.setdefault(table, []).extend(x.LOG_RULES[table]) rules = {} for table in default_rules: if table not in self.get_available_tables(ipv): continue prefix = ["-t", table] for rule in default_rules[table]: if type(rule) == list: _rule = prefix + rule else: _rule = prefix + functions.splitArgs(rule) if self._individual_calls or \ (ipv == "eb" and not self.ebtables_backend.restore_noflush_option): self.rule(ipv, _rule) else: rules.setdefault(ipv, []).append(_rule) for ipv in rules: self.rules(ipv, rules[ipv]) def _apply_default_rules(self): for ipv in ["ipv4", "ipv6", "eb"]: self.__apply_default_rules(ipv) if self.ipv6_rpfilter_enabled and \ "raw" in self.get_available_tables("ipv6"): # here is no check for ebtables.restore_noflush_option needed # as ebtables is not used in here rules = [ ("ipv6", [ "PREROUTING", 1, "-t", "raw", "-p", "ipv6-icmp", "--icmpv6-type=router-advertisement", "-j", "ACCEPT" ]), # RHBZ#1058505 ("ipv6", [ "PREROUTING", 2, "-t", "raw", "-m", "rpfilter", "--invert", "-j", "DROP" ]), ] if self._log_denied != "off": rules.append(("ipv6", [ "PREROUTING", 2, "-t", "raw", "-m", "rpfilter", "--invert", "-j", "LOG", "--log-prefix", "rpfilter_DROP: " ])) # handle rules ret = self.handle_rules(rules, True, insert=True) if ret: (cleanup_rules, msg) = ret self.handle_rules(cleanup_rules, False) log.error(msg) # flush and policy def _flush(self): log.debug1("Flushing rule set") if self.ip4tables_enabled: try: self.ip4tables_backend.flush(individual=self._individual_calls) except Exception as e: log.error("Failed to flush ipv4 firewall: %s" % e) if self.ip6tables_enabled: try: self.ip6tables_backend.flush(individual=self._individual_calls) except Exception as e: log.error("Failed to flush ipv6 firewall: %s" % e) if self.ebtables_enabled: try: self.ebtables_backend.flush( individual=self._individual_calls or \ not self.ebtables_backend.restore_noflush_option) except Exception as e: log.error("Failed to flush eb firewall: %s" % e) def _set_policy(self, policy, which="used"): log.debug1("Setting policy to '%s'", policy) if self.ip4tables_enabled: try: self.ip4tables_backend.set_policy( policy, which, individual=self._individual_calls) except Exception as e: log.error("Failed to set policy of ipv4 firewall: %s" % e) if self.ip6tables_enabled: try: self.ip6tables_backend.set_policy( policy, which, individual=self._individual_calls) except Exception as e: log.error("Failed to set policy of ipv6 firewall: %s" % e) if self.ebtables_enabled: try: self.ebtables_backend.set_policy( policy, which, individual=self._individual_calls or \ not self.ebtables_backend.restore_noflush_option) except Exception as e: log.error("Failed to set policy of eb firewall: %s" % e) # rule function used in handle_ functions def rule(self, ipv, rule): # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i:i + 1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(errors.EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%LOGTYPE%% try: i = rule.index("%%LOGTYPE%%") except: pass else: if self._log_denied == "off": return "" if ipv not in ["ipv4", "ipv6"]: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if self._log_denied in ["unicast", "broadcast", "multicast"]: rule[i:i + 1] = ["-m", "pkttype", "--pkt-type", self._log_denied] else: rule.pop(i) # remove leading and trailing '"' for use with execve i = 0 while i < len(rule): x = rule[i] if len(x) > 2 and x[0] == '"' and x[-1] == '"': rule[i] = x[1:-1] i += 1 if ipv == "ipv4": # do not call if disabled if self.ip4tables_enabled: return self.ip4tables_backend.set_rule(rule) elif ipv == "ipv6": # do not call if disabled if self.ip6tables_enabled: return self.ip6tables_backend.set_rule(rule) elif ipv == "eb": # do not call if disabled if self.ebtables_enabled: return self.ebtables_backend.set_rule(rule) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) return "" def rules(self, ipv, rules): _rules = [] for rule in rules: # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i:i + 1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(errors.EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%LOGTYPE%% try: i = rule.index("%%LOGTYPE%%") except: pass else: if self._log_denied == "off": continue if ipv not in ["ipv4", "ipv6"]: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if self._log_denied in ["unicast", "broadcast", "multicast"]: rule[i:i + 1] = [ "-m", "pkttype", "--pkt-type", self._log_denied ] else: rule.pop(i) _rules.append(rule) if ipv == "ipv4": # do not call if disabled if self.ip4tables_enabled: return self.ip4tables_backend.set_rules(_rules) elif ipv == "ipv6": # do not call if disabled if self.ip6tables_enabled: return self.ip6tables_backend.set_rules(_rules) elif ipv == "eb": # do not call if disabled if self.ebtables_enabled: return self.ebtables_backend.set_rules(_rules) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) return "" # check functions def check_panic(self): if self._panic: raise FirewallError(errors.PANIC_MODE) def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(errors.INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(errors.INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug1("'%s': port > 65535" % port) elif range == -1: log.debug1("'%s': port is invalid" % port) elif range is None: log.debug1("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug1("'%s': range start >= end" % port) raise FirewallError(errors.INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(errors.MISSING_PROTOCOL) if not protocol in ["tcp", "udp"]: raise FirewallError(errors.INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(errors.INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(errors.INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(errors.INVALID_ADDR, source) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) def check_timeout(self, timeout): if not isinstance(timeout, int): raise TypeError("%s is %s, expected int" % (timeout, type(timeout))) if int(timeout) < 0: raise FirewallError( errors.INVALID_VALUE, "timeout '%d' is not positive number" % timeout) # RELOAD def reload(self, stop=False): _panic = self._panic # save zone interfaces _zone_interfaces = {} for zone in self.zone.get_zones(): _zone_interfaces[zone] = self.zone.get_settings(zone)["interfaces"] # save direct config _direct_config = self.direct.get_runtime_config() _old_dz = self.get_default_zone() # stop self.cleanup() # start self._start(reload=True, complete_reload=stop) # handle interfaces in the default zone and move them to the new # default zone if it changed _new_dz = self.get_default_zone() if _new_dz != _old_dz: # if_new_dz has been introduced with the reload, we need to add it # https://github.com/t-woerner/firewalld/issues/53 if _new_dz not in _zone_interfaces: _zone_interfaces[_new_dz] = {} # default zone changed. Move interfaces from old default zone to # the new one. for iface, settings in list(_zone_interfaces[_old_dz].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as # default) _zone_interfaces[_new_dz][iface] = \ _zone_interfaces[_old_dz][iface] del _zone_interfaces[_old_dz][iface] # add interfaces to zones again for zone in self.zone.get_zones(): if zone in _zone_interfaces: self.zone.set_settings(zone, {"interfaces": _zone_interfaces[zone]}) del _zone_interfaces[zone] else: log.info1("New zone '%s'.", zone) if len(_zone_interfaces) > 0: for zone in _zone_interfaces.keys(): log.info1("Lost zone '%s', zone interfaces dropped.", zone) del _zone_interfaces[zone] del _zone_interfaces # restore direct config self.direct.set_config(_direct_config) # enable panic mode again if it has been enabled before or set policy # to ACCEPT if _panic: self.enable_panic_mode() else: self._set_policy("ACCEPT") # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): if self._panic: raise FirewallError(errors.ALREADY_ENABLED, "panic mode already enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("DROP", "all") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = True def disable_panic_mode(self): if not self._panic: raise FirewallError(errors.NOT_ENABLED, "panic mode is not enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("ACCEPT", "all") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = False def query_panic_mode(self): return self._panic # LOG DENIED def get_log_denied(self): return self._log_denied def set_log_denied(self, value): if value not in config.LOG_DENIED_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.LOG_DENIED_VALUES))) if value != self.get_log_denied(): self._log_denied = value self._firewalld_conf.set("LogDenied", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() # remove old default zone from ZONES and add new default zone self.zone.change_default_zone(_old_dz, _zone) # Move interfaces from old default zone to the new one. _old_dz_settings = self.zone.get_settings(_old_dz) for iface, settings in list( _old_dz_settings["interfaces"].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as default) self.zone.change_zone_of_interface("", iface) else: raise FirewallError(errors.ZONE_ALREADY_SET, _zone)
class Firewall_test(object): def __init__(self): self._firewalld_conf = firewalld_conf(config.FIREWALLD_CONF) self.ip4tables_enabled = False self.ip6tables_enabled = False self.ebtables_enabled = False self.ipset_enabled = False self.ipset_supported_types = IPSET_TYPES self.available_tables = { } self.available_tables["ipv4"] = [ ] self.available_tables["ipv6"] = [ ] self.available_tables["eb"] = [ ] self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.helper = FirewallHelper(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled, self._individual_calls, self._log_denied, self._automatic_helpers) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] # fallback settings will be overloaded by firewalld.conf self._min_mark = config.FALLBACK_MINIMAL_MARK self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS self._log_denied = config.FALLBACK_LOG_DENIED self._automatic_helpers = config.FALLBACK_AUTOMATIC_HELPERS def individual_calls(self): return self._individual_calls def _start(self, reload=False, complete_reload=False): # initialize firewall default_zone = config.FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") if self._firewalld_conf.get("IndividualCalls"): value = self._firewalld_conf.get("IndividualCalls") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("IndividualCalls is enabled") self._individual_calls = True if self._firewalld_conf.get("LogDenied"): value = self._firewalld_conf.get("LogDenied") if value is None or value.lower() == "no": self._log_denied = "off" else: self._log_denied = value.lower() log.debug1("LogDenied is set to '%s'", self._log_denied) if self._firewalld_conf.get("AutomaticHelpers"): value = self._firewalld_conf.get("AutomaticHelpers") if value is not None: if value.lower() in [ "no", "false" ]: self._automatic_helpers = "no" elif value.lower() in [ "yes", "true" ]: self._automatic_helpers = "yes" else: self._automatic_helpers = value.lower() log.debug1("AutomaticHelpers is set to '%s'", self._automatic_helpers) self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(config.FIREWALLD_IPSETS, "ipset") self._loader(config.ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(config.FIREWALLD_ICMPTYPES, "icmptype") self._loader(config.ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load helper files self._loader(config.FIREWALLD_HELPERS, "helper") self._loader(config.ETC_FIREWALLD_HELPERS, "helper") # load service files self._loader(config.FIREWALLD_SERVICES, "service") self._loader(config.ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(config.FIREWALLD_ZONES, "zone") self._loader(config.ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) # load direct rules obj = Direct(config.FIREWALLD_DIRECT) if os.path.exists(config.FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % \ config.FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", config.FIREWALLD_DIRECT, msg) self.config.set_direct(copy.deepcopy(obj)) self._default_zone = self.check_zone(default_zone) self._state = "RUNNING" def start(self): self._start() def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(config.ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(config.ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True config_obj.default = True self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.ipset.add_ipset(obj) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) elif reader_type == "helper": obj = helper_reader(filename, path) if obj.name in self.helper.get_helpers(): orig_obj = self.helper.get_helper(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.helper.remove_helper(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.helper.add_helper(obj) # add a deep copy to the configuration interface self.config.add_helper(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def get_available_tables(self, ipv): if ipv in [ "ipv4", "ipv6", "eb" ]: return self.available_tables[ipv] return [ ] def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.ipset.cleanup() self.helper.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): self.cleanup() # check functions def check_panic(self): return def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(errors.INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(errors.INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug1("'%s': port > 65535" % port) elif range == -1: log.debug1("'%s': port is invalid" % port) elif range is None: log.debug1("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug1("'%s': range start >= end" % port) raise FirewallError(errors.INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(errors.MISSING_PROTOCOL) if not protocol in [ "tcp", "udp" ]: raise FirewallError(errors.INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(errors.INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(errors.INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(errors.INVALID_ADDR, source) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) # RELOAD def reload(self, stop=False): return # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): return def disable_panic_mode(self): return def query_panic_mode(self): return self._panic # LOG DENIED def get_log_denied(self): return self._log_denied def set_log_denied(self, value): if value not in config.LOG_DENIED_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.LOG_DENIED_VALUES))) if value != self.get_log_denied(): self._log_denied = value self._firewalld_conf.set("LogDenied", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # AUTOMATIC HELPERS def get_automatic_helpers(self): return self._automatic_helpers def set_automatic_helpers(self, value): if value not in config.AUTOMATIC_HELPERS_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.AUTOMATIC_HELPERS_VALUES))) if value != self.get_automatic_helpers(): self._automatic_helpers = value self._firewalld_conf.set("AutomaticHelpers", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() else: raise FirewallError(errors.ZONE_ALREADY_SET, _zone) # lockdown def enable_lockdown(self): self._firewalld_conf.set("Lockdown", "yes") self._firewalld_conf.write() def disable_lockdown(self): self._firewalld_conf.set("Lockdown", "no") self._firewalld_conf.write()
class Firewall_test: def __init__(self): self._firewalld_conf = firewalld_conf(FIREWALLD_CONF) self.ip4tables_enabled = False self.ip6tables_enabled = False self.ebtables_enabled = False self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] self._min_mark = FALLBACK_MINIMAL_MARK # will be overloaded by firewalld.conf self.cleanup_on_exit = True self.ipv6_rpfilter_enabled = True def start(self): # initialize firewall default_zone = FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.error("Failed to open firewalld config file '%s': %s", FIREWALLD_CONF, msg) else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): mark = self._firewalld_conf.get("MinimalMark") if mark is not None: try: self._min_mark = int(mark) except Exception as msg: log.error("MinimalMark %s is not valid, using default " "value %d", mark, self._min_mark) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load icmptype files self._loader(FIREWALLD_ICMPTYPES, "icmptype") self._loader(ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load service files self._loader(FIREWALLD_SERVICES, "service") self._loader(ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(FIREWALLD_ZONES, "zone") self._loader(ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if not z in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # load direct rules obj = Direct(FIREWALLD_DIRECT) if os.path.exists(FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", FIREWALLD_DIRECT, msg) self.config.set_direct(copy.deepcopy(obj)) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) self._default_zone = self.check_zone(default_zone) self._state = "RUNNING" def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): self.cleanup() # check functions def check_panic(self): return def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug2("'%s': port > 65535" % port) elif range == -1: log.debug2("'%s': port is invalid" % port) elif range is None: log.debug2("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug2("'%s': range start >= end" % port) raise FirewallError(INVALID_PORT, port) def check_protocol(self, protocol): if not protocol: raise FirewallError(MISSING_PROTOCOL) if not protocol in [ "tcp", "udp" ]: raise FirewallError(INVALID_PROTOCOL, protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(INVALID_ADDR, source) else: raise FirewallError(INVALID_IPV) def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) # RELOAD def reload(self, stop=False): return # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): return def disable_panic_mode(self): return def query_panic_mode(self): return self._panic # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() else: raise FirewallError(ZONE_ALREADY_SET, _zone) # lockdown def enable_lockdown(self): self._firewalld_conf.set("Lockdown", "yes") self._firewalld_conf.write() def disable_lockdown(self): self._firewalld_conf.set("Lockdown", "no") self._firewalld_conf.write()
class Firewall(object): def __init__(self): self._firewalld_conf = firewalld_conf(FIREWALLD_CONF) self._ip4tables = ipXtables.ip4tables() self.ip4tables_enabled = True self._ip6tables = ipXtables.ip6tables() self.ip6tables_enabled = True self._ebtables = ebtables.ebtables() self.ebtables_enabled = True self._modules = modules.modules() self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = {} self._marks = [] self._min_mark = FALLBACK_MINIMAL_MARK # will be overloaded by firewalld.conf self.cleanup_on_exit = FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = FALLBACK_IPV6_RPFILTER def _check_tables(self): # check if iptables, ip6tables and ebtables are usable, else disable if "filter" not in ipXtables.ip4tables_available_tables: log.warning("iptables not usable, disabling IPv4 firewall.") self.ip4tables_enabled = False if "filter" not in ipXtables.ip6tables_available_tables: log.warning("ip6tables not usable, disabling IPv6 firewall.") self.ip6tables_enabled = False if "filter" not in ebtables.ebtables_available_tables: log.error( "ebtables not usable, disabling ethernet bridge firewall.") self.ebtables_enabled = False if not self.ip4tables_enabled and not self.ip6tables_enabled: log.fatal("No IPv4 and IPv6 firewall.") sys.exit(1) def _start(self): # initialize firewall default_zone = FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in ["no", "false"]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in ["yes", "true"]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in ["no", "false"]: self.ipv6_rpfilter_enabled = False if value.lower() in ["yes", "true"]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) # apply default rules self._apply_default_rules() # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load icmptype files self._loader(FIREWALLD_ICMPTYPES, "icmptype") self._loader(ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load service files self._loader(FIREWALLD_SERVICES, "service") self._loader(ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(FIREWALLD_ZONES, "zone") self._loader(ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in ["block", "drop", "trusted"]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # apply settings for loaded zones self.zone.apply_zones() # load direct rules obj = Direct(FIREWALLD_DIRECT) if os.path.exists(FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", FIREWALLD_DIRECT, msg) self.direct.set_permanent_config(obj) self.config.set_direct(copy.deepcopy(obj)) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) self._default_zone = self.check_zone(default_zone) self.zone.change_default_zone(None, self._default_zone) self._state = "RUNNING" def start(self): self._check_tables() self._flush() self._set_policy("ACCEPT") self._start() def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % (os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): if self.cleanup_on_exit: self._flush() self._set_policy("ACCEPT") self._modules.unload_firewall_modules() self.cleanup() # marks def new_mark(self): # return first unused mark i = self._min_mark while i in self._marks: i += 1 self._marks.append(i) return i def del_mark(self, mark): self._marks.remove(mark) # handle rules, chains and modules def handle_rules(self, rules, enable, insert=False): if insert: append_delete = { True: "-I", False: "-D", } else: append_delete = { True: "-A", False: "-D", } # appends rules # returns None if all worked, else (cleanup rules, error message) for i, value in enumerate(rules): if len(value) == 3: (ipv, rule, insert) = value else: (ipv, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) table = None for t in ipXtables.BUILT_IN_CHAINS.keys(): if t in rule: table = t if table and not self.is_table_available(ipv, table): if ((ipv == "ipv4" and self.ip4tables_enabled) or (ipv == "ipv6" and self.ip6tables_enabled)): log.error("Unable to add %s into %s %s" % (rule, ipv, table)) continue # run try: self.rule(ipv, [ append_delete[enable], ] + rule) except Exception as msg: log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) return (rules[:i], msg) # cleanup rules and error message return None def handle_rules2(self, rules, enable, insert=False): if insert: append_delete = { True: "-I", False: "-D", } else: append_delete = { True: "-A", False: "-D", } # appends rules # returns None if all worked, else (cleanup rules, error message) for i, value in enumerate(rules): if len(value) == 5: (ipv, table, chain, rule, insert) = value else: (ipv, table, chain, rule) = value # drop insert rule number if it exists if insert and not enable and isinstance(rule[1], int): rule.pop(1) if not self.is_table_available(ipv, table): if ((ipv == "ipv4" and self.ip4tables_enabled) or (ipv == "ipv6" and self.ip6tables_enabled)): log.error("Unable to add %s into %s %s" % (rule, ipv, table)) continue # run try: self.rule(ipv, [ "-t", table, append_delete[enable], chain, ] + rule) except Exception as msg: log.error( "Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables." ) log.error(msg) return (rules[:i], msg) # cleanup rules and error message return None def handle_chains(self, rules, enable): new_delete = {True: "-N", False: "-X"} # appends chains # returns None if all worked, else (cleanup chains, error message) for i, (ipv, rule) in enumerate(rules): try: self.rule(ipv, [ new_delete[enable], ] + rule) except Exception as msg: log.error(msg) return (rules[:i], msg) # cleanup chains and error message return None def handle_modules(self, modules, enable): for i, module in enumerate(modules): if enable: (status, msg) = self._modules.load_module(module) else: if self._module_refcount[module] > 1: status = 0 # module referenced more then one, do not unload else: (status, msg) = self._modules.unload_module(module) if status != 0: if enable: return (modules[:i], msg) # cleanup modules and error msg # else: ignore cleanup if enable: self._module_refcount.setdefault(module, 0) self._module_refcount[module] += 1 else: if module in self._module_refcount: self._module_refcount[module] -= 1 if self._module_refcount[module] == 0: del self._module_refcount[module] return None def is_table_available(self, ipv, table): return ( (ipv == "ipv4" and table in ipXtables.ip4tables_available_tables) or (ipv == "ipv6" and table in ipXtables.ip6tables_available_tables) or (ipv == "eb" and table in ebtables.ebtables_available_tables)) # apply default rules def __apply_default_rules(self, ipv): if ipv in ["ipv4", "ipv6"]: default_rules = ipXtables.DEFAULT_RULES else: default_rules = ebtables.DEFAULT_RULES for table in default_rules: if not self.is_table_available(ipv, table): continue prefix = ["-t", table] for rule in default_rules[table]: _rule = prefix + rule.split() self.rule(ipv, _rule) # try: # except Exception as msg: # TODO: better handling of init error # if "Chain already exists." in msg: # continue # raise FirewallError, <code> def _apply_default_rules(self): for ipv in ["ipv4", "ipv6", "eb"]: self.__apply_default_rules(ipv) if self.ipv6_rpfilter_enabled and \ self.is_table_available("ipv6", "raw"): rule = [ "-t", "raw", "-I", "PREROUTING", "1", "-p", "icmpv6", "--icmpv6-type=router-advertisement", "-j", "ACCEPT" ] # RHBZ#1058505 self.rule("ipv6", rule) rule = [ "-t", "raw", "-I", "PREROUTING", "2", "-m", "rpfilter", "--invert", "-j", "DROP" ] try: self.rule("ipv6", rule) except ValueError: # some problem with ip6t_rpfilter module ? rule = ["-t", "raw", "-D", "PREROUTING", "1"] self.rule("ipv6", rule) # flush and policy def _flush(self): if self.ip4tables_enabled: self._ip4tables.flush() if self.ip6tables_enabled: self._ip6tables.flush() if self.ebtables_enabled: self._ebtables.flush() def _set_policy(self, policy, which="used"): if self.ip4tables_enabled: self._ip4tables.set_policy(policy, which) if self.ip6tables_enabled: self._ip6tables.set_policy(policy, which) if self.ebtables_enabled: self._ebtables.set_policy(policy, which) # rule function used in handle_ functions def rule(self, ipv, rule): # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i:i + 1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except: pass else: if ipv in ["ipv4", "ipv6"]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if ipv == "ipv4": # do not call if disabled if self.ip4tables_enabled: return self._ip4tables.set_rule(rule) elif ipv == "ipv6": # do not call if disabled if self.ip6tables_enabled: return self._ip6tables.set_rule(rule) elif ipv == "eb": # do not call if disabled if self.ebtables_enabled: return self._ebtables.set_rule(rule) else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) return "" # check functions def check_panic(self): if self._panic: raise FirewallError(PANIC_MODE) def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): range = functions.getPortRange(port) if range == -2 or range == -1 or range is None or \ (len(range) == 2 and range[0] >= range[1]): if range == -2: log.debug1("'%s': port > 65535" % port) elif range == -1: log.debug1("'%s': port is invalid" % port) elif range is None: log.debug1("'%s': port is ambiguous" % port) elif len(range) == 2 and range[0] >= range[1]: log.debug1("'%s': range start >= end" % port) raise FirewallError(INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(MISSING_PROTOCOL) if not protocol in ["tcp", "udp"]: raise FirewallError(INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(INVALID_ADDR, source) else: raise FirewallError(INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) def check_timeout(self, timeout): if not isinstance(timeout, int): raise TypeError("%s is %s, expected int" % (timeout, type(timeout))) if int(timeout) < 0: raise FirewallError( INVALID_VALUE, "timeout '%d' is not positive number" % timeout) # RELOAD def reload(self, stop=False): _panic = self._panic # save zone interfaces _zone_interfaces = {} for zone in self.zone.get_zones(): _zone_interfaces[zone] = self.zone.get_settings(zone)["interfaces"] # save direct config _direct_config = self.direct.get_runtime_config() _old_dz = self.get_default_zone() # stop self._set_policy("DROP") self._flush() if stop: self._modules.unload_firewall_modules() self.cleanup() # start self._start() # handle interfaces in the default zone and move them to the new # default zone if it changed _new_dz = self.get_default_zone() if _new_dz != _old_dz: # if_new_dz has been introduced with the reload, we need to add it # https://github.com/t-woerner/firewalld/issues/53 if _new_dz not in _zone_interfaces: _zone_interfaces[_new_dz] = {} # default zone changed. Move interfaces from old default zone to # the new one. for iface, settings in list(_zone_interfaces[_old_dz].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as # default) _zone_interfaces[_new_dz][iface] = \ _zone_interfaces[_old_dz][iface] del _zone_interfaces[_old_dz][iface] # add interfaces to zones again for zone in self.zone.get_zones(): if zone in _zone_interfaces: self.zone.set_settings(zone, {"interfaces": _zone_interfaces[zone]}) del _zone_interfaces[zone] else: log.info1("New zone '%s'.", zone) if len(_zone_interfaces) > 0: for zone in _zone_interfaces.keys(): log.info1("Lost zone '%s', zone interfaces dropped.", zone) del _zone_interfaces[zone] del _zone_interfaces # restore direct config self.direct.set_config(_direct_config) # enable panic mode again if it has been enabled before or set policy # to ACCEPT if _panic: self.enable_panic_mode() else: self._set_policy("ACCEPT") # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): if self._panic: raise FirewallError(ALREADY_ENABLED, "panic mode already enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("DROP", "all") except Exception as msg: raise FirewallError(COMMAND_FAILED, msg) self._panic = True def disable_panic_mode(self): if not self._panic: raise FirewallError(NOT_ENABLED, "panic mode is not enabled") # TODO: use rule in raw table not default chain policy try: self._set_policy("ACCEPT", "all") except Exception as msg: raise FirewallError(COMMAND_FAILED, msg) self._panic = False def query_panic_mode(self): return self._panic # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() # remove old default zone from ZONES and add new default zone self.zone.change_default_zone(_old_dz, _zone) # Move interfaces from old default zone to the new one. _old_dz_settings = self.zone.get_settings(_old_dz) for iface, settings in list( _old_dz_settings["interfaces"].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as default) self.zone.change_zone_of_interface("", iface) else: raise FirewallError(ZONE_ALREADY_SET, _zone)
class Firewall(object): def __init__(self): self._firewalld_conf = firewalld_conf(config.FIREWALLD_CONF) self.ip4tables_backend = ipXtables.ip4tables() self.ip4tables_enabled = True self.ip6tables_backend = ipXtables.ip6tables() self.ip6tables_enabled = True self.ebtables_backend = ebtables.ebtables() self.ebtables_enabled = True self.ipset_backend = ipset.ipset() self.ipset_enabled = True self.ipset_supported_types = [ ] self.available_tables = { } self.available_tables["ipv4"] = self.ip4tables_backend.available_tables() self.available_tables["ipv6"] = self.ip6tables_backend.available_tables() self.available_tables["eb"] = self.ebtables_backend.available_tables() self.modules_backend = modules.modules() self.icmptype = FirewallIcmpType(self) self.service = FirewallService(self) self.zone = FirewallZone(self) self.direct = FirewallDirect(self) self.config = FirewallConfig(self) self.policies = FirewallPolicies() self.ipset = FirewallIPSet(self) self.helper = FirewallHelper(self) self.__init_vars() def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled, self.ebtables_enabled, self._state, self._panic, self._default_zone, self._module_refcount, self._marks, self._min_mark, self.cleanup_on_exit, self.ipv6_rpfilter_enabled, self.ipset_enabled, self._individual_calls, self._log_denied, self._automatic_helpers) def __init_vars(self): self._state = "INIT" self._panic = False self._default_zone = "" self._module_refcount = { } self._marks = [ ] # fallback settings will be overloaded by firewalld.conf self._min_mark = config.FALLBACK_MINIMAL_MARK self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS self._log_denied = config.FALLBACK_LOG_DENIED self._automatic_helpers = config.FALLBACK_AUTOMATIC_HELPERS self.nf_conntrack_helper_setting = 0 self.nf_conntrack_helpers = { } def individual_calls(self): return self._individual_calls def _check_tables(self): # check if iptables, ip6tables and ebtables are usable, else disable if self.ip4tables_enabled and \ "filter" not in self.get_available_tables("ipv4"): log.warning("iptables not usable, disabling IPv4 firewall.") self.ip4tables_enabled = False if self.ip6tables_enabled and \ "filter" not in self.get_available_tables("ipv6"): log.warning("ip6tables not usable, disabling IPv6 firewall.") self.ip6tables_enabled = False if self.ebtables_enabled and \ "filter" not in self.get_available_tables("eb"): log.warning("ebtables not usable, disabling ethernet bridge firewall.") self.ebtables_enabled = False # is there at least support for ipv4 or ipv6 if not self.ip4tables_enabled and not self.ip6tables_enabled: log.fatal("No IPv4 and IPv6 firewall.") sys.exit(1) def _start_check(self): try: self.ipset_backend.list() except ValueError: log.warning("ipset not usable, disabling ipset usage in firewall.") # ipset is not usable, no supported types self.ipset_enabled = False self.ipset_supported_types = [ ] else: # ipset is usable, get all supported types self.ipset_supported_types = self.ipset_backend.supported_types() self.ip4tables_backend.fill_exists() if not self.ip4tables_backend.restore_command_exists: if self.ip4tables_backend.command_exists: log.warning("iptables-restore is missing, using " "individual calls for IPv4 firewall.") else: log.warning("iptables-restore and iptables are missing, " "disabling IPv4 firewall.") self.ip4tables_enabled = False self.ip6tables_backend.fill_exists() if not self.ip6tables_backend.restore_command_exists: if self.ip6tables_backend.command_exists: log.warning("ip6tables-restore is missing, using " "individual calls for IPv6 firewall.") else: log.warning("ip6tables-restore and ip6tables are missing, " "disabling IPv6 firewall.") self.ip6tables_enabled = False self.ebtables_backend.fill_exists() if not self.ebtables_backend.restore_command_exists: if self.ebtables_backend.command_exists: log.warning("ebtables-restore is missing, using " "individual calls for bridge firewall.") else: log.warning("ebtables-restore and ebtables are missing, " "disabling bridge firewall.") self.ebtables_enabled = False if self.ebtables_enabled and not self._individual_calls and \ not self.ebtables_backend.restore_noflush_option: log.debug1("ebtables-restore is not supporting the --noflush " "option, will therefore not be used") if os.path.exists(config.COMMANDS["modinfo"]): self.nf_conntrack_helpers = functions.get_nf_conntrack_helpers() if len(self.nf_conntrack_helpers) > 0: log.debug1("Conntrack helpers supported by the kernel:") for key,values in self.nf_conntrack_helpers.items(): log.debug1(" %s: %s", key, ", ".join(values)) else: log.debug1("No conntrack helpers supported by the kernel.") else: self.nf_conntrack_helpers = { } log.warning("modinfo command is missing, not able to detect conntrack helpers.") def _start(self, reload=False, complete_reload=False): # initialize firewall default_zone = config.FALLBACK_ZONE # load firewalld config log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) try: self._firewalld_conf.read() except Exception as msg: log.warning(msg) log.warning("Using fallback firewalld configuration settings.") else: if self._firewalld_conf.get("DefaultZone"): default_zone = self._firewalld_conf.get("DefaultZone") if self._firewalld_conf.get("MinimalMark"): self._min_mark = int(self._firewalld_conf.get("MinimalMark")) if self._firewalld_conf.get("CleanupOnExit"): value = self._firewalld_conf.get("CleanupOnExit") if value is not None and value.lower() in [ "no", "false" ]: self.cleanup_on_exit = False if self._firewalld_conf.get("Lockdown"): value = self._firewalld_conf.get("Lockdown") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("Lockdown is enabled") try: self.policies.enable_lockdown() except FirewallError: # already enabled, this is probably reload pass if self._firewalld_conf.get("IPv6_rpfilter"): value = self._firewalld_conf.get("IPv6_rpfilter") if value is not None: if value.lower() in [ "no", "false" ]: self.ipv6_rpfilter_enabled = False if value.lower() in [ "yes", "true" ]: self.ipv6_rpfilter_enabled = True if self.ipv6_rpfilter_enabled: log.debug1("IPv6 rpfilter is enabled") else: log.debug1("IPV6 rpfilter is disabled") if self._firewalld_conf.get("IndividualCalls"): value = self._firewalld_conf.get("IndividualCalls") if value is not None and value.lower() in [ "yes", "true" ]: log.debug1("IndividualCalls is enabled") self._individual_calls = True if self._firewalld_conf.get("LogDenied"): value = self._firewalld_conf.get("LogDenied") if value is None or value.lower() == "no": self._log_denied = "off" else: self._log_denied = value.lower() log.debug1("LogDenied is set to '%s'", self._log_denied) if self._firewalld_conf.get("AutomaticHelpers"): value = self._firewalld_conf.get("AutomaticHelpers") if value is not None: if value.lower() in [ "no", "false" ]: self._automatic_helpers = "no" elif value.lower() in [ "yes", "true" ]: self._automatic_helpers = "yes" else: self._automatic_helpers = value.lower() log.debug1("AutomaticHelpers is set to '%s'", self._automatic_helpers) self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) self._start_check() # load lockdown whitelist log.debug1("Loading lockdown whitelist") try: self.policies.lockdown_whitelist.read() except Exception as msg: if self.policies.query_lockdown(): log.error("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) else: log.debug1("Failed to load lockdown whitelist '%s': %s", self.policies.lockdown_whitelist.filename, msg) # copy policies to config interface self.config.set_policies(copy.deepcopy(self.policies)) # load ipset files self._loader(config.FIREWALLD_IPSETS, "ipset") self._loader(config.ETC_FIREWALLD_IPSETS, "ipset") # load icmptype files self._loader(config.FIREWALLD_ICMPTYPES, "icmptype") self._loader(config.ETC_FIREWALLD_ICMPTYPES, "icmptype") if len(self.icmptype.get_icmptypes()) == 0: log.error("No icmptypes found.") # load helper files self._loader(config.FIREWALLD_HELPERS, "helper") self._loader(config.ETC_FIREWALLD_HELPERS, "helper") # load service files self._loader(config.FIREWALLD_SERVICES, "service") self._loader(config.ETC_FIREWALLD_SERVICES, "service") if len(self.service.get_services()) == 0: log.error("No services found.") # load zone files self._loader(config.FIREWALLD_ZONES, "zone") self._loader(config.ETC_FIREWALLD_ZONES, "zone") if len(self.zone.get_zones()) == 0: log.fatal("No zones found.") sys.exit(1) # check minimum required zones error = False for z in [ "block", "drop", "trusted" ]: if z not in self.zone.get_zones(): log.fatal("Zone '%s' is not available.", z) error = True if error: sys.exit(1) # check if default_zone is a valid zone if default_zone not in self.zone.get_zones(): if "public" in self.zone.get_zones(): zone = "public" elif "external" in self.zone.get_zones(): zone = "external" else: zone = "block" # block is a base zone, therefore it has to exist log.error("Default zone '%s' is not valid. Using '%s'.", default_zone, zone) default_zone = zone else: log.debug1("Using default zone '%s'", default_zone) # load direct rules obj = Direct(config.FIREWALLD_DIRECT) if os.path.exists(config.FIREWALLD_DIRECT): log.debug1("Loading direct rules file '%s'" % \ config.FIREWALLD_DIRECT) try: obj.read() except Exception as msg: log.debug1("Failed to load direct rules file '%s': %s", config.FIREWALLD_DIRECT, msg) self.direct.set_permanent_config(obj) self.config.set_direct(copy.deepcopy(obj)) # automatic helpers if self._automatic_helpers != "system": functions.set_nf_conntrack_helper_setting(self._automatic_helpers == "yes") self.nf_conntrack_helper_setting = \ functions.get_nf_conntrack_helper_setting() # check if needed tables are there self._check_tables() if log.getDebugLogLevel() > 0: # get time before flushing and applying tm1 = time.time() # Start transaction transaction = FirewallTransaction(self) if reload: self.set_policy("DROP", use_transaction=transaction) # flush rules self.flush(use_transaction=transaction) # If modules need to be unloaded in complete reload or if there are # ipsets to get applied, limit the transaction to set_policy and flush. # # Future optimization for the ipset case in reload: The transaction # only needs to be split here if there are conflicting ipset types in # exsting ipsets and the configuration in firewalld. if (reload and complete_reload) or \ (self.ipset_enabled and self.ipset.has_ipsets()): transaction.execute(True) transaction.clear() # complete reload: unload modules also if reload and complete_reload: log.debug1("Unloading firewall modules") self.modules_backend.unload_firewall_modules() # apply settings for loaded ipsets while reloading here if self.ipset_enabled and self.ipset.has_ipsets(): log.debug1("Applying ipsets") self.ipset.apply_ipsets() # Start or continue with transaction # apply default rules log.debug1("Applying default rule set") self.apply_default_rules(use_transaction=transaction) # apply settings for loaded zones log.debug1("Applying used zones") self.zone.apply_zones(use_transaction=transaction) self._default_zone = self.check_zone(default_zone) self.zone.change_default_zone(None, self._default_zone, use_transaction=transaction) # Execute transaction transaction.execute(True) # Start new transaction for direct rules transaction.clear() # apply direct chains, rules and passthrough rules if self.direct.has_configuration(): transaction.enable_generous_mode() log.debug1("Applying direct chains rules and passthrough rules") self.direct.apply_direct(transaction) # Execute transaction transaction.execute(True) transaction.disable_generous_mode() transaction.clear() del transaction if log.getDebugLogLevel() > 1: # get time after flushing and applying tm2 = time.time() log.debug2("Flushing and applying took %f seconds" % (tm2 - tm1)) self._state = "RUNNING" def start(self): self._start() self.set_policy("ACCEPT") def _loader(self, path, reader_type, combine=False): # combine: several zone files are getting combined into one obj if not os.path.isdir(path): return if combine: if path.startswith(config.ETC_FIREWALLD) and reader_type == "zone": combined_zone = Zone() combined_zone.name = os.path.basename(path) combined_zone.check_name(combined_zone.name) combined_zone.path = path combined_zone.default = False else: combine = False for filename in sorted(os.listdir(path)): if not filename.endswith(".xml"): if path.startswith(config.ETC_FIREWALLD) and \ reader_type == "zone" and \ os.path.isdir("%s/%s" % (path, filename)): self._loader("%s/%s" % (path, filename), reader_type, combine=True) continue name = "%s/%s" % (path, filename) log.debug1("Loading %s file '%s'", reader_type, name) try: if reader_type == "icmptype": obj = icmptype_reader(filename, path) if obj.name in self.icmptype.get_icmptypes(): orig_obj = self.icmptype.get_icmptype(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.icmptype.remove_icmptype(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.icmptype.add_icmptype(obj) # add a deep copy to the configuration interface self.config.add_icmptype(copy.deepcopy(obj)) elif reader_type == "service": obj = service_reader(filename, path) if obj.name in self.service.get_services(): orig_obj = self.service.get_service(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.service.remove_service(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.service.add_service(obj) # add a deep copy to the configuration interface self.config.add_service(copy.deepcopy(obj)) elif reader_type == "zone": obj = zone_reader(filename, path) if combine: # Change name for permanent configuration obj.name = "%s/%s" % ( os.path.basename(path), os.path.basename(filename)[0:-4]) obj.check_name(obj.name) # Copy object before combine config_obj = copy.deepcopy(obj) if obj.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(obj.name) self.zone.remove_zone(orig_obj.name) if orig_obj.combined: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, obj.name, path, filename) obj.combine(orig_obj) else: log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True config_obj.default = True self.config.add_zone(config_obj) if combine: log.debug1(" Combining %s '%s' ('%s/%s')", reader_type, combined_zone.name, path, filename) combined_zone.combine(obj) else: self.zone.add_zone(obj) elif reader_type == "ipset": obj = ipset_reader(filename, path) if obj.name in self.ipset.get_ipsets(): orig_obj = self.ipset.get_ipset(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.ipset.remove_ipset(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.ipset.add_ipset(obj) # add a deep copy to the configuration interface self.config.add_ipset(copy.deepcopy(obj)) elif reader_type == "helper": obj = helper_reader(filename, path) if obj.name in self.helper.get_helpers(): orig_obj = self.helper.get_helper(obj.name) log.debug1(" Overloads %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) self.helper.remove_helper(orig_obj.name) elif obj.path.startswith(config.ETC_FIREWALLD): obj.default = True self.helper.add_helper(obj) # add a deep copy to the configuration interface self.config.add_helper(copy.deepcopy(obj)) else: log.fatal("Unknown reader type %s", reader_type) except FirewallError as msg: log.error("Failed to load %s file '%s': %s", reader_type, name, msg) except Exception as msg: log.error("Failed to load %s file '%s':", reader_type, name) log.exception() if combine and combined_zone.combined: if combined_zone.name in self.zone.get_zones(): orig_obj = self.zone.get_zone(combined_zone.name) log.debug1(" Overloading and deactivating %s '%s' ('%s/%s')", reader_type, orig_obj.name, orig_obj.path, orig_obj.filename) try: self.zone.remove_zone(combined_zone.name) except Exception: pass self.config.forget_zone(combined_zone.name) self.zone.add_zone(combined_zone) def get_available_tables(self, ipv): if ipv in [ "ipv4", "ipv6", "eb" ]: return self.available_tables[ipv] return [ ] def cleanup(self): self.icmptype.cleanup() self.service.cleanup() self.zone.cleanup() self.ipset.cleanup() self.helper.cleanup() self.config.cleanup() self.direct.cleanup() self.policies.cleanup() self._firewalld_conf.cleanup() self.__init_vars() def stop(self): if self.cleanup_on_exit: self.flush() self.set_policy("ACCEPT") self.modules_backend.unload_firewall_modules() self.cleanup() # marks def new_mark(self): # return first unused mark i = self._min_mark while i in self._marks: i += 1 self._marks.append(i) return i def del_mark(self, mark): self._marks.remove(mark) # handle modules def handle_modules(self, _modules, enable): for i,module in enumerate(_modules): if enable: (status, msg) = self.modules_backend.load_module(module) else: if self._module_refcount[module] > 1: status = 0 # module referenced more then one, do not unload else: (status, msg) = self.modules_backend.unload_module(module) if status != 0: if enable: return (_modules[:i], msg) # cleanup modules and error msg # else: ignore cleanup if enable: self._module_refcount.setdefault(module, 0) self._module_refcount[module] += 1 else: if module in self._module_refcount: self._module_refcount[module] -= 1 if self._module_refcount[module] == 0: del self._module_refcount[module] return None # apply default rules def __apply_default_rules(self, ipv, transaction): default_rules = { } if ipv in [ "ipv4", "ipv6" ]: x = ipXtables else: x = ebtables for table in x.DEFAULT_RULES: default_rules[table] = x.DEFAULT_RULES[table][:] if self._log_denied != "off": for table in x.LOG_RULES: default_rules.setdefault(table, []).extend(x.LOG_RULES[table]) for table in default_rules: if table not in self.get_available_tables(ipv): continue prefix = [ "-t", table ] for rule in default_rules[table]: if type(rule) == list: _rule = prefix + rule else: _rule = prefix + functions.splitArgs(rule) #if self._individual_calls or \ # (ipv == "eb" and not # self.ebtables_backend.restore_noflush_option): # self.rule(ipv, _rule) #else: # transaction.add_rule(ipv, _rule) transaction.add_rule(ipv, _rule) def apply_default_rules(self, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction for ipv in [ "ipv4", "ipv6", "eb" ]: self.__apply_default_rules(ipv, transaction) if self.ipv6_rpfilter_enabled and \ "raw" in self.get_available_tables("ipv6"): # Execute existing transaction transaction.execute(True) # Start new transaction transaction.clear() # here is no check for ebtables.restore_noflush_option needed # as ebtables is not used in here transaction.add_rule("ipv6", [ "-I", "PREROUTING", "1", "-t", "raw", "-p", "ipv6-icmp", "--icmpv6-type=router-advertisement", "-j", "ACCEPT" ]) # RHBZ#1058505 transaction.add_rule("ipv6", [ "-I", "PREROUTING", "2", "-t", "raw", "-m", "rpfilter", "--invert", "-j", "DROP" ]) if self._log_denied != "off": transaction.add_rule("ipv6", [ "-I", "PREROUTING", "2", "-t", "raw", "-m", "rpfilter", "--invert", "-j", "LOG", "--log-prefix", "rpfilter_DROP: " ]) # Execute ipv6_rpfilter transaction, it might fail try: transaction.execute(True) except FirewallError as msg: log.warning("Applying rules for ipv6_rpfilter failed: %s", msg) # Start new transaction transaction.clear() else: if use_transaction is None: transaction.execute(True) # flush and policy def flush(self, use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction log.debug1("Flushing rule set") if self.ip4tables_enabled: try: self.ip4tables_backend.flush(transaction) except Exception as e: log.error("Failed to flush ipv4 firewall: %s" % e) if self.ip6tables_enabled: try: self.ip6tables_backend.flush(transaction) except Exception as e: log.error("Failed to flush ipv6 firewall: %s" % e) if self.ebtables_enabled: try: self.ebtables_backend.flush(transaction) except Exception as e: log.error("Failed to flush eb firewall: %s" % e) if use_transaction is None: transaction.execute(True) def set_policy(self, policy, which="used", use_transaction=None): if use_transaction is None: transaction = FirewallTransaction(self) else: transaction = use_transaction log.debug1("Setting policy to '%s'", policy) if self.ip4tables_enabled: try: self.ip4tables_backend.set_policy(policy, which, transaction) except Exception as e: log.error("Failed to set policy of ipv4 firewall: %s" % e) if self.ip6tables_enabled: try: self.ip6tables_backend.set_policy(policy, which, transaction) except Exception as e: log.error("Failed to set policy of ipv6 firewall: %s" % e) if self.ebtables_enabled: try: self.ebtables_backend.set_policy(policy, which, transaction) except Exception as e: log.error("Failed to set policy of eb firewall: %s" % e) if use_transaction is None: transaction.execute(True) # rule function used in handle_ functions def rule(self, ipv, rule): # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except ValueError: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i:i+1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(errors.EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except ValueError: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%LOGTYPE%% try: i = rule.index("%%LOGTYPE%%") except ValueError: pass else: if self._log_denied == "off": return "" if ipv not in [ "ipv4", "ipv6" ]: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if self._log_denied in [ "unicast", "broadcast", "multicast" ]: rule[i:i+1] = [ "-m", "pkttype", "--pkt-type", self._log_denied ] else: rule.pop(i) # remove leading and trailing '"' for use with execve i = 0 while i < len(rule): x = rule[i] if len(x) > 2 and x[0] == '"' and x[-1] == '"': rule[i] = x[1:-1] i += 1 if ipv == "ipv4": # do not call if disabled if self.ip4tables_enabled: return self.ip4tables_backend.set_rule(rule) elif ipv == "ipv6": # do not call if disabled if self.ip6tables_enabled: return self.ip6tables_backend.set_rule(rule) elif ipv == "eb": # do not call if disabled if self.ebtables_enabled: return self.ebtables_backend.set_rule(rule) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) return "" def rules(self, ipv, rules): _rules = [ ] for rule in rules: # replace %%REJECT%% try: i = rule.index("%%REJECT%%") except ValueError: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i:i+1] = [ "REJECT", "--reject-with", ipXtables.DEFAULT_REJECT_TYPE[ipv] ] else: raise FirewallError(errors.EBTABLES_NO_REJECT, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%ICMP%% try: i = rule.index("%%ICMP%%") except ValueError: pass else: if ipv in [ "ipv4", "ipv6" ]: rule[i] = ipXtables.ICMP[ipv] else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) # replace %%LOGTYPE%% try: i = rule.index("%%LOGTYPE%%") except ValueError: pass else: if self._log_denied == "off": continue if ipv not in [ "ipv4", "ipv6" ]: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}" % ipv) if self._log_denied in [ "unicast", "broadcast", "multicast" ]: rule[i:i+1] = [ "-m", "pkttype", "--pkt-type", self._log_denied ] else: rule.pop(i) _rules.append(rule) backend = None if ipv == "ipv4": if self.ip4tables_enabled: # do not call if disabled backend = self.ip4tables_backend elif ipv == "ipv6": if self.ip6tables_enabled: # do not call if disabled backend = self.ip6tables_backend elif ipv == "eb": if self.ebtables_enabled: # do not call if disabled backend = self.ebtables_backend else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'|'eb'}" % ipv) if not backend: return "" if self._individual_calls or \ not backend.restore_command_exists or \ (ipv == "eb" and not self.ebtables_backend.restore_noflush_option): for i,rule in enumerate(_rules): # remove leading and trailing '"' for use with execve j = 0 while j < len(rule): x = rule[j] if len(x) > 2 and x[0] == '"' and x[-1] == '"': rule[j] = x[1:-1] j += 1 try: backend.set_rule(rule) except Exception as msg: log.error("Failed to apply rules. A firewall reload might solve the issue if the firewall has been modified using ip*tables or ebtables.") log.error(msg) for rule in reversed(_rules[:i]): try: backend.set_rule(reverse_rule(rule)) except Exception: # ignore errors here pass return False return True else: return backend.set_rules(_rules) # check functions def check_panic(self): if self._panic: raise FirewallError(errors.PANIC_MODE) def check_zone(self, zone): _zone = zone if not _zone or _zone == "": _zone = self.get_default_zone() if _zone not in self.zone.get_zones(): raise FirewallError(errors.INVALID_ZONE, _zone) return _zone def check_interface(self, interface): if not functions.checkInterface(interface): raise FirewallError(errors.INVALID_INTERFACE, interface) def check_service(self, service): self.service.check_service(service) def check_port(self, port): if not functions.check_port(port): raise FirewallError(errors.INVALID_PORT, port) def check_tcpudp(self, protocol): if not protocol: raise FirewallError(errors.MISSING_PROTOCOL) if protocol not in [ "tcp", "udp" ]: raise FirewallError(errors.INVALID_PROTOCOL, "'%s' not in {'tcp'|'udp'}" % protocol) def check_ip(self, ip): if not functions.checkIP(ip): raise FirewallError(errors.INVALID_ADDR, ip) def check_address(self, ipv, source): if ipv == "ipv4": if not functions.checkIPnMask(source): raise FirewallError(errors.INVALID_ADDR, source) elif ipv == "ipv6": if not functions.checkIP6nMask(source): raise FirewallError(errors.INVALID_ADDR, source) else: raise FirewallError(errors.INVALID_IPV, "'%s' not in {'ipv4'|'ipv6'}") def check_icmptype(self, icmp): self.icmptype.check_icmptype(icmp) def check_timeout(self, timeout): if not isinstance(timeout, int): raise TypeError("%s is %s, expected int" % (timeout, type(timeout))) if int(timeout) < 0: raise FirewallError(errors.INVALID_VALUE, "timeout '%d' is not positive number" % timeout) # RELOAD def reload(self, stop=False): _panic = self._panic # save zone interfaces _zone_interfaces = { } for zone in self.zone.get_zones(): _zone_interfaces[zone] = self.zone.get_settings(zone)["interfaces"] # save direct config _direct_config = self.direct.get_runtime_config() _old_dz = self.get_default_zone() # stop self.cleanup() # start self._start(reload=True, complete_reload=stop) # handle interfaces in the default zone and move them to the new # default zone if it changed _new_dz = self.get_default_zone() if _new_dz != _old_dz: # if_new_dz has been introduced with the reload, we need to add it # https://github.com/t-woerner/firewalld/issues/53 if _new_dz not in _zone_interfaces: _zone_interfaces[_new_dz] = { } # default zone changed. Move interfaces from old default zone to # the new one. for iface, settings in list(_zone_interfaces[_old_dz].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as # default) _zone_interfaces[_new_dz][iface] = \ _zone_interfaces[_old_dz][iface] del _zone_interfaces[_old_dz][iface] # add interfaces to zones again for zone in self.zone.get_zones(): if zone in _zone_interfaces: self.zone.set_settings(zone, { "interfaces": _zone_interfaces[zone] }) del _zone_interfaces[zone] else: log.info1("New zone '%s'.", zone) if len(_zone_interfaces) > 0: for zone in _zone_interfaces.keys(): log.info1("Lost zone '%s', zone interfaces dropped.", zone) del _zone_interfaces[zone] del _zone_interfaces # restore direct config self.direct.set_config(_direct_config) # enable panic mode again if it has been enabled before or set policy # to ACCEPT if _panic: self.enable_panic_mode() else: self.set_policy("ACCEPT") # STATE def get_state(self): return self._state # PANIC MODE def enable_panic_mode(self): if self._panic: raise FirewallError(errors.ALREADY_ENABLED, "panic mode already enabled") # TODO: use rule in raw table not default chain policy try: self.set_policy("DROP", "all") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = True def disable_panic_mode(self): if not self._panic: raise FirewallError(errors.NOT_ENABLED, "panic mode is not enabled") # TODO: use rule in raw table not default chain policy try: self.set_policy("ACCEPT", "all") except Exception as msg: raise FirewallError(errors.COMMAND_FAILED, msg) self._panic = False def query_panic_mode(self): return self._panic # LOG DENIED def get_log_denied(self): return self._log_denied def set_log_denied(self, value): if value not in config.LOG_DENIED_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.LOG_DENIED_VALUES))) if value != self.get_log_denied(): self._log_denied = value self._firewalld_conf.set("LogDenied", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # AUTOMATIC HELPERS def get_automatic_helpers(self): return self._automatic_helpers def set_automatic_helpers(self, value): if value not in config.AUTOMATIC_HELPERS_VALUES: raise FirewallError(errors.INVALID_VALUE, "'%s', choose from '%s'" % \ (value, "','".join(config.AUTOMATIC_HELPERS_VALUES))) if value != self.get_automatic_helpers(): self._automatic_helpers = value self._firewalld_conf.set("AutomaticHelpers", value) self._firewalld_conf.write() # now reload the firewall self.reload() else: raise FirewallError(errors.ALREADY_SET, value) # DEFAULT ZONE def get_default_zone(self): return self._default_zone def set_default_zone(self, zone): _zone = self.check_zone(zone) if _zone != self._default_zone: _old_dz = self._default_zone self._default_zone = _zone self._firewalld_conf.set("DefaultZone", _zone) self._firewalld_conf.write() # remove old default zone from ZONES and add new default zone self.zone.change_default_zone(_old_dz, _zone) # Move interfaces from old default zone to the new one. _old_dz_settings = self.zone.get_settings(_old_dz) for iface, settings in list(_old_dz_settings["interfaces"].items()): if settings["__default__"]: # move only those that were added to default zone # (not those that were added to specific zone same as default) self.zone.change_zone_of_interface("", iface) else: raise FirewallError(errors.ZONE_ALREADY_SET, _zone)