at master 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.cloudflare-ddns; 9 10 boolToString = b: if b then "true" else "false"; 11 formatList = l: lib.concatStringsSep "," l; 12 formatDuration = d: d.String; 13in 14{ 15 options.services.cloudflare-ddns = { 16 enable = lib.mkEnableOption "Cloudflare Dynamic DNS service"; 17 18 package = lib.mkPackageOption pkgs "cloudflare-ddns" { }; 19 20 credentialsFile = lib.mkOption { 21 type = lib.types.path; 22 description = '' 23 Path to a file containing the Cloudflare API authentication token. 24 The file content should be in the format `CLOUDFLARE_API_TOKEN=YOUR_SECRET_TOKEN`. 25 The service user `${cfg.user}` needs read access to this file. 26 Ensure permissions are secure (e.g., `0400` or `0440`) and ownership is appropriate 27 (e.g., `owner = root`, `group = ${cfg.group}`). 28 Using `CLOUDFLARE_API_TOKEN` is preferred over the deprecated `CF_API_TOKEN`. 29 ''; 30 example = "/run/secrets/cloudflare-ddns-token"; 31 }; 32 33 domains = lib.mkOption { 34 type = lib.types.listOf lib.types.str; 35 default = [ ]; 36 description = '' 37 List of domain names (FQDNs) to manage. Wildcards like `*.example.com` are supported. 38 These domains will be managed for both IPv4 and IPv6 unless overridden by 39 `ip4Domains` or `ip6Domains`, or if the respective providers are disabled. 40 This corresponds to the `DOMAINS` environment variable. 41 ''; 42 example = [ 43 "home.example.com" 44 "*.dynamic.example.org" 45 ]; 46 }; 47 48 ip4Domains = lib.mkOption { 49 type = lib.types.nullOr (lib.types.listOf lib.types.str); 50 default = null; 51 description = '' 52 Explicit list of domains to manage only for IPv4. If set, overrides `domains` for IPv4. 53 Corresponds to the `IP4_DOMAINS` environment variable. 54 ''; 55 example = [ "ipv4.example.com" ]; 56 }; 57 58 ip6Domains = lib.mkOption { 59 type = lib.types.nullOr (lib.types.listOf lib.types.str); 60 default = null; 61 description = '' 62 Explicit list of domains to manage only for IPv6. If set, overrides `domains` for IPv6. 63 Corresponds to the `IP6_DOMAINS` environment variable. 64 ''; 65 example = [ "ipv6.example.com" ]; 66 }; 67 68 wafLists = lib.mkOption { 69 type = lib.types.listOf lib.types.str; 70 default = [ ]; 71 description = '' 72 List of WAF IP Lists to manage, in the format `account-id/list-name`. 73 (Experimental feature as of cloudflare-ddns 1.14.0). 74 ''; 75 example = [ "YOUR_ACCOUNT_ID/allowed_dynamic_ips" ]; 76 }; 77 78 provider = { 79 ipv4 = lib.mkOption { 80 type = lib.types.str; 81 default = "cloudflare.trace"; 82 description = '' 83 IP detection provider for IPv4. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`. 84 Use `none` to disable IPv4 updates. 85 See cloudflare-ddns documentation for all options. 86 ''; 87 }; 88 ipv6 = lib.mkOption { 89 type = lib.types.str; 90 default = "cloudflare.trace"; 91 description = '' 92 IP detection provider for IPv6. Common values: `cloudflare.trace`, `cloudflare.doh`, `local`, `url:URL`, `none`. 93 Use `none` to disable IPv6 updates. 94 See cloudflare-ddns documentation for all options. 95 ''; 96 }; 97 }; 98 99 updateCron = lib.mkOption { 100 type = lib.types.str; 101 default = "@every 5m"; 102 description = '' 103 Cron expression for how often to check and update IPs. 104 Use "@once" to run only once and then exit. 105 ''; 106 example = "@hourly"; 107 }; 108 109 updateOnStart = lib.mkOption { 110 type = lib.types.bool; 111 default = true; 112 description = "Whether to perform an update check immediately on service start."; 113 }; 114 115 deleteOnStop = lib.mkOption { 116 type = lib.types.bool; 117 default = false; 118 description = '' 119 Whether to delete the managed DNS records and clear WAF lists when the service is stopped gracefully. 120 Warning: Setting this to true with `updateCron = "@once"` will cause immediate deletion. 121 ''; 122 }; 123 124 cacheExpiration = lib.mkOption { 125 type = lib.types.str; 126 default = "6h"; 127 description = '' 128 Duration for which API responses (like Zone ID, Record IDs) are cached. 129 Uses Go's duration format (e.g., "6h", "1h30m"). 130 ''; 131 }; 132 133 ttl = lib.mkOption { 134 type = lib.types.ints.positive; 135 default = 1; 136 description = '' 137 Time To Live (TTL) for the DNS records in seconds. 138 Must be 1 (for automatic) or between 30 and 86400. 139 ''; 140 }; 141 142 proxied = lib.mkOption { 143 type = lib.types.str; 144 default = "false"; 145 description = '' 146 Whether the managed DNS records should be proxied through Cloudflare ('orange cloud'). 147 Accepts boolean values (`true`, `false`) or a domain expression. 148 See cloudflare-ddns documentation for expression syntax (e.g., "is(a.com) || sub(b.org)"). 149 ''; 150 example = "true"; 151 }; 152 153 recordComment = lib.mkOption { 154 type = lib.types.str; 155 default = ""; 156 description = "Comment to add to managed DNS records."; 157 }; 158 159 wafListDescription = lib.mkOption { 160 type = lib.types.str; 161 default = ""; 162 description = "Description for managed WAF lists (used when creating or verifying lists)."; 163 }; 164 165 detectionTimeout = lib.mkOption { 166 type = lib.types.str; 167 default = "5s"; 168 description = "Timeout for detecting the public IP address."; 169 }; 170 171 updateTimeout = lib.mkOption { 172 type = lib.types.str; 173 default = "30s"; 174 description = "Timeout for updating records via the Cloudflare API."; 175 }; 176 177 healthchecks = lib.mkOption { 178 type = lib.types.nullOr lib.types.str; 179 default = null; 180 description = "URL for Healthchecks.io monitoring endpoint (optional)."; 181 example = "https://hc-ping.com/your-uuid"; 182 }; 183 184 uptimeKuma = lib.mkOption { 185 type = lib.types.nullOr lib.types.str; 186 default = null; 187 description = "URL for Uptime Kuma push monitor endpoint (optional)."; 188 example = "https://status.example.com/api/push/tag?status=up&msg=OK&ping="; 189 }; 190 191 shoutrrr = lib.mkOption { 192 type = lib.types.nullOr (lib.types.listOf lib.types.str); 193 default = null; 194 description = "List of Shoutrrr notification service URLs (optional)."; 195 example = [ 196 "discord://token@id" 197 "gotify://host/token" 198 ]; 199 }; 200 201 user = lib.mkOption { 202 type = lib.types.str; 203 default = "cloudflare-ddns"; 204 description = "User account under which the service runs."; 205 }; 206 207 group = lib.mkOption { 208 type = lib.types.str; 209 default = "cloudflare-ddns"; 210 description = "Group under which the service runs."; 211 }; 212 }; 213 214 config = lib.mkIf cfg.enable { 215 assertions = [ 216 { 217 assertion = cfg.ttl == 1 || (cfg.ttl >= 30 && cfg.ttl <= 86400); 218 message = "services.cloudflare-ddns.ttl must be 1 or between 30 and 86400"; 219 } 220 { 221 assertion = cfg.updateCron == "@once" -> !cfg.deleteOnStop; 222 message = "services.cloudflare-ddns.deleteOnStop cannot be true when updateCron is \"@once\""; 223 } 224 { 225 assertion = 226 cfg.domains != [ ] || cfg.ip4Domains != null || cfg.ip6Domains != null || cfg.wafLists != [ ]; 227 message = "services.cloudflare-ddns requires at least one domain (domains, ip4Domains, ip6Domains) or WAF list (wafLists) to be specified"; 228 } 229 { 230 assertion = cfg.provider.ipv4 != "none" || cfg.provider.ipv6 != "none"; 231 message = "services.cloudflare-ddns requires at least one provider (ipv4 or ipv6) to be enabled (not 'none')"; 232 } 233 ]; 234 235 users.users.${cfg.user} = { 236 description = "Cloudflare DDNS service user"; 237 isSystemUser = true; 238 group = cfg.group; 239 home = "/var/lib/${cfg.user}"; 240 }; 241 242 users.groups.${cfg.group} = { }; 243 244 systemd.tmpfiles.settings."cloudflare-ddns" = { 245 "/var/lib/${cfg.user}".d = { 246 mode = "0750"; 247 user = cfg.user; 248 group = cfg.group; 249 }; 250 }; 251 252 systemd.services.cloudflare-ddns = { 253 description = "Cloudflare Dynamic DNS Client Service (favonia)"; 254 wantedBy = [ "multi-user.target" ]; 255 after = [ "network-online.target" ]; 256 wants = [ "network-online.target" ]; 257 258 serviceConfig = { 259 User = cfg.user; 260 Group = cfg.group; 261 262 WorkingDirectory = "/var/lib/${cfg.user}"; 263 264 EnvironmentFile = cfg.credentialsFile; 265 266 Environment = 267 let 268 toEnv = name: value: "${name}=\"${toString value}\""; 269 toEnvList = name: value: "${name}=\"${formatList value}\""; 270 toEnvDuration = name: value: "${name}=\"${formatDuration value}\""; 271 toEnvBool = name: value: "${name}=\"${boolToString value}\""; 272 toEnvMaybe = 273 pred: name: value: 274 lib.optionalString pred (toEnv name value); 275 toEnvMaybeList = 276 pred: name: value: 277 lib.optionalString pred (toEnvList name value); 278 in 279 lib.filter (envVar: envVar != "") [ 280 (toEnvList "DOMAINS" cfg.domains) 281 (toEnvMaybeList (cfg.ip4Domains != null) "IP4_DOMAINS" cfg.ip4Domains) 282 (toEnvMaybeList (cfg.ip6Domains != null) "IP6_DOMAINS" cfg.ip6Domains) 283 284 (toEnv "IP4_PROVIDER" cfg.provider.ipv4) 285 (toEnv "IP6_PROVIDER" cfg.provider.ipv6) 286 287 (toEnvMaybeList (cfg.wafLists != [ ]) "WAF_LISTS" cfg.wafLists) 288 (toEnvMaybe (cfg.wafListDescription != "") "WAF_LIST_DESCRIPTION" cfg.wafListDescription) 289 290 (toEnv "UPDATE_CRON" cfg.updateCron) 291 (toEnvBool "UPDATE_ON_START" cfg.updateOnStart) 292 (toEnvBool "DELETE_ON_STOP" cfg.deleteOnStop) 293 (toEnv "CACHE_EXPIRATION" cfg.cacheExpiration) 294 295 (toEnv "TTL" cfg.ttl) 296 (toEnv "PROXIED" cfg.proxied) 297 (toEnvMaybe (cfg.recordComment != "") "RECORD_COMMENT" cfg.recordComment) 298 299 (toEnv "DETECTION_TIMEOUT" cfg.detectionTimeout) 300 (toEnv "UPDATE_TIMEOUT" cfg.updateTimeout) 301 302 (toEnvMaybe (cfg.healthchecks != null) "HEALTHCHECKS" cfg.healthchecks) 303 (toEnvMaybe (cfg.uptimeKuma != null) "UPTIMEKUMA" cfg.uptimeKuma) 304 (toEnvMaybeList (cfg.shoutrrr != null) "SHOUTRRR" (lib.concatStringsSep "\n" cfg.shoutrrr)) 305 ]; 306 307 ExecStart = lib.getExe cfg.package; 308 309 Restart = "on-failure"; 310 RestartSec = "30s"; 311 312 ProtectSystem = "strict"; 313 ProtectHome = true; 314 PrivateTmp = true; 315 PrivateDevices = true; 316 ProtectKernelTunables = true; 317 ProtectKernelModules = true; 318 ProtectControlGroups = true; 319 NoNewPrivileges = true; 320 RestrictAddressFamilies = [ 321 "AF_INET" 322 "AF_INET6" 323 ]; 324 }; 325 }; 326 }; 327}