{ config, lib, pkgs, ... }: with lib; let cfg = config.services.inadyn; # check if a value of an attrset is not null or an empty collection nonEmptyValue = _: v: v != null && v != [ ] && v != { }; renderOption = k: v: if builtins.elem k [ "provider" "custom" ] then lib.concatStringsSep "\n" (mapAttrsToList (name: config: '' ${k} ${name} { ${lib.concatStringsSep "\n " (mapAttrsToList renderOption (filterAttrs nonEmptyValue config))} }'') v) else if k == "include" then "${k}(\"${v}\")" else if k == "hostname" && builtins.isList v then "${k} = { ${builtins.concatStringsSep ", " (map (s: "\"${s}\"") v)} }" else if builtins.isBool v then "${k} = ${boolToString v}" else if builtins.isString v then "${k} = \"${v}\"" else "${k} = ${toString v}"; configFile' = pkgs.writeText "inadyn.conf" '' # This file was generated by nix # do not edit ${(lib.concatStringsSep "\n" (mapAttrsToList renderOption (filterAttrs nonEmptyValue cfg.settings)))} ''; configFile = if (cfg.configFile != null) then cfg.configFile else configFile'; in { options.services.inadyn = with types; let providerOptions = { include = mkOption { default = null; description = "File to include additional settings for this provider from."; type = nullOr path; }; ssl = mkOption { default = true; description = "Whether to use HTTPS for this DDNS provider."; type = bool; }; username = mkOption { default = null; description = "Username for this DDNS provider."; type = nullOr str; }; password = mkOption { default = null; description = '' Password for this DDNS provider. WARNING: This will be world-readable in the nix store. To store credentials securely, use the `include` or `configFile` options. ''; type = nullOr str; }; hostname = mkOption { default = "*"; example = "your.cool-domain.com"; description = "Hostname alias(es)."; type = either str (listOf str); }; }; in { enable = mkEnableOption ('' synchronise your machine's IP address with a dynamic DNS provider using inadyn ''); user = mkOption { default = "inadyn"; type = types.str; description = '' User account under which inadyn runs. ::: {.note} If left as the default value this user will automatically be created on system activation, otherwise you are responsible for ensuring the user exists before the inadyn service starts. ::: ''; }; group = mkOption { default = "inadyn"; type = types.str; description = '' Group account under which inadyn runs. ::: {.note} If left as the default value this user will automatically be created on system activation, otherwise you are responsible for ensuring the user exists before the inadyn service starts. ::: ''; }; interval = mkOption { default = "*-*-* *:*:00"; description = '' How often to check the current IP. Uses the format described in {manpage}`systemd.time(7)`"; ''; type = str; }; logLevel = lib.mkOption { type = lib.types.enum [ "none" "err" "warning" "info" "notice" "debug" ]; default = "notice"; description = "Set inadyn's log level."; }; settings = mkOption { default = { }; description = "See `inadyn.conf (5)`"; type = submodule { freeformType = attrs; options = { allow-ipv6 = mkOption { default = config.networking.enableIPv6; defaultText = "`config.networking.enableIPv6`"; description = "Whether to get IPv6 addresses from interfaces."; type = bool; }; forced-update = mkOption { default = 2592000; description = "Duration (in seconds) after which an update is forced."; type = ints.positive; }; provider = mkOption { default = { }; description = '' Settings for DDNS providers built-in to inadyn. For a list of built-in providers, see `inadyn.conf (5)`. ''; type = attrsOf (submodule { freeformType = attrs; options = providerOptions; }); }; custom = mkOption { default = { }; description = '' Settings for custom DNS providers. ''; type = attrsOf (submodule { freeformType = attrs; options = providerOptions // { ddns-server = mkOption { description = "DDNS server name."; type = str; }; ddns-path = mkOption { description = '' DDNS server path. See `inadnyn.conf (5)` for a list for format specifiers that can be used. ''; example = "/update?user=%u&password=%p&domain=%h&myip=%i"; type = str; }; }; }); }; }; }; }; configFile = mkOption { default = null; description = '' Configuration file for inadyn. Setting this will override all other configuration options. Passed to the inadyn service using LoadCredential. ''; type = nullOr path; }; }; config = lib.mkIf cfg.enable { systemd = { services.inadyn = { description = "Update nameservers using inadyn"; documentation = [ "man:inadyn" "man:inadyn.conf" "file:${pkgs.inadyn}/share/doc/inadyn/README.md" ]; requires = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; startAt = cfg.interval; serviceConfig = { Type = "oneshot"; ExecStart = ''${lib.getExe pkgs.inadyn} -f ${configFile} --cache-dir ''${CACHE_DIRECTORY}/inadyn -1 --foreground -l ${cfg.logLevel}''; LoadCredential = "config:${configFile}"; CacheDirectory = "inadyn"; User = cfg.user; Group = cfg.group; UMask = "0177"; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictAddressFamilies = "AF_INET AF_INET6 AF_NETLINK"; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; PrivateUsers = true; ProtectSystem = "strict"; ProtectProc = "invisible"; ProtectHome = true; ProtectClock = true; ProtectControlGroups = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallErrorNumber = "EPERM"; SystemCallFilter = "@system-service"; CapabilityBoundingSet = ""; }; }; timers.inadyn.timerConfig.Persistent = true; }; users.users.inadyn = mkIf (cfg.user == "inadyn") { group = cfg.group; isSystemUser = true; }; users.groups = mkIf (cfg.group == "inadyn") { inadyn = { }; }; }; }