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>
This commit is contained in:
160
lib/dns-zone.nix
Normal file
160
lib/dns-zone.nix
Normal file
@@ -0,0 +1,160 @@
|
||||
{ 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;
|
||||
}
|
||||
Reference in New Issue
Block a user