From 125d2d3d57744580a61571263b005ad5c1b4edce Mon Sep 17 00:00:00 2001 From: niten Date: Tue, 1 Feb 2022 10:49:35 -0800 Subject: [PATCH] Add text library function (format-json-file) --- lib/fudo/adguard-dns-proxy.nix | 272 +++++++++++++++++++++++ lib/fudo/backplane-service/dns.nix | 131 +++++++++++ lib/fudo/backplane.nix | 134 ++++++++++++ lib/fudo/powerdns.nix | 339 +++++++++++++++++++++++++++++ lib/lib/text.nix | 17 ++ 5 files changed, 893 insertions(+) create mode 100644 lib/fudo/adguard-dns-proxy.nix create mode 100644 lib/fudo/backplane-service/dns.nix create mode 100644 lib/fudo/backplane.nix create mode 100644 lib/fudo/powerdns.nix create mode 100644 lib/lib/text.nix diff --git a/lib/fudo/adguard-dns-proxy.nix b/lib/fudo/adguard-dns-proxy.nix new file mode 100644 index 0000000..546fc79 --- /dev/null +++ b/lib/fudo/adguard-dns-proxy.nix @@ -0,0 +1,272 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.fudo.adguard-dns-proxy; + + hostname = config.instance.hostname; + + get-basename = filename: + head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename)); + + format-json-file = filename: pkgs.stdenv.mkDerivation { + name = "formatted-${get-basename filename}"; + phases = [ "installPhase" ]; + buildInputs = with pkgs; [ python ]; + installPhase = "python -mjson.tool ${filename} > $out"; + }; + + + admin-passwd-file = + pkgs.lib.passwd.stablerandom-passwd-file + "adguard-dns-proxy-admin" + config.instance.build-seed; + + filterOpts = { + options = with types; { + enable = mkOption { + type = bool; + description = "Enable this filter on DNS traffic."; + default = true; + }; + name = mkOption { + type = str; + description = "Name of this filter."; + }; + url = mkOption { + type = str; + description = "URL to the filter itself."; + default = true; + }; + }; + }; + + generate-config = { dns, + http, + filters, + verbose, + upstream-dns, + bootstrap-dns, + blocked-hosts, + enable-dnssec, + local-domain-name, + ... }: { + bind_host = http.listen-ip; + bind_port = http.listen-port; + users = [ + { + name = "admin"; + password = pkgs.lib.passwd.bcrypt-passwd + "adguard-dns-proxy-admin" + admin-passwd-file; + } + ]; + auth_attempts = 5; + block_auth_min = 30; + web_session_ttl = 720; + dns = { + bind_hosts = dns.listen-ips; + port = dns.listen-port; + upstream_dns = upstream-dns; + bootstrap_dns = bootstrap-dns; + blocking_mode = "default"; + blocked_hosts = blocked-hosts; + enable_dnssec = enable-dnssec; + local_domain_name = local-domain-name; + }; + tls.enabled = false; + filters = imap1 (i: filter: { + enabled = true; + name = filter.name; + url = filter.url; + }) filters; + dhcp.enabled = false; + clients = []; + verbose = verbose; + schema_version = 10; + }; + + generate-config-file = opts: + format-json-file (pkgs.writeText "adguard-dns-proxy-config.yaml" + (builtins.toJSON (generate-config opts))); + +in { + options.fudo.adguard-dns-proxy = with types; { + enable = mkEnableOption "Enable AdGuardHome DNS proxy."; + + dns = { + listen-ips = mkOption { + type = listOf str; + description = "IP on which to listen for incoming DNS requests."; + default = [ "0.0.0.0" ]; + }; + + listen-port = mkOption { + type = port; + description = "Port on which to listen for DNS queries."; + default = 53; + }; + }; + + http = { + listen-ip = mkOption { + type = str; + description = "IP on which to listen for incoming HTTP requests."; + }; + + listen-port = mkOption { + type = port; + description = "Port on which to listen for incoming HTTP queries."; + default = 8053; + }; + }; + + filters = mkOption { + type = listOf (submodule filterOpts); + description = "List of filters to apply to DNS traffic."; + default = [ + { + name = "AdGuard DNS filter"; + url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"; + } + { + name = "AdAway Default Blocklist"; + url = "https://adaway.org/hosts.txt"; + } + { + name = "MalwareDomainList.com Hosts List"; + url = "https://www.malwaredomainlist.com/hostslist/hosts.txt"; + } + { + name = "OISD.NL Blocklist"; + url = "https://abp.oisd.nl/"; + } + ]; + }; + + blocked-hosts = mkOption { + type = listOf str; + description = "List of hosts to explicitly block."; + default = [ + "version.bind" + "id.server" + "hostname.bind" + ]; + }; + + enable-dnssec = mkOption { + type = bool; + description = "Enable DNSSEC"; + default = true; + }; + + upstream-dns = mkOption { + type = listOf str; + description = '' + List of upstream DNS services to use. + + See https://github.com/AdguardTeam/dnsproxy for correct formatting. + ''; + default = [ + "https://1.1.1.1/dns-query" + "https://1.0.0.1/dns-query" + # These 11 addrs send the network, so the response can prefer closer answers + "https://9.9.9.11/dns-query" + "https://149.112.112.11/dns-query" + "https://2620:fe::11/dns-query" + "https://2620:fe::fe:11/dns-query" + ]; + }; + + bootstrap-dns = mkOption { + type = listOf str; + description = "List of DNS servers used to bootstrap DNS-over-HTTPS."; + default = [ + "1.1.1.1" + "1.0.0.1" + "9.9.9.9" + "149.112.112.112" + "2620:fe::10" + "2620:fe::fe:10" + ]; + }; + + allowed-networks = mkOption { + type = nullOr (listOf str); + description = "Optional list of networks with which this job may communicate."; + default = null; + }; + + user = mkOption { + type = str; + description = "User as which this job will run."; + default = "adguard-dns-proxy"; + }; + + local-domain-name = mkOption { + type = str; + description = "Local domain name."; + }; + + verbose = mkEnableOption "Keep verbose logs."; + }; + + config = mkIf cfg.enable (let + upgrade-perms = cfg.dns.listen-port <= 1024 || cfg.http.listen-port <= 1024; + in { + users = mkIf upgrade-perms { + users.${cfg.user} = { + isSystemUser = true; + group = cfg.user; + }; + + groups.${cfg.user} = { + members = [ cfg.user ]; + }; + }; + + fudo = { + secrets.host-secrets.${hostname} = { + adguard-dns-proxy-admin-password = { + source-file = admin-passwd-file; + target-file = "/run/adguard-dns-proxy/admin.passwd"; + user = "root"; + }; + }; + + system.services.adguard-dns-proxy = let + cfg-path = "/run/adguard-dns-proxy/config.yaml"; + in { + description = "DNS Proxy for ad filtering and DNS-over-HTTPS lookups."; + wantedBy = [ "default.target" ]; + after = [ "syslog.target" ]; + requires = [ "network.target" ]; + privateNetwork = false; + requiredCapabilities = optional upgrade-perms "CAP_NET_BIND_SERVICE"; + restartWhen = "always"; + addressFamilies = null; + networkWhitelist = cfg.allowed-networks; + user = mkIf upgrade-perms cfg.user; + runtimeDirectory = "adguard-dns-proxy"; + stateDirectory = "adguard-dns-proxy"; + preStart = '' + cp ${generate-config-file cfg} ${cfg-path}; + chown $USER ${cfg-path}; + chmod u+w ${cfg-path}; + ''; + + execStart = let + args = [ + "--no-check-update" + "--work-dir /var/lib/adguard-dns-proxy" + "--pidfile /run/adguard-dns-proxy/adguard-dns-proxy.pid" + "--host ${cfg.http.listen-ip}" + "--port ${toString cfg.http.listen-port}" + "--config ${cfg-path}" + ]; + arg-string = concatStringsSep " " args; + in "${pkgs.adguardhome}/bin/adguardhome ${arg-string}"; + }; + }; + }); +} diff --git a/lib/fudo/backplane-service/dns.nix b/lib/fudo/backplane-service/dns.nix new file mode 100644 index 0000000..2c70d67 --- /dev/null +++ b/lib/fudo/backplane-service/dns.nix @@ -0,0 +1,131 @@ +{ config, lib, pkgs, ... } @ toplevel: + +with lib; +let + cfg = config.fudo.backplane.dns; + + backplane-dns-home = "${config.instance.service-home}/backplane-dns"; + +in { + options.fudo.backplane.dns = with types; { + enable = mkEnableOption "Enable DNS backplane service."; + + required-services = mkOption { + type = listOf str; + description = "List of systemd units on which the DNS backplane job depends."; + default = [ ]; + }; + + backplane-server = mkOption { + type = str; + description = "Backplane XMPP server hostname."; + default = toplevel.config.fudo.backplane.backplane-hostname; + }; + + user = mkOption { + type = str; + description = "User as which to run the backplane dns service."; + default = "backplane-dns"; + }; + + group = mkOption { + type = str; + description = "Group as which to run the backplane dns service."; + default = "backplane-dns"; + }; + + database = { + host = mkOption { + type = str; + description = "Hostname or IP of the PostgreSQL server."; + }; + + database = mkOption { + type = str; + description = "Database to use for DNS backplane."; + default = "backplane_dns"; + }; + + username = mkOption { + type = str; + description = "Database user for DNS backplane."; + default = "backplane_dns"; + }; + + password-file = mkOption { + type = str; + description = "File containing password for DNS backplane database user."; + }; + + ssl-mode = mkOption { + type = enum ["no" "yes" "full" "try" "require"]; + description = "SSL connection mode."; + default = "require"; + }; + }; + + backplane-role = { + role = mkOption { + type = str; + description = "Backplane XMPP role name for DNS backplane job."; + default = "service-dns"; + }; + + password-file = mkOption { + type = str; + description = "Password file for backplane XMPP for DNS backplane job."; + }; + }; + }; + + config = mkIf cfg.enable { + users = { + users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = backplane-dns-home; + createHome = true; + }; + groups.${cfg.group} = { + members = [ cfg.user ]; + }; + }; + + fudo.system.services = { + backplane-dns = { + description = "Fudo DNS Backplane Server"; + restartIfChanged = true; + path = with pkgs; [ backplane-dns-server ]; + execStart = "${pkgs.backplane-dns-server}/bin/launch-backplane-dns.sh"; + #pidFile = "/run/backplane/dns.pid"; + partOf = [ "backplane-dns.target" ]; + wantedBy = [ "multi-user.target" ]; + requires = cfg.required-services; + user = cfg.user; + group = cfg.group; + memoryDenyWriteExecute = false; # Needed becuz Lisp + readWritePaths = [ backplane-dns-home ]; + privateNetwork = false; + addressFamilies = [ "AF_INET" "AF_INET6" ]; + environment = { + FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane-server; + FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane-role.role; + FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane-role.password-file; + + FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.database.host; + FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.database.database; + FUDO_DNS_BACKPLANE_DATABASE_USERNAME = + cfg.database.username; + FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE = + cfg.database.password-file; + FUDO_DNS_BACKPLANE_DATABASE_USE_SSL = cfg.database.ssl-mode; + + CL_SOURCE_REGISTRY = + pkgs.lib.lisp.lisp-source-registry pkgs.backplane-dns-server; + + LD_LIBRARY_PATH = "${pkgs.openssl.out}/lib"; + }; + }; + }; + }; +} diff --git a/lib/fudo/backplane.nix b/lib/fudo/backplane.nix new file mode 100644 index 0000000..795e40e --- /dev/null +++ b/lib/fudo/backplane.nix @@ -0,0 +1,134 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.fudo.backplane; + hostname = config.instance.hostname; + + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + generate-auth-file = name: files: let + make-entry = name: passwd-file: + ''("${name}" . "${readFile passwd-file}")''; + entries = mapAttrsToList make-entry files; + content = concatStringsSep "\n" entries; + in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})"; + + host-auth-file = generate-auth-file "host" + (mapAttrs (hostname: hostOpts: hostOpts.password-file) + cfg.client-hosts); + + service-auth-file = generate-auth-file "service" + (mapAttrs (service: serviceOpts: serviceOpts.password-file) + cfg.services); + + clientHostOpts = { name, ... }: { + options = with types; { + password-file = mkOption { + type = path; + description = + "Location (on the build host) of the file containing the host password."; + }; + }; + }; + + serviceOpts = { name, ... }: { + options = with types; { + password-file = mkOption { + type = path; + description = + "Location (on the build host) of the file containing the service password."; + }; + }; + }; + +in { + options.fudo.backplane = with types; { + enable = mkEnableOption "Enable backplane XMPP server."; + + client-hosts = mkOption { + type = attrsOf (submodule clientHostOpts); + description = "List of backplane client options."; + default = {}; + }; + + services = mkOption { + type = attrsOf (submodule serviceOpts); + description = "List of backplane service options."; + default = {}; + }; + + backplane-hostname = mkOption { + type = str; + description = "Hostname of the backplane XMPP server."; + }; + }; + + config = mkIf cfg.enable { + fudo = { + secrets.host-secrets.${hostname} = { + backplane-host-auth = { + source-file = generate-auth-file "host" + (mapAttrs (hostname: hostOpts: hostOpts.password-file) + cfg.client-hosts); + target-file = "/run/backplane/host-passwords.scm"; + user = config.fudo.jabber.user; + }; + backplane-service-auth = { + source-file = generate-auth-file "service" + (mapAttrs (service: serviceOpts: serviceOpts.password-file) + cfg.services); + target-file = "/run/backplane/service-passwords.scm"; + user = config.fudo.jabber.user; + }; + }; + + jabber = { + environment = { + FUDO_HOST_PASSWD_FILE = + host-secrets.backplane-host-auth.target-file; + FUDO_SERVICE_PASSWD_FILE = + host-secrets.backplane-service-auth.target-file; + }; + + sites.${cfg.backplane-hostname} = { + hostname = cfg.backplane-hostname; + + site-config = { + auth_method = "external"; + extauth_program = + "${pkgs.guile}/bin/guile -s ${pkgs.backplane-auth}/backplane-auth.scm"; + extauth_pool_size = 3; + auth_use_cache = true; + + modules = { + mod_adhoc = {}; + mod_caps = {}; + mod_carboncopy = {}; + mod_client_state = {}; + mod_configure = {}; + mod_disco = {}; + mod_fail2ban = {}; + mod_last = {}; + mod_offline.access_max_user_messages = 5000; + mod_ping = {}; + mod_pubsub = { + access_createnode = "pubsub_createnode"; + ignore_pep_from_offline = true; + last_item_cache = false; + plugins = [ + "flat" + "pep" + ]; + }; + mod_roster = {}; + mod_stream_mgmt = {}; + mod_time = {}; + mod_version = {}; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/fudo/powerdns.nix b/lib/fudo/powerdns.nix new file mode 100644 index 0000000..59314df --- /dev/null +++ b/lib/fudo/powerdns.nix @@ -0,0 +1,339 @@ +{ config, lib, pkgs, ... } @ toplevel: + +# NOTE: To get DNS records: +# pdnsutil --config-dir=... show-zone + +with lib; +let + cfg = config.fudo.powerdns; + + hostname = config.instance.hostname; + + runtime-dir = "/run/powerdns/conf"; + + target-gpgsql-config = "${runtime-dir}/pdns.local.gpgsql.conf"; + + gpgsql-template = pkgs.writeText "pdns.gpgsql.conf.template" '' + launch+=gpgsql + gpgsql-host=${cfg.database.host} + gpgsql-dbname=${cfg.database.database} + gpgsql-user=${cfg.database.user} + gpgsql-password=__PASSWORD__ + gpgsql-dnssec=${if cfg.enable-dnssec then "yes" else "no"} + gpgsql-extra-connection-parameters=sslmode=require + ''; + + pdns-config-dir = pkgs.writeTextDir "pdns.conf" '' + local-address=${concatStringsSep ", " cfg.listen-v4-addresses} + local-ipv6=${concatStringsSep ", " cfg.listen-v6-addresses} + local-port=${toString cfg.port} + launch= + include-dir=${runtime-dir} + ''; + + make-pgpass-file = user: target-file: let + db = cfg.database; + in pkgs.writeShellScript "genenrate-pgpass-file.sh" '' + touch ${target-file} + chown ${user} ${target-file} + chmod 700 ${target-file} + PASSWORD=$(cat ${db.password-file}) + echo "${db.host}:${toString db.port}:${db.database}:${db.user}:__PASSWORD__" | sed "s/__PASSWORD__/$PASSWORD/" > ${target-file} + ''; + + mkRecord = name: type: content: { + inherit name type content; + }; + + initialize-domain-sql = domain: let + domain-name = domain.domain; + host-ip = pkgs.lib.network.host-ipv4 config hostname; + ipv6-net = net: (builtins.match ":" net) != null; + ipv4-net = net: !(ipv6-net net); + domain-records = [ + (mkRecord domain-name "SOA" "ns1.${domain-name} hostmaster.${domain-name} ${toString config.instance.build-timestamp} 10800 3600 1209600 3600") + (mkRecord "_dmark.${domain-name}" "TXT" ''"v=DMARC1; p=reject; rua=mailto:${domain.admin}; ruf=mailto:${domain.admin}; fo=1;"'') + (mkRecord domain-name "NS" "ns1.${domain-name}") + (mkRecord domain-name "TXT" (let + networks = config.instance.local-networks; + v4-nets = map (net: "ip4:${net}") (filter ipv4-net networks); + v6-nets = map (net: "ip6:${net}") (filter ipv6-net networks); + networks-string = concatStringsSep " " (v4-nets ++ v6-nets); + in ''"v=spf1 mx ${networks-string} -all"'')) + (mkRecord "ns1.${domain-name}" "A" host-ip) + ] ++ + (optional (domain.gssapi-realm != null) + (mkRecord "_kerberos.${domain-name}" "TXT" ''"domain.gssapi-realm"'')) ++ + (mapAttrsToList (alias: target: mkRecord alias "CNAME" target) + domain.aliases); + records-strings = map (record: + "INSERT INTO records (domain_id, name, type, content) SELECT id, '${record.name}', '${record.type}', '${record.content}' FROM domains WHERE name='${domain-name}';") + domain-records; + in pkgs.writeText "initialize-${domain-name}.sql" '' + INSERT INTO domains (name, master, type, notified_serial) VALUES ('${domain-name}', '${host-ip}', 'MASTER', '${toString config.instance.build-timestamp}'); + ${concatStringsSep "\n" records-strings} + ''; + + initialize-domain-script = domain: let + domain-name = domain.domain; + in pkgs.writeShellScript "initialize-${domain-name}.sh" '' + if [ "$( psql -tAc "SELECT id FROM domains WHERE name='${domain-name}'" )" ]; then + logger "${domain-name} already initialized, skipping" + exit 0 + else + logger "initializing ${domain-name} in powerdns database" + psql -f ${initialize-domain-sql domain} + fi + ''; + + domainOpts = { name, ... }: { + options = with types; { + domain = mkOption { + type = str; + description = "Domain name."; + default = name; + }; + + admin = mkOption { + type = str; + description = "Administrator email."; + default = "admin@${name}"; + }; + + aliases = mkOption { + type = attrsOf str; + description = "Map of alias to authoritative hostname for this domain."; + default = {}; + }; + + gssapi-realm = mkOption { + type = nullOr str; + description = "GSSAPI realm of this domain."; + default = null; + }; + }; + }; + +in { + options.fudo.powerdns = with types; { + enable = mkEnableOption "Enable PowerDNS nameserver."; + + port = mkOption { + type = port; + description = "Port on which to listen for DNS requests."; + default = 53; + }; + + user = mkOption { + type = str; + description = "User as which to run PowerDNS server."; + default = "powerdns"; + }; + + group = mkOption { + type = str; + description = "Group as which to run PowerDNS server."; + default = "powerdns"; + }; + + listen-v4-addresses = mkOption { + type = listOf str; + description = "List of IPv4 addresses on which to listen."; + }; + + listen-v6-addresses = mkOption { + type = listOf str; + description = "List of IPv6 addresses on which to listen."; + default = []; + }; + + domains = mkOption { + type = attrsOf (submodule domainOpts); + description = "Domains to be served by this DNS server."; + }; + + enable-dnssec = mkOption { + type = bool; + description = "Enable DNSSEC for this domain."; + default = true; + }; + + database = { + host = mkOption { + type = str; + description = "Hostname or IP of the PostgreSQL server."; + }; + + port = mkOption { + type = port; + description = "Port of the PostgreSQL server."; + default = 5432; + }; + + database = mkOption { + type = str; + description = "Database to use for DNS backplane."; + default = "backplane_dns"; + }; + + user = mkOption { + type = str; + description = "Database user for DNS backplane."; + default = "backplane_dns"; + }; + + password-file = mkOption { + type = str; + description = "File containing password for database user."; + }; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + users = { + users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + groups.${cfg.group}.members = [ cfg.user ]; + }; + + fudo.system.services.powerdns-config-generator = { + description = "Generate PostgreSQL config for backplane DNS server."; + type = "oneshot"; + restartIfChanged = true; + readWritePaths = [ runtime-dir ]; + user = cfg.user; + execStart = let + script = pkgs.writeShellScript "generate-powerdns-config.sh" '' + TARGET=${target-gpgsql-config} + touch $TARGET + chown ${cfg.user}:${cfg.group} $TARGET + chmod 0700 $TARGET + PASSWORD=$( cat ${cfg.database.password-file} | tr -d '\n') + sed -e 's/__PASSWORD__/$PASSWORD/' ${gpgsql-template} > $TARGET + ''; + in "${script}"; + }; + + systemd = let + pgpass-file = "${runtime-dir}/pgpass"; + + initialize-jobs = mapAttrs' (_: domain: let + domain-name = domain.domain; + in nameValuePair "powerdns-initialize-${domain-name}" { + description = "Initialize the ${domain-name} domain"; + requires = [ "powerdns-initialize-db.service" "powerdns-generate-pgpass.service" ]; + after = [ "powerdns-initialize-db.service" "powerdns-generate-pgpass.service" ]; + requiredBy = [ "powerdns.service" ]; + wantedBy = [ "powerdns.service" ]; + before = [ "powerdns.service" ]; + environment = { + PGHOST = cfg.database.host; + PGUSER = cfg.database.user; + PGDATABASE = cfg.database.database; + PGPORT = toString cfg.database.port; + PGSSLMODE= "require"; + PGPASSFILE = pgpass-file; + }; + path = with pkgs; [ postgresql util-linux ]; + serviceConfig = { + ExecStart = initialize-domain-script domain; + }; + }) cfg.domains; + in { + tmpfiles.rules = [ + "d ${runtime-dir} 0750 ${cfg.user} ${cfg.group} - -" + ]; + + services = initialize-jobs // { + powerdns-generate-pgpass = { + description = "Create pgpass file required for database init."; + serviceConfig = { + ExecStart = make-pgpass-file cfg.user "${runtime-dir}/pgpass"; + }; + }; + + powerdns-initialize-db = { + description = "Initialize the powerdns database."; + requiredBy = [ "powerdns.service" ]; + before = [ "powerdns.service" ]; + requires = [ "powerdns-generate-pgpass.service" ]; + after = [ "powerdns-generate-pgpass.service" ]; + path = with pkgs; [ postgresql util-linux ]; + environment = { + PGHOST = cfg.database.host; + PGUSER = cfg.database.user; + PGDATABASE = cfg.database.database; + PGPORT = toString cfg.database.port; + PGSSLMODE= "require"; + PGPASSFILE = pgpass-file; + }; + serviceConfig = { + ExecStart = pkgs.writeShellScript "powerdns-initialize-db.sh" '' + if [ "$( psql -tAc "SELECT to_regclass('public.domains')" )" ]; then + logger "database initialized, skipping" + else + logger "initializing powerdns database" + psql -f ${pkgs.powerdns}/share/doc/pdns/schema.pgsql.sql + fi + ''; + }; + }; + + powerdns = { + description = "PowerDNS nameserver."; + requires = [ "powerdns-config-generator.service" ]; + after = [ + "network.target" + "powerdns-config-generator.service" + ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ powerdns postgresql util-linux ]; + serviceConfig = { + ExecStartPre = pkgs.writeShellScript "powerdns-init-config.sh" '' + TARGET=${target-gpgsql-config} + touch $TARGET + chown ${cfg.user}:${cfg.group} $TARGET + chmod 0700 $TARGET + PASSWORD=$( cat ${cfg.database.password-file} | tr -d '\n') + sed -e "s/__PASSWORD__/$PASSWORD/" ${gpgsql-template} > $TARGET + ''; + ExecStart = pkgs.writeShellScript "launch-powerdns.sh" (concatStringsSep " " [ + "${pkgs.powerdns}/bin/pdns_server" + "--setuid=${cfg.user}" + "--setgid=${cfg.group}" + "--chroot=${runtime-dir}" + "--daemon=no" + "--guardian=no" + "--disable-syslog" + "--write-pid=no" + "--config-dir=${pdns-config-dir}" + ]); + ExecStartPost = pkgs.writeShellScript "powerdns-secure-zones.sh" + (concatStringsSep "\n" + (mapAttrsToList + (_: domain: '' + DNSINFO=$(${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} show-zone ${domain.domain}) + if [[ "x$DNSINFO" =~ "xNo such zone in the database" ]]; then + logger "zone ${domain.domain} does not exist in powerdns database" + elif [[ "x$DNSINFO" =~ "xZone is not actively secured" ]]; then + logger "securing zone ${domain.domain} in powerdns database" + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} + elif [[ "x$DNSINFO" =~ "xNo keys for zone" ]]; then + logger "securing zone ${domain.domain} in powerdns database" + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} + else + logger "not securing zone ${domain.domain} in powerdns database" + fi + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} rectify-zone ${domain.domain} + '') + cfg.domains)); + }; + }; + }; + }; + }; +} diff --git a/lib/lib/text.nix b/lib/lib/text.nix new file mode 100644 index 0000000..321a374 --- /dev/null +++ b/lib/lib/text.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: + +with pkgs.lib; +let + get-basename = filename: + head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename)); + + format-json-file = filename: pkgs.stdenv.mkDerivation { + name = "formatted-${get-basename filename}"; + phases = [ "installPhase" ]; + buildInputs = with pkgs; [ python ]; + installPhase = "python -mjson.tool ${filename} > $out"; + }; + +in { + inherit format-json-file; +}