Add RFC 2317 classless reverse zone names for sub-/24 subnets
This commit is contained in:
6 files changed
+214
-11
No files matched your search
+38
-4
@@ -169,16 +169,18 @@ class DhcpCustomFieldInline(admin.TabularInline):
|
||||
extra = 0
|
||||
|
||||
|
||||
class SubnetAdmin(admin.ModelAdmin):
|
||||
class Ip4SubnetAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"name",
|
||||
"network",
|
||||
"netmask",
|
||||
"domain_name",
|
||||
"num_addresses",
|
||||
"broadcast_address",
|
||||
"first_address",
|
||||
"last_address",
|
||||
]
|
||||
readonly_fields = ["domain_name"]
|
||||
inlines = [DhcpOptionInline, DhcpCustomFieldInline, Ip4AddressInline]
|
||||
|
||||
def num_addresses(self, ip4subnet):
|
||||
@@ -204,7 +206,39 @@ class SubnetAdmin(admin.ModelAdmin):
|
||||
|
||||
def get_queryset(self, request):
|
||||
# TODO: optimize
|
||||
return super(SubnetAdmin, self).get_queryset(request)
|
||||
return super(Ip4SubnetAdmin, self).get_queryset(request)
|
||||
|
||||
|
||||
class Ip6SubnetAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"name",
|
||||
"network",
|
||||
"netmask",
|
||||
"domain_name",
|
||||
"num_addresses",
|
||||
"first_address",
|
||||
"last_address",
|
||||
]
|
||||
readonly_fields = ["domain_name"]
|
||||
|
||||
def _network(self, ip6subnet):
|
||||
return ipaddress.IPv6Network(
|
||||
"%s::/%d" % (ip6subnet.network, ip6subnet.netmask), strict=False
|
||||
)
|
||||
|
||||
def num_addresses(self, ip6subnet):
|
||||
return self._network(ip6subnet).num_addresses
|
||||
|
||||
def first_address(self, ip6subnet):
|
||||
return self._network(ip6subnet).network_address
|
||||
|
||||
def last_address(self, ip6subnet):
|
||||
# subnet[-1] is O(1); never materialize hosts() for a /64.
|
||||
return self._network(ip6subnet)[-1]
|
||||
|
||||
num_addresses.short_description = "# addresses"
|
||||
first_address.short_description = "first address"
|
||||
last_address.short_description = "last address"
|
||||
|
||||
|
||||
class DomainSrvRecordInline(admin.TabularInline):
|
||||
@@ -283,8 +317,8 @@ class OperatingSystemAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(Domain, DomainAdmin)
|
||||
admin.site.register(Host, HostAdmin)
|
||||
admin.site.register(Ip4Subnet, SubnetAdmin)
|
||||
admin.site.register(Ip6Subnet)
|
||||
admin.site.register(Ip4Subnet, Ip4SubnetAdmin)
|
||||
admin.site.register(Ip6Subnet, Ip6SubnetAdmin)
|
||||
admin.site.register(Ip6Address)
|
||||
admin.site.register(Nameserver)
|
||||
admin.site.register(MailExchange)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import ipaddress
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_classless_reverse_names(apps, schema_editor):
|
||||
"""Recompute domain_name for sub-/24 IPv4 subnets (RFC 2317 classless).
|
||||
|
||||
domain_name is frozen on first save, so subnets created before classless
|
||||
support kept the plain /24 name (190.36.158.in-addr.arpa) even for a /26.
|
||||
That apex no longer matches the classless zone declared in named.conf, so
|
||||
BIND refuses to load the file. Rewrite those names to the RFC 2317 form
|
||||
(192-26.190.36.158.in-addr.arpa). /24-or-shorter subnets are left alone.
|
||||
"""
|
||||
Ip4Subnet = apps.get_model("mdb", "Ip4Subnet")
|
||||
for subnet in Ip4Subnet.objects.all():
|
||||
try:
|
||||
net = ipaddress.IPv4Network(
|
||||
"%s/%s" % (subnet.network, subnet.netmask), strict=False
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
if net.prefixlen <= 24:
|
||||
continue
|
||||
o1, o2, o3, o4 = str(net.network_address).split(".")
|
||||
name = "%s-%d.%s.%s.%s.in-addr.arpa" % (o4, net.prefixlen, o3, o2, o1)
|
||||
if subnet.domain_name != name:
|
||||
subnet.domain_name = name
|
||||
subnet.save(update_fields=["domain_name"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("mdb", "0013_host_allow_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_classless_reverse_names, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -299,6 +299,18 @@ class Ip6Subnet(models.Model):
|
||||
def __str__(self):
|
||||
return self.network + " (" + self.name + ")"
|
||||
|
||||
@property
|
||||
def reverse_zone_name(self):
|
||||
"""The ip6.arpa reverse zone name for this subnet's prefix.
|
||||
|
||||
One label per nibble, derived from ``netmask`` (the prefix length).
|
||||
Delegation in ip6.arpa happens on nibble (4-bit) boundaries, so the
|
||||
prefix is truncated down to whole nibbles.
|
||||
"""
|
||||
network = ipaddress.IPv6Address("%s::" % self.network)
|
||||
nibbles = self.netmask // 4
|
||||
return ".".join(network.exploded.replace(":", "")[:nibbles])[::-1] + ".ip6.arpa"
|
||||
|
||||
def zone_file_contents(self, generate_unassigned=False):
|
||||
content = ""
|
||||
content += "; zone file for %s\n" % self.domain_name
|
||||
@@ -379,6 +391,25 @@ class Ip4Subnet(models.Model):
|
||||
def __str__(self):
|
||||
return self.network + " (" + self.name + ")"
|
||||
|
||||
@property
|
||||
def reverse_zone_name(self):
|
||||
"""The in-addr.arpa reverse zone name for this subnet.
|
||||
|
||||
Masks of /24 or shorter give the standard octet-boundary name
|
||||
(190.36.158.in-addr.arpa). Masks longer than /24 give the RFC 2317
|
||||
classless name for the sub-/24 block (192-26.190.36.158.in-addr.arpa).
|
||||
This is the apex BIND expects -- it must match the zone name in
|
||||
named.conf or rndc refuses to load the file.
|
||||
"""
|
||||
net = ipaddress.IPv4Network(
|
||||
"%s/%s" % (self.network, self.netmask), strict=False
|
||||
)
|
||||
o1, o2, o3, o4 = str(net.network_address).split(".")
|
||||
rev = "%s.%s.%s" % (o3, o2, o1)
|
||||
if net.prefixlen > 24:
|
||||
return "%s-%d.%s.in-addr.arpa" % (o4, net.prefixlen, rev)
|
||||
return "%s.in-addr.arpa" % rev
|
||||
|
||||
def zone_file_contents(self, generate_unassigned=False):
|
||||
content = ""
|
||||
content += "; zone file for %s\n" % self.domain_name
|
||||
|
||||
+2
-7
@@ -34,9 +34,7 @@ def delete_ips_for_subnet(sender, instance, **kwargs):
|
||||
def set_domain_name_for_subnet(sender, instance, **kwargs):
|
||||
# we assume that the reverse domain_name does not change
|
||||
if len(instance.domain_name) == 0:
|
||||
ipspl = instance.network.split(".")
|
||||
rev = "%s.%s.%s" % (ipspl[2], ipspl[1], ipspl[0])
|
||||
instance.domain_name = "%s.in-addr.arpa" % rev
|
||||
instance.domain_name = instance.reverse_zone_name
|
||||
|
||||
# update it's own serial
|
||||
# if instance.domain_serial is not None:
|
||||
@@ -57,10 +55,7 @@ def set_domain_name_for_ipv6_subnet(sender, instance, **kwargs):
|
||||
if len(instance.domain_name) > 0:
|
||||
return
|
||||
|
||||
network = ipaddress.IPv6Address("%s::" % instance.network)
|
||||
instance.domain_name = (
|
||||
".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa"
|
||||
)
|
||||
instance.domain_name = instance.reverse_zone_name
|
||||
|
||||
|
||||
@receiver(post_save, sender=Interface)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
from mdb.models import Ip4Subnet, Ip6Subnet
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"network,netmask,expected",
|
||||
[
|
||||
# /24 and shorter -> standard octet-boundary name
|
||||
("203.0.113.0", "255.255.255.0", "113.0.203.in-addr.arpa"),
|
||||
("10.0.3.0", "255.255.255.0", "3.0.10.in-addr.arpa"),
|
||||
("10.0.0.0", "255.255.0.0", "0.0.10.in-addr.arpa"),
|
||||
# longer than /24 -> RFC 2317 classless name
|
||||
("192.0.2.192", "255.255.255.192", "192-26.2.0.192.in-addr.arpa"),
|
||||
("10.0.0.0", "255.255.255.252", "0-30.0.0.10.in-addr.arpa"),
|
||||
("10.1.2.0", "255.255.255.128", "0-25.2.1.10.in-addr.arpa"),
|
||||
# host bits set -> normalized down to the network address
|
||||
("192.0.2.200", "255.255.255.192", "192-26.2.0.192.in-addr.arpa"),
|
||||
],
|
||||
)
|
||||
def test_ip4_reverse_zone_name(network, netmask, expected):
|
||||
subnet = Ip4Subnet(network=network, netmask=netmask)
|
||||
assert subnet.reverse_zone_name == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"network,netmask,expected",
|
||||
[
|
||||
("2001:db8", 64, "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"),
|
||||
("2001:db8", 48, "0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"),
|
||||
("2001:db8:abcd", 56, "0.0.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa"),
|
||||
("2001:db8", 32, "8.b.d.0.1.0.0.2.ip6.arpa"),
|
||||
],
|
||||
)
|
||||
def test_ip6_reverse_zone_name(network, netmask, expected):
|
||||
subnet = Ip6Subnet(network=network, netmask=netmask)
|
||||
assert subnet.reverse_zone_name == expected
|
||||
@@ -0,0 +1,67 @@
|
||||
import pytest
|
||||
|
||||
from mdb.models import DhcpConfig, Ip4Subnet, Ip6Subnet
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# pre_save signals: populate domain_name once, then leave it frozen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_ip4subnet(**overrides):
|
||||
dhcp_config = DhcpConfig.objects.create(serial=1, active_serial=1)
|
||||
fields = {
|
||||
"name": "test-subnet",
|
||||
"netmask": "255.255.255.192",
|
||||
"network": "192.0.2.192",
|
||||
"domain_soa": "ns1.example.org",
|
||||
"domain_admin": "admin@example.org",
|
||||
"domain_filename": "db.test-subnet",
|
||||
"dhcp_config": dhcp_config,
|
||||
}
|
||||
fields.update(overrides)
|
||||
return Ip4Subnet.objects.create(**fields)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ip4_signal_sets_classless_domain_name():
|
||||
subnet = make_ip4subnet()
|
||||
assert subnet.domain_name == "192-26.2.0.192.in-addr.arpa"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ip4_signal_sets_plain_domain_name_for_slash24():
|
||||
subnet = make_ip4subnet(network="203.0.113.0", netmask="255.255.255.0")
|
||||
assert subnet.domain_name == "113.0.203.in-addr.arpa"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ip4_signal_does_not_overwrite_existing_domain_name():
|
||||
subnet = make_ip4subnet(domain_name="manual.in-addr.arpa")
|
||||
assert subnet.domain_name == "manual.in-addr.arpa"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ip4_domain_name_is_frozen_when_network_changes():
|
||||
subnet = make_ip4subnet()
|
||||
assert subnet.domain_name == "192-26.2.0.192.in-addr.arpa"
|
||||
|
||||
subnet.network = "10.0.0.0"
|
||||
subnet.netmask = "255.255.255.0"
|
||||
subnet.save()
|
||||
subnet.refresh_from_db()
|
||||
|
||||
# The name is derived once and then left alone.
|
||||
assert subnet.domain_name == "192-26.2.0.192.in-addr.arpa"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ip6_signal_sets_domain_name():
|
||||
subnet = Ip6Subnet.objects.create(
|
||||
name="ipv6-doc",
|
||||
network="2001:db8",
|
||||
netmask=64,
|
||||
domain_soa="ns1.example.org",
|
||||
domain_admin="admin@example.org",
|
||||
domain_filename="db.reverse-v6",
|
||||
)
|
||||
assert subnet.domain_name == "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"
|
||||
Reference in new issue
Block a user