feat(app): Add globals and how to deal with them
This commit is contained in:
parent
9dcb91d9dd
commit
f4379bbfc2
35
README.md
35
README.md
@ -17,6 +17,7 @@ Update a firewall rule that relies on dynamic DNS names
|
||||
* man page https://ipset.netfilter.org/iptables.man.html we do iptables
|
||||
* added in order
|
||||
* related, established? needed?
|
||||
* section names as comments?
|
||||
|
||||
## Config structure
|
||||
|
||||
@ -43,6 +44,8 @@ target = ACCEPT
|
||||
addr =
|
||||
ports = 80, 443
|
||||
proto = tcp
|
||||
do_config_check = true
|
||||
restart_firewalld_after_change = true
|
||||
|
||||
[anyone-can-access-website]
|
||||
|
||||
@ -68,14 +71,16 @@ proto =
|
||||
|
||||
### Layout
|
||||
|
||||
A config file can have an optional `[DEFAULT]` section and must have at least one `[section]` other than `[DEFAULT]`. Any `[DEFAULT]` option that's undefined retains its default value. Feel free to delete the entire `[DEFAULT]` section from your file.
|
||||
|
||||
A setting changed in `[DEFAULT]` section affects all sections. A setting changed only in a custom `[section]` overwrites it for only the section.
|
||||
A config file can have an optional `[DEFAULT]` section and must have at least one `[section]` other than `[DEFAULT]`. Any `[DEFAULT]` option that's undefined retains its default value. Feel free to delete the entire `[DEFAULT]` section from your file. A setting changed in `[DEFAULT]` section affects all sections. A setting changed only in a custom `[section]` overwrites it for only the section.
|
||||
|
||||
Custom sections such as `[maybe-a-webserver]` in above example file are treated as organizational helper constructs. You can but don't have to group IP address rules by sections. Technically nothing's stopping you from adding all IP allow list entries into a single section.
|
||||
|
||||
### Example explanation
|
||||
|
||||
With `config_check_after_change`
|
||||
|
||||
Setting `restart_firewalld_after_change` controls if you want the `firewalld` systemd unit to be restarted
|
||||
|
||||
In above example file note that `[anyone-can-access-website]` makes `[maybe-a-webserver]` irrelevant, the latter one could easily be deleted. `[anyone-can-access-website]` does not overwrite defaults, it's an empty section. With it `firewalld` will create a rule that - following all default settings - allows access from any source address on TCP ports 80 and 443. In section `[maybe-a-webserver]` we do the same and additionally limit source addresses. Rules are added in `config.ini` order so the first rule permitting access to TCP ports 80 and 443 from anywhere makes the second one irrelevant.
|
||||
|
||||
We strongly recommend you do keep the very last example section:
|
||||
@ -90,7 +95,29 @@ If a packet has traversed rules this far without being accepted it will be dropp
|
||||
|
||||
## Options
|
||||
|
||||
A custom `[section]` has the following options all of which are optional.
|
||||
### Globals
|
||||
|
||||
In `[DEFAULT]` section the following settings are called globals. They're only valid in `[DEFAULT]` context. Adding them to a custom `[section]` (see [Locals](#locals) below) won't do anything, in a custom `[section]` the following settings are ignored.
|
||||
|
||||
* `do_config_check`, __*optional*__, defaults to `true`: Do a `firewall-cmd --check-config` once before changing `/etc/firewalld/direct.xml`. Abort if this initial check fails, inform the user. Obviously something's very wrong before we've even touched firewall configs. Otherwise back up `/etc/firewalld/direct.xml`, change it then do another check. If `firewall-cmd --check-config` finds a syntax error restore the backed up known good `/etc/firewalld/direct.xml`, delete the temporary backed up one and inform the user.
|
||||
|
||||
If `restart_firewalld_after_change` is also `true` (default) restart the `firewalld` systemd service unit after the second config check. See steps __*a*__ through __*f*__ below at `restart_firewalld_after_change` for exact order.
|
||||
|
||||
* `restart_firewalld_after_change`, __*optional*__, defaults to `true`: After putting a new `/etc/firewalld/direct.xml` file in place restart the `firewalld` systemd service unit.
|
||||
* If `do_config_check` is also `true` (default) the exact order is:
|
||||
1. Config check, bail on error and inform user
|
||||
2. Back up `/etc/firewalld/direct.xml` to temporary location
|
||||
3. Write new `/etc/firewalld/direct.xml` content
|
||||
4. Perform config check, bail on error and restore backed up `/etc/firewalld/direct.xml`
|
||||
5. Delete backed up `/etc/firewalld/direct.xml`
|
||||
6. Restart `firewalld` systemd service unit
|
||||
* If `do_config_check` is `false`:
|
||||
1. Write new `/etc/firewalld/direct.xml` content
|
||||
2. Restart `firewalld` systemd service unit
|
||||
|
||||
### Locals
|
||||
|
||||
A custom `[section]` has the following options. We're calling the locals all of which are optional.
|
||||
|
||||
* `target`, __*optional*__, defaults to `ACCEPT`: A string specifying the fate of a packet that matched this rule. By default matching packets are accepted. See "TARGETS" section in [iptables man page](https://ipset.netfilter.org/iptables.man.html). You'll most likely want to stick to either `ACCEPT` or `DROP`.
|
||||
|
||||
|
@ -3,6 +3,8 @@ target = ACCEPT
|
||||
addr =
|
||||
ports = 80, 443
|
||||
proto = tcp
|
||||
do_config_check = true
|
||||
restart_firewalld_after_change = true
|
||||
|
||||
[anyone-can-access-website]
|
||||
|
||||
|
@ -30,22 +30,25 @@ class CONST(object):
|
||||
CFG_THIS_FILE_DIRNAME = os.path.dirname(__file__)
|
||||
CFG_DEFAULT_FILENAME = "config.ini"
|
||||
CFG_DEFAULT_ABS_PATH = os.path.join(CFG_THIS_FILE_DIRNAME, CFG_DEFAULT_FILENAME)
|
||||
# Values you don't have to set, these are their internal defaults
|
||||
# Values you don't have to set, these are their internal defaults. You may optionally add a key 'is_global' equal
|
||||
# to either True or False. By default if left off it'll be assumed False. Script will treat values where
|
||||
# 'is_global' equals True as not being overridable in a '[section]'. It's a setting that only makes sense in a
|
||||
# global context for the entire script.
|
||||
CFG_KNOWN_DEFAULTS = [
|
||||
{"key": "self_name", "value": "update-firewall-source"},
|
||||
{"key": "tmp_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/tmp/%(self_name)s")},
|
||||
{"key": "state_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/var/lib/%(self_name)s")},
|
||||
{"key": "state_files_dir", "value": "%(state_base_dir)s/state"},
|
||||
{"key": "state_file_retention", "value": "50"},
|
||||
{"key": "state_file_name_prefix", "value": "state-"},
|
||||
{"key": "state_file_name_suffix", "value": ".log"},
|
||||
{"key": "update_firewall_source_some_option", "value": "http://localhost:8000/api/query"},
|
||||
{"key": "another_option", "value": "first"}
|
||||
{"key": "target", "value": "ACCEPT", "is_global": False},
|
||||
{"key": "addr", "value": "", "is_global": False},
|
||||
{"key": "ports", "value": "80, 443", "is_global": False},
|
||||
{"key": "proto", "value": "tcp", "is_global": False},
|
||||
{"key": "do_config_check", "value": "true", "is_global": True},
|
||||
{"key": "restart_firewalld_after_change", "value": "true", "is_global": True}
|
||||
]
|
||||
# In all sections other than 'default' the following settings are known and accepted. We silently ignore other
|
||||
# settings. We use 'is_mandatory' to determine if we have to raise errors on missing settings.
|
||||
CFG_KNOWN_SECTION = [
|
||||
{"key": "addr", "is_mandatory": True}
|
||||
{"key": "target", "is_mandatory": False},
|
||||
{"key": "addr", "is_mandatory": False},
|
||||
{"key": "ports", "is_mandatory": False},
|
||||
{"key": "proto", "is_mandatory": False}
|
||||
]
|
||||
CFG_MANDATORY = [section_cfg["key"] for section_cfg in CFG_KNOWN_SECTION if section_cfg["is_mandatory"]]
|
||||
|
||||
@ -89,6 +92,7 @@ class ConfigParser(
|
||||
|
||||
ini_defaults = []
|
||||
internal_defaults = {default["key"]: default["value"] for default in CONST.CFG_KNOWN_DEFAULTS}
|
||||
internal_globals = [default["key"] for default in CONST.CFG_KNOWN_DEFAULTS if default["is_global"]]
|
||||
config = ConfigParser(defaults=internal_defaults,
|
||||
converters={'list': lambda x: [i.strip() for i in x.split(',')]})
|
||||
config.read(CONST.CFG_DEFAULT_ABS_PATH)
|
||||
@ -107,14 +111,19 @@ def validate_default_section(
|
||||
sys.exit(1)
|
||||
if config.defaults():
|
||||
log.debug(f"Symbol legend:\n"
|
||||
f"* Global default from section '[{config_obj.default_section}]'\n"
|
||||
f"* Default from section '[{config_obj.default_section}]'\n"
|
||||
f": Global option from '[{config_obj.default_section}]', can not be overridden in local sections\n"
|
||||
f"~ Local option, doesn't exist in '[{config_obj.default_section}]'\n"
|
||||
f"+ Local override of a value from '[{config_obj.default_section}]'\n"
|
||||
f"= Local override, same value as in '[{config_obj.default_section}]'")
|
||||
f"= Local override, same value as in '[{config_obj.default_section}]'\n"
|
||||
f"# Local attempt at overriding a global, will be ignored")
|
||||
log.debug(print_section_header(config_obj.default_section))
|
||||
for default in config_obj.defaults():
|
||||
ini_defaults.append({default: config_obj[config_obj.default_section][default]})
|
||||
log.debug(f"* {default} = {config_obj[config_obj.default_section][default]}")
|
||||
if default in internal_globals:
|
||||
log.debug(f": {default} = {config_obj[config_obj.default_section][default]}")
|
||||
else:
|
||||
log.debug(f"* {default} = {config_obj[config_obj.default_section][default]}")
|
||||
else:
|
||||
log.debug(f"No defaults defined")
|
||||
|
||||
@ -134,6 +143,11 @@ def is_default(
|
||||
return any(config_key in ini_default for ini_default in ini_defaults)
|
||||
|
||||
|
||||
def is_global(
|
||||
config_key: str) -> bool:
|
||||
return config_key in internal_globals
|
||||
|
||||
|
||||
def is_same_as_default(
|
||||
config_kv_pair: dict) -> bool:
|
||||
return config_kv_pair in ini_defaults
|
||||
@ -150,11 +164,17 @@ def validate_config_sections(
|
||||
else:
|
||||
for key in config_obj.options(this_section, no_defaults=True):
|
||||
kv_prefix = "~"
|
||||
if is_default(key):
|
||||
remove_from_section = False
|
||||
if is_global(key):
|
||||
kv_prefix = "#"
|
||||
remove_from_section = True
|
||||
elif is_default(key):
|
||||
kv_prefix = "+"
|
||||
if is_same_as_default({key: config_obj[this_section][key]}):
|
||||
kv_prefix = "="
|
||||
log.debug(f"{kv_prefix} {key} = {config_obj[this_section][key]}")
|
||||
if remove_from_section:
|
||||
config_obj.remove_option(this_section, key)
|
||||
|
||||
|
||||
def gen_fw_rule_xml(ip_addresses: dict[str, list]) -> lxml.builder.ElementMaker:
|
||||
|
Loading…
x
Reference in New Issue
Block a user