Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57195a822b | ||
|
|
0590caeebd | ||
|
|
962be7a048 | ||
|
|
204a946a84 | ||
|
|
d72dc5a115 | ||
|
|
b659268117 | ||
|
|
8c4bc91f6e | ||
|
|
c8e28d2397 | ||
|
|
0349204aff | ||
|
|
e4dbd7b014 | ||
|
|
07f8387963 | ||
|
|
d8acd3f525 | ||
|
|
8c1e9502f4 | ||
|
|
86457cc858 | ||
|
|
0bad0e31df |
No files matched your search
@@ -6,7 +6,7 @@
|
|||||||
# separate terms of service, privacy policy, and support
|
# separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# documentation.
|
||||||
|
|
||||||
name: Upload Python Package
|
name: Create and Upload Python Package
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -16,9 +16,12 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-build:
|
build-and-publish:
|
||||||
|
name: Build and Publish
|
||||||
|
environment: pypi
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -26,45 +29,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Build release distributions
|
- name: Install build tools
|
||||||
run: |
|
run: |
|
||||||
# NOTE: put your own distribution build steps here.
|
python -m pip install --upgrade pip
|
||||||
python -m pip install build
|
python -m pip install build
|
||||||
python -m build
|
|
||||||
|
|
||||||
- name: Upload distributions
|
- name: Build release
|
||||||
uses: actions/upload-artifact@v4
|
run: |
|
||||||
with:
|
python -m build --sdist --wheel
|
||||||
name: release-dists
|
|
||||||
path: dist/
|
|
||||||
|
|
||||||
pypi-publish:
|
- name: Publish package distributions to PyPI
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- release-build
|
|
||||||
permissions:
|
|
||||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Dedicated environments with protections for publishing are strongly recommended.
|
|
||||||
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
||||||
environment:
|
|
||||||
name: pypi
|
|
||||||
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
||||||
# url: https://pypi.org/p/YOURPROJECT
|
|
||||||
#
|
|
||||||
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
||||||
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
||||||
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve release distributions
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-dists
|
|
||||||
path: dist/
|
|
||||||
|
|
||||||
- name: Publish release distributions to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
|
||||||
packages-dir: dist/
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
src/netbox_plugin_bind_provisioner.egg-info
|
src/netbox_plugin_bind_provisioner.egg-info
|
||||||
dist/
|
dist/
|
||||||
|
__pycache__
|
||||||
@@ -1,124 +1,208 @@
|
|||||||
# Netbox Bind Provisioner
|
# Netbox Bind Provisioner
|
||||||
A Netbox Plugin that provides a minimal DNS Server for the automatic
|
A NetBox plugin that implements a lightweight DNS server to automate the
|
||||||
provisioning of a Bind9 Server from Netbox DNS data (netbox-plugin-dns).
|
provisioning and configuration of a BIND9 server based on DNS records
|
||||||
|
maintained in NetBox. (netbox-plugin-dns).
|
||||||
|
|
||||||
## Plugin configuration
|
## Plugin configuration
|
||||||
The plugin was re-worked; Instead of exporting zone files, it provides an
|
The plugin has been reworked: instead of exporting zone files, it now provides
|
||||||
entire (though minimal) DNS Server that is fed directly from Netbox's DNS data.
|
a minimal DNS server that is fed directly from NetBox’s DNS data. This server
|
||||||
The Server also provides specialized "catalog" zones that bind uses to
|
also exposes specialized catalog zones that BIND uses to automatically discover
|
||||||
automatically discover new zones and remove deleted ones. The plugin supports
|
newly created zones and remove deleted ones. The plugin supports BIND views and
|
||||||
view as well as basic dns security using TSIG.
|
basic DNS security via TSIG.
|
||||||
|
|
||||||
To work correctly, each view needs a tsig key installed and the
|
The plugin exposes one catalog zone per view. Each catalog zone is available
|
||||||
bind-transfer-endpoint needs to be running as its own service in the background
|
under the special zone name **"catz"** and can be queried through the built-in
|
||||||
via the `manage.py` command. Note that dnssec support will be implemented as
|
DNS server like any other zone.
|
||||||
soon as bind9 has a mechanism to allow configuration of such via the Catalog
|
|
||||||
Zones mechanism.
|
For proper operation, each view requires an installed TSIG key, and the
|
||||||
|
`bind-transfer-endpoint` must be running as a separate background service using
|
||||||
|
the `manage.py` command. Note that DNSSEC support will be added once BIND9
|
||||||
|
provides a mechanism to configure it through the Catalog Zones system.
|
||||||
|
|
||||||
|
To start the service:
|
||||||
```
|
```
|
||||||
venv/bin/python3 netbox/netbox/manage.py bind-transfer-endpoint --port 5354
|
manage.py bind-transfer-endpoint --port 5354
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parameters
|
When configuring the plugin, you need to provide a path on the filesystem for
|
||||||
|
this file where the plugin can create and manage it. Please do not update it by
|
||||||
|
hand or remove it unless you know what you are doing.
|
||||||
|
|
||||||
|
### Service parameters
|
||||||
Parameter | Description
|
Parameter | Description
|
||||||
--------- | --------------------------------------------------------
|
--------- | -------------------------------------------------------------------
|
||||||
--port | Port to listen on for requests (defaults to 5354)
|
--port | Port to listen on for requests (defaults to 5354)
|
||||||
--address | IP of interface to bind to (defaults to 0.0.0.0)
|
--address | IP of interface to bind to (defaults to 0.0.0.0)
|
||||||
|
|
||||||
```
|
### Plugin settings
|
||||||
PLUGINS_CONFIG = {
|
Setting | Description
|
||||||
"netbox_bind_provisioner": {
|
--------------------| ---------------------------------------------------------
|
||||||
"tsig_keys": {
|
catalog_serial_file | !!!NO LONGER USED!!! Replaced by DB entry.
|
||||||
"key1name": {
|
tsig_keys | Maps a TSIG Key to be used for each view.
|
||||||
"view": "public",
|
|
||||||
"keyname": "view1key",
|
## Installation guide
|
||||||
"algorithm": "hmac-sha256",
|
This setup provisions a BIND9 server directly with DNS data from NetBox.
|
||||||
"secret": "base64-encoded-secret"
|
BIND9 can optionally run on a separate server. If so, any reference to
|
||||||
},
|
127.0.0.1 in step 6 must be replaced with the IP address of the NetBox host.
|
||||||
"key2name": {
|
TCP and UDP traffic from the BIND9 server to the NetBox host must be allowed on
|
||||||
"view": "private",
|
port 5354 (or the port you have configured).
|
||||||
"keyname": "view2key",
|
|
||||||
"algorithm": "hmac-sha256",
|
This guide assumes:
|
||||||
"secret": "base64-encoded-secret"
|
- Netbox has been installed under /opt/netbox
|
||||||
|
- Bind9 is installed on the same host as Netbox
|
||||||
|
- The Netbox DNS Plugin netbox-plugin-dns is installed
|
||||||
|
- The following dns views exist in Netbox DNS:
|
||||||
|
- `public` (the default)
|
||||||
|
- `private`
|
||||||
|
|
||||||
|
1. Preliminaries
|
||||||
|
- Install Bind9 on the same host that netbox is on.
|
||||||
|
- Generate a TSIG Key for the `public` and `private` dns views respectively.
|
||||||
|
|
||||||
|
2. Adding required package
|
||||||
|
```
|
||||||
|
cd netbox
|
||||||
|
echo netbox-plugin-bind-provisioner >> local_requirements.txt
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -r local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Updating netbox plugin configuration (configuration.py)
|
||||||
|
Change following line from
|
||||||
|
```
|
||||||
|
PLUGINS = ['netbox_dns']
|
||||||
|
```
|
||||||
|
to
|
||||||
|
```
|
||||||
|
PLUGINS = ['netbox_dns', 'netbox_plugin_bind_provisioner']
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the Bind Exporter Plugin using the PLUGINS_CONFIG dictionary.
|
||||||
|
Change
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG = {}
|
||||||
|
```
|
||||||
|
to
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG = {
|
||||||
|
"netbox_plugin_bind_provisioner": {
|
||||||
|
"tsig_keys": {
|
||||||
|
"public": {
|
||||||
|
"keyname": "public_view_key",
|
||||||
|
"algorithm": "hmac-sha256",
|
||||||
|
"secret": "base64-encoded-secret"
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"keyname": "private_view_key",
|
||||||
|
"algorithm": "hmac-sha256",
|
||||||
|
"secret": "base64-encoded-secret"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
```
|
||||||
```
|
Note that the tsig-key attributes keyname, algorithm and secret form a
|
||||||
|
dictionary in following python structure path:
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG.netbox_plugin_bind_provisioner.tsig_keys.<dns_view_name>
|
||||||
|
```
|
||||||
|
This allows the plugin to map requests to the right dns view using the tsig
|
||||||
|
signature from each request.
|
||||||
|
|
||||||
## Bind configuration
|
4. Run migrations
|
||||||
```
|
```
|
||||||
options {
|
python3 netbox/manage.py migrate
|
||||||
...
|
```
|
||||||
...
|
|
||||||
allow-update { none; };
|
|
||||||
allow-query { any; };
|
|
||||||
allow-recursion { none; };
|
|
||||||
notify yes;
|
|
||||||
...
|
|
||||||
...
|
|
||||||
};
|
|
||||||
|
|
||||||
# ACLs
|
5. Start listener
|
||||||
|
This step runs the DNS endpoint used by bind to configure itself. You may want
|
||||||
|
to write a service wrapper that runs this in the background. Dont forget to
|
||||||
|
activate the venv if you do decide to run this service in the background.
|
||||||
|
Note that `--port 5354` is optional. The listener will bind this port
|
||||||
|
by default.
|
||||||
|
```
|
||||||
|
python3 netbox/manage.py bind-transfer-endpoint --port 5354
|
||||||
|
```
|
||||||
|
|
||||||
acl public {
|
6. Configuring a Bind9 to interact with Netbox via the bind-transfer-endpoint
|
||||||
!10.0.0.0/8;
|
endpoint. Note that its not possible to give all the correct details of the
|
||||||
!172.16.0.0/12;
|
`options` block as it is heavily dependent on the Operating System used.
|
||||||
!192.168.0.0/16;
|
Please dont forget to adjust as required.
|
||||||
any;
|
|
||||||
};
|
```
|
||||||
|
########## OPTIONS ##########
|
||||||
|
|
||||||
acl private {
|
options {
|
||||||
10.0.0.0/8;
|
allow-update { none; };
|
||||||
172.16.0.0/12;
|
allow-query { any; };
|
||||||
192.168.0.0/16;
|
allow-recursion { none; };
|
||||||
};
|
notify yes;
|
||||||
|
min-refresh-time 60;
|
||||||
# Zone definitions
|
|
||||||
|
|
||||||
view "public" {
|
|
||||||
key "key1name" {
|
|
||||||
algorithm hmac-sha256;
|
|
||||||
secret "base64-encoded-secret";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match-clients { public; };
|
########## ACLs ##########
|
||||||
|
|
||||||
catalog-zones {
|
acl public {
|
||||||
zone "catz"
|
!10.0.0.0/8;
|
||||||
default-masters { 127.0.0.1 port 5354 key "key1name"; }
|
!172.16.0.0/12;
|
||||||
zone-directory "/var/lib/bind/zones"
|
!192.168.0.0/16;
|
||||||
min-update-interval 1;
|
any;
|
||||||
};
|
};
|
||||||
|
|
||||||
zone "catz" {
|
acl private {
|
||||||
type slave;
|
10.0.0.0/8;
|
||||||
file "/var/lib/bind/zones/catz_public";
|
172.16.0.0/12;
|
||||||
masters { 127.0.0.1 port 5354 key "key1name"; };
|
192.168.0.0/16;
|
||||||
notify no;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
view "private" {
|
|
||||||
key "key2name" {
|
|
||||||
algorithm hmac-sha256;
|
|
||||||
secret "base64-encoded-secret";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match-clients { private; };
|
########## ZONES ##########
|
||||||
|
|
||||||
catalog-zones {
|
view "public" {
|
||||||
zone "catz"
|
key "public_view_key" {
|
||||||
default-masters { 127.0.0.1 port 5354 key "key2name"; }
|
algorithm hmac-sha256;
|
||||||
zone-directory "/var/lib/bind/zones"
|
secret "base64-encoded-secret";
|
||||||
min-update-interval 1;
|
};
|
||||||
|
|
||||||
|
match-clients { public; };
|
||||||
|
|
||||||
|
catalog-zones {
|
||||||
|
zone "catz"
|
||||||
|
default-masters { 127.0.0.1 port 5354 key "public_view_key"; }
|
||||||
|
zone-directory "/var/lib/bind/zones"
|
||||||
|
min-update-interval 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
zone "catz" {
|
||||||
|
type slave;
|
||||||
|
file "/var/lib/bind/zones/catz_public";
|
||||||
|
masters { 127.0.0.1 port 5354 key "public_view_key"; };
|
||||||
|
notify no;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
zone "catz" {
|
view "private" {
|
||||||
type slave;
|
key "private_view_key" {
|
||||||
file "/var/lib/bind/zones/catz_private";
|
algorithm hmac-sha256;
|
||||||
masters { 127.0.0.1 port 5354 key "key2name"; };
|
secret "base64-encoded-secret";
|
||||||
notify no;
|
};
|
||||||
|
|
||||||
|
match-clients { private; };
|
||||||
|
|
||||||
|
catalog-zones {
|
||||||
|
zone "catz"
|
||||||
|
default-masters { 127.0.0.1 port 5354 key "private_view_key"; }
|
||||||
|
zone-directory "/var/lib/bind/zones"
|
||||||
|
min-update-interval 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
zone "catz" {
|
||||||
|
type slave;
|
||||||
|
file "/var/lib/bind/zones/catz_private";
|
||||||
|
masters { 127.0.0.1 port 5354 key "private_view_key"; };
|
||||||
|
notify no;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
```
|
||||||
```
|
|
||||||
|
7. Restart bind - Done
|
||||||
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
netbox-plugin-dns>=1.2.0
|
netbox-plugin-dns>=1.2.0
|
||||||
|
dnspython>=2.8.0
|
||||||
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
__version__ = "0.9.5"
|
__version__ = "0.9.8"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,24 +32,5 @@ class BindProvisionerConfig(PluginConfig):
|
|||||||
f"{self.name}: Plugin {self.verbose_name} failed to initialize due to missing settings. Terminating Netbox."
|
f"{self.name}: Plugin {self.verbose_name} failed to initialize due to missing settings. Terminating Netbox."
|
||||||
)
|
)
|
||||||
|
|
||||||
# from . import bind, rndc
|
|
||||||
#
|
|
||||||
# if not bind.zoneDirWritable():
|
|
||||||
# BIND_ZONE_DIR = settings.get("BIND_ZONE_DIR")
|
|
||||||
# logger.error(f"Bind's zone file directory {BIND_ZONE_DIR} is not writable.")
|
|
||||||
# raise RuntimeError(
|
|
||||||
# "Plugin initialization failed due to failed writable-zone-directory healthcheck."
|
|
||||||
# )
|
|
||||||
# if not rndc.tsigKeyFileAccessible():
|
|
||||||
# RNDC_KEY_FILE = settings.get("RNDC_KEY_FILE")
|
|
||||||
# logger.error(f"The RNDC key file {RNDC_KEY_FILE} is inaccessible.")
|
|
||||||
# raise RuntimeError(
|
|
||||||
# "Plugin initialization failed due to failed rndc-key-accessible healthcheck."
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# # Import signal callbacks from signals.py (With @receiver decorations)
|
|
||||||
# from . import signals
|
|
||||||
|
|
||||||
|
|
||||||
config = BindProvisionerConfig
|
config = BindProvisionerConfig
|
||||||
default_app_config = ".apps.NetboxBindProvisionerConfig"
|
default_app_config = ".apps.NetboxBindProvisionerConfig"
|
||||||
@@ -20,70 +20,44 @@ from django.utils import timezone
|
|||||||
from netbox_dns.models import Zone, Record, View
|
from netbox_dns.models import Zone, Record, View
|
||||||
from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices
|
from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from netbox_plugin_bind_provisioner.models import IntegerKeyValueSetting
|
||||||
|
|
||||||
logger = logging.getLogger('bind-transfer-endpoint')
|
logger = logging.getLogger('bind-transfer-endpoint')
|
||||||
|
|
||||||
class CatalogZoneAssembler():
|
class CatalogZone():
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
_serial_max = 0xFFFFFFFF
|
_serial_max = 0xFFFFFFFF
|
||||||
_serial = 1
|
_serial_obj = None
|
||||||
_serial_file = None
|
|
||||||
_previous_last_zone_update = None
|
_previous_last_zone_update = None
|
||||||
|
|
||||||
# Following function loads the last serial from file. No return value
|
# Following function loads the last serial from the DB. No return value
|
||||||
# but it sets the setting "catalog_serial" or terminates the plugin on failure.
|
# but it sets the setting "catalog_serial" or terminates the plugin on failure.
|
||||||
@classmethod
|
@classmethod
|
||||||
def initCatalogZoneSerial(cls, serial_file):
|
def initSerial(cls):
|
||||||
"""
|
try:
|
||||||
Load a 32-bit unsigned int serial from a text file.
|
cls._serial_obj = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
|
||||||
If the file doesn't exist, initialize it with 1.
|
logger.debug(f"Catalog zone SOA serial number {cls._serial_obj.value} loaded from database")
|
||||||
Returns the serial as an int.
|
except IntegerKeyValueSetting.DoesNotExist:
|
||||||
"""
|
cls._serial_obj = IntegerKeyValueSetting.objects.create(key="catalog-zone-soa-serial", value=1)
|
||||||
cls._serial_file = serial_file
|
logger.debug(f"Catalog zone SOA serial number was not set in the database. Set to {cls._serial_obj.value}")
|
||||||
|
|
||||||
if not os.path.exists(cls._serial_file):
|
|
||||||
try:
|
|
||||||
with open(cls._serial_file, "w") as f:
|
|
||||||
f.write(f"{cls._serial}\n")
|
|
||||||
return
|
|
||||||
except OSError as e:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Failed to initialize serial file {cls._serial_file}: {e}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(cls._serial_file, "r") as f:
|
|
||||||
value = int(f.read().strip())
|
|
||||||
|
|
||||||
if 0 < value <= cls._serial_max:
|
|
||||||
cls._serial = value
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Serial value out of 32-bit range: {value}")
|
|
||||||
except (OSError, ValueError) as e:
|
|
||||||
raise RuntimeError(f"Failed to read serial from {cls._serial_file}: {e}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def incrementCatalogZoneSerial(cls):
|
def _incrementSerial(cls):
|
||||||
"""
|
if 0 < cls._serial_obj.value < cls._serial_max:
|
||||||
Increments the in-memory catalog serial and saves it to file.
|
cls._serial_obj.value += 1
|
||||||
Updates _serial.
|
cls._serial_obj.save()
|
||||||
"""
|
|
||||||
if 0 < cls._serial < cls._serial_max:
|
|
||||||
cls._serial += 1
|
|
||||||
else:
|
else:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
f"Failed to incremenet catalog serial {cls._serial}. Will overflow serial back to 1."
|
f"Failed to incremenet catalog serial {cls._serial_obj.value}. Will overflow serial back to 1"
|
||||||
)
|
)
|
||||||
cls._serial = 1
|
cls._serial_obj = 1
|
||||||
|
cls._serial_obj.save()
|
||||||
|
logger.debug(f"Catalog zone SOA serial number incremented to {_serial_obj.value}")
|
||||||
|
|
||||||
try:
|
|
||||||
with open(cls._serial_file, "w") as f:
|
|
||||||
f.write(f"{cls._serial}\n")
|
|
||||||
except OSError as e:
|
|
||||||
logger.error(f"Failed to write updated serial to {cls._serial_file}: {e}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def createCatalogZone(cls, view_name, soa_only=False):
|
def create(cls, view_name, soa_only=False):
|
||||||
# Synchronize following across threads as TCP and UDP listener both use it.
|
# Synchronize following across threads as TCP and UDP listener both use it.
|
||||||
with cls.lock:
|
with cls.lock:
|
||||||
#cls = self.__class__
|
#cls = self.__class__
|
||||||
@@ -102,7 +76,7 @@ class CatalogZoneAssembler():
|
|||||||
)
|
)
|
||||||
# Setting previous last zone update for next iteration:
|
# Setting previous last zone update for next iteration:
|
||||||
cls._previous_last_zone_update = last_zone_update
|
cls._previous_last_zone_update = last_zone_update
|
||||||
cls.incrementCatalogZoneSerial()
|
cls._incrementSerial()
|
||||||
# If there was no update, do nothing.
|
# If there was no update, do nothing.
|
||||||
#else:
|
#else:
|
||||||
# logger.debug(f"No need to update serial for catalog zone")
|
# logger.debug(f"No need to update serial for catalog zone")
|
||||||
@@ -159,7 +133,7 @@ class CatalogZoneAssembler():
|
|||||||
rtype = dns.rdatatype.SOA
|
rtype = dns.rdatatype.SOA
|
||||||
mname = dns.name.root # dns.name.from_text("invalid.", origin)
|
mname = dns.name.root # dns.name.from_text("invalid.", origin)
|
||||||
rname = dns.name.root # dns.name.from_text("contact.master.", origin)
|
rname = dns.name.root # dns.name.from_text("contact.master.", origin)
|
||||||
serial = cls._serial
|
serial = cls._serial_obj.value
|
||||||
refresh = 60
|
refresh = 60
|
||||||
retry = 10
|
retry = 10
|
||||||
expire = 1209600
|
expire = 1209600
|
||||||
@@ -208,9 +182,22 @@ class CatalogZoneAssembler():
|
|||||||
|
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def importLegacySerialFile(cls, file_path):
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
serial = int(f.read().strip())
|
||||||
|
cls._serial_obj.value = int(serial)
|
||||||
|
cls._serial_obj.save()
|
||||||
|
logger.info(f"Legacy catalog serial file {file_path} imported into database and file removed. Serial is now {serial}")
|
||||||
|
os.remove(file_path)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Failed to import catalog zone serial from legacy file {file_path}")
|
||||||
|
|
||||||
|
|
||||||
class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
||||||
# getZoneFromNB rewritten
|
# getZoneFromNB rewritten
|
||||||
def getZoneFromNB(self, zone_name, view_name, soa_only=False):
|
def getZoneFromNB(self, zone_name, view_name, soa_only=False):
|
||||||
# Find the zone
|
# Find the zone
|
||||||
try:
|
try:
|
||||||
@@ -273,17 +260,15 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|||||||
|
|
||||||
# Replace the rdataset for the given name and type
|
# Replace the rdataset for the given name and type
|
||||||
zone.replace_rdataset(name, rdataset)
|
zone.replace_rdataset(name, rdataset)
|
||||||
|
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
|
|
||||||
|
# def denyRequest(self, query, rcode: dns.rcode = dns.rcode.REFUSED):
|
||||||
def denyRequest(self, query, rcode: dns.rcode = dns.rcode.REFUSED):
|
# response = dns.message.make_response(query)
|
||||||
response = dns.message.make_response(query)
|
# response.set_rcode(rcode)
|
||||||
response.set_rcode(rcode)
|
# wire = response.to_wire(multi=False)
|
||||||
wire = response.to_wire(multi=False)
|
# length = len(wire).to_bytes(2, byteorder="big")
|
||||||
length = len(wire).to_bytes(2, byteorder="big")
|
# self.request.sendall(length + wire)
|
||||||
self.request.sendall(length + wire)
|
|
||||||
|
|
||||||
|
|
||||||
def denyRequestBadTSIG(self, wire, tsig_error: dns.rcode):
|
def denyRequestBadTSIG(self, wire, tsig_error: dns.rcode):
|
||||||
@@ -305,6 +290,13 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class UDPRequestHandler(DNSBaseRequestHandler):
|
class UDPRequestHandler(DNSBaseRequestHandler):
|
||||||
|
def denyRequest(self, query, rcode: dns.rcode = dns.rcode.REFUSED):
|
||||||
|
response = dns.message.make_response(query)
|
||||||
|
response.set_rcode(rcode)
|
||||||
|
wire = response.to_wire(multi=False)
|
||||||
|
sock = self.request[1]
|
||||||
|
sock.sendto(wire, self.client_address)
|
||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
data, sock = self.request
|
data, sock = self.request
|
||||||
peer = self.client_address[0]
|
peer = self.client_address[0]
|
||||||
@@ -314,11 +306,11 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
query = dns.message.from_wire(
|
query = dns.message.from_wire(
|
||||||
data,
|
data,
|
||||||
keyring=self.server.keyring,
|
keyring=self.server.keyring,
|
||||||
continue_on_error=True,
|
continue_on_error=False,
|
||||||
ignore_trailing=True
|
ignore_trailing=True
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error parsing query:", e)
|
logger.error("Error parsing query: ", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a response
|
# Create a response
|
||||||
@@ -341,12 +333,13 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Identify TSIG key used
|
# Identify TSIG key used
|
||||||
key_name = query.keyname.canonicalize().to_text() if query.keyname else None
|
if not query.had_tsig:
|
||||||
if not key_name:
|
|
||||||
logger.warning(f"Request denied from {peer}: No TSIG key used")
|
logger.warning(f"Request denied from {peer}: No TSIG key used")
|
||||||
self.denyRequest(query)
|
self.denyRequest(query)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
key_name = query.keyname.canonicalize().to_text()
|
||||||
|
|
||||||
# Check if key matches a view
|
# Check if key matches a view
|
||||||
nb_view = self.server.tsig_view_map.get(key_name)
|
nb_view = self.server.tsig_view_map.get(key_name)
|
||||||
if not nb_view:
|
if not nb_view:
|
||||||
@@ -356,7 +349,7 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
|
|
||||||
# Check if catalog zone
|
# Check if catalog zone
|
||||||
if dname == "catz":
|
if dname == "catz":
|
||||||
zone = CatalogZoneAssembler.createCatalogZone(nb_view.name, soa_only=True)
|
zone = CatalogZone.create(nb_view.name, soa_only=True)
|
||||||
# If generic zone, retreive from NB
|
# If generic zone, retreive from NB
|
||||||
else:
|
else:
|
||||||
zone = self.getZoneFromNB(dname, nb_view, soa_only=True)
|
zone = self.getZoneFromNB(dname, nb_view, soa_only=True)
|
||||||
@@ -400,6 +393,13 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class TCPRequestHandler(DNSBaseRequestHandler):
|
class TCPRequestHandler(DNSBaseRequestHandler):
|
||||||
|
def denyRequest(self, query, rcode: dns.rcode = dns.rcode.REFUSED):
|
||||||
|
response = dns.message.make_response(query)
|
||||||
|
response.set_rcode(rcode)
|
||||||
|
wire = response.to_wire(multi=False)
|
||||||
|
length = len(wire).to_bytes(2, byteorder="big")
|
||||||
|
self.request.sendall(length + wire)
|
||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
MAX_WIRE = 65535
|
MAX_WIRE = 65535
|
||||||
# MAX_WIRE = 2000 # For testing fragmentation
|
# MAX_WIRE = 2000 # For testing fragmentation
|
||||||
@@ -456,12 +456,13 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Identify TSIG key used
|
# Identify TSIG key used
|
||||||
key_name = query.keyname.canonicalize().to_text() if query.keyname else None
|
if not query.had_tsig:
|
||||||
if not key_name:
|
|
||||||
logger.warning(f"Request denied from {peer}: No TSIG key used")
|
logger.warning(f"Request denied from {peer}: No TSIG key used")
|
||||||
self.denyRequest(query)
|
self.denyRequest(query)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
key_name = query.keyname.canonicalize().to_text()
|
||||||
|
|
||||||
# Check if the key matches a view
|
# Check if the key matches a view
|
||||||
nb_view = self.server.tsig_view_map.get(key_name)
|
nb_view = self.server.tsig_view_map.get(key_name)
|
||||||
if not nb_view:
|
if not nb_view:
|
||||||
@@ -471,8 +472,7 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|||||||
|
|
||||||
# Check if catalog zone
|
# Check if catalog zone
|
||||||
if dname == "catz":
|
if dname == "catz":
|
||||||
#zone = self.createCatalogZone(str(nb_view))
|
zone = CatalogZone.create(nb_view.name)
|
||||||
zone = CatalogZoneAssembler.createCatalogZone(nb_view.name)
|
|
||||||
else:
|
else:
|
||||||
zone = self.getZoneFromNB(dname, nb_view)
|
zone = self.getZoneFromNB(dname, nb_view)
|
||||||
|
|
||||||
@@ -610,17 +610,8 @@ class Command(BaseCommand):
|
|||||||
"tsig_keys variable not set in plugin settings."
|
"tsig_keys variable not set in plugin settings."
|
||||||
)
|
)
|
||||||
|
|
||||||
def loadCatalogZoneSettings(self):
|
# Load TSIG keys and map them to views
|
||||||
serial_file = self.settings.get("catalog_serial_file", None)
|
|
||||||
if not serial_file:
|
|
||||||
raise RuntimeError(
|
|
||||||
"catalog_serial_file variable not set in plugin settings."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
CatalogZoneAssembler.initCatalogZoneSerial(serial_file)
|
|
||||||
|
|
||||||
def loadTSIGKeySettings(self):
|
def loadTSIGKeySettings(self):
|
||||||
# Load TSIG keys and map them to views
|
|
||||||
self.keyring = {}
|
self.keyring = {}
|
||||||
self.tsig_view_map = {}
|
self.tsig_view_map = {}
|
||||||
|
|
||||||
@@ -653,7 +644,7 @@ class Command(BaseCommand):
|
|||||||
name=key_name_obj, secret=secret, algorithm=algorithm_str
|
name=key_name_obj, secret=secret, algorithm=algorithm_str
|
||||||
)
|
)
|
||||||
self.tsig_view_map[key_name_str] = nb_view
|
self.tsig_view_map[key_name_str] = nb_view
|
||||||
logger.debug(f"Loaded TSIG key: {key_name_str} (view: {nb_view.name})")
|
logger.debug(f"Loaded TSIG key: {key_name_str} view: {nb_view.name}")
|
||||||
|
|
||||||
if not self.keyring:
|
if not self.keyring:
|
||||||
msg = "No TSIG keys found in database."
|
msg = "No TSIG keys found in database."
|
||||||
@@ -668,10 +659,15 @@ class Command(BaseCommand):
|
|||||||
# Load parameters
|
# Load parameters
|
||||||
port = options['port']
|
port = options['port']
|
||||||
address = options['address']
|
address = options['address']
|
||||||
|
CatalogZone.initSerial()
|
||||||
# Initialize settings
|
# Initialize settings
|
||||||
self.loadSettings()
|
self.loadSettings()
|
||||||
self.loadCatalogZoneSettings()
|
#self.loadCatalogZoneSettings()
|
||||||
self.loadTSIGKeySettings()
|
self.loadTSIGKeySettings()
|
||||||
|
# Following is temporary. Function and invocation can be deleted once v1.0 is reached.
|
||||||
|
serial_file = self.settings.get("catalog_serial_file", None)
|
||||||
|
if serial_file:
|
||||||
|
CatalogZone.importLegacySerialFile(serial_file)
|
||||||
|
|
||||||
udp_server = UDPDNSServer((address, port), UDPRequestHandler, self.keyring, self.tsig_view_map)
|
udp_server = UDPDNSServer((address, port), UDPRequestHandler, self.keyring, self.tsig_view_map)
|
||||||
tcp_server = TCPDNSServer((address, port), TCPRequestHandler, self.keyring, self.tsig_view_map)
|
tcp_server = TCPDNSServer((address, port), TCPRequestHandler, self.keyring, self.tsig_view_map)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-11-23 14:36
|
||||||
|
|
||||||
|
import netbox.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
import utilities.json
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0133_make_cf_minmax_decimal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IntegerKeyValueSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||||
|
('key', models.CharField(max_length=64)),
|
||||||
|
('value', models.IntegerField()),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(netbox.models.deletion.DeleteMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
Whitespace-only changes.
@@ -0,0 +1,23 @@
|
|||||||
|
from django.db import models
|
||||||
|
import netbox.models
|
||||||
|
import netbox_dns.models
|
||||||
|
|
||||||
|
#class CatalogZoneSerial(models.Model):
|
||||||
|
class IntegerKeyValueSetting(netbox.models.NetBoxModel):
|
||||||
|
#view = models.ForeignKey(netbox_dns.models.View, on_delete=models.CASCADE)
|
||||||
|
key = models.CharField(max_length=64)
|
||||||
|
value = models.IntegerField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
#return f'{self.view.name}: {self.serial}'
|
||||||
|
return f'{self.key}: {str(self.value)}'
|
||||||
|
|
||||||
|
|
||||||
|
#class TSIGKey(netbox.models.NetBoxModel):
|
||||||
|
# view = models.ForeignKey(netbox_dns.models.View, on_delete=models.CASCADE)
|
||||||
|
# name = models.CharField(max_length=256)
|
||||||
|
# algorithm = models.CharField(max_length=25)
|
||||||
|
# secret = models.CharField(max_length=50)
|
||||||
|
#
|
||||||
|
# def __str__(self):
|
||||||
|
# return f'key "{self.name}" {{ algorithm {self.algorithm}; secret "{self.secret}"; }};'
|
||||||
Reference in new issue
Block a user