Compare commits

...

2 Commits

Author SHA1 Message Date
19372524d5 feat(app): Initial commit 2022-06-18 02:32:46 +02:00
f061fdc8cc refactor(meta): Add PyCharm config files 2022-06-18 02:29:44 +02:00
15 changed files with 771 additions and 1 deletions

241
.gitignore vendored Normal file
View File

@ -0,0 +1,241 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> Custom
.idea/deployment.xml
.idea/misc.xml
.idea/remote-mappings.xml
.idea/*.iml
# Project-specific
config.ini

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,35 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="3.11" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="google" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="str.decode" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/markdown.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="customStylesheetText" value="body {&#10; font-size: 95% !important;&#10;}&#10;" />
<option name="useCustomStylesheetText" value="true" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/update-firewall-source.iml" filepath="$PROJECT_DIR$/.idea/update-firewall-source.iml" />
</modules>
</component>
</project>

3
.idea/scopes/ini_example.xml generated Normal file
View File

@ -0,0 +1,3 @@
<component name="DependencyValidationManager">
<scope name="ini_example" pattern="file:examples/config.ini.example||file:README.md" />
</component>

12
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

25
.idea/watcherTasks.xml generated Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="-r &quot;$ContentRoot$\README.md&quot;" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="*" />
<option name="immediateSync" value="true" />
<option name="name" value="Run cog on Markdown files" />
<option name="output" value="" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$USER_HOME$/AppData/Local/Miniconda3/envs/cog/Scripts/cog" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="ini_example" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
</component>
</project>

105
README.md
View File

@ -1,3 +1,106 @@
# update-firewall-source
Update a firewall rule that relies on dynamic DNS names
Update a firewall rule that relies on dynamic DNS names
## What
* This script assumes exclusive ownership of the `firewalld` direct rules file `/etc/firewalld/direct.xml` or whereever configured
* List of address can be empty, direct file will then be removed
* After every execution script will trigger systemd firewalld service restart
* No subnet, will simply not be validated
* Include example systemd unit file and install instructions
* firewall-cmd --check-config
* default location for config file and default name for config file
* we should deduplicate list
* is intended to help with just docker-user
* man page https://ipset.netfilter.org/iptables.man.html we do iptables
* added in order
* related, established? needed?
## Config structure
Package configuration happens via a `config.ini` file that follows INI-style syntax. Copy [examples/config.ini.example](examples/config.ini.example) to `config.ini` to get started:
<!-- [[[cog
import os
examples_dir = "examples"
config_ini_example = "config.ini.example"
config_ini_example_abs = os.path.join(examples_dir, config_ini_example)
try:
with open(config_ini_example_abs) as config_ini_example_handle:
config_ini_example_content = config_ini_example_handle.read()
except OSError:
pass
else:
cog.out(f"```\n"
f"{config_ini_example_content.rstrip()}\n"
f"```")
]]] -->
```
[DEFAULT]
target = ACCEPT
addr =
ports = 80, 443
proto = tcp
[anyone-can-access-website]
[these-guys-can-dns]
addr = google.li, 142.251.36.195, lowendbox.com, 2606:4700:20::ac43:4775
ports = 53
proto = tcp, udp
[maybe-a-webserver]
addr = 2606:4700:20::681a:804, lowendtalk.com
ports = 80, 443
[allow-anyone-to-access-mail-services]
ports = 143, 993, 110, 995, 25, 465, 587
[deny-all]
target = DENY
addr =
ports =
proto =
```
<!-- [[[end]]] -->
### 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.
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
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:
```
[deny-all]
target = DENY
addr =
ports =
proto =
```
If a packet has traversed rules this far without being accepted it will be dropped.
## Options
A custom `[section]` has the following options 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`.
* `addr`, __*optional*__: A comma-separated list of any combination of IPv4 addresses, IPv6 addresses and domain names. When `update-firewall-source.py` constructs `firewalld` rules these addresses are allowed to access the server. If left undefined `addr` defaults to an empty list meaning rules apply to any arbitrary source address.
Subnets are unsupported, both as subnet masks (`142.251.36.195/255.255.255.248`) and in [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation (`142.251.36.195/29`). Do not single- nor double-quote list entries. Do feel free to separate entries with comma-space instead of just a comma.
* `ports`, __*optional*__, defaults to `80, 443`: A comma-separated list of ports that should be accessible from `addr`. If overwritten to an empty option `addr` may access all ports.
* `proto`, __*optional*__, defaults to `tcp`: A protocol that should be allowed for `addr` on `ports`. If undefined this defaults to `tcp` being allowed. Since `firewalld` direct rules use `iptables` syntax the list of possible protocol names is largely identical to what the [iptables man page](https://ipset.netfilter.org/iptables.man.html) says about its `--protocol` argument:
> The specified protocol can be one of `tcp`, `udp`, `udplite`, `icmp`, `icmpv6`, `esp`, `ah`, `sctp`, `mh` or the special keyword `all`, or it can be a numeric value, representing one of these protocols or a different one. A protocol name from `/etc/protocols` is also allowed. A `!` argument before the protocol inverts the test. The number zero is equivalent to `all`.
Your mileage may vary depending on which specific OS flavor and version you're running.

19
cog/render-readme.py Normal file
View File

@ -0,0 +1,19 @@
# Base example looks like this. Put below code in empty 'cog' line, replace print() with cog.out()
# < !-- [[[cog
#
# ]]] -->
# <!-- [[[end]]] -->
import os
examples_dir = "examples"
config_ini_example = "config.ini.example"
config_ini_example_abs = os.path.join(examples_dir, config_ini_example)
try:
with open(config_ini_example_abs) as config_ini_example_handle:
config_ini_example_content = config_ini_example_handle.read()
except OSError:
pass
else:
print(f"```\n"
f"{config_ini_example_content.rstrip()}\n"
f"```")

View File

@ -0,0 +1,25 @@
[DEFAULT]
target = ACCEPT
addr =
ports = 80, 443
proto = tcp
[anyone-can-access-website]
[these-guys-can-dns]
addr = google.li, 142.251.36.195, lowendbox.com, 2606:4700:20::ac43:4775
ports = 53
proto = tcp, udp
[maybe-a-webserver]
addr = 2606:4700:20::681a:804, lowendtalk.com
ports = 80, 443
[allow-anyone-to-access-mail-services]
ports = 143, 993, 110, 995, 25, 465, 587
[deny-all]
target = DENY
addr =
ports =
proto =

4
requirements.in Normal file
View File

@ -0,0 +1,4 @@
lxml
rich
validators
dnspython

20
requirements.txt Normal file
View File

@ -0,0 +1,20 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile
#
commonmark==0.9.1
# via rich
decorator==5.1.1
# via validators
dnspython==2.2.1
# via -r requirements.in
lxml==4.9.0
# via -r requirements.in
pygments==2.12.0
# via rich
rich==12.4.4
# via -r requirements.in
validators==0.20.0
# via -r requirements.in

254
update-firewall-source.py Normal file
View File

@ -0,0 +1,254 @@
# Exit with various exit codes
import sys
# Path manipulation
import os
# Manipulate style and content of logs
import logging
from rich.logging import RichHandler
# Use a config file
import configparser
# Verify IP address family
import ipaddress
# Resolve host names
import dns.resolver
# Validate if string is fqdn
import validators
# Build XML structure
import lxml.etree
import lxml.builder
# Exit codes
# 1: Config file invalid, it has no sections
# 2: Config file invalid, sections must define at least CONST.CFG_MANDATORY
class CONST(object):
__slots__ = ()
LOG_FORMAT = "%(message)s"
# How to find a config file
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
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"}
]
# 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}
]
CFG_MANDATORY = [section_cfg["key"] for section_cfg in CFG_KNOWN_SECTION if section_cfg["is_mandatory"]]
logging.basicConfig(
# Default for all modules is NOTSET so log everything
level="NOTSET",
format=CONST.LOG_FORMAT,
datefmt="[%X]",
handlers=[RichHandler(
rich_tracebacks=True
)]
)
log = logging.getLogger("rich")
# Our own code logs with this level
log.setLevel(logging.DEBUG)
# Use this version of class ConfigParser to log.debug contents of our config file. When parsing sections other than
# 'default' we don't want to reprint defaults over and over again. This custom class achieves that.
class ConfigParser(
configparser.ConfigParser):
"""Can get options() without defaults
Taken from https://stackoverflow.com/a/12600066.
"""
def options(self, section, no_defaults=False, **kwargs):
if no_defaults:
try:
return list(self._sections[section].keys())
except KeyError:
raise configparser.NoSectionError(section)
else:
return super().options(section)
# arg_allow_list = ["77.13.129.237", "2a0b:7080:20::1:f485", "home.seneve.de", "208.87.98.188", "outlook.com",
# "uberspace.de"]
ini_defaults = []
internal_defaults = {default["key"]: default["value"] for default in CONST.CFG_KNOWN_DEFAULTS}
config = ConfigParser(defaults=internal_defaults,
converters={'list': lambda x: [i.strip() for i in x.split(',')]})
config.read(CONST.CFG_DEFAULT_ABS_PATH)
def print_section_header(
header: str) -> str:
return f"Loading config section '[{header}]' ..."
def validate_default_section(
config_obj: configparser.ConfigParser()) -> None:
log.debug(f"Loading config from file '{CONST.CFG_DEFAULT_ABS_PATH}' ...")
if not config_obj.sections():
log.debug(f"No config sections found in '{CONST.CFG_DEFAULT_ABS_PATH}'. Exiting 1 ...")
sys.exit(1)
if config.defaults():
log.debug(f"Symbol legend:\n"
f"* Global default from section '[{config_obj.default_section}]'\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}]'")
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]}")
else:
log.debug(f"No defaults defined")
def config_has_valid_section(
config_obj: configparser.ConfigParser()) -> bool:
has_valid_section = False
for config_obj_section in config_obj.sections():
if set(CONST.CFG_MANDATORY).issubset(config_obj.options(config_obj_section)):
has_valid_section = True
break
return has_valid_section
def is_default(
config_key: str) -> bool:
return any(config_key in ini_default for ini_default in ini_defaults)
def is_same_as_default(
config_kv_pair: dict) -> bool:
return config_kv_pair in ini_defaults
def validate_config_sections(
config_obj: configparser.ConfigParser()) -> None:
for this_section in config_obj.sections():
log.debug(print_section_header(this_section))
if not set(CONST.CFG_MANDATORY).issubset(config_obj.options(this_section, no_defaults=True)):
log.debug(f"Config section '[{this_section}]' does not have all mandatory options "
f"{CONST.CFG_MANDATORY} set, skipping section ...")
config_obj.remove_section(this_section)
else:
for key in config_obj.options(this_section, no_defaults=True):
kv_prefix = "~"
if 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]}")
def gen_fw_rule_xml(ip_addresses: dict[str, list]) -> lxml.builder.ElementMaker:
len_ipv4_addresses = len(ip_addresses["ipv4"])
len_ipv6_addresses = len(ip_addresses["ipv6"])
data = lxml.builder.ElementMaker()
direct_tag = data.direct
chain_tag = data.chain
rule_tag = data.rule
fw_rule_data = direct_tag(
chain_tag(ipv="ipv4", table="filter", chain="DOCKER-USER"),
# rule_tag("-s 208.87.98.188 -j DROP", ipv="ipv4", table="filter", chain="DOCKER-USER", priority="0"),
chain_tag(ipv="ipv6", table="filter", chain="DOCKER-USER"),
# rule_tag("-s 2a0b:7080:20::1:f485 -j DROP", ipv="ipv6", table="filter", chain="DOCKER-USER", priority="0")
*(rule_tag(f"-s {addr} -j DROP", ipv=f"ipv4", table=f"filter", chain="DOCKER-USER", priority=f"{count}")
for count, addr in enumerate(ip_addresses["ipv4"])),
*(rule_tag(f"-s {addr} -j DROP", ipv=f"ipv6", table=f"filter", chain="DOCKER-USER", priority=f"{count}")
for count, addr in enumerate(ip_addresses["ipv6"])),
rule_tag(f"-s -j DROP", ipv="ipv4", table="filter", chain="DOCKER-USER", priority=f"{len_ipv4_addresses}"),
rule_tag(f"-s -j DROP", ipv="ipv6", table="filter", chain="DOCKER-USER", priority=f"{len_ipv6_addresses}")
)
# fw_rule_data_str = lxml.etree.tostring(
# fw_rule_data,
# pretty_print=True,
# xml_declaration=True,
# encoding="UTF-8").decode()
# log.debug(f"{fw_rule_data_str}")
return fw_rule_data
def resolve_domain(domain: str) -> list[str]:
log.debug(f"Resolving DNS A and AAAA records for '{domain}' ...")
try:
a_records = dns.resolver.resolve(domain, rdtype=dns.rdatatype.A)
except dns.resolver.NoAnswer:
log.debug(f"DNS didn't return an A record for '{domain}', ignoring ...")
a_records = []
try:
aaaa_records = dns.resolver.resolve(domain, rdtype=dns.rdatatype.AAAA)
except dns.resolver.NoAnswer:
log.debug(f"DNS didn't return a AAAA record for '{domain}', ignoring ...")
aaaa_records = []
dns_records = []
[dns_records.append(dns_record.address) for dns_record in a_records if a_records]
[dns_records.append(dns_record.address) for dns_record in aaaa_records if aaaa_records]
log.debug(f"Found records: {dns_records}")
return dns_records
def resolve_addresses(allow_list_mixed: list[str]) -> dict[str, list]:
allow_sources = {"ipv4": [], "ipv6": []}
allow_list_ip_only = []
for allow_source in allow_list_mixed:
if validators.domain(allow_source):
log.debug(f"'{allow_source}' is a domain.")
[allow_list_ip_only.append(addr) for addr in resolve_domain(allow_source)]
else:
allow_list_ip_only.append(allow_source)
for allow_source in allow_list_ip_only:
try:
ipv4_addr = str(ipaddress.IPv4Address(allow_source))
log.debug(f"Adding IPv4 address '{allow_source}' ...")
allow_sources["ipv4"].append(ipv4_addr)
except ipaddress.AddressValueError:
log.debug(f"Address '{allow_source}' is not a valid IPv4 address. Trying to match against IPv6 ...")
try:
ipv6_addr = str(ipaddress.IPv6Address(allow_source))
log.debug(f"Adding IPv6 address '{allow_source}' ...")
allow_sources["ipv6"].append(ipv6_addr)
except ipaddress.AddressValueError:
log.warning(f"Address '{allow_source}' is not a valid IPv6 address either. Ignoring ...")
return allow_sources
if __name__ == '__main__':
validate_default_section(config)
if config_has_valid_section(config):
validate_config_sections(config)
else:
log.debug(f"No valid config section found. A valid config section has at least the mandatory options "
f"{CONST.CFG_MANDATORY} set. Exiting 2 ...")
sys.exit(2)
log.debug(f"Iterating over config sections ...")
for section in config.sections():
log.debug(f"Processing section '[{section}]' ...")
log.debug(config.getlist(section, "addr"))
# arg_allow_sources = resolve_addresses(arg_allow_list)
# gen_fw_rule_xml(arg_allow_sources)