nixos-config/lib/fudo/local-network.nix

239 lines
7.1 KiB
Nix

{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.fudo.local-network;
join-lines = concatStringsSep "\n";
traceout = out: builtins.trace out out;
in {
options.fudo.local-network = with types; {
enable = mkEnableOption "Enable local network configuration (DHCP & DNS).";
domain = mkOption {
type = str;
description = "The domain to use for the local network.";
};
dns-servers = mkOption {
type = listOf str;
description = "A list of domain name servers to pass to local clients.";
};
dhcp-interfaces = mkOption {
type = listOf str;
description = "A list of interfaces on which to serve DHCP.";
};
dns-listen-ips = mkOption {
type = listOf str;
description = "A list of IPs on which to server DNS queries.";
};
gateway = mkOption {
type = str;
description = "The gateway to use for the local network.";
};
network = mkOption {
type = str;
description = "Network to treat as local.";
example = "10.0.0.0/16";
};
dhcp-dynamic-network = mkOption {
type = str;
description = ''
The network from which to dynamically allocate IPs via DHCP.
Must be a subnet of <network>.
'';
example = "10.0.1.0/24";
};
enable-reverse-mappings = mkOption {
type = bool;
description = "Genereate PTR reverse lookup records.";
default = false;
};
recursive-resolver = mkOption {
type = str;
description = "DNS nameserver to use for recursive resolution.";
default = "1.1.1.1 port 53";
};
search-domains = mkOption {
type = listOf str;
description = "A list of domains which clients should consider local.";
example = [ "my-domain.com" "other-domain.com" ];
default = [ ];
};
network-definition = let
networkOpts = import ../types/network-definition.nix { inherit lib; };
in mkOption {
type = submodule networkOpts;
description = "Definition of network to be served by local server.";
default = { };
};
extra-records = mkOption {
type = listOf str;
description = "Extra records to add to the local zone.";
default = [ ];
};
};
config = mkIf cfg.enable {
fudo.system.hostfile-entries = let
other-hosts = filterAttrs
(hostname: hostOpts: hostname != config.instance.hostname)
cfg.network-definition.hosts;
in mapAttrs' (hostname: hostOpts:
nameValuePair hostOpts.ipv4-address ["${hostname}.${cfg.domain}" hostname])
other-hosts;
services.dhcpd4 = let network = cfg.network-definition;
in {
enable = true;
machines = mapAttrsToList (hostname: hostOpts: {
ethernetAddress = hostOpts.mac-address;
hostName = hostname;
ipAddress = hostOpts.ipv4-address;
}) (filterAttrs (host: hostOpts:
hostOpts.mac-address != null && hostOpts.ipv4-address != null)
network.hosts);
interfaces = cfg.dhcp-interfaces;
extraConfig = ''
subnet ${pkgs.lib.fudo.ip.getNetworkBase cfg.network} netmask ${
pkgs.lib.fudo.ip.maskFromV32Network cfg.network
} {
authoritative;
option subnet-mask ${pkgs.lib.fudo.ip.maskFromV32Network cfg.network};
option broadcast-address ${pkgs.lib.fudo.ip.networkMaxIp cfg.network};
option routers ${cfg.gateway};
option domain-name-servers ${concatStringsSep " " cfg.dns-servers};
option domain-name "${cfg.domain}";
option domain-search "${
concatStringsSep " " ([ cfg.domain ] ++ cfg.search-domains)
}";
range ${pkgs.lib.fudo.ip.networkMinIp cfg.dhcp-dynamic-network} ${
pkgs.lib.fudo.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.ipv4-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 config.instance.build-timestamp}
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; }) network.hosts;
hostsByBlock =
groupBy (host-data: ipToBlock host-data.ipv4-address) compactHosts;
hostPtrRecord = host-data:
"${
last (splitString "." host-data.ipv4-address)
} IN PTR ${host-data.host}.${cfg.domain}.";
blockZones = mapAttrsToList blockHostsToZone hostsByBlock;
hostARecord = host: data: "${host} IN A ${data.ipv4-address}";
hostSshFpRecords = host: data:
let
ssh-fingerprints = if (hasAttr host known-hosts) then
known-hosts.${host}.ssh-fingerprints
else
[ ];
in join-lines
(map (sshfp: "${host} IN SSHFP ${sshfp}") ssh-fingerprints);
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
network = cfg.network-definition;
known-hosts = config.fudo.hosts;
in {
enable = true;
cacheNetworks = [ cfg.network "localhost" "localnets" ];
forwarders = [ cfg.recursive-resolver ];
listenOn = cfg.dns-listen-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 config.instance.build-timestamp}
5m
2m
6w
5m)
$TTL 1h
@ IN NS ns1.${cfg.domain}.
$ORIGIN ${cfg.domain}.
$TTL 30m
${optionalString (network.gssapi-realm != null)
''_kerberos IN TXT "${network.gssapi-realm}"''}
${join-lines
(imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}")
cfg.dns-servers)}
${join-lines (mapAttrsToList hostARecord network.hosts)}
${join-lines (mapAttrsToList hostSshFpRecords network.hosts)}
${join-lines (mapAttrsToList cnameRecord network.aliases)}
${join-lines network.verbatim-dns-records}
${pkgs.lib.fudo.dns.srvRecordsToBindZone network.srv-records}
${join-lines cfg.extra-records}
'';
}] ++ blockZones;
};
};
}