Files
nixos-servers/lib/dns-zone.nix
Torjus Håkestad cee1b264cd dns: auto-generate zone entries from host configurations
Replace static zone file with dynamically generated records:
- Add homelab.dns module with enable/cnames options
- Extract IPs from systemd.network configs (filters VPN interfaces)
- Use git commit timestamp as zone serial number
- Move external hosts to separate external-hosts.nix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:43:44 +01:00

161 lines
5.2 KiB
Nix

{ lib }:
let
# Pad string on the right to reach a fixed width
rightPad = width: str:
let
len = builtins.stringLength str;
padding = if len >= width then "" else lib.strings.replicate (width - len) " ";
in
str + padding;
# Extract IP address from CIDR notation (e.g., "10.69.13.5/24" -> "10.69.13.5")
extractIP = address:
let
parts = lib.splitString "/" address;
in
builtins.head parts;
# Check if a network interface name looks like a VPN/tunnel interface
isVpnInterface = ifaceName:
lib.hasPrefix "wg" ifaceName ||
lib.hasPrefix "tun" ifaceName ||
lib.hasPrefix "tap" ifaceName ||
lib.hasPrefix "vti" ifaceName;
# Extract DNS information from a single host configuration
# Returns null if host should not be included in DNS
extractHostDNS = name: hostConfig:
let
cfg = hostConfig.config;
# Handle cases where homelab module might not be imported
dnsConfig = (cfg.homelab or { }).dns or { enable = true; cnames = [ ]; };
hostname = cfg.networking.hostName;
networks = cfg.systemd.network.networks or { };
# Filter out VPN interfaces and find networks with static addresses
# Check matchConfig.Name instead of network unit name (which can have prefixes like "40-")
physicalNetworks = lib.filterAttrs
(netName: netCfg:
let
ifaceName = netCfg.matchConfig.Name or "";
in
!(isVpnInterface ifaceName) && (netCfg.address or [ ]) != [ ])
networks;
# Get addresses from physical networks only
networkAddresses = lib.flatten (
lib.mapAttrsToList
(netName: netCfg: netCfg.address or [ ])
physicalNetworks
);
# Get the first address, if any
firstAddress = if networkAddresses != [ ] then builtins.head networkAddresses else null;
# Check if host uses DHCP (no static address)
usesDHCP = firstAddress == null ||
lib.any
(netName: (networks.${netName}.networkConfig.DHCP or "no") != "no")
(lib.attrNames networks);
in
if !(dnsConfig.enable or true) || firstAddress == null then
null
else
{
inherit hostname;
ip = extractIP firstAddress;
cnames = dnsConfig.cnames or [ ];
};
# Generate A record line
generateARecord = hostname: ip:
"${rightPad 20 hostname}IN A ${ip}";
# Generate CNAME record line
generateCNAME = alias: target:
"${rightPad 20 alias}IN CNAME ${target}";
# Generate zone file from flake configurations and external hosts
generateZone =
{ self
, externalHosts
, serial
, domain ? "home.2rjus.net"
, ttl ? 1800
, refresh ? 3600
, retry ? 900
, expire ? 1209600
, minTtl ? 120
, nameservers ? [ "ns1" "ns2" "ns3" ]
, adminEmail ? "admin.test.2rjus.net"
}:
let
# Extract DNS info from all flake hosts
nixosConfigs = self.nixosConfigurations or { };
hostDNSList = lib.filter (x: x != null) (
lib.mapAttrsToList extractHostDNS nixosConfigs
);
# Sort hosts by IP for consistent output
sortedHosts = lib.sort (a: b: a.ip < b.ip) hostDNSList;
# Generate A records for flake hosts
flakeARecords = lib.concatMapStringsSep "\n" (host:
generateARecord host.hostname host.ip
) sortedHosts;
# Generate CNAMEs for flake hosts
flakeCNAMEs = lib.concatMapStringsSep "\n" (host:
lib.concatMapStringsSep "\n" (cname:
generateCNAME cname host.hostname
) host.cnames
) (lib.filter (h: h.cnames != [ ]) sortedHosts);
# Generate A records for external hosts
externalARecords = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: ip:
generateARecord name ip
) (externalHosts.aRecords or { })
);
# Generate CNAMEs for external hosts
externalCNAMEs = lib.concatStringsSep "\n" (
lib.mapAttrsToList (alias: target:
generateCNAME alias target
) (externalHosts.cnames or { })
);
# NS records
nsRecords = lib.concatMapStringsSep "\n" (ns:
" IN NS ${ns}.${domain}."
) nameservers;
# SOA record
soa = ''
$ORIGIN ${domain}.
$TTL ${toString ttl}
@ IN SOA ns1.${domain}. ${adminEmail}. (
${toString serial} ; serial number
${toString refresh} ; refresh
${toString retry} ; retry
${toString expire} ; expire
${toString minTtl} ; ttl
)'';
in
lib.concatStringsSep "\n\n" (lib.filter (s: s != "") [
soa
nsRecords
"; Flake-managed hosts (auto-generated)"
flakeARecords
(if flakeCNAMEs != "" then "; Flake-managed CNAMEs\n${flakeCNAMEs}" else "")
"; External hosts (not managed by this flake)"
externalARecords
(if externalCNAMEs != "" then "; External CNAMEs\n${externalCNAMEs}" else "")
""
]);
in
{
inherit extractIP extractHostDNS generateARecord generateCNAME generateZone;
}