mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-11-10 09:43:30 +01:00
328 lines
11 KiB
Nix
328 lines
11 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.cloudflare-ddns;
|
|
|
|
boolToString = b: if b then "true" else "false";
|
|
formatList = l: lib.concatStringsSep "," l;
|
|
formatDuration = d: d.String;
|
|
in
|
|
{
|
|
options.services.cloudflare-ddns = {
|
|
enable = lib.mkEnableOption "Cloudflare Dynamic DNS service";
|
|
|
|
package = lib.mkPackageOption pkgs "cloudflare-ddns" { };
|
|
|
|
credentialsFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = ''
|
|
Path to a file containing the Cloudflare API authentication token.
|
|
The file content should be in the format `CLOUDFLARE_API_TOKEN=YOUR_SECRET_TOKEN`.
|
|
The service user `${cfg.user}` needs read access to this file.
|
|
Ensure permissions are secure (e.g., `0400` or `0440`) and ownership is appropriate
|
|
(e.g., `owner = root`, `group = ${cfg.group}`).
|
|
Using `CLOUDFLARE_API_TOKEN` is preferred over the deprecated `CF_API_TOKEN`.
|
|
'';
|
|
example = "/run/secrets/cloudflare-ddns-token";
|
|
};
|
|
|
|
domains = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = ''
|
|
List of domain names (FQDNs) to manage. Wildcards like `*.example.com` are supported.
|
|
These domains will be managed for both IPv4 and IPv6 unless overridden by
|
|
`ip4Domains` or `ip6Domains`, or if the respective providers are disabled.
|
|
This corresponds to the `DOMAINS` environment variable.
|
|
'';
|
|
example = [
|
|
"home.example.com"
|
|
"*.dynamic.example.org"
|
|
];
|
|
};
|
|
|
|
ip4Domains = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
description = ''
|
|
Explicit list of domains to manage only for IPv4. If set, overrides `domains` for IPv4.
|
|
Corresponds to the `IP4_DOMAINS` environment variable.
|
|
'';
|
|
example = [ "ipv4.example.com" ];
|
|
};
|
|
|
|
ip6Domains = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
description = ''
|
|
Explicit list of domains to manage only for IPv6. If set, overrides `domains` for IPv6.
|
|
Corresponds to the `IP6_DOMAINS` environment variable.
|
|
'';
|
|
example = [ "ipv6.example.com" ];
|
|
};
|
|
|
|
wafLists = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = ''
|
|
List of WAF IP Lists to manage, in the format `account-id/list-name`.
|
|
(Experimental feature as of cloudflare-ddns 1.14.0).
|
|
'';
|
|
example = [ "YOUR_ACCOUNT_ID/allowed_dynamic_ips" ];
|
|
};
|
|
|
|
provider = {
|
|
ipv4 = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "cloudflare.trace";
|
|
description = ''
|
|
IP detection provider for IPv4. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`.
|
|
Use `none` to disable IPv4 updates.
|
|
See cloudflare-ddns documentation for all options.
|
|
'';
|
|
};
|
|
ipv6 = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "cloudflare.trace";
|
|
description = ''
|
|
IP detection provider for IPv6. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`.
|
|
Use `none` to disable IPv6 updates.
|
|
See cloudflare-ddns documentation for all options.
|
|
'';
|
|
};
|
|
};
|
|
|
|
updateCron = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "@every 5m";
|
|
description = ''
|
|
Cron expression for how often to check and update IPs.
|
|
Use "@once" to run only once and then exit.
|
|
'';
|
|
example = "@hourly";
|
|
};
|
|
|
|
updateOnStart = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to perform an update check immediately on service start.";
|
|
};
|
|
|
|
deleteOnStop = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether to delete the managed DNS records and clear WAF lists when the service is stopped gracefully.
|
|
Warning: Setting this to true with `updateCron = "@once"` will cause immediate deletion.
|
|
'';
|
|
};
|
|
|
|
cacheExpiration = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "6h";
|
|
description = ''
|
|
Duration for which API responses (like Zone ID, Record IDs) are cached.
|
|
Uses Go's duration format (e.g., "6h", "1h30m").
|
|
'';
|
|
};
|
|
|
|
ttl = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 1;
|
|
description = ''
|
|
Time To Live (TTL) for the DNS records in seconds.
|
|
Must be 1 (for automatic) or between 30 and 86400.
|
|
'';
|
|
};
|
|
|
|
proxied = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "false";
|
|
description = ''
|
|
Whether the managed DNS records should be proxied through Cloudflare ('orange cloud').
|
|
Accepts boolean values (`true`, `false`) or a domain expression.
|
|
See cloudflare-ddns documentation for expression syntax (e.g., "is(a.com) || sub(b.org)").
|
|
'';
|
|
example = "true";
|
|
};
|
|
|
|
recordComment = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Comment to add to managed DNS records.";
|
|
};
|
|
|
|
wafListDescription = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Description for managed WAF lists (used when creating or verifying lists).";
|
|
};
|
|
|
|
detectionTimeout = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "5s";
|
|
description = "Timeout for detecting the public IP address.";
|
|
};
|
|
|
|
updateTimeout = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "30s";
|
|
description = "Timeout for updating records via the Cloudflare API.";
|
|
};
|
|
|
|
healthchecks = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "URL for Healthchecks.io monitoring endpoint (optional).";
|
|
example = "https://hc-ping.com/your-uuid";
|
|
};
|
|
|
|
uptimeKuma = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "URL for Uptime Kuma push monitor endpoint (optional).";
|
|
example = "https://status.example.com/api/push/tag?status=up&msg=OK&ping=";
|
|
};
|
|
|
|
shoutrrr = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
description = "List of Shoutrrr notification service URLs (optional).";
|
|
example = [
|
|
"discord://token@id"
|
|
"gotify://host/token"
|
|
];
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "cloudflare-ddns";
|
|
description = "User account under which the service runs.";
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "cloudflare-ddns";
|
|
description = "Group under which the service runs.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.ttl == 1 || (cfg.ttl >= 30 && cfg.ttl <= 86400);
|
|
message = "services.cloudflare-ddns.ttl must be 1 or between 30 and 86400";
|
|
}
|
|
{
|
|
assertion = cfg.updateCron == "@once" -> !cfg.deleteOnStop;
|
|
message = "services.cloudflare-ddns.deleteOnStop cannot be true when updateCron is \"@once\"";
|
|
}
|
|
{
|
|
assertion =
|
|
cfg.domains != [ ] || cfg.ip4Domains != null || cfg.ip6Domains != null || cfg.wafLists != [ ];
|
|
message = "services.cloudflare-ddns requires at least one domain (domains, ip4Domains, ip6Domains) or WAF list (wafLists) to be specified";
|
|
}
|
|
{
|
|
assertion = cfg.provider.ipv4 != "none" || cfg.provider.ipv6 != "none";
|
|
message = "services.cloudflare-ddns requires at least one provider (ipv4 or ipv6) to be enabled (not 'none')";
|
|
}
|
|
];
|
|
|
|
users.users.${cfg.user} = {
|
|
description = "Cloudflare DDNS service user";
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
home = "/var/lib/${cfg.user}";
|
|
};
|
|
|
|
users.groups.${cfg.group} = { };
|
|
|
|
systemd.tmpfiles.settings."cloudflare-ddns" = {
|
|
"/var/lib/${cfg.user}".d = {
|
|
mode = "0750";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
};
|
|
|
|
systemd.services.cloudflare-ddns = {
|
|
description = "Cloudflare Dynamic DNS Client Service (favonia)";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
|
|
serviceConfig = {
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
|
|
WorkingDirectory = "/var/lib/${cfg.user}";
|
|
|
|
EnvironmentFile = cfg.credentialsFile;
|
|
|
|
Environment =
|
|
let
|
|
toEnv = name: value: "${name}=\"${toString value}\"";
|
|
toEnvList = name: value: "${name}=\"${formatList value}\"";
|
|
toEnvDuration = name: value: "${name}=\"${formatDuration value}\"";
|
|
toEnvBool = name: value: "${name}=\"${boolToString value}\"";
|
|
toEnvMaybe =
|
|
pred: name: value:
|
|
lib.optionalString pred (toEnv name value);
|
|
toEnvMaybeList =
|
|
pred: name: value:
|
|
lib.optionalString pred (toEnvList name value);
|
|
in
|
|
lib.filter (envVar: envVar != "") [
|
|
(toEnvList "DOMAINS" cfg.domains)
|
|
(toEnvMaybeList (cfg.ip4Domains != null) "IP4_DOMAINS" cfg.ip4Domains)
|
|
(toEnvMaybeList (cfg.ip6Domains != null) "IP6_DOMAINS" cfg.ip6Domains)
|
|
|
|
(toEnv "IP4_PROVIDER" cfg.provider.ipv4)
|
|
(toEnv "IP6_PROVIDER" cfg.provider.ipv6)
|
|
|
|
(toEnvMaybeList (cfg.wafLists != [ ]) "WAF_LISTS" cfg.wafLists)
|
|
(toEnvMaybe (cfg.wafListDescription != "") "WAF_LIST_DESCRIPTION" cfg.wafListDescription)
|
|
|
|
(toEnv "UPDATE_CRON" cfg.updateCron)
|
|
(toEnvBool "UPDATE_ON_START" cfg.updateOnStart)
|
|
(toEnvBool "DELETE_ON_STOP" cfg.deleteOnStop)
|
|
(toEnv "CACHE_EXPIRATION" cfg.cacheExpiration)
|
|
|
|
(toEnv "TTL" cfg.ttl)
|
|
(toEnv "PROXIED" cfg.proxied)
|
|
(toEnvMaybe (cfg.recordComment != "") "RECORD_COMMENT" cfg.recordComment)
|
|
|
|
(toEnv "DETECTION_TIMEOUT" cfg.detectionTimeout)
|
|
(toEnv "UPDATE_TIMEOUT" cfg.updateTimeout)
|
|
|
|
(toEnvMaybe (cfg.healthchecks != null) "HEALTHCHECKS" cfg.healthchecks)
|
|
(toEnvMaybe (cfg.uptimeKuma != null) "UPTIMEKUMA" cfg.uptimeKuma)
|
|
(toEnvMaybeList (cfg.shoutrrr != null) "SHOUTRRR" (lib.concatStringsSep "\n" cfg.shoutrrr))
|
|
];
|
|
|
|
ExecStart = lib.getExe cfg.package;
|
|
|
|
Restart = "on-failure";
|
|
RestartSec = "30s";
|
|
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
NoNewPrivileges = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_INET"
|
|
"AF_INET6"
|
|
];
|
|
};
|
|
};
|
|
};
|
|
}
|