Compare commits
2 Commits
6268884c00
...
19372524d5
Author | SHA1 | Date | |
---|---|---|---|
19372524d5 | |||
f061fdc8cc |
241
.gitignore
vendored
Normal file
241
.gitignore
vendored
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
|
35
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
35
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="customStylesheetText" value="body { font-size: 95% !important; } " />
|
||||
<option name="useCustomStylesheetText" value="true" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal 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
3
.idea/scopes/ini_example.xml
generated
Normal 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
12
.idea/vcs.xml
generated
Normal 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
25
.idea/watcherTasks.xml
generated
Normal 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 "$ContentRoot$\README.md"" />
|
||||
<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
105
README.md
@ -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
19
cog/render-readme.py
Normal 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"```")
|
25
examples/config.ini.example
Normal file
25
examples/config.ini.example
Normal 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
4
requirements.in
Normal file
@ -0,0 +1,4 @@
|
||||
lxml
|
||||
rich
|
||||
validators
|
||||
dnspython
|
20
requirements.txt
Normal file
20
requirements.txt
Normal 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
254
update-firewall-source.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user