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>
161 lines
5.2 KiB
Nix
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;
|
|
}
|