{ lib, config, pkgs, ... }: with lib; let cfg = config.fudo.local-network; dns = import ../lib/dns.nix { inherit lib; }; ip = import ../lib/ip.nix { inherit lib; }; join-lines = concatStringsSep "\n"; hostOpts = { hostname, ... }: { options = { ip-address = mkOption { type = types.str; description = '' The V4 IP of a given host, if any. ''; }; mac-address = mkOption { type = with types; nullOr types.str; description = '' The MAC address of a given host, if desired for IP reservation. ''; default = null; }; ssh-fingerprints = mkOption { type = with types; listOf str; description = "A list of DNS SSHFP records for this host."; default = [ ]; }; }; }; traceout = out: builtins.trace out out; in { options.fudo.local-network = { enable = mkEnableOption "Enable local network configuration (DHCP & DNS)."; hosts = mkOption { type = with types; attrsOf (submodule hostOpts); default = { }; description = "A map of hostname => { host_attributes }."; }; domain = mkOption { type = types.str; description = "The domain to use for the local network."; }; dns-servers = mkOption { type = with types; listOf str; description = "A list of domain name server to use for the local network."; }; dhcp-interfaces = mkOption { type = with types; listOf str; description = "A list of interfaces on which to serve DHCP."; }; dns-serve-ips = mkOption { type = with types; listOf str; description = "A list of IPs on which to server DNS queries."; }; gateway = mkOption { type = types.str; description = "The gateway to use for the local network."; }; aliases = mkOption { type = with types; attrsOf str; default = { }; description = "A mapping of host-alias => hostname to use on the local network."; }; network = mkOption { type = types.str; description = "Network to treat as local."; }; enable-reverse-mappings = mkOption { type = types.bool; description = "Genereate PTR reverse lookup records."; default = false; }; dhcp-dynamic-network = mkOption { type = types.str; description = '' The network from which to dynamically allocate IPs via DHCP. Must be a subnet of . ''; }; recursive-resolver = mkOption { type = types.str; description = "DNS nameserver to use for recursive resolution."; }; server-ip = mkOption { type = types.str; description = "IP of the DNS server."; }; extra-dns-records = mkOption { type = with types; listOf str; description = "Records to be inserted verbatim into the DNS zone."; example = [ "some-host IN CNAME other-host" ]; default = [ ]; }; srv-records = mkOption { type = dns.srvRecords; description = "Map of traffic type to srv records."; default = { }; example = { tcp = { kerberos = { port = 88; host = "auth-host.my-domain.com"; }; }; }; }; search-domains = mkOption { type = with types; listOf str; description = "A list of domains to search for DNS names."; example = [ "my-domain.com" "other-domain.com" ]; default = [ ]; }; # TODO: srv records }; config = mkIf cfg.enable { services.dhcpd4 = { enable = true; machines = mapAttrsToList (hostname: hostOpts: { ethernetAddress = hostOpts.mac-address; hostName = hostname; ipAddress = hostOpts.ip-address; }) (filterAttrs (host: hostOpts: hostOpts.mac-address != null) cfg.hosts); interfaces = cfg.dhcp-interfaces; extraConfig = '' subnet ${ip.getNetworkBase cfg.network} netmask ${ ip.maskFromV32Network cfg.network } { authoritative; option subnet-mask ${ip.maskFromV32Network cfg.network}; option broadcast-address ${ip.networkMaxIp cfg.network}; option routers ${cfg.gateway}; option domain-name-servers ${concatStringsSep " " cfg.dns-servers}; option domain-name "${cfg.domain}"; option domain-search ${ join-lines (map (dom: ''"${dom}"'') ([ cfg.domain ] ++ cfg.search-domains)) }; range ${ip.networkMinIp cfg.dhcp-dynamic-network} ${ ip.networkMaxButOneIp cfg.dhcp-dynamic-network }; } ''; }; services.bind = let blockHostsToZone = block: hosts-data: { master = true; name = "${block}.in-addr.arpa"; file = let # We should add these...but need a domain to assign them to. # ip-last-el = ip: toInt (last (splitString "." ip)); # used-els = map (host-data: ip-last-el host-data.ip-address) hosts-data; # unused-els = subtractLists used-els (map toString (range 1 255)); in pkgs.writeText "db.${block}-zone" '' $ORIGIN ${block}.in-addr.arpa. $TTL 1h @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. ( ${toString builtins.currentTime} 1800 900 604800 1800) @ IN NS ns1.${cfg.domain}. ${join-lines (map hostPtrRecord hosts-data)} ''; }; ipToBlock = ip: concatStringsSep "." (reverseList (take 3 (splitString "." ip))); compactHosts = mapAttrsToList (host: data: data // { host = host; }) cfg.hosts; hostsByBlock = groupBy (host-data: ipToBlock host-data.ip-address) compactHosts; hostPtrRecord = host-data: "${ last (splitString "." host-data.ip-address) } IN PTR ${host-data.host}.${cfg.domain}."; blockZones = mapAttrsToList blockHostsToZone hostsByBlock; hostARecord = host: data: "${host} IN A ${data.ip-address}"; hostSshFpRecords = host: data: join-lines (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints); cnameRecord = alias: host: "${alias} IN CNAME ${host}"; in { enable = true; cacheNetworks = [ cfg.network "localhost" "localnets" ]; forwarders = [ cfg.recursive-resolver ]; listenOn = cfg.dns-serve-ips; extraOptions = concatStringsSep "\n" [ "dnssec-enable yes;" "dnssec-validation yes;" "auth-nxdomain no;" "recursion yes;" "allow-recursion { any; };" ]; zones = [{ master = true; name = cfg.domain; file = pkgs.writeText "${cfg.domain}-zone" '' @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. ( ${toString builtins.currentTime} 5m 2m 6w 5m) $TTL 1h @ IN NS ns1.${cfg.domain}. $ORIGIN ${cfg.domain}. $TTL 30m ns1 IN A ${cfg.server-ip} ${join-lines (mapAttrsToList hostARecord cfg.hosts)} ${join-lines (mapAttrsToList hostSshFpRecords cfg.hosts)} ${join-lines (mapAttrsToList cnameRecord cfg.aliases)} ${join-lines cfg.extra-dns-records} ${dns.srvRecordsToBindZone cfg.srv-records} ''; }] ++ blockZones; }; }; }