nixos/applications/dns/dns.nix

430 lines
13 KiB
Nix
Raw Permalink Normal View History

{
lib,
pkgs,
config,
nodes,
2024-07-20 11:21:30 +00:00
self,
...
}: let
name = "dns";
cfg = config.services.skynet."${name}";
# reads that date to a string (will need to be fixed in 2038)
2024-07-20 11:21:30 +00:00
current_date = self.lastModified;
2024-07-17 00:38:31 +00:00
# this gets a list of all domains we have records for
domains = lib.lists.naturalSort (lib.lists.unique (
lib.lists.forEach records (x: x.domain)
));
# get the ip's of our servers
servers = lib.lists.naturalSort (lib.lists.unique (
lib.lists.forEach (sort_records_a_server records) (x: x.value)
));
2024-07-17 00:38:31 +00:00
domains_owned = [
# for historic reasons we own this
"csn.ul.ie"
# the main one we use now
"skynet.ie"
# a backup
"ulcompsoc.ie"
];
# gets a list of records that match this type
filter_records_type = records: r_type: builtins.filter (x: x.r_type == r_type) records;
# Get all the A records that are for servers (base record for them)
filter_records_a_server = records: builtins.filter (x: builtins.hasAttr "server" x && x.server) (filter_records_type records "A");
# Every other A record
filter_records_a = records: builtins.filter (x: builtins.hasAttr "server" x && !x.server) (filter_records_type records "A");
# These functions are to get the final 3 digits of an IP address so we can use them for reverse pointer
process_ptr = records: lib.lists.forEach records (x: process_ptr_sub x);
process_ptr_sub = record: {
record = builtins.substring 9 3 record.record;
r_type = "PTR";
value = record.value;
};
ip_ptr_to_int = ip: lib.strings.toInt (builtins.substring 9 3 ip);
# filter and sort records so we cna group them in the right place later
sort_records_a_server = records: builtins.sort (a: b: a.record < b.record) (filter_records_a_server records);
sort_records_a = records: builtins.sort (a: b: (ip_ptr_to_int a.value) < (ip_ptr_to_int b.value)) (filter_records_a records);
sort_records_cname = records: builtins.sort (a: b: a.value < b.value) (filter_records_type records "CNAME");
sort_records_ptr = records: builtins.sort (a: b: (lib.strings.toInt a.record) < (lib.strings.toInt b.record)) (process_ptr (filter_records_type records "PTR"));
sort_records_srv = records: builtins.sort (a: b: a.record < b.record) (filter_records_type records "SRV");
# a tad overkill but type guarding is useful
max = x: y:
assert builtins.isInt x;
assert builtins.isInt y;
if x < y
then y
else x;
2024-10-05 11:41:25 +00:00
# get teh max length of a list of strings
max_len = records: lib.lists.foldr (a: b: (max a b)) 0 (lib.lists.forEach records (record: lib.strings.stringLength record.record));
2024-10-05 11:41:25 +00:00
# Now that we can get teh max lenth of a list of strings
# we can pad it out to the max len +1
# this is so that teh generated file is easier for a human to read
format_records = records: let
offset = (max_len records) + 1;
in
lib.strings.concatMapStrings (x: "${padString x.record offset} IN ${padString x.r_type 5} ${x.value}\n") records;
# small function to add spaces until it reaches teh required length
padString = text: length: fixedWidthString_post length " " text;
# like lib.strings.fixedWidthString but postfix
# recursive function to extend a string up to a limit
fixedWidthString_post = width: filler: str: let
strw = lib.stringLength str;
reqWidth = width - (lib.stringLength filler);
in
# this is here because we were manually setting teh length, now max_len does that for us
assert lib.assertMsg (strw <= width) "fixedWidthString_post: requested string length (${toString width}) must not be shorter than actual length (${toString strw})";
if strw == width
then str
else (fixedWidthString_post reqWidth filler str) + filler;
# base config for domains we own (skynet.ie, csn.ul.ie, ulcompsoc.ie)
# ";" are comments in this file
get_config_file = (
domain: records: ''
$TTL 60 ; 1 minute
; hostmaster@skynet.ie is an email address that recieves stuff related to dns
@ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
2024-07-19 23:51:24 +00:00
${toString current_date}
600 ; Refresh (10 minutes)
300 ; Retry (5 minutes)
604800 ; Expire (1 week)
3600 ; Minimum (1 hour)
)
2024-07-16 23:53:28 +00:00
; @ stands for teh root domain so teh A record below is where ${domain} points to
@ NS ns1.skynet.ie.
@ NS ns2.skynet.ie.
; ------------------------------------------
; Server Names (A Records)
; ------------------------------------------
${format_records (sort_records_a_server records)}
; ------------------------------------------
; A (non server names
; ------------------------------------------
${format_records (sort_records_a records)}
; ------------------------------------------
; CNAMES
; ------------------------------------------
${format_records (sort_records_cname records)}
; ------------------------------------------
; TXT
; ------------------------------------------
${format_records (filter_records_type records "TXT")}
; ------------------------------------------
; MX
; ------------------------------------------
${format_records (filter_records_type records "MX")}
; ------------------------------------------
; SRV
; ------------------------------------------
${format_records (sort_records_srv records)}
''
2023-05-21 15:18:39 +00:00
);
# https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/4/html/reference_guide/s2-bind-configuration-zone-reverse
# config for our reverse dns pointers (not properly working)
get_config_file_rev = (
domain: ''
$ORIGIN 64-64.99.1.193.in-addr.arpa.
$TTL 60 ; 1 minute
; hostmaster@skynet.ie is an email address that recieves stuff related to dns
@ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
2024-07-19 23:51:24 +00:00
${toString current_date}
600 ; Refresh (10 minutes)
300 ; Retry (5 minutes)
604800 ; Expire (1 week)
3600 ; Minimum (1 hour)
)
@ NS ns1.skynet.ie.
@ NS ns2.skynet.ie.
; ------------------------------------------
; PTR
; ------------------------------------------
${format_records (sort_records_ptr records)}
''
);
# arrays of teh two nameservers
nameserver_1 = ["193.1.99.109"];
nameserver_2 = ["193.1.99.120"];
primaries = (
if cfg.server.primary
then
# primary servers have no primaries (ones they listen to)
[]
else if builtins.elem cfg.server.ip nameserver_1
then nameserver_2
else nameserver_1
);
secondaries = (
if cfg.server.primary
then
if builtins.elem cfg.server.ip nameserver_1
then nameserver_2
else nameserver_1
else []
);
# small function to tidy up the spam of the cache networks, would use teh subnet except all external traffic has the ip of teh router
# now limited explicitly to servers that we are administering
# See i24-09-30_050 for more information
create_cache_networks = map (x: "${toString x}/32") servers;
2023-05-21 20:25:21 +00:00
# standard function to create the etc file, pass in the text and domain and it makes it
create_entry_etc_sub = domain: text: {
# Creates /etc/skynet/dns/domain
2023-05-21 20:25:21 +00:00
"skynet/dns/${domain}" = {
user = "named";
group = "named";
# The UNIX file mode bits
mode = "0664";
2023-05-21 20:25:21 +00:00
# content of the file
2023-05-21 20:25:21 +00:00
text = text;
};
};
# standard function to create the etc file, pass in the text and domain and it makes it
create_entry_etc = domain: type: let
domain_records = lib.lists.filter (x: x.domain == domain) records;
in
# this is the main type of record that most folks are used to
if type == "owned"
then create_entry_etc_sub domain (get_config_file domain domain_records)
# reverse lookups allow for using an IP to find domains pointing to it
else if type == "reverse"
then create_entry_etc_sub domain (get_config_file_rev domain)
else {};
2024-07-17 00:38:31 +00:00
create_entry_zone = domain: let
if_primary_and_owned =
if cfg.server.primary && (lib.lists.any (item: item == domain) domains_owned)
then ''
allow-update { key rfc2136key.skynet.ie.; };
dnssec-policy default;
inline-signing yes;
''
else "";
in {
"${domain}" = {
extraConfig = ''
2024-07-17 00:38:31 +00:00
${if_primary_and_owned}
// for bumping the config
2024-07-19 23:51:24 +00:00
// ${toString current_date}
'';
# really wish teh nixos config didnt use master/slave
master = cfg.server.primary;
masters = primaries;
slaves = secondaries;
# need to write this to a file
# using the date in it so it will trigger a restart
file = "/etc/skynet/dns/${domain}";
# no leading whitespace for first line
};
};
2023-05-21 20:25:21 +00:00
records =
config.skynet.records
2024-07-17 03:08:04 +00:00
/*
Need to "manually" grab it from each server.
Nix is laxy evalusted so if it does not need to open a file it wont.
This is to iterate through each server (node) and evaluate the dns records for that server.
*/
++ builtins.concatLists (
lib.attrsets.mapAttrsToList (
key: value: value.config.services.skynet.dns.records
)
nodes
);
nameserver =
if cfg.server.primary
then "ns1"
else "ns2";
2023-01-25 11:48:44 +00:00
in {
2023-05-24 15:12:48 +00:00
imports = [
2024-07-16 21:33:27 +00:00
../../config/dns.nix
2023-05-24 15:12:48 +00:00
];
options.services.skynet."${name}" = {
server = {
enable = lib.mkEnableOption {
default = false;
description = "Skynet DNS server";
type = lib.types.bool;
2023-01-25 11:48:44 +00:00
};
primary = lib.mkOption {
type = lib.types.bool;
default = false;
};
ip = lib.mkOption {
type = lib.types.str;
description = ''
ip of this server
'';
2023-01-25 11:48:44 +00:00
};
};
records = lib.mkOption {
description = "Records, sorted based on therir type";
2024-07-16 21:33:27 +00:00
type = lib.types.listOf (lib.types.submodule (import ./options-records.nix {
2024-07-16 21:31:28 +00:00
inherit lib;
}));
};
2023-01-25 11:48:44 +00:00
};
config = lib.mkIf cfg.server.enable {
# logging
services.prometheus.exporters.bind = {
enable = true;
openFirewall = true;
};
# services.skynet.backup.normal.backups = ["/etc/skynet/dns"];
2023-05-24 15:12:48 +00:00
# open the firewall for this
skynet_firewall.forward = [
"ip daddr ${cfg.server.ip} tcp dport 53 counter packets 0 bytes 0 accept"
"ip daddr ${cfg.server.ip} udp dport 53 counter packets 0 bytes 0 accept"
2023-05-24 15:12:48 +00:00
];
2024-07-17 03:08:04 +00:00
services.skynet.dns.records = [
{
record = nameserver;
r_type = "A";
value = config.services.skynet.host.ip;
}
];
services.bind.zones = lib.attrsets.mergeAttrsList (
# uses teh domains lsited in teh records
(lib.lists.forEach domains (domain: (create_entry_zone domain)))
# we have to do a reverse dns
++ [
(create_entry_zone "64-64.99.1.193.in-addr.arpa")
]
);
environment.etc = lib.attrsets.mergeAttrsList (
# uses teh domains lsited in teh records
(lib.lists.forEach domains (domain: (create_entry_etc domain "owned")))
# we have to do a reverse dns
++ [
(create_entry_etc "64-64.99.1.193.in-addr.arpa" "reverse")
]
);
# secrets required
age.secrets.dns_dnskeys = {
2024-07-16 21:33:27 +00:00
file = ../../secrets/dns_dnskeys.conf.age;
owner = "named";
group = "named";
};
# basic but ensure teh dns ports are open
2023-04-20 23:53:25 +00:00
networking.firewall = {
allowedTCPPorts = [53];
allowedUDPPorts = [53];
};
2023-01-25 11:48:44 +00:00
services.bind = {
2023-04-23 03:22:01 +00:00
enable = true;
ipv4Only = true;
# need to take a look at https://nixos.org/manual/nixos/unstable/#module-security-acme-config-dns
extraConfig = ''
include "/run/agenix/dns_dnskeys";
statistics-channels {
inet 127.0.0.1 port 8053 allow { 127.0.0.1; };
};
2023-04-23 03:22:01 +00:00
'';
# piles of no valid RRSIG resolving 'com/DS/IN' errors
extraOptions = ''
dnssec-validation yes;
'';
2023-04-23 03:22:01 +00:00
# set the upstream dns servers
# overrides the default dns servers
forwarders = [
# Cloudflare
"1.1.1.1"
# Google
"8.8.8.8"
# Quad9
"9.9.9.9"
];
cacheNetworks =
[
# this server itself
"127.0.0.0/24"
# skynet server in the dmz
"193.1.96.165/32"
# all of skynet can use this as a resolver
/*
Origianl idea, however all external traffic had the ip of the router
"193.1.99.64/26"
So to fix this we need to allow smaller ranges? - Didnt work
Fallback is explisitly listing each ip we have
Now have a function for it
*/
]
++ create_cache_networks;
2023-04-23 03:22:01 +00:00
};
2023-01-25 11:48:44 +00:00
systemd.services.bind = {
# deletes teh journal files evey start so it no longer stalls out
preStart = ''
rm -vf /etc/skynet/dns/*.jnl
rm -vf /etc/skynet/dns/*.jbk
'';
restartTriggers = [
"${config.environment.etc."skynet/dns/skynet.ie".source}"
];
};
2023-09-15 19:30:37 +00:00
2023-04-23 03:22:01 +00:00
# creates a folder in /etc for the dns to use
2023-11-17 09:19:05 +00:00
users.groups.named = {};
2023-04-23 03:22:01 +00:00
users.users.named = {
createHome = true;
2023-05-21 20:25:21 +00:00
home = "/etc/skynet/dns";
2023-11-17 09:19:05 +00:00
group = "named";
# X11 is to ensure the directory can be traversed
homeMode = "711";
2023-04-23 03:22:01 +00:00
};
};
}