{ lib, pkgs, config, nodes, self, ... }: let name = "dns"; cfg = config.services.skynet."${name}"; # reads that date to a string (will need to be fixed in 2038) current_date = self.lastModified; # 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) )); 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; # 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)); # 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 ${toString current_date} 600 ; Refresh (10 minutes) 300 ; Retry (5 minutes) 604800 ; Expire (1 week) 3600 ; Minimum (1 hour) ) ; @ 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)} '' ); # 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 ${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; # 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 "skynet/dns/${domain}" = { user = "named"; group = "named"; # The UNIX file mode bits mode = "0664"; # content of the file 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 {}; 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 = '' ${if_primary_and_owned} // for bumping the config // ${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 }; }; records = config.skynet.records /* 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"; in { imports = [ ../../config/dns.nix ]; options.services.skynet."${name}" = { server = { enable = lib.mkEnableOption { default = false; description = "Skynet DNS server"; type = lib.types.bool; }; primary = lib.mkOption { type = lib.types.bool; default = false; }; ip = lib.mkOption { type = lib.types.str; description = '' ip of this server ''; }; }; records = lib.mkOption { description = "Records, sorted based on therir type"; type = lib.types.listOf (lib.types.submodule (import ./options-records.nix { inherit lib; })); }; }; config = lib.mkIf cfg.server.enable { # logging services.prometheus.exporters.bind = { enable = true; openFirewall = true; }; # services.skynet.backup.normal.backups = ["/etc/skynet/dns"]; # 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" ]; 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 = { file = ../../secrets/dns_dnskeys.conf.age; owner = "named"; group = "named"; }; # basic but ensure teh dns ports are open networking.firewall = { allowedTCPPorts = [53]; allowedUDPPorts = [53]; }; services.bind = { 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; }; }; ''; # piles of no valid RRSIG resolving 'com/DS/IN' errors extraOptions = '' dnssec-validation yes; ''; # 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; }; 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}" ]; }; # creates a folder in /etc for the dns to use users.groups.named = {}; users.users.named = { createHome = true; home = "/etc/skynet/dns"; group = "named"; # X11 is to ensure the directory can be traversed homeMode = "711"; }; }; }