Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c59f4c57 | ||
|
|
787c3d0a20 | ||
|
|
57195a822b | ||
|
|
0590caeebd | ||
|
|
962be7a048 | ||
|
|
204a946a84 | ||
|
|
d72dc5a115 | ||
|
|
b659268117 |
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,3 +1,4 @@
|
|||||||
src/netbox_plugin_bind_provisioner.egg-info
|
src/netbox_plugin_bind_provisioner.egg-info
|
||||||
dist/
|
dist/
|
||||||
|
build/
|
||||||
__pycache__
|
__pycache__
|
||||||
@@ -109,15 +109,18 @@ This guide assumes:
|
|||||||
This allows the plugin to map requests to the right dns view using the tsig
|
This allows the plugin to map requests to the right dns view using the tsig
|
||||||
signature from each request.
|
signature from each request.
|
||||||
|
|
||||||
4. Run migrations (This step is not yet required. Just adding for completeness)
|
4. Run migrations
|
||||||
```
|
```
|
||||||
python3 netbox/manage.py migrate
|
python3 netbox/manage.py migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Start listener
|
5. Start listener
|
||||||
This step runs the DNS endpoint used by bind to configure itself. You may want
|
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
|
to write a service wrapper that runs this in the background.
|
||||||
activate the venv if you do decide to run this service in the background.
|
A guide for setting up a systemd service on Ubuntu is provided by Matt
|
||||||
|
Kollross [here](docs/install-systemd-service.md). 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
|
Note that `--port 5354` is optional. The listener will bind this port
|
||||||
by default.
|
by default.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
1. Create System Unit file
|
||||||
|
```
|
||||||
|
cat <<EOF > /etc/systemd/system/netbox-bind-endpoint.service
|
||||||
|
[Unit]
|
||||||
|
Description=Netbox BIND Catalog Endpoing
|
||||||
|
Documentation=https://github.com/Suraxius/netbox-plugin-bind-provisioner
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
User=netbox
|
||||||
|
Group=netbox
|
||||||
|
PIDFile=/var/tmp/netbox.pid
|
||||||
|
WorkingDirectory=/opt/netbox
|
||||||
|
|
||||||
|
ExecStart=/opt/netbox/venv/bin/python3 /opt/netbox/netbox/manage.py bind-transfer-endpoint --port 5354
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enable and start the service
|
||||||
|
```
|
||||||
|
systemctl enable netbox-bind-endpoint
|
||||||
|
systemctl start netbox-bind-endpoint
|
||||||
|
systemctl status netbox-bind-endpoint
|
||||||
|
```
|
||||||
@@ -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.6"
|
__version__ = "0.9.9"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class CatalogZone():
|
|||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, view_name, soa_only=False):
|
def create(cls, name, view_name):
|
||||||
# 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__
|
||||||
@@ -77,12 +77,9 @@ class CatalogZone():
|
|||||||
# 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._incrementSerial()
|
cls._incrementSerial()
|
||||||
# If there was no update, do nothing.
|
|
||||||
#else:
|
|
||||||
# logger.debug(f"No need to update serial for catalog zone")
|
|
||||||
|
|
||||||
# Zone origin
|
# Zone origin
|
||||||
origin = dns.name.from_text("catz", dns.name.root)
|
origin = dns.name.from_text(name, dns.name.root)
|
||||||
|
|
||||||
# Create a new empty zone
|
# Create a new empty zone
|
||||||
zone = dns.zone.Zone(origin)
|
zone = dns.zone.Zone(origin)
|
||||||
@@ -197,12 +194,12 @@ class CatalogZone():
|
|||||||
|
|
||||||
|
|
||||||
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):
|
||||||
# Find the zone
|
# Find the zone
|
||||||
try:
|
try:
|
||||||
nb_zone = Zone.objects.get(
|
nb_zone = Zone.objects.get(
|
||||||
name=zone_name, view=view_name, status=ZoneStatusChoices.STATUS_ACTIVE
|
name=zone_name, view__name=view_name, status=ZoneStatusChoices.STATUS_ACTIVE
|
||||||
)
|
)
|
||||||
except Zone.DoesNotExist:
|
except Zone.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
@@ -260,19 +257,9 @@ 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):
|
|
||||||
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 denyRequestBadTSIG(self, wire, tsig_error: dns.rcode):
|
def denyRequestBadTSIG(self, wire, tsig_error: dns.rcode):
|
||||||
# Use empty keyring to parse TSIG without validating
|
# Use empty keyring to parse TSIG without validating
|
||||||
query = dns.message.from_wire(wire, keyring={}, ignore_trailing=True, continue_on_error=True)
|
query = dns.message.from_wire(wire, keyring={}, ignore_trailing=True, continue_on_error=True)
|
||||||
@@ -292,6 +279,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]
|
||||||
@@ -301,11 +295,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
|
||||||
@@ -328,12 +322,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:
|
||||||
@@ -342,11 +337,11 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Check if catalog zone
|
# Check if catalog zone
|
||||||
if dname == "catz":
|
if dname == "catz" or dname == f"{nb_view.name}.catz":
|
||||||
zone = CatalogZone.create(nb_view.name, soa_only=True)
|
zone = CatalogZone.create(dname, nb_view.name)
|
||||||
# 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.name)
|
||||||
# When zone was not found, let client know
|
# When zone was not found, let client know
|
||||||
if not zone:
|
if not zone:
|
||||||
logger.warning(f"Zone {nb_view.name}/{dname} not found in NB")
|
logger.warning(f"Zone {nb_view.name}/{dname} not found in NB")
|
||||||
@@ -357,10 +352,9 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|||||||
soa_rdataset = zone.get_rdataset(zone.origin, dns.rdatatype.SOA)
|
soa_rdataset = zone.get_rdataset(zone.origin, dns.rdatatype.SOA)
|
||||||
|
|
||||||
# We assume that the SOA rdataset has at least one record (it usually does).
|
# We assume that the SOA rdataset has at least one record (it usually does).
|
||||||
soa_rdata = soa_rdataset[0] # Get the first SOA record
|
soa_rdata = soa_rdataset[0] # Get the first SOA record
|
||||||
|
|
||||||
# Now, create the rrset from the soa_rdata
|
# Now, create the rrset from the soa_rdata
|
||||||
#rrset = dns.rrset.from_rdata(dname, soa_rdataset.ttl, soa_rdata)
|
|
||||||
rrset = dns.rrset.from_rdata(zone.origin, soa_rdataset.ttl, soa_rdata)
|
rrset = dns.rrset.from_rdata(zone.origin, soa_rdataset.ttl, soa_rdata)
|
||||||
|
|
||||||
# Append the rrset to the response's answer section
|
# Append the rrset to the response's answer section
|
||||||
@@ -387,6 +381,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
|
||||||
@@ -443,12 +444,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:
|
||||||
@@ -457,10 +459,10 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Check if catalog zone
|
# Check if catalog zone
|
||||||
if dname == "catz":
|
if dname == "catz" or dname == f"{nb_view.name}.catz":
|
||||||
zone = CatalogZone.create(nb_view.name)
|
zone = CatalogZone.create(dname, nb_view.name)
|
||||||
else:
|
else:
|
||||||
zone = self.getZoneFromNB(dname, nb_view)
|
zone = self.getZoneFromNB(dname, nb_view.name)
|
||||||
|
|
||||||
# When zone was not found, let client know
|
# When zone was not found, let client know
|
||||||
if not zone:
|
if not zone:
|
||||||
@@ -630,7 +632,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."
|
||||||
|
|||||||
Reference in new issue
Block a user